]> git.lyx.org Git - lyx.git/blob - src/tex2lyx/tex2lyx.cpp
c27f0c0886e8217e8b29973e441adf2d5c9147ff
[lyx.git] / src / tex2lyx / tex2lyx.cpp
1 /**
2  * \file tex2lyx.cpp
3  * This file is part of LyX, the document processor.
4  * Licence details can be found in the file COPYING.
5  *
6  * \author André Pönitz
7  *
8  * Full author contact details are available in file CREDITS.
9  */
10
11 // {[(
12
13 #include <config.h>
14
15 #include "tex2lyx.h"
16
17 #include "Context.h"
18 #include "Encoding.h"
19 #include "Layout.h"
20 #include "TextClass.h"
21
22 #include "support/convert.h"
23 #include "support/debug.h"
24 #include "support/ExceptionMessage.h"
25 #include "support/filetools.h"
26 #include "support/lassert.h"
27 #include "support/lstrings.h"
28 #include "support/Messages.h"
29 #include "support/os.h"
30 #include "support/Package.h"
31
32 #include <cstdlib>
33 #include <iostream>
34 #include <string>
35 #include <sstream>
36 #include <vector>
37 #include <map>
38
39 using namespace std;
40 using namespace lyx::support;
41 using namespace lyx::support::os;
42
43 namespace lyx {
44
45 // Dummy translation support
46 Messages messages_;
47 Messages & getMessages(std::string const &)
48 {
49         return messages_;
50 }
51
52
53 Messages & getGuiMessages()
54 {
55         return messages_;
56 }
57
58
59 string const trim(string const & a, char const * p)
60 {
61         // LASSERT(p, /**/);
62
63         if (a.empty() || !*p)
64                 return a;
65
66         size_t r = a.find_last_not_of(p);
67         size_t l = a.find_first_not_of(p);
68
69         // Is this the minimal test? (lgb)
70         if (r == string::npos && l == string::npos)
71                 return string();
72
73         return a.substr(l, r - l + 1);
74 }
75
76
77 void split(string const & s, vector<string> & result, char delim)
78 {
79         //cerr << "split 1: '" << s << "'\n";
80         istringstream is(s);
81         string t;
82         while (getline(is, t, delim))
83                 result.push_back(t);
84         //cerr << "split 2\n";
85 }
86
87
88 string join(vector<string> const & input, char const * delim)
89 {
90         ostringstream os;
91         for (size_t i = 0; i != input.size(); ++i) {
92                 if (i)
93                         os << delim;
94                 os << input[i];
95         }
96         return os.str();
97 }
98
99
100 char const * const * is_known(string const & str, char const * const * what)
101 {
102         for ( ; *what; ++what)
103                 if (str == *what)
104                         return what;
105         return 0;
106 }
107
108
109
110 // current stack of nested environments
111 vector<string> active_environments;
112
113
114 string active_environment()
115 {
116         return active_environments.empty() ? string() : active_environments.back();
117 }
118
119
120 CommandMap known_commands;
121 CommandMap known_environments;
122 CommandMap known_math_environments;
123
124
125 void add_known_command(string const & command, string const & o1,
126                        bool o2)
127 {
128         // We have to handle the following cases:
129         // definition                      o1    o2    invocation result
130         // \newcommand{\foo}{bar}          ""    false \foo       bar
131         // \newcommand{\foo}[1]{bar #1}    "[1]" false \foo{x}    bar x
132         // \newcommand{\foo}[1][]{bar #1}  "[1]" true  \foo       bar
133         // \newcommand{\foo}[1][]{bar #1}  "[1]" true  \foo[x]    bar x
134         // \newcommand{\foo}[1][x]{bar #1} "[1]" true  \foo[x]    bar x
135         unsigned int nargs = 0;
136         vector<ArgumentType> arguments;
137         string const opt1 = rtrim(ltrim(o1, "["), "]");
138         if (isStrUnsignedInt(opt1)) {
139                 // The command has arguments
140                 nargs = convert<unsigned int>(opt1);
141                 if (nargs > 0 && o2) {
142                         // The first argument is optional
143                         arguments.push_back(optional);
144                         --nargs;
145                 }
146         }
147         for (unsigned int i = 0; i < nargs; ++i)
148                 arguments.push_back(required);
149         known_commands[command] = arguments;
150 }
151
152
153 bool noweb_mode = false;
154
155
156 namespace {
157
158
159 /*!
160  * Read one command definition from the syntax file
161  */
162 void read_command(Parser & p, string command, CommandMap & commands)
163 {
164         if (p.next_token().asInput() == "*") {
165                 p.get_token();
166                 command += '*';
167         }
168         vector<ArgumentType> arguments;
169         while (p.next_token().cat() == catBegin ||
170                p.next_token().asInput() == "[") {
171                 if (p.next_token().cat() == catBegin) {
172                         string const arg = p.getArg('{', '}');
173                         if (arg == "translate")
174                                 arguments.push_back(required);
175                         else
176                                 arguments.push_back(verbatim);
177                 } else {
178                         p.getArg('[', ']');
179                         arguments.push_back(optional);
180                 }
181         }
182         commands[command] = arguments;
183 }
184
185
186 /*!
187  * Read a class of environments from the syntax file
188  */
189 void read_environment(Parser & p, string const & begin,
190                       CommandMap & environments)
191 {
192         string environment;
193         while (p.good()) {
194                 Token const & t = p.get_token();
195                 if (t.cat() == catLetter)
196                         environment += t.asInput();
197                 else if (!environment.empty()) {
198                         p.putback();
199                         read_command(p, environment, environments);
200                         environment.erase();
201                 }
202                 if (t.cat() == catEscape && t.asInput() == "\\end") {
203                         string const end = p.getArg('{', '}');
204                         if (end == begin)
205                                 return;
206                 }
207         }
208 }
209
210
211 /*!
212  * Read a list of TeX commands from a reLyX compatible syntax file.
213  * Since this list is used after all commands that have a LyX counterpart
214  * are handled, it does not matter that the "syntax.default" file
215  * has almost all of them listed. For the same reason the reLyX-specific
216  * reLyXre environment is ignored.
217  */
218 void read_syntaxfile(FileName const & file_name)
219 {
220         ifdocstream is(file_name.toFilesystemEncoding().c_str());
221         if (!is.good()) {
222                 cerr << "Could not open syntax file \"" << file_name
223                      << "\" for reading." << endl;
224                 exit(2);
225         }
226         // We can use our TeX parser, since the syntax of the layout file is
227         // modeled after TeX.
228         // Unknown tokens are just silently ignored, this helps us to skip some
229         // reLyX specific things.
230         Parser p(is);
231         while (p.good()) {
232                 Token const & t = p.get_token();
233                 if (t.cat() == catEscape) {
234                         string const command = t.asInput();
235                         if (command == "\\begin") {
236                                 string const name = p.getArg('{', '}');
237                                 if (name == "environments" || name == "reLyXre")
238                                         // We understand "reLyXre", but it is
239                                         // not as powerful as "environments".
240                                         read_environment(p, name,
241                                                 known_environments);
242                                 else if (name == "mathenvironments")
243                                         read_environment(p, name,
244                                                 known_math_environments);
245                         } else {
246                                 read_command(p, command, known_commands);
247                         }
248                 }
249         }
250 }
251
252
253 string documentclass;
254 string default_encoding;
255 string syntaxfile;
256 bool overwrite_files = false;
257 int error_code = 0;
258
259 /// return the number of arguments consumed
260 typedef int (*cmd_helper)(string const &, string const &);
261
262
263 int parse_help(string const &, string const &)
264 {
265         cerr << "Usage: tex2lyx [options] infile.tex [outfile.lyx]\n"
266                 "Options:\n"
267                 "\t-c textclass       Declare the textclass.\n"
268                 "\t-e encoding        Set the default encoding (latex name).\n"
269                 "\t-f                 Force overwrite of .lyx files.\n"
270                 "\t-help              Print this message and quit.\n"
271                 "\t-n                 translate a noweb (aka literate programming) file.\n"
272                 "\t-s syntaxfile      read additional syntax file.\n" 
273                 "\t-sysdir dir        Set system directory to DIR.\n"
274                 "\t-userdir DIR       Set user directory to DIR."
275              << endl;
276         exit(error_code);
277 }
278
279
280 void error_message(string const & message)
281 {
282         cerr << "tex2lyx: " << message << "\n\n";
283         error_code = 1;
284         parse_help(string(), string());
285 }
286
287
288 int parse_class(string const & arg, string const &)
289 {
290         if (arg.empty())
291                 error_message("Missing textclass string after -c switch");
292         documentclass = arg;
293         return 1;
294 }
295
296
297 int parse_encoding(string const & arg, string const &)
298 {
299         if (arg.empty())
300                 error_message("Missing encoding string after -e switch");
301         default_encoding = arg;
302         return 1;
303 }
304
305
306 int parse_syntaxfile(string const & arg, string const &)
307 {
308         if (arg.empty())
309                 error_message("Missing syntaxfile string after -s switch");
310         syntaxfile = internal_path(arg);
311         return 1;
312 }
313
314
315 // Filled with the command line arguments "foo" of "-sysdir foo" or
316 // "-userdir foo".
317 string cl_system_support;
318 string cl_user_support;
319
320
321 int parse_sysdir(string const & arg, string const &)
322 {
323         if (arg.empty())
324                 error_message("Missing directory for -sysdir switch");
325         cl_system_support = internal_path(arg);
326         return 1;
327 }
328
329
330 int parse_userdir(string const & arg, string const &)
331 {
332         if (arg.empty())
333                 error_message("Missing directory for -userdir switch");
334         cl_user_support = internal_path(arg);
335         return 1;
336 }
337
338
339 int parse_force(string const &, string const &)
340 {
341         overwrite_files = true;
342         return 0;
343 }
344
345
346 int parse_noweb(string const &, string const &)
347 {
348         noweb_mode = true;
349         return 0;
350 }
351
352
353 void easyParse(int & argc, char * argv[])
354 {
355         map<string, cmd_helper> cmdmap;
356
357         cmdmap["-c"] = parse_class;
358         cmdmap["-e"] = parse_encoding;
359         cmdmap["-f"] = parse_force;
360         cmdmap["-s"] = parse_syntaxfile;
361         cmdmap["-help"] = parse_help;
362         cmdmap["--help"] = parse_help;
363         cmdmap["-n"] = parse_noweb;
364         cmdmap["-sysdir"] = parse_sysdir;
365         cmdmap["-userdir"] = parse_userdir;
366
367         for (int i = 1; i < argc; ++i) {
368                 map<string, cmd_helper>::const_iterator it
369                         = cmdmap.find(argv[i]);
370
371                 // don't complain if not found - may be parsed later
372                 if (it == cmdmap.end()) {
373                         if (argv[i][0] == '-')
374                                 error_message(string("Unknown option `") + argv[i] + "'.");
375                         else
376                                 continue;
377                 }
378
379                 string arg(to_utf8(from_local8bit((i + 1 < argc) ? argv[i + 1] : "")));
380                 string arg2(to_utf8(from_local8bit((i + 2 < argc) ? argv[i + 2] : "")));
381
382                 int const remove = 1 + it->second(arg, arg2);
383
384                 // Now, remove used arguments by shifting
385                 // the following ones remove places down.
386                 argc -= remove;
387                 for (int j = i; j < argc; ++j)
388                         argv[j] = argv[j + remove];
389                 --i;
390         }
391 }
392
393
394 // path of the first parsed file
395 string masterFilePath;
396 // path of the currently parsed file
397 string parentFilePath;
398
399 } // anonymous namespace
400
401
402 string getMasterFilePath()
403 {
404         return masterFilePath;
405 }
406
407 string getParentFilePath()
408 {
409         return parentFilePath;
410 }
411
412
413 namespace {
414
415 /*!
416  *  Reads tex input from \a is and writes lyx output to \a os.
417  *  Uses some common settings for the preamble, so this should only
418  *  be used more than once for included documents.
419  *  Caution: Overwrites the existing preamble settings if the new document
420  *  contains a preamble.
421  *  You must ensure that \p parentFilePath is properly set before calling
422  *  this function!
423  */
424 void tex2lyx(idocstream & is, ostream & os, string const & encoding)
425 {
426         Parser p(is);
427         if (!encoding.empty())
428                 p.setEncoding(encoding);
429         //p.dump();
430
431         stringstream ss;
432         TeX2LyXDocClass textclass;
433         parse_preamble(p, ss, documentclass, textclass);
434
435         active_environments.push_back("document");
436         Context context(true, textclass);
437         parse_text(p, ss, FLAG_END, true, context);
438         if (Context::empty)
439                 // Empty document body. LyX needs at least one paragraph.
440                 context.check_layout(ss);
441         context.check_end_layout(ss);
442         ss << "\n\\end_body\n\\end_document\n";
443         active_environments.pop_back();
444         ss.seekg(0);
445         os << ss.str();
446 #ifdef TEST_PARSER
447         p.reset();
448         ofdocstream parsertest("parsertest.tex");
449         while (p.good())
450                 parsertest << p.get_token().asInput();
451         // <origfile> and parsertest.tex should now have identical content
452 #endif
453 }
454
455
456 /// convert TeX from \p infilename to LyX and write it to \p os
457 bool tex2lyx(FileName const & infilename, ostream & os, string const & encoding)
458 {
459         ifdocstream is;
460         // forbid buffering on this stream
461         is.rdbuf()->pubsetbuf(0,0);
462         is.open(infilename.toFilesystemEncoding().c_str());
463         if (!is.good()) {
464                 cerr << "Could not open input file \"" << infilename
465                      << "\" for reading." << endl;
466                 return false;
467         }
468         string const oldParentFilePath = parentFilePath;
469         parentFilePath = onlyPath(infilename.absFilename());
470         tex2lyx(is, os, encoding);
471         parentFilePath = oldParentFilePath;
472         return true;
473 }
474
475 } // anonymous namespace
476
477
478 bool tex2lyx(string const & infilename, FileName const & outfilename, 
479              string const & encoding)
480 {
481         if (outfilename.isReadableFile()) {
482                 if (overwrite_files) {
483                         cerr << "Overwriting existing file "
484                              << outfilename << endl;
485                 } else {
486                         cerr << "Not overwriting existing file "
487                              << outfilename << endl;
488                         return false;
489                 }
490         } else {
491                 cerr << "Creating file " << outfilename << endl;
492         }
493         ofstream os(outfilename.toFilesystemEncoding().c_str());
494         if (!os.good()) {
495                 cerr << "Could not open output file \"" << outfilename
496                      << "\" for writing." << endl;
497                 return false;
498         }
499 #ifdef FILEDEBUG
500         cerr << "Input file: " << infilename << "\n";
501         cerr << "Output file: " << outfilename << "\n";
502 #endif
503         return tex2lyx(FileName(infilename), os, encoding);
504 }
505
506 } // namespace lyx
507
508
509 int main(int argc, char * argv[])
510 {
511         using namespace lyx;
512
513         //setlocale(LC_CTYPE, "");
514
515         lyxerr.setStream(cerr);
516
517         easyParse(argc, argv);
518
519         if (argc <= 1) 
520                 error_message("Not enough arguments.");
521         os::init(argc, argv);
522
523         try {
524                 init_package(internal_path(to_utf8(from_local8bit(argv[0]))),
525                              cl_system_support, cl_user_support,
526                              top_build_dir_is_two_levels_up);
527         } catch (ExceptionMessage const & message) {
528                 cerr << to_utf8(message.title_) << ":\n"
529                      << to_utf8(message.details_) << endl;
530                 if (message.type_ == ErrorException)
531                         exit(1);
532         }
533
534         // Now every known option is parsed. Look for input and output
535         // file name (the latter is optional).
536         string infilename = internal_path(to_utf8(from_local8bit(argv[1])));
537         infilename = makeAbsPath(infilename).absFilename();
538
539         string outfilename;
540         if (argc > 2) {
541                 outfilename = internal_path(to_utf8(from_local8bit(argv[2])));
542                 if (outfilename != "-")
543                         outfilename = makeAbsPath(outfilename).absFilename();
544         } else
545                 outfilename = changeExtension(infilename, ".lyx");
546
547         // Read the syntax tables
548         FileName const system_syntaxfile = libFileSearch("", "syntax.default");
549         if (system_syntaxfile.empty()) {
550                 cerr << "Error: Could not find syntax file \"syntax.default\"." << endl;
551                 exit(1);
552         }
553         read_syntaxfile(system_syntaxfile);
554         if (!syntaxfile.empty())
555                 read_syntaxfile(makeAbsPath(syntaxfile));
556
557         // Read the encodings table.
558         FileName const symbols_path = libFileSearch(string(), "unicodesymbols");
559         if (symbols_path.empty()) {
560                 cerr << "Error: Could not find file \"unicodesymbols\"." 
561                      << endl;
562                 exit(1);
563         }
564         FileName const enc_path = libFileSearch(string(), "encodings");
565         if (enc_path.empty()) {
566                 cerr << "Error: Could not find file \"encodings\"." 
567                      << endl;
568                 exit(1);
569         }
570         encodings.read(enc_path, symbols_path);
571         if (!default_encoding.empty() && !encodings.fromLaTeXName(default_encoding))
572                 error_message("Unknown LaTeX encoding `" + default_encoding + "'");
573
574         // The real work now.
575         masterFilePath = onlyPath(infilename);
576         parentFilePath = masterFilePath;
577         if (outfilename == "-") {
578                 if (tex2lyx(FileName(infilename), cout, default_encoding))
579                         return EXIT_SUCCESS;
580                 else
581                         return EXIT_FAILURE;
582         } else {
583                 if (tex2lyx(infilename, FileName(outfilename), default_encoding))
584                         return EXIT_SUCCESS;
585                 else
586                         return EXIT_FAILURE;
587         }
588 }
589
590 // }])