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