]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/lyx2lyx_tools.py
3bed2311e22aebef308a4a08e6e3193bf045570b
[lyx.git] / lib / lyx2lyx / lyx2lyx_tools.py
1 # This file is part of lyx2lyx
2 # -*- coding: utf-8 -*-
3 # Copyright (C) 2011 The LyX team
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
18
19 '''
20 This module offers several free functions to help with lyx2lyx'ing. 
21 More documentaton is below, but here is a quick guide to what 
22 they do. Optional arguments are marked by brackets.
23
24 add_to_preamble(document, text):
25   Here, text can be either a single line or a list of lines. It
26   is bad practice to pass something with embedded newlines, but
27   we will handle that properly.
28   The routine checks to see whether the provided material is
29   already in the preamble. If not, it adds it.
30   Prepends a comment "% Added by lyx2lyx" to text.
31
32 insert_to_preamble(document, text[, index]):
33   Here, text can be either a single line or a list of lines. It
34   is bad practice to pass something with embedded newlines, but
35   we will handle that properly.
36   The routine inserts text at document.preamble[index], where by
37   default index is 0, so the material is inserted at the beginning.
38   Prepends a comment "% Added by lyx2lyx" to text.
39
40 put_cmd_in_ert(arg):
41   Here arg should be a list of strings (lines), which we want to
42   wrap in ERT. Returns a list of strings so wrapped.
43   A call to this routine will often go something like this:
44     i = find_token('\\begin_inset FunkyInset', ...)
45     j = find_end_of_inset(document.body, i)
46     content = lyx2latex(document[i:j + 1])
47     ert = put_cmd_in_ert(content)
48     document.body[i:j+1] = ert
49
50 lyx2latex(document, lines):
51   Here, lines is a list of lines of LyX material we want to convert 
52   to LaTeX. We do the best we can and return a string containing
53   the translated material.
54
55 latex_length(slen):
56     Convert lengths (in LyX form) to their LaTeX representation. Returns 
57     (bool, length), where the bool tells us if it was a percentage, and 
58     the length is the LaTeX representation.
59
60 '''
61
62 import string
63 from parser_tools import find_token, find_end_of_inset
64 from unicode_symbols import unicode_reps
65
66
67 # This will accept either a list of lines or a single line.
68 # It is bad practice to pass something with embedded newlines,
69 # though we will handle that.
70 def add_to_preamble(document, text):
71     " Add text to the preamble if it is not already there. "
72
73     if not type(text) is list:
74       # split on \n just in case
75       # it'll give us the one element list we want
76       # if there's no \n, too
77       text = text.split('\n')
78
79     i = 0
80     prelen = len(document.preamble)
81     while True:
82       i = find_token(document.preamble, text[0], i)
83       if i == -1:
84         break
85       # we need a perfect match
86       matched = True
87       for line in text:
88         if i >= prelen or line != document.preamble[i]:
89           matched = False
90           break
91         i += 1
92       if matched:
93         return
94
95     document.preamble.extend(["% Added by lyx2lyx"])
96     document.preamble.extend(text)
97
98
99 # Note that text can be either a list of lines or a single line.
100 # It should really be a list.
101 def insert_to_preamble(document, text, index = 0):
102     """ Insert text to the preamble at a given line"""
103     
104     if not type(text) is list:
105       # split on \n just in case
106       # it'll give us the one element list we want
107       # if there's no \n, too
108       text = text.split('\n')
109     
110     text.insert(0, "% Added by lyx2lyx")
111     document.preamble[index:index] = text
112
113
114 def put_cmd_in_ert(arg):
115     '''
116     arg should be a list of lines we want to wrap in ERT.
117     Returns a list of strings, with the lines so wrapped.
118     '''
119     
120     ret = ["\\begin_inset ERT", "status collapsed", "", "\\begin_layout Plain Layout", ""]
121     # It will be faster for us to work with a single string internally. 
122     # That way, we only go through the unicode_reps loop once.
123     if type(arg) is list:
124       s = "\n".join(arg)
125     else:
126       s = arg
127     for rep in unicode_reps:
128       s = s.replace(rep[1], rep[0].replace('\\\\', '\\'))
129     s = s.replace('\\', "\\backslash\n")
130     ret += s.splitlines()
131     ret += ["\\end_layout", "", "\\end_inset"]
132     return ret
133
134
135 def get_ert(lines, i):
136     'Convert an ERT inset into LaTeX.'
137     if not lines[i].startswith("\\begin_inset ERT"):
138         return ""
139     j = find_end_of_inset(lines, i)
140     if j == -1:
141         return ""
142     while i < j and not lines[i].startswith("status"):
143         i = i + 1
144     i = i + 1
145     ret = ""
146     first = True
147     while i < j:
148         if lines[i] == "\\begin_layout Plain Layout":
149             if first:
150                 first = False
151             else:
152                 ret = ret + "\n"
153             while i + 1 < j and lines[i+1] == "":
154                 i = i + 1
155         elif lines[i] == "\\end_layout":
156             while i + 1 < j and lines[i+1] == "":
157                 i = i + 1
158         elif lines[i] == "\\backslash":
159             ret = ret + "\\"
160         else:
161             ret = ret + lines[i]
162         i = i + 1
163     return ret
164
165
166 def lyx2latex(document, lines):
167     'Convert some LyX stuff into corresponding LaTeX stuff, as best we can.'
168
169     content = ""
170     ert_end = 0
171     note_end = 0
172     hspace = ""
173
174     for curline in range(len(lines)):
175       line = lines[curline]
176       if line.startswith("\\begin_inset Note Note"):
177           # We want to skip LyX notes, so remember where the inset ends
178           note_end = find_end_of_inset(lines, curline + 1)
179           continue
180       elif note_end >= curline:
181           # Skip LyX notes
182           continue
183       elif line.startswith("\\begin_inset ERT"):
184           # We don't want to replace things inside ERT, so figure out
185           # where the end of the inset is.
186           ert_end = find_end_of_inset(lines, curline + 1)
187           continue
188       elif line.startswith("\\begin_inset Formula"):
189           line = line[20:]
190       elif line.startswith("\\begin_inset Quotes"):
191           # For now, we do a very basic reversion. Someone who understands
192           # quotes is welcome to fix it up.
193           qtype = line[20:].strip()
194           # lang = qtype[0]
195           side = qtype[1]
196           dbls = qtype[2]
197           if side == "l":
198               if dbls == "d":
199                   line = "``"
200               else:
201                   line = "`"
202           else:
203               if dbls == "d":
204                   line = "''"
205               else:
206                   line = "'"
207       elif line.startswith("\\begin_inset Newline newline"):
208           line = "\\\\ "
209       elif line.startswith("\\begin_inset space"):
210           line = line[18:].strip()
211           if line.startswith("\\hspace"):
212               # Account for both \hspace and \hspace*
213               hspace = line[:-2]
214               continue
215           elif line == "\\space{}":
216               line = "\\ "
217           elif line == "\\thinspace{}":
218               line = "\\,"
219       elif hspace != "":
220           # The LyX length is in line[8:], after the \length keyword
221           length = latex_length(line[8:])[1]
222           line = hspace + "{" + length + "}"
223           hspace = ""
224       elif line.isspace() or \
225             line.startswith("\\begin_layout") or \
226             line.startswith("\\end_layout") or \
227             line.startswith("\\begin_inset") or \
228             line.startswith("\\end_inset") or \
229             line.startswith("\\lang") or \
230             line.strip() == "status collapsed" or \
231             line.strip() == "status open":
232           #skip all that stuff
233           continue
234
235       # this needs to be added to the preamble because of cases like
236       # \textmu, \textbackslash, etc.
237       add_to_preamble(document, ['% added by lyx2lyx for converted index entries',
238                                  '\\@ifundefined{textmu}',
239                                  ' {\\usepackage{textcomp}}{}'])
240       # a lossless reversion is not possible
241       # try at least to handle some common insets and settings
242       if ert_end >= curline:
243           line = line.replace(r'\backslash', '\\')
244       else:
245           # No need to add "{}" after single-nonletter macros
246           line = line.replace('&', '\\&')
247           line = line.replace('#', '\\#')
248           line = line.replace('^', '\\textasciicircum{}')
249           line = line.replace('%', '\\%')
250           line = line.replace('_', '\\_')
251           line = line.replace('$', '\\$')
252
253           # Do the LyX text --> LaTeX conversion
254           for rep in unicode_reps:
255             line = line.replace(rep[1], rep[0] + "{}")
256           line = line.replace(r'\backslash', r'\textbackslash{}')
257           line = line.replace(r'\series bold', r'\bfseries{}').replace(r'\series default', r'\mdseries{}')
258           line = line.replace(r'\shape italic', r'\itshape{}').replace(r'\shape smallcaps', r'\scshape{}')
259           line = line.replace(r'\shape slanted', r'\slshape{}').replace(r'\shape default', r'\upshape{}')
260           line = line.replace(r'\emph on', r'\em{}').replace(r'\emph default', r'\em{}')
261           line = line.replace(r'\noun on', r'\scshape{}').replace(r'\noun default', r'\upshape{}')
262           line = line.replace(r'\bar under', r'\underbar{').replace(r'\bar default', r'}')
263           line = line.replace(r'\family sans', r'\sffamily{}').replace(r'\family default', r'\normalfont{}')
264           line = line.replace(r'\family typewriter', r'\ttfamily{}').replace(r'\family roman', r'\rmfamily{}')
265           line = line.replace(r'\InsetSpace ', r'').replace(r'\SpecialChar ', r'')
266       content += line
267     return content
268
269
270 def latex_length(slen):
271     ''' 
272     Convert lengths to their LaTeX representation. Returns (bool, length),
273     where the bool tells us if it was a percentage, and the length is the
274     LaTeX representation.
275     '''
276     i = 0
277     percent = False
278     # the slen has the form
279     # ValueUnit+ValueUnit-ValueUnit or
280     # ValueUnit+-ValueUnit
281     # the + and - (glue lengths) are optional
282     # the + always precedes the -
283
284     # Convert relative lengths to LaTeX units
285     units = {"text%":"\\textwidth", "col%":"\\columnwidth",
286              "page%":"\\paperwidth", "line%":"\\linewidth",
287              "theight%":"\\textheight", "pheight%":"\\paperheight"}
288     for unit in list(units.keys()):
289         i = slen.find(unit)
290         if i == -1:
291             continue
292         percent = True
293         minus = slen.rfind("-", 1, i)
294         plus = slen.rfind("+", 0, i)
295         latex_unit = units[unit]
296         if plus == -1 and minus == -1:
297             value = slen[:i]
298             value = str(float(value)/100)
299             end = slen[i + len(unit):]
300             slen = value + latex_unit + end
301         if plus > minus:
302             value = slen[plus + 1:i]
303             value = str(float(value)/100)
304             begin = slen[:plus + 1]
305             end = slen[i+len(unit):]
306             slen = begin + value + latex_unit + end
307         if plus < minus:
308             value = slen[minus + 1:i]
309             value = str(float(value)/100)
310             begin = slen[:minus + 1]
311             slen = begin + value + latex_unit
312
313     # replace + and -, but only if the - is not the first character
314     slen = slen[0] + slen[1:].replace("+", " plus ").replace("-", " minus ")
315     # handle the case where "+-1mm" was used, because LaTeX only understands
316     # "plus 1mm minus 1mm"
317     if slen.find("plus  minus"):
318         lastvaluepos = slen.rfind(" ")
319         lastvalue = slen[lastvaluepos:]
320         slen = slen.replace("  ", lastvalue + " ")
321     return (percent, slen)
322
323
324 def revert_flex_inset(lines, name, LaTeXname):
325   " Convert flex insets to TeX code "
326   i = 0
327   while True:
328     i = find_token(lines, '\\begin_inset Flex ' + name, i)
329     if i == -1:
330       return
331     z = find_end_of_inset(lines, i)
332     if z == -1:
333       document.warning("Can't find end of Flex " + name + " inset.")
334       i += 1
335       continue
336     # remove the \end_inset
337     lines[z - 2:z + 1] = put_cmd_in_ert("}")
338     # we need to reset character layouts if necessary
339     j = find_token(lines, '\\emph on', i, z)
340     k = find_token(lines, '\\noun on', i, z)
341     l = find_token(lines, '\\series', i, z)
342     m = find_token(lines, '\\family', i, z)
343     n = find_token(lines, '\\shape', i, z)
344     o = find_token(lines, '\\color', i, z)
345     p = find_token(lines, '\\size', i, z)
346     q = find_token(lines, '\\bar under', i, z)
347     r = find_token(lines, '\\uuline on', i, z)
348     s = find_token(lines, '\\uwave on', i, z)
349     t = find_token(lines, '\\strikeout on', i, z)
350     if j != -1:
351       lines.insert(z - 2, "\\emph default")
352     if k != -1:
353       lines.insert(z - 2, "\\noun default")
354     if l != -1:
355       lines.insert(z - 2, "\\series default")
356     if m != -1:
357       lines.insert(z - 2, "\\family default")
358     if n != -1:
359       lines.insert(z - 2, "\\shape default")
360     if o != -1:
361       lines.insert(z - 2, "\\color inherit")
362     if p != -1:
363       lines.insert(z - 2, "\\size default")
364     if q != -1:
365       lines.insert(z - 2, "\\bar default")
366     if r != -1:
367       lines.insert(z - 2, "\\uuline default")
368     if s != -1:
369       lines.insert(z - 2, "\\uwave default")
370     if t != -1:
371       lines.insert(z - 2, "\\strikeout default")
372     lines[i:i + 4] = put_cmd_in_ert(LaTeXname + "{")
373     i += 1
374
375
376 def revert_font_attrs(lines, name, LaTeXname):
377   " Reverts font changes to TeX code "
378   i = 0
379   changed = False
380   while True:
381     i = find_token(lines, name + ' on', i)
382     if i == -1:
383       return changed
384     j = find_token(lines, name + ' default', i)
385     k = find_token(lines, name + ' on', i + 1)
386     # if there is no default set, the style ends with the layout
387     # assure hereby that we found the correct layout end
388     if j != -1 and (j < k or k == -1):
389       lines[j:j + 1] = put_cmd_in_ert("}")
390     else:
391       j = find_token(lines, '\\end_layout', i)
392       lines[j:j] = put_cmd_in_ert("}")
393     lines[i:i + 1] = put_cmd_in_ert(LaTeXname + "{")
394     changed = True
395     i += 1
396
397
398 def revert_layout_command(lines, name, LaTeXname):
399   " Reverts a command from a layout to TeX code "
400   i = 0
401   while True:
402     i = find_token(lines, '\\begin_layout ' + name, i)
403     if i == -1:
404       return
405     k = -1
406     # find the next layout
407     j = i + 1
408     while k == -1:
409       j = find_token(lines, '\\begin_layout', j)
410       l = len(lines)
411       # if nothing was found it was the last layout of the document
412       if j == -1:
413         lines[l - 4:l - 4] = put_cmd_in_ert("}")
414         k = 0
415       # exclude plain layout because this can be TeX code or another inset
416       elif lines[j] != '\\begin_layout Plain Layout':
417         lines[j - 2:j - 2] = put_cmd_in_ert("}")
418         k = 0
419       else:
420         j += 1
421     lines[i] = '\\begin_layout Standard'
422     lines[i + 1:i + 1] = put_cmd_in_ert(LaTeXname + "{")
423     i += 1
424
425
426 def hex2ratio(s):
427   " Converts an RRGGBB-type hexadecimal string to a float in [0.0,1.0] "
428   try:
429     val = int(s, 16)
430   except:
431     val = 0
432   if val != 0:
433     val += 1
434   return str(val / 256.0)
435
436
437 def str2bool(s):
438   "'true' goes to True, case-insensitively, and we strip whitespace."
439   s = s.strip().lower()
440   return s == "true"