]> git.lyx.org Git - lyx.git/blobdiff - src/insets/InsetBibtex.cpp
New attempt on #9906: allow following hyperlinks via context menu.
[lyx.git] / src / insets / InsetBibtex.cpp
index 05a154466a642a62eb75d02ba714161efc0bccc5..876adea54b1770ad15a140a3a8d50eb2890edff5 100644 (file)
 #include "FuncRequest.h"
 #include "FuncStatus.h"
 #include "LaTeXFeatures.h"
-#include "output_xhtml.h"
+#include "output_latex.h"
+#include "xml.h"
 #include "OutputParams.h"
 #include "PDFOptions.h"
 #include "texstream.h"
 #include "TextClass.h"
+#include "TocBackend.h"
 
 #include "frontends/alert.h"
 
 #include "support/convert.h"
 #include "support/debug.h"
 #include "support/docstream.h"
+#include "support/docstring_list.h"
 #include "support/ExceptionMessage.h"
 #include "support/FileNameList.h"
 #include "support/filetools.h"
+#include "support/regex.h"
 #include "support/gettext.h"
 #include "support/lstrings.h"
 #include "support/os.h"
 #include "support/textutils.h"
 
 #include <limits>
+#include <map>
+#include <utility>
+
+#include <iostream>
 
 using namespace std;
 using namespace lyx::support;
@@ -59,19 +67,7 @@ namespace os = support::os;
 
 InsetBibtex::InsetBibtex(Buffer * buf, InsetCommandParams const & p)
        : InsetCommand(buf, p)
-{
-       buffer().invalidateBibfileCache();
-       buffer().removeBiblioTempFiles();
-}
-
-
-InsetBibtex::~InsetBibtex()
-{
-       if (isBufferLoaded()) {
-               buffer().invalidateBibfileCache();
-               buffer().removeBiblioTempFiles();
-       }
-}
+{}
 
 
 ParamInfo const & InsetBibtex::findInfo(string const & /* cmdName */)
@@ -81,6 +77,8 @@ ParamInfo const & InsetBibtex::findInfo(string const & /* cmdName */)
                param_info_.add("btprint", ParamInfo::LATEX_OPTIONAL);
                param_info_.add("bibfiles", ParamInfo::LATEX_REQUIRED);
                param_info_.add("options", ParamInfo::LYX_INTERNAL);
+               param_info_.add("encoding", ParamInfo::LYX_INTERNAL);
+               param_info_.add("file_encodings", ParamInfo::LYX_INTERNAL);
                param_info_.add("biblatexopts", ParamInfo::LATEX_OPTIONAL);
        }
        return param_info_;
