]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/lyx2lyx_tools.py
lyx2lyx refactoring
[lyx.git] / lib / lyx2lyx / lyx2lyx_tools.py
1 # This file is part of lyx2lyx
2 # -*- coding: utf-8 -*-
3 # Copyright (C) 2011 The LyX team
4 #
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.
9 #
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.
14 #
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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18
19 '''
20 This module offers several free functions to help with lyx2lyx'ing.
21 More documentaton is below, but here is a quick guide to what
22 they do. Optional arguments are marked by brackets.
23
24 add_to_preamble(document, text):
25   Here, text can be either a single line or a list of lines. It
26   is bad practice to pass something with embedded newlines, but
27   we will handle that properly.
28   The routine checks to see whether the provided material is
29   already in the preamble. If not, it adds it.
30   Prepends a comment "% Added by lyx2lyx" to text.
31
32 insert_to_preamble(document, text[, index]):
33   Here, text can be either a single line or a list of lines. It
34   is bad practice to pass something with embedded newlines, but
35   we will handle that properly.
36   The routine inserts text at document.preamble[index], where by
37   default index is 0, so the material is inserted at the beginning.
38   Prepends a comment "% Added by lyx2lyx" to text.
39
40 put_cmd_in_ert(cmd):
41   Here cmd should be a list of strings (lines), which we want to
42   wrap in ERT. Returns a list of strings so wrapped.
43   A call to this routine will often go something like this:
44     i = find_token('\\begin_inset FunkyInset', ...)
45     j = find_end_of_inset(document.body, i)
46     content = lyx2latex(document[i:j + 1])
47     ert = put_cmd_in_ert(content)
48     document.body[i:j+1] = ert
49
50 get_ert(lines, i[, verbatim]):
51   Here, lines is a list of lines of LyX material containing an ERT inset,
52   whose content we want to convert to LaTeX. The ERT starts at index i.
53   If the optional (by default: False) bool verbatim is True, the content
54   of the ERT is returned verbatim, that is in LyX syntax (not LaTeX syntax)
55   for the use in verbatim insets.
56
57 lyx2latex(document, lines):
58   Here, lines is a list of lines of LyX material we want to convert
59   to LaTeX. We do the best we can and return a string containing
60   the translated material.
61
62 lyx2verbatim(document, lines):
63   Here, lines is a list of lines of LyX material we want to convert
64   to verbatim material (used in ERT an the like). We do the best we
65   can and return a string containing the translated material.
66
67 latex_length(slen):
68   Convert lengths (in LyX form) to their LaTeX representation. Returns
69   (bool, length), where the bool tells us if it was a percentage, and
70   the length is the LaTeX representation.
71
72 convert_info_insets(document, type, func):
73   Applies func to the argument of all info insets matching certain types
74   type : the type to match. This can be a regular expression.
75   func : function from string to string to apply to the "arg" field of
76          the info insets.
77
78 is_document_option(document, option):
79   Find if _option_ is a document option (\\options in the header).
80
81 insert_document_option(document, option):
82   Insert _option_ as a document option.
83
84 remove_document_option(document, option):
85   Remove _option_ as a document option.
86
87 revert_language(document, lyxname, babelname="", polyglossianame=""):
88   Reverts native language support to ERT
89   If babelname or polyglossianame is empty, it is assumed
90   this language package is not supported for the given language.
91 '''
92
93 import re, sys
94 from parser_tools import find_token, find_end_of_inset, get_containing_layout, get_value, get_bool_value
95 from unicode_symbols import unicode_reps
96
97 # This will accept either a list of lines or a single line.
98 # It is bad practice to pass something with embedded newlines,
99 # though we will handle that.
100 def add_to_preamble(document, text):
101     " Add text to the preamble if it is not already there. "
102
103     if not type(text) is list:
104       # split on \n just in case
105       # it'll give us the one element list we want
106       # if there's no \n, too
107       text = text.split('\n')
108
109     i = 0
110     prelen = len(document.preamble)
111     while True:
112       i = find_token(document.preamble, text[0], i)
113       if i == -1:
114         break
115       # we need a perfect match
116       matched = True
117       for line in text:
118         if i >= prelen or line != document.preamble[i]:
119           matched = False
120           break
121         i += 1
122       if matched:
123         return
124
125     document.preamble.extend(["% Added by lyx2lyx"])
126     document.preamble.extend(text)
127
128
129 # Note that text can be either a list of lines or a single line.
130 # It should really be a list.
131 def insert_to_preamble(document, text, index = 0):
132     """ Insert text to the preamble at a given line"""
133
134     if not type(text) is list:
135       # split on \n just in case
136       # it'll give us the one element list we want
137       # if there's no \n, too
138       text = text.split('\n')
139
140     text.insert(0, "% Added by lyx2lyx")
141     document.preamble[index:index] = text
142
143
144 # A dictionary of Unicode->LICR mappings for use in a Unicode string's translate() method
145 # Created from the reversed list to keep the first of alternative definitions.
146 licr_table = dict((ord(ch), cmd) for cmd, ch in unicode_reps[::-1])
147
148 def put_cmd_in_ert(cmd, is_open=False, as_paragraph=False):
149     """
150     Return ERT inset wrapping `cmd` as a list of strings.
151
152     `cmd` can be a string or list of lines. Non-ASCII characters are converted
153     to the respective LICR macros if defined in unicodesymbols,
154     `is_open` is a boolean setting the inset status to "open",
155     `as_paragraph` wraps the ERT inset in a Standard paragraph.
156     """
157     
158     status = {False:"collapsed", True:"open"}
159     ert_inset = ["\\begin_inset ERT", "status %s"%status[is_open], "",
160                  "\\begin_layout Plain Layout", "",
161                  # content here ([5:5])
162                  "\\end_layout", "", "\\end_inset"]
163
164     paragraph = ["\\begin_layout Standard", 
165                  # content here ([1:1])
166                  "", "", "\\end_layout", ""]
167     # ensure cmd is an unicode instance and make it "LyX safe".
168     if isinstance(cmd, list):
169         cmd = u"\n".join(cmd)
170     elif sys.version_info[0] == 2 and isinstance(cmd, str):
171         cmd = cmd.decode('utf8')
172     cmd = cmd.translate(licr_table)
173     cmd = cmd.replace("\\", "\n\\backslash\n")
174     
175     ert_inset[5:5] = cmd.splitlines()
176     if not as_paragraph:
177         return ert_inset
178     paragraph[1:1] = ert_inset
179     return paragraph
180
181
182 def get_ert(lines, i, verbatim = False):
183     'Convert an ERT inset into LaTeX.'
184     if not lines[i].startswith("\\begin_inset ERT"):
185         return ""
186     j = find_end_of_inset(lines, i)
187     if j == -1:
188         return ""
189     while i < j and not lines[i].startswith("status"):
190         i = i + 1
191     i = i + 1
192     ret = ""
193     first = True
194     while i < j:
195         if lines[i] == "\\begin_layout Plain Layout":
196             if first:
197                 first = False
198             else:
199                 ret = ret + "\n"
200             while i + 1 < j and lines[i+1] == "":
201                 i = i + 1
202         elif lines[i] == "\\end_layout":
203             while i + 1 < j and lines[i+1] == "":
204                 i = i + 1
205         elif lines[i] == "\\backslash":
206             if verbatim:
207                 ret = ret + "\n" + lines[i] + "\n"
208             else:
209                 ret = ret + "\\"
210         else:
211             ret = ret + lines[i]
212         i = i + 1
213     return ret
214
215
216 def lyx2latex(document, lines):
217     'Convert some LyX stuff into corresponding LaTeX stuff, as best we can.'
218
219     content = ""
220     ert_end = 0
221     note_end = 0
222     hspace = ""
223
224     for curline in range(len(lines)):
225       line = lines[curline]
226       if line.startswith("\\begin_inset Note Note"):
227           # We want to skip LyX notes, so remember where the inset ends
228           note_end = find_end_of_inset(lines, curline + 1)
229           continue
230       elif note_end >= curline:
231           # Skip LyX notes
232           continue
233       elif line.startswith("\\begin_inset ERT"):
234           # We don't want to replace things inside ERT, so figure out
235           # where the end of the inset is.
236           ert_end = find_end_of_inset(lines, curline + 1)
237           continue
238       elif line.startswith("\\begin_inset Formula"):
239           line = line[20:]
240       elif line.startswith("\\begin_inset Quotes"):
241           # For now, we do a very basic reversion. Someone who understands
242           # quotes is welcome to fix it up.
243           qtype = line[20:].strip()
244           # lang = qtype[0]
245           side = qtype[1]
246           dbls = qtype[2]
247           if side == "l":
248               if dbls == "d":
249                   line = "``"
250               else:
251                   line = "`"
252           else:
253               if dbls == "d":
254                   line = "''"
255               else:
256                   line = "'"
257       elif line.startswith("\\begin_inset Newline newline"):
258           line = "\\\\ "
259       elif line.startswith("\\noindent"):
260           line = "\\noindent " # we need the space behind the command
261       elif line.startswith("\\begin_inset space"):
262           line = line[18:].strip()
263           if line.startswith("\\hspace"):
264               # Account for both \hspace and \hspace*
265               hspace = line[:-2]
266               continue
267           elif line == "\\space{}":
268               line = "\\ "
269           elif line == "\\thinspace{}":
270               line = "\\,"
271       elif hspace != "":
272           # The LyX length is in line[8:], after the \length keyword
273           length = latex_length(line[8:])[1]
274           line = hspace + "{" + length + "}"
275           hspace = ""
276       elif line.isspace() or \
277             line.startswith("\\begin_layout") or \
278             line.startswith("\\end_layout") or \
279             line.startswith("\\begin_inset") or \
280             line.startswith("\\end_inset") or \
281             line.startswith("\\lang") or \
282             line.strip() == "status collapsed" or \
283             line.strip() == "status open":
284           #skip all that stuff
285           continue
286
287       # this needs to be added to the preamble because of cases like
288       # \textmu, \textbackslash, etc.
289       add_to_preamble(document, ['% added by lyx2lyx for converted index entries',
290                                  '\\@ifundefined{textmu}',
291                                  ' {\\usepackage{textcomp}}{}'])
292       # a lossless reversion is not possible
293       # try at least to handle some common insets and settings
294       if ert_end >= curline:
295           line = line.replace(r'\backslash', '\\')
296       else:
297           # No need to add "{}" after single-nonletter macros
298           line = line.replace('&', '\\&')
299           line = line.replace('#', '\\#')
300           line = line.replace('^', '\\textasciicircum{}')
301           line = line.replace('%', '\\%')
302           line = line.replace('_', '\\_')
303           line = line.replace('$', '\\$')
304
305           # Do the LyX text --> LaTeX conversion
306           for rep in unicode_reps:
307               line = line.replace(rep[1], rep[0])
308           line = line.replace(r'\backslash', r'\textbackslash{}')
309           line = line.replace(r'\series bold', r'\bfseries{}').replace(r'\series default', r'\mdseries{}')
310           line = line.replace(r'\shape italic', r'\itshape{}').replace(r'\shape smallcaps', r'\scshape{}')
311           line = line.replace(r'\shape slanted', r'\slshape{}').replace(r'\shape default', r'\upshape{}')
312           line = line.replace(r'\emph on', r'\em{}').replace(r'\emph default', r'\em{}')
313           line = line.replace(r'\noun on', r'\scshape{}').replace(r'\noun default', r'\upshape{}')
314           line = line.replace(r'\bar under', r'\underbar{').replace(r'\bar default', r'}')
315           line = line.replace(r'\family sans', r'\sffamily{}').replace(r'\family default', r'\normalfont{}')
316           line = line.replace(r'\family typewriter', r'\ttfamily{}').replace(r'\family roman', r'\rmfamily{}')
317           line = line.replace(r'\InsetSpace ', r'').replace(r'\SpecialChar ', r'')
318       content += line
319     return content
320
321
322 def lyx2verbatim(document, lines):
323     'Convert some LyX stuff into corresponding verbatim stuff, as best we can.'
324
325     content = lyx2latex(document, lines)
326     content = re.sub(r'\\(?!backslash)', r'\n\\backslash\n', content)
327
328     return content
329
330
331 def latex_length(slen):
332     '''
333     Convert lengths to their LaTeX representation. Returns (bool, length),
334     where the bool tells us if it was a percentage, and the length is the
335     LaTeX representation.
336     '''
337     i = 0
338     percent = False
339     # the slen has the form
340     # ValueUnit+ValueUnit-ValueUnit or
341     # ValueUnit+-ValueUnit
342     # the + and - (glue lengths) are optional
343     # the + always precedes the -
344
345     # Convert relative lengths to LaTeX units
346     units = {"col%": "\\columnwidth",
347              "text%": "\\textwidth",
348              "page%": "\\paperwidth",
349              "line%": "\\linewidth",
350              "theight%": "\\textheight",
351              "pheight%": "\\paperheight",
352              "baselineskip%": "\\baselineskip"
353             }
354     for unit in list(units.keys()):
355         i = slen.find(unit)
356         if i == -1:
357             continue
358         percent = True
359         minus = slen.rfind("-", 1, i)
360         plus = slen.rfind("+", 0, i)
361         latex_unit = units[unit]
362         if plus == -1 and minus == -1:
363             value = slen[:i]
364             value = str(float(value)/100)
365             end = slen[i + len(unit):]
366             slen = value + latex_unit + end
367         if plus > minus:
368             value = slen[plus + 1:i]
369             value = str(float(value)/100)
370             begin = slen[:plus + 1]
371             end = slen[i+len(unit):]
372             slen = begin + value + latex_unit + end
373         if plus < minus:
374             value = slen[minus + 1:i]
375             value = str(float(value)/100)
376             begin = slen[:minus + 1]
377             slen = begin + value + latex_unit
378
379     # replace + and -, but only if the - is not the first character
380     slen = slen[0] + slen[1:].replace("+", " plus ").replace("-", " minus ")
381     # handle the case where "+-1mm" was used, because LaTeX only understands
382     # "plus 1mm minus 1mm"
383     if slen.find("plus  minus"):
384         lastvaluepos = slen.rfind(" ")
385         lastvalue = slen[lastvaluepos:]
386         slen = slen.replace("  ", lastvalue + " ")
387     return (percent, slen)
388
389
390 def length_in_bp(length):
391     " Convert a length in LyX format to its value in bp units "
392
393     em_width = 10.0 / 72.27 # assume 10pt font size
394     text_width = 8.27 / 1.7 # assume A4 with default margins
395     # scale factors are taken from Length::inInch()
396     scales = {"bp"       : 1.0,
397               "cc"       : (72.0 / (72.27 / (12.0 * 0.376 * 2.845))),
398               "cm"       : (72.0 / 2.54),
399               "dd"       : (72.0 / (72.27 / (0.376 * 2.845))),
400               "em"       : (72.0 * em_width),
401               "ex"       : (72.0 * em_width * 0.4305),
402               "in"       : 72.0,
403               "mm"       : (72.0 / 25.4),
404               "mu"       : (72.0 * em_width / 18.0),
405               "pc"       : (72.0 / (72.27 / 12.0)),
406               "pt"       : (72.0 / (72.27)),
407               "sp"       : (72.0 / (72.27 * 65536.0)),
408               "text%"    : (72.0 * text_width / 100.0),
409               "col%"     : (72.0 * text_width / 100.0), # assume 1 column
410               "page%"    : (72.0 * text_width * 1.7 / 100.0),
411               "line%"    : (72.0 * text_width / 100.0),
412               "theight%" : (72.0 * text_width * 1.787 / 100.0),
413               "pheight%" : (72.0 * text_width * 2.2 / 100.0)}
414
415     rx = re.compile(r'^\s*([^a-zA-Z%]+)([a-zA-Z%]+)\s*$')
416     m = rx.match(length)
417     if not m:
418         document.warning("Invalid length value: " + length + ".")
419         return 0
420     value = m.group(1)
421     unit = m.group(2)
422     if not unit in scales.keys():
423         document.warning("Unknown length unit: " + unit + ".")
424         return value
425     return "%g" % (float(value) * scales[unit])
426
427
428 def revert_flex_inset(lines, name, LaTeXname):
429   " Convert flex insets to TeX code "
430   i = 0
431   while True:
432     i = find_token(lines, '\\begin_inset Flex ' + name, i)
433     if i == -1:
434       return
435     z = find_end_of_inset(lines, i)
436     if z == -1:
437       document.warning("Can't find end of Flex " + name + " inset.")
438       i += 1
439       continue
440     # remove the \end_inset
441     lines[z - 2:z + 1] = put_cmd_in_ert("}")
442     # we need to reset character layouts if necessary
443     j = find_token(lines, '\\emph on', i, z)
444     k = find_token(lines, '\\noun on', i, z)
445     l = find_token(lines, '\\series', i, z)
446     m = find_token(lines, '\\family', i, z)
447     n = find_token(lines, '\\shape', i, z)
448     o = find_token(lines, '\\color', i, z)
449     p = find_token(lines, '\\size', i, z)
450     q = find_token(lines, '\\bar under', i, z)
451     r = find_token(lines, '\\uuline on', i, z)
452     s = find_token(lines, '\\uwave on', i, z)
453     t = find_token(lines, '\\strikeout on', i, z)
454     if j != -1:
455       lines.insert(z - 2, "\\emph default")
456     if k != -1:
457       lines.insert(z - 2, "\\noun default")
458     if l != -1:
459       lines.insert(z - 2, "\\series default")
460     if m != -1:
461       lines.insert(z - 2, "\\family default")
462     if n != -1:
463       lines.insert(z - 2, "\\shape default")
464     if o != -1:
465       lines.insert(z - 2, "\\color inherit")
466     if p != -1:
467       lines.insert(z - 2, "\\size default")
468     if q != -1:
469       lines.insert(z - 2, "\\bar default")
470     if r != -1:
471       lines.insert(z - 2, "\\uuline default")
472     if s != -1:
473       lines.insert(z - 2, "\\uwave default")
474     if t != -1:
475       lines.insert(z - 2, "\\strikeout default")
476     lines[i:i + 4] = put_cmd_in_ert(LaTeXname + "{")
477     i += 1
478
479
480 def revert_font_attrs(lines, name, LaTeXname):
481   " Reverts font changes to TeX code "
482   i = 0
483   changed = False
484   while True:
485     i = find_token(lines, name + ' on', i)
486     if i == -1:
487       break
488     j = find_token(lines, name + ' default', i)
489     k = find_token(lines, name + ' on', i + 1)
490     # if there is no default set, the style ends with the layout
491     # assure hereby that we found the correct layout end
492     if j != -1 and (j < k or k == -1):
493       lines[j:j + 1] = put_cmd_in_ert("}")
494     else:
495       j = find_token(lines, '\\end_layout', i)
496       lines[j:j] = put_cmd_in_ert("}")
497     lines[i:i + 1] = put_cmd_in_ert(LaTeXname + "{")
498     changed = True
499     i += 1
500
501   # now delete all remaining lines that manipulate this attribute
502   i = 0
503   while True:
504     i = find_token(lines, name, i)
505     if i == -1:
506       break
507     del lines[i]
508
509   return changed
510
511
512 def revert_layout_command(lines, name, LaTeXname):
513   " Reverts a command from a layout to TeX code "
514   i = 0
515   while True:
516     i = find_token(lines, '\\begin_layout ' + name, i)
517     if i == -1:
518       return
519     k = -1
520     # find the next layout
521     j = i + 1
522     while k == -1:
523       j = find_token(lines, '\\begin_layout', j)
524       l = len(lines)
525       # if nothing was found it was the last layout of the document
526       if j == -1:
527         lines[l - 4:l - 4] = put_cmd_in_ert("}")
528         k = 0
529       # exclude plain layout because this can be TeX code or another inset
530       elif lines[j] != '\\begin_layout Plain Layout':
531         lines[j - 2:j - 2] = put_cmd_in_ert("}")
532         k = 0
533       else:
534         j += 1
535     lines[i] = '\\begin_layout Standard'
536     lines[i + 1:i + 1] = put_cmd_in_ert(LaTeXname + "{")
537     i += 1
538
539
540 def hex2ratio(s):
541   " Converts an RRGGBB-type hexadecimal string to a float in [0.0,1.0] "
542   try:
543     val = int(s, 16)
544   except:
545     val = 0
546   if val != 0:
547     val += 1
548   return str(val / 256.0)
549
550
551 def str2bool(s):
552   "'true' goes to True, case-insensitively, and we strip whitespace."
553   s = s.strip().lower()
554   return s == "true"
555
556
557 def convert_info_insets(document, type, func):
558     "Convert info insets matching type using func."
559     i = 0
560     type_re = re.compile(r'^type\s+"(%s)"$' % type)
561     arg_re = re.compile(r'^arg\s+"(.*)"$')
562     while True:
563         i = find_token(document.body, "\\begin_inset Info", i)
564         if i == -1:
565             return
566         t = type_re.match(document.body[i + 1])
567         if t:
568             arg = arg_re.match(document.body[i + 2])
569             if arg:
570                 new_arg = func(arg.group(1))
571                 document.body[i + 2] = 'arg   "%s"' % new_arg
572         i += 3
573
574
575 def insert_document_option(document, option):
576     "Insert _option_ as a document option."
577
578     # Find \options in the header
579     options_line = find_token(document.header, "\\options", 0)
580
581     # if the options does not exists add it after the textclass
582     if options_line == -1:
583         textclass_line = find_token(document.header, "\\textclass", 0)
584         document.header.insert(textclass_line +1,
585                                r"\options %s" % option)
586         return
587
588     # add it to the end of the options
589     document.header[options_line] += ",%s" % option
590
591
592 def remove_document_option(document, option):
593     """ Remove _option_ as a document option.
594
595     It is assumed that option belongs to the \options.
596     That can be done running is_document_option(document, option)."""
597
598     options_line = find_token(document.header, "\\options", 0)
599     option_pos = document.header[options_line].find(option)
600
601     # Remove option from \options
602     comma_before_pos = document.header[options_line].rfind(',', 0, option_pos)
603     comma_after_pos  = document.header[options_line].find(',', option_pos)
604
605     # if there are no commas then it is the single option
606     # and the options line should be removed since it will be empty
607     if comma_before_pos == comma_after_pos == -1:
608         del document.header[options_line]
609         return
610
611     # last option
612     options = document.header[options_line]
613     if comma_after_pos == -1:
614         document.header[options_line] = options[:comma_before_pos].rsplit()
615         return
616
617     document.header[options_line] = options[comma_before_pos: comma_after_pos]
618
619
620 def is_document_option(document, option):
621     "Find if _option_ is a document option"
622
623     # Find \options in the header
624     options_line = find_token(document.header, "\\options", 0)
625
626     # \options is not present in the header
627     if options_line == -1:
628         return False
629
630     option_pos = document.header[options_line].find(option)
631     # option is not present in the \options
632     if option_pos == -1:
633         return False
634
635     return True
636
637 def revert_language(document, lyxname, babelname="", polyglossianame=""):
638     " Revert native language support "
639
640     # Does the document use polyglossia?
641     use_polyglossia = False
642     if get_bool_value(document.header, "\\use_non_tex_fonts"):
643         i = find_token(document.header, "\\language_package")
644         if i == -1:
645             document.warning("Malformed document! Missing \\language_package")
646         else:
647             pack = get_value(document.header, "\\language_package", i)
648             if pack == "default" or pack == "auto":
649                 use_polyglossia = True
650
651     # Do we use this language with polyglossia?
652     with_polyglossia = use_polyglossia and polyglossianame != ""
653     # Do we use this language with babel?
654     with_babel = with_polyglossia == False and babelname != ""
655
656     # Are we dealing with a primary or secondary language?
657     primary = False
658     secondary = False
659
660     orig_doc_language = document.language
661     # Main language first
662     if document.language == lyxname:
663         primary = True
664         document.language = "english"
665         i = find_token(document.header, "\\language %s" % lyxname, 0)
666         if i != -1:
667             document.header[i] = "\\language english"
668         if with_polyglossia:
669             add_to_preamble(document, ["\\AtBeginDocument{\setotherlanguage{%s}}" % polyglossianame])
670             document.body[2 : 2] = put_cmd_in_ert("\\resetdefaultlanguage{%s}"%polyglossianame,
671                                                   is_open=True, as_paragraph=True)
672
673     # Now secondary languages
674     i = 0
675     while True:
676         i = find_token(document.body, '\\lang', i)
677         if i == -1:
678             break
679         if document.body[i].startswith('\\lang %s' % lyxname):
680             secondary = True
681             endlang = get_containing_layout(document.body, i)[2]
682             langswitch = find_token(document.body, '\\lang', i + 1, endlang)
683             as_paragraph = True
684             if langswitch != -1:
685                 endlang = langswitch
686                 as_paragraph = False
687             if with_polyglossia:
688                 add_to_preamble(document, ["\\AtBeginDocument{\setotherlanguage{%s}}" % polyglossianame])
689                 document.body[endlang:endlang] = put_cmd_in_ert("\\end{%s}"%polyglossianame,
690                                                                 is_open=True,
691                                                                 as_paragraph=as_paragraph)
692             elif with_babel:
693                 document.body[endlang:endlang] = put_cmd_in_ert("\\end{otherlanguage}",
694                                                                 is_open=True,
695                                                                 as_paragraph=as_paragraph)
696             del document.body[i]
697             if with_polyglossia:
698                 document.body[i:i] = put_cmd_in_ert("\\begin{%s}"%polyglossianame,
699                                                     is_open=True)
700             elif with_babel:
701                 document.body[i:i] = put_cmd_in_ert("\\begin{otherlanguage}{%s}" % babelname,
702                                                     is_open=True)
703         elif primary and document.body[i].startswith('\\lang english'):
704             # Since we switched the main language manually, English parts need to be marked
705             endlang = get_containing_layout(document.body, i)[2]
706             langswitch = find_token(document.body, '\\lang', i + 1, endlang)
707             as_paragraph = True
708             if langswitch != -1:
709                 endlang = langswitch
710                 as_paragraph = False
711             if with_polyglossia:
712                 parent = get_containing_layout(document.body, i)
713                 document.body[endlang:endlang] = put_cmd_in_ert("\\end{english}",
714                                                                 is_open=True,
715                                                                 as_paragraph=as_paragraph)
716             elif with_babel:
717                 parent = get_containing_layout(document.body, i)
718                 document.body[endlang:endlang] = put_cmd_in_ert("\\end{otherlanguage}",
719                                                                 is_open=True,
720                                                                 as_paragraph=as_paragraph)
721             del document.body[i]
722             if with_polyglossia:
723                 document.body[i:i] = put_cmd_in_ert("\\begin{english}",
724                                                     is_open=True)
725             elif with_babel:
726                 document.body[i:i] = put_cmd_in_ert("\\begin{otherlanguage}{english}",
727                                                     is_open=True)
728         else:
729             i += 1
730
731     # With babel, we need to add the language options
732     if with_babel and (primary or secondary):
733         insert_document_option(document, babelname)
734         if secondary and document.body[10] != "selectlanguage{%s}" % orig_doc_language:
735             # Since the user options are always placed after the babel options,
736             # we need to reset the main language
737             document.body[2:2] = put_cmd_in_ert("\\selectlanguage{%s}" % orig_doc_language,
738                                                 is_open=True, as_paragraph=True)