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