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 d2829e7cde8c6b27a40514f7d610d30c7a563d62
parent 080700f583d8c9b3a3322c6e628ca8acbfc87c74
Author: Alex Balgavy <alex@balgavy.eu>
Date:   Sat, 26 Feb 2022 10:52:33 +0100

commit

Diffstat:
Mautoload/literate_markdown.vim | 408++++++++++++++++++++++++++++++++++++++++++-------------------------------------
1 file changed, 215 insertions(+), 193 deletions(-)

diff --git a/autoload/literate_markdown.vim b/autoload/literate_markdown.vim @@ -1,33 +1,28 @@ + if exists('g:loaded_literate_markdown_autoload') finish endif let g:loaded_literate_markdown_autoload = 1 + let s:codeblock_start = '^ *```[a-z]\+' let s:codeblock_end = '^ *```$' -let s:tangle_directive = '<!-- *:Tangle' -let s:result_comment_start = '<!--\nRESULT:' -let s:result_comment_end = '^-->' + +let s:tangle_directive = '^\s*<!-- *:Tangle' + let s:ALL_INTERP = '' -function! s:ParseTangleDirective(ln) - let theline = getline(a:ln)->matchlist('\v:Tangle(\([a-zA-Z0-9]*\))* (\<\^?\>)? ?(\<[^>]+\>\+?)? ?(.*)? --\>')[1:4] - if empty(theline) - throw 'Cannot parse tangle directive on line ' .. a:ln - endif - let [interp, should_expand_macros, macro_group, fname] = theline +let s:result_comment_start = '<!--\nRESULT:' +let s:result_comment_end = '^-->' - if empty(should_expand_macros) && empty(macro_group) && empty(fname) - throw 'No filename in tangle directive on line ' .. a:ln - endif - if !empty(fname) - if fname[0] !=# '/' - let fname = expand("%:p:h") . '/' . fname - endif - let fname = fnameescape(fname) +" Write [lines] to fname and open it in a split +function! s:SaveLines(lines, fname) + if writefile(a:lines, a:fname) ==# 0 + exe 'split '.a:fname + else + echoerr "Could not write to file ".a:fname endif - return [s:Lang2Interpreter(interp[1:-2]), should_expand_macros, macro_group, fnameescape(fname)] endfunction " returns end line of a code block @@ -56,6 +51,85 @@ function! s:GetBlockContents(start, end) return retval endfunction +" Returns the interpreter name for a programming language +function! s:Lang2Interpreter(lang) + let lang = a:lang + if exists('g:literate_markdown_interpreters') + for [interp, langnames] in items(g:literate_markdown_interpreters) + if index(langnames, lang) >= 0 + return interp + endif + endfor + endif + if exists('b:literate_markdown_interpreters') + for [interp, langnames] in items(b:literate_markdown_interpreters) + if index(langnames, lang) >= 0 + return interp + endif + endfor + endif + + let lang2interp = { + \ 'python3': ['py', 'python', 'python3'], + \ 'python2': ['python2'], + \ 'ruby': ['rb', 'ruby'], + \ 'sh': ['sh'], + \ 'bash': ['bash'], + \ 'cat /tmp/program.c && gcc /tmp/program.c -o /tmp/program && /tmp/program': ['c'], + \ } + for [interp, langnames] in items(lang2interp) + if index(langnames, lang) >= 0 + return interp + endif + endfor + return '' +endfunction + +function! s:GetBlockLang(blockstart) + let lang = getline(a:blockstart)[3:] + return lang +endfunction + +" Gets the interpreter name for a code block +function! s:GetBlockInterpreter(blockstart) + " A markdown block beginning looks like this: ```lang + let lang = s:GetBlockLang(a:blockstart) + if empty(lang) + return '' + endif + + let interp = s:Lang2Interpreter(lang) + + return interp +endfunction + + + +function! s:ParseTangleDirective(ln) + let theline = getline(a:ln)->matchlist('\v^\s*\<!-- :Tangle(\([a-zA-Z0-9]*\))* (\<\^?\>)? ?(\<[^>]+\>\+?)? ?(.*)? --\>')[1:4] + if empty(theline) + throw 'Cannot parse tangle directive on line ' .. a:ln + endif + let [interp, should_expand_macros, macro_group, fname] = theline + + if empty(should_expand_macros) && empty(macro_group) && empty(fname) + throw 'No filename in tangle directive on line ' .. a:ln + endif + + if !empty(fname) + if fname[0] !=# '/' + let fname = expand("%:p:h") . '/' . fname + endif + let fname = fnameescape(fname) + endif + + let theinterp = s:Lang2Interpreter(interp[1:-2]) + if empty(theinterp) + let theinterp = interp[1:-2] + endif + return [theinterp, should_expand_macros, macro_group, fnameescape(fname)] +endfunction + function! s:AddBlock(interps_files, block, block_start_line, last_set_interp, curfiles) let interps_files = a:interps_files @@ -74,6 +148,9 @@ function! s:AddBlock(interps_files, block, block_start_line, last_set_interp, cu " Get the interpreter for this block let block_interp = s:GetBlockInterpreter(a:block_start_line) + if empty(block_interp) + let block_interp = s:GetBlockLang(a:block_start_line) + endif if !empty(block_interp) " Allow overriding all interpreters to a 'general' file: " If the last Tangle directive didn't have an interpreter, direct @@ -82,7 +159,7 @@ function! s:AddBlock(interps_files, block, block_start_line, last_set_interp, cu " Get the current file for 'all interpreters' let curfile = a:curfiles[s:ALL_INTERP] let curinterp = s:ALL_INTERP - " If the last Tangle directive specified an interpreter + " If the last Tangle directive specified an interpreter else " If the interpreter was specified in a Tangle directive, use its " current file @@ -109,31 +186,98 @@ function! s:AddBlock(interps_files, block, block_start_line, last_set_interp, cu return interps_files endfunction -" returns all code in the current buffer in the form -" {'interpreter': {'file1': [line1, line2], 'file2': [line1, line2]}...} -function! s:GetAllCode() - " Finalized code, by interpreter and file - let interps_files = {} - " Code with macros to be expanded, by interpreter and file - " { 'c': [ - " 'output.c': { - " 'toplevel': ['<<whatever>>', ..], - " 'macros': {'<<whatever>>': ["printf('Hello')", ...]}}} +function! s:ProcessMacroExpansions(lines, macros) + let final_lines = {} + for [interp, fnames] in a:lines->items() + if has_key(a:macros, interp) + for [fname, flines] in fnames->items() + if has_key(a:macros[interp], fname) + if !has_key(a:macros[interp][fname], 'toplevel') + throw "Macros exist, but no top-level structure defined for file " .. fname + endif - let macros = {} + let toplevel = a:macros[interp][fname]['toplevel'] + let lines_here = [] + for line in toplevel + if line->trim()->match('<<[^>]\+>>') >=# 0 + call extend(lines_here, s:ExpandMacro(a:macros, interp, fname, line)) + else + call add(lines_here, line) + endif + endfor + + if !has_key(final_lines, interp) + let final_lines[interp] = {fname: lines_here} + else + let final_lines[interp][fname] = lines_here + endif + else + if !has_key(final_lines, interp) + let final_lines[interp] = {fname: a:lines[interp][fname]} + else + let final_lines[interp][fname] = a:lines[interp][fname] + endif + endif + endfor + else + let final_lines[interp] = a:lines[interp] + endif + endfor + return final_lines +endfunction + +function! s:ExpandMacro(macros, interp, fname, line) + let nleadingspaces = matchend(a:line, '^ \+') + if nleadingspaces ==# -1 + let nleadingspaces = 0 + endif + + let macro_tag = trim(a:line)[2:-3] + let expanded = [] + if !has_key(a:macros[a:interp][a:fname]['macros'], macro_tag) + throw "Macro " .. macro_tag .. " not defined for file " .. a:fname + endif + + let expansion = a:macros[a:interp][a:fname]['macros'][macro_tag] + if type(expansion) ==# v:t_dict + let expansion = [expansion] + endif + for expanded_line in expansion + if type(expanded_line) ==# v:t_dict && expanded_line['expand'] + for l in expanded_line['contents'] + if l->trim()->match('<<[^>]\+>>') >=# 0 + call extend(expanded, s:ExpandMacro(a:macros, a:interp, a:fname, repeat(" ", nleadingspaces)..l)) + else + call add(expanded, repeat(" ", nleadingspaces)..l) + endif + endfor + else + call add(expanded, repeat(" ", nleadingspaces)..expanded_line) + endif + endfor + + return expanded +endfunction + +function! s:GetAllCode() " The current files set for various interpreters let curfiles = {} - " The interpreter specified in the most recent Tangle directive + " Finalized code, by interpreter and file + let interps_files = {} + let last_set_interp = s:ALL_INTERP + let macros = {} + let curline = 1 let endline = line("$") " Loop through lines while curline <=# endline + " If this line has a Tangle directive if match(getline(curline), s:tangle_directive) >=# 0 @@ -162,10 +306,9 @@ function! s:GetAllCode() endif endif - if !empty(should_expand_macros) || !empty(macro_group) if getline(curline+1)->match(s:codeblock_start) ==# -1 - throw "Tangle directive specifies macros on line " .. curline .. "but no code block follows." + throw "Tangle directive specifies macros on line " .. curline .. " but no code block follows." endif let block_contents = s:GetBlockContents(curline+1, s:GetBlockEnd(curline+1)) let block_interp = s:GetBlockInterpreter(curline+1) @@ -174,10 +317,12 @@ function! s:GetAllCode() throw "Macro expansion defined, but no block language set on line " .. curline+1 endif + " If the last set interpreter was generic, it should override all blocks if last_set_interp ==# s:ALL_INTERP let block_interp = s:ALL_INTERP endif + " Process macro expansion " Top-level macros if !empty(should_expand_macros) && stridx(should_expand_macros, "^") >=# 0 @@ -199,6 +344,7 @@ function! s:GetAllCode() if !has_key(macros[block_interp], curfile) let macros[block_interp][curfile] = {} endif + if has_key(macros[block_interp][curfile], 'toplevel') throw "Duplicate top-level macro definition on line " .. curline endif @@ -212,10 +358,12 @@ function! s:GetAllCode() if !empty(macro_group) " If extending an existing macro if !empty(should_expand_macros) - let to_add = {'expand': 1, 'contents': block_contents} + let to_add = [{'expand': 1, 'contents': ['']+(block_contents) }] else - let to_add = block_contents + let to_add = ['']+block_contents endif + + " If adding to an existing macro if stridx(macro_group, "+") ==# len(macro_group)-1 let macro_tag = macro_group[1:-3] if empty(macro_tag) @@ -290,6 +438,7 @@ function! s:GetAllCode() " 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)) @@ -301,6 +450,7 @@ function! s:GetAllCode() " Skip to after the block let curline += len(block_contents)+2 + " Otherwise, go to the next line else let curline += 1 @@ -308,67 +458,10 @@ function! s:GetAllCode() endif endwhile - return [interps_files, macros] -endfunction - -" Write [lines] to fname and open it in a split -function! s:SaveLines(lines, fname) - if writefile(a:lines, a:fname) ==# 0 - 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') - for [interp, langnames] in items(g:literate_markdown_interpreters) - if index(langnames, lang) >= 0 - return interp - endif - endfor - endif - if exists('b:literate_markdown_interpreters') - for [interp, langnames] in items(b:literate_markdown_interpreters) - if index(langnames, lang) >= 0 - return interp - endif - endfor - endif - - let lang2interp = { - \ 'python3': ['py', 'python', 'python3'], - \ 'python2': ['python2'], - \ 'ruby': ['rb', 'ruby'], - \ 'sh': ['sh'], - \ 'bash': ['bash'], - \ 'cat /tmp/program.c && gcc /tmp/program.c -o /tmp/program && /tmp/program': ['c'], - \ } - for [interp, langnames] in items(lang2interp) - if index(langnames, lang) >= 0 - return interp - endif - endfor - return '' + return [interps_files, macros] 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:] - if empty(lang) - return '' - endif - - let interp = s:Lang2Interpreter(lang) - if empty(interp) - throw 'No interpreter configured for language ' . lang - endif - - return interp -endfunction function! s:GetResultLine(blockend) let rowsave = line('.') @@ -398,106 +491,6 @@ function! s:ClearResult(outputline) call cursor(rowsave, colsave) endfunction -function! literate_markdown#ExecPreviousBlock() - let blockstart = search(s:codeblock_start, 'nbW') - if blockstart == 0 - throw 'No previous block found' - endif - - let blockend = s:GetBlockEnd(blockstart) - - if blockend == 0 - throw 'No end for block' - endif - - let interp = s:GetBlockInterpreter(blockstart) - if empty(interp) - throw 'No interpreter specified for block' - endif - - let block_contents = s:GetBlockContents(blockstart, blockend) - - " TODO: This here will need to be different if accounting for state - " (try channels? jobs? hidden term? other options?) - let result_lines = systemlist(interp, block_contents) - - let outputline = s:GetResultLine(blockend) - call s:ClearResult(outputline) - call append(outputline-1, ['RESULT:'] + result_lines + ['-->']) -endfunction - -function! s:ExpandMacro(macros, interp, fname, line) - let nleadingspaces = matchend(a:line, '^ \+') - if nleadingspaces ==# -1 - let nleadingspaces = 0 - endif - - let macro_tag = trim(a:line)[2:-3] - let expanded = [] - if !has_key(a:macros[a:interp][a:fname]['macros'], macro_tag) - throw "Macro " .. macro_tag .. " not defined for file " .. a:fname - endif - - let expansion = a:macros[a:interp][a:fname]['macros'][macro_tag] - if type(expansion) ==# v:t_dict - let expansion = [expansion] - endif - for expanded_line in expansion - if type(expanded_line) ==# v:t_dict && expanded_line['expand'] - for l in expanded_line['contents'] - if l->trim()->match('<<[^>]\+>>') >=# 0 - call extend(expanded, s:ExpandMacro(a:macros, a:interp, a:fname, repeat(" ", nleadingspaces)..l)) - else - call add(expanded, repeat(" ", nleadingspaces)..l) - endif - endfor - else - call add(expanded, repeat(" ", nleadingspaces)..expanded_line) - endif - endfor - - return expanded -endfunction - -function! s:ProcessMacroExpansions(lines, macros) - let final_lines = {} - for [interp, fnames] in a:lines->items() - if has_key(a:macros, interp) - for [fname, flines] in fnames->items() - if has_key(a:macros[interp], fname) - if !has_key(a:macros[interp][fname], 'toplevel') - throw "Macros exist, but no top-level structure defined for file " .. fname - endif - - let toplevel = a:macros[interp][fname]['toplevel'] - let lines_here = [] - for line in toplevel - if line->trim()->match('<<[^>]\+>>') >=# 0 - call extend(lines_here, s:ExpandMacro(a:macros, interp, fname, line)) - else - call add(lines_here, line) - endif - endfor - - if !has_key(final_lines, interp) - let final_lines[interp] = {fname: lines_here} - else - let final_lines[interp][fname] = lines_here - endif - else - if !has_key(final_lines, interp) - let final_lines[interp] = {fname: a:lines[interp][fname]} - else - let final_lines[interp][fname] = a:lines[interp][fname] - endif - endif - endfor - else - let final_lines[interp] = a:lines[interp] - endif - endfor - return final_lines -endfunction function! literate_markdown#Tangle() " Get all of the code blocks in the file @@ -511,6 +504,7 @@ function! literate_markdown#Tangle() " If there's any, tangle it if len(lines) ># 0 + " Merge lines from all interpreters into the files let all_interps_combined = {} for fname_and_lines in lines->values() @@ -523,6 +517,7 @@ function! literate_markdown#Tangle() endfor endfor + " Loop through the filenames and corresponding code for [fname, flines] in items(all_interps_combined) " Write the code to the respective file @@ -534,3 +529,30 @@ function! literate_markdown#Tangle() endtry endfunction +function! literate_markdown#ExecPreviousBlock() + let blockstart = search(s:codeblock_start, 'nbW') + if blockstart == 0 + throw 'No previous block found' + endif + + let blockend = s:GetBlockEnd(blockstart) + + if blockend == 0 + throw 'No end for block' + endif + + let interp = s:GetBlockInterpreter(blockstart) + if empty(interp) + throw 'No interpreter specified for block' + endif + + let block_contents = s:GetBlockContents(blockstart, blockend) + + " TODO: This here will need to be different if accounting for state + " (try channels? jobs? hidden term? other options?) + let result_lines = systemlist(interp, block_contents) + + let outputline = s:GetResultLine(blockend) + call s:ClearResult(outputline) + call append(outputline-1, ['RESULT:'] + result_lines + ['-->']) +endfunction