]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/LyX.py
Move DrawStrategy enum to update_flags.h.
[lyx.git] / lib / lyx2lyx / LyX.py
1 # This file is part of lyx2lyx
2 # Copyright (C) 2002-2024 The LyX Team
3 # Copyright (C) 2002-2004 Dekel Tsur <dekel@lyx.org>
4 # Copyright (C) 2002-2006 José Matos <jamatos@lyx.org>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20 "The LyX module has all the rules related with different lyx file formats."
21
22 import codecs
23 import gzip
24 import io
25 import locale
26 import os.path
27 import re
28 import sys
29 import time
30
31 from parser_tools import (
32     check_token,
33     find_complete_lines,
34     find_end_of,
35     find_token,
36     get_value,
37 )
38
39 try:
40     import lyx2lyx_version
41
42     version__ = lyx2lyx_version.version
43     stable_version = True
44 except:  # we are running from build directory so assume the last version
45     version__ = "2.5"
46     stable_version = False
47
48 default_debug__ = 2
49
50
51 ####################################################################
52 # Private helper functions
53
54
55 def find_end_of_inset(lines, i):
56     "Find beginning of inset, where lines[i] is included."
57     return find_end_of(lines, i, "\\begin_inset", "\\end_inset")
58
59
60 def minor_versions(major, last_minor_version):
61     """Generate minor versions, using major as prefix and minor
62     versions from 0 until last_minor_version, plus the generic version.
63
64     Example:
65
66       minor_versions("1.2", 4) ->
67       [ "1.2", "1.2.0", "1.2.1", "1.2.2", "1.2.3"]
68     """
69     return [major] + [major + ".%d" % i for i in range(last_minor_version + 1)]
70
71
72 # End of helper functions
73 ####################################################################
74
75
76 # Regular expressions used
77 format_re = re.compile(r"(\d)[\.,]?(\d\d)")
78 fileformat = re.compile(r"\\lyxformat\s*(\S*)")
79 original_version = re.compile(b".*?LyX ([\\d.]*)")
80 original_tex2lyx_version = re.compile(b".*?tex2lyx ([\\d.]*)")
81
82 ##
83 # file format information:
84 #  file, supported formats, stable release versions
85 format_relation = [
86     ("0_06", [200], minor_versions("0.6", 4)),
87     ("0_08", [210], minor_versions("0.8", 6) + ["0.7"]),
88     ("0_10", [210], minor_versions("0.10", 7) + ["0.9"]),
89     ("0_12", [215], minor_versions("0.12", 1) + ["0.11"]),
90     ("1_0", [215], minor_versions("1.0", 4)),
91     ("1_1", [215], minor_versions("1.1", 4)),
92     ("1_1_5", [216], ["1.1", "1.1.5", "1.1.5.1", "1.1.5.2"]),
93     ("1_1_6_0", [217], ["1.1", "1.1.6", "1.1.6.1", "1.1.6.2"]),
94     ("1_1_6_3", [218], ["1.1", "1.1.6.3", "1.1.6.4"]),
95     ("1_2", [220], minor_versions("1.2", 4)),
96     ("1_3", [221], minor_versions("1.3", 7)),
97     # Note that range(i,j) is up to j *excluded*.
98     ("1_4", list(range(222, 246)), minor_versions("1.4", 5)),
99     ("1_5", list(range(246, 277)), minor_versions("1.5", 7)),
100     ("1_6", list(range(277, 346)), minor_versions("1.6", 10)),
101     ("2_0", list(range(346, 414)), minor_versions("2.0", 8)),
102     ("2_1", list(range(414, 475)), minor_versions("2.1", 5)),
103     ("2_2", list(range(475, 509)), minor_versions("2.2", 4)),
104     ("2_3", list(range(509, 545)), minor_versions("2.3", 7)),
105     ("2_4", list(range(545, 621)), minor_versions("2.4", 0)),
106     ("2_5", (), minor_versions("2.5", 0)),
107 ]
108
109 ####################################################################
110 # This is useful just for development versions                     #
111 # if the list of supported formats is empty get it from last step  #
112 if not format_relation[-1][1]:
113     step, mode = format_relation[-1][0], "convert"
114     convert = getattr(__import__("lyx_" + step), mode)
115     format_relation[-1] = (step, [conv[0] for conv in convert], format_relation[-1][2])
116 #                                                                  #
117 ####################################################################
118
119
120 def formats_list():
121     "Returns a list with supported file formats."
122     formats = []
123     for version in format_relation:
124         for format in version[1]:
125             if format not in formats:
126                 formats.append(format)
127     return formats
128
129
130 def format_info():
131     "Returns a list with the supported file formats."
132     template = """
133 %s\tstable format:       %s
134   \tstable versions:     %s
135   \tdevelopment formats: %s
136 """
137
138     out = "version: formats and versions"
139     for version in format_relation:
140         major = str(version[2][0])
141         versions = str(version[2][1:])
142         if len(version[1]) == 1:
143             formats = str(version[1][0])
144             stable_format = str(version[1][0])
145         elif not stable_version and major == version__:
146             stable_format = "-- not yet --"
147             versions = "-- not yet --"
148             formats = f"{version[1][0]} - {version[1][-1]}"
149         else:
150             formats = f"{version[1][0]} - {version[1][-2]}"
151             stable_format = str(version[1][-1])
152
153         out += template % (major, stable_format, versions, formats)
154     return out + "\n"
155
156
157 def get_end_format():
158     "Returns the more recent file format available."
159     # this check will fail only when we have a new version
160     # and there is no format change yet.
161     if format_relation[-1][1]:
162         return format_relation[-1][1][-1]
163     return format_relation[-2][1][-1]
164
165
166 def get_backend(textclass):
167     "For _textclass_ returns its backend."
168     if textclass == "linuxdoc" or textclass == "manpage":
169         return "linuxdoc"
170     if textclass.startswith("docbook") or textclass.startswith("agu-"):
171         return "docbook"
172     return "latex"
173
174
175 def trim_eol(line):
176     "Remove end of line char(s)."
177     if line[-1] != "\n" and line[-1] != "\r":
178         # May happen for the last line of a document
179         return line
180     if line[-2:-1] == "\r":
181         return line[:-2]
182     else:
183         return line[:-1]
184
185
186 def trim_eol_binary(line):
187     "Remove end of line char(s)."
188     if line[-1] != 10 and line[-1] != 13:
189         # May happen for the last line of a document
190         return line
191     if line[-2:-1] == 13:
192         return line[:-2]
193     else:
194         return line[:-1]
195
196
197 def get_encoding(language, inputencoding, format, cjk_encoding):
198     "Returns enconding of the lyx file"
199     if format > 248:
200         return "utf8"
201     # CJK-LyX encodes files using the current locale encoding.
202     # This means that files created by CJK-LyX can only be converted using
203     # the correct locale settings unless the encoding is given as commandline
204     # argument.
205     if cjk_encoding == "auto":
206         return locale.getpreferredencoding()
207     elif cjk_encoding:
208         return cjk_encoding
209     from lyx2lyx_lang import lang
210
211     if inputencoding == "auto" or inputencoding == "default":
212         return lang[language][3]
213     if inputencoding == "":
214         return "latin1"
215     if inputencoding == "utf8x":
216         return "utf8"
217     # python does not know the alias latin9
218     if inputencoding == "latin9":
219         return "iso-8859-15"
220     return inputencoding
221
222
223 ##
224 # Class
225 #
226 class LyX_base:
227     """This class carries all the information of the LyX file."""
228
229     def __init__(
230         self,
231         end_format=0,
232         input="",
233         output="",
234         error="",
235         debug=default_debug__,
236         try_hard=0,
237         cjk_encoding="",
238         final_version="",
239         systemlyxdir="",
240         language="english",
241         encoding="auto",
242     ):
243         """Arguments:
244         end_format: final format that the file should be converted. (integer)
245         input: the name of the input source, if empty resort to standard input.
246         output: the name of the output file, if empty use the standard output.
247         error: the name of the error file, if empty use the standard error.
248         debug: debug level, O means no debug, as its value increases be more verbose.
249         """
250         self.choose_input(input)
251         self.output = output
252
253         if error:
254             self.err = open(error, "w")
255         else:
256             self.err = sys.stderr
257
258         self.debug = debug
259         self.try_hard = try_hard
260         self.cjk_encoding = cjk_encoding
261
262         if end_format:
263             self.end_format = self.lyxformat(end_format)
264
265             # In case the target version and format are both specified
266             # verify that they are compatible. If not send a warning
267             # and ignore the version.
268             if final_version:
269                 message = "Incompatible version %s for specified format %d" % (
270                     final_version,
271                     self.end_format,
272                 )
273                 for version in format_relation:
274                     if self.end_format in version[1]:
275                         if final_version not in version[2]:
276                             self.warning(message)
277                             final_version = ""
278         elif final_version:
279             for version in format_relation:
280                 if final_version in version[2]:
281                     # set the last format for that version
282                     self.end_format = version[1][-1]
283                     break
284             else:
285                 final_version = ""
286         else:
287             self.end_format = get_end_format()
288
289         if not final_version:
290             for step in format_relation:
291                 if self.end_format in step[1]:
292                     final_version = step[2][1]
293         self.final_version = final_version
294         self.warning("Final version: %s" % self.final_version, 10)
295         self.warning("Final format: %d" % self.end_format, 10)
296
297         self.backend = "latex"
298         self.textclass = "article"
299         # This is a hack: We use '' since we don't know the default
300         # layout of the text class. LyX will parse it as default layout.
301         # FIXME: Read the layout file and use the real default layout
302         self.default_layout = ""
303         self.header = []
304         self.preamble = []
305         self.body = []
306         self.status = 0
307         self.encoding = encoding
308         self.language = language
309         self.systemlyxdir = systemlyxdir
310
311     def warning(self, message, debug_level=default_debug__):
312         """Emits warning to self.error, if the debug_level is less
313         than the self.debug."""
314         if debug_level <= self.debug:
315             self.err.write("lyx2lyx warning: " + message + "\n")
316
317     def error(self, message):
318         "Emits a warning and exits if not in try_hard mode."
319         self.warning(message)
320         if not self.try_hard:
321             self.warning("Quitting.")
322             sys.exit(1)
323
324         self.status = 2
325
326     def read(self):
327         """Reads a file into the self.header and
328         self.body parts, from self.input."""
329
330         # First pass: Read header to determine file encoding
331         # If we are running under python3 then all strings are binary in this
332         # pass. In some cases we need to convert binary to unicode in order to
333         # use our parser tools. Since we do not know the true encoding yet we
334         # use latin1. This works since a) the parts we are interested in are
335         # pure ASCII (subset of latin1) and b) in contrast to pure ascii or
336         # utf8, one can decode any 8byte string using latin1.
337         first_line = True
338         while True:
339             line = self.input.readline()
340             if not line:
341                 # eof found before end of header
342                 self.error("Invalid LyX file: Missing body.")
343
344             if first_line:
345                 # Remove UTF8 BOM marker if present
346                 if line.startswith(codecs.BOM_UTF8):
347                     line = line[len(codecs.BOM_UTF8) :]
348
349                 first_line = False
350
351             line = trim_eol_binary(line)
352             decoded = line.decode("latin1")
353             if check_token(decoded, "\\begin_preamble"):
354                 while True:
355                     line = self.input.readline()
356                     if not line:
357                         # eof found before end of header
358                         self.error("Invalid LyX file: Missing body.")
359
360                     line = trim_eol_binary(line)
361                     decoded = line.decode("latin1")
362                     if check_token(decoded, "\\end_preamble"):
363                         break
364
365                     if decoded.split()[:0] in (
366                         "\\layout",
367                         "\\begin_layout",
368                         "\\begin_body",
369                     ):
370                         self.warning(
371                             "Malformed LyX file:"
372                             "Missing '\\end_preamble'."
373                             "\nAdding it now and hoping"
374                             "for the best."
375                         )
376
377                     self.preamble.append(line)
378
379             if check_token(decoded, "\\end_preamble"):
380                 continue
381
382             line = line.rstrip()
383             if not line:
384                 continue
385
386             if decoded.split()[0] in (
387                 "\\layout",
388                 "\\begin_layout",
389                 "\\begin_body",
390                 "\\begin_deeper",
391             ):
392                 self.body.append(line)
393                 break
394
395             self.header.append(line)
396
397         i = find_token(self.header, b"\\textclass", 0)
398         if i == -1:
399             self.warning("Malformed LyX file: Missing '\\textclass'.")
400             i = find_token(self.header, b"\\lyxformat", 0) + 1
401             self.header[i:i] = [b"\\textclass article"]
402
403         self.textclass = get_value(self.header, b"\\textclass", 0, default=b"")
404         self.language = get_value(self.header, b"\\language", 0, default=b"english").decode(
405             "ascii"
406         )
407         self.inputencoding = get_value(
408             self.header, b"\\inputencoding", 0, default=b"auto"
409         ).decode("ascii")
410         self.format = self.read_format()
411         self.initial_format = self.format
412         self.encoding = get_encoding(
413             self.language, self.inputencoding, self.format, self.cjk_encoding
414         )
415         self.initial_version = self.read_version()
416
417         # Second pass over header and preamble, now we know the file encoding
418         # Do not forget the textclass (Debian bug #700828)
419         self.textclass = self.textclass.decode(self.encoding)
420         self.backend = get_backend(self.textclass)
421         for i in range(len(self.header)):
422             self.header[i] = self.header[i].decode(self.encoding)
423         for i in range(len(self.preamble)):
424             self.preamble[i] = self.preamble[i].decode(self.encoding)
425         for i in range(len(self.body)):
426             self.body[i] = self.body[i].decode(self.encoding)
427
428         # Read document body
429         while True:
430             line = self.input.readline().decode(self.encoding)
431             if not line:
432                 break
433             self.body.append(trim_eol(line))
434
435     def write(self):
436         "Writes the LyX file to self.output."
437         self.choose_output(self.output)
438         self.set_version()
439         self.set_format()
440         self.set_textclass()
441         if self.encoding == "auto":
442             self.encoding = get_encoding(
443                 self.language, self.encoding, self.format, self.cjk_encoding
444             )
445         if self.preamble:
446             i = find_token(self.header, "\\textclass", 0) + 1
447             preamble = ["\\begin_preamble"] + self.preamble + ["\\end_preamble"]
448             header = self.header[:i] + preamble + self.header[i:]
449         else:
450             header = self.header
451
452         for line in header + [""] + self.body:
453             self.output.write(line + "\n")
454
455     def choose_output(self, output):
456         """Choose output streams dealing transparently with
457         compressed files."""
458
459         # This is a bit complicated, because we need to be compatible both with
460         # python 2 and python 3. Therefore we handle the encoding here and not
461         # when writing individual lines and may need up to 3 layered file like
462         # interfaces.
463         if self.compressed:
464             if output:
465                 outputfileobj = open(output, "wb")
466             else:
467                 # We cannot not use stdout directly since it needs text, not bytes in python 3
468                 outputfileobj = os.fdopen(sys.stdout.fileno(), "wb")
469             # We cannot not use gzip.open() since it is not supported by python 2
470             zipbuffer = gzip.GzipFile(mode="wb", fileobj=outputfileobj)
471             # We do not want to use different newlines on different OSes inside zipped files
472             self.output = io.TextIOWrapper(zipbuffer, encoding=self.encoding, newline="\n")
473         else:
474             if output:
475                 self.output = open(output, "w", encoding=self.encoding)
476             else:
477                 self.output = open(sys.stdout.fileno(), "w", encoding=self.encoding)
478
479     def choose_input(self, input):
480         """Choose input stream, dealing transparently with
481         compressed files."""
482
483         # Since we do not know the encoding yet we need to read the input as
484         # bytes in binary mode, and convert later to unicode.
485         if input and input != "-":
486             self.dir = os.path.dirname(os.path.abspath(input))
487             try:
488                 gzip.open(input).readline()
489                 self.input = gzip.open(input)
490                 self.compressed = True
491             except:
492                 self.input = open(input, "rb")
493                 self.compressed = False
494         else:
495             self.dir = ""
496             self.input = os.fdopen(sys.stdin.fileno(), "rb")
497             self.compressed = False
498
499     def lyxformat(self, format):
500         "Returns the file format representation, an integer."
501         result = format_re.match(format)
502         if result:
503             format = int(result.group(1) + result.group(2))
504         elif format == "2":
505             format = 200
506         else:
507             self.error(str(format) + ": " + "Invalid LyX file.")
508
509         if format in formats_list():
510             return format
511
512         self.error(str(format) + ": " + "Format not supported.")
513         return None
514
515     def read_version(self):
516         """Searchs for clues of the LyX version used to write the
517         file, returns the most likely value, or None otherwise."""
518
519         for line in self.header:
520             if line[0:1] != b"#":
521                 return None
522
523             line = line.replace(b"fix", b".")
524             # need to test original_tex2lyx_version first because tex2lyx
525             # writes "#LyX file created by tex2lyx 2.2"
526             result = original_tex2lyx_version.match(line)
527             if not result:
528                 result = original_version.match(line)
529                 if result:
530                     # Special know cases: reLyX and KLyX
531                     if line.find(b"reLyX") != -1 or line.find(b"KLyX") != -1:
532                         return "0.12"
533             if result:
534                 res = result.group(1)
535                 if not res:
536                     self.warning(line)
537                 # self.warning("Version %s" % result.group(1))
538                 return res.decode("ascii")
539         self.warning(str(self.header[:2]))
540         return None
541
542     def set_version(self):
543         "Set the header with the version used."
544
545         initial_comment = " ".join(
546             [
547                 "#LyX %s created this file." % version__,
548                 "For more info see https://www.lyx.org/",
549             ]
550         )
551
552         # Simple heuristic to determine the comment that always starts
553         # a lyx file
554         if self.header[0].startswith("#"):
555             self.header[0] = initial_comment
556         else:
557             self.header.insert(0, initial_comment)
558
559         # Old lyx files had a two lines comment header:
560         # 1) the first line had the user who had created it
561         # 2) the second line had the lyx version used
562         # later we decided that 1) was a privacy risk for no gain
563         # here we remove the second line effectively erasing 1)
564         if self.header[1][0] == "#":
565             del self.header[1]
566
567     def read_format(self):
568         "Read from the header the fileformat of the present LyX file."
569         for line in self.header:
570             result = fileformat.match(line.decode("ascii"))
571             if result:
572                 return self.lyxformat(result.group(1))
573         else:
574             self.error("Invalid LyX File: Missing format.")
575         return None
576
577     def set_format(self):
578         "Set the file format of the file, in the header."
579         if self.format <= 217:
580             format = str(float(self.format) / 100)
581         else:
582             format = str(self.format)
583         i = find_token(self.header, "\\lyxformat", 0)
584         self.header[i] = "\\lyxformat %s" % format
585
586     def set_textclass(self):
587         i = find_token(self.header, "\\textclass", 0)
588         self.header[i] = "\\textclass %s" % self.textclass
589
590     # Note that the module will be added at the END of the extant ones
591     def add_module(self, module):
592         "Append module to the modules list."
593         i = find_token(self.header, "\\begin_modules", 0)
594         if i == -1:
595             # No modules yet included
596             i = find_token(self.header, "\\textclass", 0)
597             if i == -1:
598                 self.warning("Malformed LyX document: No \\textclass!!")
599                 return
600             modinfo = ["\\begin_modules", module, "\\end_modules"]
601             self.header[i + 1 : i + 1] = modinfo
602             return
603         j = find_token(self.header, "\\end_modules", i)
604         if j == -1:
605             self.warning("(add_module)Malformed LyX document: No \\end_modules.")
606             return
607         k = find_token(self.header, module, i)
608         if k != -1 and k < j:
609             return
610         self.header.insert(j, module)
611
612     def del_module(self, module):
613         "Delete `module` from module list, return success."
614         modlist = self.get_module_list()
615         if module not in modlist:
616             return False
617         self.set_module_list([line for line in modlist if line != module])
618         return True
619
620     def get_module_list(self):
621         "Return list of modules."
622         i = find_token(self.header, "\\begin_modules", 0)
623         if i == -1:
624             return []
625         j = find_token(self.header, "\\end_modules", i)
626         return self.header[i + 1 : j]
627
628     def set_module_list(self, mlist):
629         i = find_token(self.header, "\\begin_modules", 0)
630         if i == -1:
631             # No modules yet included
632             tclass = find_token(self.header, "\\textclass", 0)
633             if tclass == -1:
634                 self.warning("Malformed LyX document: No \\textclass!!")
635                 return
636             i = j = tclass + 1
637         else:
638             j = find_token(self.header, "\\end_modules", i)
639             if j == -1:
640                 self.warning("(set_module_list) Malformed LyX document: No \\end_modules.")
641                 return
642             j += 1
643         if mlist:
644             mlist = ["\\begin_modules"] + mlist + ["\\end_modules"]
645         self.header[i:j] = mlist
646
647     def set_parameter(self, param, value):
648         "Set the value of the header parameter."
649         i = find_token(self.header, "\\" + param, 0)
650         if i == -1:
651             self.warning("Parameter not found in the header: %s" % param, 3)
652             return
653         self.header[i] = f"\\{param} {str(value)}"
654
655     def is_default_layout(self, layout):
656         "Check whether a layout is the default layout of this class."
657         # FIXME: Check against the real text class default layout
658         if layout == "Standard" or layout == self.default_layout:
659             return 1
660         return 0
661
662     def convert(self):
663         "Convert from current (self.format) to self.end_format."
664         if self.format == self.end_format:
665             self.warning(
666                 "No conversion needed: Target format %s "
667                 "same as current format!" % self.format,
668                 default_debug__,
669             )
670             return
671
672         mode, conversion_chain = self.chain()
673         self.warning("conversion chain: " + str(conversion_chain), 3)
674
675         for step in conversion_chain:
676             steps = getattr(__import__("lyx_" + step), mode)
677
678             self.warning(f"Convertion step: {step} - {mode}", default_debug__ + 1)
679             if not steps:
680                 self.error(
681                     "The conversion to an older "
682                     "format (%s) is not implemented." % self.format
683                 )
684
685             multi_conv = len(steps) != 1
686             for version, table in steps:
687                 if (
688                     multi_conv
689                     and (self.format >= version and mode == "convert")
690                     or (self.format <= version and mode == "revert")
691                 ):
692                     continue
693
694                 for conv in table:
695                     init_t = time.time()
696                     try:
697                         conv(self)
698                     except:
699                         self.warning(
700                             "An error occurred in %s, %s" % (version, str(conv)),
701                             default_debug__,
702                         )
703                         if not self.try_hard:
704                             raise
705                         self.status = 2
706                     else:
707                         self.warning(
708                             "%lf: Elapsed time on %s" % (time.time() - init_t, str(conv)),
709                             default_debug__ + 1,
710                         )
711                 self.format = version
712                 if self.end_format == self.format:
713                     return
714
715     def chain(self):
716         """This is where all the decisions related with the
717         conversion are taken.  It returns a list of modules needed to
718         convert the LyX file from self.format to self.end_format"""
719
720         format = self.format
721         correct_version = 0
722
723         for rel in format_relation:
724             if self.initial_version in rel[2]:
725                 if format in rel[1]:
726                     initial_step = rel[0]
727                     correct_version = 1
728                     break
729
730         if not correct_version:
731             if format <= 215:
732                 self.warning(
733                     "Version does not match file format, "
734                     "discarding it. (Version %s, format %d)"
735                     % (self.initial_version, self.format)
736                 )
737             for rel in format_relation:
738                 if format in rel[1]:
739                     initial_step = rel[0]
740                     break
741             else:
742                 # This should not happen, really.
743                 self.error("Format not supported.")
744
745         # Find the final step
746         for rel in format_relation:
747             if self.end_format in rel[1]:
748                 final_step = rel[0]
749                 break
750         else:
751             self.error("Format not supported.")
752
753         # Convertion mode, back or forth
754         steps = []
755         if (initial_step, self.initial_format) < (final_step, self.end_format):
756             mode = "convert"
757             full_steps = []
758             for step in format_relation:
759                 if initial_step <= step[0] <= final_step and step[2][0] <= self.final_version:
760                     full_steps.append(step)
761             if full_steps[0][1][-1] == self.format:
762                 full_steps = full_steps[1:]
763             for step in full_steps:
764                 steps.append(step[0])
765         else:
766             mode = "revert"
767             relation_format = format_relation[:]
768             relation_format.reverse()
769             last_step = None
770
771             for step in relation_format:
772                 if final_step <= step[0] <= initial_step:
773                     steps.append(step[0])
774                     last_step = step
775
776             if last_step[1][-1] == self.end_format:
777                 steps.pop()
778
779         self.warning("Convertion mode: %s\tsteps%s" % (mode, steps), 10)
780         return mode, steps
781
782     def append_local_layout(self, new_layout):
783         "Append `new_layout` to the local layouts."
784         # new_layout may be a string or a list of strings (lines)
785         try:
786             new_layout = new_layout.splitlines()
787         except AttributeError:
788             pass
789         i = find_token(self.header, "\\begin_local_layout", 0)
790         if i == -1:
791             k = find_token(self.header, "\\language", 0)
792             if k == -1:
793                 # this should not happen
794                 self.warning("Malformed LyX document! No \\language header found!")
795                 return
796             self.header[k:k] = ["\\begin_local_layout", "\\end_local_layout"]
797             i = k
798
799         j = find_end_of(self.header, i, "\\begin_local_layout", "\\end_local_layout")
800         if j == -1:
801             # this should not happen
802             self.warning("Malformed LyX document: Can't find end of local layout!")
803             return
804
805         self.header[i + 1 : i + 1] = new_layout
806
807     def del_local_layout(self, layout_def):
808         "Delete `layout_def` from local layouts, return success."
809         i = find_complete_lines(self.header, layout_def)
810         if i == -1:
811             return False
812         j = i + len(layout_def)
813         if (
814             self.header[i - 1] == "\\begin_local_layout"
815             and self.header[j] == "\\end_local_layout"
816         ):
817             i -= 1
818             j += 1
819         self.header[i:j] = []
820         return True
821
822     def del_from_header(self, lines):
823         "Delete `lines` from the document header, return success."
824         i = find_complete_lines(self.header, lines)
825         if i == -1:
826             return False
827         j = i + len(lines)
828         self.header[i:j] = []
829         return True
830
831
832 # Part of an unfinished attempt to make lyx2lyx gave a more
833 # structured view of the document.
834 #    def get_toc(self, depth = 4):
835 #        " Returns the TOC of this LyX document."
836 #        paragraphs_filter = {'Title' : 0,'Chapter' : 1, 'Section' : 2,
837 #                             'Subsection' : 3, 'Subsubsection': 4}
838 #        allowed_insets = ['Quotes']
839 #        allowed_parameters = ('\\paragraph_spacing', '\\noindent',
840 #                              '\\align', '\\labelwidthstring',
841 #                              "\\start_of_appendix", "\\leftindent")
842 #        sections = []
843 #        for section in paragraphs_filter.keys():
844 #            sections.append('\\begin_layout %s' % section)
845
846 #        toc_par = []
847 #        i = 0
848 #        while True:
849 #            i = find_tokens(self.body, sections, i)
850 #            if i == -1:
851 #                break
852
853 #            j = find_end_of(self.body,  i + 1, '\\begin_layout', '\\end_layout')
854 #            if j == -1:
855 #                self.warning('Incomplete file.', 0)
856 #                break
857
858 #            section = self.body[i].split()[1]
859 #            if section[-1] == '*':
860 #                section = section[:-1]
861
862 #            par = []
863
864 #            k = i + 1
865 #            # skip paragraph parameters
866 #            while not self.body[k].strip() or self.body[k].split()[0] \
867 #                      in allowed_parameters:
868 #                k += 1
869
870 #            while k < j:
871 #                if check_token(self.body[k], '\\begin_inset'):
872 #                    inset = self.body[k].split()[1]
873 #                    end = find_end_of_inset(self.body, k)
874 #                    if end == -1 or end > j:
875 #                        self.warning('Malformed file.', 0)
876
877 #                    if inset in allowed_insets:
878 #                        par.extend(self.body[k: end+1])
879 #                    k = end + 1
880 #                else:
881 #                    par.append(self.body[k])
882 #                    k += 1
883
884 #            # trim empty lines in the end.
885 #            while par and par[-1].strip() == '':
886 #                par.pop()
887
888 #            toc_par.append(Paragraph(section, par))
889
890 #            i = j + 1
891
892 #        return toc_par
893
894
895 class File(LyX_base):
896     "This class reads existing LyX files."
897
898     def __init__(
899         self,
900         end_format=0,
901         input="",
902         output="",
903         error="",
904         debug=default_debug__,
905         try_hard=0,
906         cjk_encoding="",
907         final_version="",
908         systemlyxdir="",
909     ):
910         LyX_base.__init__(
911             self,
912             end_format,
913             input,
914             output,
915             error,
916             debug,
917             try_hard,
918             cjk_encoding,
919             final_version,
920             systemlyxdir,
921         )
922         self.read()
923
924
925 # FIXME: header settings are completely outdated, don't use like this
926 # class NewFile(LyX_base):
927 #    " This class is to create new LyX files."
928 #    def set_header(self, **params):
929 #        # set default values
930 #        self.header.extend([
931 #            "#LyX xxxx created this file."
932 #            "For more info see http://www.lyx.org/",
933 #            "\\lyxformat xxx",
934 #            "\\begin_document",
935 #            "\\begin_header",
936 #            "\\textclass article",
937 #            "\\language english",
938 #            "\\inputencoding auto",
939 #            "\\font_roman default",
940 #            "\\font_sans default",
941 #            "\\font_typewriter default",
942 #            "\\font_default_family default",
943 #            "\\font_sc false",
944 #            "\\font_osf false",
945 #            "\\font_sf_scale 100",
946 #            "\\font_tt_scale 100",
947 #            "\\graphics default",
948 #            "\\paperfontsize default",
949 #            "\\papersize default",
950 #            "\\use_geometry false",
951 #            "\\use_amsmath 1",
952 #            "\\cite_engine basic",
953 #            "\\use_bibtopic false",
954 #            "\\use_indices false",
955 #            "\\paperorientation portrait",
956 #            "\\secnumdepth 3",
957 #            "\\tocdepth 3",
958 #            "\\paragraph_separation indent",
959 #            "\\defskip medskip",
960 #            "\\quotes_language english",
961 #            "\\papercolumns 1",
962 #            "\\papersides 1",
963 #            "\\paperpagestyle default",
964 #            "\\tracking_changes false",
965 #            "\\end_header"])
966
967 #        self.format = get_end_format()
968 #        for param in params:
969 #            self.set_parameter(param, params[param])
970
971
972 #    def set_body(self, paragraphs):
973 #        self.body.extend(['\\begin_body',''])
974
975 #        for par in paragraphs:
976 #            self.body.extend(par.asLines())
977
978 #        self.body.extend(['','\\end_body', '\\end_document'])
979
980
981 # Part of an unfinished attempt to make lyx2lyx gave a more
982 # structured view of the document.
983 # class Paragraph:
984 #    # unfinished implementation, it is missing the Text and Insets
985 #    # representation.
986 #    " This class represents the LyX paragraphs."
987 #    def __init__(self, name, body=[], settings = [], child = []):
988 #        """ Parameters:
989 #        name: paragraph name.
990 #        body: list of lines of body text.
991 #        child: list of paragraphs that descend from this paragraph.
992 #        """
993 #        self.name = name
994 #        self.body = body
995 #        self.settings = settings
996 #        self.child = child
997
998 #    def asLines(self):
999 #        """ Converts the paragraph to a list of strings, representing
1000 #        it in the LyX file."""
1001
1002 #        result = ['','\\begin_layout %s' % self.name]
1003 #        result.extend(self.settings)
1004 #        result.append('')
1005 #        result.extend(self.body)
1006 #        result.append('\\end_layout')
1007
1008 #        if not self.child:
1009 #            return result
1010
1011 #        result.append('\\begin_deeper')
1012 #        for node in self.child:
1013 #            result.extend(node.asLines())
1014 #        result.append('\\end_deeper')
1015
1016 #        return result