diff options
Diffstat (limited to 'autoload/vimtex/parser')
-rw-r--r-- | autoload/vimtex/parser/auxiliary.vim | 58 | ||||
-rw-r--r-- | autoload/vimtex/parser/bib.vim | 370 | ||||
-rw-r--r-- | autoload/vimtex/parser/fls.vim | 19 | ||||
-rw-r--r-- | autoload/vimtex/parser/tex.vim | 205 | ||||
-rw-r--r-- | autoload/vimtex/parser/toc.vim | 778 |
5 files changed, 1430 insertions, 0 deletions
diff --git a/autoload/vimtex/parser/auxiliary.vim b/autoload/vimtex/parser/auxiliary.vim new file mode 100644 index 00000000..b8805ebd --- /dev/null +++ b/autoload/vimtex/parser/auxiliary.vim @@ -0,0 +1,58 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'latex') == -1 + +" vimtex - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" + +function! vimtex#parser#auxiliary#parse(file) abort " {{{1 + return s:parse_recurse(a:file, []) +endfunction + +" }}}1 + +function! s:parse_recurse(file, parsed) abort " {{{1 + if !filereadable(a:file) || index(a:parsed, a:file) >= 0 + return [] + endif + call add(a:parsed, a:file) + + let l:lines = [] + for l:line in readfile(a:file) + call add(l:lines, l:line) + + if l:line =~# '\\@input{' + let l:file = s:input_line_parser(l:line, a:file) + call extend(l:lines, s:parse_recurse(l:file, a:parsed)) + endif + endfor + + return l:lines +endfunction + +" }}}1 + +function! s:input_line_parser(line, file) abort " {{{1 + let l:file = matchstr(a:line, '\\@input{\zs[^}]\+\ze}') + + " Remove extension to simplify the parsing (e.g. for "my file name".aux) + let l:file = substitute(l:file, '\.aux', '', '') + + " Trim whitespaces and quotes from beginning/end of string, append extension + let l:file = substitute(l:file, '^\(\s\|"\)*', '', '') + let l:file = substitute(l:file, '\(\s\|"\)*$', '', '') + let l:file .= '.aux' + + " Use absolute paths + if l:file !~# '\v^(\/|[A-Z]:)' + let l:file = fnamemodify(a:file, ':p:h') . '/' . l:file + endif + + " Only return filename if it is readable + return filereadable(l:file) ? l:file : '' +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/bib.vim b/autoload/vimtex/parser/bib.vim new file mode 100644 index 00000000..7ea2c238 --- /dev/null +++ b/autoload/vimtex/parser/bib.vim @@ -0,0 +1,370 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'latex') == -1 + +" vimtex - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" + +function! vimtex#parser#bib#parse(file, opts) abort " {{{1 + if !filereadable(a:file) | return [] | endif + + let l:backend = get(a:opts, 'backend', g:vimtex_parser_bib_backend) + + if l:backend ==# 'bibtex' + if !executable('bibtex') | let l:backend = 'vim' | endif + elseif l:backend ==# 'bibparse' + if !executable('bibparse') | let l:backend = 'vim' | endif + else + let l:backend = 'vim' + endif + + return s:parse_with_{l:backend}(a:file) +endfunction + +" }}}1 + + +function! s:parse_with_bibtex(file) abort " {{{1 + call s:parse_with_bibtex_init() + if s:bibtex_not_executable | return [] | endif + + " Define temporary files + let tmp = { + \ 'aux' : 'tmpfile.aux', + \ 'bbl' : 'tmpfile.bbl', + \ 'blg' : 'tmpfile.blg', + \ } + + " Write temporary aux file + call writefile([ + \ '\citation{*}', + \ '\bibstyle{' . s:bibtex_bstfile . '}', + \ '\bibdata{' . fnamemodify(a:file, ':r') . '}', + \ ], tmp.aux) + + " Create the temporary bbl file + call vimtex#process#run('bibtex -terse ' . fnameescape(tmp.aux), { + \ 'background' : 0, + \ 'silent' : 1, + \}) + + " Parse temporary bbl file + let lines = join(readfile(tmp.bbl), "\n") + let lines = substitute(lines, '\n\n\@!\(\s\=\)\s*\|{\|}', '\1', 'g') + let lines = vimtex#util#tex2unicode(lines) + let lines = split(lines, "\n") + + let l:entries = [] + for line in lines + let matches = split(line, '||') + if empty(matches) || empty(matches[0]) | continue | endif + + let l:entry = { + \ 'key': matches[0], + \ 'type': matches[1], + \} + + if !empty(matches[2]) + let l:entry.author = matches[2] + endif + if !empty(matches[3]) + let l:entry.year = matches[3] + endif + if !empty(get(matches, 4, '')) + let l:entry.title = get(matches, 4, '') + endif + + call add(l:entries, l:entry) + endfor + + " Clean up + call delete(tmp.aux) + call delete(tmp.bbl) + call delete(tmp.blg) + + return l:entries +endfunction + +" }}}1 +function! s:parse_with_bibtex_init() abort " {{{1 + if exists('s:bibtex_init_done') | return | endif + + " Check if bibtex is executable + let s:bibtex_not_executable = !executable('bibtex') + if s:bibtex_not_executable + call vimtex#log#warning( + \ 'bibtex is not executable and may not be used to parse bib files!') + endif + + " Check if bstfile contains whitespace (not handled by vimtex) + if stridx(s:bibtex_bstfile, ' ') >= 0 + let l:oldbst = s:bibtex_bstfile . '.bst' + let s:bibtex_bstfile = tempname() + call writefile(readfile(l:oldbst), s:bibtex_bstfile . '.bst') + endif + + let s:bibtex_init_done = 1 +endfunction + +let s:bibtex_bstfile = expand('<sfile>:p:h') . '/vimcomplete' + +" }}}1 + +function! s:parse_with_bibparse(file) abort " {{{1 + call s:parse_with_bibparse_init() + if s:bibparse_not_executable | return [] | endif + + call vimtex#process#run('bibparse ' . fnameescape(a:file) + \ . ' >_vimtex_bibparsed.log', {'background' : 0, 'silent' : 1}) + let l:lines = readfile('_vimtex_bibparsed.log') + call delete('_vimtex_bibparsed.log') + + let l:current = {} + let l:entries = [] + for l:line in l:lines + if l:line[0] ==# '@' + if !empty(l:current) + call add(l:entries, l:current) + let l:current = {} + endif + + let l:index = stridx(l:line, ' ') + if l:index > 0 + let l:type = l:line[1:l:index-1] + let l:current.type = l:type + let l:current.key = l:line[l:index+1:] + endif + elseif !empty(l:current) + let l:index = stridx(l:line, '=') + if l:index < 0 | continue | endif + + let l:key = l:line[:l:index-1] + let l:value = l:line[l:index+1:] + let l:current[tolower(l:key)] = l:value + endif + endfor + + if !empty(l:current) + call add(l:entries, l:current) + endif + + return l:entries +endfunction + +" }}}1 +function! s:parse_with_bibparse_init() abort " {{{1 + if exists('s:bibparse_init_done') | return | endif + + " Check if bibtex is executable + let s:bibparse_not_executable = !executable('bibparse') + if s:bibparse_not_executable + call vimtex#log#warning( + \ 'bibparse is not executable and may not be used to parse bib files!') + endif + + let s:bibparse_init_done = 1 +endfunction + +" }}}1 + +function! s:parse_with_vim(file) abort " {{{1 + " Adheres to the format description found here: + " http://www.bibtex.org/Format/ + + if !filereadable(a:file) + return [] + endif + + let l:current = {} + let l:strings = {} + let l:entries = [] + for l:line in filter(readfile(a:file), 'v:val !~# ''^\s*\%(%\|$\)''') + if empty(l:current) + if s:parse_type(l:line, l:current, l:strings) + let l:current = {} + endif + continue + endif + + if l:current.type ==# 'string' + if s:parse_string(l:line, l:current, l:strings) + let l:current = {} + endif + else + if s:parse_entry(l:line, l:current, l:entries) + let l:current = {} + endif + endif + endfor + + return map(l:entries, 's:parse_entry_body(v:val, l:strings)') +endfunction + +" }}}1 + +function! s:parse_type(line, current, strings) abort " {{{1 + let l:matches = matchlist(a:line, '\v^\@(\w+)\s*\{\s*(.*)') + if empty(l:matches) | return 0 | endif + + let l:type = tolower(l:matches[1]) + if index(['preamble', 'comment'], l:type) >= 0 | return 0 | endif + + let a:current.level = 1 + let a:current.body = '' + + if l:type ==# 'string' + return s:parse_string(l:matches[2], a:current, a:strings) + else + let a:current.type = l:type + let a:current.key = matchstr(l:matches[2], '.*\ze,\s*') + return 0 + endif +endfunction + +" }}}1 +function! s:parse_string(line, string, strings) abort " {{{1 + let a:string.level += s:count(a:line, '{') - s:count(a:line, '}') + if a:string.level > 0 + let a:string.body .= a:line + return 0 + endif + + let a:string.body .= matchstr(a:line, '.*\ze}') + + let l:matches = matchlist(a:string.body, '\v^\s*(\w+)\s*\=\s*"(.*)"\s*$') + if !empty(l:matches) && !empty(l:matches[1]) + let a:strings[l:matches[1]] = l:matches[2] + endif + + return 1 +endfunction + +" }}}1 +function! s:parse_entry(line, entry, entries) abort " {{{1 + let a:entry.level += s:count(a:line, '{') - s:count(a:line, '}') + if a:entry.level > 0 + let a:entry.body .= a:line + return 0 + endif + + let a:entry.body .= matchstr(a:line, '.*\ze}') + + call add(a:entries, a:entry) + return 1 +endfunction + +" }}}1 + +function! s:parse_entry_body(entry, strings) abort " {{{1 + unlet a:entry.level + + let l:key = '' + let l:pos = matchend(a:entry.body, '^\s*') + while l:pos >= 0 + if empty(l:key) + let [l:key, l:pos] = s:get_key(a:entry.body, l:pos) + else + let [l:value, l:pos] = s:get_value(a:entry.body, l:pos, a:strings) + let a:entry[l:key] = l:value + let l:key = '' + endif + endwhile + + unlet a:entry.body + return a:entry +endfunction + +" }}}1 +function! s:get_key(body, head) abort " {{{1 + " Parse the key part of a bib entry tag. + " Assumption: a:body is left trimmed and either empty or starts with a key. + " Returns: The key and the remaining part of the entry body. + + let l:matches = matchlist(a:body, '^\v(\w+)\s*\=\s*', a:head) + return empty(l:matches) + \ ? ['', -1] + \ : [tolower(l:matches[1]), a:head + strlen(l:matches[0])] +endfunction + +" }}}1 +function! s:get_value(body, head, strings) abort " {{{1 + " Parse the value part of a bib entry tag, until separating comma or end. + " Assumption: a:body is left trimmed and either empty or starts with a value. + " Returns: The value and the remaining part of the entry body. + " + " A bib entry value is either + " 1. A number. + " 2. A concatenation (with #s) of double quoted strings, curlied strings, + " and/or bibvariables, + " + if a:body[a:head] =~# '\d' + let l:value = matchstr(a:body, '^\d\+', a:head) + let l:head = matchend(a:body, '^\s*,\s*', a:head + len(l:value)) + return [l:value, l:head] + else + return s:get_value_string(a:body, a:head, a:strings) + endif + + return ['s:get_value failed', -1] +endfunction + +" }}}1 +function! s:get_value_string(body, head, strings) abort " {{{1 + if a:body[a:head] ==# '{' + let l:sum = 1 + let l:i1 = a:head + 1 + let l:i0 = l:i1 + + while l:sum > 0 + let [l:match, l:_, l:i1] = matchstrpos(a:body, '[{}]', l:i1) + if l:i1 < 0 | break | endif + + let l:i0 = l:i1 + let l:sum += l:match ==# '{' ? 1 : -1 + endwhile + + let l:value = a:body[a:head+1:l:i0-2] + let l:head = matchend(a:body, '^\s*', l:i0) + elseif a:body[a:head] ==# '"' + let l:index = match(a:body, '\\\@<!"', a:head+1) + if l:index < 0 + return ['s:get_value_string failed', ''] + endif + + let l:value = a:body[a:head+1:l:index-1] + let l:head = matchend(a:body, '^\s*', l:index+1) + return [l:value, l:head] + elseif a:body[a:head:] =~# '^\w' + let l:value = matchstr(a:body, '^\w\+', a:head) + let l:head = matchend(a:body, '^\s*', a:head + strlen(l:value)) + let l:value = get(a:strings, l:value, '@(' . l:value . ')') + else + let l:head = a:head + endif + + if a:body[l:head] ==# '#' + let l:head = matchend(a:body, '^\s*', l:head + 1) + let [l:vadd, l:head] = s:get_value_string(a:body, l:head, a:strings) + let l:value .= l:vadd + endif + + return [l:value, matchend(a:body, '^,\s*', l:head)] +endfunction + +" }}}1 + +function! s:count(container, item) abort " {{{1 + " Necessary because in old Vim versions, count() does not work for strings + try + let l:count = count(a:container, a:item) + catch /E712/ + let l:count = count(split(a:container, '\zs'), a:item) + endtry + + return l:count +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/fls.vim b/autoload/vimtex/parser/fls.vim new file mode 100644 index 00000000..46b0db2f --- /dev/null +++ b/autoload/vimtex/parser/fls.vim @@ -0,0 +1,19 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'latex') == -1 + +" vimtex - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" + +function! vimtex#parser#fls#parse(file) abort " {{{1 + if !filereadable(a:file) + return [] + endif + + return readfile(a:file) +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/tex.vim b/autoload/vimtex/parser/tex.vim new file mode 100644 index 00000000..6259b5fa --- /dev/null +++ b/autoload/vimtex/parser/tex.vim @@ -0,0 +1,205 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'latex') == -1 + +" vimtex - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" + +function! vimtex#parser#tex#parse(file, opts) abort " {{{1 + let l:opts = extend({ + \ 'detailed': 1, + \ 'root' : exists('b:vimtex.root') ? b:vimtex.root : '', + \}, a:opts) + + let l:cache = vimtex#cache#open('texparser', { + \ 'local': 1, + \ 'persistent': 0, + \ 'default': {'ftime': -2}, + \}) + + let l:parsed = s:parse(a:file, l:opts, l:cache) + + if !l:opts.detailed + call map(l:parsed, 'v:val[2]') + endif + + return l:parsed +endfunction + +" }}}1 +function! vimtex#parser#tex#parse_files(file, opts) abort " {{{1 + let l:opts = extend({ + \ 'root' : exists('b:vimtex.root') ? b:vimtex.root : '', + \}, a:opts) + + let l:cache = vimtex#cache#open('texparser', { + \ 'local': 1, + \ 'persistent': 0, + \ 'default': {'ftime': -2}, + \}) + + return vimtex#util#uniq_unsorted( + \ s:parse_files(a:file, l:opts, l:cache)) +endfunction + +" }}}1 +function! vimtex#parser#tex#parse_preamble(file, opts) abort " {{{1 + let l:opts = extend({ + \ 'inclusive' : 0, + \ 'root' : exists('b:vimtex.root') ? b:vimtex.root : '', + \}, a:opts) + + return s:parse_preamble(a:file, l:opts, []) +endfunction + +" }}}1 + +function! s:parse(file, opts, cache) abort " {{{1 + let l:current = a:cache.get(a:file) + let l:ftime = getftime(a:file) + if l:ftime > l:current.ftime + let l:current.ftime = l:ftime + call s:parse_current(a:file, a:opts, l:current) + endif + + let l:parsed = [] + + for l:val in l:current.lines + if type(l:val) == type([]) + call add(l:parsed, l:val) + else + call extend(l:parsed, s:parse(l:val, a:opts, a:cache)) + endif + endfor + + return l:parsed +endfunction + +" }}}1 +function! s:parse_files(file, opts, cache) abort " {{{1 + let l:current = a:cache.get(a:file) + let l:ftime = getftime(a:file) + if l:ftime > l:current.ftime + let l:current.ftime = l:ftime + call s:parse_current(a:file, a:opts, l:current) + endif + + " Only include existing files + if !filereadable(a:file) | return [] | endif + + let l:files = [a:file] + for l:file in l:current.includes + let l:files += s:parse_files(l:file, a:opts, a:cache) + endfor + + return l:files +endfunction + +" }}}1 +function! s:parse_current(file, opts, current) abort " {{{1 + let a:current.lines = [] + let a:current.includes = [] + + " Also load includes from glsentries + let l:re_input = g:vimtex#re#tex_input . '|^\s*\\loadglsentries' + + if filereadable(a:file) + let l:lnum = 0 + for l:line in readfile(a:file) + let l:lnum += 1 + call add(a:current.lines, [a:file, l:lnum, l:line]) + + " Minor optimization: Avoid complex regex on "simple" lines + if stridx(l:line, '\') < 0 | continue | endif + + if l:line =~# l:re_input + let l:file = s:input_parser(l:line, a:file, a:opts.root) + call add(a:current.lines, l:file) + call add(a:current.includes, l:file) + endif + endfor + endif +endfunction + +" }}}1 +function! s:parse_preamble(file, opts, parsed_files) abort " {{{1 + if !filereadable(a:file) || index(a:parsed_files, a:file) >= 0 + return [] + endif + call add(a:parsed_files, a:file) + + let l:lines = [] + for l:line in readfile(a:file) + if l:line =~# '\\begin\s*{document}' + if a:opts.inclusive + call add(l:lines, l:line) + endif + break + endif + + call add(l:lines, l:line) + + if l:line =~# g:vimtex#re#tex_input + let l:file = s:input_parser(l:line, a:file, a:opts.root) + call extend(l:lines, s:parse_preamble(l:file, a:opts, a:parsed_files)) + endif + endfor + + return l:lines +endfunction + +" }}}1 + +function! s:input_parser(line, current_file, root) abort " {{{1 + " Handle \space commands + let l:file = substitute(a:line, '\\space\s*', ' ', 'g') + + " Handle import package commands + if l:file =~# g:vimtex#re#tex_input_import + let l:root = l:file =~# '\\sub' + \ ? fnamemodify(a:current_file, ':p:h') + \ : a:root + + let l:candidate = s:input_to_filename( + \ substitute(copy(l:file), '}\s*{', '', 'g'), l:root) + if !empty(l:candidate) + return l:candidate + else + return s:input_to_filename( + \ substitute(copy(l:file), '{.{-}}', '', ''), l:root) + endif + else + return s:input_to_filename(l:file, a:root) + endif +endfunction + +" }}}1 +function! s:input_to_filename(input, root) abort " {{{1 + let l:file = matchstr(a:input, '\zs[^{}]\+\ze}\s*\%(%\|$\)') + + " Trim whitespaces and quotes from beginning/end of string + let l:file = substitute(l:file, '^\(\s\|"\)*', '', '') + let l:file = substitute(l:file, '\(\s\|"\)*$', '', '') + + " Ensure that the file name has extension + if empty(fnamemodify(l:file, ':e')) + let l:file .= '.tex' + endif + + if vimtex#paths#is_abs(l:file) + return l:file + endif + + let l:candidate = a:root . '/' . l:file + if filereadable(l:candidate) + return l:candidate + endif + + let l:candidate = vimtex#kpsewhich#find(l:file) + return filereadable(l:candidate) ? l:candidate : l:file +endfunction + +" }}}1 + +endif diff --git a/autoload/vimtex/parser/toc.vim b/autoload/vimtex/parser/toc.vim new file mode 100644 index 00000000..517a25be --- /dev/null +++ b/autoload/vimtex/parser/toc.vim @@ -0,0 +1,778 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'latex') == -1 + +" vimtex - LaTeX plugin for Vim +" +" Maintainer: Karl Yngve Lervåg +" Email: karl.yngve@gmail.com +" +" +" Parses tex project for ToC-like entries. Each entry is a dictionary +" similar to the following: +" +" entry = { +" title : "Some title", +" number : "3.1.2", +" file : /path/to/file.tex, +" line : 142, +" rank : cumulative line number, +" level : 2, +" type : [content | label | todo | include], +" link : [0 | 1], +" } +" + +function! vimtex#parser#toc#parse(file) abort " {{{1 + let l:entries = [] + let l:content = vimtex#parser#tex(a:file) + + let l:max_level = 0 + for [l:file, l:lnum, l:line] in l:content + if l:line =~# s:matcher_sections.re + let l:max_level = max([ + \ l:max_level, + \ s:sec_to_value[matchstr(l:line, s:matcher_sections.re_level)] + \]) + endif + endfor + + call s:level.reset('preamble', l:max_level) + + " No more parsing if there is no content + if empty(l:content) | return l:entries | endif + + " + " Begin parsing LaTeX files + " + let l:lnum_total = 0 + let l:matchers = s:matchers_preamble + for [l:file, l:lnum, l:line] in l:content + let l:lnum_total += 1 + let l:context = { + \ 'file' : l:file, + \ 'line' : l:line, + \ 'lnum' : l:lnum, + \ 'lnum_total' : l:lnum_total, + \ 'level' : s:level, + \ 'max_level' : l:max_level, + \ 'entry' : get(l:entries, -1, {}), + \ 'num_entries' : len(l:entries), + \} + + " Detect end of preamble + if s:level.preamble && l:line =~# '\v^\s*\\begin\{document\}' + let s:level.preamble = 0 + let l:matchers = s:matchers_content + continue + endif + + " Handle multi-line entries + if exists('s:matcher_continue') + call s:matcher_continue.continue(l:context) + continue + endif + + " Apply prefilter - this gives considerable speedup for large documents + if l:line !~# s:re_prefilter | continue | endif + + " Apply the matchers + for l:matcher in l:matchers + if l:line =~# l:matcher.re + let l:entry = l:matcher.get_entry(l:context) + if type(l:entry) == type([]) + call extend(l:entries, l:entry) + elseif !empty(l:entry) + call add(l:entries, l:entry) + endif + endif + endfor + endfor + + for l:matcher in s:matchers + try + call l:matcher.filter(l:entries) + catch /E716/ + endtry + endfor + + return l:entries +endfunction + +" }}}1 +function! vimtex#parser#toc#get_topmatters() abort " {{{1 + let l:topmatters = s:level.frontmatter + let l:topmatters += s:level.mainmatter + let l:topmatters += s:level.appendix + let l:topmatters += s:level.backmatter + + for l:level in get(s:level, 'old', []) + let l:topmatters += l:level.frontmatter + let l:topmatters += l:level.mainmatter + let l:topmatters += l:level.appendix + let l:topmatters += l:level.backmatter + endfor + + return l:topmatters +endfunction + +" }}}1 +function! vimtex#parser#toc#get_entry_general(context) abort dict " {{{1 + return { + \ 'title' : self.title, + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'rank' : a:context.lnum_total, + \ 'level' : 0, + \ 'type' : 'content', + \} +endfunction + +" }}}1 + +" IMPORTANT: The following defines a prefilter for optimizing the toc parser. +" Any line that should be parsed has to be matched by this regexp! +" {{{1 let s:re_prefilter = ... +let s:re_prefilter = '\v%(\\' . join([ + \ '%(front|main|back)matter', + \ 'add%(global|section)?bib', + \ 'appendix', + \ 'begin', + \ 'bibliography', + \ 'chapter', + \ 'documentclass', + \ 'import', + \ 'include', + \ 'includegraphics', + \ 'input', + \ 'label', + \ 'part', + \ 'printbib', + \ 'printindex', + \ 'paragraph', + \ 'section', + \ 'subfile', + \ 'tableofcontents', + \ 'todo', + \], '|') . ')' + \ . '|\%\s*%(' . join(g:vimtex_toc_todo_keywords, '|') . ')' + \ . '|\%\s*vimtex-include' +for s:m in g:vimtex_toc_custom_matchers + if has_key(s:m, 'prefilter') + let s:re_prefilter .= '|' . s:m.prefilter + endif +endfor + +" }}}1 + +" Adds entries for included files +let s:matcher_include = { + \ 're' : vimtex#re#tex_input . '\zs\f{-}\s*\ze\}', + \ 'in_preamble' : 1, + \ 'priority' : 0, + \} +function! s:matcher_include.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + if !vimtex#paths#is_abs(l:file[0]) + let l:file = b:vimtex.root . '/' . l:file + endif + let l:file = fnamemodify(l:file, ':~:.') + if !filereadable(l:file) + let l:file .= '.tex' + endif + return { + \ 'title' : 'tex incl: ' . (strlen(l:file) < 70 + \ ? l:file + \ : l:file[0:30] . '...' . l:file[-36:]), + \ 'number' : '', + \ 'file' : l:file, + \ 'line' : 1, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ } +endfunction + +" }}}1 + +" Adds entries for included graphics files (filetype tikz, tex) +let s:matcher_include_graphics = { + \ 're' : '\v^\s*\\includegraphics\*?%(\s*\[[^]]*\]){0,2}\s*\{\zs[^}]*', + \ 'priority' : 1, + \} +function! s:matcher_include_graphics.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + if !vimtex#paths#is_abs(l:file) + let l:file = vimtex#misc#get_graphicspath(l:file) + endif + let l:file = fnamemodify(l:file, ':~:.') + let l:ext = fnamemodify(l:file, ':e') + + return !filereadable(l:file) || index(['asy', 'tikz'], l:ext) < 0 + \ ? {} + \ : { + \ 'title' : 'fig incl: ' . (strlen(l:file) < 70 + \ ? l:file + \ : l:file[0:30] . '...' . l:file[-36:]), + \ 'number' : '', + \ 'file' : l:file, + \ 'line' : 1, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \ } +endfunction + +" }}}1 + +" Adds entries for included files through vimtex specific syntax (this allows +" to add entries for any filetype or file) +let s:matcher_include_vimtex = { + \ 're' : '^\s*%\s*vimtex-include:\?\s\+\zs\f\+', + \ 'in_preamble' : 1, + \ 'priority' : 1, + \} +function! s:matcher_include_vimtex.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + if !vimtex#paths#is_abs(l:file) + let l:file = b:vimtex.root . '/' . l:file + endif + let l:file = fnamemodify(l:file, ':~:.') + return { + \ 'title' : 'vtx incl: ' . (strlen(l:file) < 70 + \ ? l:file + \ : l:file[0:30] . '...' . l:file[-36:]), + \ 'number' : '', + \ 'file' : l:file, + \ 'line' : 1, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \ } +endfunction + +" }}}1 + +let s:matcher_include_bibtex = { + \ 're' : '\v^\s*\\bibliography\s*\{\zs[^}]+\ze\}', + \ 'in_preamble' : 1, + \ 'priority' : 0, + \} +function! s:matcher_include_bibtex.get_entry(context) abort dict " {{{1 + let l:entries = [] + + for l:file in split(matchstr(a:context.line, self.re), ',') + " Ensure that the file name has extension + if l:file !~# '\.bib$' + let l:file .= '.bib' + endif + + call add(l:entries, { + \ 'title' : printf('bib incl: %-.67s', fnamemodify(l:file, ':t')), + \ 'number' : '', + \ 'file' : vimtex#kpsewhich#find(l:file), + \ 'line' : 1, + \ 'level' : 0, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \}) + endfor + + return l:entries +endfunction + +" }}}1 + +let s:matcher_include_biblatex = { + \ 're' : '\v^\s*\\add(bibresource|globalbib|sectionbib)\s*\{\zs[^}]+\ze\}', + \ 'in_preamble' : 1, + \ 'in_content' : 0, + \ 'priority' : 0, + \} +function! s:matcher_include_biblatex.get_entry(context) abort dict " {{{1 + let l:file = matchstr(a:context.line, self.re) + + return { + \ 'title' : printf('bib incl: %-.67s', fnamemodify(l:file, ':t')), + \ 'number' : '', + \ 'file' : vimtex#kpsewhich#find(l:file), + \ 'line' : 1, + \ 'level' : 0, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'include', + \ 'link' : 1, + \} +endfunction + +" }}}1 + +let s:matcher_preamble = { + \ 're' : '\v^\s*\\documentclass', + \ 'in_preamble' : 1, + \ 'in_content' : 0, + \ 'priority' : 0, + \} +function! s:matcher_preamble.get_entry(context) abort " {{{1 + return g:vimtex_toc_show_preamble + \ ? { + \ 'title' : 'Preamble', + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : 0, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'content', + \ } + \ : {} +endfunction + +" }}}1 + +let s:matcher_parts = { + \ 're' : '\v^\s*\\\zs((front|main|back)matter|appendix)>', + \ 'priority' : 0, + \} +function! s:matcher_parts.get_entry(context) abort dict " {{{1 + call a:context.level.reset( + \ matchstr(a:context.line, self.re), + \ a:context.max_level) + return {} +endfunction + +" }}}1 + +let s:matcher_sections = { + \ 're' : '\v^\s*\\%(part|chapter|%(sub)*section|%(sub)?paragraph)\*?\s*(\[|\{)', + \ 're_starred' : '\v^\s*\\%(part|chapter|%(sub)*section)\*', + \ 're_level' : '\v^\s*\\\zs%(part|chapter|%(sub)*section|%(sub)?paragraph)', + \ 'priority' : 0, + \} +let s:matcher_sections.re_title = s:matcher_sections.re . '\zs.{-}\ze\%?\s*$' +function! s:matcher_sections.get_entry(context) abort dict " {{{1 + let level = matchstr(a:context.line, self.re_level) + let type = matchlist(a:context.line, self.re)[1] + let title = matchstr(a:context.line, self.re_title) + let number = '' + + let [l:end, l:count] = s:find_closing(0, title, 1, type) + if l:count == 0 + let title = self.parse_title(strpart(title, 0, l:end+1)) + else + let self.type = type + let self.count = l:count + let s:matcher_continue = deepcopy(self) + endif + + if a:context.line !~# self.re_starred + call a:context.level.increment(level) + if a:context.line !~# '\v^\s*\\%(sub)?paragraph' + let number = deepcopy(a:context.level) + endif + endif + + return { + \ 'title' : title, + \ 'number' : number, + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'content', + \ } +endfunction + +" }}}1 +function! s:matcher_sections.parse_title(title) abort dict " {{{1 + let l:title = substitute(a:title, '\v%(\]|\})\s*$', '', '') + return s:clear_texorpdfstring(l:title) +endfunction + +" }}}1 +function! s:matcher_sections.continue(context) abort dict " {{{1 + let [l:end, l:count] = s:find_closing(0, a:context.line, self.count, self.type) + if l:count == 0 + let a:context.entry.title = self.parse_title(a:context.entry.title . strpart(a:context.line, 0, l:end+1)) + unlet! s:matcher_continue + else + let a:context.entry.title .= a:context.line + let self.count = l:count + endif +endfunction + +" }}}1 + +let s:matcher_table_of_contents = { + \ 'title' : 'Table of contents', + \ 're' : '\v^\s*\\tableofcontents', + \ 'priority' : 0, + \} + +let s:matcher_index = { + \ 'title' : 'Alphabetical index', + \ 're' : '\v^\s*\\printindex\[?', + \ 'priority' : 0, + \} + +let s:matcher_titlepage = { + \ 'title' : 'Titlepage', + \ 're' : '\v^\s*\\begin\{titlepage\}', + \ 'priority' : 0, + \} + +let s:matcher_bibliography = { + \ 'title' : 'Bibliography', + \ 're' : '\v^\s*\\%(' + \ . 'printbib%(liography|heading)\s*(\{|\[)?' + \ . '|begin\s*\{\s*thebibliography\s*\}' + \ . '|bibliography\s*\{)', + \ 're_biblatex' : '\v^\s*\\printbib%(liography|heading)', + \ 'priority' : 0, + \} +function! s:matcher_bibliography.get_entry(context) abort dict " {{{1 + let l:entry = call('vimtex#parser#toc#get_entry_general', [a:context], self) + + if a:context.line !~# self.re_biblatex + return l:entry + endif + + let self.options = matchstr(a:context.line, self.re_biblatex . '\s*\[\zs.*') + + let [l:end, l:count] = s:find_closing( + \ 0, self.options, !empty(self.options), '[') + if l:count == 0 + let self.options = strpart(self.options, 0, l:end) + call self.parse_options(a:context, l:entry) + else + let self.count = l:count + let s:matcher_continue = deepcopy(self) + endif + + return l:entry +endfunction + +" }}}1 +function! s:matcher_bibliography.continue(context) abort dict " {{{1 + let [l:end, l:count] = s:find_closing(0, a:context.line, self.count, '[') + if l:count == 0 + let self.options .= strpart(a:context.line, 0, l:end) + unlet! s:matcher_continue + call self.parse_options(a:context, a:context.entry) + else + let self.options .= a:context.line + let self.count = l:count + endif +endfunction + +" }}}1 +function! s:matcher_bibliography.parse_options(context, entry) abort dict " {{{1 + " Parse the options + let l:opt_pairs = map(split(self.options, ','), 'split(v:val, ''='')') + let l:opts = {} + for [l:key, l:val] in l:opt_pairs + let l:key = substitute(l:key, '^\s*\|\s*$', '', 'g') + let l:val = substitute(l:val, '^\s*\|\s*$', '', 'g') + let l:val = substitute(l:val, '{\|}', '', 'g') + let l:opts[l:key] = l:val + endfor + + " Check if entry should appear in the TOC + let l:heading = get(l:opts, 'heading') + let a:entry.added_to_toc = l:heading =~# 'intoc\|numbered' + + " Check if entry should be numbered + if l:heading =~# '\v%(sub)?bibnumbered' + if a:context.level.chapter > 0 + let l:levels = ['chapter', 'section'] + else + let l:levels = ['section', 'subsection'] + endif + call a:context.level.increment(l:levels[l:heading =~# '^sub']) + let a:entry.level = a:context.max_level - a:context.level.current + let a:entry.number = deepcopy(a:context.level) + endif + + " Parse title + try + let a:entry.title = remove(l:opts, 'title') + catch /E716/ + let a:entry.title = l:heading =~# '^sub' ? 'References' : 'Bibliography' + endtry +endfunction + +" }}}1 +function! s:matcher_bibliography.filter(entries) abort dict " {{{1 + if !empty( + \ filter(deepcopy(a:entries), 'get(v:val, "added_to_toc")')) + call filter(a:entries, 'get(v:val, "added_to_toc", 1)') + endif +endfunction + +" }}}1 + +let s:matcher_todos = { + \ 're' : g:vimtex#re#not_bslash . '\%\s+(' + \ . join(g:vimtex_toc_todo_keywords, '|') . ')[ :]+\s*(.*)', + \ 'in_preamble' : 1, + \ 'priority' : 2, + \} +function! s:matcher_todos.get_entry(context) abort dict " {{{1 + let [l:type, l:text] = matchlist(a:context.line, self.re)[1:2] + return { + \ 'title' : toupper(l:type) . ': ' . l:text, + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'todo', + \ } +endfunction + +" }}}1 + +let s:matcher_todonotes = { + \ 're' : g:vimtex#re#not_comment . '\\\w*todo\w*%(\[[^]]*\])?\{\zs.*', + \ 'priority' : 2, + \} +function! s:matcher_todonotes.get_entry(context) abort dict " {{{1 + let title = matchstr(a:context.line, self.re) + + let [l:end, l:count] = s:find_closing(0, title, 1, '{') + if l:count == 0 + let title = strpart(title, 0, l:end) + else + let self.count = l:count + let s:matcher_continue = deepcopy(self) + endif + + return { + \ 'title' : 'TODO: ' . title, + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'todo', + \ } +endfunction + +" }}}1 +function! s:matcher_todonotes.continue(context) abort dict " {{{1 + let [l:end, l:count] = s:find_closing(0, a:context.line, self.count, '{') + if l:count == 0 + let a:context.entry.title .= strpart(a:context.line, 0, l:end) + unlet! s:matcher_continue + else + let a:context.entry.title .= a:context.line + let self.count = l:count + endif +endfunction + +" }}}1 + +let s:matcher_labels = { + \ 're' : g:vimtex#re#not_comment . '\\label\{\zs.{-}\ze\}', + \ 'priority' : 1, + \} +function! s:matcher_labels.get_entry(context) abort dict " {{{1 + return { + \ 'title' : matchstr(a:context.line, self.re), + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'label', + \ } +endfunction +" }}}1 + +let s:matcher_beamer_frame = { + \ 're' : '^\s*\\begin{frame}', + \ 'priority' : 0, + \} +function! s:matcher_beamer_frame.get_entry(context) abort dict " {{{1 + let l:title = vimtex#util#trim( + \ matchstr(a:context.line, self.re . '\s*{\zs.*\ze}\s*$')) + + return { + \ 'title' : 'Frame' . (empty(l:title) ? '' : ': ' . l:title), + \ 'number' : '', + \ 'file' : a:context.file, + \ 'line' : a:context.lnum, + \ 'level' : a:context.max_level - a:context.level.current, + \ 'rank' : a:context.lnum_total, + \ 'type' : 'content', + \ } +endfunction +" }}}1 + +" +" Utility functions +" +function! s:clear_texorpdfstring(title) abort " {{{1 + let l:i1 = match(a:title, '\\texorpdfstring') + if l:i1 < 0 | return a:title | endif + + " Find start of included part + let [l:i2, l:dummy] = s:find_closing( + \ match(a:title, '{', l:i1+1), a:title, 1, '{') + let l:i2 = match(a:title, '{', l:i2+1) + if l:i2 < 0 | return a:title | endif + + " Find end of included part + let [l:i3, l:dummy] = s:find_closing(l:i2, a:title, 1, '{') + if l:i3 < 0 | return a:title | endif + + return strpart(a:title, 0, l:i1) + \ . strpart(a:title, l:i2+1, l:i3-l:i2-1) + \ . s:clear_texorpdfstring(strpart(a:title, l:i3+1)) +endfunction + +" }}}1 +function! s:find_closing(start, string, count, type) abort " {{{1 + if a:type ==# '{' + let l:re = '{\|}' + let l:open = '{' + else + let l:re = '\[\|\]' + let l:open = '[' + endif + let l:i2 = a:start-1 + let l:count = a:count + while l:count > 0 + let l:i2 = match(a:string, l:re, l:i2+1) + if l:i2 < 0 | break | endif + + if a:string[l:i2] ==# l:open + let l:count += 1 + else + let l:count -= 1 + endif + endwhile + + return [l:i2, l:count] +endfunction + +" }}}1 +function! s:sort_by_priority(d1, d2) abort " {{{1 + let l:p1 = get(a:d1, 'priority') + let l:p2 = get(a:d2, 'priority') + return l:p1 >= l:p2 ? l:p1 > l:p2 : -1 +endfunction + +" }}}1 + +" +" Section level counter +" +let s:level = {} +function! s:level.reset(part, level) abort dict " {{{1 + if a:part ==# 'preamble' + let self.old = [] + else + let self.old += [copy(self)] + endif + + let self.preamble = 0 + let self.frontmatter = 0 + let self.mainmatter = 0 + let self.appendix = 0 + let self.backmatter = 0 + let self.part = 0 + let self.chapter = 0 + let self.section = 0 + let self.subsection = 0 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + let self.current = a:level + let self[a:part] = 1 +endfunction + +" }}}1 +function! s:level.increment(level) abort dict " {{{1 + let self.current = s:sec_to_value[a:level] + + let self.part_toggle = 0 + + if a:level ==# 'part' + let self.part += 1 + let self.part_toggle = 1 + elseif a:level ==# 'chapter' + let self.chapter += 1 + let self.section = 0 + let self.subsection = 0 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'section' + let self.section += 1 + let self.subsection = 0 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'subsection' + let self.subsection += 1 + let self.subsubsection = 0 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'subsubsection' + let self.subsubsection += 1 + let self.subsubsubsection = 0 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'subsubsubsection' + let self.subsubsubsection += 1 + let self.paragraph = 0 + let self.subparagraph = 0 + elseif a:level ==# 'paragraph' + let self.paragraph += 1 + let self.subparagraph = 0 + elseif a:level ==# 'subparagraph' + let self.subparagraph += 1 + endif +endfunction + +" }}}1 + +let s:sec_to_value = { + \ '_' : 0, + \ 'subparagraph' : 1, + \ 'paragraph' : 2, + \ 'subsubsubsection' : 3, + \ 'subsubsection' : 4, + \ 'subsection' : 5, + \ 'section' : 6, + \ 'chapter' : 7, + \ 'part' : 8, + \ } + +" +" Create the lists of matchers +" +let s:matchers = map( + \ filter(items(s:), 'v:val[0] =~# ''^matcher_'''), + \ 'v:val[1]') + \ + g:vimtex_toc_custom_matchers +call sort(s:matchers, function('s:sort_by_priority')) + +for s:m in s:matchers + if !has_key(s:m, 'get_entry') + let s:m.get_entry = function('vimtex#parser#toc#get_entry_general') + endif +endfor +unlet! s:m + +let s:matchers_preamble = filter( + \ deepcopy(s:matchers), "get(v:val, 'in_preamble')") +let s:matchers_content = filter( + \ deepcopy(s:matchers), "get(v:val, 'in_content', 1)") + +endif |