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  | 
