]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/lyx_2_4.py
use revert_language in more cases
[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, 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, revert_language)
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             words = val.split() # ! splits also values like '"DejaVu Sans"'
223             words[0] = '"' + fn + '"'
224             document.header[j] = ft + ' ' + ' '.join(words)
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(' ')     # ! splits also values like '"DejaVu Sans"'
247         font = words[0].strip('"') # TeX font name has no whitespace
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         "malayalam" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1001         "marathi" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1002         "mongolian" : ["%A, %Y оны %m сарын %d", "%Y-%m-%d", "%Y оны %m сарын %d", "%d-%m-%Y", "%d-%m-%Y"],
1003         "naustrian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1004         "newzealand" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1005         "ngerman" : ["%A, %d. %B %Y", "%d.%m.%y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1006         "norsk" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1007         "nynorsk" : ["%A %d. %B %Y", "%d.%m.%Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1008         "occitan" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1009         "piedmontese" : ["%A, %d %B %Y", "%d %b %Y", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1010         "polish" : ["%A, %d %B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1011         "polutonikogreek" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1012         "portuguese" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B de %Y", "%d de %b de %Y", "%Y/%m/%d"],
1013         "romanian" : ["%A, %d %B %Y", "%d.%m.%Y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1014         "romansh" : ["%A, ils %d da %B %Y", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1015         "russian" : ["%A, %d %B %Y г.", "%d.%m.%Y", "%d %B %Y г.", "%d %b %Y г.", "%d.%m.%Y"],
1016         "samin" : ["%Y %B %d, %A", "%Y-%m-%d", "%B %d. b. %Y", "%b %d. b. %Y", "%d.%m.%Y"],
1017         "sanskrit" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1018         "scottish" : ["%A, %dmh %B %Y", "%d/%m/%Y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1019         "serbian" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1020         "serbian-latin" : ["%A, %d. %B %Y.", "%d.%m.%y.", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1021         "slovak" : ["%A, %d. %B %Y", "%d. %m. %Y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1022         "slovene" : ["%A, %d. %B %Y", "%d. %m. %y", "%d. %B %Y", "%d. %b %Y", "%d.%m.%Y"],
1023         "spanish" : ["%A, %d de %B de %Y", "%d/%m/%y", "%d de %B %de %Y", "%d %b %Y", "%d/%m/%Y"],
1024         "spanish-mexico" : ["%A, %d de %B %de %Y", "%d/%m/%y", "%d de %B de %Y", "%d %b %Y", "%d/%m/%Y"],
1025         "swedish" : ["%A %d %B %Y", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%Y-%m-%d"],
1026         "syriac" : ["%Y %B %d, %A", "%Y-%m-%d", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1027         "tamil" : ["%A, %d %B, %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1028         "telugu" : ["%d, %B %Y, %A", "%d-%m-%y", "%d %B %Y", "%d %b %Y", "%d-%m-%Y"],
1029         "thai" : ["%Aที่ %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1030         "tibetan" : ["%Y %Bའི་ཚེས་%d, %A", "%Y-%m-%d", "%B %d, %Y", "%b %d, %Y", "%m/%d/%Y"],
1031         "turkish" : ["%d %B %Y %A", "%d.%m.%Y", "%d %B %Y", "%d.%b.%Y", "%d.%m.%Y"],
1032         "turkmen" : ["%d %B %Y %A", "%d.%m.%Y", "%Y ý. %B %d", "%d.%m.%Y ý.", "%d.%m.%y ý."],
1033         "ukrainian" : ["%A, %d %B %Y р.", "%d.%m.%y", "%d %B %Y", "%d %m %Y", "%d.%m.%Y"],
1034         "uppersorbian" : ["%A, %d. %B %Y", "%d.%m.%y", "%d %B %Y", "%d %b %Y", "%d.%m.%Y"],
1035         "urdu" : ["%A، %d %B، %Y", "%d/%m/%y", "%d %B, %Y", "%d %b %Y", "%d/%m/%Y"],
1036         "vietnamese" : ["%A, %d %B, %Y", "%d/%m/%Y", "%d tháng %B %Y", "%d-%m-%Y", "%d/%m/%Y"],
1037         "welsh" : ["%A, %d %B %Y", "%d/%m/%y", "%d %B %Y", "%d %b %Y", "%d/%m/%Y"],
1038     }
1039
1040     types = ["date", "fixdate", "moddate" ]
1041     i = 0
1042     i = find_token(document.header, "\\language", 0)
1043     if i == -1:
1044         # this should not happen
1045         document.warning("Malformed LyX document! No \\language header found!")
1046         return
1047     lang = get_value(document.header, "\\language", i)
1048
1049     i = 0
1050     while True:
1051         i = find_token(document.body, "\\begin_inset Info", i)
1052         if i == -1:
1053             return
1054         j = find_end_of_inset(document.body, i + 1)
1055         if j == -1:
1056             document.warning("Malformed LyX document: Could not find end of Info inset.")
1057             i = i + 1
1058             continue
1059         tp = find_token(document.body, 'type', i, j)
1060         tpv = get_quoted_value(document.body, "type", tp)
1061         if tpv not in types:
1062             i = i + 1
1063             continue
1064         arg = find_token(document.body, 'arg', i, j)
1065         argv = get_quoted_value(document.body, "arg", arg)
1066         isodate = ""
1067         dte = date.today()
1068         if tpv == "fixdate":
1069             datecomps = argv.split('@')
1070             if len(datecomps) > 1:
1071                 argv = datecomps[0]
1072                 isodate = datecomps[1]
1073                 m = re.search('(\d\d\d\d)-(\d\d)-(\d\d)', isodate)
1074                 if m:
1075                     dte = date(int(m.group(1)), int(m.group(2)), int(m.group(3)))
1076 # FIXME if we had the path to the original document (not the one in the tmp dir),
1077 #        we could use the mtime.
1078 #        elif tpv == "moddate":
1079 #            dte = date.fromtimestamp(os.path.getmtime(document.dir))
1080         result = ""
1081         if argv == "ISO":
1082             result = dte.isodate()
1083         elif argv == "long":
1084             result = dte.strftime(dateformats[lang][0])
1085         elif argv == "short":
1086             result = dte.strftime(dateformats[lang][1])
1087         elif argv == "loclong":
1088             result = dte.strftime(dateformats[lang][2])
1089         elif argv == "locmedium":
1090             result = dte.strftime(dateformats[lang][3])
1091         elif argv == "locshort":
1092             result = dte.strftime(dateformats[lang][4])
1093         else:
1094             fmt = argv.replace("MMMM", "%b").replace("MMM", "%b").replace("MM", "%m").replace("M", "%m")
1095             fmt = fmt.replace("yyyy", "%Y").replace("yy", "%y")
1096             fmt = fmt.replace("dddd", "%A").replace("ddd", "%a").replace("dd", "%d")
1097             fmt = re.sub('[^\'%]d', '%d', fmt)
1098             fmt = fmt.replace("'", "")
1099             result = dte.strftime(fmt)
1100         if sys.version_info < (3,0):
1101             # In Python 2, datetime module works with binary strings,
1102             # our dateformat strings are utf8-encoded:
1103             result = result.decode('utf-8')
1104         document.body[i : j+1] = result
1105         i = i + 1
1106
1107
1108 def revert_timeinfo(document):
1109     " Revert time info insets to static text. "
1110
1111 # FIXME This currently only considers the main language and uses the system locale
1112 # Ideally, it should honor context languages and switch the locale accordingly.
1113 # Also, the time object is "naive", i.e., it does not know of timezones (%Z will
1114 # be empty).
1115
1116     # The time formats for each language using strftime syntax:
1117     # long, short
1118     timeformats = {
1119         "afrikaans" : ["%H:%M:%S %Z", "%H:%M"],
1120         "albanian" : ["%I:%M:%S %p, %Z", "%I:%M %p"],
1121         "american" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1122         "amharic" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1123         "ancientgreek" : ["%H:%M:%S %Z", "%H:%M:%S"],
1124         "arabic_arabi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1125         "arabic_arabtex" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1126         "armenian" : ["%H:%M:%S %Z", "%H:%M"],
1127         "asturian" : ["%H:%M:%S %Z", "%H:%M"],
1128         "australian" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1129         "austrian" : ["%H:%M:%S %Z", "%H:%M"],
1130         "bahasa" : ["%H.%M.%S %Z", "%H.%M"],
1131         "bahasam" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1132         "basque" : ["%H:%M:%S (%Z)", "%H:%M"],
1133         "belarusian" : ["%H:%M:%S, %Z", "%H:%M"],
1134         "bosnian" : ["%H:%M:%S %Z", "%H:%M"],
1135         "brazilian" : ["%H:%M:%S %Z", "%H:%M"],
1136         "breton" : ["%H:%M:%S %Z", "%H:%M"],
1137         "british" : ["%H:%M:%S %Z", "%H:%M"],
1138         "bulgarian" : ["%H:%M:%S %Z", "%H:%M"],
1139         "canadian" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1140         "canadien" : ["%H:%M:%S %Z", "%H h %M"],
1141         "catalan" : ["%H:%M:%S %Z", "%H:%M"],
1142         "chinese-simplified" : ["%Z %p%I:%M:%S", "%p%I:%M"],
1143         "chinese-traditional" : ["%p%I:%M:%S [%Z]", "%p%I:%M"],
1144         "coptic" : ["%H:%M:%S %Z", "%H:%M:%S"],
1145         "croatian" : ["%H:%M:%S (%Z)", "%H:%M"],
1146         "czech" : ["%H:%M:%S %Z", "%H:%M"],
1147         "danish" : ["%H.%M.%S %Z", "%H.%M"],
1148         "divehi" : ["%H:%M:%S %Z", "%H:%M"],
1149         "dutch" : ["%H:%M:%S %Z", "%H:%M"],
1150         "english" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1151         "esperanto" : ["%H:%M:%S %Z", "%H:%M:%S"],
1152         "estonian" : ["%H:%M:%S %Z", "%H:%M"],
1153         "farsi" : ["%H:%M:%S (%Z)", "%H:%M"],
1154         "finnish" : ["%H.%M.%S %Z", "%H.%M"],
1155         "french" : ["%H:%M:%S %Z", "%H:%M"],
1156         "friulan" : ["%H:%M:%S %Z", "%H:%M"],
1157         "galician" : ["%H:%M:%S %Z", "%H:%M"],
1158         "georgian" : ["%H:%M:%S %Z", "%H:%M"],
1159         "german" : ["%H:%M:%S %Z", "%H:%M"],
1160         "german-ch" : ["%H:%M:%S %Z", "%H:%M"],
1161         "german-ch-old" : ["%H:%M:%S %Z", "%H:%M"],
1162         "greek" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1163         "hebrew" : ["%H:%M:%S %Z", "%H:%M"],
1164         "hindi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1165         "icelandic" : ["%H:%M:%S %Z", "%H:%M"],
1166         "interlingua" : ["%H:%M:%S %Z", "%H:%M"],
1167         "irish" : ["%H:%M:%S %Z", "%H:%M"],
1168         "italian" : ["%H:%M:%S %Z", "%H:%M"],
1169         "japanese" : ["%H時%M分%S秒 %Z", "%H:%M"],
1170         "japanese-cjk" : ["%H時%M分%S秒 %Z", "%H:%M"],
1171         "kannada" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1172         "kazakh" : ["%H:%M:%S %Z", "%H:%M"],
1173         "khmer" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1174         "korean" : ["%p %I시%M분 %S초 %Z", "%p %I:%M"],
1175         "kurmanji" : ["%H:%M:%S %Z", "%H:%M:%S"],
1176         "lao" : ["%H ໂມງ%M ນາທີ  %S ວິນາທີ %Z", "%H:%M"],
1177         "latin" : ["%H:%M:%S %Z", "%H:%M:%S"],
1178         "latvian" : ["%H:%M:%S %Z", "%H:%M"],
1179         "lithuanian" : ["%H:%M:%S %Z", "%H:%M"],
1180         "lowersorbian" : ["%H:%M:%S %Z", "%H:%M"],
1181         "macedonian" : ["%H:%M:%S %Z", "%H:%M"],
1182         "magyar" : ["%H:%M:%S %Z", "%H:%M"],
1183         "malayalam" : ["%p %I:%M:%S %Z", "%p %I:%M"],
1184         "marathi" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1185         "mongolian" : ["%H:%M:%S %Z", "%H:%M"],
1186         "naustrian" : ["%H:%M:%S %Z", "%H:%M"],
1187         "newzealand" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1188         "ngerman" : ["%H:%M:%S %Z", "%H:%M"],
1189         "norsk" : ["%H:%M:%S %Z", "%H:%M"],
1190         "nynorsk" : ["kl. %H:%M:%S %Z", "%H:%M"],
1191         "occitan" : ["%H:%M:%S %Z", "%H:%M"],
1192         "piedmontese" : ["%H:%M:%S %Z", "%H:%M:%S"],
1193         "polish" : ["%H:%M:%S %Z", "%H:%M"],
1194         "polutonikogreek" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1195         "portuguese" : ["%H:%M:%S %Z", "%H:%M"],
1196         "romanian" : ["%H:%M:%S %Z", "%H:%M"],
1197         "romansh" : ["%H:%M:%S %Z", "%H:%M"],
1198         "russian" : ["%H:%M:%S %Z", "%H:%M"],
1199         "samin" : ["%H:%M:%S %Z", "%H:%M"],
1200         "sanskrit" : ["%H:%M:%S %Z", "%H:%M"],
1201         "scottish" : ["%H:%M:%S %Z", "%H:%M"],
1202         "serbian" : ["%H:%M:%S %Z", "%H:%M"],
1203         "serbian-latin" : ["%H:%M:%S %Z", "%H:%M"],
1204         "slovak" : ["%H:%M:%S %Z", "%H:%M"],
1205         "slovene" : ["%H:%M:%S %Z", "%H:%M"],
1206         "spanish" : ["%H:%M:%S (%Z)", "%H:%M"],
1207         "spanish-mexico" : ["%H:%M:%S %Z", "%H:%M"],
1208         "swedish" : ["kl. %H:%M:%S %Z", "%H:%M"],
1209         "syriac" : ["%H:%M:%S %Z", "%H:%M"],
1210         "tamil" : ["%p %I:%M:%S %Z", "%p %I:%M"],
1211         "telugu" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1212         "thai" : ["%H นาฬิกา %M นาที  %S วินาที %Z", "%H:%M"],
1213         "tibetan" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1214         "turkish" : ["%H:%M:%S %Z", "%H:%M"],
1215         "turkmen" : ["%H:%M:%S %Z", "%H:%M"],
1216         "ukrainian" : ["%H:%M:%S %Z", "%H:%M"],
1217         "uppersorbian" : ["%H:%M:%S %Z", "%H:%M hodź."],
1218         "urdu" : ["%I:%M:%S %p %Z", "%I:%M %p"],
1219         "vietnamese" : ["%H:%M:%S %Z", "%H:%M"],
1220         "welsh" : ["%H:%M:%S %Z", "%H:%M"]
1221     }
1222
1223     types = ["time", "fixtime", "modtime" ]
1224     i = 0
1225     i = find_token(document.header, "\\language", 0)
1226     if i == -1:
1227         # this should not happen
1228         document.warning("Malformed LyX document! No \\language header found!")
1229         return
1230     lang = get_value(document.header, "\\language", i)
1231
1232     i = 0
1233     while True:
1234         i = find_token(document.body, "\\begin_inset Info", i)
1235         if i == -1:
1236             return
1237         j = find_end_of_inset(document.body, i + 1)
1238         if j == -1:
1239             document.warning("Malformed LyX document: Could not find end of Info inset.")
1240             i = i + 1
1241             continue
1242         tp = find_token(document.body, 'type', i, j)
1243         tpv = get_quoted_value(document.body, "type", tp)
1244         if tpv not in types:
1245             i = i + 1
1246             continue
1247         arg = find_token(document.body, 'arg', i, j)
1248         argv = get_quoted_value(document.body, "arg", arg)
1249         isotime = ""
1250         dtme = datetime.now()
1251         tme = dtme.time()
1252         if tpv == "fixtime":
1253             timecomps = argv.split('@')
1254             if len(timecomps) > 1:
1255                 argv = timecomps[0]
1256                 isotime = timecomps[1]
1257                 m = re.search('(\d\d):(\d\d):(\d\d)', isotime)
1258                 if m:
1259                     tme = time(int(m.group(1)), int(m.group(2)), int(m.group(3)))
1260                 else:
1261                     m = re.search('(\d\d):(\d\d)', isotime)
1262                     if m:
1263                         tme = time(int(m.group(1)), int(m.group(2)))
1264 # FIXME if we had the path to the original document (not the one in the tmp dir),
1265 #        we could use the mtime.
1266 #        elif tpv == "moddate":
1267 #            dte = date.fromtimestamp(os.path.getmtime(document.dir))
1268         result = ""
1269         if argv == "ISO":
1270             result = tme.isoformat()
1271         elif argv == "long":
1272             result = tme.strftime(timeformats[lang][0])
1273         elif argv == "short":
1274             result = tme.strftime(timeformats[lang][1])
1275         else:
1276             fmt = argv.replace("HH", "%H").replace("H", "%H").replace("hh", "%I").replace("h", "%I")
1277             fmt = fmt.replace("mm", "%M").replace("m", "%M").replace("ss", "%S").replace("s", "%S")
1278             fmt = fmt.replace("zzz", "%f").replace("z", "%f").replace("t", "%Z")
1279             fmt = fmt.replace("AP", "%p").replace("ap", "%p").replace("A", "%p").replace("a", "%p")
1280             fmt = fmt.replace("'", "")
1281             result = dte.strftime(fmt)
1282         document.body[i : j+1] = result
1283         i = i + 1
1284
1285
1286 def revert_namenoextinfo(document):
1287     " Merge buffer Info inset type name-noext to name. "
1288
1289     i = 0
1290     while True:
1291         i = find_token(document.body, "\\begin_inset Info", i)
1292         if i == -1:
1293             return
1294         j = find_end_of_inset(document.body, i + 1)
1295         if j == -1:
1296             document.warning("Malformed LyX document: Could not find end of Info inset.")
1297             i = i + 1
1298             continue
1299         tp = find_token(document.body, 'type', i, j)
1300         tpv = get_quoted_value(document.body, "type", tp)
1301         if tpv != "buffer":
1302             i = i + 1
1303             continue
1304         arg = find_token(document.body, 'arg', i, j)
1305         argv = get_quoted_value(document.body, "arg", arg)
1306         if argv != "name-noext":
1307             i = i + 1
1308             continue
1309         document.body[arg] = "arg \"name\""
1310         i = i + 1
1311
1312
1313 def revert_l7ninfo(document):
1314     " Revert l7n Info inset to text. "
1315
1316     i = 0
1317     while True:
1318         i = find_token(document.body, "\\begin_inset Info", i)
1319         if i == -1:
1320             return
1321         j = find_end_of_inset(document.body, i + 1)
1322         if j == -1:
1323             document.warning("Malformed LyX document: Could not find end of Info inset.")
1324             i = i + 1
1325             continue
1326         tp = find_token(document.body, 'type', i, j)
1327         tpv = get_quoted_value(document.body, "type", tp)
1328         if tpv != "l7n":
1329             i = i + 1
1330             continue
1331         arg = find_token(document.body, 'arg', i, j)
1332         argv = get_quoted_value(document.body, "arg", arg)
1333         # remove trailing colons, menu accelerator (|...) and qt accelerator (&), while keeping literal " & "
1334         argv = argv.rstrip(':').split('|')[0].replace(" & ", "</amp;>").replace("&", "").replace("</amp;>", " & ")
1335         document.body[i : j+1] = argv
1336         i = i + 1
1337
1338
1339 def revert_listpargs(document):
1340     " Reverts listpreamble arguments to TeX-code "
1341     i = 0
1342     while True:
1343         i = find_token(document.body, "\\begin_inset Argument listpreamble:", i)
1344         if i == -1:
1345             return
1346         j = find_end_of_inset(document.body, i)
1347         # Find containing paragraph layout
1348         parent = get_containing_layout(document.body, i)
1349         if parent == False:
1350             document.warning("Malformed LyX document: Can't find parent paragraph layout")
1351             i += 1
1352             continue
1353         parbeg = parent[3]
1354         beginPlain = find_token(document.body, "\\begin_layout Plain Layout", i)
1355         endPlain = find_end_of_layout(document.body, beginPlain)
1356         content = document.body[beginPlain + 1 : endPlain]
1357         del document.body[i:j+1]
1358         subst = ["\\begin_inset ERT", "status collapsed", "", "\\begin_layout Plain Layout",
1359                  "{"] + content + ["}", "\\end_layout", "", "\\end_inset", ""]
1360         document.body[parbeg : parbeg] = subst
1361         i += 1
1362
1363
1364 def revert_lformatinfo(document):
1365     " Revert layout format Info inset to text. "
1366
1367     i = 0
1368     while True:
1369         i = find_token(document.body, "\\begin_inset Info", i)
1370         if i == -1:
1371             return
1372         j = find_end_of_inset(document.body, i + 1)
1373         if j == -1:
1374             document.warning("Malformed LyX document: Could not find end of Info inset.")
1375             i = i + 1
1376             continue
1377         tp = find_token(document.body, 'type', i, j)
1378         tpv = get_quoted_value(document.body, "type", tp)
1379         if tpv != "lyxinfo":
1380             i = i + 1
1381             continue
1382         arg = find_token(document.body, 'arg', i, j)
1383         argv = get_quoted_value(document.body, "arg", arg)
1384         if argv != "layoutformat":
1385             i = i + 1
1386             continue
1387         # hardcoded for now
1388         document.body[i : j+1] = "69"
1389         i = i + 1
1390
1391
1392 def convert_hebrew_parentheses(document):
1393     " Don't reverse parentheses in Hebrew text"
1394     current_language = document.language
1395     for i, line in enumerate(document.body):
1396         if line.startswith('\\lang '):
1397             current_language = line[len('\\lang '):]
1398         elif line.startswith('\\end_layout'):
1399             current_language = document.language
1400         elif current_language == 'hebrew' and not line.startswith('\\'):
1401             document.body[i] = line.replace('(','\x00').replace(')','(').replace('\x00',')')
1402
1403
1404 def revert_hebrew_parentheses(document):
1405     " Store parentheses in Hebrew text reversed"
1406     # This only exists to keep the convert/revert naming convention
1407     convert_hebrew_parentheses(document)
1408
1409
1410 def revert_malayalam(document):
1411     " Set the document language to English but assure Malayalam output "
1412
1413     revert_language(document, "malayalam", "", "malayalam")
1414
1415
1416 ##
1417 # Conversion hub
1418 #
1419
1420 supported_versions = ["2.4.0", "2.4"]
1421 convert = [
1422            [545, [convert_lst_literalparam]],
1423            [546, []],
1424            [547, []],
1425            [548, []],
1426            [549, []],
1427            [550, [convert_fontenc]],
1428            [551, []],
1429            [552, []],
1430            [553, []],
1431            [554, []],
1432            [555, []],
1433            [556, []],
1434            [557, [convert_vcsinfo]],
1435            [558, [removeFrontMatterStyles]],
1436            [559, []],
1437            [560, []],
1438            [561, [convert_latexFonts]], # Handle dejavu, ibmplex fonts in GUI
1439            [562, []],
1440            [563, []],
1441            [564, []],
1442            [565, [convert_AdobeFonts]], # Handle adobe fonts in GUI
1443            [566, [convert_hebrew_parentheses]],
1444            [567, []],
1445           ]
1446
1447 revert =  [
1448            [566, [revert_malayalam]],
1449            [565, [revert_hebrew_parentheses]],
1450            [564, [revert_AdobeFonts]],
1451            [563, [revert_lformatinfo]],
1452            [562, [revert_listpargs]],
1453            [561, [revert_l7ninfo]],
1454            [560, [revert_latexFonts]], # Handle dejavu, ibmplex fonts in user preamble
1455            [559, [revert_timeinfo, revert_namenoextinfo]],
1456            [558, [revert_dateinfo]],
1457            [557, [addFrontMatterStyles]],
1458            [556, [revert_vcsinfo]],
1459            [555, [revert_bibencoding]],
1460            [554, [revert_vcolumns]],
1461            [553, [revert_stretchcolumn]],
1462            [552, [revert_tuftecite]],
1463            [551, [revert_floatpclass, revert_floatalignment]],
1464            [550, [revert_nospellcheck]],
1465            [549, [revert_fontenc]],
1466            [548, []],# dummy format change
1467            [547, [revert_lscape]],
1468            [546, [revert_xcharter]],
1469            [545, [revert_paratype]],
1470            [544, [revert_lst_literalparam]]
1471           ]
1472
1473
1474 if __name__ == "__main__":
1475     pass