summaryrefslogtreecommitdiffstats
path: root/autoload/vimtex/parser/bib.vim
diff options
context:
space:
mode:
Diffstat (limited to 'autoload/vimtex/parser/bib.vim')
-rw-r--r--autoload/vimtex/parser/bib.vim370
1 files changed, 370 insertions, 0 deletions
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