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