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