├── .gitignore ├── .rubocop.yml ├── .rubocop_todo.yml ├── .travis.yml ├── Gemfile ├── History.markdown ├── LICENSE.txt ├── README.md ├── Rakefile ├── appveyor.yml ├── jekyll-compose.gemspec ├── lib ├── jekyll-compose.rb ├── jekyll-compose │ ├── arg_parser.rb │ ├── file_creator.rb │ ├── file_editor.rb │ ├── file_info.rb │ ├── file_mover.rb │ ├── movement_arg_parser.rb │ └── version.rb └── jekyll │ └── commands │ ├── compose.rb │ ├── draft.rb │ ├── page.rb │ ├── post.rb │ ├── publish.rb │ ├── rename.rb │ └── unpublish.rb ├── script ├── bootstrap ├── cibuild ├── fmt ├── release └── test └── spec ├── compose_spec.rb ├── draft_spec.rb ├── file_info_spec.rb ├── page_spec.rb ├── post_spec.rb ├── publish_spec.rb ├── rename_spec.rb ├── spec_helper.rb └── unpublish_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | spec/source 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | vendor/bundle 20 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: rubocop-jekyll 4 | 5 | inherit_gem: 6 | rubocop-jekyll: .rubocop.yml 7 | 8 | AllCops: 9 | TargetRubyVersion: 2.4 10 | Exclude: 11 | - vendor/**/* 12 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config --auto-gen-only-exclude` 3 | # on 2020-03-19 16:38:01 +0100 using RuboCop version 0.80.1. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 4 10 | # Cop supports --auto-correct. 11 | # Configuration parameters: AutoCorrect, Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 12 | # URISchemes: http, https 13 | Layout/LineLength: 14 | Exclude: 15 | - 'lib/jekyll/commands/compose.rb' 16 | - 'spec/publish_spec.rb' 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | rvm: 4 | - &latest_ruby 2.7 5 | - 2.5 6 | 7 | before_install: 8 | - gem update --system 9 | - gem install bundler 10 | 11 | before_script: bundle update 12 | script: script/cibuild 13 | 14 | env: 15 | matrix: 16 | - JEKYLL_VERSION="~> 3.9" 17 | matrix: 18 | include: 19 | - rvm: *latest_ruby 20 | env: JEKYLL_VERSION="~> 3.9" 21 | - rvm: *latest_ruby 22 | env: JEKYLL_VERSION="~> 4.2" 23 | 24 | notifications: 25 | email: false 26 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | gemspec 5 | 6 | gem "jekyll", ENV["JEKYLL_VERSION"] if ENV["JEKYLL_VERSION"] 7 | -------------------------------------------------------------------------------- /History.markdown: -------------------------------------------------------------------------------- 1 | ## 0.12.0 / 2019-12-28 2 | 3 | ### Minor Enhancements 4 | 5 | * `compose` command for collections (#103) 6 | * Add `rename` command to change titles and filenames (#101) 7 | * Support a custom timestamp format in YAML front matter on posts (#98) 8 | * Include VISUAL as an editor option (#102) 9 | * Prepare for collections support with default front matter configuration (#104) 10 | 11 | ### Bug Fixes 12 | 13 | * Removing source option for commands as it is appearing twice. (#100) 14 | 15 | ### Development Fixes 16 | 17 | * ci: test with Jekyll 4.0 (#99) 18 | * Extract logic for computing default front matter (#105) 19 | * Refactoring to remove some rubocop warnings and for consistency (#106) 20 | 21 | ## 0.11.0 / 2019-04-05 22 | 23 | ### Minor Enhancements 24 | 25 | * Add/Remove Dates When Publishing/Unpublishing (#92) 26 | 27 | ## 0.10.1 / 2019-03-23 28 | 29 | ### Bug Fixes 30 | 31 | * Re-introduce Ruby 2.3 support and test Jekyll 3.7+ (#90) 32 | 33 | ## 0.10.0 / 2019-03-21 34 | 35 | ### Development Fixes 36 | 37 | * Allow Jekyll v4 (still in alpha) 38 | * Drop support for Ruby 2.3 (#86) 39 | 40 | ## 0.9.0 / 2018-12-07 41 | 42 | ### Minor Enhancements 43 | 44 | * Allow additional front matter for Post (#41) 45 | * Add support for collections_dir configuration (#74) 46 | * Add some color to the success msg like jekyll new (#75) 47 | * Generate configuration from CLI options (#76) 48 | * Mirror `draft` command to current `post` command (#79) 49 | 50 | ### Bug Fixes 51 | 52 | * Display a warning when file exists (#81) 53 | * Warn on error and exit gracefully (#83) 54 | 55 | ### Development Fixes 56 | 57 | * Formatting of dates and times in a DRY manner (#60) 58 | * style: replace `puts` calls with `Jekyll.logger.info` (#69) 59 | * style: appease Rubocop (#71) 60 | * style: rubocop-jekyll 0.3 (#80) 61 | 62 | ## 0.8.0 / 2018-03-24 63 | 64 | ### Minor Enhancements 65 | 66 | * Auto open newly generated files in selected editor (#64) 67 | 68 | ## 0.7.0 / 2018-02-06 69 | 70 | ### Development Fixes 71 | 72 | * Add Rubocop autorrect offenses (#57) 73 | * Test against Ruby 2.5 (#56) 74 | 75 | ### Minor Enhancements 76 | 77 | * Check if a file should be overwritten when publishing or unpublishing a post (#59) 78 | 79 | ## 0.6.0 / 2017-11-14 80 | 81 | ### Development Fixes 82 | 83 | * Modernize Travis config (#53) 84 | * Define path with __dir__ (#51) 85 | * Inherit Jekyll's rubocop config for consistency (#52) 86 | * Execute FileInfo tests in source_dir, Fix tests (#46) 87 | 88 | ### Minor Enhancements 89 | 90 | * Add date to front matter when publish (#54) 91 | 92 | ## 0.5.0 / 2016-10-11 93 | 94 | * Allow Jekyll Source Directory (#42) 95 | * Ensure colons do not break titles (#39) 96 | * Require Jekyll 3 or higher (#40) 97 | 98 | ## 0.4.1 / 2015-12-30 99 | 100 | * Change Jekyll dependency to a runtime dependency to enforce v2.5.0 or greater 101 | 102 | ## 0.4.0 / 2015-12-30 103 | 104 | * Depend on jekyll at least of version 2.5.0 (#33) 105 | 106 | ## 0.3.0 / 2015-08-31 107 | 108 | * Add the `page` command (#15) 109 | * Add the `unpublish` command (#21) 110 | * Commands will create directories if necessary (#17, #23) 111 | * Display relative directories without ./ (#22) 112 | * Change `-t`, `--type` options to `-x`, `--extension` (#25) 113 | 114 | ## 0.2.1 / 2015-01-17 115 | 116 | * Create the `_drafts` dir if it's not already there (#11) 117 | * Update docs with usage examples (#10) 118 | 119 | ## 0.2.0 / 2015-01-10 120 | 121 | * Change the default file extension from `.markdown` to `.md` (#9) 122 | * The `publish` command should receive a path. (#7) 123 | * Rewrite the tests. (#3) 124 | 125 | ## 0.1.1 / 2014-12-29 126 | 127 | * Require the command files so it can be used via `:jekyll_plugins` (#5) 128 | 129 | ## 0.1.0 / 2014-12-29 130 | 131 | * Initial iteration of the `draft`, `post`, and `publish` commands. 132 | 133 | ## 0.0.0 / 2014-05-10 134 | 135 | * Birthday! 136 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-present Parker Moore and jekyll-compose contributors 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jekyll::Compose 2 | 3 | Streamline your writing in Jekyll with some commands. 4 | 5 | [![Linux Build Status](https://img.shields.io/travis/jekyll/jekyll-compose/master.svg?label=Linux%20build)][travis] 6 | [![Windows Build status](https://img.shields.io/appveyor/ci/jekyll/jekyll-compose/master.svg?label=Windows%20build)][appveyor] 7 | 8 | [travis]: https://travis-ci.org/jekyll/jekyll-compose 9 | [appveyor]: https://ci.appveyor.com/project/jekyll/jekyll-compose 10 | 11 | ## Installation 12 | 13 | Add this line to your application's Gemfile: 14 | 15 | gem 'jekyll-compose', group: [:jekyll_plugins] 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | ## Usage 22 | 23 | After you have installed (see above), run `bundle exec jekyll help` and you should see: 24 | 25 | Listed in help you will see new commands available to you: 26 | 27 | ```sh 28 | draft # Creates a new draft post with the given NAME 29 | post # Creates a new post with the given NAME 30 | publish # Moves a draft into the _posts directory and sets the date 31 | unpublish # Moves a post back into the _drafts directory 32 | page # Creates a new page with the given NAME 33 | rename # Moves a draft to a given NAME and sets the title 34 | compose # Creates a new file with the given NAME 35 | ``` 36 | 37 | Create your new page using: 38 | 39 | ```sh 40 | $ bundle exec jekyll page "My New Page" 41 | ``` 42 | 43 | Create your new post using: 44 | 45 | ```sh 46 | $ bundle exec jekyll post "My New Post" 47 | # or specify a custom format for the date attribute in the yaml front matter 48 | $ bundle exec jekyll post "My New Post" --timestamp-format "%Y-%m-%d %H:%M:%S %z" 49 | ``` 50 | 51 | ```sh 52 | # or by using the compose command 53 | $ bundle exec jekyll compose "My New Post" 54 | ``` 55 | 56 | ```sh 57 | # or by using the compose command with post specified 58 | $ bundle exec jekyll compose "My New Post" --post 59 | ``` 60 | 61 | ```sh 62 | # or by using the compose command with the posts collection specified 63 | $ bundle exec jekyll compose "My New Post" --collection "posts" 64 | ``` 65 | 66 | Create your new draft using: 67 | 68 | ```sh 69 | $ bundle exec jekyll draft "My new draft" 70 | ``` 71 | 72 | ```sh 73 | # or by using the compose command with draft specified 74 | $ bundle exec jekyll compose "My new draft" --draft 75 | ``` 76 | 77 | ```sh 78 | # or by using the compose command with the drafts collection specified 79 | $ bundle exec jekyll compose "My new draft" --collection "drafts" 80 | ``` 81 | 82 | Rename your draft using: 83 | 84 | ```sh 85 | $ bundle exec jekyll rename _drafts/my-new-draft.md "My Renamed Draft" 86 | ``` 87 | 88 | ```sh 89 | # or rename it back 90 | $ bundle exec jekyll rename _drafts/my-renamed-draft.md "My new draft" 91 | ``` 92 | 93 | Publish your draft using: 94 | 95 | ```sh 96 | $ bundle exec jekyll publish _drafts/my-new-draft.md 97 | ``` 98 | 99 | ```sh 100 | # or specify a specific date on which to publish it 101 | $ bundle exec jekyll publish _drafts/my-new-draft.md --date 2014-01-24 102 | # or specify a custom format for the date attribute in the yaml front matter 103 | $ bundle exec jekyll publish _drafts/my-new-draft.md --timestamp-format "%Y-%m-%d %H:%M:%S %z" 104 | ``` 105 | 106 | Rename your post using: 107 | 108 | ```sh 109 | $ bundle exec jekyll rename _posts/2014-01-24-my-new-draft.md "My New Post" 110 | ``` 111 | 112 | ```sh 113 | # or specify a specific date 114 | $ bundle exec jekyll rename _posts/2014-01-24-my-new-post.md "My Old Post" --date "2012-03-04" 115 | ``` 116 | 117 | ```sh 118 | # or specify the current date 119 | $ bundle exec jekyll rename _posts/2012-03-04-my-old-post.md "My New Post" --now 120 | ``` 121 | 122 | Unpublish your post using: 123 | 124 | ```sh 125 | $ bundle exec jekyll unpublish _posts/2014-01-24-my-new-draft.md 126 | ``` 127 | 128 | Create your new file in a collection using: 129 | 130 | ```sh 131 | $ bundle exec jekyll compose "My New Thing" --collection "things" 132 | ``` 133 | 134 | ## Configuration 135 | 136 | To customize the default plugin configuration edit the `jekyll_compose` section within your jekyll config file. 137 | 138 | ### auto-open new drafts or posts in your editor 139 | 140 | ```yaml 141 | jekyll_compose: 142 | auto_open: true 143 | ``` 144 | 145 | and make sure that you have `EDITOR`, `VISUAL` or `JEKYLL_EDITOR` environment variable set. 146 | For instance if you wish to open newly created Jekyll posts and drafts in Atom editor you can add the following line in your shell configuration: 147 | ```sh 148 | export JEKYLL_EDITOR=atom 149 | ``` 150 | 151 | `JEKYLL_EDITOR` will override default `EDITOR` or `VISUAL` value. 152 | `VISUAL` will override default `EDITOR` value. 153 | 154 | ### Set default front matter for drafts and posts 155 | 156 | If you wish to add default front matter to newly created posts or drafts, you can specify as many as you want under `default_front_matter` config keys, for instance: 157 | 158 | ```yaml 159 | jekyll_compose: 160 | default_front_matter: 161 | drafts: 162 | description: 163 | image: 164 | category: 165 | tags: 166 | posts: 167 | description: 168 | image: 169 | category: 170 | tags: 171 | published: false 172 | sitemap: false 173 | ``` 174 | 175 | This will also auto add: 176 | - The creation timestamp under the `date` attribute. 177 | - The title attribute under the `title` attribute 178 | 179 | 180 | For collections, you can add default front matter to newly created collection files using `default_front_matter` and the collection name as a config key, for instance for the collection `things`: 181 | 182 | ```yaml 183 | jekyll_compose: 184 | default_front_matter: 185 | things: 186 | description: 187 | image: 188 | category: 189 | tags: 190 | ``` 191 | 192 | ## Contributing 193 | 194 | 1. Fork it ( http://github.com/jekyll/jekyll-compose/fork ) 195 | 2. Create your feature branch (`git checkout -b my-new-feature`) 196 | 3. Run the specs and our linter (`script/cibuild`) 197 | 4. Commit your changes (`git commit -am 'Add some feature'`) 198 | 5. Push to the branch (`git push origin my-new-feature`) 199 | 6. Create new Pull Request 200 | 201 | ### Submitting a Pull Request based on an existing proposal 202 | 203 | When submitting a pull request that uses code from an unmerged pull request, please be aware of the following: 204 | * Changes proposed in the older pull request is still the original author's property. Moving forward from where they left it 205 | means that you're a `co-author`. 206 | * GitHub allows attributing 207 | [credit to multiple authors](https://help.github.com/en/articles/creating-a-commit-with-multiple-authors) 208 | However, pull requests in this project are automatically squashed and then merged onto the base branch. So, only authors and 209 | co-authors of the opening commit gets credit once the pull request gets merged. 210 | * If the original pull request contained multiple commits, you may squash them into a single commit but ensure that you list 211 | any additional authors (and yourselves) as co-authors of that commit. 212 | * Use appropriate [keywords](https://help.github.com/en/articles/closing-issues-using-keywords) in your pull request post to 213 | link to the existing pull request or issue-ticket so that they're automatically closed when your pull request gets merged. 214 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "{build}" 2 | clone_depth: 5 3 | branches: 4 | only: 5 | - master 6 | 7 | build: off 8 | 9 | environment: 10 | JEKYLL_VERSION: "~> 3.8" 11 | matrix: 12 | - RUBY_FOLDER_VER: "26" 13 | JEKYLL_VERSION : "~> 3.8.5" 14 | - RUBY_FOLDER_VER: "26" 15 | JEKYLL_VERSION : ">= 4.0.0" 16 | - RUBY_FOLDER_VER: "26" 17 | - RUBY_FOLDER_VER: "24" 18 | 19 | install: 20 | - SET PATH=C:\Ruby%RUBY_FOLDER_VER%-x64\bin;%PATH% 21 | - bundle install --retry 5 --jobs=%NUMBER_OF_PROCESSORS% --clean --path vendor\bundle 22 | 23 | test_script: 24 | - ruby --version 25 | - gem --version 26 | - bundler --version 27 | - bash ./script/test 28 | 29 | cache: 30 | # If one of the files after the right arrow changes, cache will be invalidated 31 | - 'vendor\bundle -> appveyor.yml,Gemfile,jekyll-compose.gemspec' 32 | -------------------------------------------------------------------------------- /jekyll-compose.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "lib/jekyll-compose/version" 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "jekyll-compose" 7 | spec.version = Jekyll::Compose::VERSION 8 | spec.authors = ["Parker Moore"] 9 | spec.email = ["parkrmoore@gmail.com"] 10 | spec.summary = "Streamline your writing in Jekyll with these commands." 11 | spec.description = "Streamline your writing in Jekyll with these commands." 12 | spec.homepage = "https://github.com/jekyll/jekyll-compose" 13 | spec.license = "MIT" 14 | 15 | spec.files = `git ls-files -z`.split("\x0").grep(%r!^lib/!) 16 | spec.require_paths = ["lib"] 17 | 18 | spec.required_ruby_version = ">= 2.4.0" 19 | 20 | spec.add_dependency "jekyll", ">= 3.7", "< 5.0" 21 | 22 | spec.add_development_dependency "bundler" 23 | spec.add_development_dependency "rake", "~> 12.0" 24 | spec.add_development_dependency "rspec", "~> 3.0" 25 | spec.add_development_dependency "rubocop-jekyll", "~> 0.5" 26 | end 27 | -------------------------------------------------------------------------------- /lib/jekyll-compose.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "jekyll-compose/version" 4 | require "jekyll-compose/arg_parser" 5 | require "jekyll-compose/movement_arg_parser" 6 | require "jekyll-compose/file_creator" 7 | require "jekyll-compose/file_mover" 8 | require "jekyll-compose/file_info" 9 | require "jekyll-compose/file_editor" 10 | 11 | module Jekyll 12 | module Compose 13 | DEFAULT_TYPE = "md" 14 | DEFAULT_LAYOUT = "post" 15 | DEFAULT_LAYOUT_PAGE = "page" 16 | DEFAULT_DATESTAMP_FORMAT = "%Y-%m-%d" 17 | DEFAULT_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M %z" 18 | end 19 | end 20 | 21 | %w(draft post publish unpublish page rename compose).each do |file| 22 | require File.expand_path("jekyll/commands/#{file}.rb", __dir__) 23 | end 24 | -------------------------------------------------------------------------------- /lib/jekyll-compose/arg_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Compose 5 | class ArgParser 6 | attr_reader :args, :options, :config 7 | 8 | # TODO: Remove `nil` parameter in v1.0 9 | def initialize(args, options, config = nil) 10 | @args = args 11 | @options = options 12 | @config = config || Jekyll.configuration(options) 13 | end 14 | 15 | def validate! 16 | raise ArgumentError, "You must specify a name." if args.empty? 17 | end 18 | 19 | def type 20 | options["extension"] || Jekyll::Compose::DEFAULT_TYPE 21 | end 22 | 23 | def layout 24 | options["layout"] || Jekyll::Compose::DEFAULT_LAYOUT 25 | end 26 | 27 | def title 28 | args.join " " 29 | end 30 | 31 | def force? 32 | !!options["force"] 33 | end 34 | 35 | def timestamp_format 36 | options["timestamp_format"] || Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT 37 | end 38 | 39 | def source 40 | File.join(config["source"], config["collections_dir"]) 41 | .gsub(%r!^#{Regexp.quote(Dir.pwd)}/*!, "") 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/jekyll-compose/file_creator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Compose 5 | class FileCreator 6 | attr_reader :file, :force, :root 7 | def initialize(file_info, force = false, root = nil) 8 | @file = file_info 9 | @force = force 10 | @root = root 11 | end 12 | 13 | def create! 14 | return unless create? 15 | 16 | ensure_directory_exists 17 | write_file 18 | end 19 | 20 | def file_path 21 | return file.path if root.nil? || root.empty? 22 | 23 | File.join(root, file.path) 24 | end 25 | 26 | private 27 | 28 | def create? 29 | return true if force 30 | return true unless File.exist?(file_path) 31 | 32 | Jekyll.logger.warn "A #{file.resource_type} already exists at #{file_path}" 33 | false 34 | end 35 | 36 | def ensure_directory_exists 37 | dir = File.dirname file_path 38 | FileUtils.mkdir_p(dir) unless Dir.exist?(dir) 39 | end 40 | 41 | def write_file 42 | File.open(file_path, "w") do |f| 43 | f.puts(file.content) 44 | end 45 | 46 | Jekyll.logger.info "New #{file.resource_type} created at #{file_path.cyan}" 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/jekyll-compose/file_editor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # 4 | # This class is aimed to open the created file in the selected editor. 5 | # To use this feature specify at Jekyll config: 6 | # 7 | # ``` 8 | # jekyll_compose: 9 | # auto_open: true 10 | # ``` 11 | # 12 | # And make sure, that you have JEKYLL_EDITOR, VISUAL, or EDITOR environment variables set up. 13 | # This will allow to open the file in your default editor automatically. 14 | 15 | module Jekyll 16 | module Compose 17 | class FileEditor 18 | class << self 19 | attr_reader :compose_config 20 | alias_method :jekyll_compose_config, :compose_config 21 | 22 | def bootstrap(config) 23 | @compose_config = config["jekyll_compose"] || {} 24 | end 25 | 26 | def open_editor(filepath) 27 | run_editor(post_editor, File.expand_path(filepath)) if post_editor 28 | end 29 | 30 | def run_editor(editor_name, filepath) 31 | system("#{editor_name} #{filepath}") 32 | end 33 | 34 | def post_editor 35 | return unless auto_open? 36 | 37 | ENV["JEKYLL_EDITOR"] || ENV["VISUAL"] || ENV["EDITOR"] 38 | end 39 | 40 | def auto_open? 41 | compose_config["auto_open"] 42 | end 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/jekyll-compose/file_info.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Compose 5 | class FileInfo 6 | attr_reader :params 7 | def initialize(params) 8 | @params = params 9 | end 10 | 11 | def file_name 12 | name = Jekyll::Utils.slugify params.title 13 | "#{name}.#{params.type}" 14 | end 15 | 16 | def content(custom_front_matter = {}) 17 | front_matter = YAML.dump({ 18 | "layout" => params.layout, 19 | "title" => params.title, 20 | }.merge(custom_front_matter)) 21 | 22 | front_matter + "---\n" 23 | end 24 | 25 | private 26 | 27 | def front_matter_defaults_for(key) 28 | params.config.dig("jekyll_compose", "default_front_matter", key) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/jekyll-compose/file_mover.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Compose 5 | class FileMover 6 | attr_reader :force, :movement, :root 7 | def initialize(movement, force = false, root = nil) 8 | @movement = movement 9 | @force = force 10 | @root = root 11 | end 12 | 13 | def resource_type_from 14 | "file" 15 | end 16 | 17 | def resource_type_to 18 | "file" 19 | end 20 | 21 | def move 22 | return unless valid_source? && valid_destination? 23 | 24 | ensure_directory_exists 25 | update_front_matter 26 | move_file 27 | end 28 | 29 | def validate_source 30 | unless File.exist? from 31 | raise ArgumentError, "There was no #{resource_type_from} found at '#{from}'." 32 | end 33 | end 34 | 35 | def ensure_directory_exists 36 | dir = File.dirname to 37 | Dir.mkdir(dir) unless Dir.exist?(dir) 38 | end 39 | 40 | def validate_should_write! 41 | if File.exist?(to) && !force 42 | raise ArgumentError, "A #{resource_type_to} already exists at #{to}" 43 | end 44 | end 45 | 46 | def move_file 47 | FileUtils.mv(from, to) 48 | Jekyll.logger.info "#{resource_type_from.capitalize} #{from} was moved to #{to}" 49 | end 50 | 51 | def update_front_matter 52 | content = File.read(from) 53 | if content =~ Jekyll::Document::YAML_FRONT_MATTER_REGEXP 54 | content = $POSTMATCH 55 | match = Regexp.last_match[1] if Regexp.last_match 56 | data = movement.front_matter(Psych.safe_load(match)) 57 | File.write(from, "#{Psych.dump(data)}---\n#{content}") 58 | end 59 | rescue Psych::SyntaxError => e 60 | Jekyll.logger.warn e 61 | rescue StandardError => e 62 | Jekyll.logger.warn e 63 | end 64 | 65 | private 66 | 67 | def valid_source? 68 | return true if File.exist?(from) 69 | 70 | invalidate_with "There was no #{resource_type_from} found at '#{from}'." 71 | end 72 | 73 | def valid_destination? 74 | return true if force 75 | return true unless File.exist?(to) 76 | 77 | invalidate_with "A #{resource_type_to} already exists at #{to}" 78 | end 79 | 80 | def invalidate_with(msg) 81 | Jekyll.logger.warn msg 82 | false 83 | end 84 | 85 | def from 86 | movement.from 87 | end 88 | 89 | def to 90 | file_path(movement.to) 91 | end 92 | 93 | def file_path(path) 94 | return path if root.nil? || root.empty? 95 | 96 | File.join(root, path) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/jekyll-compose/movement_arg_parser.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Compose 5 | class MovementArgParser < ArgParser 6 | def validate! 7 | raise ArgumentError, "You must specify a #{resource_type} path." if args.empty? 8 | end 9 | 10 | def path 11 | File.join(source, args.join(" ")).sub(%r!\A/!, "") 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/jekyll-compose/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Compose 5 | VERSION = "0.12.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/jekyll/commands/compose.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class ComposeCommand < Command 6 | def self.init_with_program(prog) 7 | prog.command(:compose) do |c| 8 | c.syntax "compose NAME" 9 | c.description "Creates a new document with the given NAME" 10 | 11 | options.each { |opt| c.option(*opt) } 12 | 13 | c.action { |args, options| process(args, options) } 14 | end 15 | end 16 | 17 | def self.options 18 | [ 19 | ["extension", "-x EXTENSION", "--extension EXTENSION", "Specify the file extension"], 20 | ["layout", "-l LAYOUT", "--layout LAYOUT", "Specify the document layout"], 21 | ["force", "-f", "--force", "Overwrite a document if it already exists"], 22 | ["date", "-d DATE", "--date DATE", "Specify the document date"], 23 | ["collection", "-c COLLECTION", "--collection COLLECTION", "Specify the document collection"], 24 | ["post", "--post", "Create a new post (default)"], 25 | ["draft", "--draft", "Create a new draft"], 26 | ["config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"], 27 | ] 28 | end 29 | 30 | def self.process(args = [], options = {}) 31 | config = configuration_from_options(options) 32 | params = ComposeCommandArgParser.new(args, options, config) 33 | params.validate! 34 | 35 | document = ComposeCommandFileInfo.new(params) 36 | 37 | file_creator = Compose::FileCreator.new(document, params.force?, params.source) 38 | file_creator.create! 39 | 40 | Compose::FileEditor.bootstrap(config) 41 | Compose::FileEditor.open_editor(file_creator.file_path) 42 | end 43 | 44 | class ComposeCommandArgParser < Compose::ArgParser 45 | def validate! 46 | if options.values_at("post", "draft", "collection").compact.length > 1 47 | raise ArgumentError, "You can only specify one of --post, --draft, or --collection COLLECTION." 48 | end 49 | 50 | super 51 | end 52 | 53 | def date 54 | @date ||= options["date"] ? Date.parse(options["date"]) : Time.now 55 | end 56 | 57 | def collection 58 | if (coll = options["collection"]) 59 | coll 60 | elsif options["draft"] 61 | "drafts" 62 | else 63 | "posts" 64 | end 65 | end 66 | end 67 | 68 | class ComposeCommandFileInfo < Compose::FileInfo 69 | def initialize(params) 70 | @params = params 71 | @collection = params.collection 72 | end 73 | 74 | def resource_type 75 | case @collection 76 | when "posts" then "post" 77 | when "drafts" then "draft" 78 | else 79 | "file" 80 | end 81 | end 82 | 83 | def path 84 | File.join("_#{@collection}", file_name) 85 | end 86 | 87 | def file_name 88 | @collection == "posts" ? "#{date_stamp}-#{super}" : super 89 | end 90 | 91 | def content(custom_front_matter = {}) 92 | default_front_matter = front_matter_defaults_for(@collection) 93 | custom_front_matter.merge!(default_front_matter) if default_front_matter.is_a?(Hash) 94 | 95 | super({ "date" => time_stamp }.merge!(custom_front_matter)) 96 | end 97 | 98 | private 99 | 100 | def date_stamp 101 | @params.date.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) 102 | end 103 | 104 | def time_stamp 105 | @params.date.strftime(Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT) 106 | end 107 | end 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/jekyll/commands/draft.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Draft < Command 6 | def self.init_with_program(prog) 7 | prog.command(:draft) do |c| 8 | c.syntax "draft NAME" 9 | c.description "Creates a new draft post with the given NAME" 10 | 11 | options.each { |opt| c.option(*opt) } 12 | 13 | c.action { |args, options| process(args, options) } 14 | end 15 | end 16 | 17 | def self.options 18 | [ 19 | ["extension", "-x EXTENSION", "--extension EXTENSION", "Specify the file extension"], 20 | ["layout", "-l LAYOUT", "--layout LAYOUT", "Specify the draft layout"], 21 | ["force", "-f", "--force", "Overwrite a draft if it already exists"], 22 | ["config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"], 23 | ] 24 | end 25 | 26 | def self.process(args = [], options = {}) 27 | config = configuration_from_options(options) 28 | params = Compose::ArgParser.new(args, options, config) 29 | params.validate! 30 | 31 | draft = DraftFileInfo.new(params) 32 | 33 | file_creator = Compose::FileCreator.new(draft, params.force?, params.source) 34 | file_creator.create! 35 | 36 | Compose::FileEditor.bootstrap(config) 37 | Compose::FileEditor.open_editor(file_creator.file_path) 38 | end 39 | 40 | class DraftFileInfo < Compose::FileInfo 41 | def resource_type 42 | "draft" 43 | end 44 | 45 | def path 46 | "_drafts/#{file_name}" 47 | end 48 | 49 | def content(custom_front_matter = {}) 50 | default_front_matter = front_matter_defaults_for("drafts") 51 | custom_front_matter.merge!(default_front_matter) if default_front_matter.is_a?(Hash) 52 | 53 | super(custom_front_matter) 54 | end 55 | end 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/jekyll/commands/page.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Page < Command 6 | def self.init_with_program(prog) 7 | prog.command(:page) do |c| 8 | c.syntax "page NAME" 9 | c.description "Creates a new page with the given NAME" 10 | 11 | options.each { |opt| c.option(*opt) } 12 | 13 | c.action { |args, options| process(args, options) } 14 | end 15 | end 16 | 17 | def self.options 18 | [ 19 | ["extension", "-x EXTENSION", "--extension EXTENSION", "Specify the file extension"], 20 | ["layout", "-l LAYOUT", "--layout LAYOUT", "Specify the page layout"], 21 | ["force", "-f", "--force", "Overwrite a page if it already exists"], 22 | ["config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"], 23 | ] 24 | end 25 | 26 | def self.process(args = [], options = {}) 27 | config = configuration_from_options(options) 28 | params = PageArgParser.new(args, options, config) 29 | params.validate! 30 | 31 | page = PageFileInfo.new(params) 32 | 33 | Compose::FileCreator.new(page, params.force?, params.source).create! 34 | end 35 | 36 | class PageArgParser < Compose::ArgParser 37 | def layout 38 | options["layout"] || Jekyll::Compose::DEFAULT_LAYOUT_PAGE 39 | end 40 | end 41 | 42 | class PageFileInfo < Compose::FileInfo 43 | def resource_type 44 | "page" 45 | end 46 | 47 | alias_method :path, :file_name 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/jekyll/commands/post.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Post < Command 6 | def self.init_with_program(prog) 7 | prog.command(:post) do |c| 8 | c.syntax "post NAME" 9 | c.description "Creates a new post with the given NAME" 10 | 11 | options.each { |opt| c.option(*opt) } 12 | 13 | c.action { |args, options| process(args, options) } 14 | end 15 | end 16 | 17 | def self.options 18 | [ 19 | ["extension", "-x EXTENSION", "--extension EXTENSION", "Specify the file extension"], 20 | ["layout", "-l LAYOUT", "--layout LAYOUT", "Specify the post layout"], 21 | ["force", "-f", "--force", "Overwrite a post if it already exists"], 22 | ["date", "-d DATE", "--date DATE", "Specify the post date"], 23 | ["config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"], 24 | ["timestamp_format", "--timestamp-format FORMAT", "Custom timestamp format"], 25 | ] 26 | end 27 | 28 | def self.process(args = [], options = {}) 29 | config = configuration_from_options(options) 30 | params = PostArgParser.new(args, options, config) 31 | params.validate! 32 | 33 | post = PostFileInfo.new(params) 34 | 35 | file_creator = Compose::FileCreator.new(post, params.force?, params.source) 36 | file_creator.create! 37 | 38 | Compose::FileEditor.bootstrap(config) 39 | Compose::FileEditor.open_editor(file_creator.file_path) 40 | end 41 | 42 | class PostArgParser < Compose::ArgParser 43 | def date 44 | @date ||= options["date"] ? Date.parse(options["date"]) : Time.now 45 | end 46 | end 47 | 48 | class PostFileInfo < Compose::FileInfo 49 | def resource_type 50 | "post" 51 | end 52 | 53 | def path 54 | "_posts/#{file_name}" 55 | end 56 | 57 | def file_name 58 | "#{_date_stamp}-#{super}" 59 | end 60 | 61 | def _date_stamp 62 | @params.date.strftime Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT 63 | end 64 | 65 | def _time_stamp 66 | @params.date.strftime @params.timestamp_format 67 | end 68 | 69 | def content(custom_front_matter = {}) 70 | default_front_matter = front_matter_defaults_for("posts") 71 | custom_front_matter.merge!(default_front_matter) if default_front_matter.is_a?(Hash) 72 | 73 | super({ "date" => _time_stamp }.merge(custom_front_matter)) 74 | end 75 | end 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/jekyll/commands/publish.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Publish < Command 6 | def self.init_with_program(prog) 7 | prog.command(:publish) do |c| 8 | c.syntax "publish DRAFT_PATH" 9 | c.description "Moves a draft into the _posts directory and sets the date" 10 | 11 | options.each { |opt| c.option(*opt) } 12 | 13 | c.action { |args, options| process(args, options) } 14 | end 15 | end 16 | 17 | def self.options 18 | [ 19 | ["date", "-d DATE", "--date DATE", "Specify the post date"], 20 | ["config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"], 21 | ["force", "-f", "--force", "Overwrite a post if it already exists"], 22 | ["timestamp_format", "--timestamp-format FORMAT", "Custom timestamp format"], 23 | ] 24 | end 25 | 26 | def self.process(args = [], options = {}) 27 | config = configuration_from_options(options) 28 | params = PublishArgParser.new args, options, config 29 | params.validate! 30 | 31 | movement = DraftMovementInfo.new params 32 | 33 | mover = DraftMover.new movement, params.force?, params.source 34 | mover.move 35 | end 36 | end 37 | 38 | class PublishArgParser < Compose::MovementArgParser 39 | def resource_type 40 | "draft" 41 | end 42 | 43 | def date 44 | @date ||= options["date"] ? Date.parse(options["date"]) : Time.now 45 | end 46 | 47 | def name 48 | File.basename path 49 | end 50 | end 51 | 52 | class DraftMovementInfo 53 | attr_reader :params 54 | def initialize(params) 55 | @params = params 56 | end 57 | 58 | def from 59 | params.path 60 | end 61 | 62 | def to 63 | date_stamp = params.date.strftime Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT 64 | "_posts/#{date_stamp}-#{params.name}" 65 | end 66 | 67 | def front_matter(data) 68 | data["date"] ||= params.date.strftime(params.timestamp_format) 69 | data 70 | end 71 | end 72 | 73 | class DraftMover < Compose::FileMover 74 | def resource_type_from 75 | "draft" 76 | end 77 | 78 | def resource_type_to 79 | "post" 80 | end 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/jekyll/commands/rename.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Rename < Command 6 | def self.init_with_program(prog) 7 | prog.command(:rename) do |c| 8 | c.syntax "rename PATH NAME" 9 | c.description "Moves a file to a given NAME and sets the title and date" 10 | 11 | options.each { |opt| c.option(*opt) } 12 | 13 | c.action { |args, options| process(args, options) } 14 | end 15 | end 16 | 17 | def self.options 18 | [ 19 | ["force", "-f", "--force", "Overwrite a post if it already exists"], 20 | ["config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"], 21 | ["date", "-d DATE", "--date DATE", "Specify the date"], 22 | ["now", "--now", "Specify the date as now"], 23 | ] 24 | end 25 | 26 | def self.process(args = [], options = {}) 27 | config = configuration_from_options(options) 28 | params = RenameArgParser.new(args, options, config) 29 | params.validate! 30 | 31 | movement = RenameMovementInfo.new(params) 32 | 33 | mover = RenameMover.new(movement, params.force?, params.source) 34 | mover.move 35 | end 36 | end 37 | 38 | class RenameArgParser < Compose::ArgParser 39 | def validate! 40 | raise ArgumentError, "You must specify current path and the new title." if args.length < 2 41 | 42 | if options.values_at("date", "now").compact.length > 1 43 | raise ArgumentError, "You can only specify one of --date DATE or --now." 44 | end 45 | end 46 | 47 | def current_path 48 | @current_path ||= args[0] 49 | end 50 | 51 | def path 52 | File.join(source, current_path).sub(%r!\A/!, "") 53 | end 54 | 55 | def dirname 56 | @dirname ||= File.dirname(current_path) 57 | end 58 | 59 | def basename 60 | @basename ||= File.basename(current_path) 61 | end 62 | 63 | def title 64 | @title ||= args.drop(1).join(" ") 65 | end 66 | 67 | def touch? 68 | !!options["date"] || options["now"] 69 | end 70 | 71 | def date 72 | @date ||= if options["now"] 73 | Time.now 74 | elsif options["date"] 75 | Date.parse(options["date"]) 76 | end 77 | end 78 | 79 | def date_from_filename 80 | Date.parse(Regexp.last_match(1)) if basename =~ Jekyll::Document::DATE_FILENAME_MATCHER 81 | end 82 | 83 | def post? 84 | dirname == "_posts" 85 | end 86 | 87 | def draft? 88 | dirname == "_drafts" 89 | end 90 | end 91 | 92 | class RenameMovementInfo < Compose::FileInfo 93 | attr_reader :params 94 | def initialize(params) 95 | @params = params 96 | end 97 | 98 | def from 99 | params.path 100 | end 101 | 102 | def resource_type 103 | if @params.post? 104 | "post" 105 | elsif @params.draft? 106 | "draft" 107 | else 108 | "file" 109 | end 110 | end 111 | 112 | def to 113 | if @params.post? 114 | File.join(@params.dirname, "#{date_stamp}-#{file_name}") 115 | else 116 | File.join(@params.dirname, file_name) 117 | end 118 | end 119 | 120 | def front_matter(data) 121 | data["title"] = params.title 122 | data["date"] = time_stamp if @params.touch? 123 | data 124 | end 125 | 126 | private 127 | 128 | def date_stamp 129 | if @params.touch? 130 | @params.date.strftime Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT 131 | else 132 | @params.date_from_filename.strftime Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT 133 | end 134 | end 135 | 136 | def time_stamp 137 | @params.date.strftime Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT 138 | end 139 | end 140 | 141 | class RenameMover < Compose::FileMover 142 | def resource_type_from 143 | @movement.resource_type 144 | end 145 | alias_method :resource_type_to, :resource_type_from 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/jekyll/commands/unpublish.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Jekyll 4 | module Commands 5 | class Unpublish < Command 6 | def self.init_with_program(prog) 7 | prog.command(:unpublish) do |c| 8 | c.syntax "unpublish POST_PATH" 9 | c.description "Moves a post back into the _drafts directory" 10 | 11 | options.each { |opt| c.option(*opt) } 12 | 13 | c.action { |args, options| process(args, options) } 14 | end 15 | end 16 | 17 | def self.options 18 | [ 19 | ["config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array, "Custom configuration file"], 20 | ["force", "-f", "--force", "Overwrite a draft if it already exists"], 21 | ] 22 | end 23 | 24 | def self.process(args = [], options = {}) 25 | config = configuration_from_options(options) 26 | params = UnpublishArgParser.new(args, options, config) 27 | params.validate! 28 | 29 | movement = PostMovementInfo.new(params) 30 | 31 | mover = PostMover.new(movement, params.force?, params.source) 32 | mover.move 33 | end 34 | end 35 | 36 | class UnpublishArgParser < Compose::MovementArgParser 37 | def resource_type 38 | "post" 39 | end 40 | 41 | def name 42 | File.basename(path).sub %r!\d{4}-\d{2}-\d{2}-!, "" 43 | end 44 | end 45 | 46 | class PostMovementInfo 47 | attr_reader :params 48 | def initialize(params) 49 | @params = params 50 | end 51 | 52 | def from 53 | params.path 54 | end 55 | 56 | def to 57 | "_drafts/#{params.name}" 58 | end 59 | 60 | def front_matter(data) 61 | data.reject { |key, _value| key == "date" } 62 | end 63 | end 64 | 65 | class PostMover < Compose::FileMover 66 | def resource_type_from 67 | "post" 68 | end 69 | 70 | def resource_type_to 71 | "draft" 72 | end 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | bundle install --jobs=8 --retry=3 3 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | script/test --format progress $@ 3 | script/fmt 4 | -------------------------------------------------------------------------------- /script/fmt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "Rubocop $(bundle exec rubocop --version)" 5 | bundle exec rubocop -D -E $@ 6 | success=$? 7 | if ((success != 0)); then 8 | echo -e "\nTry running \`script/fmt -a\` to automatically fix errors" 9 | fi 10 | exit $success 11 | -------------------------------------------------------------------------------- /script/release: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Tag and push a release. 3 | 4 | set -e 5 | 6 | script/cibuild 7 | bundle exec rake release 8 | -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | bundle exec rspec --color --require spec_helper $@ 3 | -------------------------------------------------------------------------------- /spec/compose_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Commands::ComposeCommand) do 4 | let(:datestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) } 5 | let(:timestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT) } 6 | let(:posts_dir) { Pathname.new source_dir("_posts") } 7 | let(:drafts_dir) { Pathname.new source_dir("_drafts") } 8 | let(:things_dir) { Pathname.new source_dir("_things") } 9 | 10 | before(:all) do 11 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 12 | Dir.chdir source_dir 13 | end 14 | 15 | context "posts" do 16 | let(:name) { "A test post" } 17 | let(:args) { [name] } 18 | let(:filename) { "#{datestamp}-a-test-post.md" } 19 | let(:path) { posts_dir.join(filename) } 20 | 21 | before(:each) do 22 | FileUtils.mkdir_p posts_dir unless File.directory? posts_dir 23 | allow(Jekyll::Compose::FileEditor).to receive(:system) 24 | end 25 | 26 | after(:each) do 27 | FileUtils.rm_r posts_dir if File.directory? posts_dir 28 | end 29 | 30 | it "creates a new post" do 31 | expect(path).not_to exist 32 | capture_stdout { described_class.process(args) } 33 | expect(path).to exist 34 | end 35 | 36 | it "creates a new post with --post option" do 37 | expect(path).not_to exist 38 | capture_stdout { described_class.process(args, "post" => true) } 39 | expect(path).to exist 40 | end 41 | 42 | it "creates a new post with --collection posts" do 43 | expect(path).not_to exist 44 | capture_stdout { described_class.process(args, "collection" => "posts") } 45 | expect(path).to exist 46 | end 47 | 48 | it "creates a post with a specified date" do 49 | path = posts_dir.join "2012-03-04-a-test-post.md" 50 | expect(path).not_to exist 51 | capture_stdout { described_class.process(args, "date" => "2012-3-4") } 52 | expect(path).to exist 53 | expect(File.read(path)).to match(%r!date: 2012-03-04 00:00 \+0000!) 54 | end 55 | 56 | it "creates the post with a specified extension" do 57 | html_path = posts_dir.join "#{datestamp}-a-test-post.html" 58 | expect(html_path).not_to exist 59 | capture_stdout { described_class.process(args, "extension" => "html") } 60 | expect(html_path).to exist 61 | end 62 | 63 | it "creates a new post with the specified layout" do 64 | capture_stdout { described_class.process(args, "layout" => "other-layout") } 65 | expect(File.read(path)).to match(%r!layout: other-layout!) 66 | end 67 | 68 | it "should write a helpful message when successful" do 69 | output = capture_stdout { described_class.process(args) } 70 | expect(output).to include("New post created at #{File.join("_posts", filename).cyan}") 71 | end 72 | 73 | it "errors with no arguments" do 74 | expect(lambda { 75 | capture_stdout { described_class.process } 76 | }).to raise_error("You must specify a name.") 77 | end 78 | 79 | it "creates the posts folder if necessary" do 80 | FileUtils.rm_r posts_dir if File.directory? posts_dir 81 | capture_stdout { described_class.process(args) } 82 | expect(posts_dir).to exist 83 | end 84 | 85 | context "when the post already exists" do 86 | let(:name) { "An existing post" } 87 | let(:filename) { "#{datestamp}-an-existing-post.md" } 88 | 89 | before(:each) do 90 | FileUtils.touch path 91 | end 92 | 93 | it "displays a warning and returns" do 94 | output = capture_stdout { described_class.process(args) } 95 | expect(output).to include("A post already exists at _posts/#{filename}") 96 | expect(File.read(path)).to_not match("layout: post") 97 | end 98 | 99 | it "overwrites if --force is given" do 100 | output = capture_stdout { described_class.process(args, "force" => true) } 101 | expect(output).to_not include("A post already exists at _posts/#{filename}") 102 | expect(File.read(path)).to match("layout: post") 103 | end 104 | end 105 | 106 | context "when a configuration file exists" do 107 | let(:config) { source_dir("_config.yml") } 108 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 109 | let(:config_data) do 110 | %( 111 | source: site 112 | ) 113 | end 114 | 115 | before(:each) do 116 | File.open(config, "w") do |f| 117 | f.write(config_data) 118 | end 119 | end 120 | 121 | after(:each) do 122 | FileUtils.rm(config) 123 | end 124 | 125 | it "should use source directory set by config" do 126 | expect(path).not_to exist 127 | capture_stdout { described_class.process(args) } 128 | expect(path).to exist 129 | end 130 | 131 | context "configuration is set" do 132 | let(:posts_dir) { Pathname.new source_dir("_posts") } 133 | let(:config_data) do 134 | %( 135 | jekyll_compose: 136 | auto_open: true 137 | default_front_matter: 138 | posts: 139 | description: my description 140 | category: 141 | ) 142 | end 143 | 144 | it "creates post with the specified config" do 145 | capture_stdout { described_class.process(args) } 146 | post = File.read(path) 147 | expect(post).to match(%r!description: my description!) 148 | expect(post).to match(%r!category: !) 149 | end 150 | 151 | context "env variable EDITOR is set up" do 152 | before { ENV["EDITOR"] = "nano" } 153 | 154 | it "opens post in default editor" do 155 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 156 | capture_stdout { described_class.process(args) } 157 | end 158 | 159 | context "env variable JEKYLL_EDITOR is set up" do 160 | before { ENV["JEKYLL_EDITOR"] = "nano" } 161 | 162 | it "opens post in jekyll editor" do 163 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 164 | capture_stdout { described_class.process(args) } 165 | end 166 | end 167 | end 168 | end 169 | 170 | context "and collections_dir is set" do 171 | let(:collections_dir) { "my_collections" } 172 | let(:posts_dir) { Pathname.new source_dir("site", collections_dir, "_posts") } 173 | let(:config_data) do 174 | %( 175 | source: site 176 | collections_dir: #{collections_dir} 177 | ) 178 | end 179 | 180 | it "should create posts at the correct location" do 181 | expect(path).not_to exist 182 | capture_stdout { described_class.process(args) } 183 | expect(path).to exist 184 | end 185 | 186 | it "should write a helpful message when successful" do 187 | output = capture_stdout { described_class.process(args) } 188 | generated_path = File.join("site", collections_dir, "_posts", filename).cyan 189 | expect(output).to include("New post created at #{generated_path}") 190 | end 191 | end 192 | end 193 | 194 | context "when source option is set" do 195 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 196 | 197 | it "should use source directory set by command line option" do 198 | expect(path).not_to exist 199 | capture_stdout { described_class.process(args, "source" => "site") } 200 | expect(path).to exist 201 | end 202 | end 203 | end 204 | context "drafts" do 205 | let(:name) { "A test draft" } 206 | let(:args) { [name] } 207 | let(:options) { { "draft" => true } } 208 | let(:path) { drafts_dir.join("a-test-draft.md") } 209 | 210 | before(:each) do 211 | FileUtils.mkdir_p drafts_dir unless File.directory? drafts_dir 212 | allow(Jekyll::Compose::FileEditor).to receive(:system) 213 | end 214 | 215 | after(:each) do 216 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 217 | end 218 | 219 | it "creates a new draft" do 220 | expect(path).not_to exist 221 | capture_stdout { described_class.process(args, options) } 222 | expect(path).to exist 223 | end 224 | 225 | it "creates a new draft with --draft option" do 226 | expect(path).not_to exist 227 | capture_stdout { described_class.process(args, "draft" => true) } 228 | expect(path).to exist 229 | end 230 | 231 | it "creates a new draft with --collection drafts" do 232 | expect(path).not_to exist 233 | capture_stdout { described_class.process(args, "collection" => "drafts") } 234 | expect(path).to exist 235 | end 236 | 237 | it "writes a helpful success message" do 238 | output = capture_stdout { described_class.process(args, options) } 239 | expect(output).to include("New draft created at #{"_drafts/a-test-draft.md".cyan}") 240 | end 241 | 242 | it "errors with no arguments" do 243 | expect(lambda { 244 | capture_stdout { described_class.process } 245 | }).to raise_error("You must specify a name.") 246 | end 247 | 248 | it "errors with no arguments with draft option" do 249 | expect(lambda { 250 | capture_stdout { described_class.process([], options) } 251 | }).to raise_error("You must specify a name.") 252 | end 253 | 254 | it "creates the drafts folder if necessary" do 255 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 256 | capture_stdout { described_class.process(args, options) } 257 | expect(drafts_dir).to exist 258 | end 259 | 260 | it "creates the draft with a specified extension" do 261 | html_path = drafts_dir.join "a-test-draft.html" 262 | expect(html_path).not_to exist 263 | capture_stdout { described_class.process(args, options.merge("extension" => "html")) } 264 | expect(html_path).to exist 265 | end 266 | 267 | it "creates a new draft with the specified layout" do 268 | capture_stdout { described_class.process(args, options.merge("layout" => "other-layout")) } 269 | expect(File.read(path)).to match(%r!layout: other-layout!) 270 | end 271 | 272 | context "when the draft already exists" do 273 | let(:name) { "An existing draft" } 274 | let(:path) { drafts_dir.join("an-existing-draft.md") } 275 | 276 | before(:each) do 277 | FileUtils.touch path 278 | end 279 | 280 | it "displays a warning and returns" do 281 | output = capture_stdout { described_class.process(args, options) } 282 | expect(output).to include("A draft already exists at _drafts/an-existing-draft.md") 283 | expect(File.read(path)).to_not match("layout: post") 284 | end 285 | 286 | it "overwrites if --force is given" do 287 | output = capture_stdout { described_class.process(args, options.merge("force" => true)) } 288 | expect(output).to_not include("A draft already exists at _drafts/an-existing-draft.md") 289 | expect(File.read(path)).to match("layout: post") 290 | end 291 | end 292 | 293 | context "when a configuration file exists" do 294 | let(:config) { source_dir("_config.yml") } 295 | let(:drafts_dir) { Pathname.new source_dir(File.join("site", "_drafts")) } 296 | let(:config_data) do 297 | %( 298 | source: site 299 | ) 300 | end 301 | 302 | before(:each) do 303 | File.open(config, "w") do |f| 304 | f.write(config_data) 305 | end 306 | end 307 | 308 | after(:each) do 309 | FileUtils.rm(config) 310 | end 311 | 312 | it "should use source directory set by config" do 313 | expect(path).not_to exist 314 | capture_stdout { described_class.process(args, options) } 315 | expect(path).to exist 316 | end 317 | 318 | context "configuration is set" do 319 | let(:drafts_dir) { Pathname.new source_dir("_drafts") } 320 | let(:config_data) do 321 | %( 322 | jekyll_compose: 323 | auto_open: true 324 | default_front_matter: 325 | drafts: 326 | description: my description 327 | category: 328 | ) 329 | end 330 | 331 | it "creates post with the specified config" do 332 | capture_stdout { described_class.process(args, options) } 333 | post = File.read(path) 334 | expect(post).to match(%r!description: my description!) 335 | expect(post).to match(%r!category: !) 336 | end 337 | 338 | context "env variable EDITOR is set up" do 339 | before { ENV["EDITOR"] = "nano" } 340 | 341 | it "opens post in default editor" do 342 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 343 | capture_stdout { described_class.process(args, options) } 344 | end 345 | 346 | context "env variable JEKYLL_EDITOR is set up" do 347 | before { ENV["JEKYLL_EDITOR"] = "nano" } 348 | 349 | it "opens post in jekyll editor" do 350 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 351 | capture_stdout { described_class.process(args, options) } 352 | end 353 | end 354 | end 355 | end 356 | 357 | context "and collections_dir is set" do 358 | let(:collections_dir) { "my_collections" } 359 | let(:drafts_dir) { Pathname.new source_dir("site", collections_dir, "_drafts") } 360 | let(:config_data) do 361 | %( 362 | source: site 363 | collections_dir: #{collections_dir} 364 | ) 365 | end 366 | 367 | it "should create drafts at the correct location" do 368 | expect(path).not_to exist 369 | capture_stdout { described_class.process(args, options) } 370 | expect(path).to exist 371 | end 372 | 373 | it "should write a helpful message when successful" do 374 | output = capture_stdout { described_class.process(args, options) } 375 | generated_path = File.join("site", collections_dir, "_drafts", "a-test-draft.md").cyan 376 | expect(output).to include("New draft created at #{generated_path}") 377 | end 378 | end 379 | end 380 | 381 | context "when source option is set" do 382 | let(:drafts_dir) { Pathname.new source_dir(File.join("site", "_drafts")) } 383 | 384 | it "should use source directory set by command line option" do 385 | expect(path).not_to exist 386 | capture_stdout { described_class.process(args, options.merge("source" => "site")) } 387 | expect(path).to exist 388 | end 389 | end 390 | end 391 | context "collections" do 392 | let(:name) { "A test thing" } 393 | let(:args) { [name] } 394 | let(:options) { { "collection" => "things" } } 395 | let(:path) { things_dir.join("a-test-thing.md") } 396 | 397 | before(:each) do 398 | FileUtils.mkdir_p things_dir unless File.directory? things_dir 399 | allow(Jekyll::Compose::FileEditor).to receive(:system) 400 | end 401 | 402 | after(:each) do 403 | FileUtils.rm_r things_dir if File.directory? things_dir 404 | end 405 | 406 | it "creates a new collection file" do 407 | expect(path).not_to exist 408 | capture_stdout { described_class.process(args, options) } 409 | expect(path).to exist 410 | end 411 | 412 | it "creates a new collection file with --collection things option" do 413 | expect(path).not_to exist 414 | capture_stdout { described_class.process(args, "collection" => "things") } 415 | expect(path).to exist 416 | end 417 | 418 | it "writes a helpful success message" do 419 | output = capture_stdout { described_class.process(args, options) } 420 | expect(output).to include("New file created at #{"_things/a-test-thing.md".cyan}") 421 | end 422 | 423 | it "errors with no arguments" do 424 | expect(lambda { 425 | capture_stdout { described_class.process } 426 | }).to raise_error("You must specify a name.") 427 | end 428 | 429 | it "errors with no arguments with collection option" do 430 | expect(lambda { 431 | capture_stdout { described_class.process([], options) } 432 | }).to raise_error("You must specify a name.") 433 | end 434 | 435 | it "creates the collection folder if necessary" do 436 | FileUtils.rm_r things_dir if File.directory? things_dir 437 | capture_stdout { described_class.process(args, options) } 438 | expect(things_dir).to exist 439 | end 440 | 441 | it "creates the collection file with a specified extension" do 442 | html_path = things_dir.join "a-test-thing.html" 443 | expect(html_path).not_to exist 444 | capture_stdout { described_class.process(args, options.merge("extension" => "html")) } 445 | expect(html_path).to exist 446 | end 447 | 448 | it "creates a new collection item with the specified layout" do 449 | capture_stdout { described_class.process(args, options.merge("layout" => "other-layout")) } 450 | expect(File.read(path)).to match(%r!layout: other-layout!) 451 | end 452 | 453 | context "when the collection file already exists" do 454 | let(:name) { "An existing thing" } 455 | let(:path) { things_dir.join("an-existing-thing.md") } 456 | 457 | before(:each) do 458 | FileUtils.touch path 459 | end 460 | 461 | it "displays a warning and returns" do 462 | output = capture_stdout { described_class.process(args, options) } 463 | expect(output).to include("A file already exists at _things/an-existing-thing.md") 464 | expect(File.read(path)).to_not match("layout: post") 465 | end 466 | 467 | it "overwrites if --force is given" do 468 | output = capture_stdout { described_class.process(args, options.merge("force" => true)) } 469 | expect(output).to_not include("A file already exists at _things/an-existing-thing.md") 470 | expect(File.read(path)).to match("layout: post") 471 | end 472 | end 473 | 474 | context "when a configuration file exists" do 475 | let(:config) { source_dir("_config.yml") } 476 | let(:things_dir) { Pathname.new source_dir(File.join("site", "_things")) } 477 | let(:config_data) do 478 | %( 479 | source: site 480 | ) 481 | end 482 | 483 | before(:each) do 484 | File.open(config, "w") do |f| 485 | f.write(config_data) 486 | end 487 | end 488 | 489 | after(:each) do 490 | FileUtils.rm(config) 491 | end 492 | 493 | it "should use source directory set by config" do 494 | expect(path).not_to exist 495 | capture_stdout { described_class.process(args, options) } 496 | expect(path).to exist 497 | end 498 | 499 | context "configuration is set" do 500 | let(:things_dir) { Pathname.new source_dir("_things") } 501 | let(:config_data) do 502 | %( 503 | jekyll_compose: 504 | auto_open: true 505 | default_front_matter: 506 | things: 507 | description: my description 508 | category: 509 | ) 510 | end 511 | 512 | it "creates collection item with the specified config" do 513 | capture_stdout { described_class.process(args, options) } 514 | post = File.read(path) 515 | expect(post).to match(%r!description: my description!) 516 | expect(post).to match(%r!category: !) 517 | end 518 | 519 | context "env variable EDITOR is set up" do 520 | before { ENV["EDITOR"] = "nano" } 521 | 522 | it "opens collection item in default editor" do 523 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 524 | capture_stdout { described_class.process(args, options) } 525 | end 526 | 527 | context "env variable JEKYLL_EDITOR is set up" do 528 | before { ENV["JEKYLL_EDITOR"] = "nano" } 529 | 530 | it "opens post in jekyll editor" do 531 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 532 | capture_stdout { described_class.process(args, options) } 533 | end 534 | end 535 | end 536 | end 537 | 538 | context "and collections_dir is set" do 539 | let(:collections_dir) { "my_collections" } 540 | let(:things_dir) { Pathname.new source_dir("site", collections_dir, "_things") } 541 | let(:config_data) do 542 | %( 543 | source: site 544 | collections_dir: #{collections_dir} 545 | ) 546 | end 547 | 548 | it "should create collection files at the correct location" do 549 | expect(path).not_to exist 550 | capture_stdout { described_class.process(args, options) } 551 | expect(path).to exist 552 | end 553 | 554 | it "should write a helpful message when successful" do 555 | output = capture_stdout { described_class.process(args, options) } 556 | generated_path = File.join("site", collections_dir, "_things", "a-test-thing.md").cyan 557 | expect(output).to include("New file created at #{generated_path}") 558 | end 559 | end 560 | end 561 | 562 | context "when source option is set" do 563 | let(:things_dir) { Pathname.new source_dir(File.join("site", "_things")) } 564 | 565 | it "should use source directory set by command line option" do 566 | expect(path).not_to exist 567 | capture_stdout { described_class.process(args, options.merge("source" => "site")) } 568 | expect(path).to exist 569 | end 570 | end 571 | end 572 | end 573 | -------------------------------------------------------------------------------- /spec/draft_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Commands::Draft) do 4 | let(:name) { "A test post" } 5 | let(:args) { [name] } 6 | let(:drafts_dir) { Pathname.new source_dir("_drafts") } 7 | let(:path) { drafts_dir.join("a-test-post.md") } 8 | 9 | before(:all) do 10 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 11 | Dir.chdir source_dir 12 | end 13 | 14 | before(:each) do 15 | FileUtils.mkdir_p drafts_dir unless File.directory? drafts_dir 16 | allow(Jekyll::Compose::FileEditor).to receive(:system) 17 | end 18 | 19 | after(:each) do 20 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 21 | end 22 | 23 | it "creates a new draft" do 24 | expect(path).not_to exist 25 | capture_stdout { described_class.process(args) } 26 | expect(path).to exist 27 | end 28 | 29 | it "writes a helpful success message" do 30 | output = capture_stdout { described_class.process(args) } 31 | expect(output).to include("New draft created at #{"_drafts/a-test-post.md".cyan}") 32 | end 33 | 34 | it "errors with no arguments" do 35 | expect(lambda { 36 | capture_stdout { described_class.process } 37 | }).to raise_error("You must specify a name.") 38 | end 39 | 40 | it "creates the drafts folder if necessary" do 41 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 42 | capture_stdout { described_class.process(args) } 43 | expect(drafts_dir).to exist 44 | end 45 | 46 | it "creates the draft with a specified extension" do 47 | html_path = drafts_dir.join "a-test-post.html" 48 | expect(html_path).not_to exist 49 | capture_stdout { described_class.process(args, "extension" => "html") } 50 | expect(html_path).to exist 51 | end 52 | 53 | it "creates a new draft with the specified layout" do 54 | capture_stdout { described_class.process(args, "layout" => "other-layout") } 55 | expect(File.read(path)).to match(%r!layout: other-layout!) 56 | end 57 | 58 | context "when the draft already exists" do 59 | let(:name) { "An existing draft" } 60 | let(:path) { drafts_dir.join("an-existing-draft.md") } 61 | 62 | before(:each) do 63 | FileUtils.touch path 64 | end 65 | 66 | it "displays a warning and returns" do 67 | output = capture_stdout { described_class.process(args) } 68 | expect(output).to include("A draft already exists at _drafts/an-existing-draft.md") 69 | expect(File.read(path)).to_not match("layout: post") 70 | end 71 | 72 | it "overwrites if --force is given" do 73 | output = capture_stdout { described_class.process(args, "force" => true) } 74 | expect(output).to_not include("A draft already exists at _drafts/an-existing-draft.md") 75 | expect(File.read(path)).to match("layout: post") 76 | end 77 | end 78 | 79 | context "when a configuration file exists" do 80 | let(:config) { source_dir("_config.yml") } 81 | let(:drafts_dir) { Pathname.new source_dir(File.join("site", "_drafts")) } 82 | let(:config_data) do 83 | %( 84 | source: site 85 | ) 86 | end 87 | 88 | before(:each) do 89 | File.open(config, "w") do |f| 90 | f.write(config_data) 91 | end 92 | end 93 | 94 | after(:each) do 95 | FileUtils.rm(config) 96 | end 97 | 98 | it "should use source directory set by config" do 99 | expect(path).not_to exist 100 | capture_stdout { described_class.process(args) } 101 | expect(path).to exist 102 | end 103 | 104 | context "configuration is set" do 105 | let(:drafts_dir) { Pathname.new source_dir("_drafts") } 106 | let(:config_data) do 107 | %( 108 | jekyll_compose: 109 | auto_open: true 110 | default_front_matter: 111 | drafts: 112 | description: my description 113 | category: 114 | ) 115 | end 116 | 117 | it "creates post with the specified config" do 118 | capture_stdout { described_class.process(args) } 119 | post = File.read(path) 120 | expect(post).to match(%r!description: my description!) 121 | expect(post).to match(%r!category: !) 122 | end 123 | 124 | context "env variable EDITOR is set up" do 125 | before { ENV["EDITOR"] = "nano" } 126 | 127 | it "opens post in default editor" do 128 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 129 | capture_stdout { described_class.process(args) } 130 | end 131 | 132 | context "env variable JEKYLL_EDITOR is set up" do 133 | before { ENV["JEKYLL_EDITOR"] = "nano" } 134 | 135 | it "opens post in jekyll editor" do 136 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 137 | capture_stdout { described_class.process(args) } 138 | end 139 | end 140 | end 141 | end 142 | 143 | context "and collections_dir is set" do 144 | let(:collections_dir) { "my_collections" } 145 | let(:drafts_dir) { Pathname.new source_dir("site", collections_dir, "_drafts") } 146 | let(:config_data) do 147 | %( 148 | source: site 149 | collections_dir: #{collections_dir} 150 | ) 151 | end 152 | 153 | it "should create drafts at the correct location" do 154 | expect(path).not_to exist 155 | capture_stdout { described_class.process(args) } 156 | expect(path).to exist 157 | end 158 | 159 | it "should write a helpful message when successful" do 160 | output = capture_stdout { described_class.process(args) } 161 | generated_path = File.join("site", collections_dir, "_drafts", "a-test-post.md").cyan 162 | expect(output).to include("New draft created at #{generated_path}") 163 | end 164 | end 165 | end 166 | 167 | context "when source option is set" do 168 | let(:drafts_dir) { Pathname.new source_dir(File.join("site", "_drafts")) } 169 | 170 | it "should use source directory set by command line option" do 171 | expect(path).not_to exist 172 | capture_stdout { described_class.process(args, "source" => "site") } 173 | expect(path).to exist 174 | end 175 | end 176 | end 177 | -------------------------------------------------------------------------------- /spec/file_info_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Compose::FileInfo) do 4 | before(:all) do 5 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 6 | Dir.chdir source_dir 7 | end 8 | 9 | describe "#content" do 10 | context "with a title of only words" do 11 | let(:expected_result) do 12 | <<~CONTENT 13 | --- 14 | layout: post 15 | title: A test arg parser 16 | --- 17 | CONTENT 18 | end 19 | 20 | let(:parsed_args) do 21 | Jekyll::Compose::ArgParser.new( 22 | ["A test arg parser"], 23 | {} 24 | ) 25 | end 26 | 27 | it "does not wrap the title in quotes" do 28 | file_info = described_class.new parsed_args 29 | expect(file_info.content).to eq(expected_result) 30 | end 31 | end 32 | 33 | context "with a title that includes a colon" do 34 | let(:expected_result) do 35 | <<~CONTENT 36 | --- 37 | layout: post 38 | title: 'A test: arg parser' 39 | --- 40 | CONTENT 41 | end 42 | 43 | let(:parsed_args) do 44 | Jekyll::Compose::ArgParser.new( 45 | ["A test: arg parser"], 46 | {} 47 | ) 48 | end 49 | 50 | it "does wrap the title in quotes" do 51 | file_info = described_class.new parsed_args 52 | expect(file_info.content).to eq(expected_result) 53 | end 54 | end 55 | 56 | context "with custom values" do 57 | let(:expected_result) do 58 | <<~CONTENT 59 | --- 60 | layout: post 61 | title: A test 62 | foo: bar 63 | --- 64 | CONTENT 65 | end 66 | 67 | let(:parsed_args) do 68 | Jekyll::Compose::ArgParser.new( 69 | ["A test arg parser"], 70 | {} 71 | ) 72 | end 73 | 74 | it "does not wrap the title in quotes" do 75 | file_info = described_class.new parsed_args 76 | expect(file_info.content("title" => "A test", "foo" => "bar")).to eq(expected_result) 77 | end 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/page_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Commands::Page) do 4 | let(:name) { "A test page" } 5 | let(:args) { [name] } 6 | let(:filename) { "a-test-page.md" } 7 | let(:path) { Pathname.new(source_dir).join(filename) } 8 | 9 | before(:all) do 10 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 11 | Dir.chdir source_dir 12 | end 13 | 14 | after(:each) do 15 | FileUtils.rm path if File.exist? path 16 | end 17 | 18 | it "creates a new page" do 19 | expect(path).not_to exist 20 | capture_stdout { described_class.process(args) } 21 | expect(path).to exist 22 | end 23 | 24 | it "creates a new page with the specified extension" do 25 | html_path = Pathname.new(source_dir).join "a-test-page.html" 26 | FileUtils.rm html_path if File.exist? html_path 27 | capture_stdout { described_class.process(args, "extension" => "html") } 28 | expect(html_path).to exist 29 | end 30 | 31 | it "creates a new page with the specified layout" do 32 | capture_stdout { described_class.process(args, "layout" => "other-layout") } 33 | expect(File.read(path)).to match(%r!layout: other-layout!) 34 | end 35 | 36 | it "should write a helpful message when successful" do 37 | output = capture_stdout { described_class.process(args) } 38 | expect(output).to include("New page created at #{filename.cyan}") 39 | end 40 | 41 | it "errors with no arguments" do 42 | expect(lambda { 43 | capture_stdout { described_class.process } 44 | }).to raise_error("You must specify a name.") 45 | end 46 | 47 | context "when the page already exists" do 48 | let(:name) { "An existing page" } 49 | let(:filename) { "an-existing-page.md" } 50 | 51 | before(:each) do 52 | FileUtils.touch path 53 | end 54 | 55 | it "displays a warning and returns" do 56 | output = capture_stdout { described_class.process(args) } 57 | expect(output).to include("A page already exists at #{filename}") 58 | expect(File.read(path)).to_not match("layout: page") 59 | end 60 | 61 | it "overwrites if --force is given" do 62 | output = capture_stdout { described_class.process(args, "force" => true) } 63 | expect(output).to_not include("A page already exists at #{filename}") 64 | expect(File.read(path)).to match("layout: page") 65 | end 66 | end 67 | 68 | context "when a configuration file exists" do 69 | let(:config) { source_dir("_config.yml") } 70 | let(:path) { Pathname.new(source_dir).join("site", filename) } 71 | 72 | before(:each) do 73 | File.open(config, "w") do |f| 74 | f.write(%( 75 | source: site 76 | )) 77 | end 78 | end 79 | 80 | after(:each) do 81 | FileUtils.rm(config) 82 | end 83 | 84 | it "should use source directory set by config" do 85 | expect(path).not_to exist 86 | capture_stdout { described_class.process(args) } 87 | expect(path).to exist 88 | end 89 | end 90 | 91 | context "when source option is set" do 92 | let(:path) { Pathname.new(source_dir).join("site", filename) } 93 | 94 | it "should use source directory set by command line option" do 95 | expect(path).not_to exist 96 | capture_stdout { described_class.process(args, "source" => "site") } 97 | expect(path).to exist 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/post_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Commands::Post) do 4 | let(:name) { "A test post" } 5 | let(:args) { [name] } 6 | let(:posts_dir) { Pathname.new source_dir("_posts") } 7 | let(:datestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) } 8 | let(:timestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT) } 9 | let(:filename) { "#{datestamp}-a-test-post.md" } 10 | let(:path) { posts_dir.join(filename) } 11 | 12 | before(:all) do 13 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 14 | Dir.chdir source_dir 15 | end 16 | 17 | before(:each) do 18 | FileUtils.mkdir_p posts_dir unless File.directory? posts_dir 19 | allow(Jekyll::Compose::FileEditor).to receive(:system) 20 | end 21 | 22 | after(:each) do 23 | FileUtils.rm_r posts_dir if File.directory? posts_dir 24 | end 25 | 26 | it "creates a new post" do 27 | expect(path).not_to exist 28 | capture_stdout { described_class.process(args) } 29 | expect(path).to exist 30 | end 31 | 32 | it "creates a post with a specified date" do 33 | path = posts_dir.join "2012-03-04-a-test-post.md" 34 | expect(path).not_to exist 35 | capture_stdout { described_class.process(args, "date" => "2012-3-4") } 36 | expect(path).to exist 37 | expect(File.read(path)).to match(%r!date: 2012-03-04 00:00 \+0000!) 38 | end 39 | 40 | it "creates the post with a specified extension" do 41 | html_path = posts_dir.join "#{datestamp}-a-test-post.html" 42 | expect(html_path).not_to exist 43 | capture_stdout { described_class.process(args, "extension" => "html") } 44 | expect(html_path).to exist 45 | end 46 | 47 | it "creates a new post with the specified layout" do 48 | capture_stdout { described_class.process(args, "layout" => "other-layout") } 49 | expect(File.read(path)).to match(%r!layout: other-layout!) 50 | end 51 | 52 | it "should write a helpful message when successful" do 53 | output = capture_stdout { described_class.process(args) } 54 | expect(output).to include("New post created at #{File.join("_posts", filename).cyan}") 55 | end 56 | 57 | it "errors with no arguments" do 58 | expect(lambda { 59 | capture_stdout { described_class.process } 60 | }).to raise_error("You must specify a name.") 61 | end 62 | 63 | it "creates the posts folder if necessary" do 64 | FileUtils.rm_r posts_dir if File.directory? posts_dir 65 | capture_stdout { described_class.process(args) } 66 | expect(posts_dir).to exist 67 | end 68 | 69 | context "when the post already exists" do 70 | let(:name) { "An existing post" } 71 | let(:filename) { "#{datestamp}-an-existing-post.md" } 72 | 73 | before(:each) do 74 | FileUtils.touch path 75 | end 76 | 77 | it "displays a warning and returns" do 78 | output = capture_stdout { described_class.process(args) } 79 | expect(output).to include("A post already exists at _posts/#{filename}") 80 | expect(File.read(path)).to_not match("layout: post") 81 | end 82 | 83 | it "overwrites if --force is given" do 84 | output = capture_stdout { described_class.process(args, "force" => true) } 85 | expect(output).to_not include("A post already exists at _posts/#{filename}") 86 | expect(File.read(path)).to match("layout: post") 87 | end 88 | end 89 | 90 | context "when a configuration file exists" do 91 | let(:config) { source_dir("_config.yml") } 92 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 93 | let(:config_data) do 94 | %( 95 | source: site 96 | ) 97 | end 98 | 99 | before(:each) do 100 | File.open(config, "w") do |f| 101 | f.write(config_data) 102 | end 103 | end 104 | 105 | after(:each) do 106 | FileUtils.rm(config) 107 | end 108 | 109 | it "should use source directory set by config" do 110 | expect(path).not_to exist 111 | capture_stdout { described_class.process(args) } 112 | expect(path).to exist 113 | end 114 | 115 | context "configuration is set" do 116 | let(:posts_dir) { Pathname.new source_dir("_posts") } 117 | let(:config_data) do 118 | %( 119 | jekyll_compose: 120 | auto_open: true 121 | default_front_matter: 122 | posts: 123 | description: my description 124 | category: 125 | ) 126 | end 127 | 128 | it "creates post with the specified config" do 129 | capture_stdout { described_class.process(args) } 130 | post = File.read(path) 131 | expect(post).to match(%r!description: my description!) 132 | expect(post).to match(%r!category: !) 133 | end 134 | 135 | context "env variable EDITOR is set up" do 136 | before { ENV["EDITOR"] = "nano" } 137 | 138 | it "opens post in default editor" do 139 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 140 | capture_stdout { described_class.process(args) } 141 | end 142 | 143 | context "env variable VISUAL is set up" do 144 | before { ENV["VISUAL"] = "nano" } 145 | 146 | it "opens post in jekyll editor" do 147 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 148 | capture_stdout { described_class.process(args) } 149 | end 150 | 151 | context "env variable JEKYLL_EDITOR is set up" do 152 | before { ENV["JEKYLL_EDITOR"] = "nano" } 153 | 154 | it "opens post in jekyll editor" do 155 | expect(Jekyll::Compose::FileEditor).to receive(:run_editor).with("nano", path.to_s) 156 | capture_stdout { described_class.process(args) } 157 | end 158 | end 159 | end 160 | end 161 | end 162 | 163 | context "and collections_dir is set" do 164 | let(:collections_dir) { "my_collections" } 165 | let(:posts_dir) { Pathname.new source_dir("site", collections_dir, "_posts") } 166 | let(:config_data) do 167 | %( 168 | source: site 169 | collections_dir: #{collections_dir} 170 | ) 171 | end 172 | 173 | it "should create posts at the correct location" do 174 | expect(path).not_to exist 175 | capture_stdout { described_class.process(args) } 176 | expect(path).to exist 177 | end 178 | 179 | it "should write a helpful message when successful" do 180 | output = capture_stdout { described_class.process(args) } 181 | generated_path = File.join("site", collections_dir, "_posts", filename).cyan 182 | expect(output).to include("New post created at #{generated_path}") 183 | end 184 | end 185 | end 186 | 187 | context "when source option is set" do 188 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 189 | 190 | it "should use source directory set by command line option" do 191 | expect(path).not_to exist 192 | capture_stdout { described_class.process(args, "source" => "site") } 193 | expect(path).to exist 194 | end 195 | end 196 | end 197 | -------------------------------------------------------------------------------- /spec/publish_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Commands::Publish) do 4 | let(:drafts_dir) { Pathname.new source_dir("_drafts") } 5 | let(:posts_dir) { Pathname.new source_dir("_posts") } 6 | let(:draft_to_publish) { "a-test-post.md" } 7 | let(:timestamp_format) { Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT } 8 | let(:date) { Time.now } 9 | let(:timestamp) { date.strftime(timestamp_format) } 10 | let(:datestamp) { date.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) } 11 | let(:post_filename) { "#{datestamp}-#{draft_to_publish}" } 12 | let(:args) { ["_drafts/#{draft_to_publish}"] } 13 | 14 | let(:draft_path) { drafts_dir.join draft_to_publish } 15 | let(:post_path) { posts_dir.join post_filename } 16 | 17 | before(:all) do 18 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 19 | Dir.chdir source_dir 20 | end 21 | 22 | before(:each) do 23 | FileUtils.mkdir_p drafts_dir unless File.directory? drafts_dir 24 | FileUtils.mkdir_p posts_dir unless File.directory? posts_dir 25 | File.write(draft_path, "---\nlayout: post\n---\n") 26 | end 27 | 28 | after(:each) do 29 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 30 | FileUtils.rm_r posts_dir if File.directory? posts_dir 31 | FileUtils.rm_r draft_path if File.file? draft_path 32 | FileUtils.rm_r post_path if File.file? post_path 33 | end 34 | 35 | it "publishes a draft post" do 36 | expect(post_path).not_to exist 37 | expect(draft_path).to exist 38 | capture_stdout { described_class.process(args) } 39 | expect(post_path).to exist 40 | expect(draft_path).not_to exist 41 | expect(File.read(post_path)).to include("date: #{timestamp}") 42 | end 43 | 44 | context "when date option is set" do 45 | let(:date) { Date.parse("2012-3-4") } 46 | 47 | it "publishes with a specified date" do 48 | expect(post_path).not_to exist 49 | expect(draft_path).to exist 50 | capture_stdout { described_class.process(args, "date"=>"2012-3-4") } 51 | expect(post_path).to exist 52 | expect(draft_path).not_to exist 53 | expect(File.read(post_path)).to include("date: 2012-03-04") 54 | end 55 | 56 | context "and timestamp format is set" do 57 | let(:timestamp_format) { "%Y-%m-%d %H:%M:%S" } 58 | 59 | it "published with a specified date in a given format" do 60 | expect(post_path).not_to exist 61 | expect(draft_path).to exist 62 | capture_stdout { described_class.process(args, "date" => "2012-3-4", "timestamp_format" => timestamp_format) } 63 | expect(post_path).to exist 64 | expect(draft_path).not_to exist 65 | expect(File.read(post_path)).to include("date: '2012-03-04 00:00:00'") 66 | end 67 | end 68 | end 69 | 70 | it "writes a helpful message on success" do 71 | expect(draft_path).to exist 72 | output = capture_stdout { described_class.process(args) } 73 | expect(output).to include("Draft _drafts/#{draft_to_publish} was moved to _posts/#{post_filename}") 74 | end 75 | 76 | it "publishes a draft on the specified date" do 77 | path = posts_dir.join "2012-03-04-a-test-post.md" 78 | capture_stdout { described_class.process(args, "date" => "2012-3-4") } 79 | expect(path).to exist 80 | expect(draft_path).not_to exist 81 | expect(File.read(path)).to include("date: 2012-03-04") 82 | end 83 | 84 | it "creates the posts folder if necessary" do 85 | FileUtils.rm_r posts_dir if File.directory? posts_dir 86 | capture_stdout { described_class.process(args) } 87 | expect(posts_dir).to exist 88 | end 89 | 90 | it "errors if there is no argument" do 91 | expect(lambda { 92 | capture_stdout { described_class.process } 93 | }).to raise_error("You must specify a draft path.") 94 | end 95 | 96 | it "outputs a warning and returns if no file exists at given path" do 97 | weird_path = "_drafts/i-do-not-exist.markdown" 98 | output = capture_stdout { described_class.process [weird_path] } 99 | expect(output).to include("There was no draft found at '_drafts/i-do-not-exist.markdown'.") 100 | expect(draft_path).to exist 101 | expect(post_path).to_not exist 102 | end 103 | 104 | context "when the post already exists" do 105 | let(:args) { ["_drafts/#{draft_to_publish}"] } 106 | 107 | before(:each) do 108 | FileUtils.touch post_path 109 | end 110 | 111 | it "outputs a warning and returns" do 112 | output = capture_stdout { described_class.process(args) } 113 | expect(output).to include("A post already exists at _posts/#{post_filename}") 114 | expect(draft_path).to exist 115 | expect(post_path).to exist 116 | end 117 | 118 | it "overwrites if --force is given" do 119 | output = capture_stdout { described_class.process(args, "force" => true) } 120 | expect(output).to_not include("A post already exists at _posts/#{post_filename}") 121 | expect(draft_path).not_to exist 122 | expect(post_path).to exist 123 | expect(File.read(post_path)).to include("date: #{timestamp}") 124 | end 125 | end 126 | 127 | context "when a configuration file exists" do 128 | let(:config) { source_dir("_config.yml") } 129 | let(:drafts_dir) { Pathname.new source_dir("site", "_drafts") } 130 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 131 | let(:config_data) do 132 | %( 133 | source: site 134 | ) 135 | end 136 | 137 | before(:each) do 138 | File.open(config, "w") do |f| 139 | f.write(config_data) 140 | end 141 | end 142 | 143 | after(:each) do 144 | FileUtils.rm(config) 145 | end 146 | 147 | it "should use source directory set by config" do 148 | expect(post_path).not_to exist 149 | expect(draft_path).to exist 150 | capture_stdout { described_class.process(args) } 151 | expect(post_path).to exist 152 | expect(draft_path).not_to exist 153 | end 154 | end 155 | 156 | context "when source option is set" do 157 | let(:drafts_dir) { Pathname.new source_dir("site", "_drafts") } 158 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 159 | 160 | it "should use source directory set by command line option" do 161 | expect(post_path).not_to exist 162 | expect(draft_path).to exist 163 | capture_stdout { described_class.process(args, "source" => "site") } 164 | expect(post_path).to exist 165 | expect(draft_path).not_to exist 166 | end 167 | end 168 | 169 | context "when timestamp format option is set" do 170 | context "to a custom value" do 171 | let(:timestamp_format) { "%Y-%m-%d %H:%M:%S" } 172 | 173 | it "should use timestamp format set by command line option" do 174 | expect(post_path).not_to exist 175 | expect(draft_path).to exist 176 | capture_stdout { described_class.process(args, "timestamp_format" => timestamp_format) } 177 | expect(post_path).to exist 178 | expect(draft_path).not_to exist 179 | expect(File.read(post_path)).to include("date: '#{timestamp}'") 180 | end 181 | end 182 | context "to the default value" do 183 | let(:timestamp_format) { Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT } 184 | 185 | it "should use timestamp format set by command line option" do 186 | expect(post_path).not_to exist 187 | expect(draft_path).to exist 188 | capture_stdout { described_class.process(args, "timestamp_format" => timestamp_format) } 189 | expect(post_path).to exist 190 | expect(draft_path).not_to exist 191 | expect(File.read(post_path)).to include("date: #{timestamp}") 192 | end 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /spec/rename_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Commands::Rename) do 4 | let(:datestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) } 5 | let(:timestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT) } 6 | 7 | context "drafts" do 8 | let(:drafts_dir) { Pathname.new source_dir("_drafts") } 9 | let(:old_name) { "Old Draft" } 10 | let(:new_name) { "New Draft" } 11 | let(:old_filename) { "old-draft.md" } 12 | let(:new_filename) { "new-draft.md" } 13 | let(:old_path) { drafts_dir.join(old_filename) } 14 | let(:new_path) { drafts_dir.join(new_filename) } 15 | let(:old_arg) { File.join("_drafts", old_filename) } 16 | let(:new_arg) { File.join("_drafts", new_filename) } 17 | let(:args) { [old_arg, new_name] } 18 | 19 | before(:all) do 20 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 21 | Dir.chdir source_dir 22 | end 23 | 24 | before(:each) do 25 | FileUtils.mkdir_p drafts_dir unless File.directory? drafts_dir 26 | File.write(old_path, "---\nlayout: post\ntitle: Old Draft\n---\n") 27 | end 28 | 29 | after(:each) do 30 | FileUtils.rm_r old_path if File.file? old_path 31 | FileUtils.rm_r new_path if File.file? new_path 32 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 33 | end 34 | 35 | it "moves a draft" do 36 | expect(old_path).to exist 37 | expect(new_path).not_to exist 38 | capture_stdout { described_class.process(args) } 39 | expect(old_path).not_to exist 40 | expect(new_path).to exist 41 | end 42 | 43 | it "changes the title of the draft" do 44 | expect(old_path).to exist 45 | expect(new_path).not_to exist 46 | expect(File.read(old_path)).to match(%r!title: Old Draft!) 47 | capture_stdout { described_class.process(args) } 48 | expect(old_path).not_to exist 49 | expect(new_path).to exist 50 | expect(File.read(new_path)).to match(%r!title: New Draft!) 51 | end 52 | 53 | it "should write a helpful message when successful" do 54 | output = capture_stdout { described_class.process(args) } 55 | expect(output).to include("Draft #{old_arg} was moved to #{new_arg}") 56 | end 57 | 58 | it "errors with no arguments" do 59 | expect(lambda { 60 | capture_stdout { described_class.process } 61 | }).to raise_error("You must specify current path and the new title.") 62 | end 63 | 64 | it "errors with only one argument" do 65 | expect(lambda { 66 | capture_stdout { described_class.process([old_path]) } 67 | }).to raise_error("You must specify current path and the new title.") 68 | end 69 | 70 | context "when the draft with the new filename already exists" do 71 | before(:each) do 72 | FileUtils.touch new_path 73 | end 74 | 75 | it "displays a warning and returns" do 76 | output = capture_stdout { described_class.process(args) } 77 | expect(output).to include("A draft already exists at #{new_arg}") 78 | expect(output).to_not include("Draft #{old_arg} was moved to #{new_arg}") 79 | expect(File.read(new_path)).to_not include("layout: post") 80 | end 81 | 82 | it "overwrites if --force is given" do 83 | output = capture_stdout { described_class.process(args, "force" => true) } 84 | expect(output).to_not include("A draft already exists at #{new_arg}") 85 | expect(output).to include("Draft #{old_arg} was moved to #{new_arg}") 86 | expect(File.read(new_path)).to include("layout: post") 87 | end 88 | end 89 | 90 | context "when a configuration file exists" do 91 | let(:config) { source_dir("_config.yml") } 92 | let(:drafts_dir) { Pathname.new source_dir("site", "_drafts") } 93 | let(:config_data) do 94 | %( 95 | source: site 96 | ) 97 | end 98 | 99 | before(:each) do 100 | File.open(config, "w") do |f| 101 | f.write(config_data) 102 | end 103 | end 104 | 105 | after(:each) do 106 | FileUtils.rm(config) 107 | end 108 | 109 | it "should use source directory set by config" do 110 | expect(new_path).not_to exist 111 | capture_stdout { described_class.process(args) } 112 | expect(new_path).to exist 113 | end 114 | 115 | context "and collections_dir is set" do 116 | let(:collections_dir) { "my_collections" } 117 | let(:drafts_dir) { Pathname.new source_dir("site", collections_dir, "_drafts") } 118 | let(:config_data) do 119 | %( 120 | source: site 121 | collections_dir: #{collections_dir} 122 | ) 123 | end 124 | 125 | it "should create drafts at the correct location" do 126 | expect(drafts_dir).to exist 127 | expect(old_path).to exist 128 | expect(new_path).not_to exist 129 | capture_stdout { described_class.process(args) } 130 | expect(drafts_dir).to exist 131 | expect(old_path).not_to exist 132 | expect(new_path).to exist 133 | end 134 | 135 | it "should write a helpful message when successful" do 136 | output = capture_stdout { described_class.process(args) } 137 | old_arg = File.join("site", collections_dir, "_drafts", old_filename) 138 | new_arg = File.join("site", collections_dir, "_drafts", new_filename) 139 | expect(output).to include("Draft #{old_arg} was moved to #{new_arg}") 140 | end 141 | end 142 | end 143 | 144 | context "when source option is set" do 145 | let(:drafts_dir) { Pathname.new source_dir("site", "_drafts") } 146 | 147 | it "should use source directory set by command line option" do 148 | expect(old_path).to exist 149 | expect(new_path).not_to exist 150 | capture_stdout { described_class.process(args, "source" => "site") } 151 | expect(old_path).not_to exist 152 | expect(new_path).to exist 153 | end 154 | end 155 | end 156 | context "posts" do 157 | let(:posts_dir) { Pathname.new source_dir("_posts") } 158 | let(:old_name) { "Old Post" } 159 | let(:new_name) { "New Post" } 160 | let(:old_basename) { "old-post.md" } 161 | let(:new_basename) { "new-post.md" } 162 | let(:old_filename) { "#{datestamp}-#{old_basename}" } 163 | let(:new_filename) { "#{datestamp}-#{new_basename}" } 164 | let(:old_path) { posts_dir.join(old_filename) } 165 | let(:new_path) { posts_dir.join(new_filename) } 166 | let(:old_arg) { File.join("_posts", old_filename) } 167 | let(:new_arg) { File.join("_posts", new_filename) } 168 | let(:args) { [old_arg, new_name] } 169 | 170 | before(:all) do 171 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 172 | Dir.chdir source_dir 173 | end 174 | 175 | before(:each) do 176 | FileUtils.mkdir_p posts_dir unless File.directory? posts_dir 177 | File.write(old_path, "---\nlayout: post\ntitle: #{old_name}\ndate: #{timestamp}\n---\n") 178 | end 179 | 180 | after(:each) do 181 | FileUtils.rm_r old_path if File.file? old_path 182 | FileUtils.rm_r new_path if File.file? new_path 183 | FileUtils.rm_r posts_dir if File.directory? posts_dir 184 | end 185 | 186 | it "moves a post" do 187 | expect(old_path).to exist 188 | expect(new_path).not_to exist 189 | capture_stdout { described_class.process(args) } 190 | expect(old_path).not_to exist 191 | expect(new_path).to exist 192 | end 193 | 194 | it "changes the title of the post" do 195 | expect(old_path).to exist 196 | expect(new_path).not_to exist 197 | expect(File.read(old_path)).to match(%r!title: #{old_name}!) 198 | capture_stdout { described_class.process(args) } 199 | expect(old_path).not_to exist 200 | expect(new_path).to exist 201 | expect(File.read(new_path)).to match(%r!title: #{new_name}!) 202 | end 203 | 204 | it "should write a helpful message when successful" do 205 | output = capture_stdout { described_class.process(args) } 206 | expect(output).to include("Post #{old_arg} was moved to #{new_arg}") 207 | end 208 | 209 | it "errors with no arguments" do 210 | expect(lambda { 211 | capture_stdout { described_class.process } 212 | }).to raise_error("You must specify current path and the new title.") 213 | end 214 | 215 | it "errors with only one argument" do 216 | expect(lambda { 217 | capture_stdout { described_class.process([old_path]) } 218 | }).to raise_error("You must specify current path and the new title.") 219 | end 220 | 221 | context "with a date argument" do 222 | let(:new_datestamp) { "2012-03-04" } 223 | let(:new_filename) { "#{new_datestamp}-#{new_basename}" } 224 | 225 | it "moves a post" do 226 | expect(old_path).to exist 227 | expect(new_path).not_to exist 228 | capture_stdout { described_class.process(args, "date" => new_datestamp) } 229 | expect(old_path).not_to exist 230 | expect(new_path).to exist 231 | end 232 | 233 | it "changes the title of the post" do 234 | expect(old_path).to exist 235 | expect(new_path).not_to exist 236 | expect(File.read(old_path)).to match(%r!title: #{old_name}!) 237 | capture_stdout { described_class.process(args, "date" => new_datestamp) } 238 | expect(old_path).not_to exist 239 | expect(new_path).to exist 240 | expect(File.read(new_path)).to match(%r!title: #{new_name}!) 241 | end 242 | 243 | it "changes the date of the post" do 244 | expect(old_path).to exist 245 | expect(new_path).not_to exist 246 | expect(File.read(old_path)).to match(%r!title: #{old_name}!) 247 | capture_stdout { described_class.process(args, "date" => new_datestamp) } 248 | expect(old_path).not_to exist 249 | expect(new_path).to exist 250 | expect(File.read(new_path)).to match(%r!date: #{new_datestamp}!) 251 | end 252 | 253 | it "should write a helpful message when successful" do 254 | output = capture_stdout { described_class.process(args, "date" => new_datestamp) } 255 | expect(output).to include("Post #{old_arg} was moved to #{new_arg}") 256 | end 257 | end 258 | 259 | context "with a now argument" do 260 | let(:now) { Time.parse("2015-06-07 08:09") } 261 | let(:old) { Time.parse("2012-03-04 05:06") } 262 | let(:now_datestamp) { now.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) } 263 | let(:now_timestamp) { now.strftime(Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT) } 264 | let(:datestamp) { old.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) } 265 | let(:timestamp) { old.strftime(Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT) } 266 | let(:new_filename) { "#{now_datestamp}-#{new_basename}" } 267 | 268 | before(:each) do 269 | FileUtils.mkdir_p posts_dir unless File.directory? posts_dir 270 | File.write(old_path, "---\nlayout: post\ntitle: #{old_name}\ndate: #{timestamp}\n---\n") 271 | allow(Time).to receive(:now).and_return(now) 272 | end 273 | 274 | after(:each) do 275 | FileUtils.rm_r old_path if File.file? old_path 276 | FileUtils.rm_r new_path if File.file? new_path 277 | FileUtils.rm_r posts_dir if File.directory? posts_dir 278 | end 279 | 280 | it "moves a post" do 281 | expect(old_path).to exist 282 | expect(new_path).not_to exist 283 | capture_stdout { described_class.process(args, "now" => true) } 284 | expect(old_path).not_to exist 285 | expect(new_path).to exist 286 | end 287 | 288 | it "changes the title of the post" do 289 | expect(old_path).to exist 290 | expect(new_path).not_to exist 291 | expect(File.read(old_path)).to match(%r!title: #{old_name}!) 292 | capture_stdout { described_class.process(args, "now" => true) } 293 | expect(old_path).not_to exist 294 | expect(new_path).to exist 295 | expect(File.read(new_path)).to match(%r!title: #{new_name}!) 296 | end 297 | 298 | it "changes the date of the post" do 299 | expect(old_path).to exist 300 | expect(new_path).not_to exist 301 | expect(File.read(old_path)).to match(%r!title: #{old_name}!) 302 | capture_stdout { described_class.process(args, "now" => true) } 303 | expect(old_path).not_to exist 304 | expect(new_path).to exist 305 | expect(File.read(new_path)).to match(%r!date: #{Regexp.quote(now_timestamp)}!) 306 | end 307 | 308 | it "should write a helpful message when successful" do 309 | output = capture_stdout { described_class.process(args, "now" => true) } 310 | expect(output).to include("Post #{old_arg} was moved to #{new_arg}") 311 | end 312 | end 313 | 314 | context "when the post with the new filename already exists" do 315 | before(:each) do 316 | FileUtils.touch new_path 317 | end 318 | 319 | it "displays a warning and returns" do 320 | output = capture_stdout { described_class.process(args) } 321 | expect(output).to include("A post already exists at #{new_arg}") 322 | expect(output).to_not include("Post #{old_arg} was moved to #{new_arg}") 323 | expect(File.read(new_path)).to_not include("layout: post") 324 | end 325 | 326 | it "overwrites if --force is given" do 327 | output = capture_stdout { described_class.process(args, "force" => true) } 328 | expect(output).to_not include("A post already exists at #{new_arg}") 329 | expect(output).to include("Post #{old_arg} was moved to #{new_arg}") 330 | expect(File.read(new_path)).to include("layout: post") 331 | end 332 | end 333 | 334 | context "when a configuration file exists" do 335 | let(:config) { source_dir("_config.yml") } 336 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 337 | let(:config_data) do 338 | %( 339 | source: site 340 | ) 341 | end 342 | 343 | before(:each) do 344 | File.open(config, "w") do |f| 345 | f.write(config_data) 346 | end 347 | end 348 | 349 | after(:each) do 350 | FileUtils.rm(config) 351 | end 352 | 353 | it "should use source directory set by config" do 354 | expect(new_path).not_to exist 355 | capture_stdout { described_class.process(args) } 356 | expect(new_path).to exist 357 | end 358 | 359 | context "and collections_dir is set" do 360 | let(:collections_dir) { "my_collections" } 361 | let(:posts_dir) { Pathname.new source_dir("site", collections_dir, "_posts") } 362 | let(:config_data) do 363 | %( 364 | source: site 365 | collections_dir: #{collections_dir} 366 | ) 367 | end 368 | 369 | it "should create posts at the correct location" do 370 | expect(posts_dir).to exist 371 | expect(old_path).to exist 372 | expect(new_path).not_to exist 373 | capture_stdout { described_class.process(args) } 374 | expect(posts_dir).to exist 375 | expect(old_path).not_to exist 376 | expect(new_path).to exist 377 | end 378 | 379 | it "should write a helpful message when successful" do 380 | output = capture_stdout { described_class.process(args) } 381 | old_arg = File.join("site", collections_dir, "_posts", old_filename) 382 | new_arg = File.join("site", collections_dir, "_posts", new_filename) 383 | expect(output).to include("Post #{old_arg} was moved to #{new_arg}") 384 | end 385 | end 386 | end 387 | 388 | context "when source option is set" do 389 | let(:posts_dir) { Pathname.new source_dir("site", "_posts") } 390 | 391 | it "should use source directory set by command line option" do 392 | expect(old_path).to exist 393 | expect(new_path).not_to exist 394 | capture_stdout { described_class.process(args, "source" => "site") } 395 | expect(old_path).not_to exist 396 | expect(new_path).to exist 397 | end 398 | end 399 | end 400 | end 401 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "jekyll" 4 | require File.expand_path("../lib/jekyll-compose.rb", __dir__) 5 | 6 | RSpec.configure do |config| 7 | config.expect_with :rspec do |expectations| 8 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 9 | end 10 | config.mock_with :rspec do |mocks| 11 | mocks.verify_partial_doubles = true 12 | end 13 | 14 | config.filter_run :focus 15 | config.run_all_when_everything_filtered = true 16 | 17 | config.disable_monkey_patching! 18 | 19 | config.warnings = true 20 | 21 | config.default_formatter = "doc" if config.files_to_run.one? 22 | 23 | config.profile_examples = 3 24 | 25 | config.order = :random 26 | 27 | Kernel.srand config.seed 28 | 29 | Jekyll.logger.log_level = :error 30 | 31 | ### 32 | ### Helper methods 33 | ### 34 | TEST_DIR = __dir__ 35 | def test_dir(*files) 36 | File.expand_path(File.join(TEST_DIR, *files)) 37 | end 38 | 39 | def source_dir(*files) 40 | test_dir("source", *files) 41 | end 42 | 43 | def fixture_site 44 | Jekyll::Site.new(Jekyll::Utils.deep_merge_hashes( 45 | Jekyll::Configuration::DEFAULTS, 46 | "source" => source_dir, "destination" => test_dir("dest") 47 | )) 48 | end 49 | 50 | def capture_stdout(level = :debug) 51 | buffer = StringIO.new 52 | Jekyll.logger = Logger.new(buffer) 53 | Jekyll.logger.log_level = level 54 | yield 55 | buffer.rewind 56 | buffer.string.to_s 57 | ensure 58 | Jekyll.logger = Logger.new(StringIO.new, :error) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/unpublish_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe(Jekyll::Commands::Unpublish) do 4 | let(:drafts_dir) { Pathname.new(source_dir("_drafts")) } 5 | let(:posts_dir) { Pathname.new(source_dir("_posts")) } 6 | let(:post_name) { "a-test-post.md" } 7 | let(:timestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_TIMESTAMP_FORMAT) } 8 | let(:datestamp) { Time.now.strftime(Jekyll::Compose::DEFAULT_DATESTAMP_FORMAT) } 9 | let(:post_filename) { "#{datestamp}-#{post_name}" } 10 | let(:post_path) { posts_dir.join post_filename } 11 | let(:draft_path) { drafts_dir.join post_name } 12 | 13 | let(:args) { ["_posts/#{post_filename}"] } 14 | 15 | before(:all) do 16 | FileUtils.mkdir_p source_dir unless File.directory? source_dir 17 | Dir.chdir source_dir 18 | end 19 | 20 | before(:each) do 21 | FileUtils.mkdir_p drafts_dir unless File.directory? drafts_dir 22 | FileUtils.mkdir_p posts_dir unless File.directory? posts_dir 23 | File.write(post_path, "---\nlayout: post\ndate: #{timestamp}\n---\n") 24 | end 25 | 26 | after(:each) do 27 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 28 | FileUtils.rm_r posts_dir if File.directory? posts_dir 29 | end 30 | 31 | it "moves a post back to _drafts" do 32 | expect(post_path).to exist 33 | expect(draft_path).not_to exist 34 | capture_stdout { described_class.process(args) } 35 | expect(post_path).not_to exist 36 | expect(draft_path).to exist 37 | expect(File.read(draft_path)).not_to include("date: #{timestamp}") 38 | end 39 | 40 | it "writes a helpful message on success" do 41 | expect(post_path).to exist 42 | output = capture_stdout { described_class.process(args) } 43 | expect(output).to include("Post _posts/#{post_filename} was moved to _drafts/#{post_name}") 44 | end 45 | 46 | it "creates the drafts folder if necessary" do 47 | FileUtils.rm_r drafts_dir if File.directory? drafts_dir 48 | capture_stdout { described_class.process(args) } 49 | expect(drafts_dir).to exist 50 | end 51 | 52 | it "errors if there is no argument" do 53 | expect(lambda { 54 | capture_stdout { described_class.process } 55 | }).to raise_error("You must specify a post path.") 56 | end 57 | 58 | it "outputs a warning and returns if no file exists at given path" do 59 | weird_path = "_posts/i-forgot-the-date.md" 60 | output = capture_stdout { described_class.process [weird_path] } 61 | expect(output).to include("There was no post found at '#{weird_path}'.") 62 | end 63 | 64 | context "when the draft already exists" do 65 | let(:args) { ["_posts/#{post_filename}"] } 66 | 67 | before(:each) do 68 | FileUtils.touch draft_path 69 | end 70 | 71 | it "displays a warning and returns" do 72 | output = capture_stdout { described_class.process(args) } 73 | expect(output).to include("A draft already exists at _drafts/#{post_name}") 74 | expect(draft_path).to exist 75 | expect(post_path).to exist 76 | end 77 | 78 | it "overwrites if --force is given" do 79 | output = capture_stdout { described_class.process(args, "force" => true) } 80 | expect(output).to_not include("A draft already exists at _drafts/#{post_name}") 81 | expect(draft_path).to exist 82 | expect(post_path).not_to exist 83 | expect(File.read(draft_path)).not_to include("date: #{timestamp}") 84 | end 85 | end 86 | 87 | context "when a configuration file exists" do 88 | let(:config) { source_dir("_config.yml") } 89 | let(:drafts_dir) { Pathname.new(source_dir("site", "_drafts")) } 90 | let(:posts_dir) { Pathname.new(source_dir("site", "_posts")) } 91 | let(:config_data) do 92 | %( 93 | source: site 94 | ) 95 | end 96 | 97 | before(:each) do 98 | File.open(config, "w") do |f| 99 | f.write(config_data) 100 | end 101 | end 102 | 103 | after(:each) do 104 | FileUtils.rm(config) 105 | end 106 | 107 | it "should use source directory set by config" do 108 | expect(post_path).to exist 109 | expect(draft_path).not_to exist 110 | capture_stdout { described_class.process(args) } 111 | expect(post_path).not_to exist 112 | expect(draft_path).to exist 113 | end 114 | 115 | context "and collections_dir is set" do 116 | let(:collections_dir) { "my_collections" } 117 | let(:drafts_dir) { Pathname.new(source_dir("site", collections_dir, "_drafts")) } 118 | let(:posts_dir) { Pathname.new(source_dir("site", collections_dir, "_posts")) } 119 | let(:config_data) do 120 | %( 121 | source: site 122 | collections_dir: #{collections_dir} 123 | ) 124 | end 125 | 126 | it "should move posts to the correct location" do 127 | expect(post_path).to exist 128 | expect(draft_path).not_to exist 129 | capture_stdout { described_class.process(args) } 130 | expect(draft_path).to exist 131 | end 132 | 133 | it "should write a helpful message when successful" do 134 | output = capture_stdout { described_class.process(args) } 135 | post_filepath = File.join("site", collections_dir, "_posts", post_filename) 136 | draft_filepath = File.join("site", collections_dir, "_drafts", post_name) 137 | expect(output).to include("Post #{post_filepath} was moved to #{draft_filepath}") 138 | end 139 | end 140 | end 141 | 142 | context "when source option is set" do 143 | let(:drafts_dir) { Pathname.new(source_dir("site", "_drafts")) } 144 | let(:posts_dir) { Pathname.new(source_dir("site", "_posts")) } 145 | 146 | it "should use source directory set by command line option" do 147 | expect(post_path).to exist 148 | expect(draft_path).not_to exist 149 | capture_stdout { described_class.process(args, "source" => "site") } 150 | expect(post_path).not_to exist 151 | expect(draft_path).to exist 152 | end 153 | end 154 | end 155 | --------------------------------------------------------------------------------