roachhd/roachhd.github.io

View on GitHub
_posts/2014-12-27-vim-jekyll-awesomeness.md

Summary

Maintainability
Test Coverage
---
layout: post
title: Vim & Jekyll Zen
share: true
comments: true
published: false
---
INTRODUCTION                                    *jekyll*


This script is intended to automate the process of creating and editing
Jekyll (http://jekyllrb.com/) blog posts from within vim.


This is complete rewrite of csexton/jekyll.vim
(https://github.com/csexton/jekyll.vim/) with these improvements:


* Commands to edit/split/vsplit/tabnew a post
* Tab completion for opening existing posts
* Recognizes Octopress blogs and others with custom `_posts` directory
* Customizable template for new posts
* Customizable build command, with automatic support for bundler


==============================================================================
CONFIGURATION                                   *jekyll-configuration*


There are a few global variables that can be set to adjust the way vim handles
Jekyll blogs. Default values for each are listed below unless noted.

{% highlight powershell %}
                                                *g:jekyll_post_dirs*  >
    let g:jekyll_post_dirs = ['_posts', '_source/_posts']
A |List| containing paths to search to identify a Jekyll blog's posts
directory. For most Jekyll blogs, this will be `_posts`. Octopress posts are
kept in `_source/_posts`.
{% endhighlight %}

{% highlight powershell %}
                                                *g:jekyll_post_extension*  >
    let g:jekyll_post_extension = '.markdown'
The extension used when creating new posts.
{% endhighlight %}

{% highlight powershell %}
                                                *g:jekyll_post_filetype*  >
    let g:jekyll_post_filetype = 'liquid'
The |filetype| used when creating new posts.
{% endhighlight %}

{% highlight powershell %}
                                                *g:jekyll_post_template*  >
    let g:jekyll_post_template =  [
      \ '---',
      \ 'layout: post',
      \ 'title: "JEKYLL_TITLE"',
      \ 'date: "JEKYLL_DATE"',
      \ '---',
      \ '']
{% endhighlight %}


A |List| containing lines to used as a template when creating new posts.
JEKYLL_DATE and JEKYLL_TITLE will be replaced with their real values.


{% highlight powershell %}
                                                *g:jekyll_site_dir*  >
    let g:jekyll_site_dir = '_site'
Directory to place generated files in when running the |:Jbuild| command.
Relative to the root of your blog.
{% endhighlight %}

{% highlight powershell %}
                                                *g:jekyll_build_command*  >
    let g:jekyll_build_command = 'jekyll --no-auto --no-server'
Custom command to use to build your blog. By default this is unset.
{% endhighlight %}


==============================================================================

COMMANDS                                        *jekyll-commands*


The |:Jpost| (Jekyll post) command is used to create and edit blog posts. It
has variants for opening a post in a horizontal or vertical split or a new
tab. Call with a bang, (eg: :Jpost!) to create a new post. Call without a bang
(eg: :Jpost) to edit a post. The |:Jbuild| command can be used to build a
blog.

{% highlight powershell %}
                                                *jekyll-:Jpost*
:Jpost[!]  [{name}] Create or edit the specified post. With no argument, you
                    will be prompted to select a post or enter a title.
{% endhighlight %}

{% highlight powershell %}
                                                *jekyll-:JSpost*
:JSpost[!] [{name}] Same as |:Jpost|, but opens post in a horizontal split.
{% endhighlight %}

{% highlight powershell %}
                                                *jekyll-:JVpost*
:JVpost[!] [{name}] Same as |:Jpost|, but opens post in a vertical split.
{% endhighlight %}

{% highlight powershell %}
                                                *jekyll-:JTpost*
:JTpost[!] [{name}] Same as |:Jpost|, but opens post in a tab.
{% endhighlight %}

{% highlight powershell %}
                                                *jekyll-:Jbuild*
:Jbuild [{args}]    Generate blog. This will check for the presence of a
                    Gemfile; if found `bundle exec` is used to run the
                    `jekyll` command. The blog will be built with: >
{% endhighlight %}

{% highlight powershell %}
            jekyll --no-auto --no-server BLOG_ROOT BLOG_ROOT/SITE_DIR <args>
<
                    If this doesn't fit your situation, you can set a custom
                    command with |g:jekyll_build_command|. When using a custom
                    command no check for a Gemfile is performed.

{% endhighlight %}

---


### The Script

{% highlight powershell %}
" jekyll.vim
" Author:  Joshua Priddle <jpriddle@me.com>
" URL:     https://github.com/itspriddle/vim-jekyll
" Version: 0.1.0
" License: Same as Vim itself (see :help license)


if exists('g:loaded_jekyll') || &cp || v:version < 700
  finish
endif
let g:loaded_jekyll = 1


" Configuration {{{


" Directories to search for posts. _source/_posts is for Octopress.
if ! exists('g:jekyll_post_dirs')
  let g:jekyll_post_dirs = ['_posts', '_source/_posts']
endif


" Extension used when creating new posts
if ! exists('g:jekyll_post_extension')
  let g:jekyll_post_extension = '.markdown'
endif


" Filetype applied to new posts
if ! exists('g:jekyll_post_filetype')
  let g:jekyll_post_filetype = 'liquid'
endif


" Template for new posts
if ! exists('g:jekyll_post_template')
  let g:jekyll_post_template = [
    \ '---',
    \ 'layout: post',
    \ 'title: "JEKYLL_TITLE"',
    \ 'date: "JEKYLL_DATE"',
    \ '---',
    \ '']
endif


" Directory to place generated files in, relative to b:jekyll_root_dir.
" Usually _site
if ! exists('g:jekyll_site_dir')
  let g:jekyll_site_dir = '_site'
endif


" }}}


" Utility functions {{{


" Print an error message
function! s:error(string)
  echohl ErrorMsg
  echomsg "Error: ".a:string
  echohl None
  let v:errmsg = a:string
endfunction


" Substitute first occurrences of pattern in string with replacement
function! s:sub(string, pattern, replacement)
  return substitute(a:string, '\v\C'.a:pattern, a:replacement, '')
endfunction


" Substitute all occurrences of pattern in string with replacement
function! s:gsub(string, pattern, replacement)
  return substitute(a:string, '\v\C'.a:pattern, a:replacement, 'g')
endfunction


" Returns true if string stars with prefix
function! s:startswith(string, prefix)
  return strpart(a:string, 0, strlen(a:prefix)) ==# a:prefix
endfunction


" Format a file path
function! s:escape_path(path)
  let path = a:path
  let path = s:gsub(path, '/+', '/')
  let path = s:gsub(path, '[\\/]$', '')
  return path
endfunction


" Returns a lower-case string, all non-alpha numeric characters are replaced
" with dashes
function! s:dasherize(string)
  let string = tolower(a:string)
  let string = s:gsub(string, '([^a-z0-9])+', '-')
  let string = s:gsub(string, '(^-*|-*$)', '')
  return string
endfunction


" }}}


" Post functions {{{


" Returns the filename for a new post based on it's title.
function! s:post_filename(title)
  return b:jekyll_post_dir.'/'.strftime('%Y-%m-%d-').s:dasherize(a:title).g:jekyll_post_extension
endfunction
"
" Strips whitespace and escapes double quotes
function! s:post_title(title)
  let title = s:gsub(a:title, '(^[ ]*|[ ]*$)', '')
  let title = s:gsub(title, '[ ]{2,}', ' ')
  let title = s:gsub(title, '"', '\\&')
  return title
endfunction


" Used to autocomplete posts
function! s:post_list(A, L, P)
  let prefix   = b:jekyll_post_dir.'/'
  let data     = s:gsub(glob(prefix.'*.*')."\n", prefix, '')
  let data     = s:gsub(data, '\'.g:jekyll_post_extension."\n", "\n")
  let files    = reverse(split(data, "\n"))
  let filtered = filter(copy(files), 's:startswith(v:val, a:A)')


  if ! empty(filtered)
    return filtered
  endif
endfunction


" Send the given filename to the editor using cmd
function! s:load_post(cmd, filename)
  let cmd  = empty(a:cmd) ? 'E' : a:cmd
  let cmds = {'E': 'edit', 'S': 'split', 'V': 'vsplit', 'T': 'tabnew'}


  if ! has_key(cmds, cmd)
    return s:error('Invalid command: '. cmd)
  else
    execute cmds[cmd]." ".a:filename
  endif
endfunction


" Create a new blog post
function! s:create_post(cmd, ...)
  let title = a:0 && ! empty(a:1) ? a:1 : input('Post title: ')


  if empty(title)
    return s:error('You must specify a title')
  elseif filereadable(b:jekyll_post_dir.'/'.title.g:jekyll_post_extension)
    return s:error(title.' already exists!')
  endif


  call s:load_post(a:cmd, s:post_filename(title))


  let error = append(0, g:jekyll_post_template)


  if error > 0
    return s:error("Couldn't create post.")
  else
    let &ft = g:jekyll_post_filetype


    let date = strftime('%a %b %d %T %z %Y')
    silent! %s/JEKYLL_TITLE/\=s:post_title(title)/g
    silent! %s/JEKYLL_DATE/\=date/g
  endif
endfunction


" Edit a post
function! s:edit_post(cmd, post)
  let file = b:jekyll_post_dir.'/'.a:post.g:jekyll_post_extension


  if filereadable(file)
    return s:load_post(a:cmd, file)
  else
    return s:error('File '.file.' does not exist! Try :J'.a:cmd.'post! to create a new post.')
  endif
endfunction


" Create/edit a post, used by :Jpost and friends to determine if we're editing
" or creating a post.
function! s:open_post(create, cmd, ...)
  if a:create
    return s:create_post(a:cmd, a:1)
  else
    return s:edit_post(a:cmd, a:1)
  endif
endfunction


" Return the command used to build the blog.
function! s:jekyll_bin()
  let bin = 'jekyll --no-auto --no-server '


  if filereadable(b:jekyll_root_dir.'/Gemfile')
    let bin = 'bundle exec '.bin
  endif


  let bin .= b:jekyll_root_dir.' '.b:jekyll_root_dir.'/'.g:jekyll_site_dir
  return bin
endfunction


" Return 'jekyll' or 'bundle exec jekyll'
function! s:jekyll_build(cmd)
  if exists('g:jekyll_build_command') && ! empty(g:jekyll_build_command)
    let bin = g:jekyll_build_command
  else
    let bin = s:jekyll_bin()
  endif


  echo 'Building, this may take a moment'
  let lines = system(bin.' '.a:cmd)


  if v:shell_error != 0
    return s:error('Build failed')
  else
    echo 'Site built!'
  endif
endfunction


" Register a new user command
function! s:define_command(cmd)
  exe 'command! -buffer '.a:cmd
endfunction


" }}}


" Initialization {{{


" Register plugin commands
"
" :Jpost[!]  - edit in current buffer
" :JSpost[!] - edit in a split
" :JVpost[!] - edit in a vertical split
" :JTpost[!] - edit in a new tab
function! s:register_commands()
  for cmd in ['', 'S', 'V', 'T']
    call s:define_command('-bang -nargs=? -complete=customlist,s:post_list J'.cmd.'post :call s:open_post(<bang>0, "'.cmd.'", <q-args>)')
  endfor


  call s:define_command('-nargs=* Jbuild call s:jekyll_build("<args>")')
endfunction


" Try to locate the _posts directory
function! s:find_jekyll(path) abort
  let cur_path = a:path
  let old_path = ""
  while old_path != cur_path
    for dir in g:jekyll_post_dirs
      let dir      = s:escape_path(dir)
      if isdirectory(cur_path.'/'.dir)
        return [cur_path, cur_path.'/'.dir]
      endif
    endfor
    let old_path = cur_path
    let cur_path = fnamemodify(old_path, ':h')
  endwhile
  return ['', '']
endfunction


" Initialize the plugin if we can detect a Jekyll blog
function! s:init(path)
  let [root_dir, post_dir] = s:find_jekyll(a:path)


  if empty(post_dir) || empty(root_dir)
    return
  endif


  let b:jekyll_root_dir = root_dir
  let b:jekyll_post_dir = post_dir


  silent doautocmd User Jekyll
endfunction


augroup jekyll_commands
  autocmd!
  autocmd User Jekyll call s:register_commands()
augroup END


augroup jekyll_init
  autocmd!
  autocmd BufNewFile,BufReadPost * call s:init(expand('<amatch>:p'))
  autocmd FileType           netrw call s:init(expand('<afile>:p'))
  autocmd VimEnter *
    \ if expand('<amatch>') == '' |
    \   call s:init(getcwd()) |
    \ endif
augroup END


" }}}


" vim:ft=vim:fdm=marker:ts=2:sw=2:sts=2:et
{% endhighlight %}