This is my first Vim plugin and is intended to assist with adding, and editing, MarkDown headings. Features include changing the heading depth, auto-update heading reference links, and adding link title (on hover) text. Currently everything seems to function as designed, however, as this the first Vim script project that I've published, I'm certain that there is room for improvement.
Questions
- Are there any features for modification/customization that need to be added?
Or in other-words, should I offer leader/command key customization, and if so what are the best practices for achieving that?
- Is there a way to throw error codes, and if so what are the correct error codes to throw within this plugin?
Examples would be super helpful for both myself and future readers.
- Have I made any mistakes?
Requirements
This plugin is tested on Linux based operating systems, but suggestions on how to make it OS agnostic are certainly welcomed; regardless Vim should be installed prior to using this plugin, eg...
sudo apt-get install vim
Setup
Source code is maintained on GitHub, as are the documentation files and README.md
. Included within this question are the TLDR and source .vim
script code files.
mkdir -vp ~/git/hub/vim-utilities
cd ~/git/hub/vim-utilities
git clone git@github.com:vim-utilities/markdown-headings.git
If not utilizing some form of Vim plugin manager, then the linked-install.sh
script may be run on most 'nix devices...
./linked-install.sh
... which will symbolically link plugin scripts and documentation, and update Vim doc. tags.
Usage
Features of this plugin automatically activate if detected filetype
is markdown
Example Usage
- Write a line of text...
Heading line to be
- Use Vim leader sequence to convert line into level
2
MarkDown heading...
<Leader>h2
- Example results...
## Heading line to be
- Use Vim command to build Heading Link with title text...
:Hl Text about heading
- Example results...
## Heading line to be
[heading__heading_line_to_be]: #heading-line-to-be "Text about heading"
- Edit text of heading...
## Edited heading line
[heading__heading_line_to_be]: #heading-line-to-be "Text about heading"
- Use Vim leader sequence to update Heading Link...
<Leader>hl
- Example results...
## Edited heading line
[heading__edited_heading_line]: #edited-heading-line "Text about heading"
... Note, either :Hl
or <Leader>hl
will update any references to the heading within the document too!
For example for a table of contents similar to...
- [Link to Some Heading][heading__heading_line_to_be]
... would be updated to...
- [Link to Some Heading][heading__edited_heading_line]
- Convert the level two heading to a level three heading...
<Leader>h3
... example results...
### Edited heading line
Source Code
plugin/markdown-heading-link.vim
#!/usr/bin/env vim
autocmd FileType markdown call s:Register_Leader()
autocmd FileType markdown call s:Register_Commands()
""
" Registers Normal mode leader sequences
function! s:Register_Leader()
nnoremap <Leader>hl :call MarkDown_Headings_Make_Link()<cr>
endfunction
""
" Registers Ex mode commands
function! s:Register_Commands()
command! -nargs=* Hl call MarkDown_Headings_Make_Link(<f-args>)
endfunction
""
" Returns lower-cased string, replacing spaces with dashes and striping non-alpha/numeric characters
" @param {string} heading_text - Word to sluggify
" @return {string}
" @author S0AndS0
" @license AGPL-3.0
" @example
" echo s:Sluggify('## Spam Flavored Ham')
" #> spam-flavored-ham
function! s:Sluggify(heading_text)
let l:trimmed_line = substitute(a:heading_text, '^#* ', '', '')
let l:lowered_line = tolower(l:trimmed_line)
let l:sanitized_line = substitute(l:lowered_line, '[^a-z0-9 ]', '', 'g')
let l:dashed_line = substitute(l:sanitized_line, ' ', '-', 'g')
let l:slugged_line = substitute(l:dashed_line, '--*', '-', 'g')
return l:slugged_line
endfunction
""
" Returns heading ref/tag for linking within document
" @param {string} slugged_line - Slugged string to transmute into heading tag
" @return {string}
" @author S0AndS0
" @license AGPL-3.0
" @example
" echo s:Taggify_Slug('spam-flavored-ham')
" #> heading__spam_flavored_ham
function! s:Taggify_Slug(slugged_line)
return 'heading__' . substitute(a:slugged_line, '-', '_', 'g')
endfunction
""
" Updates heading link references for entire document
" @param {string} old_reference - Heading reference to search document for
" @param {string} new_reference - Replacement heading reference
" @author S0AndS0
" @license AGPL-3.0
" @throws Error codes that are **not** E486
" @example
" s:Update_Refs('heading__spam_flavored_ham]', 'heading__raspberry_jam]')
function! s:Update_Refs(old_reference, new_reference)
try
execute ':%s/\[' . a:old_reference . '/\[' . a:new_reference . '/g'
catch
if v:exception !~ '^Vim\%((\a\+)\)\=:E486'
throw v:exception
endif
endtry
endfunction
""
" Adds or updates reference link for heading under cursor postilion
" @param {string[]|number[]} ... - List of words and/or numbers
" @author S0AndS0
" @license AGPL-3.0
" @throws {string} 'Did not detect MarkDown heading!'
function! MarkDown_Headings_Make_Link(...)
let l:current_line = getline('.')
if match(l:current_line, '#') < 0
throw 'Did not detect MarkDown heading!'
endif
let l:cursor_position = getpos('.')
let l:slugged_line = s:Sluggify(l:current_line)
let l:tag_line = s:Taggify_Slug(slugged_line)
let l:next_line = getline(l:cursor_position[1] + 1)
let l:regex_link_without_title = "\[[a-z0-9_]*\]: #[a-z0-9\-]*"
let l:regex_link_with_title = l:regex_link_without_title . " \"[[:print:]]*\""
let l:heading_tag = '[' . l:tag_line . ']'
let l:heading_title = join(a:000)
let l:heading_link_text = l:heading_tag . ': #' . l:slugged_line
if len(l:heading_title) > 0
let l:heading_link_text .= ' "' . l:heading_title . '"'
elseif match(l:next_line, l:regex_link_with_title) >= 0
let l:heading_title = split(l:next_line, '"')[-1]
if len(l:heading_title) > 0
let l:heading_link_text .= ' "' . l:heading_title . '"'
endif
endif
if match(l:next_line, l:regex_link_without_title) < 0
execute "normal! A\n" . l:heading_link_text
else
let l:old_heading_tag = l:next_line[0:match(l:next_line, ']')]
execute "normal! j0c$" . l:heading_link_text
let l:tag_find = l:old_heading_tag[1:len(l:old_heading_tag)]
let l:tag_replace = l:heading_tag[1:len(l:heading_tag)]
call s:Update_Refs(l:tag_find, l:tag_replace)
endif
call setpos('.', cursor_position)
if len(l:heading_title) == 0
execute "normal! jA" . ' ""'
:startinsert
endif
endfunction
plugin/markdown-heading-transform.vim
#!/usr/bin/env vim
autocmd FileType markdown call s:Register_Leader()
autocmd FileType markdown call s:Register_Commands()
""
" Registers Normal mode leader sequences
function! s:Register_Leader()
nnoremap <Leader>h0 :call MarkDown_Heading_Transform(0)<cr>
nnoremap <Leader>h1 :call MarkDown_Heading_Transform(1)<cr>
nnoremap <Leader>h2 :call MarkDown_Heading_Transform(2)<cr>
nnoremap <Leader>h3 :call MarkDown_Heading_Transform(3)<cr>
nnoremap <Leader>h4 :call MarkDown_Heading_Transform(4)<cr>
endfunction
""
" Registers Ex mode commands
function! s:Register_Commands()
command! -nargs=1 H call MarkDown_Heading_Transform(<f-args>)
endfunction
""
" Transforms current line to MarkDown heading of defined depth
" @param {number} depth - Heading depth, eg. `0`, or `1`, or `-1`, etc
" @author S0AndS0
" @license AGPL-3.0
" @example text
" foo bar
" @example input
" \h3
" @example output
" ### foo bar
function! MarkDown_Heading_Transform(depth)
let l:cursor_position = getpos('.')
let l:current_line = getline('.')
let l:trimmed_line = substitute(l:current_line, '^#* ', '', '')
let l:current_depth = len(substitute(l:current_line, '[^#*].*', '', ''))
if a:depth == l:current_depth
return
elseif a:depth > l:current_depth && l:current_depth == 0
let l:cursor_x_offset = a:depth + 1 - l:current_depth
elseif a:depth < l:current_depth && a:depth == 0
let l:cursor_x_offset = a:depth - 1 - l:current_depth
else
let l:cursor_x_offset = a:depth - l:current_depth
endif
if a:depth > 0
let l:headings = repeat('#', a:depth)
let l:heading_line = join([l:headings, ' ', l:trimmed_line], '')
let l:cursor_position[2] += l:cursor_x_offset
elseif a:depth == 0
let l:heading_line = l:trimmed_line
if l:cursor_position[2] > 0
let l:cursor_position[2] += l:cursor_x_offset
else
let l:cursor_position[2] = 0
endif
else
let l:new_depth = len(repeat('#', l:current_depth + a:depth))
if l:new_depth > 0
call MarkDown_Heading_Transform(l:new_depth)
else
call MarkDown_Heading_Transform(0)
endif
return
endif
execute 'normal! 0d$0i' . l:heading_line
call setpos('.', l:cursor_position)
endfunction
Side note, I'm not sure what's going on with syntax highlighting here, but as of latest revisions these scripts seem to function without error.