@@ -92,7 +90,7 @@ void InsetBibtex::doDispatch(Cursor & cur, FuncRequest & cmd)
        switch (cmd.action()) {
 
        case LFUN_INSET_EDIT:
-               editDatabases();
+               editDatabases(cmd.argument());
                break;
 
        case LFUN_INSET_MODIFY: {
@@ -113,8 +111,7 @@ void InsetBibtex::doDispatch(Cursor & cur, FuncRequest & cmd)
 
                cur.recordUndo();
                setParams(p);
-               buffer().invalidateBibfileCache();
-               buffer().removeBiblioTempFiles();
+               cur.buffer()->clearBibFileCache();
                cur.forceBufferUpdate();
                break;
        }
@@ -140,15 +137,15 @@ bool InsetBibtex::getStatus(Cursor & cur, FuncRequest const & cmd,
 }
 
 
-void InsetBibtex::editDatabases() const
+void InsetBibtex::editDatabases(docstring const & db) const
 {
        vector<docstring> bibfilelist = getVectorFromString(getParam("bibfiles"));
 
        if (bibfilelist.empty())
                return;
 
-       int nr_databases = bibfilelist.size();
-       if (nr_databases > 1) {
+       size_t nr_databases = bibfilelist.size();
+       if (nr_databases > 1 && db.empty()) {
                        docstring const engine = usingBiblatex() ? _("Biblatex") : _("BibTeX");
                        docstring message = bformat(_("The %1$s[[BibTeX/Biblatex]] inset includes %2$s databases.\n"
                                                       "If you proceed, all of them will be opened."),
@@ -163,9 +160,11 @@ void InsetBibtex::editDatabases() const
        vector<docstring>::const_iterator it = bibfilelist.begin();
        vector<docstring>::const_iterator en = bibfilelist.end();
        for (; it != en; ++it) {
-               FileName const bibfile = getBibTeXPath(*it, buffer());
-               formats.edit(buffer(), bibfile,
-                    formats.getFormatFromFile(bibfile));
+               if (!db.empty() && db != *it)
+                       continue;
+               FileName const bibfile = buffer().getBibfilePath(*it);
+               theFormats().edit(buffer(), bibfile,
+                    theFormats().getFormatFromFile(bibfile));
        }
 }
 
@@ -222,6 +221,12 @@ docstring InsetBibtex::toolTip(BufferView const & /*bv*/, int /*x*/, int /*y*/)
                        tip += ", ";
                        tip += _("included in TOC");
                }
+               if (!buffer().parent()
+                   && buffer().params().multibib == "child") {
+                       tip += "<br />";
+                       tip += _("Note: This bibliography is not output, since bibliographies in the master file "
+                                "are not allowed with the setting 'Multiple bibliographies per child document'");
+               }
        } else {
                tip += _("Lists:") + " ";
                if (btprint == "bibbysection")
@@ -235,8 +240,7 @@ docstring InsetBibtex::toolTip(BufferView const & /*bv*/, int /*x*/, int /*y*/)
                        tip += _("included in TOC");
                }
                if (!getParam("biblatexopts").empty()) {
-                       if (toc)
-                               tip += "<br />";
+                       tip += "<br />";
                        tip += _("Options: ") + getParam("biblatexopts");
                }
        }
@@ -262,6 +266,20 @@ void InsetBibtex::latex(otexstream & os, OutputParams const & runparams) const
        // or
        // \bibbysection[biblatexopts] - if btprint is "bibbysection"
 
+       // chapterbib does not allow bibliographies in the master
+       if (!usingBiblatex() && !runparams.is_child
+           && buffer().params().multibib == "child")
+               return;
+
+       if (runparams.inDeletedInset) {
+               // We cannot strike-out bibligraphies,
+               // so we just output a note.
+               os << "\\textbf{"
+                  << buffer().B_("[BIBLIOGRAPHY DELETED!]")
+                  << "}";
+               return;
+       }
+
        string style = to_utf8(getParam("options")); // maybe empty! and with bibtotoc
        string bibtotoc;
        if (prefixIs(style, "bibtotoc")) {
@@ -289,8 +307,11 @@ void InsetBibtex::latex(otexstream & os, OutputParams const & runparams) const
                os << "\n";
        } else {// using BibTeX
                // Database(s)
-               vector<docstring> const db_out =
+               vector<pair<docstring, string>> const dbs =
                        buffer().prepareBibFilePaths(runparams, getBibFiles(), false);
+               vector<docstring> db_out;
+               for (pair<docstring, string> const & db : dbs)
+                       db_out.push_back(db.first);
                // Style options
                if (style == "default")
                        style = buffer().masterParams().defaultBiblioStyle();
@@ -329,6 +350,19 @@ void InsetBibtex::latex(otexstream & os, OutputParams const & runparams) const
                                       _("There are spaces in the path to your BibTeX style file.\n"
                                                      "BibTeX will be unable to find it."));
                }
+               // Encoding
+               bool encoding_switched = false;
+               Encoding const * const save_enc = runparams.encoding;
+               docstring const encoding = getParam("encoding");
+               if (!encoding.empty() && encoding != from_ascii("default")) {
+                       Encoding const * const enc = encodings.fromLyXName(to_ascii(encoding));
+                       if (enc != runparams.encoding) {
+                               os << "\\bgroup";
+                               switchEncoding(os.os(), buffer().params(), runparams, *enc, true);
+                               runparams.encoding = enc;
+                               encoding_switched = true;
+                       }
+               }
                // Handle the bibtopic case
                if (!db_out.empty() && buffer().masterParams().useBibtopic()) {
                        os << "\\begin{btSect}";
@@ -343,7 +377,8 @@ void InsetBibtex::latex(otexstream & os, OutputParams const & runparams) const
                           << "\\end{btSect}\n";
                }
                // bibtotoc option
-               if (!bibtotoc.empty() && !buffer().masterParams().useBibtopic()) {
+               if (!bibtotoc.empty() && !buffer().masterParams().useBibtopic()
+                   && !buffer().masterParams().documentClass().bibInToc()) {
                        // set label for hyperref, see http://www.lyx.org/trac/ticket/6470
                        if (buffer().masterParams().pdfoptions().use_hyperref)
                                        os << "\\phantomsection";
@@ -360,34 +395,20 @@ void InsetBibtex::latex(otexstream & os, OutputParams const & runparams) const
                        }
                        os << "\\bibliography{" << getStringFromVector(db_out) << "}\n";
                }
+               if (encoding_switched){
+                       // Switch back
+                       switchEncoding(os.os(), buffer().params(),
+                                      runparams, *save_enc, true, true);
+                       os << "\\egroup" << breakln;
+                       runparams.encoding = save_enc;
+               }
        }
 }
 
 
