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

literate_markdown.vim (18004B)


      1 
      2 if exists('g:loaded_literate_markdown_autoload')
      3   finish
      4 endif
      5 let g:loaded_literate_markdown_autoload = 1
      6 
      7 
      8 let s:codeblock_start = '^ *```[a-z]\+'
      9 let s:codeblock_end = '^ *```$'
     10 
     11 let s:tangle_directive = '^\s*<!-- *:Tangle'
     12 
     13 let s:ALL_INTERP = ''
     14 
     15 let s:result_comment_start = '<!--\nRESULT:'
     16 let s:result_comment_end = '^-->'
     17 
     18 
     19 " Write [lines] to fname and open it in a split
     20 function! s:SaveLines(lines, fname)
     21   if writefile(a:lines, a:fname) ==# 0
     22     if !exists('g:literate_markdown_no_open_tangled_files')
     23         exe 'split '.a:fname
     24     endif
     25   else
     26     echoerr "Could not write to file ".a:fname
     27   endif
     28 endfunction
     29 
     30 " returns end line of a code block
     31 function! s:GetBlockEnd(start)
     32   " Save the cursor
     33   let rowsave = line('.')
     34   let colsave = col('.')
     35 
     36   " search() starts from cursor
     37   call cursor(a:start, 1)
     38 
     39   " nW == don't move cursor, no wrap search
     40   let endblock = search(s:codeblock_end, 'nW')
     41   call cursor(rowsave, colsave)
     42   return endblock
     43 endfunction
     44 
     45 " returns [contents of a code block]
     46 function! s:GetBlockContents(start, end)
     47   " i.e. if there's no end
     48   if a:end ==# 0
     49     let retval = []
     50   else
     51     let retval = getline(a:start+1, a:end-1)
     52   endif
     53   return retval
     54 endfunction
     55 
     56 " Returns the interpreter name for a programming language
     57 function! s:Lang2Interpreter(lang)
     58   let lang = a:lang
     59   if exists('g:literate_markdown_interpreters')
     60     for [interp, langnames] in items(g:literate_markdown_interpreters)
     61       if index(langnames, lang) >= 0
     62         return interp
     63       endif
     64     endfor
     65   endif
     66   if exists('b:literate_markdown_interpreters')
     67     for [interp, langnames] in items(b:literate_markdown_interpreters)
     68       if index(langnames, lang) >= 0
     69         return interp
     70       endif
     71     endfor
     72   endif
     73 
     74   let lang2interp = {
     75         \ 'python3': ['py', 'python', 'python3'],
     76         \ 'python2': ['python2'],
     77         \ 'ruby': ['rb', 'ruby'],
     78         \ 'sh': ['sh'],
     79         \ 'bash': ['bash'],
     80         \ 'cat /tmp/program.c && gcc /tmp/program.c -o /tmp/program && /tmp/program': ['c'],
     81         \ }
     82   for [interp, langnames] in items(lang2interp)
     83     if index(langnames, lang) >= 0
     84       return interp
     85     endif
     86   endfor
     87   return ''
     88 endfunction
     89 
     90 function! s:GetBlockLang(blockstart)
     91   let lang = getline(a:blockstart)[3:]
     92   return lang
     93 endfunction
     94 
     95 " Gets the interpreter name for a code block
     96 function! s:GetBlockInterpreter(blockstart)
     97   " A markdown block beginning looks like this: ```lang
     98   let lang = s:GetBlockLang(a:blockstart)
     99   if empty(lang)
    100     return ''
    101   endif
    102 
    103   let interp = s:Lang2Interpreter(lang)
    104 
    105   if empty(interp)
    106     let interp = lang
    107   endif
    108   return interp
    109 endfunction
    110 
    111 
    112 
    113 function! s:ParseTangleDirective(ln)
    114   let theline = getline(a:ln)->matchlist('\v^\s*\<!-- :Tangle(\([a-zA-Z0-9]*\))* (\<\^?\>)? ?(\<[^>]+\>\+?)? ?(.*)? --\>')[1:4]
    115   if empty(theline)
    116     throw 'Cannot parse tangle directive on line ' .. a:ln
    117   endif
    118   let [interp, should_expand_macros, macro_group, fname] = theline
    119 
    120   if empty(should_expand_macros) && empty(macro_group) && empty(fname)
    121     throw 'No filename in tangle directive on line ' .. a:ln
    122   endif
    123 
    124   if !empty(fname)
    125     if fname[0] !=# '/'
    126       let fname = expand("%:p:h") . '/' . fname
    127     endif
    128     let fname = fnameescape(fname)
    129   endif
    130   let theinterp = s:Lang2Interpreter(interp[1:-2])
    131   if empty(theinterp)
    132     let theinterp = interp[1:-2]
    133   endif
    134   return [theinterp, should_expand_macros, macro_group, fnameescape(fname)]
    135 endfunction
    136 
    137 function! s:AddBlock(interps_files, block, block_start_line, last_set_interp, curfiles)
    138   let interps_files = a:interps_files
    139 
    140   if type(a:block) ==# v:t_dict
    141     let block_contents = a:block['contents']
    142   else
    143     let block_contents = a:block
    144   endif
    145 
    146   " Find out the amount of leading indentation (using the first line)
    147   " TODO: this should be the least indented line
    148   let nleadingspaces = matchend(block_contents[0], '^ \+')
    149   if nleadingspaces ==# -1
    150     let nleadingspaces = 0
    151   endif
    152 
    153   " Get the interpreter for this block
    154   let block_interp = s:GetBlockInterpreter(a:block_start_line)
    155   if empty(block_interp)
    156     let block_interp = s:GetBlockLang(a:block_start_line)
    157   endif
    158   if !empty(block_interp)
    159     " Allow overriding all interpreters to a 'general' file:
    160     " If the last Tangle directive didn't have an interpreter, direct
    161     " all blocks to that file
    162     if a:last_set_interp ==# s:ALL_INTERP && has_key(a:curfiles, s:ALL_INTERP)
    163       " Get the current file for 'all interpreters'
    164       let curfile = a:curfiles[s:ALL_INTERP]
    165       let curinterp = s:ALL_INTERP
    166       " If the last Tangle directive specified an interpreter
    167     else
    168       " If the interpreter was specified in a Tangle directive, use its
    169       " current file
    170       if has_key(interps_files, block_interp)
    171         let curfile = a:curfiles[block_interp]
    172         let curinterp = block_interp
    173         " Otherwise, use the 'general' file if specified
    174       elseif has_key(interps_files, s:ALL_INTERP)
    175         let curfile = a:curfiles[s:ALL_INTERP]
    176         let curinterp = s:ALL_INTERP
    177       endif
    178     endif
    179 
    180     " Add the lines to the current file to the current interpreter,
    181     " stripping leading indentation and appending a newline
    182     if exists('curinterp')
    183       if type(a:block) ==# v:t_dict
    184         call add(interps_files[curinterp][curfile], {'expand': a:block['expand'], 'contents': (map(block_contents, 'v:val['.nleadingspaces.':]')+[''])})
    185       else
    186         call extend(interps_files[curinterp][curfile], (map(block_contents, 'v:val['.nleadingspaces.':]')+['']))
    187       endif
    188     endif
    189   endif
    190 
    191   return interps_files
    192 endfunction
    193 
    194 function! s:ProcessMacroExpansions(lines, macros)
    195   let final_lines = {}
    196   for [interp, fnames] in a:lines->items()
    197     if has_key(a:macros, interp)
    198       for [fname, flines] in fnames->items()
    199         if has_key(a:macros[interp], fname)
    200           if !has_key(a:macros[interp][fname], 'toplevel')
    201             throw "Macros exist, but no top-level structure defined for file " .. fname
    202           endif
    203 
    204           let toplevel = a:macros[interp][fname]['toplevel']
    205           let lines_here = []
    206           for line in toplevel
    207             if line->trim()->match('<<[^>]\+>>') >=# 0
    208               call extend(lines_here, s:ExpandMacro(a:macros, interp, fname, line))
    209             else
    210               call add(lines_here, line)
    211             endif
    212           endfor
    213 
    214           if !has_key(final_lines, interp)
    215             let final_lines[interp] = {fname: lines_here}
    216           else
    217             let final_lines[interp][fname] = lines_here
    218           endif
    219         else
    220           if !has_key(final_lines, interp)
    221             let final_lines[interp] = {fname: a:lines[interp][fname]}
    222           else
    223             let final_lines[interp][fname] = a:lines[interp][fname]
    224           endif
    225         endif
    226       endfor
    227     else
    228       let final_lines[interp] = a:lines[interp]
    229     endif
    230   endfor
    231   return final_lines
    232 endfunction
    233 
    234 function! s:ExpandMacro(macros, interp, fname, line)
    235   let nleadingspaces = matchend(a:line, '^ \+')
    236   if nleadingspaces ==# -1
    237     let nleadingspaces = 0
    238   endif
    239 
    240   let macro_tag = trim(a:line)[2:-3]
    241   let expanded = []
    242   if !has_key(a:macros[a:interp][a:fname]['macros'], macro_tag)
    243     throw "Macro " .. macro_tag .. " not defined for file " .. a:fname
    244   endif
    245 
    246   let expansion = a:macros[a:interp][a:fname]['macros'][macro_tag]
    247   if type(expansion) ==# v:t_dict
    248     let expansion = [expansion]
    249   endif
    250   for expanded_line in expansion
    251     if type(expanded_line) ==# v:t_dict && expanded_line['expand']
    252       for l in expanded_line['contents']
    253         if l->trim()->match('<<[^>]\+>>') >=# 0
    254           call extend(expanded, s:ExpandMacro(a:macros, a:interp, a:fname, repeat(" ", nleadingspaces)..l))
    255         else
    256           call add(expanded, repeat(" ", nleadingspaces)..l)
    257         endif
    258       endfor
    259     else
    260       call add(expanded, repeat(" ", nleadingspaces)..expanded_line)
    261     endif
    262   endfor
    263 
    264   return expanded
    265 endfunction
    266 
    267 function! s:GetAllCode()
    268   
    269   " The current files set for various interpreters
    270   let curfiles = {}
    271   
    272   " Finalized code, by interpreter and file
    273   let interps_files = {}
    274   
    275   let last_set_interp = s:ALL_INTERP
    276   
    277   let macros = {}
    278 
    279   let curline = 1
    280   let endline = line("$")
    281 
    282   " Loop through lines
    283   while curline <=# endline
    284     
    285     " If this line has a Tangle directive
    286     if match(getline(curline), s:tangle_directive) >=# 0
    287       
    288       " Try to parse the directive
    289       let parsedline = s:ParseTangleDirective(curline)
    290       let [last_set_interp, should_expand_macros, macro_group, curfile] = parsedline
    291       
    292       " Process file and interpreter declaration
    293       if !empty(curfile)
    294         " Change the current file for the interpreter
    295         let curfiles[last_set_interp] = curfile
    296       
    297         " Process interpreter declaration
    298         " If the interpreter has already been specified
    299         if has_key(interps_files, last_set_interp)
    300           " If the interpreter does not yet have any lines for this file
    301           if !has_key(interps_files[last_set_interp], curfile)
    302             " Add it
    303             let interps_files[last_set_interp][curfile] = []
    304           endif
    305           " If the interpreter already has lines for the file, don't do anything
    306           " If the interpreter itself hasn't been specified yet
    307         else
    308           " Add it
    309           let interps_files[last_set_interp] = {curfile: []}
    310         endif
    311       endif
    312       
    313       if !empty(should_expand_macros) || !empty(macro_group)
    314         if getline(curline+1)->match(s:codeblock_start) ==# -1
    315           throw "Tangle directive specifies macros on line " .. curline .. " but no code block follows."
    316         endif
    317         let block_contents = s:GetBlockContents(curline+1, s:GetBlockEnd(curline+1))
    318         let block_interp = s:GetBlockInterpreter(curline+1)
    319       
    320         if empty(block_interp)
    321           throw ("Macro expansion defined, but no block language set on line " .. (curline+1))
    322         endif
    323       
    324         " If the last set interpreter was generic, it should override all blocks
    325         if last_set_interp ==# s:ALL_INTERP
    326           let block_interp = s:ALL_INTERP
    327         endif
    328       
    329         
    330         " Process macro expansion
    331         " Top-level macros
    332         if !empty(should_expand_macros) && stridx(should_expand_macros, "^") >=# 0
    333           if !empty(macro_group)
    334             throw "Top-level macro block on line "  .. curline .. " cannot also belong to macro group."
    335           endif
    336         
    337           if has_key(curfiles, block_interp)
    338             let curfile = curfiles[block_interp]
    339           elseif has_key(curfiles, s:ALL_INTERP)
    340             let curfile = curfiles[s:ALL_INTERP]
    341           else
    342             throw "No current file set for block on line " .. curline+1
    343           endif
    344         
    345           if !has_key(macros, block_interp)
    346             let macros[block_interp] = {}
    347           endif
    348           if !has_key(macros[block_interp], curfile)
    349             let macros[block_interp][curfile] = {}
    350           endif
    351         
    352           if has_key(macros[block_interp][curfile], 'toplevel')
    353             throw "Duplicate top-level macro definition on line " .. curline
    354           endif
    355         
    356           " Add the current block as a top-level macro
    357           let macros[block_interp][curfile]['toplevel'] = block_contents
    358           " For regular macro expansion, just add the block
    359         endif
    360         
    361         " Potentially save block as macro
    362         if !empty(macro_group)
    363           " If extending an existing macro
    364           if !empty(should_expand_macros)
    365             let to_add = [{'expand': 1, 'contents': ['']+(block_contents) }]
    366           else
    367             let to_add = ['']+block_contents
    368           endif
    369         
    370           " If adding to an existing macro
    371           if stridx(macro_group, "+") ==# len(macro_group)-1
    372             let macro_tag = macro_group[1:-3]
    373             if empty(macro_tag)
    374               throw "Macro tag on line " .. curline .. " cannot be empty"
    375             endif
    376         
    377             if has_key(curfiles, block_interp)
    378               let curfile = curfiles[block_interp]
    379             elseif has_key(curfiles, s:ALL_INTERP)
    380               let curfile = curfiles[s:ALL_INTERP]
    381             else
    382               throw "No current file set for block on line " .. curline+1
    383             endif
    384         
    385             if !has_key(macros, block_interp)
    386                   \ || !has_key(macros[block_interp], curfile)
    387                   \ || !has_key(macros[block_interp][curfile], 'macros')
    388                   \ || !has_key(macros[block_interp][curfile]['macros'], macro_tag)
    389               throw "Requested to extend macro <" .. macro_tag .. "> on line " .. curline .. ", but it's not yet defined"
    390             endif
    391         
    392             if type(to_add) ==# v:t_dict
    393               call add(macros[block_interp][curfile]['macros'][macro_tag], to_add)
    394             else
    395               call extend(macros[block_interp][curfile]['macros'][macro_tag], to_add)
    396             endif
    397         
    398           " If defining a new macro
    399           else
    400             if has_key(curfiles, block_interp)
    401               let curfile = curfiles[block_interp]
    402             elseif has_key(curfiles, s:ALL_INTERP)
    403               let curfile = curfiles[s:ALL_INTERP]
    404             else
    405               throw "No current file set for block on line " .. curline+1
    406             endif
    407         
    408             let macro_tag = macro_group[1:-2]
    409             if empty(macro_tag)
    410               throw "Macro tag on line " .. curline .. " cannot be empty"
    411             endif
    412         
    413             if !has_key(macros, block_interp)
    414               let macros[block_interp] = {}
    415             endif
    416             if !has_key(macros[block_interp], curfile)
    417               let macros[block_interp][curfile] = {}
    418             endif
    419         
    420             if has_key(macros[block_interp][curfile], 'macros') && has_key(macros[block_interp][curfile]['macros'], macro_tag)
    421               throw "Duplicate definition of macro tag <" .. macro_tag .. "> on line " .. curline
    422             endif
    423         
    424             if has_key(macros[block_interp][curfile], 'macros')
    425               let macros[block_interp][curfile]['macros'][macro_tag] = to_add
    426             else
    427               let macros[block_interp][curfile]['macros'] = {macro_tag: to_add}
    428             endif
    429           endif
    430         endif
    431       
    432         " When processing macros, we process the block also, so move the
    433         " cursor after it
    434         let curline += len(block_contents)+2
    435       endif
    436     
    437       " Go to next line
    438       let curline += 1
    439     else
    440       " Find a block on this line
    441       let block_pos_on_this_line = match(getline(curline), s:codeblock_start)
    442     
    443       " If there's a block, process it
    444       if block_pos_on_this_line >=# 0
    445         
    446         " Get the contents of this block
    447         let block_contents = s:GetBlockContents(curline, s:GetBlockEnd(curline))
    448         
    449         if len(block_contents) ==# 0
    450           throw 'No end of block starting on line '.curline
    451         endif
    452         
    453         let interps_files = s:AddBlock(interps_files, block_contents, curline, last_set_interp, curfiles)
    454         
    455         " Skip to after the block
    456         let curline += len(block_contents)+2
    457     
    458       " Otherwise, go to the next line
    459       else
    460         let curline += 1
    461       endif
    462     endif
    463   endwhile
    464 
    465   
    466   if exists('g:literate_markdown_debug')
    467     echomsg interps_files
    468     echomsg macros
    469   endif
    470   return [interps_files, macros]
    471 endfunction
    472 
    473 
    474 function! s:GetResultLine(blockend)
    475   let rowsave = line('.')
    476   let colsave = col('.')
    477   call cursor(a:blockend, 1)
    478   let nextblock = search(s:codeblock_start, 'nW')
    479   let linenum = search(s:result_comment_start, 'cnW', nextblock)
    480   call cursor(rowsave, colsave)
    481 
    482   if linenum == 0
    483     call append(a:blockend, ['', '<!--', 'RESULT:', '', '-->', ''])
    484     let linenum = a:blockend+2
    485   endif
    486   return linenum+1
    487 endfunction
    488 
    489 function! s:ClearResult(outputline)
    490   let rowsave = line('.')
    491   let colsave = col('.')
    492   call cursor(a:outputline, 1)
    493   let resultend = search(s:result_comment_end, 'nW')
    494   if resultend ==# 0
    495     throw 'Result block has no end'
    496   else
    497     execute a:outputline.','.resultend.'delete _'
    498   endif
    499   call cursor(rowsave, colsave)
    500 endfunction
    501 
    502 
    503 function! literate_markdown#Tangle()
    504   " Get all of the code blocks in the file
    505   try
    506     let [lines, macros] = s:GetAllCode()
    507 
    508     if !empty(macros)
    509       let lines = s:ProcessMacroExpansions(lines, macros)
    510     endif
    511 
    512 
    513     " If there's any, tangle it
    514     if len(lines) ># 0
    515       
    516       " Merge lines from all interpreters into the files
    517       let all_interps_combined = {}
    518       for fname_and_lines in lines->values()
    519         for [fname, flines] in fname_and_lines->items()
    520           if all_interps_combined->has_key(fname)
    521             call extend(all_interps_combined[fname], flines)
    522           else
    523             let all_interps_combined[fname] = flines
    524           endif
    525         endfor
    526       endfor
    527 
    528       
    529       " Loop through the filenames and corresponding code
    530       for [fname, flines] in items(all_interps_combined)
    531         " Write the code to the respective file
    532         call s:SaveLines(flines, fname)
    533       endfor
    534     endif
    535   catch
    536     echohl Error
    537     echomsg "Error: " .. v:exception .. " (from " .. v:throwpoint .. ")"
    538     echohl None
    539   endtry
    540 endfunction
    541 
    542 function! literate_markdown#ExecPreviousBlock()
    543   let blockstart = search(s:codeblock_start, 'nbW')
    544   if blockstart == 0
    545     throw 'No previous block found'
    546   endif
    547 
    548   let blockend = s:GetBlockEnd(blockstart)
    549 
    550   if blockend == 0
    551     throw 'No end for block'
    552   endif
    553 
    554   let interp = s:GetBlockInterpreter(blockstart)
    555   if empty(interp)
    556     throw 'No interpreter specified for block'
    557   endif
    558 
    559   let block_contents = s:GetBlockContents(blockstart, blockend)
    560 
    561   " TODO: This here will need to be different if accounting for state
    562   " (try channels? jobs? hidden term? other options?)
    563   let result_lines = systemlist(interp, block_contents)
    564 
    565   let outputline = s:GetResultLine(blockend)
    566   call s:ClearResult(outputline)
    567   call append(outputline-1, ['RESULT:'] + result_lines + ['-->'])
    568 endfunction