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 [email protected]: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.