├── .editorconfig ├── .gitignore ├── LICENSE.txt ├── Makefile ├── README.md └── tasks.py /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | insert_final_newline = true 10 | 11 | [*.py] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # Custom 6 | dummy-jproject 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Pavlo Dmytrenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | flake: 2 | flake8 tasks.py 3 | 4 | clean: 5 | rm -rf `find . -name __pycache__` 6 | rm -f `find . -type f -name '*.py[co]' ` 7 | rm -f `find . -type f -name '*~' ` 8 | rm -f `find . -type f -name '.*~' ` 9 | rm -f `find . -type f -name '@*' ` 10 | rm -f `find . -type f -name '#*#' ` 11 | rm -f `find . -type f -name '*.orig' ` 12 | rm -f `find . -type f -name '*.rej' ` 13 | rm -f .coverage 14 | rm -rf coverage 15 | rm -rf build 16 | rm -rf cover 17 | rm -rf dist 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jtasks: a swiss knife for Jekyll projects 2 | ========================================= 3 | 4 | **_jtasks_ (Jekyll tasks) is a collection of configurable [Invoke](http://www.pyinvoke.org/) tasks that provide simple, but powerful, interface to run both common and _advanced_ routines in your Jekyll projects.** 5 | 6 | For example, let's run development server via [Bundler](http://bundler.io/), at host `0.0.0.0` (like in Jekyll 2), port `5000` (because I have multiple Jekyll projects served simultaneously), generate output into `dist` folder, including *drafts* and enabling *incremental build*: 7 | 8 | ``` 9 | # using jtasks: 10 | $ invoke serve -bdi 11 | 12 | # common way: 13 | $ bundle exec jekyll serve --destination ./dist/ --host 0.0.0.0 --port 5000 --incremental --drafts 14 | ``` 15 | 16 | `--destination`, `--host` and `--port` are configured as global variables at the *settings* part of the script. This allows flexible setup of **jtasks** on per-project basis. 17 | 18 | 19 | How to Use 20 | ---------- 21 | 22 | ``` 23 | inv[oke] task1 [--task1-opts] ... taskN [--taskN-opts] 24 | ``` 25 | 26 | ### Examples: 27 | 28 | Build the site with Bundler: 29 | ``` 30 | $ inv build -b 31 | ``` 32 | 33 | Serve site with Bundler including draft posts: 34 | ``` 35 | $ inv serve -bd 36 | ``` 37 | 38 | Notify Google and Bing about your sitemap updates: 39 | ``` 40 | $ inv notify -gb 41 | ``` 42 | 43 | Create a new post with the given title: 44 | ``` 45 | $ inv post "My awesome post" 46 | ``` 47 | 48 | Start development server and fire up default browser to preview resulted site: 49 | ``` 50 | $ inv serve preview 51 | ``` 52 | 53 | List all posts and drafts: 54 | ``` 55 | $ inv list -d 56 | ``` 57 | 58 | Remove generated site: 59 | ``` 60 | $ inv clean 61 | ``` 62 | 63 | Check site for compatibility problems and URL conflicts: 64 | ``` 65 | $ inv doctor 66 | ``` 67 | 68 | 69 | Installation 70 | ------------ 71 | 72 | Copy *tasks.py* file into the base folder of your Jekyll site. 73 | 74 | You also need *Python (2.7+ or 3.3+)* with [Invoke](http://www.pyinvoke.org/index.html) package: 75 | 76 | * [Install Python](https://www.python.org/downloads/) 77 | * [Install Invoke](http://www.pyinvoke.org/installing.html) 78 | 79 | 80 | Configuration 81 | ------------- 82 | 83 | All settings are available at the top of the *tasks.py* file: 84 | 85 | ```python 86 | # === Settings === 87 | 88 | # Project directories 89 | _site_dest = "./_site/" # Dir where Jekyll will generate site 90 | _posts_dest = "./_posts/" # Dir with posts 91 | _drafts_dest = "./_drafts/" # Dir with drafts 92 | 93 | # Global options 94 | _hostname = '127.0.0.1' # Listen given hostname 95 | _port = '4000' # Listen given port 96 | _bundle_exec = False # Run commands with Bundler 97 | _fpolling = False # Force watch to use polling 98 | _incremental = False # Enable incremental build (Jekyll 3 and higher!) 99 | 100 | # Post settings 101 | _post_ext = '.md' # Post files extension 102 | 103 | # Notification settings (your sitemap location) 104 | _sitemap_url = 'http://www.example.com/sitemap.xml' 105 | ``` 106 | 107 | From the box, **jtasks** uses [Jekyll 3 default configuration settings](http://jekyllrb.com/docs/configuration/). They can be easily modified for your project-specific needs. E.g. if we always need to run all tasks via Bundler, it is possible to set `_bundle_exec = True` and now all tasks (where applicable) will be executed using `bundle exec`: 108 | 109 | ``` 110 | $ inv build 111 | 112 | # Same as: 113 | $ bundle exec jekyll build --destination ./_site/ 114 | ``` 115 | 116 | 117 | Available tasks 118 | --------------- 119 | 120 | Currently, following tasks are supported: 121 | 122 | ``` 123 | $ inv --list 124 | Available tasks: 125 | 126 | build Build the site. 127 | clean Clean the site. 128 | doctor Search site and print specific deprecation warnings. 129 | list List all posts. 130 | notify Notify various services about sitemap update. 131 | post Create a new post. 132 | preview Launches default browser for previewing generated site. 133 | serve Serve the site locally. 134 | ``` 135 | 136 | ### build 137 | 138 | ``` 139 | $ inv --help build 140 | Docstring: 141 | Build the site. 142 | 143 | jekyll build [options] 144 | 145 | Options: 146 | -b, --bundle-exec Build site via Bundler. 147 | -d, --drafts Build site including draft posts. 148 | -i, --incremental-build Rebuild only posts and pages that have changed. 149 | ``` 150 | 151 | ### serve 152 | 153 | ``` 154 | $ inv --help serve 155 | Docstring: 156 | Serve the site locally. 157 | 158 | jekyll serve [options] 159 | 160 | Options: 161 | -b, --bundle-exec Run Jekyll development server via Bundler. 162 | -d, --drafts Process and render draft posts. 163 | -f, --force-polling Force watch to use polling. 164 | -i, --incremental-build Rebuild only posts and pages that have changed. 165 | ``` 166 | 167 | ### clean 168 | 169 | ``` 170 | $ inv --help clean 171 | Docstring: 172 | Clean the site. 173 | 174 | Removes site output and metadata file without building. 175 | 176 | Options: 177 | none 178 | ``` 179 | 180 | ### doctor 181 | 182 | ``` 183 | $ inv --help doctor 184 | Docstring: 185 | Search site and print specific deprecation warnings. 186 | 187 | jekyll doctor 188 | 189 | Options: 190 | -b, --bundle-exec Run doctor via Bundler. 191 | ``` 192 | 193 | ### post 194 | 195 | ``` 196 | $ inv --help post 197 | Docstring: 198 | Create a new post. 199 | 200 | Options: 201 | -d, --drafts Create draft post. 202 | -t STRING, --title=STRING Post title. 203 | ``` 204 | 205 | ### list 206 | 207 | ``` 208 | $ inv --help list 209 | Docstring: 210 | List all posts. 211 | 212 | Options: 213 | -d, --drafts Include draft posts. 214 | ``` 215 | 216 | ### notify 217 | 218 | ``` 219 | $ inv --help notify 220 | Docstring: 221 | Notify various services about sitemap update. 222 | 223 | Options: 224 | -b, --bing Notify Bing about sitemap updates. 225 | -g, --google Notify Google about sitemap updates. 226 | ``` 227 | 228 | 229 | ### preview 230 | 231 | ``` 232 | $ inv --help preview 233 | Docstring: 234 | Launches default browser for previewing generated site. 235 | 236 | `build` and/or `serve` tasks should be launched manually in advance, 237 | depending on desired options. 238 | 239 | Options: 240 | none 241 | ``` 242 | 243 | 244 | License 245 | ------- 246 | 247 | Distributed under the terms of the [MIT License](https://opensource.org/licenses/MIT). 248 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | # ========================================= 2 | # Source: https://github.com/pavdmyt/jtasks 3 | # ========================================= 4 | 5 | import os 6 | import shutil 7 | import webbrowser 8 | from datetime import datetime 9 | from invoke import task, run 10 | 11 | try: 12 | import urllib2 as urequest 13 | import urllib as uparse 14 | except ImportError: 15 | import urllib.request as urequest 16 | import urllib.parse as uparse 17 | 18 | 19 | # === Settings === 20 | 21 | # Project directories 22 | _site_dest = "./_site/" # Dir where Jekyll will generate site 23 | _posts_dest = "./_posts/" # Dir with posts 24 | _drafts_dest = "./_drafts/" # Dir with drafts 25 | 26 | # Global options 27 | _hostname = '127.0.0.1' # Listen given hostname 28 | _port = '4000' # Listen given port 29 | _bundle_exec = False # Run commands with Bundler 30 | _fpolling = False # Force watch to use polling 31 | _incremental = False # Enable incremental build (Jekyll 3 and higher!) 32 | 33 | # Post settings 34 | _post_ext = '.md' # Post files extension 35 | 36 | # Notification settings (your sitemap location) 37 | _sitemap_url = 'http://www.example.com/sitemap.xml' 38 | 39 | 40 | # == Helpers === 41 | 42 | build_help = { 43 | 'drafts': "Build site including draft posts.", 44 | 'bundle-exec': "Build site via Bundler.", 45 | 'incremental-build': "Rebuild only posts and pages that have changed." 46 | } 47 | 48 | serve_help = { 49 | 'drafts': "Process and render draft posts.", 50 | 'bundle-exec': "Run Jekyll development server via Bundler.", 51 | 'force-polling': "Force watch to use polling.", 52 | 'incremental-build': "Rebuild only posts and pages that have changed." 53 | } 54 | 55 | doctor_help = { 56 | 'bundle-exec': "Run doctor via Bundler." 57 | } 58 | 59 | list_help = { 60 | 'drafts': "Include draft posts." 61 | } 62 | 63 | post_help = { 64 | 'title': "Post title.", 65 | 'drafts': "Create draft post." 66 | } 67 | 68 | notify_help = { 69 | 'google': "Notify Google about sitemap updates.", 70 | 'bing': "Notify Bing about sitemap updates." 71 | } 72 | 73 | 74 | # === Tasks === 75 | 76 | @task(help=build_help) 77 | def build(drafts=False, bundle_exec=_bundle_exec, 78 | incremental_build=_incremental): 79 | """Build the site. 80 | 81 | jekyll build [options] 82 | """ 83 | core_command = 'jekyll build' 84 | exec_lst = [core_command] 85 | 86 | # Icluding _site_dest 87 | exec_lst.append('-d ' + _site_dest) 88 | 89 | # Parsing options 90 | if bundle_exec: 91 | exec_lst.insert(0, 'bundle exec') 92 | if incremental_build: 93 | exec_lst.append('-I') 94 | if drafts: 95 | exec_lst.append('--drafts') 96 | 97 | # Print options and execute resulted command 98 | printer(exec_lst) 99 | run(' '.join(exec_lst)) 100 | 101 | 102 | @task(help=serve_help) 103 | def serve(drafts=False, bundle_exec=_bundle_exec, 104 | force_polling=_fpolling, incremental_build=_incremental): 105 | """Serve the site locally. 106 | 107 | jekyll serve [options] 108 | """ 109 | core_command = 'jekyll serve' 110 | exec_lst = [core_command] 111 | 112 | # Icluding _site_dest 113 | exec_lst.append('-d ' + _site_dest) 114 | 115 | # Listening given port and hostname 116 | exec_lst.append('--host ' + _hostname) 117 | exec_lst.append('--port ' + _port) 118 | 119 | # Parsing options 120 | if bundle_exec: 121 | exec_lst.insert(0, 'bundle exec') 122 | if incremental_build: 123 | exec_lst.append('-I') 124 | if drafts: 125 | exec_lst.append('--drafts') 126 | if force_polling: 127 | exec_lst.append('--force_polling') 128 | 129 | # Print options and execute resulted command 130 | printer(exec_lst) 131 | run(' '.join(exec_lst)) 132 | 133 | 134 | @task 135 | def clean(): 136 | """Clean the site. 137 | 138 | Removes site output and metadata file without building. 139 | """ 140 | print("\nCleaning the site from {}\n".format(_site_dest)) 141 | rm(_site_dest) 142 | 143 | 144 | @task(help=doctor_help) 145 | def doctor(bundle_exec=_bundle_exec): 146 | """Search site and print specific deprecation warnings. 147 | 148 | jekyll doctor 149 | """ 150 | core_command = 'jekyll doctor' 151 | exec_lst = [core_command] 152 | 153 | # Parsing options 154 | if bundle_exec: 155 | exec_lst.insert(0, 'bundle exec') 156 | 157 | # Print options and execute resulted command 158 | printer(exec_lst) 159 | run(' '.join(exec_lst)) 160 | 161 | 162 | @task(help=list_help) 163 | def list(drafts=False): 164 | """List all posts.""" 165 | print("\nListing posts...\n") 166 | ls(_posts_dest) 167 | print("") 168 | 169 | if drafts: 170 | print("Listing drafts...\n") 171 | ls(_drafts_dest) 172 | print("") 173 | 174 | 175 | @task(help=post_help) 176 | def post(title, drafts=False): 177 | """Create a new post.""" 178 | # Parsing options 179 | if drafts: 180 | dest = _drafts_dest 181 | else: 182 | dest = _posts_dest 183 | 184 | # File name 185 | date = get_date() 186 | name = sanitize(title) 187 | fname = "{}-{}{}".format(date, name, _post_ext) 188 | 189 | # Front Matter 190 | front_matter = [] 191 | front_matter.append('---') 192 | front_matter.append('layout: post') 193 | front_matter.append('title: {}'.format(title)) 194 | front_matter.append('---') 195 | 196 | # Create post file and write Front Matter 197 | print("\nCreating new post '{}' in {}\n".format(fname, dest)) 198 | try: 199 | f = open(dest + fname, 'w') 200 | except Exception as e: 201 | print("* [Error] occured: {}\n".format(e)) 202 | else: 203 | f.write('\n'.join(front_matter)) 204 | f.close() 205 | print("* Done.\n") 206 | 207 | 208 | @task(help=notify_help) 209 | def notify(google=False, bing=False): 210 | """Notify various services about sitemap update.""" 211 | if google: 212 | base_url = 'http://www.google.com/webmasters/sitemaps/ping' 213 | params = {'sitemap': _sitemap_url} 214 | ping_sitemap(base_url, params) 215 | if bing: 216 | base_url = 'http://www.bing.com/webmaster/ping.aspx' 217 | params = {'siteMap': _sitemap_url} 218 | ping_sitemap(base_url, params) 219 | 220 | if not (google or bing): 221 | print("\n* Specify service(s) to ping.") 222 | print("* type: 'invoke --help notify'") 223 | print("* for the list of available options.\n") 224 | 225 | 226 | @task 227 | def preview(): 228 | """Launches default browser for previewing generated site. 229 | 230 | `build` and/or `serve` tasks should be launched manually in advance, 231 | depending on desired options. 232 | """ 233 | url = "http://{}:{}".format(_hostname, _port) 234 | webbrowser.open(url) 235 | 236 | 237 | # === Helper functions === 238 | 239 | def sanitize(str): 240 | """Align post title to the Jekyll post name requirements.""" 241 | res = str.lower() 242 | return res.replace(' ', '-') 243 | 244 | 245 | def get_date(): 246 | """Get current date in YEAR-MONTH-DAY format.""" 247 | dt = datetime.now() 248 | return dt.strftime("%Y-%m-%d") 249 | 250 | 251 | def ping_sitemap(base_url, params): 252 | """Submit sitemap.""" 253 | url_values = uparse.urlencode(params) 254 | full_url = base_url + '?' + url_values 255 | req = urequest.Request(full_url) 256 | 257 | print("\nSubmitting sitemap to {}\n".format(base_url)) 258 | try: 259 | urequest.urlopen(req) 260 | print("* Done.\n") 261 | except Exception as e: 262 | print("* [Error] occured: {}\n".format(e)) 263 | 264 | 265 | def ls(path): 266 | """Print dir contents.""" 267 | try: 268 | item_lst = os.listdir(path) 269 | except Exception as e: 270 | print("* [Error] occured: {}\n".format(e)) 271 | else: 272 | for item in item_lst: 273 | print(item) 274 | 275 | 276 | def rm(path): 277 | """Recursively delete a directory tree.""" 278 | try: 279 | shutil.rmtree(path) 280 | except Exception as e: 281 | print("* [Error] occured: {}\n".format(e)) 282 | else: 283 | print("* Done.\n") 284 | 285 | 286 | def printer(exec_lst): 287 | # Core commands 288 | if 'jekyll build' in exec_lst: 289 | print("\nBuilding the site in {}\n".format(_site_dest)) 290 | if 'jekyll serve' in exec_lst: 291 | print("\nServing the site in {}\n".format(_site_dest)) 292 | if 'jekyll doctor' in exec_lst: 293 | print( 294 | "\nChecking site for compatibility problems and URL conflicts...\n" 295 | ) 296 | 297 | # Options 298 | if '--host ' + _hostname in exec_lst: 299 | print( 300 | "* Starting Jekyll development server at {}:{}". 301 | format(_hostname, _port) 302 | ) 303 | if 'bundle exec' in exec_lst: 304 | print("* Running via Bundler...") 305 | if '-I' in exec_lst: 306 | print("* Enabling incremental build (Jekyll 3 and higher only)...") 307 | if ('--drafts' in exec_lst) or (_drafts_dest in exec_lst): 308 | print("* Including drafts...") 309 | if '--force_polling' in exec_lst: 310 | print("* Forcing watch to use polling...") 311 | 312 | # Printing resulted command 313 | print("\n>>> " + ' '.join(exec_lst) + "\n") 314 | --------------------------------------------------------------------------------