-support::FileNamePairList InsetBibtex::getBibFiles() const
+docstring_list InsetBibtex::getBibFiles() const
 {
-       FileName path(buffer().filePath());
-       support::PathChanger p(path);
-
-       // We need to store both the real FileName and the way it is entered
-       // (with full path, rel path or as a single file name).
-       // The latter is needed for biblatex's central bibfile macro.
-       support::FileNamePairList vec;
-
-       vector<docstring> bibfilelist = getVectorFromString(getParam("bibfiles"));
-       vector<docstring>::const_iterator it = bibfilelist.begin();
-       vector<docstring>::const_iterator en = bibfilelist.end();
-       for (; it != en; ++it) {
-               FileName const file = getBibTeXPath(*it, buffer());
-
-               if (!file.empty())
-                       vec.push_back(make_pair(*it, file));
-               else
-                       LYXERR0("Couldn't find " + to_utf8(*it) + " in InsetBibtex::getBibFiles()!");
-       }
-
-       return vec;
-
+       return getVectorFromString(getParam("bibfiles"));
 }
 
 namespace {
@@ -437,7 +458,7 @@ namespace {
                keepCase
        };
 
-       /// remove whitespace characters, read characer sequence
+       /// remove whitespace characters, read character sequence
        /// not containing whitespace characters or characters in
        /// delimChars, and remove further whitespace characters.
        ///
@@ -463,11 +484,9 @@ namespace {
                        return false;
 
                // read value
-               bool legalChar = true;
                while (ifs && !isSpace(ch) &&
-                                                delimChars.find(ch) == docstring::npos &&
-                                                (legalChar = (illegalChars.find(ch) == docstring::npos))
-                                       )
+                      delimChars.find(ch) == docstring::npos &&
+                      illegalChars.find(ch) == docstring::npos)
                {
                        if (chCase == makeLowerCase)
                                val += lowercase(ch);
@@ -476,7 +495,7 @@ namespace {
                        ifs.get(ch);
                }
 
-               if (!legalChar) {
+               if (illegalChars.find(ch) != docstring::npos) {
                        ifs.putback(ch);
                        return false;
                }
@@ -496,7 +515,7 @@ namespace {
        /// read subsequent bibtex values that are delimited with a #-character.
        /// Concatenate all parts and replace names with the associated string in
        /// the variable strings.
-       /// @return true if reading was successfull (all single parts were delimited
+       /// @return true if reading was successful (all single parts were delimited
        /// correctly)
        bool readValue(docstring & val, ifdocstream & ifs, const VarMap & strings) {
 
@@ -628,16 +647,16 @@ namespace {
 
                return true;
        }
-}
+} // namespace
 
 
-void InsetBibtex::collectBibKeys(InsetIterator const & /*di*/) const
+void InsetBibtex::collectBibKeys(InsetIterator const & /*di*/, FileNameList & checkedFiles) const
 {
-       parseBibTeXFiles();
+       parseBibTeXFiles(checkedFiles);
 }
 
 
-void InsetBibtex::parseBibTeXFiles() const
+void InsetBibtex::parseBibTeXFiles(FileNameList & checkedFiles) const
 {
        // This bibtex parser is a first step to parse bibtex files
        // more precisely.
@@ -651,27 +670,42 @@ void InsetBibtex::parseBibTeXFiles() const
        //   bibtex does.
        //
        // Officially bibtex does only support ASCII, but in practice
-       // you can use the encoding of the main document as long as
-       // some elements like keys and names are pure ASCII. Therefore
-       // we convert the file from the buffer encoding.
+       // you can use any encoding as long as some elements like keys
+       // and names are pure ASCII. We support specifying an encoding,
+       // and we convert the file from that (default is buffer encoding).
        // We don't restrict keys to ASCII in LyX, since our own
        // InsetBibitem can generate non-ASCII keys, and nonstandard
        // 8bit clean bibtex forks exist.
 
        BiblioInfo keylist;
 
-       support::FileNamePairList const files = getBibFiles();
-       support::FileNamePairList::const_iterator it = files.begin();
-       support::FileNamePairList::const_iterator en = files.end();
-       for (; it != en; ++ it) {
-               ifdocstream ifs(it->second.toFilesystemEncoding().c_str(),
-                       ios_base::in, buffer().masterParams().encoding().iconvName());
+       docstring_list const files = getBibFiles();
+       for (auto const & bf : files) {
+               FileName const bibfile = buffer().getBibfilePath(bf);
+               if (bibfile.empty()) {
+                       LYXERR0("Unable to find path for " << bf << "!");
+                       continue;
+               }
+               if (find(checkedFiles.begin(), checkedFiles.end(), bibfile) != checkedFiles.end())
+                       // already checked this one. Skip.
+                       continue;
+               else
+                       // record that we check this.
+                       checkedFiles.push_back(bibfile);
+               string encoding = buffer().masterParams().encoding().iconvName();
+               string ienc = buffer().masterParams().bibFileEncoding(to_utf8(bf));
+               if (ienc.empty() || ienc == "general")
+                       ienc = to_ascii(params()["encoding"]);
+
+               if (!ienc.empty() && ienc != "auto-legacy-plain" && ienc != "auto-legacy" && encodings.fromLyXName(ienc))
+                       encoding = encodings.fromLyXName(ienc)->iconvName();
+               ifdocstream ifs(bibfile.toFilesystemEncoding().c_str(),
+                       ios_base::in, encoding);
 
                char_type ch;
                VarMap strings;
 
                while (ifs) {
-
                        ifs.get(ch);
                        if (!ifs)
                                break;
@@ -830,18 +864,6 @@ void InsetBibtex::parseBibTeXFiles() const
 }
 
 
-FileName InsetBibtex::getBibTeXPath(docstring const & filename, Buffer const & buf)
-{
-       string texfile = changeExtension(to_utf8(filename), "bib");
-       // note that, if the filename can be found directly from the path,
-       // findtexfile will just return a FileName object for that path.
-       FileName file(findtexfile(texfile, "bib"));
-       if (file.empty())
-               file = FileName(makeAbsPath(texfile, buf.filePath()));
-       return file;
-}
-
-
 bool InsetBibtex::addDatabase(docstring const & db)
 {
        docstring bibfiles = getParam("bibfiles");
@@ -876,8 +898,11 @@ bool InsetBibtex::delDatabase(docstring const & db)
 
 void InsetBibtex::validate(LaTeXFeatures & features) const
 {
-       if (features.buffer().masterParams().useBibtopic())
+       BufferParams const & mparams = features.buffer().masterParams();
+       if (mparams.useBibtopic())
                features.require("bibtopic");
+       else if (!mparams.useBiblatex() && mparams.multibib == "child")
+               features.require("chapterbib");
        // FIXME XHTML
        // It'd be better to be able to get this from an InsetLayout, but at present
        // InsetLayouts do not seem really to work for things that aren't InsetTexts.
@@ -888,10 +913,72 @@ void InsetBibtex::validate(LaTeXFeatures & features) const
 }
 
 
+void InsetBibtex::updateBuffer(ParIterator const &, UpdateType, bool const /*deleted*/)
+{
+       buffer().registerBibfiles(getBibFiles());
+       // record encoding of bib files for biblatex
+       string const enc = (params()["encoding"] == from_ascii("default")) ?
+                               string() : to_ascii(params()["encoding"]);
+       bool invalidate = false;
+       if (buffer().params().bibEncoding() != enc) {
+               buffer().params().setBibEncoding(enc);
+               invalidate = true;
+       }
+       map<string, string> encs = getFileEncodings();
+       map<string, string>::const_iterator it = encs.begin();
+       for (; it != encs.end(); ++it) {
+               if (buffer().params().bibFileEncoding(it->first) != it->second) {
+                       buffer().params().setBibFileEncoding(it->first, it->second);
+                       invalidate = true;
+               }
+       }
+       if (invalidate)
+               buffer().invalidateBibinfoCache();
+}
+
+
+map<string, string> InsetBibtex::getFileEncodings() const
+{
+       vector<string> ps =
+               getVectorFromString(to_utf8(getParam("file_encodings")), "\t");
+       std::map<string, string> res;
+       for (string const & s: ps) {
+               string key;
+               string val = split(s, key, ' ');
+               res[key] = val;
+       }
+       return res;
+}
+
+
+docstring InsetBibtex::getRefLabel() const
+{
+       if (buffer().masterParams().documentClass().hasLaTeXLayout("chapter"))
+               return buffer().B_("Bibliography");
+       return buffer().B_("References");
+}
+
+
+void InsetBibtex::addToToc(DocIterator const & cpit, bool output_active,
+                          UpdateType, TocBackend & backend) const
+{
+       if (!prefixIs(to_utf8(getParam("options")), "bibtotoc"))
+               return;
+
+       docstring const str = getRefLabel();
+       shared_ptr<Toc> toc = backend.toc("tableofcontents");
+       // Assign to appropriate level
+       int const item_depth =
+               (buffer().masterParams().documentClass().hasLaTeXLayout("chapter")) 
+                       ? 1 : 2;
+       toc->push_back(TocItem(cpit, item_depth, str, output_active));
+}
+
+
 int InsetBibtex::plaintext(odocstringstream & os,
        OutputParams const & op, size_t max_length) const
 {
-       docstring const reflabel = buffer().B_("References");
+       docstring const reflabel = getRefLabel();
 
        // We could output more information here, e.g., what databases are included
        // and information about options. But I don't necessarily see any reason to
@@ -926,22 +1013,22 @@ int InsetBibtex::plaintext(odocstringstream & os,
                refoutput += "[" + entry.label() + "] ";
                // FIXME Right now, we are calling BibInfo::getInfo on the key,
                // which will give us all the cross-referenced info. But for every
-               // entry, so there's a lot of repitition. This should be fixed.
+               // entry, so there's a lot of repetition. This should be fixed.
                refoutput += bibinfo.getInfo(entry.key(), buffer(), ci) + "\n\n";
        }
        os << refoutput;
-       return refoutput.size();
+       return int(refoutput.size());
 }
 
 
 // FIXME
 // docstring InsetBibtex::entriesAsXHTML(vector<docstring> const & entries)
 // And then here just: entriesAsXHTML(buffer().masterBibInfo().citedEntries())
-docstring InsetBibtex::xhtml(XHTMLStream & xs, OutputParams const &) const
+docstring InsetBibtex::xhtml(XMLStream & xs, OutputParams const &) const
 {
        BiblioInfo const & bibinfo = buffer().masterBibInfo();
        bool const all_entries = getParam("btprint") == "btPrintAll";
-       vector<docstring> const & cites = 
+       vector<docstring> const & cites =
            all_entries ? bibinfo.getKeys() : bibinfo.citedEntries();
 
        docstring const reflabel = buffer().B_("References");
@@ -952,10 +1039,10 @@ docstring InsetBibtex::xhtml(XHTMLStream & xs, OutputParams const &) const
        ci.richtext = true;
        ci.max_key_size = UINT_MAX;
 
-       xs << html::StartTag("h2", "class='bibtex'")
+       xs << xml::StartTag("h2", "class='bibtex'")
                << reflabel
-               << html::EndTag("h2")
-               << html::StartTag("div", "class='bibtex'");
+               << xml::EndTag("h2")
+               << xml::StartTag("div", "class='bibtex'");
 
        // Now we loop over the entries
        vector<docstring>::const_iterator vit = cites.begin();
@@ -966,32 +1053,366 @@ docstring InsetBibtex::xhtml(XHTMLStream & xs, OutputParams const &) const
                        continue;
 
                BibTeXInfo const & entry = biit->second;
-               string const attr = "class='bibtexentry' id='LyXCite-" 
-                   + to_utf8(html::cleanAttr(entry.key())) + "'";
-               xs << html::StartTag("div", attr);
-               
+               string const attr = "class='bibtexentry' id='LyXCite-"
+                   + to_utf8(xml::cleanAttr(entry.key())) + "'";
+               xs << xml::StartTag("div", attr);
+
                // don't print labels if we're outputting all entries
                if (!all_entries) {
-                       xs << html::StartTag("span", "class='bibtexlabel'")
+                       xs << xml::StartTag("span", "class='bibtexlabel'")
                                << entry.label()
-                               << html::EndTag("span");
+                               << xml::EndTag("span");
                }
-               
+
                // FIXME Right now, we are calling BibInfo::getInfo on the key,
                // which will give us all the cross-referenced info. But for every
-               // entry, so there's a lot of repitition. This should be fixed.
-               xs << html::StartTag("span", "class='bibtexinfo'")
-                  << XHTMLStream::ESCAPE_AND
+               // entry, so there's a lot of repetition. This should be fixed.
+               xs << xml::StartTag("span", "class='bibtexinfo'")
+                  << XMLStream::ESCAPE_AND
                   << bibinfo.getInfo(entry.key(), buffer(), ci)
-                  << html::EndTag("span")
-                  << html::EndTag("div")
-                  << html::CR();
+                  << xml::EndTag("span")
+                  << xml::EndTag("div")
+                  << xml::CR();
        }
-       xs << html::EndTag("div");
+       xs << xml::EndTag("div");
        return docstring();
 }
 
 
+void InsetBibtex::docbook(XMLStream & xs, OutputParams const &) const
+{
+       BiblioInfo const & bibinfo = buffer().masterBibInfo();
+       bool const all_entries = getParam("btprint") == "btPrintAll";
+       vector<docstring> const & cites =
+                       all_entries ? bibinfo.getKeys() : bibinfo.citedEntries();
+
+       docstring const reflabel = buffer().B_("References");
+
+       // Tell BiblioInfo our purpose (i.e. generate HTML rich text).
+       CiteItem ci;
+       ci.context = CiteItem::Export;
+       ci.richtext = true;
+       ci.max_key_size = UINT_MAX;
+
+       // Header for bibliography (title required).
+       xs << xml::StartTag("bibliography");
+       xs << xml::CR();
+       xs << xml::StartTag("title");
+       xs << reflabel;
+       xs << xml::EndTag("title") << xml::CR();
+
+       // Translation between keys in each entry and DocBook tags.
+       // IDs for publications; list: http://tdg.docbook.org/tdg/5.2/biblioid.html.
+       vector<pair<string, string>> biblioId = { // <bibtex, docbook>
+               make_pair("doi", "doi"),
+               make_pair("isbn", "isbn"),
+               make_pair("issn", "issn"),
+               make_pair("isrn", "isrn"),
+               make_pair("istc", "istc"),
+               make_pair("lccn", "libraryofcongress"),
+               make_pair("number", "pubsnumber"),
+               make_pair("url", "uri")
+       };
+       // Relations between documents.
+       vector<pair<string, string>> relations = { // <bibtex, docbook biblioset relation>
+               make_pair("journal", "journal"),
+               make_pair("booktitle", "book"),
+               make_pair("series", "series")
+       };
+       // Various things that do not fit DocBook.
+       vector<string> misc = { "language", "school", "note" };
+
+       // Store the mapping between BibTeX and DocBook.
+       map<string, string> toDocBookTag;
+       toDocBookTag["fullnames:author"] = "SPECIFIC"; // No direct translation to DocBook: <authorgroup>.
+       toDocBookTag["publisher"] = "SPECIFIC"; // No direct translation to DocBook: <publisher>.
+       toDocBookTag["address"] = "SPECIFIC"; // No direct translation to DocBook: <publisher>.
+       toDocBookTag["editor"] = "editor";
+       toDocBookTag["institution"] = "SPECIFIC"; // No direct translation to DocBook: <org>.
+
+       toDocBookTag["title"] = "title";
+       toDocBookTag["volume"] = "volumenum";
+       toDocBookTag["edition"] = "edition";
+       toDocBookTag["pages"] = "artpagenums";
+
+       toDocBookTag["abstract"] = "SPECIFIC"; // No direct translation to DocBook: <abstract>.
+       toDocBookTag["keywords"] = "SPECIFIC"; // No direct translation to DocBook: <keywordset>.
+       toDocBookTag["year"] = "SPECIFIC"; // No direct translation to DocBook: <pubdate>.
+       toDocBookTag["month"] = "SPECIFIC"; // No direct translation to DocBook: <pubdate>.
+
+       toDocBookTag["journal"] = "SPECIFIC"; // No direct translation to DocBook: <biblioset>.
+       toDocBookTag["booktitle"] = "SPECIFIC"; // No direct translation to DocBook: <biblioset>.
+       toDocBookTag["series"] = "SPECIFIC"; // No direct translation to DocBook: <biblioset>.
+
+       for (auto const & id: biblioId)
+           toDocBookTag[id.first] = "SPECIFIC"; // No direct translation to DocBook: <biblioid>.
+       for (auto const & id: relations)
+           toDocBookTag[id.first] = "SPECIFIC"; // No direct translation to DocBook: <biblioset>.
+       for (auto const & id: misc)
+           toDocBookTag[id] = "SPECIFIC"; // No direct translation to DocBook: <bibliomisc>.
+
+       // Loop over the entries. If there are no entries, add a comment to say so.
+       auto vit = cites.begin();
+       auto ven = cites.end();
+
+       if (vit == ven) {
+               xs << XMLStream::ESCAPE_NONE << "<!-- No entry in the bibliography. -->";
+       }
+
+       for (; vit != ven; ++vit) {
+               BiblioInfo::const_iterator const biit = bibinfo.find(*vit);
+               if (biit == bibinfo.end())
+                       continue;
+
+               BibTeXInfo const & entry = biit->second;
+               string const attr = "xml:id=\"" + to_utf8(xml::cleanID(entry.key())) + "\"";
+               xs << xml::StartTag("biblioentry", attr);
+               xs << xml::CR();
+
+               // FIXME Right now, we are calling BibInfo::getInfo on the key,
+               // which will give us all the cross-referenced info. But for every
+               // entry, so there's a lot of repetition. This should be fixed.
+
+               // Parse the results of getInfo and emit the corresponding DocBook tags. Interesting pieces have the form
+               // "<span class="bib-STH">STH</span>", the rest of the text may be discarded.
+               // Could have written a DocBook version of expandFormat (that parses a citation into HTML), but it implements
+               // some kind of recursion. Still, a (static) conversion step between the citation format and DocBook would have
+               // been required. All in all, both codes approaches would have been similar, but this parsing allows relying
+               // on existing building blocks.
+
+               string html = to_utf8(bibinfo.getInfo(entry.key(), buffer(), ci));
+               regex tagRegex("<span class=\"bib-([^\"]*)\">([^<]*)</span>");
+               smatch match;
+               auto tagIt = lyx::sregex_iterator(html.cbegin(), html.cend(), tagRegex, regex_constants::match_default);
+               auto tagEnd = lyx::sregex_iterator();
+               map<string, string> delayedTags;
+
+               // Read all tags from HTML and convert those that have a 1:1 matching.
+               while (tagIt != tagEnd) {
+                       string tag = tagIt->str(); // regex_match cannot work with temporary strings.
+                       ++tagIt;
+
+                       if (regex_match(tag, match, tagRegex)) {
+                               if (toDocBookTag[match[1]] == "SPECIFIC") {
+                                       delayedTags[match[1]] = match[2];
+                               } else {
+                                       xs << xml::StartTag(toDocBookTag[match[1]]);
+                                       xs << from_utf8(match[2].str());
+                                       xs << xml::EndTag(toDocBookTag[match[1]]);
+                               }
+                       } else {
+                               LYXERR0("The BibTeX field " << match[1].str() << " is unknown.");
+                               xs << XMLStream::ESCAPE_NONE << from_utf8("<!-- Output Error: The BibTeX field " + match[1].str() + " is unknown -->\n");
+                       }
+               }
+
+               // Type of document (book, journal paper, etc.).
+               xs << xml::StartTag("bibliomisc", "role=\"type\"");
+               xs << entry.entryType();
+               xs << xml::EndTag("bibliomisc");
+               xs << xml::CR();
+
+               // Handle tags that have complex transformations.
+               if (! delayedTags.empty()) {
+                       unsigned long remainingTags = delayedTags.size(); // Used as a workaround. With GCC 7, when erasing all
+                       // elements one by one, some elements may still pop in later on (even though they were deleted previously).
+                       auto hasTag = [&delayedTags](string key) { return delayedTags.find(key) != delayedTags.end(); };
+                       auto getTag = [&delayedTags](string key) { return from_utf8(delayedTags[key]); };
+                       auto eraseTag = [&delayedTags, &remainingTags](string key) {
+                               remainingTags -= 1;
+                               delayedTags.erase(key);
+                       };
+
+                       // Notes on order of checks.
+                       // - address goes with publisher if there is one, so check this first. Otherwise, the address goes with
+                       //   the entry without other details.
+
+                       // <publisher>
+                       if (hasTag("publisher")) {
+                               xs << xml::StartTag("publisher");
+                               xs << xml::CR();
+                               xs << xml::StartTag("publishername");
+                               xs << getTag("publisher");
+                               xs << xml::EndTag("publishername");
+                               xs << xml::CR();
+
+                               if (hasTag("address")) {
+                                       xs << xml::StartTag("address");
+                                       xs << getTag("address");
+                                       xs << xml::EndTag("address");
+                                       eraseTag("address");
+                               }
+
+                               xs << xml::EndTag("publisher");
+                               xs << xml::CR();
+                               eraseTag("publisher");
+                       }
+
+                       if (hasTag("address")) {
+                               xs << xml::StartTag("address");
+                               xs << getTag("address");
+                               xs << xml::EndTag("address");
+                               eraseTag("address");
+                       }
+
+                       // <keywordset>
+                       if (hasTag("keywords")) {
+                               // Split the keywords on comma.
+                               docstring keywordSet = getTag("keywords");
+                               vector<docstring> keywords;
+                               if (keywordSet.find(from_utf8(",")) == string::npos) {
+                                       keywords = { keywordSet };
+                               } else {
+                                       size_t pos = 0;
+                                       while ((pos = keywordSet.find(from_utf8(","))) != string::npos) {
+                                               keywords.push_back(keywordSet.substr(0, pos));
+                                               keywordSet.erase(0, pos + 1);
+                                       }
+                                       keywords.push_back(keywordSet);
+                               }
+
+                               xs << xml::StartTag("keywordset") << xml::CR();
+                               for (auto & kw: keywords) {
+                                       kw.erase(kw.begin(), std::find_if(kw.begin(), kw.end(),
+                                                                         [](int c) {return !std::isspace(c);}));
+                                       xs << xml::StartTag("keyword");
+                                       xs << kw;
+                                       xs << xml::EndTag("keyword");
+                                       xs << xml::CR();
+                               }
+                               xs << xml::EndTag("keywordset") << xml::CR();
+                               eraseTag("keywords");
+                       }
+
+                       // <copyright>
+                       // Example: http://tdg.docbook.org/tdg/5.1/biblioset.html
+                       if (hasTag("year")) {
+                               docstring value = getTag("year");
+                               eraseTag("year");
+
+                               // Follow xsd:gYearMonth format (http://books.xmlschemata.org/relaxng/ch19-77135.html).
+                               if (hasTag("month")) {
+                                       value += "-" + getTag("month");
+                                       eraseTag("month");
+                               }
+
+                               xs << xml::StartTag("pubdate");
+                               xs << value;
+                               xs << xml::EndTag("pubdate");
+                               xs << xml::CR();
+                       }
+
+                       // <institution>
+                       if (hasTag("institution")) {
+                               xs << xml::StartTag("org");
+                               xs << xml::CR();
+                               xs << xml::StartTag("orgname");
+                               xs << getTag("institution");
+                               xs << xml::EndTag("orgname");
+                               xs << xml::CR();
+                               xs << xml::EndTag("org");
+                               xs << xml::CR();
+                               eraseTag("institution");
+                       }
+
+                       // <biblioset>
+                       // Example: http://tdg.docbook.org/tdg/5.1/biblioset.html
+                       for (auto const & id: relations) {
+                               if (hasTag(id.first)) {
+                                       xs << xml::StartTag("biblioset", "relation=\"" + id.second + "\"");
+                                       xs << xml::CR();
+                                       xs << xml::StartTag("title");
+                                       xs << getTag(id.first);
+                                       xs << xml::EndTag("title");
+                                       xs << xml::CR();
+                                       xs << xml::EndTag("biblioset");
+                                       xs << xml::CR();
+                                       eraseTag(id.first);
+                               }
+                       }
+
+                       // <authorgroup>
+                       // Example: http://tdg.docbook.org/tdg/5.1/authorgroup.html
+                       if (hasTag("fullnames:author")) {
+                               // Perform full parsing of the BibTeX string, dealing with the many corner cases that might
+                               // be encountered.
+                               authorsToDocBookAuthorGroup(getTag("fullnames:author"), xs, buffer());
+                               eraseTag("fullnames:author");
+                       }
+
+                       // <abstract>
+                       if (hasTag("abstract")) {
+                               // Split the paragraphs on new line.
+                               docstring abstract = getTag("abstract");
+                               vector<docstring> paragraphs;
+                               if (abstract.find(from_utf8("\n")) == string::npos) {
+                                       paragraphs = { abstract };
+                               } else {
+                                       size_t pos = 0;
+                                       while ((pos = abstract.find(from_utf8(","))) != string::npos) {
+                                               paragraphs.push_back(abstract.substr(0, pos));
+                                               abstract.erase(0, pos + 1);
+                                       }
+                                       paragraphs.push_back(abstract);
+                               }
+
+                               xs << xml::StartTag("abstract");
+                               xs << xml::CR();
+                               for (auto const & para: paragraphs) {
+                                       if (para.empty())
+                                               continue;
+                                       xs << xml::StartTag("para");
+                                       xs << para;
+                                       xs << xml::EndTag("para");
+                               }
+                               xs << xml::CR();
+                               xs << xml::EndTag("abstract");
+                               xs << xml::CR();
+                               eraseTag("abstract");
+                       }
+
+                       // <biblioid>
+                       for (auto const & id: biblioId) {
+                               if (hasTag(id.first)) {
+                                       xs << xml::StartTag("biblioid", "class=\"" + id.second + "\"");
+                                       xs << getTag(id.first);
+                                       xs << xml::EndTag("biblioid");
+                                       xs << xml::CR();
+                                       eraseTag(id.first);
+                               }
+                       }
+
+                       // <bibliomisc>
+                       for (auto const & id: misc) {
+                               if (hasTag(id)) {
+                                       xs << xml::StartTag("bibliomisc", "role=\"" + id + "\"");
+                                       xs << getTag(id);
+                                       xs << xml::EndTag("bibliomisc");
+                                       xs << xml::CR();
+                                       eraseTag(id);
+                               }
+                       }
+
+                       // After all tags are processed, check for errors.
+                       if (remainingTags > 0) {
+                               LYXERR0("Still delayed tags not yet handled.");
+                               xs << XMLStream::ESCAPE_NONE << from_utf8("<!-- Output Error: still delayed tags not yet handled.\n");
+                               for (auto const & item: delayedTags) {
+                                       xs << from_utf8(" " + item.first + ": " + item.second + "\n");
+                               }
+                               xs << XMLStream::ESCAPE_NONE << from_utf8(" -->\n");
+                       }
+               }
+
+               xs << xml::EndTag("biblioentry");
+               xs << xml::CR();
+       }
+
+       // Footer for bibliography.
+       xs << xml::EndTag("bibliography");
+}
+
+
 void InsetBibtex::write(ostream & os) const
 {
        params().Write(os, &buffer());