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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.