]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/lyx_2_4.py
479f700dc87fac3c662c6b2d6c62c38c66ec47b4
[lyx.git] / lib / lyx2lyx / lyx_2_4.py
1 # -*- coding: utf-8 -*-
2 # This file is part of lyx2lyx
3 # Copyright (C) 2018 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
18
19 """ Convert files to the file format generated by lyx 2.4"""
20
21 import re, string
22 import unicodedata
23 import sys, os
24
25 from datetime import (datetime, date, time)
26
27 # Uncomment only what you need to import, please.
28
29 from parser_tools import (count_pars_in_inset, del_token, find_end_of_inset,
30     find_end_of_layout, find_token, find_token_backwards, find_token_exact,
31     find_re, get_bool_value,
32     get_containing_layout, get_option_value, get_value, get_quoted_value)
33 #    del_value, del_complete_lines,
34 #    find_complete_lines, find_end_of,
35 #    find_re, find_substring,
36 #    get_containing_inset,
37 #    is_in_inset, set_bool_value
38 #    find_tokens, check_token
39
40 from lyx2lyx_tools import (put_cmd_in_ert, add_to_preamble, insert_to_preamble, lyx2latex,
41                            revert_language, revert_flex_inset, str2bool)
42 #  revert_font_attrs, latex_length
43 #  get_ert, lyx2verbatim, length_in_bp, convert_info_insets
44 #  revert_flex_inset, hex2ratio
45
46 ####################################################################
47 # Private helper functions
48
49 def add_preamble_fonts(document, fontmap):
50     " Add collected font-packages with their option to user-preamble"
51
52     for pkg in fontmap:
53         if len(fontmap[pkg]) > 0:
54             xoption = "[" + ",".join(fontmap[pkg]) + "]"
55         else:
56             xoption = ""
57         preamble = "\\usepackage%s{%s}" % (xoption, pkg)
58         add_to_preamble(document, [preamble])
59
60
61 def createkey(pkg, options):
62     options.sort()
63     return pkg + ':' + "-".join(options)
64
65 class fontinfo:
66     def __init__(self):
67         self.fontname = None    # key into font2pkgmap
68         self.fonttype = None    # roman,sans,typewriter,math
69         self.scaletype = None   # None,sf,tt
70         self.scaleopt = None    # None, 'scaled', 'scale'
71         self.scaleval = 1
72         self.package = None
73         self.options = []
74         self.pkgkey = None      # key into pkg2fontmap
75         self.osfopt = None      # None, string
76         self.osfdef = "false"   # "false" or "true"
77
78     def addkey(self):
79         self.pkgkey = createkey(self.package, self.options)
80
81 class fontmapping:
82     def __init__(self):
83         self.font2pkgmap = dict()
84         self.pkg2fontmap = dict()
85         self.pkginmap = dict()  # defines, if a map for package exists
86
87     def expandFontMapping(self, font_list, font_type, scale_type, pkg, scaleopt = None, osfopt = None, osfdef = "false"):
88         " Expand fontinfo mapping"
89         #
90         # fontlist:    list of fontnames, each element
91         #              may contain a ','-separated list of needed options
92         #              like e.g. 'IBMPlexSansCondensed,condensed'
93         # font_type:   one of 'roman', 'sans', 'typewriter', 'math'
94         # scale_type:  one of None, 'sf', 'tt'
95         # pkg:         package defining the font. Defaults to fontname if None
96         # scaleopt:    one of None, 'scale', 'scaled', or some other string
97         #              to be used in scale option (e.g. scaled=0.7)
98         # osfopt:      None or some other string to be used in osf option
99         # osfdef:      "true" if osf is default
100         for fl in font_list:
101             fe = fontinfo()
102             fe.fonttype = font_type
103             fe.scaletype = scale_type
104             flt = fl.split(",")
105             font_name = flt[0]
106             fe.fontname = font_name
107             fe.options = flt[1:]
108             fe.scaleopt = scaleopt
109             fe.osfopt = osfopt
110             fe.osfdef = osfdef
111             if pkg == None:
112                 fe.package = font_name
113             else:
114                 fe.package = pkg
115             fe.addkey()
116             self.font2pkgmap[font_name] = fe
117             if fe.pkgkey in self.pkg2fontmap:
118                 # Repeated the same entry? Check content
119                 if self.pkg2fontmap[fe.pkgkey] != font_name:
120                     document.error("Something is wrong in pkgname+options <-> fontname mapping")
121             self.pkg2fontmap[fe.pkgkey] = font_name
122             self.pkginmap[fe.package] = 1
123
124     def getfontname(self, pkg, options):
125         options.sort()
126         pkgkey = createkey(pkg, options)
127         if not pkgkey in self.pkg2fontmap:
128             return None
129         fontname = self.pkg2fontmap[pkgkey]
130         if not fontname in self.font2pkgmap:
131             document.error("Something is wrong in pkgname+options <-> fontname mapping")
132             return None
133         if pkgkey == self.font2pkgmap[fontname].pkgkey:
134             return fontname
135         return None
136
137 def createFontMapping(fontlist):
138     # Create info for known fonts for the use in
139     #   convert_latexFonts() and
140     #   revert_latexFonts()
141     #
142     # * Would be more handy to parse latexFonts file,
143     #   but the path to this file is unknown
144     # * For now, add DejaVu and IBMPlex only.
145     # * Expand, if desired
146     fm = fontmapping()
147     for font in fontlist:
148         if font == 'DejaVu':
149             fm.expandFontMapping(['DejaVuSerif', 'DejaVuSerifCondensed'], "roman", None, None)
150             fm.expandFontMapping(['DejaVuSans','DejaVuSansCondensed'], "sans", "sf", None, "scaled")
151             fm.expandFontMapping(['DejaVuSansMono'], "typewriter", "tt", None, "scaled")
152         elif font == 'IBM':
153             fm.expandFontMapping(['IBMPlexSerif', 'IBMPlexSerifThin,thin',
154                                   'IBMPlexSerifExtraLight,extralight', 'IBMPlexSerifLight,light',
155                                   'IBMPlexSerifSemibold,semibold'],
156                                  "roman", None, "plex-serif")
157             fm.expandFontMapping(['IBMPlexSans','IBMPlexSansCondensed,condensed',
158                                   'IBMPlexSansThin,thin', 'IBMPlexSansExtraLight,extralight',
159                                   'IBMPlexSansLight,light', 'IBMPlexSansSemibold,semibold'],
160                                  "sans", "sf", "plex-sans", "scale")
161             fm.expandFontMapping(['IBMPlexMono', 'IBMPlexMonoThin,thin',
162                                   'IBMPlexMonoExtraLight,extralight', 'IBMPlexMonoLight,light',
163                                   'IBMPlexMonoSemibold,semibold'],
164                                  "typewriter", "tt", "plex-mono", "scale")
165         elif font == 'Adobe':
166             fm.expandFontMapping(['ADOBESourceSerifPro'], "roman", None, "sourceserifpro", None, "osf")
167             fm.expandFontMapping(['ADOBESourceSansPro'], "sans", "sf", "sourcesanspro", "scaled", "osf")
168             fm.expandFontMapping(['ADOBESourceCodePro'], "typewriter", "tt", "sourcecodepro", "scaled", "osf")
169         elif font == 'Noto':
170             fm.expandFontMapping(['NotoSerifRegular,regular', 'NotoSerifMedium,medium',
171                                   'NotoSerifThin,thin', 'NotoSerifLight,light',
172                                   'NotoSerifExtralight,extralight'],
173                                   "roman", None, "noto-serif", None, "osf")
174             fm.expandFontMapping(['NotoSansRegular,regular', 'NotoSansMedium,medium',
175                                   'NotoSansThin,thin', 'NotoSansLight,light',
176                                   'NotoSansExtralight,extralight'],
177                                   "sans", "sf", "noto-sans", "scaled")
178             fm.expandFontMapping(['NotoMonoRegular,regular'], "typewriter", "tt", "noto-mono", "scaled")
179         elif font == 'Cantarell':
180             fm.expandFontMapping(['cantarell,defaultsans'],
181                                   "sans", "sf", "cantarell", "scaled", "oldstyle")
182         elif font == 'Fira':
183             fm.expandFontMapping(['FiraSans', 'FiraSansBook,book',
184                                   'FiraSansThin,thin', 'FiraSansLight,light',
185                                   'FiraSansExtralight,extralight',
186                                   'FiraSansUltralight,ultralight'],
187                                   "sans", "sf", "FiraSans", "scaled", "lf", "true")
188             fm.expandFontMapping(['FiraMono'], "typewriter", "tt", "FiraMono", "scaled", "lf", "true")
189     return fm
190
191 def convert_fonts(document, fm, osfoption = "osf"):
192     " Handle font definition (LaTeX preamble -> native) "
193
194     rpkg = re.compile(r'^\\usepackage(\[([^\]]*)\])?\{([^\}]+)\}')
195     rscaleopt = re.compile(r'^scaled?=(.*)')
196
197     # Check whether we go beyond font option feature introduction
198     haveFontOpts = document.end_format > 580
199
200     i = 0
201     while i < len(document.preamble):
202         i = find_re(document.preamble, rpkg, i+1)
203         if i == -1:
204             return
205         mo = rpkg.search(document.preamble[i])
206         if mo == None or mo.group(2) == None:
207             options = []
208         else:
209             options = mo.group(2).replace(' ', '').split(",")
210         pkg = mo.group(3)
211         o = 0
212         oscale = 1
213         has_osf = False
214         while o < len(options):
215             if options[o] == osfoption:
216                 has_osf = True
217                 del options[o]
218                 continue
219             mo = rscaleopt.search(options[o])
220             if mo == None:
221                 o += 1
222                 continue
223             oscale = mo.group(1)
224             del options[o]
225             continue
226
227         if not pkg in fm.pkginmap:
228             continue
229         # determine fontname
230         fn = None
231         if haveFontOpts:
232             # Try with name-option combination first
233             # (only one default option supported currently)
234             o = 0
235             while o < len(options):
236                 opt = options[o]
237                 fn = fm.getfontname(pkg, [opt])
238                 if fn != None:
239                     del options[o]
240                     break
241                 o += 1
242                 continue
243             if fn == None:
244                 fn = fm.getfontname(pkg, [])
245         else:
246             fn = fm.getfontname(pkg, options)
247         if fn == None:
248             continue
249         del document.preamble[i]
250         fontinfo = fm.font2pkgmap[fn]
251         if fontinfo.scaletype == None:
252             fontscale = None
253         else:
254             fontscale = "\\font_" + fontinfo.scaletype + "_scale"
255             fontinfo.scaleval = oscale
256         if (has_osf and fontinfo.osfdef == "false") or (not has_osf and fontinfo.osfdef == "true"):
257             if fontinfo.osfopt == None:
258                 options.extend(osfoption)
259                 continue
260             osf = find_token(document.header, "\\font_osf false")
261             osftag = "\\font_osf"
262             if osf == -1 and fontinfo.fonttype != "math":
263                 # Try with newer format
264                 osftag = "\\font_" + fontinfo.fonttype + "_osf"
265                 osf = find_token(document.header, osftag + " false")
266             if osf != -1:
267                 document.header[osf] = osftag + " true"
268         if i > 0 and document.preamble[i-1] == "% Added by lyx2lyx":
269             del document.preamble[i-1]
270             i -= 1
271         if fontscale != None:
272             j = find_token(document.header, fontscale, 0)
273             if j != -1:
274                 val = get_value(document.header, fontscale, j)
275                 vals = val.split()
276                 scale = "100"
277                 if oscale != None:
278                     scale = "%03d" % int(float(oscale) * 100)
279                 document.header[j] = fontscale + " " + scale + " " + vals[1]
280         ft = "\\font_" + fontinfo.fonttype
281         j = find_token(document.header, ft, 0)
282         if j != -1:
283             val = get_value(document.header, ft, j)
284             words = val.split() # ! splits also values like '"DejaVu Sans"'
285             words[0] = '"' + fn + '"'
286             document.header[j] = ft + ' ' + ' '.join(words)
287         if haveFontOpts and fontinfo.fonttype != "math":
288             fotag = "\\font_" + fontinfo.fonttype + "_opts"
289             fo = find_token(document.header, fotag)
290             if fo != -1:
291                 document.header[fo] = fotag + " \"" + ",".join(options) + "\""
292             else:
293                 # Sensible place to insert tag
294                 fo = find_token(document.header, "\\font_sf_scale")
295                 if fo == -1:
296                     document.warning("Malformed LyX document! Missing \\font_sf_scale")
297                 else:
298                     document.header.insert(fo, fotag + " \"" + ",".join(options) + "\"")
299
300
301
302 def revert_fonts(document, fm, fontmap, OnlyWithXOpts = False, WithXOpts = False):
303     " Revert native font definition to LaTeX "
304     # fonlist := list of fonts created from the same package
305     # Empty package means that the font-name is the same as the package-name
306     # fontmap (key = package, val += found options) will be filled
307     # and used later in add_preamble_fonts() to be added to user-preamble
308
309     rfontscale = re.compile(r'^\s*(\\font_(roman|sans|typewriter|math))\s+')
310     rscales = re.compile(r'^\s*(\d+)\s+(\d+)')
311     i = 0
312     while i < len(document.header):
313         i = find_re(document.header, rfontscale, i+1)
314         if (i == -1):
315             return True
316         mo = rfontscale.search(document.header[i])
317         if mo == None:
318             continue
319         ft = mo.group(1)    # 'roman', 'sans', 'typewriter', 'math'
320         val = get_value(document.header, ft, i)
321         words = val.split(' ')     # ! splits also values like '"DejaVu Sans"'
322         font = words[0].strip('"') # TeX font name has no whitespace
323         if not font in fm.font2pkgmap:
324             continue
325         fontinfo = fm.font2pkgmap[font]
326         val = fontinfo.package
327         if not val in fontmap:
328             fontmap[val] = []
329         x = -1
330         if OnlyWithXOpts or WithXOpts:
331             if ft == "\\font_math":
332                 return False
333             regexp = re.compile(r'^\s*(\\font_roman_opts)\s+')
334             if ft == "\\font_sans":
335                 regexp = re.compile(r'^\s*(\\font_sans_opts)\s+')
336             elif ft == "\\font_typewriter":
337                 regexp = re.compile(r'^\s*(\\font_typewriter_opts)\s+')
338             x = find_re(document.header, regexp, 0)
339             if x == -1 and OnlyWithXOpts:
340                 return False
341
342             if x != -1:
343                 # We need to use this regex since split() does not handle quote protection
344                 xopts = re.findall(r'[^"\s]\S*|".+?"', document.header[x])
345                 opts = xopts[1].strip('"').split(",")
346                 fontmap[val].extend(opts)
347                 del document.header[x]
348         words[0] = '"default"'
349         document.header[i] = ft + ' ' + ' '.join(words)
350         if fontinfo.scaleopt != None:
351             xval =  get_value(document.header, "\\font_" + fontinfo.scaletype + "_scale", 0)
352             mo = rscales.search(xval)
353             if mo != None:
354                 xval1 = mo.group(1)
355                 xval2 = mo.group(2)
356                 if xval1 != "100":
357                     # set correct scale option
358                     fontmap[val].extend([fontinfo.scaleopt + "=" + format(float(xval1) / 100, '.2f')])
359         if fontinfo.osfopt != None:
360             oldval = "true"
361             if fontinfo.osfdef == "true":
362                 oldval = "false"
363             osf = find_token(document.header, "\\font_osf " + oldval)
364             if osf == -1 and ft != "\\font_math":
365                 # Try with newer format
366                 osftag = "\\font_roman_osf " + oldval
367                 if ft == "\\font_sans":
368                     osftag = "\\font_sans_osf " + oldval
369                 elif ft == "\\font_typewriter":
370                     osftag = "\\font_typewriter_osf " + oldval
371                 osf = find_token(document.header, osftag)
372             if osf != -1:
373                 fontmap[val].extend([fontinfo.osfopt])
374         if len(fontinfo.options) > 0:
375             fontmap[val].extend(fontinfo.options)
376     return True
377
378 ###############################################################################
379 ###
380 ### Conversion and reversion routines
381 ###
382 ###############################################################################
383
384 def convert_inputencoding_namechange(document):
385     " Rename inputencoding settings. "
386     i = find_token(document.header, "\\inputencoding", 0)
387     if i == -1:
388         return
389     s = document.header[i].replace("auto", "auto-legacy")
390     document.header[i] = s.replace("default", "auto-legacy-plain")
391
392 def revert_inputencoding_namechange(document):
393     " Rename inputencoding settings. "
394     i = find_token(document.header, "\\inputencoding", 0)
395     if i == -1:
396         return
397     s = document.header[i].replace("auto-legacy-plain", "default")
398     document.header[i] = s.replace("auto-legacy", "auto")
399
400 def convert_notoFonts(document):
401     " Handle Noto fonts definition to LaTeX "
402
403     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
404         fm = createFontMapping(['Noto'])
405         convert_fonts(document, fm)
406
407 def revert_notoFonts(document):
408     " Revert native Noto font definition to LaTeX "
409
410     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
411         fontmap = dict()
412         fm = createFontMapping(['Noto'])
413         if revert_fonts(document, fm, fontmap):
414             add_preamble_fonts(document, fontmap)
415
416 def convert_latexFonts(document):
417     " Handle DejaVu and IBMPlex fonts definition to LaTeX "
418
419     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
420         fm = createFontMapping(['DejaVu', 'IBM'])
421         convert_fonts(document, fm)
422
423 def revert_latexFonts(document):
424     " Revert native DejaVu font definition to LaTeX "
425
426     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
427         fontmap = dict()
428         fm = createFontMapping(['DejaVu', 'IBM'])
429         if revert_fonts(document, fm, fontmap):
430             add_preamble_fonts(document, fontmap)
431
432 def convert_AdobeFonts(document):
433     " Handle Adobe Source fonts definition to LaTeX "
434
435     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
436         fm = createFontMapping(['Adobe'])
437         convert_fonts(document, fm)
438
439 def revert_AdobeFonts(document):
440     " Revert Adobe Source font definition to LaTeX "
441
442     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
443         fontmap = dict()
444         fm = createFontMapping(['Adobe'])
445         if revert_fonts(document, fm, fontmap):
446             add_preamble_fonts(document, fontmap)
447
448 def removeFrontMatterStyles(document):
449     " Remove styles Begin/EndFrontmatter"
450
451     layouts = ['BeginFrontmatter', 'EndFrontmatter']
452     tokenend = len('\\begin_layout ')
453     i = 0
454     while True:
455         i = find_token_exact(document.body, '\\begin_layout ', i+1)
456         if i == -1:
457             return
458         layout = document.body[i][tokenend:].strip()
459         if layout not in layouts:
460             continue
461         j = find_end_of_layout(document.body, i)
462         if j == -1:
463             document.warning("Malformed LyX document: Can't find end of layout at line %d" % i)
464             continue
465         while document.body[j+1].strip() == '':
466             j += 1
467         document.body[i:j+1] = []
468
469 def addFrontMatterStyles(document):
470     " Use styles Begin/EndFrontmatter for elsarticle"
471
472     if document.textclass != "elsarticle":
473         return
474
475     def insertFrontmatter(prefix, line):
476         above = line
477         while above > 0 and document.body[above-1].strip() == '':
478             above -= 1
479         below = line
480         while document.body[below].strip() == '':
481             below += 1
482         document.body[above:below] = ['', '\\begin_layout ' + prefix + 'Frontmatter',
483                                     '\\begin_inset Note Note',
484                                     'status open', '',
485                                     '\\begin_layout Plain Layout',
486                                     'Keep this empty!',
487                                     '\\end_layout', '',
488                                     '\\end_inset', '', '',
489                                     '\\end_layout', '']
490
491     layouts = ['Title', 'Title footnote', 'Author', 'Author footnote',
492                 'Corresponding author', 'Address', 'Email', 'Abstract', 'Keywords']
493     tokenend = len('\\begin_layout ')
494     first = -1
495     i = 0
496     while True:
497         i = find_token_exact(document.body, '\\begin_layout ', i+1)
498         if i == -1:
499             break
500         layout = document.body[i][tokenend:].strip()
501         if layout not in layouts:
502             continue
503         k = find_end_of_layout(document.body, i)
504         if k == -1:
505             document.warning("Malformed LyX document: Can't find end of layout at line %d" % i)
506             continue
507         if first == -1:
508             first = i
509         i = k
510     if first == -1:
511         return
512     insertFrontmatter('End', k+1)
513     insertFrontmatter('Begin', first)
514
515
516 def convert_lst_literalparam(document):
517     " Add param literal to include inset "
518
519     i = 0
520     while True:
521         i = find_token(document.body, '\\begin_inset CommandInset include', i+1)
522         if i == -1:
523             break
524         j = find_end_of_inset(document.body, i)
525         if j == -1:
526             document.warning("Malformed LyX document: Can't find end of command inset at line %d" % i)
527             continue
528         while i < j and document.body[i].strip() != '':
529             i += 1
530         document.body.insert(i, 'literal "true"')
531
532
533 def revert_lst_literalparam(document):
534     " Remove param literal from include inset "
535
536     i = 0
537     while True:
538         i = find_token(document.body, '\\begin_inset CommandInset include', i+1)
539         if i == -1:
540             break
541         j = find_end_of_inset(document.body, i)
542         if j == -1:
543             document.warning("Malformed LyX document: Can't find end of include inset at line %d" % i)
544             continue
545         del_token(document.body, 'literal', i, j)
546
547
548 def revert_paratype(document):
549     " Revert ParaType font definitions to LaTeX "
550
551     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
552         preamble = ""
553         i1 = find_token(document.header, "\\font_roman \"PTSerif-TLF\"", 0)
554         i2 = find_token(document.header, "\\font_sans \"default\"", 0)
555         i3 = find_token(document.header, "\\font_typewriter \"default\"", 0)
556         j = find_token(document.header, "\\font_sans \"PTSans-TLF\"", 0)
557
558         sf_scale = 100.0
559         sfval = find_token(document.header, "\\font_sf_scale", 0)
560         if sfval == -1:
561             document.warning("Malformed LyX document: Missing \\font_sf_scale.")
562         else:
563             sfscale = document.header[sfval].split()
564             val = sfscale[1]
565             sfscale[1] = "100"
566             document.header[sfval] = " ".join(sfscale)
567             try:
568                 # float() can throw
569                 sf_scale = float(val)
570             except:
571                 document.warning("Invalid font_sf_scale value: " + val)
572
573         sfoption = ""
574         if sf_scale != "100.0":
575             sfoption = "scaled=" + str(sf_scale / 100.0)
576         k = find_token(document.header, "\\font_typewriter \"PTMono-TLF\"", 0)
577         ttval = get_value(document.header, "\\font_tt_scale", 0)
578         # cutoff " 100"
579         ttval = ttval[:-4]
580         ttoption = ""
581         if ttval != "100":
582             ttoption = "scaled=" + format(float(ttval) / 100, '.2f')
583         if i1 != -1 and i2 != -1 and i3!= -1:
584             add_to_preamble(document, ["\\usepackage{paratype}"])
585         else:
586             if i1!= -1:
587                 add_to_preamble(document, ["\\usepackage{PTSerif}"])
588                 document.header[i1] = document.header[i1].replace("PTSerif-TLF", "default")
589             if j!= -1:
590                 if sfoption != "":
591                     add_to_preamble(document, ["\\usepackage[" + sfoption + "]{PTSans}"])
592                 else:
593                     add_to_preamble(document, ["\\usepackage{PTSans}"])
594                 document.header[j] = document.header[j].replace("PTSans-TLF", "default")
595             if k!= -1:
596                 if ttoption != "":
597                     add_to_preamble(document, ["\\usepackage[" + ttoption + "]{PTMono}"])
598                 else:
599                     add_to_preamble(document, ["\\usepackage{PTMono}"])
600                 document.header[k] = document.header[k].replace("PTMono-TLF", "default")
601
602
603 def revert_xcharter(document):
604     " Revert XCharter font definitions to LaTeX "
605
606     i = find_token(document.header, "\\font_roman \"xcharter\"", 0)
607     if i == -1:
608         return
609
610     # replace unsupported font setting
611     document.header[i] = document.header[i].replace("xcharter", "default")
612     # no need for preamble code with system fonts
613     if get_bool_value(document.header, "\\use_non_tex_fonts"):
614         return
615
616     # transfer old style figures setting to package options
617     j = find_token(document.header, "\\font_osf true")
618     if j != -1:
619         options = "[osf]"
620         document.header[j] = "\\font_osf false"
621     else:
622         options = ""
623     if i != -1:
624         add_to_preamble(document, ["\\usepackage%s{XCharter}"%options])
625
626
627 def revert_lscape(document):
628     " Reverts the landscape environment (Landscape module) to TeX-code "
629
630     if not "landscape" in document.get_module_list():
631         return
632
633     i = 0
634     while True:
635         i = find_token(document.body, "\\begin_inset Flex Landscape", i+1)
636         if i == -1:
637             return
638         j = find_end_of_inset(document.body, i)
639         if j == -1:
640             document.warning("Malformed LyX document: Can't find end of Landscape inset")
641             continue
642
643         if document.body[i] == "\\begin_inset Flex Landscape (Floating)":
644             document.body[j - 2 : j + 1] = put_cmd_in_ert("\\end{landscape}}")
645             document.body[i : i + 4] = put_cmd_in_ert("\\afterpage{\\begin{landscape}")
646             add_to_preamble(document, ["\\usepackage{afterpage}"])
647         else:
648             document.body[j - 2 : j + 1] = put_cmd_in_ert("\\end{landscape}")
649             document.body[i : i + 4] = put_cmd_in_ert("\\begin{landscape}")
650
651         add_to_preamble(document, ["\\usepackage{pdflscape}"])
652
653
654 def convert_fontenc(document):
655     " Convert default fontenc setting "
656
657     i = find_token(document.header, "\\fontencoding global", 0)
658     if i == -1:
659         return
660
661     document.header[i] = document.header[i].replace("global", "auto")
662
663
664 def revert_fontenc(document):
665     " Revert default fontenc setting "
666
667     i = find_token(document.header, "\\fontencoding auto", 0)
668     if i == -1:
669         return
670
671     document.header[i] = document.header[i].replace("auto", "global")
672
673
674 def revert_nospellcheck(document):
675     " Remove nospellcheck font info param "
676
677     i = 0
678     while True:
679         i = find_token(document.body, '\\nospellcheck', i)
680         if i == -1:
681             return
682         del document.body[i]
683
684
685 def revert_floatpclass(document):
686     " Remove float placement params 'document' and 'class' "
687
688     del_token(document.header, "\\float_placement class")
689
690     i = 0
691     while True:
692         i = find_token(document.body, '\\begin_inset Float', i+1)
693         if i == -1:
694             break
695         j = find_end_of_inset(document.body, i)
696         k = find_token(document.body, 'placement class', i, i + 2)
697         if k == -1:
698             k = find_token(document.body, 'placement document', i, i + 2)
699             if k != -1:
700                 del document.body[k]
701             continue
702         del document.body[k]
703
704
705 def revert_floatalignment(document):
706     " Remove float alignment params "
707
708     galignment = get_value(document.header, "\\float_alignment", delete=True)
709
710     i = 0
711     while True:
712         i = find_token(document.body, '\\begin_inset Float', i+1)
713         if i == -1:
714             break
715         j = find_end_of_inset(document.body, i)
716         if j == -1:
717             document.warning("Malformed LyX document: Can't find end of inset at line " + str(i))
718             continue
719         k = find_token(document.body, 'alignment', i, i+4)
720         if k == -1:
721             i = j
722             continue
723         alignment = get_value(document.body, "alignment", k)
724         if alignment == "document":
725             alignment = galignment
726         del document.body[k]
727         l = find_token(document.body, "\\begin_layout Plain Layout", i, j)
728         if l == -1:
729             document.warning("Can't find float layout!")
730             continue
731         alcmd = []
732         if alignment == "left":
733             alcmd = put_cmd_in_ert("\\raggedright{}")
734         elif alignment == "center":
735             alcmd = put_cmd_in_ert("\\centering{}")
736         elif alignment == "right":
737             alcmd = put_cmd_in_ert("\\raggedleft{}")
738         if len(alcmd) > 0:
739             document.body[l+1:l+1] = alcmd
740         i = j
741
742 def revert_tuftecite(document):
743     " Revert \cite commands in tufte classes "
744
745     tufte = ["tufte-book", "tufte-handout"]
746     if document.textclass not in tufte:
747         return
748
749     i = 0
750     while (True):
751         i = find_token(document.body, "\\begin_inset CommandInset citation", i+1)
752         if i == -1:
753             break
754         j = find_end_of_inset(document.body, i)
755         if j == -1:
756             document.warning("Can't find end of citation inset at line %d!!" %(i))
757             continue
758         k = find_token(document.body, "LatexCommand", i, j)
759         if k == -1:
760             document.warning("Can't find LatexCommand for citation inset at line %d!" %(i))
761             i = j
762             continue
763         cmd = get_value(document.body, "LatexCommand", k)
764         if cmd != "cite":
765             i = j
766             continue
767         pre = get_quoted_value(document.body, "before", i, j)
768         post = get_quoted_value(document.body, "after", i, j)
769         key = get_quoted_value(document.body, "key", i, j)
770         if not key:
771             document.warning("Citation inset at line %d does not have a key!" %(i))
772             key = "???"
773         # Replace command with ERT
774         res = "\\cite"
775         if pre:
776             res += "[" + pre + "]"
777         if post:
778             res += "[" + post + "]"
779         elif pre:
780             res += "[]"
781         res += "{" + key + "}"
782         document.body[i:j+1] = put_cmd_in_ert([res])
783         i = j
784
785
786 def revert_stretchcolumn(document):
787     " We remove the column varwidth flags or everything else will become a mess. "
788     i = 0
789     while True:
790         i = find_token(document.body, "\\begin_inset Tabular", i+1)
791         if i == -1:
792             return
793         j = find_end_of_inset(document.body, i+1)
794         if j == -1:
795             document.warning("Malformed LyX document: Could not find end of tabular.")
796             continue
797         for k in range(i, j):
798             if re.search('^<column.*varwidth="[^"]+".*>$', document.body[k]):
799                 document.warning("Converting 'tabularx'/'xltabular' table to normal table.")
800                 document.body[k] = document.body[k].replace(' varwidth="true"', '')
801
802
803 def revert_vcolumns(document):
804     " Revert standard columns with line breaks etc. "
805     i = 0
806     needvarwidth = False
807     needarray = False
808     try:
809         while True:
810             i = find_token(document.body, "\\begin_inset Tabular", i+1)
811             if i == -1:
812                 return
813             j = find_end_of_inset(document.body, i)
814             if j == -1:
815                 document.warning("Malformed LyX document: Could not find end of tabular.")
816                 continue
817
818             # Collect necessary column information
819             m = i + 1
820             nrows = int(document.body[i+1].split('"')[3])
821             ncols = int(document.body[i+1].split('"')[5])
822             col_info = []
823             for k in range(ncols):
824                 m = find_token(document.body, "<column", m)
825                 width = get_option_value(document.body[m], 'width')
826                 varwidth = get_option_value(document.body[m], 'varwidth')
827                 alignment = get_option_value(document.body[m], 'alignment')
828                 special = get_option_value(document.body[m], 'special')
829                 col_info.append([width, varwidth, alignment, special, m])
830
831             # Now parse cells
832             m = i + 1
833             lines = []
834             for row in range(nrows):
835                 for col in range(ncols):
836                     m = find_token(document.body, "<cell", m)
837                     multicolumn = get_option_value(document.body[m], 'multicolumn')
838                     multirow = get_option_value(document.body[m], 'multirow')
839                     width = get_option_value(document.body[m], 'width')
840                     rotate = get_option_value(document.body[m], 'rotate')
841                     # Check for: linebreaks, multipars, non-standard environments
842                     begcell = m
843                     endcell = find_token(document.body, "</cell>", begcell)
844                     vcand = False
845                     if find_token(document.body, "\\begin_inset Newline", begcell, endcell) != -1:
846                         vcand = True
847                     elif count_pars_in_inset(document.body, begcell + 2) > 1:
848                         vcand = True
849                     elif get_value(document.body, "\\begin_layout", begcell) != "Plain Layout":
850                         vcand = True
851                     if vcand and rotate == "" and ((multicolumn == "" and multirow == "") or width == ""):
852                         if col_info[col][0] == "" and col_info[col][1] == "" and col_info[col][3] == "":
853                             needvarwidth = True
854                             alignment = col_info[col][2]
855                             col_line = col_info[col][4]
856                             vval = ""
857                             if alignment == "center":
858                                 vval = ">{\\centering}"
859                             elif  alignment == "left":
860                                 vval = ">{\\raggedright}"
861                             elif alignment == "right":
862                                 vval = ">{\\raggedleft}"
863                             if vval != "":
864                                 needarray = True
865                             vval += "V{\\linewidth}"
866
867                             document.body[col_line] = document.body[col_line][:-1] + " special=\"" + vval + "\">"
868                             # ERT newlines and linebreaks (since LyX < 2.4 automatically inserts parboxes
869                             # with newlines, and we do not want that)
870                             while True:
871                                 endcell = find_token(document.body, "</cell>", begcell)
872                                 linebreak = False
873                                 nl = find_token(document.body, "\\begin_inset Newline newline", begcell, endcell)
874                                 if nl == -1:
875                                     nl = find_token(document.body, "\\begin_inset Newline linebreak", begcell, endcell)
876                                     if nl == -1:
877                                          break
878                                     linebreak = True
879                                 nle = find_end_of_inset(document.body, nl)
880                                 del(document.body[nle:nle+1])
881                                 if linebreak:
882                                     document.body[nl:nl+1] = put_cmd_in_ert("\\linebreak{}")
883                                 else:
884                                     document.body[nl:nl+1] = put_cmd_in_ert("\\\\")
885                     m += 1
886
887             i = j
888
889     finally:
890         if needarray == True:
891             add_to_preamble(document, ["\\usepackage{array}"])
892         if needvarwidth == True:
893             add_to_preamble(document, ["\\usepackage{varwidth}"])
894
895
896 def revert_bibencoding(document):
897     " Revert bibliography encoding "
898
899     # Get cite engine
900     engine = "basic"
901     i = find_token(document.header, "\\cite_engine", 0)
902     if i == -1:
903         document.warning("Malformed document! Missing \\cite_engine")
904     else:
905         engine = get_value(document.header, "\\cite_engine", i)
906
907     # Check if biblatex
908     biblatex = False
909     if engine in ["biblatex", "biblatex-natbib"]:
910         biblatex = True
911
912     # Map lyx to latex encoding names
913     encodings = {
914         "utf8" : "utf8",
915         "utf8x" : "utf8x",
916         "armscii8" : "armscii8",
917         "iso8859-1" : "latin1",
918         "iso8859-2" : "latin2",
919         "iso8859-3" : "latin3",
920         "iso8859-4" : "latin4",
921         "iso8859-5" : "iso88595",
922         "iso8859-6" : "8859-6",
923         "iso8859-7" : "iso-8859-7",
924         "iso8859-8" : "8859-8",
925         "iso8859-9" : "latin5",
926         "iso8859-13" : "latin7",
927         "iso8859-15" : "latin9",
928         "iso8859-16" : "latin10",
929         "applemac" : "applemac",
930         "cp437" : "cp437",
931         "cp437de" : "cp437de",
932         "cp850" : "cp850",
933         "cp852" : "cp852",
934         "cp855" : "cp855",
935         "cp858" : "cp858",
936         "cp862" : "cp862",
937         "cp865" : "cp865",
938         "cp866" : "cp866",
939         "cp1250" : "cp1250",
940         "cp1251" : "cp1251",
941         "cp1252" : "cp1252",
942         "cp1255" : "cp1255",
943         "cp1256" : "cp1256",
944         "cp1257" : "cp1257",
945         "koi8-r" : "koi8-r",
946         "koi8-u" : "koi8-u",
947         "pt154" : "pt154",
948         "utf8-platex" : "utf8",
949         "ascii" : "ascii"
950     }
951
952     i = 0
953     bibresources = []
954     while (True):
955         i = find_token(document.body, "\\begin_inset CommandInset bibtex", i+1)
956         if i == -1:
957             break
958         j = find_end_of_inset(document.body, i)
959         if j == -1:
960             document.warning("Can't find end of bibtex inset at line %d!!" %(i))
961             continue
962         encoding = get_quoted_value(document.body, "encoding", i, j)
963         if not encoding:
964             continue
965         # remove encoding line
966         k = find_token(document.body, "encoding", i, j)
967         if k != -1:
968             del document.body[k]
969         if encoding == "default":
970             continue
971         # Re-find inset end line
972         j = find_end_of_inset(document.body, i)
973         if biblatex:
974             biblio_options = ""
975             h = find_token(document.header, "\\biblio_options", 0)
976             if h != -1:
977                 biblio_options = get_value(document.header, "\\biblio_options", h)
978                 if not "bibencoding" in biblio_options:
979                      document.header[h] += ",bibencoding=%s" % encodings[encoding]
980             else:
981                 bs = find_token(document.header, "\\biblatex_bibstyle", 0)
982                 if bs == -1:
983                     # this should not happen
984                     document.warning("Malformed LyX document! No \\biblatex_bibstyle header found!")
985                 else:
986                     document.header[bs-1 : bs-1] = ["\\biblio_options bibencoding=" + encodings[encoding]]
987         else:
988             document.body[j+1:j+1] = put_cmd_in_ert("\\egroup")
989             document.body[i:i] = put_cmd_in_ert("\\bgroup\\inputencoding{" + encodings[encoding] + "}")
990
991         i = j
992
993
994
995 def convert_vcsinfo(document):
996     " Separate vcs Info inset from buffer Info inset. "
997
998     types = {
999         "vcs-revision" : "revision",
1000         "vcs-tree-revision" : "tree-revision",
1001         "vcs-author" : "author",
1002         "vcs-time" : "time",
1003         "vcs-date" : "date"
1004     }
1005     i = 0
1006     while True:
1007         i = find_token(document.body, "\\begin_inset Info", i+1)
1008         if i == -1:
1009             return
1010         j = find_end_of_inset(document.body, i+1)
1011         if j == -1:
1012             document.warning("Malformed LyX document: Could not find end of Info inset.")
1013             continue
1014         tp = find_token(document.body, 'type', i, j)
1015         tpv = get_quoted_value(document.body, "type", tp)
1016         if tpv != "buffer":
1017             continue
1018         arg = find_token(document.body, 'arg', i, j)
1019         argv = get_quoted_value(document.body, "arg", arg)
1020         if argv not in list(types.keys()):
1021             continue
1022         document.body[tp] = "type \"vcs\""
1023         document.body[arg] = "arg \"" + types[argv] + "\""
1024
1025
1026 def revert_vcsinfo(document):
1027     " Merge vcs Info inset to buffer Info inset. "
1028
1029     args = ["revision", "tree-revision", "author", "time", "date" ]
1030     i = 0
1031     while True:
1032         i = find_token(document.body, "\\begin_inset Info", i+1)
1033         if i == -1:
1034             return
1035         j = find_end_of_inset(document.body, i+1)
1036         if j == -1:
1037             document.warning("Malformed LyX document: Could not find end of Info inset.")
1038             continue
1039         tp = find_token(document.body, 'type', i, j)
1040         tpv = get_quoted_value(document.body, "type", tp)
1041         if tpv != "vcs":
1042             continue
1043         arg = find_token(document.body, 'arg', i, j)
1044         argv = get_quoted_value(document.body, "arg", arg)
1045         if argv not in args:
1046             document.warning("Malformed Info inset. Invalid vcs arg.")
1047             continue
1048         document.body[tp] = "type \"buffer\""
1049         document.body[arg] = "arg \"vcs-" + argv + "\""
1050
1051
1052 def revert_dateinfo(document):
1053     " Revert date info insets to static text. "
1054
1055 # FIXME This currently only considers the main language and uses the system locale
1056 # Ideally, it should honor context languages and switch the locale accordingly.
1057
1058     # The date formats for each language using strftime syntax:
1059     # long, short, loclong, locmedium, locshort
1060     dateformats = {
1061         "afrikaans" : ["%A, %d %B %Y", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y/%m/%d"],
1062         "albanian" : ["%A, %d %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1063         "american" : ["%A, %B %d, %Y", "%m/%d/%y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1064         "amharic" : ["%A ፣%d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1065         "ancientgreek" : ["%A, %d %B %Y", "%d %b %Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1066         "arabic_arabi" : ["%A، %d %B، %Y", "%d‏/%m‏/%Y", "%d %B، %Y", "%d/%m/%Y", "%d/%m/%Y"],
1067         "arabic_arabtex" : ["%A، %d %B، %Y", "%d‏/%m‏/%Y", "%d %B، %Y", "%d/%m/%Y", "%d/%m/%Y"],
1068         "armenian" : ["%Y թ. %B %d, %A", "%d.%m.%y", "%d %B، %Y", "%d %b، %Y", "%d/%m/%Y"],
1069         "asturian" : ["%A, %d %B de %Y", "%d/%m/%y", "%d de %B de %Y", "%d %b %Y", "%d/%m/%Y"],
1070         "australian" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1071         "austrian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1072         "bahasa" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1073         "bahasam" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1074         "basque" : ["%Y(e)ko %B %d, %A", "%y/%m/%d", "%Y %B %d", "%Y %b %d", "%Y/%m/%d"],
1075         "belarusian" : ["%A, %d %B %Y г.", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1076         "bosnian" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%Y-%m-%d"],
1077         "brazilian" : ["%A, %d de %B de %Y", "%d/%m/%Y", "%d de %B de %Y", "%d de %b de %Y", "%d/%m/%Y"],
1078         "breton" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1079         "british" : ["%A, %d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1080         "bulgarian" : ["%A, %d %B %Y г.", "%d.%m.%y г.", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1081         "canadian" : ["%A, %B %d, %Y", "%Y-%m-%d", "%B %d, %Y", "%d %b %Y", "%Y-%m-%d"],
1082         "canadien" : ["%A %d %B %Y", "%y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1083         "catalan" : ["%A, %d %B de %Y", "%d/%m/%y", "%d / %B / %Y", "%d / %b / %Y", "%d/%m/%Y"],
1084         "chinese-simplified" : ["%Y年%m月%d日%A", "%Y/%m/%d", "%Y年%m月%d日", "%Y-%m-%d", "%y-%m-%d"],
1085         "chinese-traditional" : ["%Y年%m月%d日 %A", "%Y/%m/%d", "%Y年%m月%d日", "%Y年%m月%d日", "%y年%m月%d日"],
1086         "coptic" : ["%A, %d %B %Y", "%d %b %Y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1087         "croatian" : ["%A, %d. %B %Y.", "%d. %m. %Y.", "%d. %B %Y.", "%d. %b. %Y.", "%d.%m.%Y."],
1088         "czech" : ["%A %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b. %Y", "%d.%m.%Y"],
1089         "danish" : ["%A den %d. %B %Y", "%d/%m/%Y", "%d. %B %Y", "%d. %b %Y", "%d/%m/%Y"],
1090         "divehi" : ["%Y %B %d, %A", "%Y-%m-%d", "%Y %B %d", "%Y %b %d", "%d/%m/%Y"],
1091         "dutch" : ["%A %d %B %Y", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1092         "english" : ["%A, %B %d, %Y", "%m/%d/%y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1093         "esperanto" : ["%A, %d %B %Y", "%d %b %Y", "la %d de %B %Y", "la %d de %b %Y", "%m/%d/%Y"],
1094         "estonian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1095         "farsi" : ["%A %d %B %Y", "%Y/%m/%d", "%d %B %Y", "%d %b %Y", "%Y/%m/%d"],
1096         "finnish" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1097         "french" : ["%A %d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1098         "friulan" : ["%A %d di %B dal %Y", "%d/%m/%y", "%d di %B dal %Y", "%d di %b dal %Y", "%d/%m/%Y"],
1099         "galician" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B de %Y", "%d de %b de %Y", "%d/%m/%Y"],
1100         "georgian" : ["%A, %d %B, %Y", "%d.%m.%y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1101         "german" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1102         "german-ch" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1103         "german-ch-old" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1104         "greek" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1105         "hebrew" : ["%A, %d ב%B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1106         "hindi" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1107         "icelandic" : ["%A, %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1108         "interlingua" : ["%Y %B %d, %A", "%Y-%m-%d", "le %d de %B %Y", "le %d de %b %Y", "%Y-%m-%d"],
1109         "irish" : ["%A %d %B %Y", "%d/%m/%Y", "%d. %B %Y", "%d. %b %Y", "%d/%m/%Y"],
1110         "italian" : ["%A %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d/%b/%Y", "%d/%m/%Y"],
1111         "japanese" : ["%Y年%m月%d日%A", "%Y/%m/%d", "%Y年%m月%d日", "%Y/%m/%d", "%y/%m/%d"],
1112         "japanese-cjk" : ["%Y年%m月%d日%A", "%Y/%m/%d", "%Y年%m月%d日", "%Y/%m/%d", "%y/%m/%d"],
1113         "kannada" : ["%A, %B %d, %Y", "%d/%m/%y", "%d %B %Y", "%d %B %Y", "%d-%m-%Y"],
1114         "kazakh" : ["%Y ж. %d %B, %A", "%d.%m.%y", "%d %B %Y", "%d %B %Y", "%Y-%d-%m"],
1115         "khmer" : ["%A %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %B %Y", "%d/%m/%Y"],
1116         "korean" : ["%Y년 %m월 %d일 %A", "%y. %m. %d.", "%Y년 %m월 %d일", "%Y. %m. %d.", "%y. %m. %d."],
1117         "kurmanji" : ["%A, %d %B %Y", "%d %b %Y", "%d. %B %Y", "%d. %m. %Y", "%Y-%m-%d"],
1118         "lao" : ["%A ທີ %d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %B %Y", "%d/%m/%Y"],
1119         "latin" : ["%A, %d %B %Y", "%d %b %Y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1120         "latvian" : ["%A, %Y. gada %d. %B", "%d.%m.%y", "%Y. gada %d. %B", "%Y. gada %d. %b", "%d.%m.%Y"],
1121         "lithuanian" : ["%Y m. %B %d d., %A", "%Y-%m-%d", "%Y m. %B %d d.", "%Y m. %B %d d.", "%Y-%m-%d"],
1122         "lowersorbian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1123         "macedonian" : ["%A, %d %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1124         "magyar" : ["%Y. %B %d., %A", "%Y. %m. %d.", "%Y. %B %d.", "%Y. %b %d.", "%Y.%m.%d."],
1125         "malayalam" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1126         "marathi" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1127         "mongolian" : ["%A, %Y оны %m сарын %d", "%Y-%m-%d", "%Y оны %m сарын %d", "%d-%m-%Y", "%d-%m-%Y"],
1128         "naustrian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1129         "newzealand" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1130         "ngerman" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1131         "norsk" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1132         "nynorsk" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1133         "occitan" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1134         "piedmontese" : ["%A, %d %B %Y", "%d %b %Y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1135         "polish" : ["%A, %d %B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1136         "polutonikogreek" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1137         "portuguese" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B de %Y", "%d de %b de %Y", "%Y/%m/%d"],
1138         "romanian" : ["%A, %d %B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1139         "romansh" : ["%A, ils %d da %B %Y", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1140         "russian" : ["%A, %d %B %Y г.", "%d.%m.%Y", "%d %B %Y г.", "%d %b %Y г.", "%d.%m.%Y"],
1141         "samin" : ["%Y %B %d, %A", "%Y-%m-%d", "%B %d. b. %Y", "%b %d. b. %Y", "%d.%m.%Y"],
1142         "sanskrit" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1143         "scottish" : ["%A, %dmh %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1144         "serbian" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1145         "serbian-latin" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1146         "slovak" : ["%A, %d. %B %Y", "%d. %m. %Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1147         "slovene" : ["%A, %d. %B %Y", "%d. %m. %y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1148         "spanish" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B %de %Y", "%d %b %Y", "%d/%m/%Y"],
1149         "spanish-mexico" : ["%A, %d de %B %de %Y", "%d/%m/%y", "%d de %B de %Y", "%d %b %Y", "%d/%m/%Y"],
1150         "swedish" : ["%A %d %B %Y", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1151         "syriac" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1152         "tamil" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1153         "telugu" : ["%d, %B %Y, %A", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1154         "thai" : ["%Aที่ %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1155         "tibetan" : ["%Y %Bའི་ཚེས་%d, %A", "%Y-%m-%d", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1156         "turkish" : ["%d %B %Y %A", "%d.%m.%Y", "%d %B %Y", "%d.%b.%Y", "%d.%m.%Y"],
1157         "turkmen" : ["%d %B %Y %A", "%d.%m.%Y", "%Y ý. %B %d", "%d.%m.%Y ý.", "%d.%m.%y ý."],
1158         "ukrainian" : ["%A, %d %B %Y р.", "%d.%m.%y", "%d %B %Y", "%d %m %Y", "%d.%m.%Y"],
1159         "uppersorbian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1160         "urdu" : ["%A، %d %B، %Y", "%d/%m/%y", "%d %B, %Y", "%d %b %Y", "%d/%m/%Y"],
1161         "vietnamese" : ["%A, %d %B, %Y", "%d/%m/%Y", "%d tháng %B %Y", "%d-%m-%Y", "%d/%m/%Y"],
1162         "welsh" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1163     }
1164
1165     types = ["date", "fixdate", "moddate" ]
1166     lang = get_value(document.header, "\\language")
1167     if lang == "":
1168         document.warning("Malformed LyX document! No \\language header found!")
1169         return
1170
1171     i = 0
1172     while True:
1173         i = find_token(document.body, "\\begin_inset Info", i+1)
1174         if i == -1:
1175             return
1176         j = find_end_of_inset(document.body, i+1)
1177         if j == -1:
1178             document.warning("Malformed LyX document: Could not find end of Info inset.")
1179             continue
1180         tp = find_token(document.body, 'type', i, j)
1181         tpv = get_quoted_value(document.body, "type", tp)
1182         if tpv not in types:
1183             continue
1184         arg = find_token(document.body, 'arg', i, j)
1185         argv = get_quoted_value(document.body, "arg", arg)
1186         isodate = ""
1187         dte = date.today()
1188         if tpv == "fixdate":
1189             datecomps = argv.split('@')
1190             if len(datecomps) > 1:
1191                 argv = datecomps[0]
1192                 isodate = datecomps[1]
1193                 m = re.search('(\d\d\d\d)-(\d\d)-(\d\d)', isodate)
1194                 if m:
1195                     dte = date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
1196 # FIXME if we had the path to the original document (not the one in the tmp dir),
1197 #        we could use the mtime.
1198 #        elif tpv == "moddate":
1199 #            dte = date.fromtimestamp(os.path.getmtime(document.dir))
1200         result = ""
1201         if argv == "ISO":
1202             result = dte.isodate()
1203         elif argv == "long":
1204             result = dte.strftime(dateformats[lang][0])
1205         elif argv == "short":
1206             result = dte.strftime(dateformats[lang][1])
1207         elif argv == "loclong":
1208             result = dte.strftime(dateformats[lang][2])
1209         elif argv == "locmedium":
1210             result = dte.strftime(dateformats[lang][3])
1211         elif argv == "locshort":
1212             result = dte.strftime(dateformats[lang][4])
1213         else:
1214             fmt = argv.replace("MMMM", "%b").replace("MMM", "%b").replace("MM", "%m").replace("M", "%m")
1215             fmt = fmt.replace("yyyy", "%Y").replace("yy", "%y")
1216             fmt = fmt.replace("dddd", "%A").replace("ddd", "%a").replace("dd", "%d")
1217             fmt = re.sub('[^\'%]d', '%d', fmt)
1218             fmt = fmt.replace("'", "")
1219             result = dte.strftime(fmt)
1220         if sys.version_info < (3,0):
1221             # In Python 2, datetime module works with binary strings,
1222             # our dateformat strings are utf8-encoded:
1223             result = result.decode('utf-8')
1224         document.body[i : j+1] = [result]
1225
1226
1227 def revert_timeinfo(document):
1228     " Revert time info insets to static text. "
1229
1230 # FIXME This currently only considers the main language and uses the system locale
1231 # Ideally, it should honor context languages and switch the locale accordingly.
1232 # Also, the time object is "naive", i.e., it does not know of timezones (%Z will
1233 # be empty).
1234
1235     # The time formats for each language using strftime syntax:
1236     # long, short
1237     timeformats = {
1238         "afrikaans" : ["%H:%M:%S %Z", "%H:%M"],
1239         "albanian" : ["%I:%M:%S %p, %Z", "%I:%M %p"],
1240         "american" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1241         "amharic" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1242         "ancientgreek" : ["%H:%M:%S %Z", "%H:%M:%S"],
1243         "arabic_arabi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1244         "arabic_arabtex" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1245         "armenian" : ["%H:%M:%S %Z", "%H:%M"],
1246         "asturian" : ["%H:%M:%S %Z", "%H:%M"],
1247         "australian" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1248         "austrian" : ["%H:%M:%S %Z", "%H:%M"],
1249         "bahasa" : ["%H.%M.%S %Z", "%H.%M"],
1250         "bahasam" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1251         "basque" : ["%H:%M:%S (%Z)", "%H:%M"],
1252         "belarusian" : ["%H:%M:%S, %Z", "%H:%M"],
1253         "bosnian" : ["%H:%M:%S %Z", "%H:%M"],
1254         "brazilian" : ["%H:%M:%S %Z", "%H:%M"],
1255         "breton" : ["%H:%M:%S %Z", "%H:%M"],
1256         "british" : ["%H:%M:%S %Z", "%H:%M"],
1257         "bulgarian" : ["%H:%M:%S %Z", "%H:%M"],
1258         "canadian" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1259         "canadien" : ["%H:%M:%S %Z", "%H h %M"],
1260         "catalan" : ["%H:%M:%S %Z", "%H:%M"],
1261         "chinese-simplified" : ["%Z %p%I:%M:%S", "%p%I:%M"],
1262         "chinese-traditional" : ["%p%I:%M:%S [%Z]", "%p%I:%M"],
1263         "coptic" : ["%H:%M:%S %Z", "%H:%M:%S"],
1264         "croatian" : ["%H:%M:%S (%Z)", "%H:%M"],
1265         "czech" : ["%H:%M:%S %Z", "%H:%M"],
1266         "danish" : ["%H.%M.%S %Z", "%H.%M"],
1267         "divehi" : ["%H:%M:%S %Z", "%H:%M"],
1268         "dutch" : ["%H:%M:%S %Z", "%H:%M"],
1269         "english" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1270         "esperanto" : ["%H:%M:%S %Z", "%H:%M:%S"],
1271         "estonian" : ["%H:%M:%S %Z", "%H:%M"],
1272         "farsi" : ["%H:%M:%S (%Z)", "%H:%M"],
1273         "finnish" : ["%H.%M.%S %Z", "%H.%M"],
1274         "french" : ["%H:%M:%S %Z", "%H:%M"],
1275         "friulan" : ["%H:%M:%S %Z", "%H:%M"],
1276         "galician" : ["%H:%M:%S %Z", "%H:%M"],
1277         "georgian" : ["%H:%M:%S %Z", "%H:%M"],
1278         "german" : ["%H:%M:%S %Z", "%H:%M"],
1279         "german-ch" : ["%H:%M:%S %Z", "%H:%M"],
1280         "german-ch-old" : ["%H:%M:%S %Z", "%H:%M"],
1281         "greek" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1282         "hebrew" : ["%H:%M:%S %Z", "%H:%M"],
1283         "hindi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1284         "icelandic" : ["%H:%M:%S %Z", "%H:%M"],
1285         "interlingua" : ["%H:%M:%S %Z", "%H:%M"],
1286         "irish" : ["%H:%M:%S %Z", "%H:%M"],
1287         "italian" : ["%H:%M:%S %Z", "%H:%M"],
1288         "japanese" : ["%H時%M分%S秒 %Z", "%H:%M"],
1289         "japanese-cjk" : ["%H時%M分%S秒 %Z", "%H:%M"],
1290         "kannada" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1291         "kazakh" : ["%H:%M:%S %Z", "%H:%M"],
1292         "khmer" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1293         "korean" : ["%p %I시%M분 %S초 %Z", "%p %I:%M"],
1294         "kurmanji" : ["%H:%M:%S %Z", "%H:%M:%S"],
1295         "lao" : ["%H ໂມງ%M ນາທີ  %S ວິນາທີ %Z", "%H:%M"],
1296         "latin" : ["%H:%M:%S %Z", "%H:%M:%S"],
1297         "latvian" : ["%H:%M:%S %Z", "%H:%M"],
1298         "lithuanian" : ["%H:%M:%S %Z", "%H:%M"],
1299         "lowersorbian" : ["%H:%M:%S %Z", "%H:%M"],
1300         "macedonian" : ["%H:%M:%S %Z", "%H:%M"],
1301         "magyar" : ["%H:%M:%S %Z", "%H:%M"],
1302         "malayalam" : ["%p %I:%M:%S %Z", "%p %I:%M"],
1303         "marathi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1304         "mongolian" : ["%H:%M:%S %Z", "%H:%M"],
1305         "naustrian" : ["%H:%M:%S %Z", "%H:%M"],
1306         "newzealand" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1307         "ngerman" : ["%H:%M:%S %Z", "%H:%M"],
1308         "norsk" : ["%H:%M:%S %Z", "%H:%M"],
1309         "nynorsk" : ["kl. %H:%M:%S %Z", "%H:%M"],
1310         "occitan" : ["%H:%M:%S %Z", "%H:%M"],
1311         "piedmontese" : ["%H:%M:%S %Z", "%H:%M:%S"],
1312         "polish" : ["%H:%M:%S %Z", "%H:%M"],
1313         "polutonikogreek" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1314         "portuguese" : ["%H:%M:%S %Z", "%H:%M"],
1315         "romanian" : ["%H:%M:%S %Z", "%H:%M"],
1316         "romansh" : ["%H:%M:%S %Z", "%H:%M"],
1317         "russian" : ["%H:%M:%S %Z", "%H:%M"],
1318         "samin" : ["%H:%M:%S %Z", "%H:%M"],
1319         "sanskrit" : ["%H:%M:%S %Z", "%H:%M"],
1320         "scottish" : ["%H:%M:%S %Z", "%H:%M"],
1321         "serbian" : ["%H:%M:%S %Z", "%H:%M"],
1322         "serbian-latin" : ["%H:%M:%S %Z", "%H:%M"],
1323         "slovak" : ["%H:%M:%S %Z", "%H:%M"],
1324         "slovene" : ["%H:%M:%S %Z", "%H:%M"],
1325         "spanish" : ["%H:%M:%S (%Z)", "%H:%M"],
1326         "spanish-mexico" : ["%H:%M:%S %Z", "%H:%M"],
1327         "swedish" : ["kl. %H:%M:%S %Z", "%H:%M"],
1328         "syriac" : ["%H:%M:%S %Z", "%H:%M"],
1329         "tamil" : ["%p %I:%M:%S %Z", "%p %I:%M"],
1330         "telugu" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1331         "thai" : ["%H นาฬิกา %M นาที  %S วินาที %Z", "%H:%M"],
1332         "tibetan" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1333         "turkish" : ["%H:%M:%S %Z", "%H:%M"],
1334         "turkmen" : ["%H:%M:%S %Z", "%H:%M"],
1335         "ukrainian" : ["%H:%M:%S %Z", "%H:%M"],
1336         "uppersorbian" : ["%H:%M:%S %Z", "%H:%M hodź."],
1337         "urdu" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1338         "vietnamese" : ["%H:%M:%S %Z", "%H:%M"],
1339         "welsh" : ["%H:%M:%S %Z", "%H:%M"]
1340     }
1341
1342     types = ["time", "fixtime", "modtime" ]
1343     i = 0
1344     i = find_token(document.header, "\\language", 0)
1345     if i == -1:
1346         # this should not happen
1347         document.warning("Malformed LyX document! No \\language header found!")
1348         return
1349     lang = get_value(document.header, "\\language", i)
1350
1351     i = 0
1352     while True:
1353         i = find_token(document.body, "\\begin_inset Info", i+1)
1354         if i == -1:
1355             return
1356         j = find_end_of_inset(document.body, i+1)
1357         if j == -1:
1358             document.warning("Malformed LyX document: Could not find end of Info inset.")
1359             continue
1360         tp = find_token(document.body, 'type', i, j)
1361         tpv = get_quoted_value(document.body, "type", tp)
1362         if tpv not in types:
1363             continue
1364         arg = find_token(document.body, 'arg', i, j)
1365         argv = get_quoted_value(document.body, "arg", arg)
1366         isotime = ""
1367         dtme = datetime.now()
1368         tme = dtme.time()
1369         if tpv == "fixtime":
1370             timecomps = argv.split('@')
1371             if len(timecomps) > 1:
1372                 argv = timecomps[0]
1373                 isotime = timecomps[1]
1374                 m = re.search('(\d\d):(\d\d):(\d\d)', isotime)
1375                 if m:
1376                     tme = time(int(m.group(1)), int(m.group(2)), int(m.group(3)))
1377                 else:
1378                     m = re.search('(\d\d):(\d\d)', isotime)
1379                     if m:
1380                         tme = time(int(m.group(1)), int(m.group(2)))
1381 # FIXME if we had the path to the original document (not the one in the tmp dir),
1382 #        we could use the mtime.
1383 #        elif tpv == "moddate":
1384 #            dte = date.fromtimestamp(os.path.getmtime(document.dir))
1385         result = ""
1386         if argv == "ISO":
1387             result = tme.isoformat()
1388         elif argv == "long":
1389             result = tme.strftime(timeformats[lang][0])
1390         elif argv == "short":
1391             result = tme.strftime(timeformats[lang][1])
1392         else:
1393             fmt = argv.replace("HH", "%H").replace("H", "%H").replace("hh", "%I").replace("h", "%I")
1394             fmt = fmt.replace("mm", "%M").replace("m", "%M").replace("ss", "%S").replace("s", "%S")
1395             fmt = fmt.replace("zzz", "%f").replace("z", "%f").replace("t", "%Z")
1396             fmt = fmt.replace("AP", "%p").replace("ap", "%p").replace("A", "%p").replace("a", "%p")
1397             fmt = fmt.replace("'", "")
1398             result = dte.strftime(fmt)
1399         document.body[i : j+1] = result
1400
1401
1402 def revert_namenoextinfo(document):
1403     " Merge buffer Info inset type name-noext to name. "
1404
1405     i = 0
1406     while True:
1407         i = find_token(document.body, "\\begin_inset Info", i+1)
1408         if i == -1:
1409             return
1410         j = find_end_of_inset(document.body, i+1)
1411         if j == -1:
1412             document.warning("Malformed LyX document: Could not find end of Info inset.")
1413             continue
1414         tp = find_token(document.body, 'type', i, j)
1415         tpv = get_quoted_value(document.body, "type", tp)
1416         if tpv != "buffer":
1417             continue
1418         arg = find_token(document.body, 'arg', i, j)
1419         argv = get_quoted_value(document.body, "arg", arg)
1420         if argv != "name-noext":
1421             continue
1422         document.body[arg] = "arg \"name\""
1423
1424
1425 def revert_l7ninfo(document):
1426     " Revert l7n Info inset to text. "
1427
1428     i = 0
1429     while True:
1430         i = find_token(document.body, "\\begin_inset Info", i+1)
1431         if i == -1:
1432             return
1433         j = find_end_of_inset(document.body, i+1)
1434         if j == -1:
1435             document.warning("Malformed LyX document: Could not find end of Info inset.")
1436             continue
1437         tp = find_token(document.body, 'type', i, j)
1438         tpv = get_quoted_value(document.body, "type", tp)
1439         if tpv != "l7n":
1440             continue
1441         arg = find_token(document.body, 'arg', i, j)
1442         argv = get_quoted_value(document.body, "arg", arg)
1443         # remove trailing colons, menu accelerator (|...) and qt accelerator (&), while keeping literal " & "
1444         argv = argv.rstrip(':').split('|')[0].replace(" & ", "</amp;>").replace("&", "").replace("</amp;>", " & ")
1445         document.body[i : j+1] = argv
1446
1447
1448 def revert_listpargs(document):
1449     " Reverts listpreamble arguments to TeX-code "
1450     i = 0
1451     while True:
1452         i = find_token(document.body, "\\begin_inset Argument listpreamble:", i+1)
1453         if i == -1:
1454             return
1455         j = find_end_of_inset(document.body, i)
1456         # Find containing paragraph layout
1457         parent = get_containing_layout(document.body, i)
1458         if parent == False:
1459             document.warning("Malformed LyX document: Can't find parent paragraph layout")
1460             continue
1461         parbeg = parent[3]
1462         beginPlain = find_token(document.body, "\\begin_layout Plain Layout", i)
1463         endPlain = find_end_of_layout(document.body, beginPlain)
1464         content = document.body[beginPlain + 1 : endPlain]
1465         del document.body[i:j+1]
1466         subst = ["\\begin_inset ERT", "status collapsed", "", "\\begin_layout Plain Layout",
1467                  "{"] + content + ["}", "\\end_layout", "", "\\end_inset", ""]
1468         document.body[parbeg : parbeg] = subst
1469
1470
1471 def revert_lformatinfo(document):
1472     " Revert layout format Info inset to text. "
1473
1474     i = 0
1475     while True:
1476         i = find_token(document.body, "\\begin_inset Info", i+1)
1477         if i == -1:
1478             return
1479         j = find_end_of_inset(document.body, i+1)
1480         if j == -1:
1481             document.warning("Malformed LyX document: Could not find end of Info inset.")
1482             continue
1483         tp = find_token(document.body, 'type', i, j)
1484         tpv = get_quoted_value(document.body, "type", tp)
1485         if tpv != "lyxinfo":
1486             continue
1487         arg = find_token(document.body, 'arg', i, j)
1488         argv = get_quoted_value(document.body, "arg", arg)
1489         if argv != "layoutformat":
1490             continue
1491         # hardcoded for now
1492         document.body[i : j+1] = "69"
1493
1494
1495 def convert_hebrew_parentheses(document):
1496     """ Swap opening/closing parentheses in Hebrew text.
1497
1498     Up to LyX 2.4, "(" was used as closing parenthesis and
1499     ")" as opening parenthesis for Hebrew in the LyX source.
1500     """
1501     # print("convert hebrew parentheses")
1502     current_languages = [document.language]
1503     for i, line in enumerate(document.body):
1504         if line.startswith('\\lang '):
1505             current_languages[-1] = line.lstrip('\\lang ')
1506         elif line.startswith('\\begin_layout'):
1507             current_languages.append(current_languages[-1])
1508             # print (line, current_languages[-1])
1509         elif line.startswith('\\end_layout'):
1510             current_languages.pop()
1511         elif current_languages[-1] == 'hebrew' and not line.startswith('\\'):
1512             document.body[i] = line.replace('(','\x00').replace(')','(').replace('\x00',')')
1513
1514
1515 def revert_hebrew_parentheses(document):
1516     " Store parentheses in Hebrew text reversed"
1517     # This only exists to keep the convert/revert naming convention
1518     convert_hebrew_parentheses(document)
1519
1520
1521 def revert_malayalam(document):
1522     " Set the document language to English but assure Malayalam output "
1523
1524     revert_language(document, "malayalam", "", "malayalam")
1525
1526
1527 def revert_soul(document):
1528     " Revert soul module flex insets to ERT "
1529
1530     flexes = ["Spaceletters", "Strikethrough", "Underline", "Highlight", "Capitalize"]
1531
1532     for flex in flexes:
1533         i = find_token(document.body, "\\begin_inset Flex %s" % flex, 0)
1534         if i != -1:
1535             add_to_preamble(document, ["\\usepackage{soul}"])
1536             break
1537     i = find_token(document.body, "\\begin_inset Flex Highlight", 0)
1538     if i != -1:
1539         add_to_preamble(document, ["\\usepackage{color}"])
1540
1541     revert_flex_inset(document.body, "Spaceletters", "\\so")
1542     revert_flex_inset(document.body, "Strikethrough", "\\st")
1543     revert_flex_inset(document.body, "Underline", "\\ul")
1544     revert_flex_inset(document.body, "Highlight", "\\hl")
1545     revert_flex_inset(document.body, "Capitalize", "\\caps")
1546
1547
1548 def revert_tablestyle(document):
1549     " Remove tablestyle params "
1550
1551     i = 0
1552     i = find_token(document.header, "\\tablestyle")
1553     if i != -1:
1554         del document.header[i]
1555
1556
1557 def revert_bibfileencodings(document):
1558     " Revert individual Biblatex bibliography encodings "
1559
1560     # Get cite engine
1561     engine = "basic"
1562     i = find_token(document.header, "\\cite_engine", 0)
1563     if i == -1:
1564         document.warning("Malformed document! Missing \\cite_engine")
1565     else:
1566         engine = get_value(document.header, "\\cite_engine", i)
1567
1568     # Check if biblatex
1569     biblatex = False
1570     if engine in ["biblatex", "biblatex-natbib"]:
1571         biblatex = True
1572
1573     # Map lyx to latex encoding names
1574     encodings = {
1575         "utf8" : "utf8",
1576         "utf8x" : "utf8x",
1577         "armscii8" : "armscii8",
1578         "iso8859-1" : "latin1",
1579         "iso8859-2" : "latin2",
1580         "iso8859-3" : "latin3",
1581         "iso8859-4" : "latin4",
1582         "iso8859-5" : "iso88595",
1583         "iso8859-6" : "8859-6",
1584         "iso8859-7" : "iso-8859-7",
1585         "iso8859-8" : "8859-8",
1586         "iso8859-9" : "latin5",
1587         "iso8859-13" : "latin7",
1588         "iso8859-15" : "latin9",
1589         "iso8859-16" : "latin10",
1590         "applemac" : "applemac",
1591         "cp437" : "cp437",
1592         "cp437de" : "cp437de",
1593         "cp850" : "cp850",
1594         "cp852" : "cp852",
1595         "cp855" : "cp855",
1596         "cp858" : "cp858",
1597         "cp862" : "cp862",
1598         "cp865" : "cp865",
1599         "cp866" : "cp866",
1600         "cp1250" : "cp1250",
1601         "cp1251" : "cp1251",
1602         "cp1252" : "cp1252",
1603         "cp1255" : "cp1255",
1604         "cp1256" : "cp1256",
1605         "cp1257" : "cp1257",
1606         "koi8-r" : "koi8-r",
1607         "koi8-u" : "koi8-u",
1608         "pt154" : "pt154",
1609         "utf8-platex" : "utf8",
1610         "ascii" : "ascii"
1611     }
1612
1613     i = 0
1614     bibresources = []
1615     while (True):
1616         i = find_token(document.body, "\\begin_inset CommandInset bibtex", i+1)
1617         if i == -1:
1618             break
1619         j = find_end_of_inset(document.body, i)
1620         if j == -1:
1621             document.warning("Can't find end of bibtex inset at line %d!!" %(i))
1622             continue
1623         encodings = get_quoted_value(document.body, "file_encodings", i, j)
1624         if not encodings:
1625             i = j
1626             continue
1627         bibfiles = get_quoted_value(document.body, "bibfiles", i, j).split(",")
1628         opts = get_quoted_value(document.body, "biblatexopts", i, j)
1629         if len(bibfiles) == 0:
1630             document.warning("Bibtex inset at line %d does not have a bibfile!" %(i))
1631         # remove encoding line
1632         k = find_token(document.body, "file_encodings", i, j)
1633         if k != -1:
1634             del document.body[k]
1635         # Re-find inset end line
1636         j = find_end_of_inset(document.body, i)
1637         if biblatex:
1638             enclist = encodings.split("\t")
1639             encmap = dict()
1640             for pp in enclist:
1641                 ppp = pp.split(" ", 1)
1642                 encmap[ppp[0]] = ppp[1]
1643             for bib in bibfiles:
1644                 pr = "\\addbibresource"
1645                 if bib in encmap.keys():
1646                     pr += "[bibencoding=" + encmap[bib] + "]"
1647                 pr += "{" + bib + "}"
1648                 add_to_preamble(document, [pr])
1649             # Insert ERT \\printbibliography and wrap bibtex inset to a Note
1650             pcmd = "printbibliography"
1651             if opts:
1652                 pcmd += "[" + opts + "]"
1653             repl = ["\\begin_inset ERT", "status open", "", "\\begin_layout Plain Layout",\
1654                     "", "", "\\backslash", pcmd, "\\end_layout", "", "\\end_inset", "", "",\
1655                     "\\end_layout", "", "\\begin_layout Standard", "\\begin_inset Note Note",\
1656                     "status open", "", "\\begin_layout Plain Layout" ]
1657             repl += document.body[i:j+1]
1658             repl += ["", "\\end_layout", "", "\\end_inset", "", ""]
1659             document.body[i:j+1] = repl
1660             j += 27
1661
1662         i = j
1663
1664
1665 def revert_cmidruletrimming(document):
1666     " Remove \\cmidrule trimming "
1667
1668     # FIXME: Revert to TeX code?
1669     i = 0
1670     while True:
1671         # first, let's find out if we need to do anything
1672         i = find_token(document.body, '<cell ', i+1)
1673         if i == -1:
1674             return
1675         j = document.body[i].find('trim="')
1676         if j == -1:
1677              continue
1678         rgx = re.compile(r' (bottom|top)line[lr]trim="true"')
1679         # remove trim option
1680         document.body[i] = rgx.sub('', document.body[i])
1681
1682
1683 ruby_inset_def = [
1684     r'### Inserted by lyx2lyx (ruby inset) ###',
1685     r'InsetLayout Flex:Ruby',
1686     r'  LyxType       charstyle',
1687     r'  LatexType     command',
1688     r'  LatexName     ruby',
1689     r'  HTMLTag       ruby',
1690     r'  HTMLAttr      ""',
1691     r'  HTMLInnerTag  rb',
1692     r'  HTMLInnerAttr ""',
1693     r'  BgColor       none',
1694     r'  LabelString   "Ruby"',
1695     r'  Decoration    Conglomerate',
1696     r'  Preamble',
1697     r'    \ifdefined\kanjiskip',
1698     r'      \IfFileExists{okumacro.sty}{\usepackage{okumacro}}{}',
1699     r'    \else \ifdefined\luatexversion',
1700     r'      \usepackage{luatexja-ruby}',
1701     r'    \else \ifdefined\XeTeXversion',
1702     r'      \usepackage{ruby}%',
1703     r'    \fi\fi\fi',
1704     r'    \providecommand{\ruby}[2]{\shortstack{\tiny #2\\#1}}',
1705     r'  EndPreamble',
1706     r'  Argument  post:1',
1707     r'    LabelString  "ruby text"',
1708     r'    MenuString  "Ruby Text|R"',
1709     r'    Tooltip    "Reading aid (ruby, furigana) for Chinese characters."',
1710     r'    Decoration  Conglomerate',
1711     r'    Font',
1712     r'      Size    tiny',
1713     r'    EndFont',
1714     r'    LabelFont',
1715     r'      Size    tiny',
1716     r'    EndFont',
1717     r'    Mandatory  1',
1718     r'  EndArgument',
1719     r'End',
1720 ]
1721
1722 def convert_ruby_module(document):
1723     " Use ruby module instead of local module definition "
1724     if document.del_local_layout(ruby_inset_def):
1725         document.add_module("ruby")
1726
1727 def revert_ruby_module(document):
1728     " Replace ruby module with local module definition "
1729     if document.del_module("ruby"):
1730         document.append_local_layout(ruby_inset_def)
1731
1732
1733 def convert_utf8_japanese(document):
1734     " Use generic utf8 with Japanese documents."
1735     lang = get_value(document.header, "\\language")
1736     if not lang.startswith("japanese"):
1737         return
1738     inputenc = get_value(document.header, "\\inputencoding")
1739     if ((lang == "japanese" and inputenc == "utf8-platex")
1740         or (lang == "japanese-cjk" and inputenc == "utf8-cjk")):
1741         document.set_parameter("inputencoding", "utf8")
1742
1743 def revert_utf8_japanese(document):
1744     " Use Japanese utf8 variants with Japanese documents."
1745     inputenc = get_value(document.header, "\\inputencoding")
1746     if inputenc != "utf8":
1747         return
1748     lang = get_value(document.header, "\\language")
1749     if lang == "japanese":
1750         document.set_parameter("inputencoding", "utf8-platex")
1751     if lang == "japanese-cjk":
1752         document.set_parameter("inputencoding", "utf8-cjk")
1753
1754
1755 def revert_lineno(document):
1756     " Replace lineno setting with user-preamble code."
1757
1758     options = get_quoted_value(document.header, "\\lineno_options",
1759                                delete=True)
1760     if not get_bool_value(document.header, "\\use_lineno", delete=True):
1761         return
1762     if options:
1763         options = "[" + options + "]"
1764     add_to_preamble(document, ["\\usepackage%s{lineno}" % options,
1765                                "\\linenumbers"])
1766
1767 def convert_lineno(document):
1768     " Replace user-preamble code with native lineno support."
1769     use_lineno = 0
1770     options = ""
1771     i = find_token(document.preamble, "\\linenumbers", 1)
1772     if i > -1:
1773         usepkg = re.match(r"\\usepackage(.*){lineno}", document.preamble[i-1])
1774         if usepkg:
1775             use_lineno = 1
1776             options = usepkg.group(1).strip("[]")
1777             del(document.preamble[i-1:i+1])
1778             del_token(document.preamble, "% Added by lyx2lyx", i-2, i-1)
1779
1780     k = find_token(document.header, "\\index ")
1781     if options == "":
1782         document.header[k:k] = ["\\use_lineno %d" % use_lineno]
1783     else:
1784         document.header[k:k] = ["\\use_lineno %d" % use_lineno,
1785                                 "\\lineno_options %s" % options]
1786
1787
1788 def revert_new_languages(document):
1789     """Emulate support for Azerbaijani, Bengali, Church Slavonic, Korean,
1790     and Russian (Petrine orthography)."""
1791
1792     #                lyxname:          (babelname, polyglossianame)
1793     new_languages = {"azerbaijani":    ("azerbaijani", ""),
1794                      "bengali":        ("", "bengali"),
1795                      "churchslavonic": ("", "churchslavonic"),
1796                      "oldrussian":     ("", "russian"),
1797                      "korean":         ("", "korean"),
1798                     }
1799     used_languages = set()
1800     if document.language in new_languages:
1801         used_languages.add(document.language)
1802     i = 0
1803     while True:
1804         i = find_token(document.body, "\\lang", i+1)
1805         if i == -1:
1806             break
1807         if document.body[i][6:].strip() in new_languages:
1808             used_languages.add(document.language)
1809
1810     # Korean is already supported via CJK, so leave as-is for Babel
1811     if ("korean" in used_languages
1812         and get_bool_value(document.header, "\\use_non_tex_fonts")
1813         and get_value(document.header, "\\language_package") in ("default", "auto")):
1814         revert_language(document, "korean", "", "korean")
1815     used_languages.discard("korean")
1816
1817     for lang in used_languages:
1818         revert(lang, *new_languages[lang])
1819
1820
1821 gloss_inset_def = [
1822     r'### Inserted by lyx2lyx (deprecated ling glosses) ###',
1823     r'InsetLayout Flex:Glosse',
1824     r'  LyXType               custom',
1825     r'  LabelString           "Gloss (old version)"',
1826     r'  MenuString            "Gloss (old version)"',
1827     r'  LatexType             environment',
1828     r'  LatexName             linggloss',
1829     r'  Decoration            minimalistic',
1830     r'  LabelFont',
1831     r'    Size                Small',
1832     r'  EndFont',
1833     r'  MultiPar              true',
1834     r'  CustomPars            false',
1835     r'  ForcePlain            true',
1836     r'  ParbreakIsNewline     true',
1837     r'  FreeSpacing           true',
1838     r'  Requires              covington',
1839     r'  Preamble',
1840     r'          \def\glosstr{}',
1841     r'          \@ifundefined{linggloss}{%',
1842     r'          \newenvironment{linggloss}[2][]{',
1843     r'             \def\glosstr{\glt #1}%',
1844     r'             \gll #2}',
1845     r'          {\glosstr\glend}}{}',
1846     r'  EndPreamble',
1847     r'  InToc                 true',
1848     r'  ResetsFont            true',
1849     r'  Argument 1',
1850     r'          Decoration    conglomerate',
1851     r'          LabelString   "Translation"',
1852     r'          MenuString    "Glosse Translation|s"',
1853     r'          Tooltip       "Add a translation for the glosse"',
1854     r'  EndArgument',
1855     r'End'
1856 ]
1857
1858 glosss_inset_def = [
1859     r'### Inserted by lyx2lyx (deprecated ling glosses) ###',
1860     r'InsetLayout Flex:Tri-Glosse',
1861     r'  LyXType               custom',
1862     r'  LabelString           "Tri-Gloss (old version)"',
1863     r'  MenuString            "Tri-Gloss (old version)"',
1864     r'  LatexType             environment',
1865     r'  LatexName             lingglosss',
1866     r'  Decoration            minimalistic',
1867     r'  LabelFont',
1868     r'    Size                Small',
1869     r'  EndFont',
1870     r'  MultiPar              true',
1871     r'  CustomPars            false',
1872     r'  ForcePlain            true',
1873     r'  ParbreakIsNewline     true',
1874     r'  FreeSpacing           true',
1875     r'  InToc                 true',
1876     r'  Requires              covington',
1877     r'  Preamble',
1878     r'          \def\glosstr{}',
1879     r'          \@ifundefined{lingglosss}{%',
1880     r'          \newenvironment{lingglosss}[2][]{',
1881     r'              \def\glosstr{\glt #1}%',
1882     r'              \glll #2}',
1883     r'          {\glosstr\glend}}{}',
1884     r'  EndPreamble',
1885     r'  ResetsFont            true',
1886     r'  Argument 1',
1887     r'          Decoration    conglomerate',
1888     r'          LabelString   "Translation"',
1889     r'          MenuString    "Glosse Translation|s"',
1890     r'          Tooltip       "Add a translation for the glosse"',
1891     r'  EndArgument',
1892     r'End'
1893 ]
1894
1895 def convert_linggloss(document):
1896     " Move old ling glosses to local layout "
1897     if find_token(document.body, '\\begin_inset Flex Glosse', 0) != -1:
1898         document.append_local_layout(gloss_inset_def)
1899     if find_token(document.body, '\\begin_inset Flex Tri-Glosse', 0) != -1:
1900         document.append_local_layout(glosss_inset_def)
1901
1902 def revert_linggloss(document):
1903     " Revert to old ling gloss definitions "
1904     if not "linguistics" in document.get_module_list():
1905         return
1906     document.del_local_layout(gloss_inset_def)
1907     document.del_local_layout(glosss_inset_def)
1908
1909     cov_req = False
1910     glosses = ["\\begin_inset Flex Interlinear Gloss (2 Lines)", "\\begin_inset Flex Interlinear Gloss (3 Lines)"]
1911     for glosse in glosses:
1912         i = 0
1913         while True:
1914             i = find_token(document.body, glosse, i+1)
1915             if i == -1:
1916                 break
1917             j = find_end_of_inset(document.body, i)
1918             if j == -1:
1919                 document.warning("Malformed LyX document: Can't find end of Gloss inset")
1920                 continue
1921
1922             arg = find_token(document.body, "\\begin_inset Argument 1", i, j)
1923             endarg = find_end_of_inset(document.body, arg)
1924             optargcontent = []
1925             if arg != -1:
1926                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
1927                 if argbeginPlain == -1:
1928                     document.warning("Malformed LyX document: Can't find optarg plain Layout")
1929                     continue
1930                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
1931                 optargcontent = document.body[argbeginPlain + 1 : argendPlain - 2]
1932
1933                 # remove Arg insets and paragraph, if it only contains this inset
1934                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
1935                     del document.body[arg - 1 : endarg + 4]
1936                 else:
1937                     del document.body[arg : endarg + 1]
1938
1939             arg = find_token(document.body, "\\begin_inset Argument post:1", i, j)
1940             endarg = find_end_of_inset(document.body, arg)
1941             marg1content = []
1942             if arg != -1:
1943                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
1944                 if argbeginPlain == -1:
1945                     document.warning("Malformed LyX document: Can't find arg 1 plain Layout")
1946                     continue
1947                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
1948                 marg1content = document.body[argbeginPlain + 1 : argendPlain - 2]
1949
1950                 # remove Arg insets and paragraph, if it only contains this inset
1951                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
1952                     del document.body[arg - 1 : endarg + 4]
1953                 else:
1954                     del document.body[arg : endarg + 1]
1955
1956             arg = find_token(document.body, "\\begin_inset Argument post:2", i, j)
1957             endarg = find_end_of_inset(document.body, arg)
1958             marg2content = []
1959             if arg != -1:
1960                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
1961                 if argbeginPlain == -1:
1962                     document.warning("Malformed LyX document: Can't find arg 2 plain Layout")
1963                     continue
1964                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
1965                 marg2content = document.body[argbeginPlain + 1 : argendPlain - 2]
1966
1967                 # remove Arg insets and paragraph, if it only contains this inset
1968                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
1969                     del document.body[arg - 1 : endarg + 4]
1970                 else:
1971                     del document.body[arg : endarg + 1]
1972
1973             arg = find_token(document.body, "\\begin_inset Argument post:3", i, j)
1974             endarg = find_end_of_inset(document.body, arg)
1975             marg3content = []
1976             if arg != -1:
1977                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
1978                 if argbeginPlain == -1:
1979                     document.warning("Malformed LyX document: Can't find arg 3 plain Layout")
1980                     continue
1981                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
1982                 marg3content = document.body[argbeginPlain + 1 : argendPlain - 2]
1983
1984                 # remove Arg insets and paragraph, if it only contains this inset
1985                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
1986                     del document.body[arg - 1 : endarg + 4]
1987                 else:
1988                     del document.body[arg : endarg + 1]
1989
1990             cmd = "\\digloss"
1991             if glosse == "\\begin_inset Flex Interlinear Gloss (3 Lines)":
1992                 cmd = "\\trigloss"
1993
1994             beginPlain = find_token(document.body, "\\begin_layout Plain Layout", i)
1995             endInset = find_end_of_inset(document.body, i)
1996             endPlain = find_token_backwards(document.body, "\\end_layout", endInset)
1997             precontent = put_cmd_in_ert(cmd)
1998             if len(optargcontent) > 0:
1999                 precontent += put_cmd_in_ert("[") + optargcontent + put_cmd_in_ert("]")
2000             precontent += put_cmd_in_ert("{")
2001
2002             postcontent = put_cmd_in_ert("}{") + marg1content + put_cmd_in_ert("}{") + marg2content
2003             if cmd == "\\trigloss":
2004                 postcontent += put_cmd_in_ert("}{") + marg3content
2005             postcontent += put_cmd_in_ert("}")
2006
2007             document.body[endPlain:endInset + 1] = postcontent
2008             document.body[beginPlain + 1:beginPlain] = precontent
2009             del document.body[i : beginPlain + 1]
2010             if not cov_req:
2011                 document.append_local_layout("Requires covington")
2012                 cov_req = True
2013             i = beginPlain
2014
2015
2016 def revert_subexarg(document):
2017     " Revert linguistic subexamples with argument to ERT "
2018
2019     if not "linguistics" in document.get_module_list():
2020         return
2021
2022     cov_req = False
2023     i = 0
2024     while True:
2025         i = find_token(document.body, "\\begin_layout Subexample", i+1)
2026         if i == -1:
2027             break
2028         j = find_end_of_layout(document.body, i)
2029         if j == -1:
2030             document.warning("Malformed LyX document: Can't find end of Subexample layout")
2031             continue
2032         while True:
2033             # check for consecutive layouts
2034             k = find_token(document.body, "\\begin_layout", j)
2035             if k == -1 or document.body[k] != "\\begin_layout Subexample":
2036                 break
2037             j = find_end_of_layout(document.body, k)
2038             if j == -1:
2039                  document.warning("Malformed LyX document: Can't find end of Subexample layout")
2040                  continue
2041
2042         arg = find_token(document.body, "\\begin_inset Argument 1", i, j)
2043         if arg == -1:
2044             continue
2045
2046         endarg = find_end_of_inset(document.body, arg)
2047         optargcontent = ""
2048         argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
2049         if argbeginPlain == -1:
2050             document.warning("Malformed LyX document: Can't find optarg plain Layout")
2051             continue
2052         argendPlain = find_end_of_inset(document.body, argbeginPlain)
2053         optargcontent = lyx2latex(document, document.body[argbeginPlain + 1 : argendPlain - 2])
2054
2055         # remove Arg insets and paragraph, if it only contains this inset
2056         if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
2057             del document.body[arg - 1 : endarg + 4]
2058         else:
2059             del document.body[arg : endarg + 1]
2060
2061         cmd = put_cmd_in_ert("\\begin{subexamples}[" + optargcontent + "]")
2062
2063         # re-find end of layout
2064         j = find_end_of_layout(document.body, i)
2065         if j == -1:
2066             document.warning("Malformed LyX document: Can't find end of Subexample layout")
2067             continue
2068         while True:
2069             # check for consecutive layouts
2070             k = find_token(document.body, "\\begin_layout", j)
2071             if k == -1 or document.body[k] != "\\begin_layout Subexample":
2072                 break
2073             document.body[k : k + 1] = ["\\begin_layout Standard"] + put_cmd_in_ert("\\item ")
2074             j = find_end_of_layout(document.body, k)
2075             if j == -1:
2076                  document.warning("Malformed LyX document: Can't find end of Subexample layout")
2077                  continue
2078
2079         endev = put_cmd_in_ert("\\end{subexamples}")
2080
2081         document.body[j : j] = ["\\end_layout", "", "\\begin_layout Standard"] + endev
2082         document.body[i : i + 1] = ["\\begin_layout Standard"] + cmd \
2083                 + ["\\end_layout", "", "\\begin_layout Standard"] + put_cmd_in_ert("\\item ")
2084         if not cov_req:
2085             document.append_local_layout("Requires covington")
2086             cov_req = True
2087
2088
2089 def revert_drs(document):
2090     " Revert DRS insets (linguistics) to ERT "
2091
2092     if not "linguistics" in document.get_module_list():
2093         return
2094
2095     cov_req = False
2096     drses = ["\\begin_inset Flex DRS", "\\begin_inset Flex DRS*",
2097              "\\begin_inset Flex IfThen-DRS", "\\begin_inset Flex Cond-DRS",
2098              "\\begin_inset Flex QDRS", "\\begin_inset Flex NegDRS",
2099              "\\begin_inset Flex SDRS"]
2100     for drs in drses:
2101         i = 0
2102         while True:
2103             i = find_token(document.body, drs, i+1)
2104             if i == -1:
2105                 break
2106             j = find_end_of_inset(document.body, i)
2107             if j == -1:
2108                 document.warning("Malformed LyX document: Can't find end of DRS inset")
2109                 continue
2110
2111             # Check for arguments
2112             arg = find_token(document.body, "\\begin_inset Argument 1", i, j)
2113             endarg = find_end_of_inset(document.body, arg)
2114             prearg1content = []
2115             if arg != -1:
2116                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
2117                 if argbeginPlain == -1:
2118                     document.warning("Malformed LyX document: Can't find Argument 1 plain Layout")
2119                     continue
2120                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
2121                 prearg1content = document.body[argbeginPlain + 1 : argendPlain - 2]
2122
2123                 # remove Arg insets and paragraph, if it only contains this inset
2124                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
2125                     del document.body[arg - 1 : endarg + 4]
2126                 else:
2127                     del document.body[arg : endarg + 1]
2128
2129             # re-find inset end
2130             j = find_end_of_inset(document.body, i)
2131             if j == -1:
2132                 document.warning("Malformed LyX document: Can't find end of DRS inset")
2133                 continue
2134
2135             arg = find_token(document.body, "\\begin_inset Argument 2", i, j)
2136             endarg = find_end_of_inset(document.body, arg)
2137             prearg2content = []
2138             if arg != -1:
2139                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
2140                 if argbeginPlain == -1:
2141                     document.warning("Malformed LyX document: Can't find Argument 2 plain Layout")
2142                     continue
2143                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
2144                 prearg2content = document.body[argbeginPlain + 1 : argendPlain - 2]
2145
2146                 # remove Arg insets and paragraph, if it only contains this inset
2147                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
2148                     del document.body[arg - 1 : endarg + 4]
2149                 else:
2150                     del document.body[arg : endarg + 1]
2151
2152             # re-find inset end
2153             j = find_end_of_inset(document.body, i)
2154             if j == -1:
2155                 document.warning("Malformed LyX document: Can't find end of DRS inset")
2156                 continue
2157
2158             arg = find_token(document.body, "\\begin_inset Argument post:1", i, j)
2159             endarg = find_end_of_inset(document.body, arg)
2160             postarg1content = []
2161             if arg != -1:
2162                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
2163                 if argbeginPlain == -1:
2164                     document.warning("Malformed LyX document: Can't find Argument post:1 plain Layout")
2165                     continue
2166                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
2167                 postarg1content = document.body[argbeginPlain + 1 : argendPlain - 2]
2168
2169                 # remove Arg insets and paragraph, if it only contains this inset
2170                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
2171                     del document.body[arg - 1 : endarg + 4]
2172                 else:
2173                     del document.body[arg : endarg + 1]
2174
2175             # re-find inset end
2176             j = find_end_of_inset(document.body, i)
2177             if j == -1:
2178                 document.warning("Malformed LyX document: Can't find end of DRS inset")
2179                 continue
2180
2181             arg = find_token(document.body, "\\begin_inset Argument post:2", i, j)
2182             endarg = find_end_of_inset(document.body, arg)
2183             postarg2content = []
2184             if arg != -1:
2185                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
2186                 if argbeginPlain == -1:
2187                     document.warning("Malformed LyX document: Can't find Argument post:2 plain Layout")
2188                     continue
2189                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
2190                 postarg2content = document.body[argbeginPlain + 1 : argendPlain - 2]
2191
2192                 # remove Arg insets and paragraph, if it only contains this inset
2193                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
2194                     del document.body[arg - 1 : endarg + 4]
2195                 else:
2196                     del document.body[arg : endarg + 1]
2197
2198             # re-find inset end
2199             j = find_end_of_inset(document.body, i)
2200             if j == -1:
2201                 document.warning("Malformed LyX document: Can't find end of DRS inset")
2202                 continue
2203
2204             arg = find_token(document.body, "\\begin_inset Argument post:3", i, j)
2205             endarg = find_end_of_inset(document.body, arg)
2206             postarg3content = []
2207             if arg != -1:
2208                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
2209                 if argbeginPlain == -1:
2210                     document.warning("Malformed LyX document: Can't find Argument post:3 plain Layout")
2211                     continue
2212                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
2213                 postarg3content = document.body[argbeginPlain + 1 : argendPlain - 2]
2214
2215                 # remove Arg insets and paragraph, if it only contains this inset
2216                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
2217                     del document.body[arg - 1 : endarg + 4]
2218                 else:
2219                     del document.body[arg : endarg + 1]
2220
2221             # re-find inset end
2222             j = find_end_of_inset(document.body, i)
2223             if j == -1:
2224                 document.warning("Malformed LyX document: Can't find end of DRS inset")
2225                 continue
2226
2227             arg = find_token(document.body, "\\begin_inset Argument post:4", i, j)
2228             endarg = find_end_of_inset(document.body, arg)
2229             postarg4content = []
2230             if arg != -1:
2231                 argbeginPlain = find_token(document.body, "\\begin_layout Plain Layout", arg, endarg)
2232                 if argbeginPlain == -1:
2233                     document.warning("Malformed LyX document: Can't find Argument post:4 plain Layout")
2234                     continue
2235                 argendPlain = find_end_of_inset(document.body, argbeginPlain)
2236                 postarg4content = document.body[argbeginPlain + 1 : argendPlain - 2]
2237
2238                 # remove Arg insets and paragraph, if it only contains this inset
2239                 if document.body[arg - 1] == "\\begin_layout Plain Layout" and find_end_of_layout(document.body, arg - 1) == endarg + 3:
2240                     del document.body[arg - 1 : endarg + 4]
2241                 else:
2242                     del document.body[arg : endarg + 1]
2243
2244             # The respective LaTeX command
2245             cmd = "\\drs"
2246             if drs == "\\begin_inset Flex DRS*":
2247                 cmd = "\\drs*"
2248             elif drs == "\\begin_inset Flex IfThen-DRS":
2249                 cmd = "\\ifdrs"
2250             elif drs == "\\begin_inset Flex Cond-DRS":
2251                 cmd = "\\condrs"
2252             elif drs == "\\begin_inset Flex QDRS":
2253                 cmd = "\\qdrs"
2254             elif drs == "\\begin_inset Flex NegDRS":
2255                 cmd = "\\negdrs"
2256             elif drs == "\\begin_inset Flex SDRS":
2257                 cmd = "\\sdrs"
2258
2259             beginPlain = find_token(document.body, "\\begin_layout Plain Layout", i)
2260             endInset = find_end_of_inset(document.body, i)
2261             endPlain = find_token_backwards(document.body, "\\end_layout", endInset)
2262             precontent = put_cmd_in_ert(cmd)
2263             precontent += put_cmd_in_ert("{") + prearg1content + put_cmd_in_ert("}")
2264             if drs == "\\begin_inset Flex SDRS":
2265                 precontent += put_cmd_in_ert("{") + prearg2content + put_cmd_in_ert("}")
2266             precontent += put_cmd_in_ert("{")
2267
2268             postcontent = []
2269             if cmd == "\\qdrs" or cmd == "\\condrs" or cmd == "\\ifdrs":
2270                 postcontent = put_cmd_in_ert("}{") + postarg1content + put_cmd_in_ert("}{") + postarg2content + put_cmd_in_ert("}")
2271                 if cmd == "\\condrs" or cmd == "\\qdrs":
2272                     postcontent += put_cmd_in_ert("{") + postarg3content + put_cmd_in_ert("}")
2273                 if cmd == "\\qdrs":
2274                     postcontent += put_cmd_in_ert("{") + postarg4content + put_cmd_in_ert("}")
2275             else:
2276                 postcontent = put_cmd_in_ert("}")
2277
2278             document.body[endPlain:endInset + 1] = postcontent
2279             document.body[beginPlain + 1:beginPlain] = precontent
2280             del document.body[i : beginPlain + 1]
2281             if not cov_req:
2282                 document.append_local_layout("Provides covington 1")
2283                 add_to_preamble(document, ["\\usepackage{drs,covington}"])
2284                 cov_req = True
2285             i = beginPlain
2286
2287
2288
2289 def revert_babelfont(document):
2290     " Reverts the use of \\babelfont to user preamble "
2291
2292     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2293     if i == -1:
2294         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2295         return
2296     if not str2bool(get_value(document.header, "\\use_non_tex_fonts", i)):
2297         return
2298     i = find_token(document.header, '\\language_package', 0)
2299     if i == -1:
2300         document.warning("Malformed LyX document: Missing \\language_package.")
2301         return
2302     if get_value(document.header, "\\language_package", 0) != "babel":
2303         return
2304
2305     # check font settings
2306     # defaults
2307     roman = sans = typew = "default"
2308     osf = False
2309     sf_scale = tt_scale = 100.0
2310
2311     j = find_token(document.header, "\\font_roman", 0)
2312     if j == -1:
2313         document.warning("Malformed LyX document: Missing \\font_roman.")
2314     else:
2315         # We need to use this regex since split() does not handle quote protection
2316         romanfont = re.findall(r'[^"\s]\S*|".+?"', document.header[j])
2317         roman = romanfont[2].strip('"')
2318         romanfont[2] = '"default"'
2319         document.header[j] = " ".join(romanfont)
2320
2321     j = find_token(document.header, "\\font_sans", 0)
2322     if j == -1:
2323         document.warning("Malformed LyX document: Missing \\font_sans.")
2324     else:
2325         # We need to use this regex since split() does not handle quote protection
2326         sansfont = re.findall(r'[^"\s]\S*|".+?"', document.header[j])
2327         sans = sansfont[2].strip('"')
2328         sansfont[2] = '"default"'
2329         document.header[j] = " ".join(sansfont)
2330
2331     j = find_token(document.header, "\\font_typewriter", 0)
2332     if j == -1:
2333         document.warning("Malformed LyX document: Missing \\font_typewriter.")
2334     else:
2335         # We need to use this regex since split() does not handle quote protection
2336         ttfont = re.findall(r'[^"\s]\S*|".+?"', document.header[j])
2337         typew = ttfont[2].strip('"')
2338         ttfont[2] = '"default"'
2339         document.header[j] = " ".join(ttfont)
2340
2341     i = find_token(document.header, "\\font_osf", 0)
2342     if i == -1:
2343         document.warning("Malformed LyX document: Missing \\font_osf.")
2344     else:
2345         osf = str2bool(get_value(document.header, "\\font_osf", i))
2346
2347     j = find_token(document.header, "\\font_sf_scale", 0)
2348     if j == -1:
2349         document.warning("Malformed LyX document: Missing \\font_sf_scale.")
2350     else:
2351         sfscale = document.header[j].split()
2352         val = sfscale[2]
2353         sfscale[2] = "100"
2354         document.header[j] = " ".join(sfscale)
2355         try:
2356             # float() can throw
2357             sf_scale = float(val)
2358         except:
2359             document.warning("Invalid font_sf_scale value: " + val)
2360
2361     j = find_token(document.header, "\\font_tt_scale", 0)
2362     if j == -1:
2363         document.warning("Malformed LyX document: Missing \\font_tt_scale.")
2364     else:
2365         ttscale = document.header[j].split()
2366         val = ttscale[2]
2367         ttscale[2] = "100"
2368         document.header[j] = " ".join(ttscale)
2369         try:
2370             # float() can throw
2371             tt_scale = float(val)
2372         except:
2373             document.warning("Invalid font_tt_scale value: " + val)
2374
2375     # set preamble stuff
2376     pretext = ['%% This document must be processed with xelatex or lualatex!']
2377     pretext.append('\\AtBeginDocument{%')
2378     if roman != "default":
2379         pretext.append('\\babelfont{rm}[Mapping=tex-text]{' + roman + '}')
2380     if sans != "default":
2381         sf = '\\babelfont{sf}['
2382         if sf_scale != 100.0:
2383             sf += 'Scale=' + str(sf_scale / 100.0) + ','
2384         sf += 'Mapping=tex-text]{' + sans + '}'
2385         pretext.append(sf)
2386     if typew != "default":
2387         tw = '\\babelfont{tt}'
2388         if tt_scale != 100.0:
2389             tw += '[Scale=' + str(tt_scale / 100.0) + ']'
2390         tw += '{' + typew + '}'
2391         pretext.append(tw)
2392     if osf:
2393         pretext.append('\\defaultfontfeatures{Numbers=OldStyle}')
2394     pretext.append('}')
2395     insert_to_preamble(document, pretext)
2396
2397
2398 def revert_minionpro(document):
2399     " Revert native MinionPro font definition (with extra options) to LaTeX "
2400
2401     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2402     if i == -1:
2403         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2404         return
2405     if str2bool(get_value(document.header, "\\use_non_tex_fonts", i)):
2406         return
2407
2408     regexp = re.compile(r'(\\font_roman_opts)')
2409     x = find_re(document.header, regexp, 0)
2410     if x == -1:
2411         return
2412
2413     # We need to use this regex since split() does not handle quote protection
2414     romanopts = re.findall(r'[^"\s]\S*|".+?"', document.header[x])
2415     opts = romanopts[1].strip('"')
2416
2417     i = find_token(document.header, "\\font_roman", 0)
2418     if i == -1:
2419         document.warning("Malformed LyX document: Missing \\font_roman.")
2420         return
2421     else:
2422         # We need to use this regex since split() does not handle quote protection
2423         romanfont = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2424         roman = romanfont[1].strip('"')
2425         if roman != "minionpro":
2426             return
2427         romanfont[1] = '"default"'
2428         document.header[i] = " ".join(romanfont)
2429         osf = False
2430         j = find_token(document.header, "\\font_osf true", 0)
2431         if j != -1:
2432             osf = True
2433         preamble = "\\usepackage["
2434         if osf:
2435             document.header[j] = "\\font_osf false"
2436         else:
2437             preamble += "lf,"
2438         preamble += opts
2439         preamble += "]{MinionPro}"
2440         add_to_preamble(document, [preamble])
2441         del document.header[x]
2442
2443
2444 def revert_font_opts(document):
2445     " revert font options by outputting \\setxxxfont or \\babelfont to the preamble "
2446
2447     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2448     if i == -1:
2449         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2450         return
2451     NonTeXFonts = str2bool(get_value(document.header, "\\use_non_tex_fonts", i))
2452     i = find_token(document.header, '\\language_package', 0)
2453     if i == -1:
2454         document.warning("Malformed LyX document: Missing \\language_package.")
2455         return
2456     Babel = (get_value(document.header, "\\language_package", 0) == "babel")
2457
2458     # 1. Roman
2459     regexp = re.compile(r'(\\font_roman_opts)')
2460     i = find_re(document.header, regexp, 0)
2461     if i != -1:
2462         # We need to use this regex since split() does not handle quote protection
2463         romanopts = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2464         opts = romanopts[1].strip('"')
2465         del document.header[i]
2466         if NonTeXFonts:
2467             regexp = re.compile(r'(\\font_roman)')
2468             i = find_re(document.header, regexp, 0)
2469             if i != -1:
2470                 # We need to use this regex since split() does not handle quote protection
2471                 romanfont = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2472                 font = romanfont[2].strip('"')
2473                 romanfont[2] = '"default"'
2474                 document.header[i] = " ".join(romanfont)
2475                 if font != "default":
2476                     if Babel:
2477                         preamble = "\\babelfont{rm}["
2478                     else:
2479                         preamble = "\\setmainfont["
2480                     preamble += opts
2481                     preamble += ","
2482                     preamble += "Mapping=tex-text]{"
2483                     preamble += font
2484                     preamble += "}"
2485                     add_to_preamble(document, [preamble])
2486
2487     # 2. Sans
2488     regexp = re.compile(r'(\\font_sans_opts)')
2489     i = find_re(document.header, regexp, 0)
2490     if i != -1:
2491         scaleval = 100
2492         # We need to use this regex since split() does not handle quote protection
2493         sfopts = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2494         opts = sfopts[1].strip('"')
2495         del document.header[i]
2496         if NonTeXFonts:
2497             regexp = re.compile(r'(\\font_sf_scale)')
2498             i = find_re(document.header, regexp, 0)
2499             if i != -1:
2500                 scaleval = get_value(document.header, "\\font_sf_scale" , i).split()[1]
2501             regexp = re.compile(r'(\\font_sans)')
2502             i = find_re(document.header, regexp, 0)
2503             if i != -1:
2504                 # We need to use this regex since split() does not handle quote protection
2505                 sffont = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2506                 font = sffont[2].strip('"')
2507                 sffont[2] = '"default"'
2508                 document.header[i] = " ".join(sffont)
2509                 if font != "default":
2510                     if Babel:
2511                         preamble = "\\babelfont{sf}["
2512                     else:
2513                         preamble = "\\setsansfont["
2514                     preamble += opts
2515                     preamble += ","
2516                     if scaleval != 100:
2517                         preamble += "Scale=0."
2518                         preamble += scaleval
2519                         preamble += ","
2520                     preamble += "Mapping=tex-text]{"
2521                     preamble += font
2522                     preamble += "}"
2523                     add_to_preamble(document, [preamble])
2524
2525     # 3. Typewriter
2526     regexp = re.compile(r'(\\font_typewriter_opts)')
2527     i = find_re(document.header, regexp, 0)
2528     if i != -1:
2529         scaleval = 100
2530         # We need to use this regex since split() does not handle quote protection
2531         ttopts = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2532         opts = ttopts[1].strip('"')
2533         del document.header[i]
2534         if NonTeXFonts:
2535             regexp = re.compile(r'(\\font_tt_scale)')
2536             i = find_re(document.header, regexp, 0)
2537             if i != -1:
2538                 scaleval = get_value(document.header, "\\font_tt_scale" , i).split()[1]
2539             regexp = re.compile(r'(\\font_typewriter)')
2540             i = find_re(document.header, regexp, 0)
2541             if i != -1:
2542                 # We need to use this regex since split() does not handle quote protection
2543                 ttfont = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2544                 font = ttfont[2].strip('"')
2545                 ttfont[2] = '"default"'
2546                 document.header[i] = " ".join(ttfont)
2547                 if font != "default":
2548                     if Babel:
2549                         preamble = "\\babelfont{tt}["
2550                     else:
2551                         preamble = "\\setmonofont["
2552                     preamble += opts
2553                     preamble += ","
2554                     if scaleval != 100:
2555                         preamble += "Scale=0."
2556                         preamble += scaleval
2557                         preamble += ","
2558                     preamble += "Mapping=tex-text]{"
2559                     preamble += font
2560                     preamble += "}"
2561                     add_to_preamble(document, [preamble])
2562
2563
2564 def revert_plainNotoFonts_xopts(document):
2565     " Revert native (straight) Noto font definition (with extra options) to LaTeX "
2566
2567     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2568     if i == -1:
2569         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2570         return
2571     if str2bool(get_value(document.header, "\\use_non_tex_fonts", i)):
2572         return
2573
2574     osf = False
2575     y = find_token(document.header, "\\font_osf true", 0)
2576     if y != -1:
2577         osf = True
2578
2579     regexp = re.compile(r'(\\font_roman_opts)')
2580     x = find_re(document.header, regexp, 0)
2581     if x == -1 and not osf:
2582         return
2583
2584     opts = ""
2585     if x != -1:
2586         # We need to use this regex since split() does not handle quote protection
2587         romanopts = re.findall(r'[^"\s]\S*|".+?"', document.header[x])
2588         opts = romanopts[1].strip('"')
2589     if osf:
2590         if opts != "":
2591             opts += ", "
2592         opts += "osf"
2593
2594     i = find_token(document.header, "\\font_roman", 0)
2595     if i == -1:
2596         return
2597
2598     # We need to use this regex since split() does not handle quote protection
2599     romanfont = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2600     roman = romanfont[1].strip('"')
2601     if roman != "NotoSerif-TLF":
2602         return
2603
2604     j = find_token(document.header, "\\font_sans", 0)
2605     if j == -1:
2606         return
2607
2608     # We need to use this regex since split() does not handle quote protection
2609     sffont = re.findall(r'[^"\s]\S*|".+?"', document.header[j])
2610     sf = sffont[1].strip('"')
2611     if sf != "default":
2612         return
2613
2614     j = find_token(document.header, "\\font_typewriter", 0)
2615     if j == -1:
2616         return
2617
2618     # We need to use this regex since split() does not handle quote protection
2619     ttfont = re.findall(r'[^"\s]\S*|".+?"', document.header[j])
2620     tt = ttfont[1].strip('"')
2621     if tt != "default":
2622         return
2623
2624     # So we have noto as "complete font"
2625     romanfont[1] = '"default"'
2626     document.header[i] = " ".join(romanfont)
2627
2628     preamble = "\\usepackage["
2629     preamble += opts
2630     preamble += "]{noto}"
2631     add_to_preamble(document, [preamble])
2632     if osf:
2633         document.header[y] = "\\font_osf false"
2634     if x != -1:
2635         del document.header[x]
2636
2637
2638 def revert_notoFonts_xopts(document):
2639     " Revert native (extended) Noto font definition (with extra options) to LaTeX "
2640
2641     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2642     if i == -1:
2643         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2644         return
2645     if str2bool(get_value(document.header, "\\use_non_tex_fonts", i)):
2646         return
2647
2648     fontmap = dict()
2649     fm = createFontMapping(['Noto'])
2650     if revert_fonts(document, fm, fontmap, True):
2651         add_preamble_fonts(document, fontmap)
2652
2653
2654 def revert_IBMFonts_xopts(document):
2655     " Revert native IBM font definition (with extra options) to LaTeX "
2656
2657     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2658     if i == -1:
2659         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2660         return
2661     if str2bool(get_value(document.header, "\\use_non_tex_fonts", i)):
2662         return
2663
2664     fontmap = dict()
2665     fm = createFontMapping(['IBM'])
2666     ft = ""
2667     if revert_fonts(document, fm, fontmap, True):
2668         add_preamble_fonts(document, fontmap)
2669
2670
2671 def revert_AdobeFonts_xopts(document):
2672     " Revert native Adobe font definition (with extra options) to LaTeX "
2673
2674     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2675     if i == -1:
2676         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2677         return
2678     if str2bool(get_value(document.header, "\\use_non_tex_fonts", i)):
2679         return
2680
2681     fontmap = dict()
2682     fm = createFontMapping(['Adobe'])
2683     ft = ""
2684     if revert_fonts(document, fm, fontmap, True):
2685         add_preamble_fonts(document, fontmap)
2686
2687
2688 def convert_osf(document):
2689     " Convert \\font_osf param to new format "
2690
2691     NonTeXFonts = False
2692     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2693     if i == -1:
2694         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2695     else:
2696         NonTeXFonts = str2bool(get_value(document.header, "\\use_non_tex_fonts", i))
2697
2698     i = find_token(document.header, '\\font_osf', 0)
2699     if i == -1:
2700         document.warning("Malformed LyX document: Missing \\font_osf.")
2701         return
2702
2703     osfsf = ["biolinum", "ADOBESourceSansPro", "NotoSansRegular", "NotoSansMedium", "NotoSansThin", "NotoSansLight", "NotoSansExtralight" ]
2704     osftt = ["ADOBESourceCodePro", "NotoMonoRegular" ]
2705
2706     osfval = str2bool(get_value(document.header, "\\font_osf", i))
2707     document.header[i] = document.header[i].replace("\\font_osf", "\\font_roman_osf")
2708
2709     if NonTeXFonts:
2710         document.header.insert(i, "\\font_sans_osf false")
2711         document.header.insert(i + 1, "\\font_typewriter_osf false")
2712         return
2713
2714     if osfval:
2715         x = find_token(document.header, "\\font_sans", 0)
2716         if x == -1:
2717             document.warning("Malformed LyX document: Missing \\font_sans.")
2718         else:
2719             # We need to use this regex since split() does not handle quote protection
2720             sffont = re.findall(r'[^"\s]\S*|".+?"', document.header[x])
2721             sf = sffont[1].strip('"')
2722             if sf in osfsf:
2723                 document.header.insert(i, "\\font_sans_osf true")
2724             else:
2725                 document.header.insert(i, "\\font_sans_osf false")
2726
2727         x = find_token(document.header, "\\font_typewriter", 0)
2728         if x == -1:
2729             document.warning("Malformed LyX document: Missing \\font_typewriter.")
2730         else:
2731             # We need to use this regex since split() does not handle quote protection
2732             ttfont = re.findall(r'[^"\s]\S*|".+?"', document.header[x])
2733             tt = ttfont[1].strip('"')
2734             if tt in osftt:
2735                 document.header.insert(i + 1, "\\font_typewriter_osf true")
2736             else:
2737                 document.header.insert(i + 1, "\\font_typewriter_osf false")
2738
2739     else:
2740         document.header.insert(i, "\\font_sans_osf false")
2741         document.header.insert(i + 1, "\\font_typewriter_osf false")
2742
2743
2744 def revert_osf(document):
2745     " Revert \\font_*_osf params "
2746
2747     NonTeXFonts = False
2748     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2749     if i == -1:
2750         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2751     else:
2752         NonTeXFonts = str2bool(get_value(document.header, "\\use_non_tex_fonts", i))
2753
2754     i = find_token(document.header, '\\font_roman_osf', 0)
2755     if i == -1:
2756         document.warning("Malformed LyX document: Missing \\font_roman_osf.")
2757         return
2758
2759     osfval = str2bool(get_value(document.header, "\\font_roman_osf", i))
2760     document.header[i] = document.header[i].replace("\\font_roman_osf", "\\font_osf")
2761
2762     i = find_token(document.header, '\\font_sans_osf', 0)
2763     if i == -1:
2764         document.warning("Malformed LyX document: Missing \\font_sans_osf.")
2765         return
2766
2767     osfval = str2bool(get_value(document.header, "\\font_sans_osf", i))
2768     del document.header[i]
2769
2770     i = find_token(document.header, '\\font_typewriter_osf', 0)
2771     if i == -1:
2772         document.warning("Malformed LyX document: Missing \\font_typewriter_osf.")
2773         return
2774
2775     osfval |= str2bool(get_value(document.header, "\\font_typewriter_osf", i))
2776     del document.header[i]
2777
2778     if osfval:
2779         i = find_token(document.header, '\\font_osf', 0)
2780         if i == -1:
2781             document.warning("Malformed LyX document: Missing \\font_osf.")
2782             return
2783         document.header[i] = "\\font_osf true"
2784
2785
2786 def revert_texfontopts(document):
2787     " Revert native TeX font definitions (with extra options) to LaTeX "
2788
2789     i = find_token(document.header, '\\use_non_tex_fonts', 0)
2790     if i == -1:
2791         document.warning("Malformed LyX document: Missing \\use_non_tex_fonts.")
2792         return
2793     if str2bool(get_value(document.header, "\\use_non_tex_fonts", i)):
2794         return
2795
2796     rmfonts = ["ccfonts", "cochineal", "utopia", "garamondx", "libertine", "lmodern", "palatino", "times", "xcharter" ]
2797
2798     # First the sf (biolinum only)
2799     regexp = re.compile(r'(\\font_sans_opts)')
2800     x = find_re(document.header, regexp, 0)
2801     if x != -1:
2802         # We need to use this regex since split() does not handle quote protection
2803         sfopts = re.findall(r'[^"\s]\S*|".+?"', document.header[x])
2804         opts = sfopts[1].strip('"')
2805         i = find_token(document.header, "\\font_sans", 0)
2806         if i == -1:
2807             document.warning("Malformed LyX document: Missing \\font_sans.")
2808         else:
2809             # We need to use this regex since split() does not handle quote protection
2810             sffont = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2811             sans = sffont[1].strip('"')
2812             if sans == "biolinum":
2813                 sf_scale = 100.0
2814                 sffont[1] = '"default"'
2815                 document.header[i] = " ".join(sffont)
2816                 osf = False
2817                 j = find_token(document.header, "\\font_sans_osf true", 0)
2818                 if j != -1:
2819                     osf = True
2820                 k = find_token(document.header, "\\font_sf_scale", 0)
2821                 if k == -1:
2822                     document.warning("Malformed LyX document: Missing \\font_sf_scale.")
2823                 else:
2824                     sfscale = document.header[k].split()
2825                     val = sfscale[1]
2826                     sfscale[1] = "100"
2827                     document.header[k] = " ".join(sfscale)
2828                     try:
2829                         # float() can throw
2830                         sf_scale = float(val)
2831                     except:
2832                         document.warning("Invalid font_sf_scale value: " + val)
2833                 preamble = "\\usepackage["
2834                 if osf:
2835                     document.header[j] = "\\font_sans_osf false"
2836                     preamble += "osf,"
2837                 if sf_scale != 100.0:
2838                     preamble += 'scaled=' + str(sf_scale / 100.0) + ','
2839                 preamble += opts
2840                 preamble += "]{biolinum}"
2841                 add_to_preamble(document, [preamble])
2842                 del document.header[x]
2843
2844     regexp = re.compile(r'(\\font_roman_opts)')
2845     x = find_re(document.header, regexp, 0)
2846     if x == -1:
2847         return
2848
2849     # We need to use this regex since split() does not handle quote protection
2850     romanopts = re.findall(r'[^"\s]\S*|".+?"', document.header[x])
2851     opts = romanopts[1].strip('"')
2852
2853     i = find_token(document.header, "\\font_roman", 0)
2854     if i == -1:
2855         document.warning("Malformed LyX document: Missing \\font_roman.")
2856         return
2857     else:
2858         # We need to use this regex since split() does not handle quote protection
2859         romanfont = re.findall(r'[^"\s]\S*|".+?"', document.header[i])
2860         roman = romanfont[1].strip('"')
2861         if not roman in rmfonts:
2862             return
2863         romanfont[1] = '"default"'
2864         document.header[i] = " ".join(romanfont)
2865         package = roman
2866         if roman == "utopia":
2867             package = "fourier"
2868         elif roman == "palatino":
2869             package = "mathpazo"
2870         elif roman == "times":
2871             package = "mathptmx"
2872         elif roman == "xcharter":
2873             package = "XCharter"
2874         osf = ""
2875         j = find_token(document.header, "\\font_roman_osf true", 0)
2876         if j != -1:
2877             if roman == "cochineal":
2878                 osf = "proportional,osf,"
2879             elif roman == "utopia":
2880                 osf = "oldstyle,"
2881             elif roman == "garamondx":
2882                 osf = "osfI,"
2883             elif roman == "libertine":
2884                 osf = "osf,"
2885             elif roman == "palatino":
2886                 osf = "osf,"
2887             elif roman == "xcharter":
2888                 osf = "osf,"
2889             document.header[j] = "\\font_roman_osf false"
2890         k = find_token(document.header, "\\font_sc true", 0)
2891         if k != -1:
2892             if roman == "utopia":
2893                 osf += "expert,"
2894             if roman == "palatino" and osf == "":
2895                 osf = "sc,"
2896             document.header[k] = "\\font_sc false"
2897         preamble = "\\usepackage["
2898         preamble += osf
2899         preamble += opts
2900         preamble += "]{" + package + "}"
2901         add_to_preamble(document, [preamble])
2902         del document.header[x]
2903
2904
2905 def convert_CantarellFont(document):
2906     " Handle Cantarell font definition to LaTeX "
2907
2908     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
2909         fm = createFontMapping(['Cantarell'])
2910         convert_fonts(document, fm, "oldstyle")
2911
2912 def revert_CantarellFont(document):
2913     " Revert native Cantarell font definition to LaTeX "
2914
2915     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
2916         fontmap = dict()
2917         fm = createFontMapping(['Cantarell'])
2918         if revert_fonts(document, fm, fontmap, False, True):
2919             add_preamble_fonts(document, fontmap)
2920
2921
2922 def convert_FiraFont(document):
2923     " Handle Fira font definition to LaTeX "
2924
2925     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
2926         fm = createFontMapping(['Fira'])
2927         convert_fonts(document, fm, "lf")
2928
2929 def revert_FiraFont(document):
2930     " Revert native Fira font definition to LaTeX "
2931
2932     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
2933         fontmap = dict()
2934         fm = createFontMapping(['Fira'])
2935         if revert_fonts(document, fm, fontmap, False, True):
2936             add_preamble_fonts(document, fontmap)
2937
2938
2939 ##
2940 # Conversion hub
2941 #
2942
2943 supported_versions = ["2.4.0", "2.4"]
2944 convert = [
2945            [545, [convert_lst_literalparam]],
2946            [546, []],
2947            [547, []],
2948            [548, []],
2949            [549, []],
2950            [550, [convert_fontenc]],
2951            [551, []],
2952            [552, []],
2953            [553, []],
2954            [554, []],
2955            [555, []],
2956            [556, []],
2957            [557, [convert_vcsinfo]],
2958            [558, [removeFrontMatterStyles]],
2959            [559, []],
2960            [560, []],
2961            [561, [convert_latexFonts]], # Handle dejavu, ibmplex fonts in GUI
2962            [562, []],
2963            [563, []],
2964            [564, []],
2965            [565, [convert_AdobeFonts]], # Handle adobe fonts in GUI
2966            [566, [convert_hebrew_parentheses]],
2967            [567, []],
2968            [568, []],
2969            [569, []],
2970            [570, []],
2971            [571, []],
2972            [572, [convert_notoFonts]],  # Added options thin, light, extralight for Noto
2973            [573, [convert_inputencoding_namechange]],
2974            [574, [convert_ruby_module, convert_utf8_japanese]],
2975            [575, [convert_lineno]],
2976            [576, []],
2977            [577, [convert_linggloss]],
2978            [578, []],
2979            [579, []],
2980            [580, []],
2981            [581, [convert_osf]],
2982            [582, [convert_AdobeFonts,convert_latexFonts,convert_notoFonts,convert_CantarellFont,convert_FiraFont]],# old font re-converterted due to extra options
2983           ]
2984
2985 revert =  [[581, [revert_CantarellFont,revert_FiraFont]],
2986            [580, [revert_texfontopts,revert_osf]],
2987            [579, [revert_minionpro, revert_plainNotoFonts_xopts, revert_notoFonts_xopts, revert_IBMFonts_xopts, revert_AdobeFonts_xopts, revert_font_opts]], # keep revert_font_opts last!
2988            [578, [revert_babelfont]],
2989            [577, [revert_drs]],
2990            [576, [revert_linggloss, revert_subexarg]],
2991            [575, [revert_new_languages]],
2992            [574, [revert_lineno]],
2993            [573, [revert_ruby_module, revert_utf8_japanese]],
2994            [572, [revert_inputencoding_namechange]],
2995            [571, [revert_notoFonts]],
2996            [570, [revert_cmidruletrimming]],
2997            [569, [revert_bibfileencodings]],
2998            [568, [revert_tablestyle]],
2999            [567, [revert_soul]],
3000            [566, [revert_malayalam]],
3001            [565, [revert_hebrew_parentheses]],
3002            [564, [revert_AdobeFonts]],
3003            [563, [revert_lformatinfo]],
3004            [562, [revert_listpargs]],
3005            [561, [revert_l7ninfo]],
3006            [560, [revert_latexFonts]], # Handle dejavu, ibmplex fonts in user preamble
3007            [559, [revert_timeinfo, revert_namenoextinfo]],
3008            [558, [revert_dateinfo]],
3009            [557, [addFrontMatterStyles]],
3010            [556, [revert_vcsinfo]],
3011            [555, [revert_bibencoding]],
3012            [554, [revert_vcolumns]],
3013            [553, [revert_stretchcolumn]],
3014            [552, [revert_tuftecite]],
3015            [551, [revert_floatpclass, revert_floatalignment]],
3016            [550, [revert_nospellcheck]],
3017            [549, [revert_fontenc]],
3018            [548, []],# dummy format change
3019            [547, [revert_lscape]],
3020            [546, [revert_xcharter]],
3021            [545, [revert_paratype]],
3022            [544, [revert_lst_literalparam]]
3023           ]
3024
3025
3026 if __name__ == "__main__":
3027     pass