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_autoload.md (26393B)


      1 <!-- :Tangle(vim) ../autoload/literate_markdown.vim -->
      2 # vim-literate-markdown: autoloaded functions
      3 This document specifies the functionality of the vim-literate-markdown plugin.
      4 While in the case of a larger project like this, it is perhaps not ideal and may arguably hinder readability, it serves as a proof-of-concept to showcase the power of this plugin.
      5 
      6 Commands and bindings are defined in the [ftplugin](literate_markdown_ftplugin.md).
      7 
      8 Todo:
      9 - shouldn't be able to specify both `<^>` and `<>`
     10 - shouldn't allow two different top-level `<^>` (I think), unless tangling to different files
     11 - order in tangle directive should be more or less arbitrary? need to define a grammar.
     12 - squash whitespace where needed, see tests
     13 
     14 The general structure of the file is:
     15 
     16 <!-- :Tangle(vim) <^> -->
     17 ```vim
     18 <<load guard>>
     19 
     20 <<constants>>
     21 
     22 <<general utility functions>>
     23 
     24 <<tangle-related functions>>
     25 
     26 <<code execution-related functions>>
     27 
     28 <<API functions>>
     29 ```
     30 
     31 The load guard lets the user disable the autoloaded functions by setting the variable `g:loaded_literate_markdown_autoload`.
     32 If it's set, the entire file is skipped.
     33 
     34 <!-- :Tangle(vim) <load guard> -->
     35 ```vim
     36 if exists('g:loaded_literate_markdown_autoload')
     37   finish
     38 endif
     39 let g:loaded_literate_markdown_autoload = 1
     40 ```
     41 
     42 ## Tangling
     43 Tangling is when you combine several blocks of code from a literate file into one or more executable files.
     44 A code block in markdown is delimited by three backticks at the start (followed optionally by the name of a language), and three backticks at the end.
     45 We'll only tangle code that includes a language, because if there's no language set, the code may not be executable.
     46 
     47 Let's define the start and end delimiters as variables containing regular expressions:
     48 
     49 <!-- :Tangle(vim) <constants> -->
     50 ```vim
     51 let s:codeblock_start = '^ *```[a-z]\+'
     52 let s:codeblock_end = '^ *```$'
     53 ```
     54 
     55 Now, to start tangling, we need to process the text in the buffer, line-by-line:
     56 
     57 <!-- :Tangle(vim) <> <tangle-related functions> -->
     58 ```vim
     59 <<tangle helper functions>>
     60 
     61 function! s:GetAllCode()
     62   <<persistent variables>>
     63 
     64   let curline = 1
     65   let endline = line("$")
     66 
     67   " Loop through lines
     68   while curline <=# endline
     69     <<process a line>>
     70   endwhile
     71 
     72   <<return what's needed>>
     73 endfunction
     74 ```
     75 
     76 There are two types of lines we care about in the buffer:
     77 * the start of a code block (delimited by three backticks and a language name), as defined above
     78 * a tangle directive
     79 
     80 A tangle directive is a Markdown (HTML) comment, starting with the string `:Tangle` and containing some options.
     81 When we're matching a tangle directive, we're looking for this string:
     82 
     83 <!-- :Tangle(vim) <constants>+ -->
     84 ```vim
     85 let s:tangle_directive = '^\s*<!-- *:Tangle'
     86 ```
     87 
     88 So this is how we decide which way to process a particular line:
     89 
     90 <!-- :Tangle(vim) <> <process a line> -->
     91 ```vim
     92 " If this line has a Tangle directive
     93 if match(getline(curline), s:tangle_directive) >=# 0
     94   <<parse the tangle directive>>
     95 
     96   " Go to next line
     97   let curline += 1
     98 else
     99   " Find a block on this line
    100   let block_pos_on_this_line = match(getline(curline), s:codeblock_start)
    101 
    102   " If there's a block, process it
    103   if block_pos_on_this_line >=# 0
    104     <<process the block>>
    105 
    106   " Otherwise, go to the next line
    107   else
    108     let curline += 1
    109   endif
    110 endif
    111 ```
    112 
    113 ### Processing a tangle directive
    114 First we take the current line, containing the tangle directive, and split it into parts.
    115 
    116 <!-- :Tangle(vim) <parse the tangle directive> -->
    117 ```vim
    118 " Try to parse the directive
    119 let parsedline = s:ParseTangleDirective(curline)
    120 let [last_set_interp, should_expand_macros, macro_group, curfile] = parsedline
    121 ```
    122 
    123 The directive looks like this: `<!-- :Tangle(language) <> <macro name> /path/to/file -->`.
    124 The language is optional; if it's not specified, the block is tangled into a 'generic' file (specified in `/path/to/file`).
    125 The diamond (`<>`) is optional, and if it's included, it means that the block contains additional macros that should also be expanded.
    126 The macro name is also optional, and if it's included, it means that the following block defines a macro of that name.
    127 To parse the directive, we use a helper function that takes the line and splits it into the four (potentially empty) parts:
    128 
    129 <!-- :Tangle(vim) <tangle helper functions> -->
    130 ```vim
    131 function! s:ParseTangleDirective(ln)
    132   let theline = getline(a:ln)->matchlist('\v^\s*\<!-- :Tangle(\([a-zA-Z0-9]*\))* (\<\^?\>)? ?(\<[^>]+\>\+?)? ?(.*)? --\>')[1:4]
    133   if empty(theline)
    134     throw 'Cannot parse tangle directive on line ' .. a:ln
    135   endif
    136   let [interp, should_expand_macros, macro_group, fname] = theline
    137 
    138   if empty(should_expand_macros) && empty(macro_group) && empty(fname)
    139     throw 'No filename in tangle directive on line ' .. a:ln
    140   endif
    141 
    142   if !empty(fname)
    143     if fname[0] !=# '/'
    144       let fname = expand("%:p:h") . '/' . fname
    145     endif
    146     let fname = fnameescape(fname)
    147   endif
    148   let theinterp = s:Lang2Interpreter(interp[1:-2])
    149   if empty(theinterp)
    150     let theinterp = interp[1:-2]
    151   endif
    152   return [theinterp, should_expand_macros, macro_group, fnameescape(fname)]
    153 endfunction
    154 ```
    155 We use a single regular expression to match the tangle directive.
    156 The Lang2Interpreter function converts a markdown language name to an interpreter (e.g. python3 for python); it'll be specified later in the document.
    157 
    158 We take the elements returned from the tangle directive, and start processing them.
    159 But first, we need to define some data structures we'll use.
    160 
    161 #### Some data structures
    162 A directive can specify a file for an interpreter, and this can change throughout the document.
    163 So, we need a way to store this information.
    164 We'll specify a dictionary that will look like this:
    165 
    166 ```
    167 curfiles = {
    168     "interpreter1": "/path/to/file",
    169     "interpreter2": "/path/to/file",
    170     ...
    171 }
    172 ```
    173 
    174 But initially it'll be empty:
    175 
    176 <!-- :Tangle(vim) <persistent variables> -->
    177 ```vim
    178 " The current files set for various interpreters
    179 let curfiles = {}
    180 ```
    181 
    182 We also need a way to track the lines for an output file for a specific interpreter.
    183 We'll use a dictionary that looks like this:
    184 
    185 ```
    186 interps_files = {
    187     "interpreter1": { "file1": ["line1", "line2"],
    188                        "file2": ["line1", "line2"],
    189                        ... },
    190     "interpreter2": ...
    191 }
    192 ```
    193 
    194 Also initially empty:
    195 
    196 <!-- :Tangle(vim) <persistent variables>+ -->
    197 ```vim
    198 " Finalized code, by interpreter and file
    199 let interps_files = {}
    200 ```
    201 
    202 We'll define an interpreter to represent "all interpreters", just an empty string:
    203 
    204 <!-- :Tangle(vim) <constants>+ -->
    205 ```vim
    206 let s:ALL_INTERP = ''
    207 ```
    208 
    209 We'll keep track of the interpreter that was last set in a tangle directive, initializing it to "all interpreters":
    210 
    211 <!-- :Tangle(vim) <persistent variables>+ -->
    212 ```vim
    213 let last_set_interp = s:ALL_INTERP
    214 ```
    215 
    216 #### Saving the directive
    217 Finally, we can start processing the directive.
    218 First, we process the declaration of an interpreter and file.
    219 We set the current file if necessary.
    220 
    221 <!-- :Tangle(vim) <parse the tangle directive>+ -->
    222 ```vim
    223 " Process file and interpreter declaration
    224 if !empty(curfile)
    225   " Change the current file for the interpreter
    226   let curfiles[last_set_interp] = curfile
    227 
    228   " Process interpreter declaration
    229   " If the interpreter has already been specified
    230   if has_key(interps_files, last_set_interp)
    231     " If the interpreter does not yet have any lines for this file
    232     if !has_key(interps_files[last_set_interp], curfile)
    233       " Add it
    234       let interps_files[last_set_interp][curfile] = []
    235     endif
    236     " If the interpreter already has lines for the file, don't do anything
    237     " If the interpreter itself hasn't been specified yet
    238   else
    239     " Add it
    240     let interps_files[last_set_interp] = {curfile: []}
    241   endif
    242 endif
    243 ```
    244 
    245 Then, we process any macro settings in the directive.
    246 
    247 #### Processing macros
    248 Now, this gets a bit more complicated, so we'll split it up into two parts: processing a block with macro expansions, and adding a new macro definition.
    249 
    250 <!-- :Tangle(vim) <> <parse the tangle directive>+ -->
    251 ```vim
    252 if !empty(should_expand_macros) || !empty(macro_group)
    253   if getline(curline+1)->match(s:codeblock_start) ==# -1
    254     throw "Tangle directive specifies macros on line " .. curline .. " but no code block follows."
    255   endif
    256   let block_contents = s:GetBlockContents(curline+1, s:GetBlockEnd(curline+1))
    257   let block_interp = s:GetBlockInterpreter(curline+1)
    258 
    259   if empty(block_interp)
    260     throw ("Macro expansion defined, but no block language set on line " .. (curline+1))
    261   endif
    262 
    263   " If the last set interpreter was generic, it should override all blocks
    264   if last_set_interp ==# s:ALL_INTERP
    265     let block_interp = s:ALL_INTERP
    266   endif
    267 
    268   <<process top-level macro expansion>>
    269   <<process macro definition>>
    270 
    271   " When processing macros, we process the block also, so move the
    272   " cursor after it
    273   let curline += len(block_contents)+2
    274 endif
    275 ```
    276 We only process macro expansions in a special way here if they're top-level macros (not contained in any other macros).
    277 The rest of the macros are expanded when writing to the file.
    278 
    279 We use another dictionary to contain macros, which looks like this:
    280 
    281 ```
    282 macros = {
    283     'interpreter': [
    284         'file': {
    285             'toplevel': [lines of top-level macro for file],
    286             'macros': {'macro 1': [lines of macro], ...}
    287         },
    288         'file2': ...
    289         ...
    290     ],
    291     'interpreter2': ...
    292     ...
    293 }
    294 ```
    295 
    296 Also initially empty:
    297 
    298 <!-- :Tangle(vim) <persistent variables>+ -->
    299 ```vim
    300 let macros = {}
    301 ```
    302 We process the expansion like this:
    303 
    304 <!-- :Tangle(vim) <process top-level macro expansion> -->
    305 ```vim
    306 " Process macro expansion
    307 " Top-level macros
    308 if !empty(should_expand_macros) && stridx(should_expand_macros, "^") >=# 0
    309   if !empty(macro_group)
    310     throw "Top-level macro block on line "  .. curline .. " cannot also belong to macro group."
    311   endif
    312 
    313   if has_key(curfiles, block_interp)
    314     let curfile = curfiles[block_interp]
    315   elseif has_key(curfiles, s:ALL_INTERP)
    316     let curfile = curfiles[s:ALL_INTERP]
    317   else
    318     throw "No current file set for block on line " .. curline+1
    319   endif
    320 
    321   if !has_key(macros, block_interp)
    322     let macros[block_interp] = {}
    323   endif
    324   if !has_key(macros[block_interp], curfile)
    325     let macros[block_interp][curfile] = {}
    326   endif
    327 
    328   if has_key(macros[block_interp][curfile], 'toplevel')
    329     throw "Duplicate top-level macro definition on line " .. curline
    330   endif
    331 
    332   " Add the current block as a top-level macro
    333   let macros[block_interp][curfile]['toplevel'] = block_contents
    334   " For regular macro expansion, just add the block
    335 endif
    336 ```
    337 
    338 A directive can also define a new macro, or add to an existing macro definition.
    339 In that case, we just save the block as a new macro for the current interpreter and file.
    340 There's a special case here, where the block defining a macro also contains macros to be expanded.
    341 In that case, we don't just add the block contents as a list, but we add a dictionary with the key 'expand' set to 1.
    342 This is then handled when writing the output files.
    343 
    344 <!-- :Tangle(vim) <process macro definition> -->
    345 ```vim
    346 " Potentially save block as macro
    347 if !empty(macro_group)
    348   " If extending an existing macro
    349   if !empty(should_expand_macros)
    350     let to_add = [{'expand': 1, 'contents': ['']+(block_contents) }]
    351   else
    352     let to_add = ['']+block_contents
    353   endif
    354 
    355   " If adding to an existing macro
    356   if stridx(macro_group, "+") ==# len(macro_group)-1
    357     let macro_tag = macro_group[1:-3]
    358     if empty(macro_tag)
    359       throw "Macro tag on line " .. curline .. " cannot be empty"
    360     endif
    361 
    362     if has_key(curfiles, block_interp)
    363       let curfile = curfiles[block_interp]
    364     elseif has_key(curfiles, s:ALL_INTERP)
    365       let curfile = curfiles[s:ALL_INTERP]
    366     else
    367       throw "No current file set for block on line " .. curline+1
    368     endif
    369 
    370     if !has_key(macros, block_interp)
    371           \ || !has_key(macros[block_interp], curfile)
    372           \ || !has_key(macros[block_interp][curfile], 'macros')
    373           \ || !has_key(macros[block_interp][curfile]['macros'], macro_tag)
    374       throw "Requested to extend macro <" .. macro_tag .. "> on line " .. curline .. ", but it's not yet defined"
    375     endif
    376 
    377     if type(to_add) ==# v:t_dict
    378       call add(macros[block_interp][curfile]['macros'][macro_tag], to_add)
    379     else
    380       call extend(macros[block_interp][curfile]['macros'][macro_tag], to_add)
    381     endif
    382 
    383   " If defining a new macro
    384   else
    385     if has_key(curfiles, block_interp)
    386       let curfile = curfiles[block_interp]
    387     elseif has_key(curfiles, s:ALL_INTERP)
    388       let curfile = curfiles[s:ALL_INTERP]
    389     else
    390       throw "No current file set for block on line " .. curline+1
    391     endif
    392 
    393     let macro_tag = macro_group[1:-2]
    394     if empty(macro_tag)
    395       throw "Macro tag on line " .. curline .. " cannot be empty"
    396     endif
    397 
    398     if !has_key(macros, block_interp)
    399       let macros[block_interp] = {}
    400     endif
    401     if !has_key(macros[block_interp], curfile)
    402       let macros[block_interp][curfile] = {}
    403     endif
    404 
    405     if has_key(macros[block_interp][curfile], 'macros') && has_key(macros[block_interp][curfile]['macros'], macro_tag)
    406       throw "Duplicate definition of macro tag <" .. macro_tag .. "> on line " .. curline
    407     endif
    408 
    409     if has_key(macros[block_interp][curfile], 'macros')
    410       let macros[block_interp][curfile]['macros'][macro_tag] = to_add
    411     else
    412       let macros[block_interp][curfile]['macros'] = {macro_tag: to_add}
    413     endif
    414   endif
    415 endif
    416 ```
    417 
    418 ### Processing a code block
    419 Processing a code block is straightforward.
    420 Just get the block contents, and add it to the correct list in the dictionary.
    421 
    422 <!-- :Tangle(vim) <process the block> -->
    423 ```vim
    424 " Get the contents of this block
    425 let block_contents = s:GetBlockContents(curline, s:GetBlockEnd(curline))
    426 
    427 if len(block_contents) ==# 0
    428   throw 'No end of block starting on line '.curline
    429 endif
    430 
    431 let interps_files = s:AddBlock(interps_files, block_contents, curline, last_set_interp, curfiles)
    432 
    433 " Skip to after the block
    434 let curline += len(block_contents)+2
    435 ```
    436 
    437 The AddBlock function looks like this:
    438 
    439 <!-- :Tangle(vim) <tangle helper functions>+ -->
    440 ```vim
    441 function! s:AddBlock(interps_files, block, block_start_line, last_set_interp, curfiles)
    442   let interps_files = a:interps_files
    443 
    444   if type(a:block) ==# v:t_dict
    445     let block_contents = a:block['contents']
    446   else
    447     let block_contents = a:block
    448   endif
    449 
    450   " Find out the amount of leading indentation (using the first line)
    451   " TODO: this should be the least indented line
    452   let nleadingspaces = matchend(block_contents[0], '^ \+')
    453   if nleadingspaces ==# -1
    454     let nleadingspaces = 0
    455   endif
    456 
    457   " Get the interpreter for this block
    458   let block_interp = s:GetBlockInterpreter(a:block_start_line)
    459   if empty(block_interp)
    460     let block_interp = s:GetBlockLang(a:block_start_line)
    461   endif
    462   if !empty(block_interp)
    463     " Allow overriding all interpreters to a 'general' file:
    464     " If the last Tangle directive didn't have an interpreter, direct
    465     " all blocks to that file
    466     if a:last_set_interp ==# s:ALL_INTERP && has_key(a:curfiles, s:ALL_INTERP)
    467       " Get the current file for 'all interpreters'
    468       let curfile = a:curfiles[s:ALL_INTERP]
    469       let curinterp = s:ALL_INTERP
    470       " If the last Tangle directive specified an interpreter
    471     else
    472       " If the interpreter was specified in a Tangle directive, use its
    473       " current file
    474       if has_key(interps_files, block_interp)
    475         let curfile = a:curfiles[block_interp]
    476         let curinterp = block_interp
    477         " Otherwise, use the 'general' file if specified
    478       elseif has_key(interps_files, s:ALL_INTERP)
    479         let curfile = a:curfiles[s:ALL_INTERP]
    480         let curinterp = s:ALL_INTERP
    481       endif
    482     endif
    483 
    484     " Add the lines to the current file to the current interpreter,
    485     " stripping leading indentation and appending a newline
    486     if exists('curinterp')
    487       if type(a:block) ==# v:t_dict
    488         call add(interps_files[curinterp][curfile], {'expand': a:block['expand'], 'contents': (map(block_contents, 'v:val['.nleadingspaces.':]')+[''])})
    489       else
    490         call extend(interps_files[curinterp][curfile], (map(block_contents, 'v:val['.nleadingspaces.':]')+['']))
    491       endif
    492     endif
    493   endif
    494 
    495   return interps_files
    496 endfunction
    497 ```
    498 
    499 ### Tangle interface function
    500 We need a way to call the tangle functions.
    501 
    502 The GetAllCode function returns the processed lines, and the saved macros.
    503 If g:literate_markdown_debug is set, also echo everything.
    504 
    505 <!-- :Tangle(vim) <return what's needed> -->
    506 ```vim
    507 if exists('g:literate_markdown_debug')
    508   echomsg interps_files
    509   echomsg macros
    510 endif
    511 return [interps_files, macros]
    512 ```
    513 
    514 That's then used in the autoload API function:
    515 
    516 <!-- :Tangle(vim) <> <API functions> -->
    517 ```vim
    518 function! literate_markdown#Tangle()
    519   " Get all of the code blocks in the file
    520   try
    521     let [lines, macros] = s:GetAllCode()
    522 
    523     if !empty(macros)
    524       let lines = s:ProcessMacroExpansions(lines, macros)
    525     endif
    526 
    527 
    528     " If there's any, tangle it
    529     if len(lines) ># 0
    530       <<merge lines from all interpreters into files>>
    531 
    532       <<save the lines to files>>
    533     endif
    534   catch
    535     echohl Error
    536     echomsg "Error: " .. v:exception .. " (from " .. v:throwpoint .. ")"
    537     echohl None
    538   endtry
    539 endfunction
    540 ```
    541 
    542 As you see, it's at this point that macro expansions are actually processed.
    543 The function below basically loops over all interpreters and their respective files, calls the expand macro function if necessary, and adds the result to a final dictionary.
    544 
    545 <!-- :Tangle(vim) <tangle helper functions>+ -->
    546 ```vim
    547 function! s:ProcessMacroExpansions(lines, macros)
    548   let final_lines = {}
    549   for [interp, fnames] in a:lines->items()
    550     if has_key(a:macros, interp)
    551       for [fname, flines] in fnames->items()
    552         if has_key(a:macros[interp], fname)
    553           if !has_key(a:macros[interp][fname], 'toplevel')
    554             throw "Macros exist, but no top-level structure defined for file " .. fname
    555           endif
    556 
    557           let toplevel = a:macros[interp][fname]['toplevel']
    558           let lines_here = []
    559           for line in toplevel
    560             if line->trim()->match('<<[^>]\+>>') >=# 0
    561               call extend(lines_here, s:ExpandMacro(a:macros, interp, fname, line))
    562             else
    563               call add(lines_here, line)
    564             endif
    565           endfor
    566 
    567           if !has_key(final_lines, interp)
    568             let final_lines[interp] = {fname: lines_here}
    569           else
    570             let final_lines[interp][fname] = lines_here
    571           endif
    572         else
    573           if !has_key(final_lines, interp)
    574             let final_lines[interp] = {fname: a:lines[interp][fname]}
    575           else
    576             let final_lines[interp][fname] = a:lines[interp][fname]
    577           endif
    578         endif
    579       endfor
    580     else
    581       let final_lines[interp] = a:lines[interp]
    582     endif
    583   endfor
    584   return final_lines
    585 endfunction
    586 ```
    587 
    588 The expand macro function does recursive expansion, i.e. it calls itself until there's nothing left to expand, then returns the result.
    589 It also checks the number of leading spaces to preserve indentation, i.e. if a macro is indented, its expansion will be indented to the same level.
    590 
    591 <!-- :Tangle(vim) <tangle helper functions>+ -->
    592 ```vim
    593 function! s:ExpandMacro(macros, interp, fname, line)
    594   let nleadingspaces = matchend(a:line, '^ \+')
    595   if nleadingspaces ==# -1
    596     let nleadingspaces = 0
    597   endif
    598 
    599   let macro_tag = trim(a:line)[2:-3]
    600   let expanded = []
    601   if !has_key(a:macros[a:interp][a:fname]['macros'], macro_tag)
    602     throw "Macro " .. macro_tag .. " not defined for file " .. a:fname
    603   endif
    604 
    605   let expansion = a:macros[a:interp][a:fname]['macros'][macro_tag]
    606   if type(expansion) ==# v:t_dict
    607     let expansion = [expansion]
    608   endif
    609   for expanded_line in expansion
    610     if type(expanded_line) ==# v:t_dict && expanded_line['expand']
    611       for l in expanded_line['contents']
    612         if l->trim()->match('<<[^>]\+>>') >=# 0
    613           call extend(expanded, s:ExpandMacro(a:macros, a:interp, a:fname, repeat(" ", nleadingspaces)..l))
    614         else
    615           call add(expanded, repeat(" ", nleadingspaces)..l)
    616         endif
    617       endfor
    618     else
    619       call add(expanded, repeat(" ", nleadingspaces)..expanded_line)
    620     endif
    621   endfor
    622 
    623   return expanded
    624 endfunction
    625 ```
    626 
    627 Once all expansions are done, we do some merging.
    628 Namely, it's possible that different interpreters will define output to the same file.
    629 So we combine everything to a dictionary that looks like this:
    630 
    631 ```
    632 all_interps_combined = {
    633   'file1': ['line1', 'line2'...],
    634   'file2': ...,
    635   ...
    636 }
    637 ```
    638 
    639 The merging code looks like this (and yes there's probably a better way to do it):
    640 
    641 <!-- :Tangle(vim) <merge lines from all interpreters into files> -->
    642 ```vim
    643 " Merge lines from all interpreters into the files
    644 let all_interps_combined = {}
    645 for fname_and_lines in lines->values()
    646   for [fname, flines] in fname_and_lines->items()
    647     if all_interps_combined->has_key(fname)
    648       call extend(all_interps_combined[fname], flines)
    649     else
    650       let all_interps_combined[fname] = flines
    651     endif
    652   endfor
    653 endfor
    654 ```
    655 
    656 Finally, we're ready to write the output files:
    657 
    658 <!-- :Tangle(vim) <save the lines to files> -->
    659 ```vim
    660 " Loop through the filenames and corresponding code
    661 for [fname, flines] in items(all_interps_combined)
    662   " Write the code to the respective file
    663   call s:SaveLines(flines, fname)
    664 endfor
    665 ```
    666 
    667 The SaveLines function is pretty trivial:
    668 
    669 <!-- :Tangle(vim) <general utility functions> -->
    670 ```vim
    671 " Write [lines] to fname and open it in a split
    672 function! s:SaveLines(lines, fname)
    673   if writefile(a:lines, a:fname) ==# 0
    674     if !exists('g:literate_markdown_no_open_tangled_files')
    675         exe 'split '.a:fname
    676     endif
    677   else
    678     echoerr "Could not write to file ".a:fname
    679   endif
    680 endfunction
    681 ```
    682 
    683 ## Code execution
    684 The plugin also allows stateless code execution.
    685 
    686 <!-- :Tangle(vim) <constants>+ -->
    687 ```vim
    688 let s:result_comment_start = '<!--\nRESULT:'
    689 let s:result_comment_end = '^-->'
    690 ```
    691 
    692 <!-- :Tangle(vim) <code execution-related functions> -->
    693 ```vim
    694 function! s:GetResultLine(blockend)
    695   let rowsave = line('.')
    696   let colsave = col('.')
    697   call cursor(a:blockend, 1)
    698   let nextblock = search(s:codeblock_start, 'nW')
    699   let linenum = search(s:result_comment_start, 'cnW', nextblock)
    700   call cursor(rowsave, colsave)
    701 
    702   if linenum == 0
    703     call append(a:blockend, ['', '<!--', 'RESULT:', '', '-->', ''])
    704     let linenum = a:blockend+2
    705   endif
    706   return linenum+1
    707 endfunction
    708 
    709 function! s:ClearResult(outputline)
    710   let rowsave = line('.')
    711   let colsave = col('.')
    712   call cursor(a:outputline, 1)
    713   let resultend = search(s:result_comment_end, 'nW')
    714   if resultend ==# 0
    715     throw 'Result block has no end'
    716   else
    717     execute a:outputline.','.resultend.'delete _'
    718   endif
    719   call cursor(rowsave, colsave)
    720 endfunction
    721 ```
    722 
    723 <!-- :Tangle(vim) <API functions>+ -->
    724 ```vim
    725 function! literate_markdown#ExecPreviousBlock()
    726   let blockstart = search(s:codeblock_start, 'nbW')
    727   if blockstart == 0
    728     throw 'No previous block found'
    729   endif
    730 
    731   let blockend = s:GetBlockEnd(blockstart)
    732 
    733   if blockend == 0
    734     throw 'No end for block'
    735   endif
    736 
    737   let interp = s:GetBlockInterpreter(blockstart)
    738   if empty(interp)
    739     throw 'No interpreter specified for block'
    740   endif
    741 
    742   let block_contents = s:GetBlockContents(blockstart, blockend)
    743 
    744   " TODO: This here will need to be different if accounting for state
    745   " (try channels? jobs? hidden term? other options?)
    746   let result_lines = systemlist(interp, block_contents)
    747 
    748   let outputline = s:GetResultLine(blockend)
    749   call s:ClearResult(outputline)
    750   call append(outputline-1, ['RESULT:'] + result_lines + ['-->'])
    751 endfunction
    752 ```
    753 
    754 ## Utility functions
    755 There are some general utility functions that we use throughout the plugin.
    756 
    757 To get the line that contains the end of the block starting from a certain line:
    758 
    759 <!-- :Tangle(vim) <general utility functions>+ -->
    760 ```vim
    761 " returns end line of a code block
    762 function! s:GetBlockEnd(start)
    763   " Save the cursor
    764   let rowsave = line('.')
    765   let colsave = col('.')
    766 
    767   " search() starts from cursor
    768   call cursor(a:start, 1)
    769 
    770   " nW == don't move cursor, no wrap search
    771   let endblock = search(s:codeblock_end, 'nW')
    772   call cursor(rowsave, colsave)
    773   return endblock
    774 endfunction
    775 ```
    776 
    777 To get the contents of a block as a list:
    778 
    779 <!-- :Tangle(vim) <general utility functions>+ -->
    780 ```vim
    781 " returns [contents of a code block]
    782 function! s:GetBlockContents(start, end)
    783   " i.e. if there's no end
    784   if a:end ==# 0
    785     let retval = []
    786   else
    787     let retval = getline(a:start+1, a:end-1)
    788   endif
    789   return retval
    790 endfunction
    791 ```
    792 
    793 This function converts the language defined in a code block to a specific interpreter (e.g. both 'rb' and 'ruby' get converted to 'ruby').
    794 This is user-configurable using the 'g:literate_markdown_interpreters' (global) and 'b:literate_markdown_interpreters' (buffer-local) variables.
    795 
    796 <!-- :Tangle(vim) <general utility functions>+ -->
    797 ```vim
    798 " Returns the interpreter name for a programming language
    799 function! s:Lang2Interpreter(lang)
    800   let lang = a:lang
    801   if exists('g:literate_markdown_interpreters')
    802     for [interp, langnames] in items(g:literate_markdown_interpreters)
    803       if index(langnames, lang) >= 0
    804         return interp
    805       endif
    806     endfor
    807   endif
    808   if exists('b:literate_markdown_interpreters')
    809     for [interp, langnames] in items(b:literate_markdown_interpreters)
    810       if index(langnames, lang) >= 0
    811         return interp
    812       endif
    813     endfor
    814   endif
    815 
    816   let lang2interp = {
    817         \ 'python3': ['py', 'python', 'python3'],
    818         \ 'python2': ['python2'],
    819         \ 'ruby': ['rb', 'ruby'],
    820         \ 'sh': ['sh'],
    821         \ 'bash': ['bash'],
    822         \ 'cat /tmp/program.c && gcc /tmp/program.c -o /tmp/program && /tmp/program': ['c'],
    823         \ }
    824   for [interp, langnames] in items(lang2interp)
    825     if index(langnames, lang) >= 0
    826       return interp
    827     endif
    828   endfor
    829   return ''
    830 endfunction
    831 ```
    832 
    833 And we can use this function to get the interpreter used in a code block:
    834 
    835 <!-- :Tangle(vim) <general utility functions>+ -->
    836 ```vim
    837 function! s:GetBlockLang(blockstart)
    838   let lang = getline(a:blockstart)[3:]
    839   return lang
    840 endfunction
    841 
    842 " Gets the interpreter name for a code block
    843 function! s:GetBlockInterpreter(blockstart)
    844   " A markdown block beginning looks like this: ```lang
    845   let lang = s:GetBlockLang(a:blockstart)
    846   if empty(lang)
    847     return ''
    848   endif
    849 
    850   let interp = s:Lang2Interpreter(lang)
    851 
    852   if empty(interp)
    853     let interp = lang
    854   endif
    855   return interp
    856 endfunction
    857 ```
    858