3
\$\begingroup\$

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

  1. Write a line of text...
Heading line to be 
  1. Use Vim leader sequence to convert line into level 2 MarkDown heading...
<Leader>h2 
  1. Example results...
## Heading line to be 
  1. Use Vim command to build Heading Link with title text...
:Hl Text about heading 
  1. Example results...
## Heading line to be [heading__heading_line_to_be]: #heading-line-to-be "Text about heading" 
  1. Edit text of heading...
## Edited heading line [heading__heading_line_to_be]: #heading-line-to-be "Text about heading" 
  1. Use Vim leader sequence to update Heading Link...
<Leader>hl 
  1. 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] 
  1. 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.

\$\endgroup\$

    0

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.