1 # This file is part of lyx2lyx
2 # -*- coding: iso-8859-1 -*-
3 # Copyright (C) 2002-2004 Dekel Tsur <dekel@lyx.org>, José Matos <jamatos@lyx.org>
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19 from parser_tools
import get_value
, check_token
, find_token
,\
20 find_tokens
, find_end_of
, find_end_of_inset
28 version_lyx2lyx
= "1.4.0cvs"
29 default_debug_level
= 2
31 # Regular expressions used
32 format_re
= re
.compile(r
"(\d)[\.,]?(\d\d)")
33 fileformat
= re
.compile(r
"\\lyxformat\s*(\S*)")
34 original_version
= re
.compile(r
"\#LyX (\S*)")
37 # file format information:
38 # file, supported formats, stable release versions
39 format_relation
= [("0_10", [210], ["0.10.7","0.10"]),
40 ("0_12", [215], ["0.12","0.12.1","0.12"]),
41 ("1_0_0", [215], ["1.0.0","1.0"]),
42 ("1_0_1", [215], ["1.0.1","1.0.2","1.0.3","1.0.4", "1.1.2","1.1"]),
43 ("1_1_4", [215], ["1.1.4","1.1"]),
44 ("1_1_5", [216], ["1.1.5","1.1.5fix1","1.1.5fix2","1.1"]),
45 ("1_1_6", [217], ["1.1.6","1.1.6fix1","1.1.6fix2","1.1"]),
46 ("1_1_6fix3", [218], ["1.1.6fix3","1.1.6fix4","1.1"]),
47 ("1_2", [220], ["1.2.0","1.2.1","1.2.3","1.2.4","1.2"]),
48 ("1_3", [221], ["1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.3.6","1.3"]),
49 ("1_4", range(222,246), ["1.4.0cvs","1.4"])]
53 " Returns a list with supported file formats."
55 for version
in format_relation
:
56 for format
in version
[1]:
57 if format
not in formats
:
58 formats
.append(format
)
63 " Returns the more recent file format available."
64 return format_relation
[-1][1][-1]
67 def get_backend(textclass
):
68 " For _textclass_ returns its backend."
69 if textclass
== "linuxdoc" or textclass
== "manpage":
71 if textclass
[:7] == "docbook":
77 " Remove end of line char(s)."
78 if line
[-2:-1] == '\r':
88 """This class carries all the information of the LyX file."""
89 def __init__(self
, end_format
= 0, input = "", output
= "", error
= "", debug
= default_debug_level
, try_hard
= 0):
91 end_format: final format that the file should be converted. (integer)
92 input: the name of the input source, if empty resort to standard input.
93 output: the name of the output file, if empty use the standard output.
94 error: the name of the error file, if empty use the standard error.
95 debug: debug level, O means no debug, as its value increases be more verbose.
97 self
.choose_io(input, output
)
100 self
.err
= open(error
, "w")
102 self
.err
= sys
.stderr
105 self
.try_hard
= try_hard
108 self
.end_format
= self
.lyxformat(end_format
)
110 self
.end_format
= get_end_format()
112 self
.backend
= "latex"
113 self
.textclass
= "article"
120 def warning(self
, message
, debug_level
= default_debug_level
):
121 " Emits warning to self.error, if the debug_level is less than the self.debug."
122 if debug_level
<= self
.debug
:
123 self
.err
.write("Warning: " + message
+ "\n")
126 def error(self
, message
):
127 " Emits a warning and exits if not in try_hard mode."
128 self
.warning(message
)
129 if not self
.try_hard
:
130 self
.warning("Quiting.")
137 """Reads a file into the self.header and self.body parts, from self.input."""
140 line
= self
.input.readline()
142 self
.error("Invalid LyX file.")
144 line
= trim_eol(line
)
145 if check_token(line
, '\\begin_preamble'):
147 line
= self
.input.readline()
149 self
.error("Invalid LyX file.")
151 line
= trim_eol(line
)
152 if check_token(line
, '\\end_preamble'):
155 if string
.split(line
)[:0] in ("\\layout", "\\begin_layout", "\\begin_body"):
156 self
.warning("Malformed LyX file: Missing '\\end_preamble'.")
157 self
.warning("Adding it now and hoping for the best.")
159 self
.preamble
.append(line
)
161 if check_token(line
, '\\end_preamble'):
164 line
= string
.strip(line
)
168 if string
.split(line
)[0] in ("\\layout", "\\begin_layout", "\\begin_body"):
169 self
.body
.append(line
)
172 self
.header
.append(line
)
175 line
= self
.input.readline()
178 self
.body
.append(trim_eol(line
))
180 self
.textclass
= get_value(self
.header
, "\\textclass", 0)
181 self
.backend
= get_backend(self
.textclass
)
182 self
.format
= self
.read_format()
183 self
.language
= get_value(self
.header
, "\\language", 0)
184 if self
.language
== "":
185 self
.language
= "english"
186 self
.initial_version
= self
.read_version()
190 " Writes the LyX file to self.output."
195 i
= find_token(self
.header
, '\\textclass', 0) + 1
196 preamble
= ['\\begin_preamble'] + self
.preamble
+ ['\\end_preamble']
198 self
.error("Malformed LyX file: Missing '\\textclass'.")
200 header
= self
.header
[:i
] + preamble
+ self
.header
[i
:]
204 for line
in header
+ [''] + self
.body
:
205 self
.output
.write(line
+"\n")
208 def choose_io(self
, input, output
):
209 """Choose input and output streams, dealing transparently with
213 self
.output
= open(output
, "wb")
215 self
.output
= sys
.stdout
217 if input and input != '-':
218 self
.dir = os
.path
.dirname(os
.path
.abspath(input))
220 gzip
.open(input).readline()
221 self
.input = gzip
.open(input)
222 self
.output
= gzip
.GzipFile(mode
="wb", fileobj
=self
.output
)
224 self
.input = open(input)
226 self
.input = sys
.stdin
229 def lyxformat(self
, format
):
230 " Returns the file format representation, an integer."
231 result
= format_re
.match(format
)
233 format
= int(result
.group(1) + result
.group(2))
235 self
.error(str(format
) + ": " + "Invalid LyX file.")
237 if format
in formats_list():
240 self
.error(str(format
) + ": " + "Format not supported.")
244 def read_version(self
):
245 """ Searchs for clues of the LyX version used to write the file, returns the
246 most likely value, or None otherwise."""
247 for line
in self
.header
:
251 result
= original_version
.match(line
)
253 return result
.group(1)
257 def set_version(self
):
258 " Set the header with the version used."
259 self
.header
[0] = "#LyX %s created this file. For more info see http://www.lyx.org/" % version_lyx2lyx
260 if self
.header
[1][0] == '#':
264 def read_format(self
):
265 " Read from the header the fileformat of the present LyX file."
266 for line
in self
.header
:
267 result
= fileformat
.match(line
)
269 return self
.lyxformat(result
.group(1))
271 self
.error("Invalid LyX File.")
275 def set_format(self
):
276 " Set the file format of the file, in the header."
277 if self
.format
<= 217:
278 format
= str(float(self
.format
)/100)
280 format
= str(self
.format
)
281 i
= find_token(self
.header
, "\\lyxformat", 0)
282 self
.header
[i
] = "\\lyxformat %s" % format
285 def set_parameter(self
, param
, value
):
286 " Set the value of the header parameter."
287 i
= find_token(self
.header
, '\\' + param
, 0)
289 self
.warning('Parameter not found in the header: %s' % param
, 3)
291 self
.header
[i
] = '\\%s %s' % (param
, str(value
))
295 "Convert from current (self.format) to self.end_format."
296 mode
, convertion_chain
= self
.chain()
297 self
.warning("convertion chain: " + str(convertion_chain
), 3)
299 for step
in convertion_chain
:
300 steps
= getattr(__import__("lyx_" + step
), mode
)
302 self
.warning("Convertion step: %s - %s" % (step
, mode
), default_debug_level
+ 1)
304 self
.error("The convertion to an older format (%s) is not implemented." % self
.format
)
306 multi_conv
= len(steps
) != 1
307 for version
, table
in steps
:
309 (self
.format
>= version
and mode
== "convert") or\
310 (self
.format
<= version
and mode
== "revert"):
318 self
.warning("An error ocurred in %s, %s" % (version
, str(conv
)),
320 if not self
.try_hard
:
324 self
.warning("%lf: Elapsed time on %s" % (time
.time() - init_t
, str(conv
)),
325 default_debug_level
+ 1)
327 self
.format
= version
328 if self
.end_format
== self
.format
:
333 """ This is where all the decisions related with the convertion are taken.
334 It returns a list of modules needed to convert the LyX file from
335 self.format to self.end_format"""
337 self
.start
= self
.format
341 for rel
in format_relation
:
342 if self
.initial_version
in rel
[2]:
344 initial_step
= rel
[0]
348 if not correct_version
:
350 self
.warning("Version does not match file format, discarding it.")
351 for rel
in format_relation
:
353 initial_step
= rel
[0]
356 # This should not happen, really.
357 self
.error("Format not supported.")
359 # Find the final step
360 for rel
in format_relation
:
361 if self
.end_format
in rel
[1]:
365 self
.error("Format not supported.")
367 # Convertion mode, back or forth
369 if (initial_step
, self
.start
) < (final_step
, self
.end_format
):
372 for step
in format_relation
:
373 if initial_step
<= step
[0] <= final_step
:
374 if first_step
and len(step
[1]) == 1:
377 steps
.append(step
[0])
380 relation_format
= format_relation
[:]
381 relation_format
.reverse()
384 for step
in relation_format
:
385 if final_step
<= step
[0] <= initial_step
:
386 steps
.append(step
[0])
389 if last_step
[1][-1] == self
.end_format
:
395 def get_toc(self
, depth
= 4):
396 " Returns the TOC of this LyX document."
397 paragraphs_filter
= {'Title' : 0,'Chapter' : 1, 'Section' : 2, 'Subsection' : 3, 'Subsubsection': 4}
398 allowed_insets
= ['Quotes']
399 allowed_parameters
= '\\paragraph_spacing', '\\noindent', '\\align', '\\labelwidthstring', "\\start_of_appendix", "\\leftindent"
402 for section
in paragraphs_filter
.keys():
403 sections
.append('\\begin_layout %s' % section
)
408 i
= find_tokens(self
.body
, sections
, i
)
412 j
= find_end_of(self
.body
, i
+ 1, '\\begin_layout', '\\end_layout')
414 self
.warning('Incomplete file.', 0)
417 section
= string
.split(self
.body
[i
])[1]
418 if section
[-1] == '*':
419 section
= section
[:-1]
424 # skip paragraph parameters
425 while not string
.strip(self
.body
[k
]) or string
.split(self
.body
[k
])[0] in allowed_parameters
:
429 if check_token(self
.body
[k
], '\\begin_inset'):
430 inset
= string
.split(self
.body
[k
])[1]
431 end
= find_end_of_inset(self
.body
, k
)
432 if end
== -1 or end
> j
:
433 self
.warning('Malformed file.', 0)
435 if inset
in allowed_insets
:
436 par
.extend(self
.body
[k
: end
+1])
439 par
.append(self
.body
[k
])
442 # trim empty lines in the end.
443 while string
.strip(par
[-1]) == '' and par
:
446 toc_par
.append(Paragraph(section
, par
))
453 class File(LyX_Base
):
454 " This class reads existing LyX files."
455 def __init__(self
, end_format
= 0, input = "", output
= "", error
= "", debug
= default_debug_level
, try_hard
= 0):
456 LyX_Base
.__init
__(self
, end_format
, input, output
, error
, debug
, try_hard
)
460 class NewFile(LyX_Base
):
461 " This class is to create new LyX files."
462 def set_header(self
, **params
):
465 "#LyX xxxx created this file. For more info see http://www.lyx.org/",
469 "\\textclass article",
470 "\\language english",
471 "\\inputencoding auto",
472 "\\fontscheme default",
473 "\\graphics default",
474 "\\paperfontsize default",
475 "\\papersize default",
476 "\\use_geometry false",
478 "\\cite_engine basic",
479 "\\use_bibtopic false",
480 "\\paperorientation portrait",
483 "\\paragraph_separation indent",
485 "\\quotes_language english",
488 "\\paperpagestyle default",
489 "\\tracking_changes false",
492 self
.format
= get_end_format()
494 self
.set_parameter(param
, params
[param
])
497 def set_body(self
, paragraphs
):
498 self
.body
.extend(['\\begin_body',''])
500 for par
in paragraphs
:
501 self
.body
.extend(par
.asLines())
503 self
.body
.extend(['','\\end_body', '\\end_document'])
507 # unfinished implementation, it is missing the Text and Insets representation.
508 " This class represents the LyX paragraphs."
509 def __init__(self
, name
, body
=[], settings
= [], child
= []):
511 name: paragraph name.
512 body: list of lines of body text.
513 child: list of paragraphs that descend from this paragraph.
517 self
.settings
= settings
521 " Converts the paragraph to a list of strings, representing it in the LyX file."
522 result
= ['','\\begin_layout %s' % self
.name
]
523 result
.extend(self
.settings
)
525 result
.extend(self
.body
)
526 result
.append('\\end_layout')
531 result
.append('\\begin_deeper')
532 for node
in self
.child
:
533 result
.extend(node
.asLines())
534 result
.append('\\end_deeper')
540 " This class represents the LyX insets."
545 " This class represents simple chuncks of text."