vim-literate-markdown

A Vim plugin to replicate a subset of Org mode's literate programming, for Markdown files.
git clone git://git.alex.balgavy.eu/vim-literate-markdown.git
Log | Files | Refs | README

commit 85de24c998192da98ced4daec9e44f28dedc55cf
parent 49f011e20697af65bcdb8dae0f1bf59718301275
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Sun, 27 Jun 2021 13:33:38 +0200

Add ability to tangle to different files, and based on language

You can switch the file you're tangling to throughout the document. You
can specify the tangle file for a particular language by adding the
language as a parameter, i.e. :Tangle(python). The language parameter
has to match the language in the header of the block. If a file isn't
specified for a language, the general :Tangle file is used.

Diffstat:
Mafter/ftplugin/markdown.vim | 4++++
Mautoload/literate_markdown.vim | 164++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
2 files changed, 130 insertions(+), 38 deletions(-)

diff --git a/after/ftplugin/markdown.vim b/after/ftplugin/markdown.vim @@ -1,3 +1,7 @@ +if exists('g:loaded_literate_markdown') + finish +endif + command -buffer -bar Tangle call literate_markdown#Tangle() command -buffer -bar ExecPrevBlock call literate_markdown#ExecPreviousBlock() nnoremap <buffer> <Plug>LitMdExecPrevBlock :<c-u>ExecPrevBlock<CR> diff --git a/autoload/literate_markdown.vim b/autoload/literate_markdown.vim @@ -7,31 +7,26 @@ let g:loaded_literate_markdown_autoload = 1 let s:codeblock_start = '^ *```[a-z]\+' let s:codeblock_end = '^ *```$' -let s:tangle_directive = '<!-- *:Tangle ' +let s:tangle_directive = '<!-- *:Tangle' let s:result_comment_start = '<!--\nRESULT:' let s:result_comment_end = '^-->' +let s:ALL_INTERP = '' -function! s:GetFilename() - " TODO: add optional parameter to tangle only specific language - " maybe format like: <!-- :Tangle(lang) /path/to/file --> - - let ln = search(s:tangle_directive, 'n') - if ln ==# 0 - echoerr 'No :Tangle directive found' - return '' - else - let fname = getline(ln)->matchstr(':Tangle \zs.*\ze -->') - if fname ==# '' - echoerr 'No filename set.' - else - if fname[0] !=# '/' - let fname = expand("%:p:h") . '/' . fname - endif - return fnameescape(fname) - endif +" returns [interpreter, filename] +function! s:ParseTangleDirective(ln) + let theline = getline(a:ln)->matchlist(':Tangle\(([a-zA-Z0-9]*)\)* \(.*\) -->')[0:2] + if empty(theline) + echoerr 'No filename in Tangle directive on line ' .. a:ln + return [] + endif + let [_, interp, fname] = theline + if fname[0] !=# '/' + let fname = expand("%:p:h") . '/' . fname endif + return [s:Lang2Interpreter(interp[1:-2]), fnameescape(fname)] endfunction +" returns end line of a code block function! s:GetBlockEnd(start) " Save the cursor let rowsave = line('.') @@ -46,6 +41,7 @@ function! s:GetBlockEnd(start) return endblock endfunction +" returns [contents of a code block] function! s:GetBlockContents(start, end) " i.e. if there's no end if a:end ==# 0 @@ -56,47 +52,123 @@ function! s:GetBlockContents(start, end) return retval endfunction +" returns all code in the current buffer in the form +" {'interpreter': {'file1': [line1, line2], 'file2': [line1, line2]}...} function! s:GetAllCode() - let codelines = [] - let endline = line("$") + let codelines = {} + + " The current files set for various interpreters + let curfiles = {} + + " The interpreter specified in the most recent Tangle directive + let last_set_interp = s:ALL_INTERP let curline = 1 + let endline = line("$") + + " Loop through lines while curline <=# endline - let block_pos_on_this_line = match(getline(curline), s:codeblock_start) + " If this line has a Tangle directive + if match(getline(curline), s:tangle_directive) >=# 0 + + " Try to parse the directive + let parsedline = s:ParseTangleDirective(curline) + if empty(parsedline) + return {} + endif + let [last_set_interp, curfile] = parsedline - if block_pos_on_this_line >=# 0 - let block_contents = s:GetBlockContents(curline, s:GetBlockEnd(curline)) - if len(block_contents) ==# 0 - echoerr 'No end of block starting on line '.curline - return [] + " Change the current file for the interpreter + let curfiles[last_set_interp] = curfile + + " If the interpreter has already been specified + if has_key(codelines, last_set_interp) + " If the interpreter does not yet have any lines for this file + if !has_key(codelines[last_set_interp], curfile) + " Add it + let codelines[last_set_interp][curfile] = [] + endif + " If the interpreter already has lines for the file, don't do anything + " If the interpreter itself hasn't been specified yet else + " Add it + let codelines[last_set_interp] = {curfile: []} + endif + " Go to next line + let curline += 1 + else + " Find a block on this line + let block_pos_on_this_line = match(getline(curline), s:codeblock_start) + + " If there's a block, process it + if block_pos_on_this_line >=# 0 + " Get the contents of this block + let block_contents = s:GetBlockContents(curline, s:GetBlockEnd(curline)) + + if len(block_contents) ==# 0 + echoerr 'No end of block starting on line '.curline + return {} + endif + + " Find out the amount of leading indentation (using the first line) let nleadingspaces = matchend(block_contents[0], '^ \+') if nleadingspaces ==# -1 let nleadingspaces = 0 endif - if len(codelines) !=# 0 - call add(codelines, '') + + " Get the interpreter for this block + let block_interp = s:GetBlockInterpreter(curline) + if !empty(block_interp) + " Allow overriding all interpreters to a 'general' file: + " If the last Tangle directive didn't have an interpreter, direct + " all blocks to that file + if last_set_interp ==# "" + " Get the current file for 'all interpreters' + let curfile = curfiles[last_set_interp] + let curinterp = s:ALL_INTERP + " If the last Tangle directive specified an interpreter + else + " If the interpreter was specified in a Tangle directive, use its + " current file + if has_key(codelines, block_interp) + let curfile = curfiles[block_interp] + let curinterp = block_interp + " Otherwise, use the 'general' file if specified + elseif has_key(codelines, s:ALL_INTERP) + let curfile = curfiles[s:ALL_INTERP] + let curinterp = s:ALL_INTERP + endif + endif + + " Add the lines to the current file to the current interpreter, + " stripping leading indentation and appending a newline + if exists('curinterp') + call extend(codelines[curinterp][curfile], (map(block_contents, 'v:val['.nleadingspaces.':]')+[''])) + endif endif - call extend(codelines, map(block_contents, 'v:val['.nleadingspaces.':]')) + " Skip to after the block let curline += len(block_contents)+2 + " Otherwise, go to the next line + else + let curline += 1 endif - else - let curline += 1 endif endwhile return codelines endfunction +" Write [lines] to fname and open it in a split function! s:SaveLines(lines, fname) if writefile(a:lines, a:fname) ==# 0 - exe 'drop '.a:fname + exe 'split '.a:fname else echoerr "Could not write to file ".a:fname endif endfunction +" Returns the interpreter name for a programming language function! s:Lang2Interpreter(lang) let lang = a:lang if exists('g:literate_markdown_interpreters') @@ -129,6 +201,7 @@ function! s:Lang2Interpreter(lang) return '' endfunction +" Gets the interpreter name for a code block function! s:GetBlockInterpreter(blockstart) " A markdown block beginning looks like this: ```lang let lang = getline(a:blockstart)[3:] @@ -204,13 +277,28 @@ function! literate_markdown#ExecPreviousBlock() endfunction function! literate_markdown#Tangle() - let fname = s:GetFilename() - if fname ==# '' - return - endif + " Get all of the code blocks in the file let lines = s:GetAllCode() + + " If there's any, tangle it if len(lines) ># 0 - call s:SaveLines(lines, fname) + " Merge lines from all interpreters into the files + let all_interps_combined = {} + for fname_and_lines in lines->values() + for [fname, flines] in fname_and_lines->items() + if all_interps_combined->has_key(fname) + call extend(all_interps_combined[fname], flines) + else + let all_interps_combined[fname] = flines + endif + endfor + endfor + + " Loop through the filenames and corresponding code + for [fname, flines] in items(all_interps_combined) + " Write the code to the respective file + call s:SaveLines(flines, fname) + endfor endif endfunction