]> git.lyx.org Git - lyx.git/blob - src/Lexer.cpp
make Lexer interface a bit more friendly
[lyx.git] / src / Lexer.cpp
1 /**
2  * \file Lexer.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author Alejandro Aguilar Sierra
7  * \author Lars Gullik Bjønnes
8  * \author Jean-Marc Lasgouttes
9  * \author John Levon
10  *
11  * Full author contact details are available in file CREDITS.
12  */
13
14 #include <config.h>
15
16 #include "Lexer.h"
17
18 #include "support/convert.h"
19 #include "support/debug.h"
20 #include "support/FileName.h"
21 #include "support/filetools.h"
22 #include "support/gzstream.h"
23 #include "support/lstrings.h"
24 #include "support/lyxalgo.h"
25 #include "support/types.h"
26
27 #include <functional>
28 #include <istream>
29 #include <stack>
30 #include <vector>
31
32 using namespace std;
33 using namespace lyx::support;
34
35 namespace lyx {
36
37 //////////////////////////////////////////////////////////////////////
38 //
39 // Lexer::Pimpl
40 //
41 //////////////////////////////////////////////////////////////////////
42
43
44 ///
45 class Lexer::Pimpl {
46 public:
47         ///
48         Pimpl(LexerKeyword * tab, int num);
49         ///
50         string const getString() const;
51         ///
52         docstring const getDocString() const;
53         ///
54         void printError(string const & message) const;
55         ///
56         void printTable(ostream & os);
57         ///
58         void pushTable(LexerKeyword * tab, int num);
59         ///
60         void popTable();
61         ///
62         bool setFile(FileName const & filename);
63         ///
64         void setStream(istream & i);
65         ///
66         void setCommentChar(char c);
67         ///
68         bool next(bool esc = false);
69         ///
70         int searchKeyword(char const * const tag) const;
71         ///
72         int lex();
73         ///
74         bool eatLine();
75         ///
76         bool nextToken();
77         /// test if there is a pushed token or the stream is ok
78         bool inputAvailable();
79         ///
80         void pushToken(string const &);
81         /// fb_ is only used to open files, the stream is accessed through is.
82         filebuf fb_;
83
84         /// gz_ is only used to open files, the stream is accessed through is.
85         gz::gzstreambuf gz_;
86
87         /// the stream that we use.
88         istream is;
89         ///
90         string name;
91         ///
92         LexerKeyword * table;
93         ///
94         int no_items;
95         ///
96         string buff;
97         ///
98         int status;
99         ///
100         int lineno;
101         ///
102         string pushTok;
103         ///
104         char commentChar;
105         /// used for error messages
106         string context;
107 private:
108         /// non-copyable
109         Pimpl(Pimpl const &);
110         void operator=(Pimpl const &);
111
112         ///
113         void verifyTable();
114         ///
115         class PushedTable {
116         public:
117                 ///
118                 PushedTable()
119                         : table_elem(0), table_siz(0) {}
120                 ///
121                 PushedTable(LexerKeyword * ki, int siz)
122                         : table_elem(ki), table_siz(siz) {}
123                 ///
124                 LexerKeyword * table_elem;
125                 ///
126                 int table_siz;
127         };
128         ///
129         stack<PushedTable> pushed;
130 };
131
132
133
134 namespace {
135
136 class CompareTags
137         : public binary_function<LexerKeyword, LexerKeyword, bool> {
138 public:
139         // used by lower_bound, sort and sorted
140         bool operator()(LexerKeyword const & a, LexerKeyword const & b) const
141         {
142                 // we use the ascii version, because in turkish, 'i'
143                 // is not the lowercase version of 'I', and thus
144                 // turkish locale breaks parsing of tags.
145                 return compare_ascii_no_case(a.tag, b.tag) < 0;
146         }
147 };
148
149 } // end of anon namespace
150
151
152 Lexer::Pimpl::Pimpl(LexerKeyword * tab, int num)
153         : is(&fb_), table(tab), no_items(num),
154           status(0), lineno(0), commentChar('#')
155 {
156         verifyTable();
157 }
158
159
160 string const Lexer::Pimpl::getString() const
161 {
162         return buff;
163 }
164
165
166 docstring const Lexer::Pimpl::getDocString() const
167 {
168         return from_utf8(buff);
169 }
170
171
172 void Lexer::Pimpl::printError(string const & message) const
173 {
174         string const tmpmsg = subst(message, "$$Token", getString());
175         lyxerr << "LyX: " << tmpmsg << " [around line " << lineno
176                 << " of file " << to_utf8(makeDisplayPath(name)) << ']' << endl;
177 }
178
179
180 void Lexer::Pimpl::printTable(ostream & os)
181 {
182         os << "\nNumber of tags: " << no_items << endl;
183         for (int i= 0; i < no_items; ++i)
184                 os << "table[" << i
185                    << "]:  tag: `" << table[i].tag
186                    << "'  code:" << table[i].code << '\n';
187         os.flush();
188 }
189
190
191 void Lexer::Pimpl::verifyTable()
192 {
193         // Check if the table is sorted and if not, sort it.
194         if (table
195             && !lyx::sorted(table, table + no_items, CompareTags())) {
196                 lyxerr << "The table passed to Lexer is not sorted!\n"
197                        << "Tell the developers to fix it!" << endl;
198                 // We sort it anyway to avoid problems.
199                 lyxerr << "\nUnsorted:" << endl;
200                 printTable(lyxerr);
201
202                 sort(table, table + no_items, CompareTags());
203                 lyxerr << "\nSorted:" << endl;
204                 printTable(lyxerr);
205         }
206 }
207
208
209 void Lexer::Pimpl::pushTable(LexerKeyword * tab, int num)
210 {
211         PushedTable tmppu(table, no_items);
212         pushed.push(tmppu);
213
214         table = tab;
215         no_items = num;
216
217         verifyTable();
218 }
219
220
221 void Lexer::Pimpl::popTable()
222 {
223         if (pushed.empty()) {
224                 lyxerr << "Lexer error: nothing to pop!" << endl;
225                 return;
226         }
227
228         PushedTable tmp = pushed.top();
229         pushed.pop();
230         table = tmp.table_elem;
231         no_items = tmp.table_siz;
232 }
233
234
235 bool Lexer::Pimpl::setFile(FileName const & filename)
236 {
237         // Check the format of the file.
238         string const format = filename.guessFormatFromContents();
239
240         if (format == "gzip" || format == "zip" || format == "compress") {
241                 LYXERR(Debug::LYXLEX, "lyxlex: compressed");
242                 // The check only outputs a debug message, because it triggers
243                 // a bug in compaq cxx 6.2, where is_open() returns 'true' for
244                 // a fresh new filebuf.  (JMarc)
245                 if (gz_.is_open() || istream::off_type(is.tellg()) > -1)
246                         LYXERR(Debug::LYXLEX, "Error in LyXLex::setFile: "
247                                 "file or stream already set.");
248                 gz_.open(filename.toFilesystemEncoding().c_str(), ios::in);
249                 is.rdbuf(&gz_);
250                 name = filename.absFilename();
251                 lineno = 0;
252                 return gz_.is_open() && is.good();
253         } else {
254                 LYXERR(Debug::LYXLEX, "lyxlex: UNcompressed");
255
256                 // The check only outputs a debug message, because it triggers
257                 // a bug in compaq cxx 6.2, where is_open() returns 'true' for
258                 // a fresh new filebuf.  (JMarc)
259                 if (fb_.is_open() || istream::off_type(is.tellg()) > 0) {
260                         LYXERR(Debug::LYXLEX, "Error in Lexer::setFile: "
261                                 "file or stream already set.");
262                 }
263                 fb_.open(filename.toFilesystemEncoding().c_str(), ios::in);
264                 is.rdbuf(&fb_);
265                 name = filename.absFilename();
266                 lineno = 0;
267                 return fb_.is_open() && is.good();
268         }
269 }
270
271
272 void Lexer::Pimpl::setStream(istream & i)
273 {
274         if (fb_.is_open() || istream::off_type(is.tellg()) > 0) {
275                 LYXERR(Debug::LYXLEX, "Error in Lexer::setStream: "
276                         "file or stream already set.");
277         }
278         is.rdbuf(i.rdbuf());
279         lineno = 0;
280 }
281
282
283 void Lexer::Pimpl::setCommentChar(char c)
284 {
285         commentChar = c;
286 }
287
288
289 bool Lexer::Pimpl::next(bool esc /* = false */)
290 {
291         if (!pushTok.empty()) {
292                 // There can have been a whole line pushed so
293                 // we extract the first word and leaves the rest
294                 // in pushTok. (Lgb)
295                 if (pushTok[0] == '\\' && pushTok.find(' ') != string::npos) {
296                         buff.clear();
297                         pushTok = split(pushTok, buff, ' ');
298                 } else {
299                         buff = pushTok;
300                         pushTok.clear();
301                 }
302                 status = LEX_TOKEN;
303                 return true;
304         }
305
306
307         unsigned char c = 0; // getc() returns an int
308         char cc = 0;
309         status = 0;
310         while (is && !status) {
311                 is.get(cc);
312                 c = cc;
313
314                 // skip ','s
315                 if (esc && c == ',')
316                         continue;
317
318                 
319                 if (c == commentChar) {
320                         // Read rest of line (fast :-)
321 #if 1
322                         // That is not fast... (Lgb)
323                         string dummy;
324                         getline(is, dummy);
325
326                         LYXERR(Debug::LYXLEX, "Comment read: `" << c << dummy << '\'');
327 #else
328                         // unfortunately ignore is buggy (Lgb)
329                         is.ignore(100, '\n');
330 #endif
331                         ++lineno;
332                         continue;
333                 }
334
335                 if (c == '\"') {
336                         buff.clear();
337
338                         if (esc) {
339
340                                 bool escaped = false;
341                                 do {
342                                         escaped = false;
343                                         is.get(cc);
344                                         c = cc;
345                                         if (c == '\r') continue;
346                                         if (c == '\\') {
347                                                 // escape the next char
348                                                 is.get(cc);
349                                                 c = cc;
350                                                 if (c == '\"' || c == '\\')
351                                                         escaped = true;
352                                                 else
353                                                         buff.push_back('\\');
354                                         }
355                                         buff.push_back(c);
356
357                                         if (!escaped && c == '\"')
358                                                 break;
359                                 } while (c != '\n' && is);
360
361                         } else {
362
363                                 do {
364                                         is.get(cc);
365                                         c = cc;
366                                         if (c != '\r')
367                                                 buff.push_back(c);
368                                 } while (c != '\"' && c != '\n' && is);
369
370                         }
371
372                         if (c != '\"') {
373                                 printError("Missing quote");
374                                 if (c == '\n')
375                                         ++lineno;
376                         }
377
378                         buff.resize(buff.size() - 1);
379                         status = LEX_DATA;
380                         break;
381                 }
382
383                 if (!esc && c == ',')
384                         continue;              /* Skip ','s */
385
386                         // using relational operators with chars other
387                         // than == and != is not safe. And if it is done
388                         // the type _have_ to be unsigned. It usually a
389                         // lot better to use the functions from cctype
390                 if (c > ' ' && is)  {
391                         buff.clear();
392
393                         do {
394                                 if (esc && c == '\\') {
395                                         // escape the next char
396                                         is.get(cc);
397                                         c = cc;
398                                         //escaped = true;
399                                 }
400                                 buff.push_back(c);
401                                 is.get(cc);
402                                 c = cc;
403                         } while (c > ' ' && c != ',' && is);
404                         status = LEX_TOKEN;
405                 }
406
407                 if (!esc && c == '\r' && is) {
408                         // The Windows support has lead to the
409                         // possibility of "\r\n" at the end of
410                         // a line.  This will stop LyX choking
411                         // when it expected to find a '\n'
412                         is.get(cc);
413                         c = cc;
414                 }
415
416                 if (c == '\n')
417                         ++lineno;
418
419         }
420         if (status)
421                 return true;
422
423         status = is.eof() ? LEX_FEOF: LEX_UNDEF;
424         buff.clear();
425         return false;
426 }
427
428
429 int Lexer::Pimpl::searchKeyword(char const * const tag) const
430 {
431         LexerKeyword search_tag = { tag, 0 };
432         LexerKeyword * res =
433                 lower_bound(table, table + no_items,
434                             search_tag, CompareTags());
435         // use the compare_ascii_no_case instead of compare_no_case,
436         // because in turkish, 'i' is not the lowercase version of 'I',
437         // and thus turkish locale breaks parsing of tags.
438         if (res != table + no_items
439             && !compare_ascii_no_case(res->tag, tag))
440                 return res->code;
441         return LEX_UNDEF;
442 }
443
444
445 int Lexer::Pimpl::lex()
446 {
447         //NOTE: possible bug.
448         if (next() && status == LEX_TOKEN)
449                 return searchKeyword(getString().c_str());
450         return status;
451 }
452
453
454 bool Lexer::Pimpl::eatLine()
455 {
456         buff.clear();
457
458         unsigned char c = '\0';
459         char cc = 0;
460         while (is && c != '\n') {
461                 is.get(cc);
462                 c = cc;
463                 //LYXERR(Debug::LYXLEX, "Lexer::EatLine read char: `" << c << '\'');
464                 if (c != '\r')
465                         buff.push_back(c);
466         }
467
468         if (c == '\n') {
469                 ++lineno;
470                 buff.resize(buff.size() - 1);
471                 status = LEX_DATA;
472                 return true;
473         } else if (buff.length() > 0) { // last line
474                 status = LEX_DATA;
475                 return true;
476         } else {
477                 return false;
478         }
479 }
480
481
482 bool Lexer::Pimpl::nextToken()
483 {
484         if (!pushTok.empty()) {
485                 // There can have been a whole line pushed so
486                 // we extract the first word and leaves the rest
487                 // in pushTok. (Lgb)
488                 if (pushTok[0] == '\\' && pushTok.find(' ') != string::npos) {
489                         buff.clear();
490                         pushTok = split(pushTok, buff, ' ');
491                 } else {
492                         buff = pushTok;
493                         pushTok.clear();
494                 }
495                 status = LEX_TOKEN;
496                 return true;
497         }
498
499         status = 0;
500         while (is && !status) {
501                 unsigned char c = 0;
502                 char cc = 0;
503                 is.get(cc);
504                 c = cc;
505                 if (c >= ' ' && is) {
506                         buff.clear();
507
508                         if (c == '\\') { // first char == '\\'
509                                 do {
510                                         buff.push_back(c);
511                                         is.get(cc);
512                                         c = cc;
513                                 } while (c > ' ' && c != '\\' && is);
514                         } else {
515                                 do {
516                                         buff.push_back(c);
517                                         is.get(cc);
518                                         c = cc;
519                                 } while (c >= ' ' && c != '\\' && is);
520                         }
521
522                         if (c == '\\')
523                                 is.putback(c); // put it back
524                         status = LEX_TOKEN;
525                 }
526
527                 if (c == '\n')
528                         ++lineno;
529
530         }
531         if (status)
532                 return true;
533
534         status = is.eof() ? LEX_FEOF: LEX_UNDEF;
535         buff.clear();
536         return false;
537 }
538
539
540 bool Lexer::Pimpl::inputAvailable()
541 {
542         return is.good();
543 }
544
545
546 void Lexer::Pimpl::pushToken(string const & pt)
547 {
548         pushTok = pt;
549 }
550
551
552
553
554 //////////////////////////////////////////////////////////////////////
555 //
556 // Lexer
557 //
558 //////////////////////////////////////////////////////////////////////
559
560 Lexer::Lexer()
561         : pimpl_(new Pimpl(0, 0))
562 {}
563
564
565 void Lexer::init(LexerKeyword * tab, int num)
566 {
567          pimpl_ = new Pimpl(tab, num);
568 }
569
570
571 Lexer::~Lexer()
572 {
573         delete pimpl_;
574 }
575
576
577 bool Lexer::isOK() const
578 {
579         return pimpl_->inputAvailable();
580 }
581
582
583 void Lexer::setLineNumber(int l)
584 {
585         pimpl_->lineno = l;
586 }
587
588
589 int Lexer::lineNumber() const
590 {
591         return pimpl_->lineno;
592 }
593
594
595 istream & Lexer::getStream()
596 {
597         return pimpl_->is;
598 }
599
600
601 void Lexer::pushTable(LexerKeyword * tab, int num)
602 {
603         pimpl_->pushTable(tab, num);
604 }
605
606
607 void Lexer::popTable()
608 {
609         pimpl_->popTable();
610 }
611
612
613 void Lexer::printTable(ostream & os)
614 {
615         pimpl_->printTable(os);
616 }
617
618
619 void Lexer::printError(string const & message) const
620 {
621         pimpl_->printError(message);
622 }
623
624
625 bool Lexer::setFile(FileName const & filename)
626 {
627         return pimpl_->setFile(filename);
628 }
629
630
631 void Lexer::setStream(istream & i)
632 {
633         pimpl_->setStream(i);
634 }
635
636
637 void Lexer::setCommentChar(char c)
638 {
639         pimpl_->setCommentChar(c);
640 }
641
642 int Lexer::lex()
643 {
644         return pimpl_->lex();
645 }
646
647
648 int Lexer::getInteger() const
649 {
650         lastReadOk_ = pimpl_->status == LEX_DATA || pimpl_->status == LEX_TOKEN;
651         if (!lastReadOk_) {
652                 pimpl_->printError("integer token missing");
653                 return -1;
654         }
655
656         if (isStrInt(pimpl_->getString()))
657                 return convert<int>(pimpl_->getString());
658
659         lastReadOk_ = false;
660         pimpl_->printError("Bad integer `$$Token'");
661         return -1;
662 }
663
664
665 double Lexer::getFloat() const
666 {
667         // replace comma with dot in case the file was written with
668         // the wrong locale (should be rare, but is easy enough to
669         // avoid).
670         lastReadOk_ = pimpl_->status == LEX_DATA || pimpl_->status == LEX_TOKEN;
671         if (!lastReadOk_) {
672                 pimpl_->printError("float token missing");
673                 return -1;
674         }
675
676         string const str = subst(pimpl_->getString(), ",", ".");
677         if (isStrDbl(str))
678                 return convert<double>(str);
679
680         lastReadOk_ = false;
681         pimpl_->printError("Bad float `$$Token'");
682         return -1;
683 }
684
685
686 string const Lexer::getString() const
687 {
688         lastReadOk_ = pimpl_->status == LEX_DATA || pimpl_->status == LEX_TOKEN;
689
690         if (lastReadOk_)
691         return pimpl_->getString();
692
693         return string();
694 }
695
696
697 docstring const Lexer::getDocString() const
698 {
699         lastReadOk_ = pimpl_->status == LEX_DATA || pimpl_->status == LEX_TOKEN;
700
701         if (lastReadOk_)
702                 return pimpl_->getDocString();
703
704         return docstring();
705 }
706
707
708 // I would prefer to give a tag number instead of an explicit token
709 // here, but it is not possible because Buffer::readDocument uses
710 // explicit tokens (JMarc)
711 string const Lexer::getLongString(string const & endtoken)
712 {
713         string str;
714         string prefix;
715         bool firstline = true;
716
717         while (pimpl_->is) { //< eatLine only reads from is, not from pushTok
718                 if (!eatLine())
719                         // blank line in the file being read
720                         continue;
721
722                 string const token = trim(getString(), " \t");
723
724                 LYXERR(Debug::PARSER, "LongString: `" << getString() << '\'');
725
726                 // We do a case independent comparison, like searchKeyword does.
727                 if (compare_ascii_no_case(token, endtoken) == 0)
728                         break;
729
730                 string tmpstr = getString();
731                 if (firstline) {
732                         size_t i = tmpstr.find_first_not_of(' ');
733                         if (i != string::npos)
734                                 prefix = tmpstr.substr(0, i);
735                         firstline = false;
736                         LYXERR(Debug::PARSER, "Prefix = `" << prefix << "\'");
737                 }
738
739                 // further lines in long strings may have the same
740                 // whitespace prefix as the first line. Remove it.
741                 if (prefix.length() && prefixIs(tmpstr, prefix))
742                         tmpstr.erase(0, prefix.length() - 1);
743
744                 str += ltrim(tmpstr, "\t") + '\n';
745         }
746
747         if (!pimpl_->is)
748                 printError("Long string not ended by `" + endtoken + '\'');
749
750         return str;
751 }
752
753
754 bool Lexer::getBool() const
755 {
756         if (pimpl_->getString() == "true") {
757                 lastReadOk_ = true;
758                 return true;
759         } else if (pimpl_->getString() != "false") {
760                 pimpl_->printError("Bad boolean `$$Token'. "
761                                    "Use \"false\" or \"true\"");
762                 lastReadOk_ = false;
763         }
764         lastReadOk_ = true;
765         return false;
766 }
767
768
769 bool Lexer::eatLine()
770 {
771         return pimpl_->eatLine();
772 }
773
774
775 bool Lexer::next(bool esc)
776 {
777         return pimpl_->next(esc);
778 }
779
780
781 bool Lexer::nextToken()
782 {
783         return pimpl_->nextToken();
784 }
785
786
787 void Lexer::pushToken(string const & pt)
788 {
789         pimpl_->pushToken(pt);
790 }
791
792
793 Lexer::operator void const *() const
794 {
795         // This behaviour is NOT the same as the streams which would
796         // use fail() here. However, our implementation of getString() et al.
797         // can cause the eof() and fail() bits to be set, even though we
798         // haven't tried to read 'em.
799         return lastReadOk_? this : 0;
800 }
801
802
803 bool Lexer::operator!() const
804 {
805         return !lastReadOk_;
806 }
807
808
809 Lexer & Lexer::operator>>(string & s)
810 {
811         if (isOK()) {
812                 next();
813                 s = getString();
814         } else {
815                 lastReadOk_ = false;
816         }
817         return *this;
818 }
819
820
821 Lexer & Lexer::operator>>(docstring & s)
822 {
823         if (isOK()) {
824                 next();
825                 s = getDocString();
826         } else {
827                 lastReadOk_ = false;
828         }
829         return *this;
830 }
831
832
833 Lexer & Lexer::operator>>(double & s)
834 {
835         if (isOK()) {
836                 next();
837                 s = getFloat();
838         } else {
839                 lastReadOk_ = false;
840         }
841         return *this;
842 }
843
844
845 Lexer & Lexer::operator>>(int & s)
846 {
847         if (isOK()) {
848                 next();
849                 s = getInteger();
850         } else {
851                 lastReadOk_ = false;
852         }
853         return *this;
854 }
855
856
857 Lexer & Lexer::operator>>(unsigned int & s)
858 {
859         if (isOK()) {
860                 next();
861                 s = getInteger();
862         } else {
863                 lastReadOk_ = false;
864         }
865         return *this;
866 }
867
868
869 Lexer & Lexer::operator>>(bool & s)
870 {
871         if (isOK()) {
872                 next();
873                 s = getBool();
874         } else {
875                 lastReadOk_ = false;
876         }
877         return *this;
878 }
879
880
881 // quotes a string, e.g. for use in preferences files or as an argument
882 // of the "log" dialog
883 string Lexer::quoteString(string const & arg)
884 {
885         string res;
886         res += '"';
887         res += subst(subst(arg, "\\", "\\\\"), "\"", "\\\"");
888         res += '"';
889         return res;
890 }
891
892
893 Lexer & Lexer::operator>>(char const * required)
894 {
895         string token;
896         *this >> token;
897         if (token != required) {
898                 LYXERR0("Missing '" << required << "'-tag in " << pimpl_->context);
899                 pushToken(token);
900         }
901         return *this;
902 }
903
904
905 void Lexer::setContext(std::string const & str)
906 {
907         pimpl_->context = str;
908 }
909
910
911 } // namespace lyx