]> git.lyx.org Git - features.git/blob - lib/lyx2lyx/lyx_2_4.py
Amend a25b9702, lyx2lyx: Corrected handling of system-font-specs
[features.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, find_end_of_inset, find_end_of_layout,
30                           find_token, find_re, get_bool_value, get_containing_layout,
31                           get_option_value, get_value, get_quoted_value)
32 #    del_token, del_value, del_complete_lines,
33 #    find_complete_lines, find_end_of,
34 #    find_re, find_substring, find_token_backwards,
35 #    get_containing_inset,
36 #    is_in_inset, set_bool_value
37 #    find_tokens, find_token_exact, check_token
38
39 from lyx2lyx_tools import (put_cmd_in_ert, add_to_preamble)
40 #  revert_font_attrs, insert_to_preamble, latex_length
41 #  get_ert, lyx2latex, lyx2verbatim, length_in_bp, convert_info_insets
42 #  revert_flex_inset, hex2ratio, str2bool
43
44 ####################################################################
45 # Private helper functions
46
47 def add_preamble_fonts(document, fontmap):
48     " Add collected font-packages with their option to user-preamble"
49
50     for pkg in fontmap:
51         if len(fontmap[pkg]) > 0:
52             xoption = "[" + ",".join(fontmap[pkg]) + "]"
53         else:
54             xoption = ""
55         preamble = "\\usepackage" + xoption + "{%s}" % pkg
56         add_to_preamble(document, [preamble])
57
58
59 def createkey(pkg, options):
60     options.sort()
61     return pkg + ':' + "-".join(options)
62
63 class fontinfo:
64     def __init__(self):
65         self.fontname = None    # key into font2pkgmap
66         self.fonttype = None    # roman,sans,typewriter,math
67         self.scaletype = None   # None,sf,tt
68         self.scaleopt = None    # None, 'scaled', 'scale'
69         self.scaleval = 1
70         self.package = None
71         self.options = []
72         self.pkgkey = None      # key into pkg2fontmap
73
74     def addkey(self):
75         self.pkgkey = createkey(self.package, self.options)
76
77 class fontmapping:
78     def __init__(self):
79         self.font2pkgmap = dict()
80         self.pkg2fontmap = dict()
81         self.pkginmap = dict()  # defines, if a map for package exists
82
83     def expandFontMapping(self, font_list, font_type, scale_type, pkg, scaleopt = None):
84         " Expand fontinfo mapping"
85         #
86         # fontlist:    list of fontnames, each element
87         #              may contain a ','-separated list of needed options
88         #              like e.g. 'IBMPlexSansCondensed,condensed'
89         # font_type:   one of 'roman', 'sans', 'typewriter', 'math'
90         # scale_type:  one of None, 'sf', 'tt'
91         # pkg:         package defining the font. Defaults to fontname if None
92         # scaleopt:    one of None, 'scale', 'scaled', or some other string
93         #              to be used in scale option (e.g. scaled=0.7)
94         for fl in font_list:
95             fe = fontinfo()
96             fe.fonttype = font_type
97             fe.scaletype = scale_type
98             flt = fl.split(",")
99             font_name = flt[0]
100             fe.fontname = font_name
101             fe.options = flt[1:]
102             fe.scaleopt = scaleopt
103             if pkg == None:
104                 fe.package = font_name
105             else:
106                 fe.package = pkg
107             fe.addkey()
108             self.font2pkgmap[font_name] = fe
109             if fe.pkgkey in self.pkg2fontmap:
110                 # Repeated the same entry? Check content
111                 if self.pkg2fontmap[fe.pkgkey] != font_name:
112                     document.error("Something is wrong in pkgname+options <-> fontname mapping")
113             self.pkg2fontmap[fe.pkgkey] = font_name
114             self.pkginmap[fe.package] = 1
115
116     def getfontname(self, pkg, options):
117         options.sort()
118         pkgkey = createkey(pkg, options)
119         if not pkgkey in self.pkg2fontmap:
120             return None
121         fontname = self.pkg2fontmap[pkgkey]
122         if not fontname in self.font2pkgmap:
123             document.error("Something is wrong in pkgname+options <-> fontname mapping")
124             return None
125         if pkgkey == self.font2pkgmap[fontname].pkgkey:
126             return fontname
127         return None
128
129 def createFontMapping(fontlist):
130     # Create info for known fonts for the use in
131     #   convert_latexFonts() and
132     #   revert_latexFonts()
133     #
134     # * Would be more handy to parse latexFonts file,
135     #   but the path to this file is unknown
136     # * For now, add DejaVu and IBMPlex only.
137     # * Expand, if desired
138     fm = fontmapping()
139     for font in fontlist:
140         if font == 'DejaVu':
141             fm.expandFontMapping(['DejaVuSerif', 'DejaVuSerifCondensed'], "roman", None, None)
142             fm.expandFontMapping(['DejaVuSans','DejaVuSansCondensed'], "sans", "sf", None, "scaled")
143             fm.expandFontMapping(['DejaVuSansMono'], "typewriter", "tt", None, "scaled")
144         elif font == 'IBM':
145             fm.expandFontMapping(['IBMPlexSerif', 'IBMPlexSerifThin,thin',
146                                   'IBMPlexSerifExtraLight,extralight', 'IBMPlexSerifLight,light',
147                                   'IBMPlexSerifSemibold,semibold'],
148                                  "roman", None, "plex-serif")
149             fm.expandFontMapping(['IBMPlexSans','IBMPlexSansCondensed,condensed',
150                                   'IBMPlexSansThin,thin', 'IBMPlexSansExtraLight,extralight',
151                                   'IBMPlexSansLight,light', 'IBMPlexSansSemibold,semibold'],
152                                  "sans", "sf", "plex-sans", "scale")
153             fm.expandFontMapping(['IBMPlexMono', 'IBMPlexMonoThin,thin',
154                                   'IBMPlexMonoExtraLight,extralight', 'IBMPlexMonoLight,light',
155                                   'IBMPlexMonoSemibold,semibold'],
156                                  "typewriter", "tt", "plex-mono", "scale")
157         elif font == 'Adobe':
158             fm.expandFontMapping(['ADOBESourceSerifPro'], "roman", None, "sourceserifpro")
159             fm.expandFontMapping(['ADOBESourceSansPro'], "sans", "sf", "sourcesanspro", "scaled")
160             fm.expandFontMapping(['ADOBESourceCodePro'], "typewriter", "tt", "sourcecodepro", "scaled")
161     return fm
162
163 def convert_fonts(document, fm):
164     " Handle font definition to LaTeX "
165
166     rpkg = re.compile(r'^\\usepackage(\[([^\]]*)\])?\{([^\}]+)\}')
167     rscaleopt = re.compile(r'^scaled?=(.*)')
168
169     i = 0
170     while i < len(document.preamble):
171         i = find_re(document.preamble, rpkg, i)
172         if i == -1:
173             return
174         mo = rpkg.search(document.preamble[i])
175         if mo == None or mo.group(2) == None:
176             options = []
177         else:
178             options = mo.group(2).replace(' ', '').split(",")
179         pkg = mo.group(3)
180         o = 0
181         oscale = 1
182         while o < len(options):
183             mo = rscaleopt.search(options[o])
184             if mo == None:
185                 o += 1
186                 continue
187             oscale = mo.group(1)
188             del options[o]
189             break
190
191         if not pkg in fm.pkginmap:
192             i += 1
193             continue
194         # determine fontname
195         fn = fm.getfontname(pkg, options)
196         if fn == None:
197             i += 1
198             continue
199         del document.preamble[i]
200         fontinfo = fm.font2pkgmap[fn]
201         if fontinfo.scaletype == None:
202             fontscale = None
203         else:
204             fontscale = "\\font_" + fontinfo.scaletype + "_scale"
205             fontinfo.scaleval = oscale
206
207         if i > 0 and document.preamble[i-1] == "% Added by lyx2lyx":
208             del document.preamble[i-1]
209         if fontscale != None:
210             j = find_token(document.header, fontscale, 0)
211             if j != -1:
212                 val = get_value(document.header, fontscale, j)
213                 vals = val.split()
214                 scale = "100"
215                 if oscale != None:
216                     scale = "%03d" % int(float(oscale) * 100)
217                 document.header[j] = fontscale + " " + scale + " " + vals[1]
218         ft = "\\font_" + fontinfo.fonttype
219         j = find_token(document.header, ft, 0)
220         if j != -1:
221             val = get_value(document.header, ft, j)
222             vals = val.split()
223             vals[0] = '"' + fn + '"'
224             document.header[j] = ft + ' ' + ' '.join(vals)
225
226 def revert_fonts(document, fm, fontmap):
227     " Revert native font definition to LaTeX "
228     # fonlist := list of fonts created from the same package
229     # Empty package means that the font-name is the same as the package-name
230     # fontmap (key = package, val += found options) will be filled
231     # and used later in add_preamble_fonts() to be added to user-preamble
232
233     rfontscale = re.compile(r'^\s*(\\font_(roman|sans|typewriter|math))\s+')
234     rscales = re.compile(r'^\s*(\d+)\s+(\d+)')
235     i = 0
236     while i < len(document.header):
237         i = find_re(document.header, rfontscale, i)
238         if (i == -1):
239             break
240         mo = rfontscale.search(document.header[i])
241         if mo == None:
242             i += 1
243             continue
244         ft = mo.group(1)    # 'roman', 'sans', 'typewriter', 'math'
245         val = get_value(document.header, ft, i)
246         words = val.split()
247         font = words[0].replace('"', '')
248         if not font in fm.font2pkgmap:
249             i += 1
250             continue
251         fontinfo = fm.font2pkgmap[font]
252         val = fontinfo.package
253         if not val in fontmap:
254             fontmap[val] = []
255         words[0] = '"default"'
256         document.header[i] = ft + ' ' + ' '.join(words) 
257         if fontinfo.scaleopt != None:
258             xval =  get_value(document.header, "\\font_" + fontinfo.scaletype + "_scale", 0)
259             mo = rscales.search(xval)
260             if mo != None:
261                 xval1 = mo.group(1)
262                 xval2 = mo.group(2)
263                 if xval1 != "100":
264                     # set correct scale option
265                     fontmap[val].extend([fontinfo.scaleopt + "=" + format(float(xval1) / 100, '.2f')])
266         if len(fontinfo.options) > 0:
267             fontmap[val].extend(fontinfo.options)
268         i += 1
269
270 ###############################################################################
271 ###
272 ### Conversion and reversion routines
273 ###
274 ###############################################################################
275
276 def convert_latexFonts(document):
277     " Handle DejaVu and IBMPlex fonts definition to LaTeX "
278
279     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
280         fm = createFontMapping(['DejaVu', 'IBM'])
281         convert_fonts(document, fm)
282
283 def revert_latexFonts(document):
284     " Revert native DejaVu font definition to LaTeX "
285
286     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
287         fontmap = dict()
288         fm = createFontMapping(['DejaVu', 'IBM'])
289         revert_fonts(document, fm, fontmap)
290         add_preamble_fonts(document, fontmap)
291
292 def convert_AdobeFonts(document):
293     " Handle DejaVu and IBMPlex fonts definition to LaTeX "
294
295     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
296         fm = createFontMapping(['Adobe'])
297         convert_fonts(document, fm)
298
299 def revert_AdobeFonts(document):
300     " Revert native DejaVu font definition to LaTeX "
301
302     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
303         fontmap = dict()
304         fm = createFontMapping(['Adobe'])
305         revert_fonts(document, fm, fontmap)
306         add_preamble_fonts(document, fontmap)
307
308 def removeFrontMatterStyles(document):
309     " Remove styles Begin/EndFrontmatter"
310
311     layouts = ['BeginFrontmatter', 'EndFrontmatter']
312     for layout in layouts:
313         i = 0
314         while True:
315             i = find_token(document.body, '\\begin_layout ' + layout, i)
316             if i == -1:
317                 break
318             j = find_end_of_layout(document.body, i)
319             if j == -1:
320                 document.warning("Malformed LyX document: Can't find end of layout at line %d" % i)
321                 i += 1
322                 continue
323             while i > 0 and document.body[i-1].strip() == '':
324                 i -= 1
325             while document.body[j+1].strip() == '':
326                 j = j + 1
327             document.body[i:j+1] = ['']
328
329 def addFrontMatterStyles(document):
330     " Use styles Begin/EndFrontmatter for elsarticle"
331
332     def insertFrontmatter(prefix, line):
333         above = line
334         while above > 0 and document.body[above-1].strip() == '':
335             above -= 1
336         below = line
337         while document.body[below].strip() == '':
338             below += 1
339         document.body[above:below] = ['', '\\begin_layout ' + prefix + 'Frontmatter',
340                                     '\\begin_inset Note Note',
341                                     'status open', '',
342                                     '\\begin_layout Plain Layout',
343                                     'Keep this empty!',
344                                     '\\end_layout', '',
345                                     '\\end_inset', '', '',
346                                     '\\end_layout', '']
347
348     if document.textclass == "elsarticle":
349         layouts = ['Title', 'Title footnote', 'Author', 'Author footnote',
350                    'Corresponding author', 'Address', 'Email', 'Abstract', 'Keywords']
351         first = -1
352         last = -1
353         for layout in layouts:
354             i = 0
355             while True:
356                 i = find_token(document.body, '\\begin_layout ' + layout, i)
357                 if i == -1:
358                     break
359                 k = find_end_of_layout(document.body, i)
360                 if k == -1:
361                     document.warning("Malformed LyX document: Can't find end of layout at line %d" % i)
362                     i += 1;
363                     continue
364                 if first == -1 or i < first:
365                     first = i
366                 if last == -1 or last <= k:
367                     last = k+1
368                 i = k+1
369         if first == -1:
370             return
371         insertFrontmatter('End', last)
372         insertFrontmatter('Begin', first)
373
374 def convert_lst_literalparam(document):
375     " Add param literal to include inset "
376
377     i = 0
378     while True:
379         i = find_token(document.body, '\\begin_inset CommandInset include', i)
380         if i == -1:
381             break
382         j = find_end_of_inset(document.body, i)
383         if j == -1:
384             document.warning("Malformed LyX document: Can't find end of command inset at line %d" % i)
385             i += 1
386             continue
387         while i < j and document.body[i].strip() != '':
388             i += 1
389         document.body.insert(i, "literal \"true\"")
390
391
392 def revert_lst_literalparam(document):
393     " Remove param literal from include inset "
394
395     i = 0
396     while True:
397         i = find_token(document.body, '\\begin_inset CommandInset include', i)
398         if i == -1:
399             break
400         j = find_end_of_inset(document.body, i)
401         if j == -1:
402             document.warning("Malformed LyX document: Can't find end of include inset at line %d" % i)
403             i += 1
404             continue
405         k = find_token(document.body, 'literal', i, j)
406         if k == -1:
407             i += 1
408             continue
409         del document.body[k]
410
411
412 def revert_paratype(document):
413     " Revert ParaType font definitions to LaTeX "
414
415     if find_token(document.header, "\\use_non_tex_fonts false", 0) != -1:
416         preamble = ""
417         i1 = find_token(document.header, "\\font_roman \"PTSerif-TLF\"", 0)
418         i2 = find_token(document.header, "\\font_sans \"default\"", 0)
419         i3 = find_token(document.header, "\\font_typewriter \"default\"", 0)
420         j = find_token(document.header, "\\font_sans \"PTSans-TLF\"", 0)
421         sfval = get_value(document.header, "\\font_sf_scale", 0)
422         # cutoff " 100"
423         sfval = sfval[:-4]
424         sfoption = ""
425         if sfval != "100":
426             sfoption = "scaled=" + format(float(sfval) / 100, '.2f')
427         k = find_token(document.header, "\\font_typewriter \"PTMono-TLF\"", 0)
428         ttval = get_value(document.header, "\\font_tt_scale", 0)
429         # cutoff " 100"
430         ttval = ttval[:-4]
431         ttoption = ""
432         if ttval != "100":
433             ttoption = "scaled=" + format(float(ttval) / 100, '.2f')
434         if i1 != -1 and i2 != -1 and i3!= -1:
435             add_to_preamble(document, ["\\usepackage{paratype}"])
436         else:
437             if i1!= -1:
438                 add_to_preamble(document, ["\\usepackage{PTSerif}"])
439                 document.header[i1] = document.header[i1].replace("PTSerif-TLF", "default")
440             if j!= -1:
441                 if sfoption != "":
442                     add_to_preamble(document, ["\\usepackage[" + sfoption + "]{PTSans}"])
443                 else:
444                     add_to_preamble(document, ["\\usepackage{PTSans}"])
445                 document.header[j] = document.header[j].replace("PTSans-TLF", "default")
446             if k!= -1:
447                 if ttoption != "":
448                     add_to_preamble(document, ["\\usepackage[" + ttoption + "]{PTMono}"])
449                 else:
450                     add_to_preamble(document, ["\\usepackage{PTMono}"])
451                 document.header[k] = document.header[k].replace("PTMono-TLF", "default")
452
453
454 def revert_xcharter(document):
455     " Revert XCharter font definitions to LaTeX "
456
457     i = find_token(document.header, "\\font_roman \"xcharter\"", 0)
458     if i == -1:
459         return
460
461     # replace unsupported font setting
462     document.header[i] = document.header[i].replace("xcharter", "default")
463     # no need for preamble code with system fonts
464     if get_bool_value(document.header, "\\use_non_tex_fonts"):
465         return
466
467     # transfer old style figures setting to package options
468     j = find_token(document.header, "\\font_osf true")
469     if j != -1:
470         options = "[osf]"
471         document.header[j] = "\\font_osf false"
472     else:
473         options = ""
474     if i != -1:
475         add_to_preamble(document, ["\\usepackage%s{XCharter}"%options])
476
477
478 def revert_lscape(document):
479     " Reverts the landscape environment (Landscape module) to TeX-code "
480
481     if not "landscape" in document.get_module_list():
482         return
483
484     i = 0
485     while True:
486         i = find_token(document.body, "\\begin_inset Flex Landscape", i)
487         if i == -1:
488             return
489         j = find_end_of_inset(document.body, i)
490         if j == -1:
491             document.warning("Malformed LyX document: Can't find end of Landscape inset")
492             i += 1
493             continue
494
495         if document.body[i] == "\\begin_inset Flex Landscape (Floating)":
496             document.body[j - 2 : j + 1] = put_cmd_in_ert("\\end{landscape}}")
497             document.body[i : i + 4] = put_cmd_in_ert("\\afterpage{\\begin{landscape}")
498             add_to_preamble(document, ["\\usepackage{afterpage}"])
499         else:
500             document.body[j - 2 : j + 1] = put_cmd_in_ert("\\end{landscape}")
501             document.body[i : i + 4] = put_cmd_in_ert("\\begin{landscape}")
502
503         add_to_preamble(document, ["\\usepackage{pdflscape}"])
504         # no need to reset i
505
506
507 def convert_fontenc(document):
508     " Convert default fontenc setting "
509
510     i = find_token(document.header, "\\fontencoding global", 0)
511     if i == -1:
512         return
513
514     document.header[i] = document.header[i].replace("global", "auto")
515
516
517 def revert_fontenc(document):
518     " Revert default fontenc setting "
519
520     i = find_token(document.header, "\\fontencoding auto", 0)
521     if i == -1:
522         return
523
524     document.header[i] = document.header[i].replace("auto", "global")
525
526
527 def revert_nospellcheck(document):
528     " Remove nospellcheck font info param "
529
530     i = 0
531     while True:
532         i = find_token(document.body, '\\nospellcheck', i)
533         if i == -1:
534             return
535         del document.body[i]
536
537
538 def revert_floatpclass(document):
539     " Remove float placement params 'document' and 'class' "
540
541     i = 0
542     i = find_token(document.header, "\\float_placement class", 0)
543     if i != -1:
544         del document.header[i]
545
546     i = 0
547     while True:
548         i = find_token(document.body, '\\begin_inset Float', i)
549         if i == -1:
550             break
551         j = find_end_of_inset(document.body, i)
552         k = find_token(document.body, 'placement class', i, i + 2)
553         if k == -1:
554             k = find_token(document.body, 'placement document', i, i + 2)
555             if k != -1:
556                 del document.body[k]
557             i += 1
558             continue
559         del document.body[k]
560
561
562 def revert_floatalignment(document):
563     " Remove float alignment params "
564
565     i = 0
566     i = find_token(document.header, "\\float_alignment", 0)
567     galignment = ""
568     if i != -1:
569         galignment = get_value(document.header, "\\float_alignment", i)
570         del document.header[i]
571
572     i = 0
573     while True:
574         i = find_token(document.body, '\\begin_inset Float', i)
575         if i == -1:
576             break
577         j = find_end_of_inset(document.body, i)
578         if j == -1:
579             document.warning("Malformed LyX document: Can't find end of inset at line " + str(i))
580             i += 1
581         k = find_token(document.body, 'alignment', i, i + 4)
582         if k == -1:
583             i = j
584             continue
585         alignment = get_value(document.body, "alignment", k)
586         if alignment == "document":
587             alignment = galignment
588         del document.body[k]
589         l = find_token(document.body, "\\begin_layout Plain Layout", i, j)
590         if l == -1:
591             document.warning("Can't find float layout!")
592             i += 1
593             continue
594         alcmd = []
595         if alignment == "left":
596             alcmd = put_cmd_in_ert("\\raggedright{}")
597         elif alignment == "center":
598             alcmd = put_cmd_in_ert("\\centering{}")
599         elif alignment == "right":
600             alcmd = put_cmd_in_ert("\\raggedleft{}")
601         if len(alcmd) > 0:
602             document.body[l+1:l+1] = alcmd
603         i += 1 
604
605
606 def revert_tuftecite(document):
607     " Revert \cite commands in tufte classes "
608
609     tufte = ["tufte-book", "tufte-handout"]
610     if document.textclass not in tufte:
611         return
612
613     i = 0
614     while (True):
615         i = find_token(document.body, "\\begin_inset CommandInset citation", i)
616         if i == -1:
617             break
618         j = find_end_of_inset(document.body, i)
619         if j == -1:
620             document.warning("Can't find end of citation inset at line %d!!" %(i))
621             i += 1
622             continue
623         k = find_token(document.body, "LatexCommand", i, j)
624         if k == -1:
625             document.warning("Can't find LatexCommand for citation inset at line %d!" %(i))
626             i = j + 1
627             continue
628         cmd = get_value(document.body, "LatexCommand", k)
629         if cmd != "cite":
630             i = j + 1
631             continue
632         pre = get_quoted_value(document.body, "before", i, j)
633         post = get_quoted_value(document.body, "after", i, j)
634         key = get_quoted_value(document.body, "key", i, j)
635         if not key:
636             document.warning("Citation inset at line %d does not have a key!" %(i))
637             key = "???"
638         # Replace command with ERT
639         res = "\\cite"
640         if pre:
641             res += "[" + pre + "]"
642         if post:
643             res += "[" + post + "]"
644         elif pre:
645             res += "[]"
646         res += "{" + key + "}"
647         document.body[i:j+1] = put_cmd_in_ert([res])
648         i = j + 1
649
650
651 def revert_stretchcolumn(document):
652     " We remove the column varwidth flags or everything else will become a mess. "
653     i = 0
654     while True:
655         i = find_token(document.body, "\\begin_inset Tabular", i)
656         if i == -1:
657             return
658         j = find_end_of_inset(document.body, i + 1)
659         if j == -1:
660             document.warning("Malformed LyX document: Could not find end of tabular.")
661             continue
662         for k in range(i, j):
663             if re.search('^<column.*varwidth="[^"]+".*>$', document.body[k]):
664                 document.warning("Converting 'tabularx'/'xltabular' table to normal table.")
665                 document.body[k] = document.body[k].replace(' varwidth="true"', '')
666         i = i + 1
667
668
669 def revert_vcolumns(document):
670     " Revert standard columns with line breaks etc. "
671     i = 0
672     needvarwidth = False
673     needarray = False
674     try:
675         while True:
676             i = find_token(document.body, "\\begin_inset Tabular", i)
677             if i == -1:
678                 return
679             j = find_end_of_inset(document.body, i)
680             if j == -1:
681                 document.warning("Malformed LyX document: Could not find end of tabular.")
682                 i += 1
683                 continue
684
685             # Collect necessary column information
686             m = i + 1
687             nrows = int(document.body[i+1].split('"')[3])
688             ncols = int(document.body[i+1].split('"')[5])
689             col_info = []
690             for k in range(ncols):
691                 m = find_token(document.body, "<column", m)
692                 width = get_option_value(document.body[m], 'width')
693                 varwidth = get_option_value(document.body[m], 'varwidth')
694                 alignment = get_option_value(document.body[m], 'alignment')
695                 special = get_option_value(document.body[m], 'special')
696                 col_info.append([width, varwidth, alignment, special, m])
697
698             # Now parse cells
699             m = i + 1
700             lines = []
701             for row in range(nrows):
702                 for col in range(ncols):
703                     m = find_token(document.body, "<cell", m)
704                     multicolumn = get_option_value(document.body[m], 'multicolumn')
705                     multirow = get_option_value(document.body[m], 'multirow')
706                     width = get_option_value(document.body[m], 'width')
707                     rotate = get_option_value(document.body[m], 'rotate')
708                     # Check for: linebreaks, multipars, non-standard environments
709                     begcell = m
710                     endcell = find_token(document.body, "</cell>", begcell)
711                     vcand = False
712                     if find_token(document.body, "\\begin_inset Newline", begcell, endcell) != -1:
713                         vcand = True
714                     elif count_pars_in_inset(document.body, begcell + 2) > 1:
715                         vcand = True
716                     elif get_value(document.body, "\\begin_layout", begcell) != "Plain Layout":
717                         vcand = True
718                     if vcand and rotate == "" and ((multicolumn == "" and multirow == "") or width == ""):
719                         if col_info[col][0] == "" and col_info[col][1] == "" and col_info[col][3] == "":
720                             needvarwidth = True
721                             alignment = col_info[col][2]
722                             col_line = col_info[col][4]
723                             vval = ""
724                             if alignment == "center":
725                                 vval = ">{\\centering}"
726                             elif  alignment == "left":
727                                 vval = ">{\\raggedright}"
728                             elif alignment == "right":
729                                 vval = ">{\\raggedleft}"
730                             if vval != "":
731                                 needarray = True
732                             vval += "V{\\linewidth}"
733                 
734                             document.body[col_line] = document.body[col_line][:-1] + " special=\"" + vval + "\">"
735                             # ERT newlines and linebreaks (since LyX < 2.4 automatically inserts parboxes
736                             # with newlines, and we do not want that)
737                             while True:
738                                 endcell = find_token(document.body, "</cell>", begcell)
739                                 linebreak = False
740                                 nl = find_token(document.body, "\\begin_inset Newline newline", begcell, endcell)
741                                 if nl == -1:
742                                     nl = find_token(document.body, "\\begin_inset Newline linebreak", begcell, endcell)
743                                     if nl == -1:
744                                          break
745                                     linebreak = True
746                                 nle = find_end_of_inset(document.body, nl)
747                                 del(document.body[nle:nle+1])
748                                 if linebreak:
749                                     document.body[nl:nl+1] = put_cmd_in_ert("\\linebreak{}")
750                                 else:
751                                     document.body[nl:nl+1] = put_cmd_in_ert("\\\\")
752                     m += 1
753
754             i = j + 1
755
756     finally:
757         if needarray == True:
758             add_to_preamble(document, ["\\usepackage{array}"])
759         if needvarwidth == True:
760             add_to_preamble(document, ["\\usepackage{varwidth}"])
761
762
763 def revert_bibencoding(document):
764     " Revert bibliography encoding "
765
766     # Get cite engine
767     engine = "basic"
768     i = find_token(document.header, "\\cite_engine", 0)
769     if i == -1:
770         document.warning("Malformed document! Missing \\cite_engine")
771     else:
772         engine = get_value(document.header, "\\cite_engine", i)
773
774     # Check if biblatex
775     biblatex = False
776     if engine in ["biblatex", "biblatex-natbib"]:
777         biblatex = True
778
779     # Map lyx to latex encoding names 
780     encodings = {
781         "utf8" : "utf8",
782         "utf8x" : "utf8x",
783         "armscii8" : "armscii8",
784         "iso8859-1" : "latin1",
785         "iso8859-2" : "latin2",
786         "iso8859-3" : "latin3",
787         "iso8859-4" : "latin4",
788         "iso8859-5" : "iso88595",
789         "iso8859-6" : "8859-6",
790         "iso8859-7" : "iso-8859-7",
791         "iso8859-8" : "8859-8",
792         "iso8859-9" : "latin5",
793         "iso8859-13" : "latin7",
794         "iso8859-15" : "latin9",
795         "iso8859-16" : "latin10",
796         "applemac" : "applemac",
797         "cp437" : "cp437",
798         "cp437de" : "cp437de",
799         "cp850" : "cp850",
800         "cp852" : "cp852",
801         "cp855" : "cp855",
802         "cp858" : "cp858",
803         "cp862" : "cp862",
804         "cp865" : "cp865",
805         "cp866" : "cp866",
806         "cp1250" : "cp1250",
807         "cp1251" : "cp1251",
808         "cp1252" : "cp1252",
809         "cp1255" : "cp1255",
810         "cp1256" : "cp1256",
811         "cp1257" : "cp1257",
812         "koi8-r" : "koi8-r",
813         "koi8-u" : "koi8-u",
814         "pt154" : "pt154",
815         "utf8-platex" : "utf8",
816         "ascii" : "ascii"
817     }
818
819     i = 0
820     bibresources = []
821     while (True):
822         i = find_token(document.body, "\\begin_inset CommandInset bibtex", i)
823         if i == -1:
824             break
825         j = find_end_of_inset(document.body, i)
826         if j == -1:
827             document.warning("Can't find end of bibtex inset at line %d!!" %(i))
828             i += 1
829             continue
830         encoding = get_quoted_value(document.body, "encoding", i, j)
831         if not encoding:
832             i += 1
833             continue
834         # remove encoding line
835         k = find_token(document.body, "encoding", i, j)
836         if k != -1:
837             del document.body[k]
838         # Re-find inset end line
839         j = find_end_of_inset(document.body, i)
840         if biblatex:
841             biblio_options = ""
842             h = find_token(document.header, "\\biblio_options", 0)
843             if h != -1:
844                 biblio_options = get_value(document.header, "\\biblio_options", h)
845                 if not "bibencoding" in biblio_options:
846                      document.header[h] += ",bibencoding=%s" % encodings[encoding]
847             else:
848                 bs = find_token(document.header, "\\biblatex_bibstyle", 0)
849                 if bs == -1:
850                     # this should not happen
851                     document.warning("Malformed LyX document! No \\biblatex_bibstyle header found!")
852                 else:
853                     document.header[bs-1 : bs-1] = ["\\biblio_options bibencoding=" + encodings[encoding]]
854         else:
855             document.body[j+1:j+1] = put_cmd_in_ert("\\egroup")
856             document.body[i:i] = put_cmd_in_ert("\\bgroup\\inputencoding{" + encodings[encoding] + "}")
857
858         i = j + 1
859
860
861
862 def convert_vcsinfo(document):
863     " Separate vcs Info inset from buffer Info inset. "
864
865     types = {
866         "vcs-revision" : "revision",
867         "vcs-tree-revision" : "tree-revision",
868         "vcs-author" : "author",
869         "vcs-time" : "time",
870         "vcs-date" : "date"
871     }
872     i = 0
873     while True:
874         i = find_token(document.body, "\\begin_inset Info", i)
875         if i == -1:
876             return
877         j = find_end_of_inset(document.body, i + 1)
878         if j == -1:
879             document.warning("Malformed LyX document: Could not find end of Info inset.")
880             i = i + 1
881             continue
882         tp = find_token(document.body, 'type', i, j)
883         tpv = get_quoted_value(document.body, "type", tp)
884         if tpv != "buffer":
885             i = i + 1
886             continue
887         arg = find_token(document.body, 'arg', i, j)
888         argv = get_quoted_value(document.body, "arg", arg)
889         if argv not in list(types.keys()):
890             i = i + 1
891             continue
892         document.body[tp] = "type \"vcs\""
893         document.body[arg] = "arg \"" + types[argv] + "\""
894         i = i + 1
895
896
897 def revert_vcsinfo(document):
898     " Merge vcs Info inset to buffer Info inset. "
899
900     args = ["revision", "tree-revision", "author", "time", "date" ]
901     i = 0
902     while True:
903         i = find_token(document.body, "\\begin_inset Info", i)
904         if i == -1:
905             return
906         j = find_end_of_inset(document.body, i + 1)
907         if j == -1:
908             document.warning("Malformed LyX document: Could not find end of Info inset.")
909             i = i + 1
910             continue
911         tp = find_token(document.body, 'type', i, j)
912         tpv = get_quoted_value(document.body, "type", tp)
913         if tpv != "vcs":
914             i = i + 1
915             continue
916         arg = find_token(document.body, 'arg', i, j)
917         argv = get_quoted_value(document.body, "arg", arg)
918         if argv not in args:
919             document.warning("Malformed Info inset. Invalid vcs arg.")
920             i = i + 1
921             continue
922         document.body[tp] = "type \"buffer\""
923         document.body[arg] = "arg \"vcs-" + argv + "\""
924         i = i + 1
925
926
927 def revert_dateinfo(document):
928     " Revert date info insets to static text. "
929
930 # FIXME This currently only considers the main language and uses the system locale
931 # Ideally, it should honor context languages and switch the locale accordingly.
932
933     # The date formats for each language using strftime syntax:
934     # long, short, loclong, locmedium, locshort
935     dateformats = {
936         "afrikaans" : ["%A, %d %B %Y", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y/%m/%d"],
937         "albanian" : ["%A, %d %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
938         "american" : ["%A, %B %d, %Y", "%m/%d/%y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
939         "amharic" : ["%A ፣%d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
940         "ancientgreek" : ["%A, %d %B %Y", "%d %b %Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
941         "arabic_arabi" : ["%A، %d %B، %Y", "%d‏/%m‏/%Y", "%d %B، %Y", "%d/%m/%Y", "%d/%m/%Y"],
942         "arabic_arabtex" : ["%A، %d %B، %Y", "%d‏/%m‏/%Y", "%d %B، %Y", "%d/%m/%Y", "%d/%m/%Y"],
943         "armenian" : ["%Y թ. %B %d, %A", "%d.%m.%y", "%d %B، %Y", "%d %b، %Y", "%d/%m/%Y"],
944         "asturian" : ["%A, %d %B de %Y", "%d/%m/%y", "%d de %B de %Y", "%d %b %Y", "%d/%m/%Y"],
945         "australian" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
946         "austrian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
947         "bahasa" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
948         "bahasam" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
949         "basque" : ["%Y(e)ko %B %d, %A", "%y/%m/%d", "%Y %B %d", "%Y %b %d", "%Y/%m/%d"],
950         "belarusian" : ["%A, %d %B %Y г.", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
951         "bosnian" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%Y-%m-%d"],
952         "brazilian" : ["%A, %d de %B de %Y", "%d/%m/%Y", "%d de %B de %Y", "%d de %b de %Y", "%d/%m/%Y"],
953         "breton" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
954         "british" : ["%A, %d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
955         "bulgarian" : ["%A, %d %B %Y г.", "%d.%m.%y г.", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
956         "canadian" : ["%A, %B %d, %Y", "%Y-%m-%d", "%B %d, %Y", "%d %b %Y", "%Y-%m-%d"],
957         "canadien" : ["%A %d %B %Y", "%y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
958         "catalan" : ["%A, %d %B de %Y", "%d/%m/%y", "%d / %B / %Y", "%d / %b / %Y", "%d/%m/%Y"],
959         "chinese-simplified" : ["%Y年%m月%d日%A", "%Y/%m/%d", "%Y年%m月%d日", "%Y-%m-%d", "%y-%m-%d"],
960         "chinese-traditional" : ["%Y年%m月%d日 %A", "%Y/%m/%d", "%Y年%m月%d日", "%Y年%m月%d日", "%y年%m月%d日"],
961         "coptic" : ["%A, %d %B %Y", "%d %b %Y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
962         "croatian" : ["%A, %d. %B %Y.", "%d. %m. %Y.", "%d. %B %Y.", "%d. %b. %Y.", "%d.%m.%Y."],
963         "czech" : ["%A %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b. %Y", "%d.%m.%Y"],
964         "danish" : ["%A den %d. %B %Y", "%d/%m/%Y", "%d. %B %Y", "%d. %b %Y", "%d/%m/%Y"],
965         "divehi" : ["%Y %B %d, %A", "%Y-%m-%d", "%Y %B %d", "%Y %b %d", "%d/%m/%Y"],
966         "dutch" : ["%A %d %B %Y", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
967         "english" : ["%A, %B %d, %Y", "%m/%d/%y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
968         "esperanto" : ["%A, %d %B %Y", "%d %b %Y", "la %d de %B %Y", "la %d de %b %Y", "%m/%d/%Y"],
969         "estonian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
970         "farsi" : ["%A %d %B %Y", "%Y/%m/%d", "%d %B %Y", "%d %b %Y", "%Y/%m/%d"],
971         "finnish" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
972         "french" : ["%A %d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
973         "friulan" : ["%A %d di %B dal %Y", "%d/%m/%y", "%d di %B dal %Y", "%d di %b dal %Y", "%d/%m/%Y"],
974         "galician" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B de %Y", "%d de %b de %Y", "%d/%m/%Y"],
975         "georgian" : ["%A, %d %B, %Y", "%d.%m.%y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
976         "german" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
977         "german-ch" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
978         "german-ch-old" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
979         "greek" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
980         "hebrew" : ["%A, %d ב%B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
981         "hindi" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
982         "icelandic" : ["%A, %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
983         "interlingua" : ["%Y %B %d, %A", "%Y-%m-%d", "le %d de %B %Y", "le %d de %b %Y", "%Y-%m-%d"],
984         "irish" : ["%A %d %B %Y", "%d/%m/%Y", "%d. %B %Y", "%d. %b %Y", "%d/%m/%Y"],
985         "italian" : ["%A %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d/%b/%Y", "%d/%m/%Y"],
986         "japanese" : ["%Y年%m月%d日%A", "%Y/%m/%d", "%Y年%m月%d日", "%Y/%m/%d", "%y/%m/%d"],
987         "japanese-cjk" : ["%Y年%m月%d日%A", "%Y/%m/%d", "%Y年%m月%d日", "%Y/%m/%d", "%y/%m/%d"],
988         "kannada" : ["%A, %B %d, %Y", "%d/%m/%y", "%d %B %Y", "%d %B %Y", "%d-%m-%Y"],
989         "kazakh" : ["%Y ж. %d %B, %A", "%d.%m.%y", "%d %B %Y", "%d %B %Y", "%Y-%d-%m"],
990         "khmer" : ["%A %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %B %Y", "%d/%m/%Y"],
991         "korean" : ["%Y년 %m월 %d일 %A", "%y. %m. %d.", "%Y년 %m월 %d일", "%Y. %m. %d.", "%y. %m. %d."],
992         "kurmanji" : ["%A, %d %B %Y", "%d %b %Y", "%d. %B %Y", "%d. %m. %Y", "%Y-%m-%d"],
993         "lao" : ["%A ທີ %d %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %B %Y", "%d/%m/%Y"],
994         "latin" : ["%A, %d %B %Y", "%d %b %Y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
995         "latvian" : ["%A, %Y. gada %d. %B", "%d.%m.%y", "%Y. gada %d. %B", "%Y. gada %d. %b", "%d.%m.%Y"],
996         "lithuanian" : ["%Y m. %B %d d., %A", "%Y-%m-%d", "%Y m. %B %d d.", "%Y m. %B %d d.", "%Y-%m-%d"],
997         "lowersorbian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
998         "macedonian" : ["%A, %d %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
999         "magyar" : ["%Y. %B %d., %A", "%Y. %m. %d.", "%Y. %B %d.", "%Y. %b %d.", "%Y.%m.%d."],
1000         "marathi" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1001         "mongolian" : ["%A, %Y оны %m сарын %d", "%Y-%m-%d", "%Y оны %m сарын %d", "%d-%m-%Y", "%d-%m-%Y"],
1002         "naustrian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1003         "newzealand" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1004         "ngerman" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1005         "norsk" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1006         "nynorsk" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1007         "occitan" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1008         "piedmontese" : ["%A, %d %B %Y", "%d %b %Y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1009         "polish" : ["%A, %d %B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1010         "polutonikogreek" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1011         "portuguese" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B de %Y", "%d de %b de %Y", "%Y/%m/%d"],
1012         "romanian" : ["%A, %d %B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1013         "romansh" : ["%A, ils %d da %B %Y", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1014         "russian" : ["%A, %d %B %Y г.", "%d.%m.%Y", "%d %B %Y г.", "%d %b %Y г.", "%d.%m.%Y"],
1015         "samin" : ["%Y %B %d, %A", "%Y-%m-%d", "%B %d. b. %Y", "%b %d. b. %Y", "%d.%m.%Y"],
1016         "sanskrit" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1017         "scottish" : ["%A, %dmh %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1018         "serbian" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1019         "serbian-latin" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1020         "slovak" : ["%A, %d. %B %Y", "%d. %m. %Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1021         "slovene" : ["%A, %d. %B %Y", "%d. %m. %y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1022         "spanish" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B %de %Y", "%d %b %Y", "%d/%m/%Y"],
1023         "spanish-mexico" : ["%A, %d de %B %de %Y", "%d/%m/%y", "%d de %B de %Y", "%d %b %Y", "%d/%m/%Y"],
1024         "swedish" : ["%A %d %B %Y", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1025         "syriac" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1026         "tamil" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1027         "telugu" : ["%d, %B %Y, %A", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1028         "thai" : ["%Aที่ %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1029         "tibetan" : ["%Y %Bའི་ཚེས་%d, %A", "%Y-%m-%d", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1030         "turkish" : ["%d %B %Y %A", "%d.%m.%Y", "%d %B %Y", "%d.%b.%Y", "%d.%m.%Y"],
1031         "turkmen" : ["%d %B %Y %A", "%d.%m.%Y", "%Y ý. %B %d", "%d.%m.%Y ý.", "%d.%m.%y ý."],
1032         "ukrainian" : ["%A, %d %B %Y р.", "%d.%m.%y", "%d %B %Y", "%d %m %Y", "%d.%m.%Y"],
1033         "uppersorbian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1034         "urdu" : ["%A، %d %B، %Y", "%d/%m/%y", "%d %B, %Y", "%d %b %Y", "%d/%m/%Y"],
1035         "vietnamese" : ["%A, %d %B, %Y", "%d/%m/%Y", "%d tháng %B %Y", "%d-%m-%Y", "%d/%m/%Y"],
1036         "welsh" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1037     }
1038
1039     types = ["date", "fixdate", "moddate" ]
1040     i = 0
1041     i = find_token(document.header, "\\language", 0)
1042     if i == -1:
1043         # this should not happen
1044         document.warning("Malformed LyX document! No \\language header found!")
1045         return
1046     lang = get_value(document.header, "\\language", i)
1047
1048     i = 0
1049     while True:
1050         i = find_token(document.body, "\\begin_inset Info", i)
1051         if i == -1:
1052             return
1053         j = find_end_of_inset(document.body, i + 1)
1054         if j == -1:
1055             document.warning("Malformed LyX document: Could not find end of Info inset.")
1056             i = i + 1
1057             continue
1058         tp = find_token(document.body, 'type', i, j)
1059         tpv = get_quoted_value(document.body, "type", tp)
1060         if tpv not in types:
1061             i = i + 1
1062             continue
1063         arg = find_token(document.body, 'arg', i, j)
1064         argv = get_quoted_value(document.body, "arg", arg)
1065         isodate = ""
1066         dte = date.today()
1067         if tpv == "fixdate":
1068             datecomps = argv.split('@')
1069             if len(datecomps) > 1:
1070                 argv = datecomps[0]
1071                 isodate = datecomps[1]
1072                 m = re.search('(\d\d\d\d)-(\d\d)-(\d\d)', isodate)
1073                 if m:
1074                     dte = date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
1075 # FIXME if we had the path to the original document (not the one in the tmp dir),
1076 #        we could use the mtime.
1077 #        elif tpv == "moddate":
1078 #            dte = date.fromtimestamp(os.path.getmtime(document.dir))
1079         result = ""
1080         if argv == "ISO":
1081             result = dte.isodate()
1082         elif argv == "long":
1083             result = dte.strftime(dateformats[lang][0])
1084         elif argv == "short":
1085             result = dte.strftime(dateformats[lang][1])
1086         elif argv == "loclong":
1087             result = dte.strftime(dateformats[lang][2])
1088         elif argv == "locmedium":
1089             result = dte.strftime(dateformats[lang][3])
1090         elif argv == "locshort":
1091             result = dte.strftime(dateformats[lang][4])
1092         else:
1093             fmt = argv.replace("MMMM", "%b").replace("MMM", "%b").replace("MM", "%m").replace("M", "%m")
1094             fmt = fmt.replace("yyyy", "%Y").replace("yy", "%y")
1095             fmt = fmt.replace("dddd", "%A").replace("ddd", "%a").replace("dd", "%d")
1096             fmt = re.sub('[^\'%]d', '%d', fmt)
1097             fmt = fmt.replace("'", "")
1098             result = dte.strftime(fmt)
1099         if sys.version_info < (3,0):
1100             # In Python 2, datetime module works with binary strings,
1101             # our dateformat strings are utf8-encoded:
1102             result = result.decode('utf-8')
1103         document.body[i : j+1] = result
1104         i = i + 1
1105
1106
1107 def revert_timeinfo(document):
1108     " Revert time info insets to static text. "
1109
1110 # FIXME This currently only considers the main language and uses the system locale
1111 # Ideally, it should honor context languages and switch the locale accordingly.
1112 # Also, the time object is "naive", i.e., it does not know of timezones (%Z will
1113 # be empty).
1114
1115     # The time formats for each language using strftime syntax:
1116     # long, short
1117     timeformats = {
1118         "afrikaans" : ["%H:%M:%S %Z", "%H:%M"],
1119         "albanian" : ["%I:%M:%S %p, %Z", "%I:%M %p"],
1120         "american" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1121         "amharic" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1122         "ancientgreek" : ["%H:%M:%S %Z", "%H:%M:%S"],
1123         "arabic_arabi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1124         "arabic_arabtex" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1125         "armenian" : ["%H:%M:%S %Z", "%H:%M"],
1126         "asturian" : ["%H:%M:%S %Z", "%H:%M"],
1127         "australian" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1128         "austrian" : ["%H:%M:%S %Z", "%H:%M"],
1129         "bahasa" : ["%H.%M.%S %Z", "%H.%M"],
1130         "bahasam" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1131         "basque" : ["%H:%M:%S (%Z)", "%H:%M"],
1132         "belarusian" : ["%H:%M:%S, %Z", "%H:%M"],
1133         "bosnian" : ["%H:%M:%S %Z", "%H:%M"],
1134         "brazilian" : ["%H:%M:%S %Z", "%H:%M"],
1135         "breton" : ["%H:%M:%S %Z", "%H:%M"],
1136         "british" : ["%H:%M:%S %Z", "%H:%M"],
1137         "bulgarian" : ["%H:%M:%S %Z", "%H:%M"],
1138         "canadian" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1139         "canadien" : ["%H:%M:%S %Z", "%H h %M"],
1140         "catalan" : ["%H:%M:%S %Z", "%H:%M"],
1141         "chinese-simplified" : ["%Z %p%I:%M:%S", "%p%I:%M"],
1142         "chinese-traditional" : ["%p%I:%M:%S [%Z]", "%p%I:%M"],
1143         "coptic" : ["%H:%M:%S %Z", "%H:%M:%S"],
1144         "croatian" : ["%H:%M:%S (%Z)", "%H:%M"],
1145         "czech" : ["%H:%M:%S %Z", "%H:%M"],
1146         "danish" : ["%H.%M.%S %Z", "%H.%M"],
1147         "divehi" : ["%H:%M:%S %Z", "%H:%M"],
1148         "dutch" : ["%H:%M:%S %Z", "%H:%M"],
1149         "english" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1150         "esperanto" : ["%H:%M:%S %Z", "%H:%M:%S"],
1151         "estonian" : ["%H:%M:%S %Z", "%H:%M"],
1152         "farsi" : ["%H:%M:%S (%Z)", "%H:%M"],
1153         "finnish" : ["%H.%M.%S %Z", "%H.%M"],
1154         "french" : ["%H:%M:%S %Z", "%H:%M"],
1155         "friulan" : ["%H:%M:%S %Z", "%H:%M"],
1156         "galician" : ["%H:%M:%S %Z", "%H:%M"],
1157         "georgian" : ["%H:%M:%S %Z", "%H:%M"],
1158         "german" : ["%H:%M:%S %Z", "%H:%M"],
1159         "german-ch" : ["%H:%M:%S %Z", "%H:%M"],
1160         "german-ch-old" : ["%H:%M:%S %Z", "%H:%M"],
1161         "greek" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1162         "hebrew" : ["%H:%M:%S %Z", "%H:%M"],
1163         "hindi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1164         "icelandic" : ["%H:%M:%S %Z", "%H:%M"],
1165         "interlingua" : ["%H:%M:%S %Z", "%H:%M"],
1166         "irish" : ["%H:%M:%S %Z", "%H:%M"],
1167         "italian" : ["%H:%M:%S %Z", "%H:%M"],
1168         "japanese" : ["%H時%M分%S秒 %Z", "%H:%M"],
1169         "japanese-cjk" : ["%H時%M分%S秒 %Z", "%H:%M"],
1170         "kannada" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1171         "kazakh" : ["%H:%M:%S %Z", "%H:%M"],
1172         "khmer" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1173         "korean" : ["%p %I시%M분 %S초 %Z", "%p %I:%M"],
1174         "kurmanji" : ["%H:%M:%S %Z", "%H:%M:%S"],
1175         "lao" : ["%H ໂມງ%M ນາທີ  %S ວິນາທີ %Z", "%H:%M"],
1176         "latin" : ["%H:%M:%S %Z", "%H:%M:%S"],
1177         "latvian" : ["%H:%M:%S %Z", "%H:%M"],
1178         "lithuanian" : ["%H:%M:%S %Z", "%H:%M"],
1179         "lowersorbian" : ["%H:%M:%S %Z", "%H:%M"],
1180         "macedonian" : ["%H:%M:%S %Z", "%H:%M"],
1181         "magyar" : ["%H:%M:%S %Z", "%H:%M"],
1182         "marathi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1183         "mongolian" : ["%H:%M:%S %Z", "%H:%M"],
1184         "naustrian" : ["%H:%M:%S %Z", "%H:%M"],
1185         "newzealand" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1186         "ngerman" : ["%H:%M:%S %Z", "%H:%M"],
1187         "norsk" : ["%H:%M:%S %Z", "%H:%M"],
1188         "nynorsk" : ["kl. %H:%M:%S %Z", "%H:%M"],
1189         "occitan" : ["%H:%M:%S %Z", "%H:%M"],
1190         "piedmontese" : ["%H:%M:%S %Z", "%H:%M:%S"],
1191         "polish" : ["%H:%M:%S %Z", "%H:%M"],
1192         "polutonikogreek" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1193         "portuguese" : ["%H:%M:%S %Z", "%H:%M"],
1194         "romanian" : ["%H:%M:%S %Z", "%H:%M"],
1195         "romansh" : ["%H:%M:%S %Z", "%H:%M"],
1196         "russian" : ["%H:%M:%S %Z", "%H:%M"],
1197         "samin" : ["%H:%M:%S %Z", "%H:%M"],
1198         "sanskrit" : ["%H:%M:%S %Z", "%H:%M"],
1199         "scottish" : ["%H:%M:%S %Z", "%H:%M"],
1200         "serbian" : ["%H:%M:%S %Z", "%H:%M"],
1201         "serbian-latin" : ["%H:%M:%S %Z", "%H:%M"],
1202         "slovak" : ["%H:%M:%S %Z", "%H:%M"],
1203         "slovene" : ["%H:%M:%S %Z", "%H:%M"],
1204         "spanish" : ["%H:%M:%S (%Z)", "%H:%M"],
1205         "spanish-mexico" : ["%H:%M:%S %Z", "%H:%M"],
1206         "swedish" : ["kl. %H:%M:%S %Z", "%H:%M"],
1207         "syriac" : ["%H:%M:%S %Z", "%H:%M"],
1208         "tamil" : ["%p %I:%M:%S %Z", "%p %I:%M"],
1209         "telugu" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1210         "thai" : ["%H นาฬิกา %M นาที  %S วินาที %Z", "%H:%M"],
1211         "tibetan" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1212         "turkish" : ["%H:%M:%S %Z", "%H:%M"],
1213         "turkmen" : ["%H:%M:%S %Z", "%H:%M"],
1214         "ukrainian" : ["%H:%M:%S %Z", "%H:%M"],
1215         "uppersorbian" : ["%H:%M:%S %Z", "%H:%M hodź."],
1216         "urdu" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1217         "vietnamese" : ["%H:%M:%S %Z", "%H:%M"],
1218         "welsh" : ["%H:%M:%S %Z", "%H:%M"]
1219     }
1220
1221     types = ["time", "fixtime", "modtime" ]
1222     i = 0
1223     i = find_token(document.header, "\\language", 0)
1224     if i == -1:
1225         # this should not happen
1226         document.warning("Malformed LyX document! No \\language header found!")
1227         return
1228     lang = get_value(document.header, "\\language", i)
1229
1230     i = 0
1231     while True:
1232         i = find_token(document.body, "\\begin_inset Info", i)
1233         if i == -1:
1234             return
1235         j = find_end_of_inset(document.body, i + 1)
1236         if j == -1:
1237             document.warning("Malformed LyX document: Could not find end of Info inset.")
1238             i = i + 1
1239             continue
1240         tp = find_token(document.body, 'type', i, j)
1241         tpv = get_quoted_value(document.body, "type", tp)
1242         if tpv not in types:
1243             i = i + 1
1244             continue
1245         arg = find_token(document.body, 'arg', i, j)
1246         argv = get_quoted_value(document.body, "arg", arg)
1247         isotime = ""
1248         dtme = datetime.now()
1249         tme = dtme.time()
1250         if tpv == "fixtime":
1251             timecomps = argv.split('@')
1252             if len(timecomps) > 1:
1253                 argv = timecomps[0]
1254                 isotime = timecomps[1]
1255                 m = re.search('(\d\d):(\d\d):(\d\d)', isotime)
1256                 if m:
1257                     tme = time(int(m.group(1)), int(m.group(2)), int(m.group(3)))
1258                 else:
1259                     m = re.search('(\d\d):(\d\d)', isotime)
1260                     if m:
1261                         tme = time(int(m.group(1)), int(m.group(2)))
1262 # FIXME if we had the path to the original document (not the one in the tmp dir),
1263 #        we could use the mtime.
1264 #        elif tpv == "moddate":
1265 #            dte = date.fromtimestamp(os.path.getmtime(document.dir))
1266         result = ""
1267         if argv == "ISO":
1268             result = tme.isoformat()
1269         elif argv == "long":
1270             result = tme.strftime(timeformats[lang][0])
1271         elif argv == "short":
1272             result = tme.strftime(timeformats[lang][1])
1273         else:
1274             fmt = argv.replace("HH", "%H").replace("H", "%H").replace("hh", "%I").replace("h", "%I")
1275             fmt = fmt.replace("mm", "%M").replace("m", "%M").replace("ss", "%S").replace("s", "%S")
1276             fmt = fmt.replace("zzz", "%f").replace("z", "%f").replace("t", "%Z")
1277             fmt = fmt.replace("AP", "%p").replace("ap", "%p").replace("A", "%p").replace("a", "%p")
1278             fmt = fmt.replace("'", "")
1279             result = dte.strftime(fmt)
1280         document.body[i : j+1] = result
1281         i = i + 1
1282
1283
1284 def revert_namenoextinfo(document):
1285     " Merge buffer Info inset type name-noext to name. "
1286
1287     i = 0
1288     while True:
1289         i = find_token(document.body, "\\begin_inset Info", i)
1290         if i == -1:
1291             return
1292         j = find_end_of_inset(document.body, i + 1)
1293         if j == -1:
1294             document.warning("Malformed LyX document: Could not find end of Info inset.")
1295             i = i + 1
1296             continue
1297         tp = find_token(document.body, 'type', i, j)
1298         tpv = get_quoted_value(document.body, "type", tp)
1299         if tpv != "buffer":
1300             i = i + 1
1301             continue
1302         arg = find_token(document.body, 'arg', i, j)
1303         argv = get_quoted_value(document.body, "arg", arg)
1304         if argv != "name-noext":
1305             i = i + 1
1306             continue
1307         document.body[arg] = "arg \"name\""
1308         i = i + 1
1309
1310
1311 def revert_l7ninfo(document):
1312     " Revert l7n Info inset to text. "
1313
1314     i = 0
1315     while True:
1316         i = find_token(document.body, "\\begin_inset Info", i)
1317         if i == -1:
1318             return
1319         j = find_end_of_inset(document.body, i + 1)
1320         if j == -1:
1321             document.warning("Malformed LyX document: Could not find end of Info inset.")
1322             i = i + 1
1323             continue
1324         tp = find_token(document.body, 'type', i, j)
1325         tpv = get_quoted_value(document.body, "type", tp)
1326         if tpv != "l7n":
1327             i = i + 1
1328             continue
1329         arg = find_token(document.body, 'arg', i, j)
1330         argv = get_quoted_value(document.body, "arg", arg)
1331         # remove trailing colons, menu accelerator (|...) and qt accelerator (&), while keeping literal " & " 
1332         argv = argv.rstrip(':').split('|')[0].replace(" & ", "</amp;>").replace("&", "").replace("</amp;>", " & ")
1333         document.body[i : j+1] = argv
1334         i = i + 1
1335
1336
1337 def revert_listpargs(document):
1338     " Reverts listpreamble arguments to TeX-code "
1339     i = 0
1340     while True:
1341         i = find_token(document.body, "\\begin_inset Argument listpreamble:", i)
1342         if i == -1:
1343             return
1344         j = find_end_of_inset(document.body, i)
1345         # Find containing paragraph layout
1346         parent = get_containing_layout(document.body, i)
1347         if parent == False:
1348             document.warning("Malformed LyX document: Can't find parent paragraph layout")
1349             i += 1
1350             continue
1351         parbeg = parent[3]
1352         beginPlain = find_token(document.body, "\\begin_layout Plain Layout", i)
1353         endPlain = find_end_of_layout(document.body, beginPlain)
1354         content = document.body[beginPlain + 1 : endPlain]
1355         del document.body[i:j+1]
1356         subst = ["\\begin_inset ERT", "status collapsed", "", "\\begin_layout Plain Layout",
1357                  "{"] + content + ["}", "\\end_layout", "", "\\end_inset", ""]
1358         document.body[parbeg : parbeg] = subst
1359         i += 1
1360
1361
1362 def revert_lformatinfo(document):
1363     " Revert layout format Info inset to text. "
1364
1365     i = 0
1366     while True:
1367         i = find_token(document.body, "\\begin_inset Info", i)
1368         if i == -1:
1369             return
1370         j = find_end_of_inset(document.body, i + 1)
1371         if j == -1:
1372             document.warning("Malformed LyX document: Could not find end of Info inset.")
1373             i = i + 1
1374             continue
1375         tp = find_token(document.body, 'type', i, j)
1376         tpv = get_quoted_value(document.body, "type", tp)
1377         if tpv != "lyxinfo":
1378             i = i + 1
1379             continue
1380         arg = find_token(document.body, 'arg', i, j)
1381         argv = get_quoted_value(document.body, "arg", arg)
1382         if argv != "layoutformat":
1383             i = i + 1
1384             continue
1385         # hardcoded for now
1386         document.body[i : j+1] = "69"
1387         i = i + 1
1388
1389
1390 def convert_hebrew_parentheses(document):
1391     " Don't reverse parentheses in Hebrew text"
1392     current_language = document.language
1393     for i, line in enumerate(document.body):
1394         if line.startswith('\\lang '):
1395             current_language = line[len('\\lang '):]
1396         elif line.startswith('\\end_layout'):
1397             current_language = document.language
1398         elif current_language == 'hebrew' and not line.startswith('\\'):
1399             document.body[i] = line.replace('(','\x00').replace(')','(').replace('\x00',')')
1400
1401
1402 def revert_hebrew_parentheses(document):
1403     " Store parentheses in Hebrew text reversed"
1404     # This only exists to keep the convert/revert naming convention
1405     convert_hebrew_parentheses(document)
1406
1407
1408 ##
1409 # Conversion hub
1410 #
1411
1412 supported_versions = ["2.4.0", "2.4"]
1413 convert = [
1414            [545, [convert_lst_literalparam]],
1415            [546, []],
1416            [547, []],
1417            [548, []],
1418            [549, []],
1419            [550, [convert_fontenc]],
1420            [551, []],
1421            [552, []],
1422            [553, []],
1423            [554, []],
1424            [555, []],
1425            [556, []],
1426            [557, [convert_vcsinfo]],
1427            [558, [removeFrontMatterStyles]],
1428            [559, []],
1429            [560, []],
1430            [561, [convert_latexFonts]], # Handle dejavu, ibmplex fonts in GUI
1431            [562, []],
1432            [563, []],
1433            [564, []],
1434            [565, [convert_AdobeFonts]], # Handle adobe fonts in GUI
1435            [566, [convert_hebrew_parentheses]],
1436           ]
1437
1438 revert =  [
1439            [565, [revert_hebrew_parentheses]],
1440            [564, [revert_AdobeFonts]],
1441            [563, [revert_lformatinfo]],
1442            [562, [revert_listpargs]],
1443            [561, [revert_l7ninfo]],
1444            [560, [revert_latexFonts]], # Handle dejavu, ibmplex fonts in user preamble
1445            [559, [revert_timeinfo, revert_namenoextinfo]],
1446            [558, [revert_dateinfo]],
1447            [557, [addFrontMatterStyles]],
1448            [556, [revert_vcsinfo]],
1449            [555, [revert_bibencoding]],
1450            [554, [revert_vcolumns]],
1451            [553, [revert_stretchcolumn]],
1452            [552, [revert_tuftecite]],
1453            [551, [revert_floatpclass, revert_floatalignment]],
1454            [550, [revert_nospellcheck]],
1455            [549, [revert_fontenc]],
1456            [548, []],# dummy format change
1457            [547, [revert_lscape]],
1458            [546, [revert_xcharter]],
1459            [545, [revert_paratype]],
1460            [544, [revert_lst_literalparam]]
1461           ]
1462
1463
1464 if __name__ == "__main__":
1465     pass