1 # This file is part of lyx2lyx
2 # -*- coding: utf-8 -*-
3 # Copyright (C) 2002-2004 Dekel Tsur <dekel@lyx.org>
4 # Copyright (C) 2002-2006 José Matos <jamatos@lyx.org>
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.
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.
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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
20 " The LyX module has all the rules related with different lyx file formats."
22 from parser_tools
import get_value
, check_token
, find_token
, \
23 find_tokens
, find_end_of
32 import lyx2lyx_version
33 version__
= lyx2lyx_version
.version
34 except: # we are running from build directory so assume the last version
35 version__
= '1.6.0svn'
39 ####################################################################
40 # Private helper functions
42 def find_end_of_inset(lines
, i
):
43 " Find beginning of inset, where lines[i] is included."
44 return find_end_of(lines
, i
, "\\begin_inset", "\\end_inset")
46 def minor_versions(major
, last_minor_version
):
47 """ Generate minor versions, using major as prefix and minor
48 versions from 0 until last_minor_version, plus the generic version.
52 minor_versions("1.2", 4) ->
53 [ "1.2", "1.2.0", "1.2.1", "1.2.2", "1.2.3"]
55 return [major
] + [major
+ ".%d" % i
for i
in range(last_minor_version
+ 1)]
58 # End of helper functions
59 ####################################################################
62 # Regular expressions used
63 format_re
= re
.compile(r
"(\d)[\.,]?(\d\d)")
64 fileformat
= re
.compile(r
"\\lyxformat\s*(\S*)")
65 original_version
= re
.compile(r
".*?LyX ([\d.]*)")
68 # file format information:
69 # file, supported formats, stable release versions
70 format_relation
= [("0_06", [200], minor_versions("0.6" , 4)),
71 ("0_08", [210], minor_versions("0.8" , 6) + ["0.7"]),
72 ("0_10", [210], minor_versions("0.10", 7) + ["0.9"]),
73 ("0_12", [215], minor_versions("0.12", 1) + ["0.11"]),
74 ("1_0", [215], minor_versions("1.0" , 4)),
75 ("1_1", [215], minor_versions("1.1" , 4)),
76 ("1_1_5", [216], ["1.1.5","1.1.5.1","1.1.5.2","1.1"]),
77 ("1_1_6_0", [217], ["1.1.6","1.1.6.1","1.1.6.2","1.1"]),
78 ("1_1_6_3", [218], ["1.1.6.3","1.1.6.4","1.1"]),
79 ("1_2", [220], minor_versions("1.2" , 4)),
80 ("1_3", [221], minor_versions("1.3" , 7)),
81 ("1_4", range(222,246), minor_versions("1.4" , 5)),
82 ("1_5", range(246,277), minor_versions("1.5" , 6)),
83 ("1_6", range(277,346), minor_versions("1.6" , 0))]
85 ####################################################################
86 # This is useful just for development versions #
87 # if the list of supported formats is empty get it from last step #
88 if not format_relation
[-1][1]:
89 step
, mode
= format_relation
[-1][0], "convert"
90 convert
= getattr(__import__("lyx_" + step
), mode
)
91 format_relation
[-1] = (step
,
92 [conv
[0] for conv
in convert
],
93 format_relation
[-1][2])
95 ####################################################################
98 " Returns a list with supported file formats."
100 for version
in format_relation
:
101 for format
in version
[1]:
102 if format
not in formats
:
103 formats
.append(format
)
107 def get_end_format():
108 " Returns the more recent file format available."
109 return format_relation
[-1][1][-1]
112 def get_backend(textclass
):
113 " For _textclass_ returns its backend."
114 if textclass
== "linuxdoc" or textclass
== "manpage":
116 if textclass
.startswith("docbook") or textclass
.startswith("agu-"):
122 " Remove end of line char(s)."
123 if line
[-2:-1] == '\r':
129 def get_encoding(language
, inputencoding
, format
, cjk_encoding
):
130 " Returns enconding of the lyx file"
133 # CJK-LyX encodes files using the current locale encoding.
134 # This means that files created by CJK-LyX can only be converted using
135 # the correct locale settings unless the encoding is given as commandline
137 if cjk_encoding
== 'auto':
138 return locale
.getpreferredencoding()
141 from lyx2lyx_lang
import lang
142 if inputencoding
== "auto" or inputencoding
== "default":
143 return lang
[language
][3]
144 if inputencoding
== "":
146 if inputencoding
== "utf8x":
148 # python does not know the alias latin9
149 if inputencoding
== "latin9":
157 """This class carries all the information of the LyX file."""
159 def __init__(self
, end_format
= 0, input = "", output
= "", error
= "",
160 debug
= default_debug__
, try_hard
= 0, cjk_encoding
= '',
161 language
= "english", encoding
= "auto"):
164 end_format: final format that the file should be converted. (integer)
165 input: the name of the input source, if empty resort to standard input.
166 output: the name of the output file, if empty use the standard output.
167 error: the name of the error file, if empty use the standard error.
168 debug: debug level, O means no debug, as its value increases be more verbose.
170 self
.choose_io(input, output
)
173 self
.err
= open(error
, "w")
175 self
.err
= sys
.stderr
178 self
.try_hard
= try_hard
179 self
.cjk_encoding
= cjk_encoding
182 self
.end_format
= self
.lyxformat(end_format
)
184 self
.end_format
= get_end_format()
186 self
.backend
= "latex"
187 self
.textclass
= "article"
188 # This is a hack: We use '' since we don't know the default
189 # layout of the text class. LyX will parse it as default layout.
190 # FIXME: Read the layout file and use the real default layout
191 self
.default_layout
= ''
196 self
.encoding
= encoding
197 self
.language
= language
200 def warning(self
, message
, debug_level
= default_debug__
):
201 """ Emits warning to self.error, if the debug_level is less
202 than the self.debug."""
203 if debug_level
<= self
.debug
:
204 self
.err
.write("Warning: " + message
+ "\n")
207 def error(self
, message
):
208 " Emits a warning and exits if not in try_hard mode."
209 self
.warning(message
)
210 if not self
.try_hard
:
211 self
.warning("Quiting.")
218 """Reads a file into the self.header and
219 self.body parts, from self.input."""
222 line
= self
.input.readline()
224 self
.error("Invalid LyX file.")
226 line
= trim_eol(line
)
227 if check_token(line
, '\\begin_preamble'):
229 line
= self
.input.readline()
231 self
.error("Invalid LyX file.")
233 line
= trim_eol(line
)
234 if check_token(line
, '\\end_preamble'):
237 if line
.split()[:0] in ("\\layout",
238 "\\begin_layout", "\\begin_body"):
240 self
.warning("Malformed LyX file:"
241 "Missing '\\end_preamble'."
242 "\nAdding it now and hoping"
245 self
.preamble
.append(line
)
247 if check_token(line
, '\\end_preamble'):
254 if line
.split()[0] in ("\\layout", "\\begin_layout",
255 "\\begin_body", "\\begin_deeper"):
256 self
.body
.append(line
)
259 self
.header
.append(line
)
261 i
= find_token(self
.header
, '\\textclass', 0)
263 self
.warning("Malformed LyX file: Missing '\\textclass'.")
264 i
= find_token(self
.header
, '\\lyxformat', 0) + 1
265 self
.header
[i
:i
] = ['\\textclass article']
267 self
.textclass
= get_value(self
.header
, "\\textclass", 0)
268 self
.backend
= get_backend(self
.textclass
)
269 self
.format
= self
.read_format()
270 self
.language
= get_value(self
.header
, "\\language", 0,
272 self
.inputencoding
= get_value(self
.header
, "\\inputencoding",
274 self
.encoding
= get_encoding(self
.language
,
275 self
.inputencoding
, self
.format
,
277 self
.initial_version
= self
.read_version()
279 # Second pass over header and preamble, now we know the file encoding
280 for i
in range(len(self
.header
)):
281 self
.header
[i
] = self
.header
[i
].decode(self
.encoding
)
282 for i
in range(len(self
.preamble
)):
283 self
.preamble
[i
] = self
.preamble
[i
].decode(self
.encoding
)
287 line
= self
.input.readline().decode(self
.encoding
)
290 self
.body
.append(trim_eol(line
))
294 " Writes the LyX file to self.output."
298 if self
.encoding
== "auto":
299 self
.encoding
= get_encoding(self
.language
, self
.encoding
,
300 self
.format
, self
.cjk_encoding
)
302 i
= find_token(self
.header
, '\\textclass', 0) + 1
303 preamble
= ['\\begin_preamble'] + self
.preamble
+ ['\\end_preamble']
304 header
= self
.header
[:i
] + preamble
+ self
.header
[i
:]
308 for line
in header
+ [''] + self
.body
:
309 self
.output
.write(line
.encode(self
.encoding
)+"\n")
312 def choose_io(self
, input, output
):
313 """Choose input and output streams, dealing transparently with
317 self
.output
= open(output
, "wb")
319 self
.output
= sys
.stdout
321 if input and input != '-':
322 self
.dir = os
.path
.dirname(os
.path
.abspath(input))
324 gzip
.open(input).readline()
325 self
.input = gzip
.open(input)
326 self
.output
= gzip
.GzipFile(mode
="wb", fileobj
=self
.output
)
328 self
.input = open(input)
331 self
.input = sys
.stdin
334 def lyxformat(self
, format
):
335 " Returns the file format representation, an integer."
336 result
= format_re
.match(format
)
338 format
= int(result
.group(1) + result
.group(2))
342 self
.error(str(format
) + ": " + "Invalid LyX file.")
344 if format
in formats_list():
347 self
.error(str(format
) + ": " + "Format not supported.")
351 def read_version(self
):
352 """ Searchs for clues of the LyX version used to write the
353 file, returns the most likely value, or None otherwise."""
355 for line
in self
.header
:
359 line
= line
.replace("fix",".")
360 result
= original_version
.match(line
)
362 # Special know cases: reLyX and KLyX
363 if line
.find("reLyX") != -1 or line
.find("KLyX") != -1:
366 res
= result
.group(1)
369 #self.warning("Version %s" % result.group(1))
371 self
.warning(str(self
.header
[:2]))
375 def set_version(self
):
376 " Set the header with the version used."
377 self
.header
[0] = " ".join(["#LyX %s created this file." % version__
,
378 "For more info see http://www.lyx.org/"])
379 if self
.header
[1][0] == '#':
383 def read_format(self
):
384 " Read from the header the fileformat of the present LyX file."
385 for line
in self
.header
:
386 result
= fileformat
.match(line
)
388 return self
.lyxformat(result
.group(1))
390 self
.error("Invalid LyX File.")
394 def set_format(self
):
395 " Set the file format of the file, in the header."
396 if self
.format
<= 217:
397 format
= str(float(self
.format
)/100)
399 format
= str(self
.format
)
400 i
= find_token(self
.header
, "\\lyxformat", 0)
401 self
.header
[i
] = "\\lyxformat %s" % format
404 def set_textclass(self
):
405 i
= find_token(self
.header
, "\\textclass", 0)
406 self
.header
[i
] = "\\textclass %s" % self
.textclass
409 #Note that the module will be added at the END of the extant ones
410 def add_module(self
, module
):
411 i
= find_token(self
.header
, "\\begin_modules", 0)
413 #No modules yet included
414 i
= find_token(self
.header
, "\\textclass", 0)
416 self
.warning("Malformed LyX document: No \\textclass!!")
418 modinfo
= ["\\begin_modules", module
, "\\end_modules"]
419 self
.header
[i
+ 1: i
+ 1] = modinfo
421 j
= find_token(self
.header
, "\\end_modules", i
)
423 self
.warning("(add_module)Malformed LyX document: No \\end_modules.")
425 k
= find_token(self
.header
, module
, i
)
426 if k
!= -1 and k
< j
:
428 self
.header
.insert(j
, module
)
431 def get_module_list(self
):
432 i
= find_token(self
.header
, "\\begin_modules", 0)
435 j
= find_token(self
.header
, "\\end_modules", i
)
436 return self
.header
[i
+ 1 : j
]
439 def set_module_list(self
, mlist
):
440 modbegin
= find_token(self
.header
, "\\begin_modules", 0)
441 newmodlist
= ['\\begin_modules'] + mlist
+ ['\\end_modules']
443 #No modules yet included
444 tclass
= find_token(self
.header
, "\\textclass", 0)
446 self
.warning("Malformed LyX document: No \\textclass!!")
448 modbegin
= tclass
+ 1
449 self
.header
[modbegin
:modbegin
] = newmodlist
451 modend
= find_token(self
.header
, "\\end_modules", modbegin
)
453 self
.warning("(set_module_list)Malformed LyX document: No \\end_modules.")
455 newmodlist
= ['\\begin_modules'] + mlist
+ ['\\end_modules']
456 self
.header
[modbegin
:modend
+ 1] = newmodlist
459 def set_parameter(self
, param
, value
):
460 " Set the value of the header parameter."
461 i
= find_token(self
.header
, '\\' + param
, 0)
463 self
.warning('Parameter not found in the header: %s' % param
, 3)
465 self
.header
[i
] = '\\%s %s' % (param
, str(value
))
468 def is_default_layout(self
, layout
):
469 " Check whether a layout is the default layout of this class."
470 # FIXME: Check against the real text class default layout
471 if layout
== 'Standard' or layout
== self
.default_layout
:
477 "Convert from current (self.format) to self.end_format."
478 mode
, convertion_chain
= self
.chain()
479 self
.warning("convertion chain: " + str(convertion_chain
), 3)
481 for step
in convertion_chain
:
482 steps
= getattr(__import__("lyx_" + step
), mode
)
484 self
.warning("Convertion step: %s - %s" % (step
, mode
),
487 self
.error("The convertion to an older "
488 "format (%s) is not implemented." % self
.format
)
490 multi_conv
= len(steps
) != 1
491 for version
, table
in steps
:
493 (self
.format
>= version
and mode
== "convert") or\
494 (self
.format
<= version
and mode
== "revert"):
502 self
.warning("An error ocurred in %s, %s" %
503 (version
, str(conv
)),
505 if not self
.try_hard
:
509 self
.warning("%lf: Elapsed time on %s" %
510 (time
.time() - init_t
,
511 str(conv
)), default_debug__
+
513 self
.format
= version
514 if self
.end_format
== self
.format
:
519 """ This is where all the decisions related with the
520 convertion are taken. It returns a list of modules needed to
521 convert the LyX file from self.format to self.end_format"""
523 self
.start
= self
.format
527 for rel
in format_relation
:
528 if self
.initial_version
in rel
[2]:
530 initial_step
= rel
[0]
534 if not correct_version
:
536 self
.warning("Version does not match file format, "
537 "discarding it. (Version %s, format %d)" %
538 (self
.initial_version
, self
.format
))
539 for rel
in format_relation
:
541 initial_step
= rel
[0]
544 # This should not happen, really.
545 self
.error("Format not supported.")
547 # Find the final step
548 for rel
in format_relation
:
549 if self
.end_format
in rel
[1]:
553 self
.error("Format not supported.")
555 # Convertion mode, back or forth
557 if (initial_step
, self
.start
) < (final_step
, self
.end_format
):
560 for step
in format_relation
:
561 if initial_step
<= step
[0] <= final_step
:
562 if first_step
and len(step
[1]) == 1:
565 steps
.append(step
[0])
568 relation_format
= format_relation
[:]
569 relation_format
.reverse()
572 for step
in relation_format
:
573 if final_step
<= step
[0] <= initial_step
:
574 steps
.append(step
[0])
577 if last_step
[1][-1] == self
.end_format
:
580 self
.warning("Convertion mode: %s\tsteps%s" %(mode
, steps
), 10)
584 def get_toc(self
, depth
= 4):
585 " Returns the TOC of this LyX document."
586 paragraphs_filter
= {'Title' : 0,'Chapter' : 1, 'Section' : 2,
587 'Subsection' : 3, 'Subsubsection': 4}
588 allowed_insets
= ['Quotes']
589 allowed_parameters
= ('\\paragraph_spacing', '\\noindent',
590 '\\align', '\\labelwidthstring',
591 "\\start_of_appendix", "\\leftindent")
593 for section
in paragraphs_filter
.keys():
594 sections
.append('\\begin_layout %s' % section
)
599 i
= find_tokens(self
.body
, sections
, i
)
603 j
= find_end_of(self
.body
, i
+ 1, '\\begin_layout', '\\end_layout')
605 self
.warning('Incomplete file.', 0)
608 section
= self
.body
[i
].split()[1]
609 if section
[-1] == '*':
610 section
= section
[:-1]
615 # skip paragraph parameters
616 while not self
.body
[k
].strip() or self
.body
[k
].split()[0] \
617 in allowed_parameters
:
621 if check_token(self
.body
[k
], '\\begin_inset'):
622 inset
= self
.body
[k
].split()[1]
623 end
= find_end_of_inset(self
.body
, k
)
624 if end
== -1 or end
> j
:
625 self
.warning('Malformed file.', 0)
627 if inset
in allowed_insets
:
628 par
.extend(self
.body
[k
: end
+1])
631 par
.append(self
.body
[k
])
634 # trim empty lines in the end.
635 while par
and par
[-1].strip() == '':
638 toc_par
.append(Paragraph(section
, par
))
645 class File(LyX_base
):
646 " This class reads existing LyX files."
648 def __init__(self
, end_format
= 0, input = "", output
= "", error
= "",
649 debug
= default_debug__
, try_hard
= 0, cjk_encoding
= ''):
650 LyX_base
.__init
__(self
, end_format
, input, output
, error
,
651 debug
, try_hard
, cjk_encoding
)
655 class NewFile(LyX_base
):
656 " This class is to create new LyX files."
657 def set_header(self
, **params
):
660 "#LyX xxxx created this file."
661 "For more info see http://www.lyx.org/",
665 "\\textclass article",
666 "\\language english",
667 "\\inputencoding auto",
668 "\\font_roman default",
669 "\\font_sans default",
670 "\\font_typewriter default",
671 "\\font_default_family default",
674 "\\font_sf_scale 100",
675 "\\font_tt_scale 100",
676 "\\graphics default",
677 "\\paperfontsize default",
678 "\\papersize default",
679 "\\use_geometry false",
681 "\\cite_engine basic",
682 "\\use_bibtopic false",
683 "\\paperorientation portrait",
686 "\\paragraph_separation indent",
688 "\\quotes_language english",
691 "\\paperpagestyle default",
692 "\\tracking_changes false",
695 self
.format
= get_end_format()
697 self
.set_parameter(param
, params
[param
])
700 def set_body(self
, paragraphs
):
701 self
.body
.extend(['\\begin_body',''])
703 for par
in paragraphs
:
704 self
.body
.extend(par
.asLines())
706 self
.body
.extend(['','\\end_body', '\\end_document'])
710 # unfinished implementation, it is missing the Text and Insets
712 " This class represents the LyX paragraphs."
713 def __init__(self
, name
, body
=[], settings
= [], child
= []):
715 name: paragraph name.
716 body: list of lines of body text.
717 child: list of paragraphs that descend from this paragraph.
721 self
.settings
= settings
725 """ Converts the paragraph to a list of strings, representing
726 it in the LyX file."""
728 result
= ['','\\begin_layout %s' % self
.name
]
729 result
.extend(self
.settings
)
731 result
.extend(self
.body
)
732 result
.append('\\end_layout')
737 result
.append('\\begin_deeper')
738 for node
in self
.child
:
739 result
.extend(node
.asLines())
740 result
.append('\\end_deeper')