]> git.lyx.org Git - lyx.git/blob - lib/scripts/layout2layout.py
e26c811de1f7cc37b10e489c25f5422faa12bd25
[lyx.git] / lib / scripts / layout2layout.py
1 #! /usr/bin/env python
2 # -*- coding: utf-8 -*-
3
4 # file layout2layout.py
5 # This file is part of LyX, the document processor.
6 # Licence details can be found in the file COPYING.
7
8 # author Georg Baum
9
10 # Full author contact details are available in file CREDITS
11
12 # This script will update a .layout file to current format
13
14
15 import os, re, string, sys
16
17 # Incremented to format 4, 6 April 2007, lasgouttes
18 # Introduction of generic "Provides" declaration
19
20 # Incremented to format 5, 22 August 2007 by vermeer
21 # InsetLayout material
22
23 # Incremented to format 6, 7 January 2008 by spitz
24 # Requires tag added to layout files
25
26 # Incremented to format 7, 24 March 2008 by rgh
27 # AddToPreamble tag added to layout files
28
29 # Incremented to format 8, 25 July 2008 by rgh
30 # UseModule tag added to layout files
31 # CopyStyle added to InsetLayout
32
33 # Incremented to format 9, 5 October 2008 by rgh
34 # ForcePlain and CustomPars tags added to InsetLayout
35
36 # Incremented to format 10, 6 October 2008 by rgh
37 # Change format of counters
38
39 # Incremented to format 11, 14 October 2008 by rgh
40 # Add ProvidesModule, ExcludesModule tags
41
42 # Incremented to format 12, 10 January 2009 by gb
43 # Add I18NPreamble tag
44
45 # Incremented to format 13, 5 February 2009 by rgh
46 # Add InToc tag for InsetLayout
47
48 # Incremented to format 14, 14 February 2009 by gb
49 # Rename I18NPreamble to BabelPreamble and add LangPreamble
50
51 # Incremented to format 15, 28 May 2009 by lasgouttes
52 # Add new tag OutputFormat; modules can be conditioned on feature 
53 # "from->to".
54
55 # Incremented to format 16, 5 June 2009 by rgh
56 # Add new tags for Text Class:
57 #   HTMLPreamble, HTMLAddToPreamble
58 # For Layout:
59 #   HTMLTag, HTMLAttr, HTMLLabel, HTMLLabelAttr, HTMLItem, HTMLItemAttr
60 #   HTMLStyle, and HTMLPreamble
61 # For InsetLayout:
62 #   HTMLTag, HTMLAttr, HTMLStyle, and HTMLPreamble
63 # For Floats:
64 #   HTMLType, HTMLClass, HTMLStyle
65
66 # Incremented to format 17, 12 August 2009 by rgh
67 # Add IfStyle and IfCounter tags for layout.
68
69 # Incremented to format 18, 27 October 2009 by rgh
70 # Added some new tags for HTML output.
71
72 # Incremented to format 19, 17 November 2009 by rgh
73 # Added InPreamble tag.
74
75 # Incremented to format 20, 17 December 2009 by rgh
76 # Added ContentAsLabel tag.
77
78 # Incremented to format 21, 12 January 2010 by rgh
79 # Added HTMLTocLayout and HTMLTitle tags.
80
81 # Incremented to format 22, 20 January 2010 by rgh
82 # Added HTMLFormat tag to Counters.
83
84 # Incremented to format 23, 13 February 2010 by spitz
85 # Added Spellcheck tag.
86
87 # Do not forget to document format change in Customization
88 # Manual (section "Declaring a new text class").
89
90 # You might also want to consider running the
91 # development/tools/updatelayouts.sh script to update all
92 # layout files to the new format.
93
94 currentFormat = 23
95
96
97 def usage(prog_name):
98     return ("Usage: %s inputfile outputfile\n" % prog_name +
99             "or     %s <inputfile >outputfile" % prog_name)
100
101
102 def error(message):
103     sys.stderr.write(message + '\n')
104     sys.exit(1)
105
106
107 def trim_bom(line):
108     " Remove byte order mark."
109     if line[0:3] == "\357\273\277":
110         return line[3:]
111     else:
112         return line
113
114
115 def read(source):
116     " Read input file and strip lineendings."
117     lines = source.read().splitlines()
118     lines[0] = trim_bom(lines[0])
119     return lines
120
121
122 def write(output, lines):
123     " Write output file with native lineendings."
124     output.write(os.linesep.join(lines) + os.linesep)
125
126
127 # Concatenates old and new in an intelligent way:
128 # If old is wrapped in ", they are stripped. The result is wrapped in ".
129 def concatenate_label(old, new):
130     # Don't use strip as long as we support python 1.5.2
131     if old[0] == '"':
132         return old[0:-1] + new + '"'
133     else:
134         return '"' + old + new + '"'
135
136 # appends a string to a list unless it's already there
137 def addstring(s, l):
138     if l.count(s) > 0:
139         return
140     l.append(s)
141
142
143 def convert(lines):
144     " Convert to new format."
145     re_Comment = re.compile(r'^(\s*)#')
146     re_Counter = re.compile(r'\s*Counter\s*', re.IGNORECASE)
147     re_Name = re.compile(r'\s*Name\s+(\S+)\s*', re.IGNORECASE)
148     re_UseMod = re.compile(r'^\s*UseModule\s+(.*)', re.IGNORECASE)
149     re_Empty = re.compile(r'^(\s*)$')
150     re_Format = re.compile(r'^(\s*)(Format)(\s+)(\S+)', re.IGNORECASE)
151     re_Preamble = re.compile(r'^(\s*)Preamble', re.IGNORECASE)
152     re_EndPreamble = re.compile(r'^(\s*)EndPreamble', re.IGNORECASE)
153     re_LangPreamble = re.compile(r'^(\s*)LangPreamble', re.IGNORECASE)
154     re_EndLangPreamble = re.compile(r'^(\s*)EndLangPreamble', re.IGNORECASE)
155     re_BabelPreamble = re.compile(r'^(\s*)BabelPreamble', re.IGNORECASE)
156     re_EndBabelPreamble = re.compile(r'^(\s*)EndBabelPreamble', re.IGNORECASE)
157     re_MaxCounter = re.compile(r'^(\s*)(MaxCounter)(\s+)(\S+)', re.IGNORECASE)
158     re_LabelType = re.compile(r'^(\s*)(LabelType)(\s+)(\S+)', re.IGNORECASE)
159     re_LabelString = re.compile(r'^(\s*)(LabelString)(\s+)(("[^"]+")|(\S+))', re.IGNORECASE)
160     re_LabelStringAppendix = re.compile(r'^(\s*)(LabelStringAppendix)(\s+)(("[^"]+")|(\S+))', re.IGNORECASE)
161     re_LatexType = re.compile(r'^(\s*)(LatexType)(\s+)(\S+)', re.IGNORECASE)
162     re_Style = re.compile(r'^(\s*)(Style)(\s+)(\S+)', re.IGNORECASE)
163     re_CopyStyle = re.compile(r'^(\s*)(CopyStyle)(\s+)(\S+)', re.IGNORECASE)
164     re_NoStyle = re.compile(r'^(\s*)(NoStyle)(\s+)(\S+)', re.IGNORECASE)
165     re_End = re.compile(r'^(\s*)(End)(\s*)$', re.IGNORECASE)
166     re_Provides = re.compile(r'^(\s*)Provides(\S+)(\s+)(\S+)', re.IGNORECASE)
167     re_CharStyle = re.compile(r'^(\s*)CharStyle(\s+)(\S+)$', re.IGNORECASE)
168     re_AMSMaths = re.compile(r'^\s*Input ams(?:math|def)s.inc\s*')
169     re_AMSMathsPlain = re.compile(r'^\s*Input amsmaths-plain.inc\s*')
170     re_AMSMathsSeq = re.compile(r'^\s*Input amsmaths-seq.inc\s*')
171     re_TocLevel = re.compile(r'^(\s*)(TocLevel)(\s+)(\S+)', re.IGNORECASE)
172     re_I18nPreamble = re.compile(r'^(\s*)I18nPreamble', re.IGNORECASE)
173     re_EndI18nPreamble = re.compile(r'^(\s*)EndI18nPreamble', re.IGNORECASE)
174
175     # counters for sectioning styles (hardcoded in 1.3)
176     counters = {"part"          : "\\Roman{part}",
177                 "chapter"       : "\\arabic{chapter}",
178                 "section"       : "\\arabic{section}",
179                 "subsection"    : "\\arabic{section}.\\arabic{subsection}",
180                 "subsubsection" : "\\arabic{section}.\\arabic{subsection}.\\arabic{subsubsection}",
181                 "paragraph"     : "\\arabic{section}.\\arabic{subsection}.\\arabic{subsubsection}.\\arabic{paragraph}",
182                 "subparagraph"  : "\\arabic{section}.\\arabic{subsection}.\\arabic{subsubsection}.\\arabic{paragraph}.\\arabic{subparagraph}"}
183
184     # counters for sectioning styles in appendix (hardcoded in 1.3)
185     appendixcounters = {"chapter"       : "\\Alph{chapter}",
186                         "section"       : "\\Alph{section}",
187                         "subsection"    : "\\arabic{section}.\\arabic{subsection}",
188                         "subsubsection" : "\\arabic{section}.\\arabic{subsection}.\\arabic{subsubsection}",
189                         "paragraph"     : "\\arabic{section}.\\arabic{subsection}.\\arabic{subsubsection}.\\arabic{paragraph}",
190                         "subparagraph"  : "\\arabic{section}.\\arabic{subsection}.\\arabic{subsubsection}.\\arabic{paragraph}.\\arabic{subparagraph}"}
191
192     # Value of TocLevel for sectioning styles
193     toclevels = {"part"          : 0,
194                  "chapter"       : 0,
195                  "section"       : 1,
196                  "subsection"    : 2,
197                  "subsubsection" : 3,
198                  "paragraph"     : 4,
199                  "subparagraph"  : 5}
200
201     i = 0
202     only_comment = 1
203     counter = ""
204     toclevel = ""
205     label = ""
206     labelstring = ""
207     labelstringappendix = ""
208     space1 = ""
209     labelstring_line = -1
210     labelstringappendix_line = -1
211     labeltype_line = -1
212     latextype = ""
213     latextype_line = -1
214     style = ""
215     maxcounter = 0
216     format = 1
217     formatline = 0
218     usemodules = []
219
220     while i < len(lines):
221         # Skip comments and empty lines
222         if re_Comment.match(lines[i]) or re_Empty.match(lines[i]):
223             i += 1
224             continue
225
226         # insert file format if not already there
227         if (only_comment):
228             match = re_Format.match(lines[i])
229             if match:
230                 formatline = i
231                 format = int(match.group(4))
232                 if format > 1 and format < currentFormat:
233                     lines[i] = "Format %d" % (format + 1)
234                     only_comment = 0
235                 elif format == currentFormat:
236                     # nothing to do
237                     return format
238                 else:
239                     error('Cannot convert file format %s' % format)
240             else:
241                 lines.insert(i, "Format 2")
242                 only_comment = 0
243                 continue
244
245         # Don't get confused by LaTeX code
246         if re_Preamble.match(lines[i]):
247             i += 1
248             while i < len(lines) and not re_EndPreamble.match(lines[i]):
249                 i += 1
250             continue
251         if re_LangPreamble.match(lines[i]):
252             i += 1
253             while i < len(lines) and not re_EndLangPreamble.match(lines[i]):
254                 i += 1
255             continue
256         if re_BabelPreamble.match(lines[i]):
257             i += 1
258             while i < len(lines) and not re_EndBabelPreamble.match(lines[i]):
259                 i += 1
260             continue
261
262         # This just involved new features, not any changes to old ones
263         if format >= 14 and format <= 22:
264           i += 1
265           continue
266
267         # Rename I18NPreamble to BabelPreamble
268         if format == 13:
269             match = re_I18nPreamble.match(lines[i])
270             if match:
271                 lines[i] = match.group(1) + "BabelPreamble"
272                 i += 1
273                 match = re_EndI18nPreamble.match(lines[i])
274                 while i < len(lines) and not match:
275                     i += 1
276                     match = re_EndI18nPreamble.match(lines[i])
277                 lines[i] = match.group(1) + "EndBabelPreamble"
278                 i += 1
279                 continue
280
281         # These just involved new features, not any changes to old ones
282         if format == 11 or format == 12:
283           i += 1
284           continue
285
286         if format == 10:
287             match = re_UseMod.match(lines[i])
288             if match:
289                 module = match.group(1)
290                 lines[i] = "DefaultModule " + module
291             i += 1
292             continue
293
294         if format == 9:
295             match = re_Counter.match(lines[i])
296             if match:
297                 counterline = i
298                 i += 1
299                 while i < len(lines):
300                     namem = re_Name.match(lines[i])
301                     if namem:
302                         name = namem.group(1)
303                         lines.pop(i)
304                         lines[counterline] = "Counter %s" % name
305                         # we don't need to increment i
306                         continue
307                     endem = re_End.match(lines[i])
308                     if endem:
309                         i += 1
310                         break
311                     i += 1
312             i += 1
313             continue
314
315         if format == 8:
316             # We want to scan for ams-type includes and, if we find them,
317             # add corresponding UseModule tags to the layout.
318             match = re_AMSMaths.match(lines[i])
319             if match:
320                 addstring("theorems-ams", usemodules)
321                 addstring("theorems-ams-extended", usemodules)
322                 addstring("theorems-sec", usemodules)
323                 lines.pop(i)
324                 continue
325             match = re_AMSMathsPlain.match(lines[i])
326             if match:
327                 addstring("theorems-starred", usemodules)
328                 lines.pop(i)
329                 continue
330             match = re_AMSMathsSeq.match(lines[i])
331             if match:
332                 addstring("theorems-ams", usemodules)
333                 addstring("theorems-ams-extended", usemodules)
334                 lines.pop(i)
335                 continue
336             i += 1
337             continue
338
339         # These just involved new features, not any changes to old ones
340         if format >= 5 and format <= 7:
341           i += 1
342           continue
343
344         if format == 4:
345             # Handle conversion to long CharStyle names
346             match = re_CharStyle.match(lines[i])
347             if match:
348                 lines[i] = "InsetLayout CharStyle:%s" % (match.group(3))
349                 i += 1
350                 lines.insert(i, "\tLyXType charstyle")
351                 i += 1
352                 lines.insert(i, "")
353                 lines[i] = "\tLabelString %s" % (match.group(3))
354             i += 1
355             continue
356
357         if format == 3:
358             # convert 'providesamsmath x',  'providesmakeidx x',  'providesnatbib x',  'providesurl x' to
359             #         'provides amsmath x', 'provides makeidx x', 'provides natbib x', 'provides url x'
360             # x is either 0 or 1
361             match = re_Provides.match(lines[i])
362             if match:
363                 lines[i] = "%sProvides %s%s%s" % (match.group(1), match.group(2).lower(),
364                                                   match.group(3), match.group(4))
365             i += 1
366             continue
367
368         if format == 2:
369             caption = []
370
371             # delete caption styles
372             match = re_Style.match(lines[i])
373             if match:
374                 style = string.lower(match.group(4))
375                 if style == "caption":
376                     del lines[i]
377                     while i < len(lines) and not re_End.match(lines[i]):
378                         caption.append(lines[i])
379                         del lines[i]
380                     if i == len(lines):
381                         error('Incomplete caption style.')
382                     else:
383                         del lines[i]
384                         continue
385
386             # delete undefinition of caption styles
387             match = re_NoStyle.match(lines[i])
388             if match:
389                 style = string.lower(match.group(4))
390                 if style == "caption":
391                     del lines[i]
392                     continue
393
394             # replace the CopyStyle statement with the definition of the real
395             # style. This may result in duplicate statements, but that is OK
396             # since the second one will overwrite the first one.
397             match = re_CopyStyle.match(lines[i])
398             if match:
399                 style = string.lower(match.group(4))
400                 if style == "caption":
401                     if len(caption) > 0:
402                         lines[i:i+1] = caption
403                     else:
404                         # FIXME: This style comes from an include file, we
405                         # should replace the real style and not this default.
406                         lines[i:i+1] = ['       Margin                First_Dynamic',
407                                         '       LatexType             Command',
408                                         '       LatexName             caption',
409                                         '       NeedProtect           1',
410                                         '       LabelSep              xx',
411                                         '       ParSkip               0.4',
412                                         '       TopSep                0.5',
413                                         '       Align                 Center',
414                                         '       AlignPossible         Center',
415                                         '       LabelType             Sensitive',
416                                         '       LabelString           "Senseless!"',
417                                         '       OptionalArgs          1',
418                                         '       LabelFont',
419                                         '         Series              Bold',
420                                         '       EndFont']
421
422             i += 1
423             continue
424
425         # Delete MaxCounter and remember the value of it
426         match = re_MaxCounter.match(lines[i])
427         if match:
428             level = match.group(4)
429             if string.lower(level) == "counter_chapter":
430                 maxcounter = 0
431             elif string.lower(level) == "counter_section":
432                 maxcounter = 1
433             elif string.lower(level) == "counter_subsection":
434                 maxcounter = 2
435             elif string.lower(level) == "counter_subsubsection":
436                 maxcounter = 3
437             elif string.lower(level) == "counter_paragraph":
438                 maxcounter = 4
439             elif string.lower(level) == "counter_subparagraph":
440                 maxcounter = 5
441             elif string.lower(level) == "counter_enumi":
442                 maxcounter = 6
443             elif string.lower(level) == "counter_enumii":
444                 maxcounter = 7
445             elif string.lower(level) == "counter_enumiii":
446                 maxcounter = 8
447             del lines[i]
448             continue
449
450         # Replace line
451         #
452         # LabelType Counter_EnumI
453         #
454         # with two lines
455         #
456         # LabelType Counter
457         # LabelCounter EnumI
458         #
459         match = re_LabelType.match(lines[i])
460         if match:
461             label = match.group(4)
462             # Remember indenting space for later reuse in added lines
463             space1 = match.group(1)
464             # Remember the line for adding the LabelCounter later.
465             # We can't do it here because it could shift latextype_line etc.
466             labeltype_line = i
467             if string.lower(label[:8]) == "counter_":
468                 counter = string.lower(label[8:])
469                 lines[i] = re_LabelType.sub(r'\1\2\3Counter', lines[i])
470
471         # Remember the LabelString line
472         match = re_LabelString.match(lines[i])
473         if match:
474             labelstring = match.group(4)
475             labelstring_line = i
476
477         # Remember the LabelStringAppendix line
478         match = re_LabelStringAppendix.match(lines[i])
479         if match:
480             labelstringappendix = match.group(4)
481             labelstringappendix_line = i
482
483         # Remember the LatexType line
484         match = re_LatexType.match(lines[i])
485         if match:
486             latextype = string.lower(match.group(4))
487             latextype_line = i
488
489         # Remember the TocLevel line
490         match = re_TocLevel.match(lines[i])
491         if match:
492             toclevel = string.lower(match.group(4))
493
494         # Reset variables at the beginning of a style definition
495         match = re_Style.match(lines[i])
496         if match:
497             style = string.lower(match.group(4))
498             counter = ""
499             toclevel = ""
500             label = ""
501             space1 = ""
502             labelstring = ""
503             labelstringappendix = ""
504             labelstring_line = -1
505             labelstringappendix_line = -1
506             labeltype_line = -1
507             latextype = ""
508             latextype_line = -1
509
510         if re_End.match(lines[i]):
511
512             # Add a line "LatexType Bib_Environment" if LabelType is Bibliography
513             # (or change the existing LatexType)
514             if string.lower(label) == "bibliography":
515                 if (latextype_line < 0):
516                     lines.insert(i, "%sLatexType Bib_Environment" % space1)
517                     i += 1
518                 else:
519                     lines[latextype_line] = re_LatexType.sub(r'\1\2\3Bib_Environment', lines[latextype_line])
520
521             # Change "LabelType Static" to "LabelType Itemize" for itemize environments
522             if latextype == "item_environment" and string.lower(label) == "static":
523                 lines[labeltype_line] = re_LabelType.sub(r'\1\2\3Itemize', lines[labeltype_line])
524
525             # Change "LabelType Counter_EnumI" to "LabelType Enumerate" for enumerate environments
526             if latextype == "item_environment" and string.lower(label) == "counter_enumi":
527                 lines[labeltype_line] = re_LabelType.sub(r'\1\2\3Enumerate', lines[labeltype_line])
528                 # Don't add the LabelCounter line later
529                 counter = ""
530
531             # Replace
532             #
533             # LabelString "Chapter"
534             #
535             # with
536             #
537             # LabelString "Chapter \arabic{chapter}"
538             #
539             # if this style has a counter. Ditto for LabelStringAppendix.
540             # This emulates the hardcoded article style numbering of 1.3
541             #
542             if counter != "":
543                 if counters.has_key(style):
544                     if labelstring_line < 0:
545                         lines.insert(i, '%sLabelString "%s"' % (space1, counters[style]))
546                         i += 1
547                     else:
548                         new_labelstring = concatenate_label(labelstring, counters[style])
549                         lines[labelstring_line] = re_LabelString.sub(
550                                 r'\1\2\3%s' % new_labelstring.replace("\\", "\\\\"),
551                                 lines[labelstring_line])
552                 if appendixcounters.has_key(style):
553                     if labelstringappendix_line < 0:
554                         lines.insert(i, '%sLabelStringAppendix "%s"' % (space1, appendixcounters[style]))
555                         i += 1
556                     else:
557                         new_labelstring = concatenate_label(labelstring, appendixcounters[style])
558                         lines[labelstringappendix_line] = re_LabelStringAppendix.sub(
559                                 r'\1\2\3%s' % new_labelstring.replace("\\", "\\\\"),
560                                 lines[labelstringappendix_line])
561
562                 # Now we can safely add the LabelCounter line
563                 lines.insert(labeltype_line + 1, "%sLabelCounter %s" % (space1, counter))
564                 i += 1
565
566             # Add the TocLevel setting for sectioning styles
567             if toclevel == "" and toclevels.has_key(style) and maxcounter <= toclevels[style]:
568                 lines.insert(i, '%s\tTocLevel %d' % (space1, toclevels[style]))
569                 i += 1
570
571         i += 1
572
573     if usemodules:
574         i = formatline + 1
575         for mod in usemodules:
576             lines.insert(i, "UseModule " + mod)
577             i += 1
578
579     return format + 1
580
581
582 def main(argv):
583
584     # Open files
585     if len(argv) == 1:
586         source = sys.stdin
587         output = sys.stdout
588     elif len(argv) == 3:
589         source = open(argv[1], 'rb')
590         output = open(argv[2], 'wb')
591     else:
592         error(usage(argv[0]))
593
594     # Do the real work
595     lines = read(source)
596     format = 1
597     while (format < currentFormat):
598         format = convert(lines)
599     write(output, lines)
600
601     # Close files
602     if len(argv) == 3:
603         source.close()
604         output.close()
605
606     return 0
607
608
609 if __name__ == "__main__":
610     main(sys.argv)