]> git.lyx.org Git - lyx.git/blob - src/frontends/controllers/biblio.C
frontends/controllers/ControlBibtex.[Ch]: new members bibtotoc(), getStylefile(),
[lyx.git] / src / frontends / controllers / biblio.C
1 /**
2  * \file biblio.C
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Angus Leeming
7  * \author Herbert Voß
8  *
9  * Full author contact details are available in file CREDITS.
10  */
11
12 #include <config.h>
13
14 #include "biblio.h"
15
16 #include "buffer.h"
17 #include "bufferparams.h"
18 #include "gettext.h"
19
20 #include "support/lstrings.h"
21
22 #include <boost/regex.hpp>
23
24 #include <algorithm>
25 #include <sstream>
26
27 using std::string;
28 using std::ostringstream;
29 using std::vector;
30
31
32 namespace lyx {
33
34 using support::ascii_lowercase;
35 using support::bformat;
36 using support::compare_ascii_no_case;
37 using support::contains;
38 using support::getVectorFromString;
39 using support::ltrim;
40 using support::prefixIs;
41 using support::rtrim;
42 using support::split;
43 using support::subst;
44 using support::token;
45 using support::trim;
46
47 namespace biblio {
48
49 namespace {
50
51 vector<string> const init_possible_cite_commands()
52 {
53         char const * const pos[] = {
54                 "cite",
55                 "citet", "citep", "citealt", "citealp",
56                 "citeauthor", "citeyear", "citeyearpar",
57                 "citet*", "citep*", "citealt*", "citealp*", "citeauthor*",
58                 "Citet",  "Citep",  "Citealt",  "Citealp",  "Citeauthor",
59                 "Citet*", "Citep*", "Citealt*", "Citealp*", "Citeauthor*",
60                 "fullcite",
61                 "footcite", "footcitet", "footcitep", "footcitealt",
62                 "footcitealp", "footciteauthor", "footciteyear",
63                 "footciteyearpar",
64                 "citefield",
65                 "citetitle",
66                 "cite*"
67         };
68         size_t const size_pos = sizeof(pos) / sizeof(pos[0]);
69
70         return vector<string>(pos, pos + size_pos);
71 }
72
73
74 vector<string> const & possible_cite_commands()
75 {
76         static vector<string> const pos = init_possible_cite_commands();
77         return pos;
78 }
79
80
81 bool is_possible_cite_command(string const & input)
82 {
83         vector<string> const & possibles = possible_cite_commands();
84         vector<string>::const_iterator const end = possibles.end();
85         return std::find(possibles.begin(), end, input) != end;
86 }
87
88
89 string const default_cite_command(CiteEngine engine)
90 {
91         string str;
92         switch (engine) {
93         case ENGINE_BASIC:
94                 str = "cite";
95                 break;
96         case ENGINE_NATBIB_AUTHORYEAR:
97                 str = "citet";
98                 break;
99         case ENGINE_NATBIB_NUMERICAL:
100                 str = "citep";
101                 break;
102         case ENGINE_JURABIB:
103                 str = "cite";
104                 break;
105         }
106         return str;
107 }
108
109 } // namespace anon
110
111
112 string const asValidLatexCommand(string const & input,
113                                  CiteEngine_enum const & engine)
114 {
115         string const default_str = default_cite_command(engine);
116         if (!is_possible_cite_command(input))
117                 return default_str;
118
119         string output;
120         switch (engine) {
121         case ENGINE_BASIC:
122                 output = default_str;
123                 break;
124
125         case ENGINE_NATBIB_AUTHORYEAR:
126         case ENGINE_NATBIB_NUMERICAL:
127                 if (input == "cite" || input == "citefield" ||
128                     input == "citetitle" || input == "cite*")
129                         output = default_str;
130                 else if (prefixIs(input, "foot"))
131                         output = input.substr(4);
132                 else
133                         output = input;
134                 break;
135
136         case ENGINE_JURABIB: {
137                 // Jurabib does not support the 'uppercase' natbib style.
138                 if (input[0] == 'C')
139                         output = string(1, 'c') + input.substr(1);
140                 else
141                         output = input;
142
143                 // Jurabib does not support the 'full' natbib style.
144                 string::size_type const n = output.size() - 1;
145                 if (output != "cite*" && output[n] == '*')
146                         output = output.substr(0, n);
147
148                 break;
149         }
150         }
151
152         return output;
153 }
154
155
156 string const familyName(string const & name)
157 {
158         // Very simple parser
159         string fname = name;
160
161         // possible authorname combinations are:
162         // "Surname, FirstName"
163         // "Surname, F."
164         // "FirstName Surname"
165         // "F. Surname"
166         string::size_type idx = fname.find(',');
167         if (idx != string::npos)
168                 return ltrim(fname.substr(0, idx));
169         idx = fname.rfind('.');
170         if (idx != string::npos)
171                 fname = ltrim(fname.substr(idx + 1));
172         // test if we have a LaTeX Space in front
173         if (fname[0] == '\\')
174                 return fname.substr(2);
175
176         return rtrim(fname);
177 }
178
179
180 string const getAbbreviatedAuthor(InfoMap const & map, string const & key)
181 {
182         BOOST_ASSERT(!map.empty());
183
184         InfoMap::const_iterator it = map.find(key);
185         if (it == map.end())
186                 return string();
187         string const & data = it->second;
188
189         // Is the entry a BibTeX one or one from lyx-layout "bibliography"?
190         string::size_type const pos = data.find("TheBibliographyRef");
191         if (pos != string::npos) {
192                 if (pos <= 2) {
193                         return string();
194                 }
195
196                 string const opt = trim(data.substr(0, pos - 1));
197                 if (opt.empty())
198                         return string();
199
200                 string authors;
201                 split(opt, authors, '(');
202                 return authors;
203         }
204
205         string author = parseBibTeX(data, "author");
206
207         if (author.empty())
208                 author = parseBibTeX(data, "editor");
209
210         if (author.empty()) {
211                 author = parseBibTeX(data, "key");
212                 if (author.empty())
213                         author = key;
214                 return author;
215         }
216
217         vector<string> const authors = getVectorFromString(author, " and ");
218         if (authors.empty())
219                 return author;
220
221         if (authors.size() == 2)
222                 return bformat(_("%1$s and %2$s"),
223                         familyName(authors[0]), familyName(authors[1]));
224
225         if (authors.size() > 2)
226                 return bformat(_("%1$s et al."), familyName(authors[0]));
227
228         return familyName(authors[0]);
229 }
230
231
232 string const getYear(InfoMap const & map, string const & key)
233 {
234         BOOST_ASSERT(!map.empty());
235
236         InfoMap::const_iterator it = map.find(key);
237         if (it == map.end())
238                 return string();
239         string const & data = it->second;
240
241         // Is the entry a BibTeX one or one from lyx-layout "bibliography"?
242         string::size_type const pos = data.find("TheBibliographyRef");
243         if (pos != string::npos) {
244                 if (pos <= 2) {
245                         return string();
246                 }
247
248                 string const opt =
249                         trim(data.substr(0, pos - 1));
250                 if (opt.empty())
251                         return string();
252
253                 string authors;
254                 string const tmp = split(opt, authors, '(');
255                 string year;
256                 split(tmp, year, ')');
257                 return year;
258
259         }
260
261         string year = parseBibTeX(data, "year");
262         if (year.empty())
263                 year = _("No year");
264
265         return year;
266 }
267
268
269 namespace {
270
271 // A functor for use with std::sort, leading to case insensitive sorting
272 class compareNoCase: public std::binary_function<string, string, bool>
273 {
274 public:
275         bool operator()(string const & s1, string const & s2) const {
276                 return compare_ascii_no_case(s1, s2) < 0;
277         }
278 };
279
280 } // namespace anon
281
282
283 vector<string> const getKeys(InfoMap const & map)
284 {
285         vector<string> bibkeys;
286         InfoMap::const_iterator it  = map.begin();
287         InfoMap::const_iterator end = map.end();
288         for (; it != end; ++it) {
289                 bibkeys.push_back(it->first);
290         }
291
292         std::sort(bibkeys.begin(), bibkeys.end(), compareNoCase());
293         return bibkeys;
294 }
295
296
297 string const getInfo(InfoMap const & map, string const & key)
298 {
299         BOOST_ASSERT(!map.empty());
300
301         InfoMap::const_iterator it = map.find(key);
302         if (it == map.end())
303                 return string();
304         string const & data = it->second;
305
306         // is the entry a BibTeX one or one from lyx-layout "bibliography"?
307         string const separator("TheBibliographyRef");
308         string::size_type const pos = data.find(separator);
309         if (pos != string::npos) {
310                 string::size_type const pos2 = pos + separator.size();
311                 string const info = trim(data.substr(pos2));
312                 return info;
313         }
314
315         // Search for all possible "required" keys
316         string author = parseBibTeX(data, "author");
317         if (author.empty())
318                 author = parseBibTeX(data, "editor");
319
320         string year       = parseBibTeX(data, "year");
321         string title      = parseBibTeX(data, "title");
322         string booktitle  = parseBibTeX(data, "booktitle");
323         string chapter    = parseBibTeX(data, "chapter");
324         string number     = parseBibTeX(data, "number");
325         string volume     = parseBibTeX(data, "volume");
326         string pages      = parseBibTeX(data, "pages");
327         string annote     = parseBibTeX(data, "annote");
328
329         string media      = parseBibTeX(data, "journal");
330         if (media.empty())
331                 media = parseBibTeX(data, "publisher");
332         if (media.empty())
333                 media = parseBibTeX(data, "school");
334         if (media.empty())
335                 media = parseBibTeX(data, "institution");
336
337         ostringstream result;
338         if (!author.empty())
339                 result << author << ", ";
340         if (!title.empty())
341                 result << title;
342         if (!booktitle.empty())
343                 result << ", in " << booktitle;
344         if (!chapter.empty())
345                 result << ", Ch. " << chapter;
346         if (!media.empty())
347                 result << ", " << media;
348         if (!volume.empty())
349                 result << ", vol. " << volume;
350         if (!number.empty())
351                 result << ", no. " << number;
352         if (!pages.empty())
353                 result << ", pp. " << pages;
354         if (!year.empty())
355                 result << ", " << year;
356         if (!annote.empty())
357                 result << "\n\n" << annote;
358
359         string const result_str = rtrim(result.str());
360         if (!result_str.empty())
361                 return result_str;
362
363         // This should never happen (or at least be very unusual!)
364         return data;
365 }
366
367
368 namespace {
369
370 // Escape special chars.
371 // All characters are literals except: '.|*?+(){}[]^$\'
372 // These characters are literals when preceded by a "\", which is done here
373 // @todo: This function should be moved to support, and then the test in tests
374 //        should be moved there as well.
375 string const escape_special_chars(string const & expr)
376 {
377         // Search for all chars '.|*?+(){}[^$]\'
378         // Note that '[' and '\' must be escaped.
379         // This is a limitation of boost::regex, but all other chars in BREs
380         // are assumed literal.
381         boost::regex reg("[].|*?+(){}^$\\[\\\\]");
382
383         // $& is a perl-like expression that expands to all
384         // of the current match
385         // The '$' must be prefixed with the escape character '\' for
386         // boost to treat it as a literal.
387         // Thus, to prefix a matched expression with '\', we use:
388         return boost::regex_replace(expr, reg, "\\\\$&");
389 }
390
391
392 // A functor for use with std::find_if, used to ascertain whether a
393 // data entry matches the required regex_
394 // @throws: boost::regex_error if the supplied regex pattern is not valid
395 // @todo: This function should be moved to support.
396 class RegexMatch : public std::unary_function<string, bool>
397 {
398 public:
399         // re and icase are used to construct an instance of boost::RegEx.
400         // if icase is true, then matching is insensitive to case
401         RegexMatch(InfoMap const & m, string const & re, bool icase)
402                 : map_(m), regex_(re, icase) {}
403
404         bool operator()(string const & key) const {
405                 // the data searched is the key + its associated BibTeX/biblio
406                 // fields
407                 string data = key;
408                 InfoMap::const_iterator info = map_.find(key);
409                 if (info != map_.end())
410                         data += ' ' + info->second;
411
412                 // Attempts to find a match for the current RE
413                 // somewhere in data.
414                 return boost::regex_search(data, regex_);
415         }
416 private:
417         InfoMap const map_;
418         mutable boost::regex regex_;
419 };
420
421 } // namespace anon
422
423
424 vector<string>::const_iterator
425 searchKeys(InfoMap const & theMap,
426            vector<string> const & keys,
427            string const & search_expr,
428            vector<string>::const_iterator start,
429            Search type,
430            Direction dir,
431            bool caseSensitive)
432 {
433         // Preliminary checks
434         if (start < keys.begin() || start >= keys.end())
435                 return keys.end();
436
437         string expr = trim(search_expr);
438         if (expr.empty())
439                 return keys.end();
440
441         if (type == SIMPLE)
442                 // We must escape special chars in the search_expr so that
443                 // it is treated as a simple string by boost::regex.
444                 expr = escape_special_chars(expr);
445
446         try {
447                 // Build the functor that will be passed to find_if.
448                 RegexMatch const match(theMap, expr, !caseSensitive);
449
450                 // Search the vector of 'keys' from 'start' for one
451                 // that matches the predicate 'match'. Searching can
452                 // be forward or backward from start.
453                 if (dir == FORWARD)
454                         return std::find_if(start, keys.end(), match);
455
456                 vector<string>::const_reverse_iterator rit(start);
457                 vector<string>::const_reverse_iterator rend = keys.rend();
458                 rit = std::find_if(rit, rend, match);
459                 
460                 if (rit == rend)
461                         return keys.end();
462                 // This is correct and always safe.
463                 // (See Meyer's Effective STL, Item 28.)
464                 return (++rit).base();
465         }
466         catch (boost::regex_error & regerr) {
467                 return keys.end();
468         }
469 }
470
471
472 string const parseBibTeX(string data, string const & findkey)
473 {
474         string keyvalue;
475         // at first we delete all characters right of '%' and
476         // replace tabs through a space and remove leading spaces
477         // we read the data line by line so that the \n are
478         // ignored, too.
479         string data_;
480         int Entries = 0;
481         string dummy = token(data,'\n', Entries);
482         while (!dummy.empty()) {
483                 dummy = subst(dummy, '\t', ' ');        // no tabs
484                 dummy = ltrim(dummy);           // no leading spaces
485                 // ignore lines with a beginning '%' or ignore all right of %
486                 string::size_type const idx =
487                         dummy.empty() ? string::npos : dummy.find('%');
488                 if (idx != string::npos)
489                         // Check if this is really a comment or just "\%"
490                         if (idx == 0 || dummy[idx - 1] != '\\')
491                                 dummy.erase(idx, string::npos);
492                         else  //  This is "\%", so just erase the '\'
493                                 dummy.erase(idx - 1, 1);
494                 // do we have a new token or a new line of
495                 // the same one? In the first case we ignore
496                 // the \n and in the second we replace it
497                 // with a space
498                 if (!dummy.empty()) {
499                         if (!contains(dummy, '='))
500                                 data_ += ' ' + dummy;
501                         else
502                                 data_ += dummy;
503                 }
504                 dummy = token(data, '\n', ++Entries);
505         }
506
507         // replace double commas with "" for easy scanning
508         data = subst(data_, ",,", "\"\"");
509
510         // unlikely!
511         if (data.empty())
512                 return string();
513
514         // now get only the important line of the bibtex entry.
515         // all entries are devided by ',' except the last one.
516         data += ',';  // now we have same behaviour for all entries
517                       // because the last one is "blah ... }"
518         Entries = 0;
519         bool found = false;
520         // parsing of title and booktitle is different from the
521         // others, because booktitle contains title
522         do {
523                 dummy = token(data, ',', Entries++);
524                 if (!dummy.empty()) {
525                         found = contains(ascii_lowercase(dummy), findkey);
526                         if (findkey == "title" &&
527                                 contains(ascii_lowercase(dummy), "booktitle"))
528                                 found = false;
529                 }
530         } while (!found && !dummy.empty());
531         if (dummy.empty())
532                 // no such keyword
533                 return string();
534
535         // we are not sure, if we get all, because "key= "blah, blah" is
536         // allowed.
537         // Therefore we read all until the next "=" character, which follows a
538         // new keyword
539         keyvalue = dummy;
540         dummy = token(data, ',', Entries++);
541         while (!contains(dummy, '=') && !dummy.empty()) {
542                 keyvalue += ',' + dummy;
543                 dummy = token(data, ',', Entries++);
544         }
545
546         // replace double "" with originals ,, (two commas)
547         // leaving us with the all-important line
548         data = subst(keyvalue, "\"\"", ",,");
549
550         // Clean-up.
551         // 1. Spaces
552         data = rtrim(data);
553         // 2. if there is no opening '{' then a closing '{' is probably cruft.
554         if (!contains(data, '{'))
555                 data = rtrim(data, "}");
556         // happens, when last keyword
557         string::size_type const idx =
558                 !data.empty() ? data.find('=') : string::npos;
559
560         if (idx == string::npos)
561                 return string();
562
563         data = trim(data.substr(idx));
564
565         if (data.length() < 2 || data[0] != '=') {      // a valid entry?
566                 return string();
567         } else {
568                 // delete '=' and the following spaces
569                 data = ltrim(data, " =");
570                 if (data.length() < 2) {
571                         return data;    // not long enough to find delimiters
572                 } else {
573                         string::size_type keypos = 1;
574                         char enclosing;
575                         if (data[0] == '{') {
576                                 enclosing = '}';
577                         } else if (data[0] == '"') {
578                                 enclosing = '"';
579                         } else {
580                                 // no {} and no "", pure data but with a
581                                 // possible ',' at the end
582                                 return rtrim(data, ",");
583                         }
584                         string tmp = data.substr(keypos);
585                         while (tmp.find('{') != string::npos &&
586                                tmp.find('}') != string::npos &&
587                                tmp.find('{') < tmp.find('}') &&
588                                tmp.find('{') < tmp.find(enclosing)) {
589
590                                 keypos += tmp.find('{') + 1;
591                                 tmp = data.substr(keypos);
592                                 keypos += tmp.find('}') + 1;
593                                 tmp = data.substr(keypos);
594                         }
595                         if (tmp.find(enclosing) == string::npos)
596                                 return data;
597                         else {
598                                 keypos += tmp.find(enclosing);
599                                 return data.substr(1, keypos - 1);
600                         }
601                 }
602         }
603 }
604
605
606 namespace {
607
608
609 char const * const citeCommands[] = {
610         "cite", "citet", "citep", "citealt", "citealp", "citeauthor",
611         "citeyear", "citeyearpar" };
612
613 unsigned int const nCiteCommands =
614         sizeof(citeCommands) / sizeof(char *);
615
616 CiteStyle const citeStyles[] = {
617         CITE, CITET, CITEP, CITEALT, CITEALP,
618         CITEAUTHOR, CITEYEAR, CITEYEARPAR };
619
620 unsigned int const nCiteStyles =
621         sizeof(citeStyles) / sizeof(CiteStyle);
622
623 CiteStyle const citeStylesFull[] = {
624         CITET, CITEP, CITEALT, CITEALP, CITEAUTHOR };
625
626 unsigned int const nCiteStylesFull =
627         sizeof(citeStylesFull) / sizeof(CiteStyle);
628
629 CiteStyle const citeStylesUCase[] = {
630         CITET, CITEP, CITEALT, CITEALP, CITEAUTHOR };
631
632 unsigned int const nCiteStylesUCase =
633         sizeof(citeStylesUCase) / sizeof(CiteStyle);
634
635 } // namespace anon
636
637
638 CitationStyle::CitationStyle(string const & command)
639         : style(CITE), full(false), forceUCase(false)
640 {
641         if (command.empty())
642                 return;
643
644         string cmd = command;
645         if (cmd[0] == 'C') {
646                 forceUCase = true;
647                 cmd[0] = 'c';
648         }
649
650         string::size_type const n = cmd.size() - 1;
651         if (cmd != "cite" && cmd[n] == '*') {
652                 full = true;
653                 cmd = cmd.substr(0,n);
654         }
655
656         char const * const * const last = citeCommands + nCiteCommands;
657         char const * const * const ptr = std::find(citeCommands, last, cmd);
658
659         if (ptr != last) {
660                 size_t idx = ptr - citeCommands;
661                 style = citeStyles[idx];
662         }
663 }
664
665
666 string const CitationStyle::asLatexStr() const
667 {
668         string cite = citeCommands[style];
669         if (full) {
670                 CiteStyle const * last = citeStylesFull + nCiteStylesFull;
671                 if (std::find(citeStylesFull, last, style) != last)
672                         cite += '*';
673         }
674
675         if (forceUCase) {
676                 CiteStyle const * last = citeStylesUCase + nCiteStylesUCase;
677                 if (std::find(citeStylesUCase, last, style) != last)
678                         cite[0] = 'C';
679         }
680
681         return cite;
682 }
683
684
685 CiteEngine_enum getEngine(Buffer const & buffer)
686 {
687         return buffer.params().cite_engine;
688 }
689
690
691 vector<CiteStyle> const getCiteStyles(CiteEngine_enum const & engine)
692 {
693         unsigned int nStyles = 0;
694         unsigned int start = 0;
695
696         switch (engine) {
697         case ENGINE_BASIC:
698                 nStyles = 1;
699                 start = 0;
700                 break;
701         case ENGINE_NATBIB_AUTHORYEAR:
702         case ENGINE_NATBIB_NUMERICAL:
703                 nStyles = nCiteStyles - 1;
704                 start = 1;
705                 break;
706         case ENGINE_JURABIB:
707                 nStyles = nCiteStyles;
708                 start = 0;
709                 break;
710         }
711
712         typedef vector<CiteStyle> cite_vec;
713
714         cite_vec styles(nStyles);
715         cite_vec::size_type i = 0;
716         int j = start;
717         for (; i != styles.size(); ++i, ++j)
718                 styles[i] = citeStyles[j];
719
720         return styles;
721 }
722
723
724 vector<string> const
725 getNumericalStrings(string const & key,
726                     InfoMap const & map, vector<CiteStyle> const & styles)
727 {
728         if (map.empty()) {
729                 return vector<string>();
730         }
731
732         string const author = getAbbreviatedAuthor(map, key);
733         string const year   = getYear(map, key);
734         if (author.empty() || year.empty())
735                 return vector<string>();
736
737         vector<string> vec(styles.size());
738         for (vector<string>::size_type i = 0; i != vec.size(); ++i) {
739                 string str;
740
741                 switch (styles[i]) {
742                 case CITE:
743                 case CITEP:
744                         str = "[#ID]";
745                         break;
746
747                 case CITET:
748                         str = author + " [#ID]";
749                         break;
750
751                 case CITEALT:
752                         str = author + " #ID";
753                         break;
754
755                 case CITEALP:
756                         str = "#ID";
757                         break;
758
759                 case CITEAUTHOR:
760                         str = author;
761                         break;
762
763                 case CITEYEAR:
764                         str = year;
765                         break;
766
767                 case CITEYEARPAR:
768                         str = '(' + year + ')';
769                         break;
770                 }
771
772                 vec[i] = str;
773         }
774
775         return vec;
776 }
777
778
779 vector<string> const
780 getAuthorYearStrings(string const & key,
781                     InfoMap const & map, vector<CiteStyle> const & styles)
782 {
783         if (map.empty()) {
784                 return vector<string>();
785         }
786
787         string const author = getAbbreviatedAuthor(map, key);
788         string const year   = getYear(map, key);
789         if (author.empty() || year.empty())
790                 return vector<string>();
791
792         vector<string> vec(styles.size());
793         for (vector<string>::size_type i = 0; i != vec.size(); ++i) {
794                 string str;
795
796                 switch (styles[i]) {
797                 case CITE:
798                         // jurabib only: Author/Annotator
799                         // (i.e. the "before" field, 2nd opt arg)
800                         str = author + "/<" + _("before") + '>';
801                         break;
802
803                 case CITET:
804                         str = author + " (" + year + ')';
805                         break;
806
807                 case CITEP:
808                         str = '(' + author + ", " + year + ')';
809                         break;
810
811                 case CITEALT:
812                         str = author + ' ' + year ;
813                         break;
814
815                 case CITEALP:
816                         str = author + ", " + year ;
817                         break;
818
819                 case CITEAUTHOR:
820                         str = author;
821                         break;
822
823                 case CITEYEAR:
824                         str = year;
825                         break;
826
827                 case CITEYEARPAR:
828                         str = '(' + year + ')';
829                         break;
830                 }
831
832                 vec[i] = str;
833         }
834
835         return vec;
836 }
837
838 } // namespace biblio
839 } // namespace lyx