]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/lyx2lyx_tools.py
Minor style.
[lyx.git] / lib / lyx2lyx / lyx2lyx_tools.py
1 # This file is part of lyx2lyx
2 # -*- coding: utf-8 -*-
3 # Copyright (C) 2010 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 '''
20 This modules offer 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
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 lyx2latex(document, lines):
136     'Convert some LyX stuff into corresponding LaTeX stuff, as best we can.'
137
138     content = ""
139     ert_end = 0
140     note_end = 0
141     hspace = ""
142
143     for curline in range(len(lines)):
144       line = lines[curline]
145       if line.startswith("\\begin_inset Note Note"):
146           # We want to skip LyX notes, so remember where the inset ends
147           note_end = find_end_of_inset(lines, curline + 1)
148           continue
149       elif note_end >= curline:
150           # Skip LyX notes
151           continue
152       elif line.startswith("\\begin_inset ERT"):
153           # We don't want to replace things inside ERT, so figure out
154           # where the end of the inset is.
155           ert_end = find_end_of_inset(lines, curline + 1)
156           continue
157       elif line.startswith("\\begin_inset Formula"):
158           line = line[20:]
159       elif line.startswith("\\begin_inset Quotes"):
160           # For now, we do a very basic reversion. Someone who understands
161           # quotes is welcome to fix it up.
162           qtype = line[20:].strip()
163           # lang = qtype[0]
164           side = qtype[1]
165           dbls = qtype[2]
166           if side == "l":
167               if dbls == "d":
168                   line = "``"
169               else:
170                   line = "`"
171           else:
172               if dbls == "d":
173                   line = "''"
174               else:
175                   line = "'"
176       elif line.startswith("\\begin_inset space"):
177           line = line[18:].strip()
178           if line.startswith("\\hspace"):
179               # Account for both \hspace and \hspace*
180               hspace = line[:-2]
181               continue
182           elif line == "\\space{}":
183               line = "\\ "
184           elif line == "\\thinspace{}":
185               line = "\\,"
186       elif hspace != "":
187           # The LyX length is in line[8:], after the \length keyword
188           length = latex_length(line[8:])[1]
189           line = hspace + "{" + length + "}"
190           hspace = ""
191       elif line.isspace() or \
192             line.startswith("\\begin_layout") or \
193             line.startswith("\\end_layout") or \
194             line.startswith("\\begin_inset") or \
195             line.startswith("\\end_inset") or \
196             line.startswith("\\lang") or \
197             line.strip() == "status collapsed" or \
198             line.strip() == "status open":
199           #skip all that stuff
200           continue
201
202       # this needs to be added to the preamble because of cases like
203       # \textmu, \textbackslash, etc.
204       add_to_preamble(document, ['% added by lyx2lyx for converted index entries',
205                                  '\\@ifundefined{textmu}',
206                                  ' {\\usepackage{textcomp}}{}'])
207       # a lossless reversion is not possible
208       # try at least to handle some common insets and settings
209       if ert_end >= curline:
210           line = line.replace(r'\backslash', '\\')
211       else:
212           # No need to add "{}" after single-nonletter macros
213           line = line.replace('&', '\\&')
214           line = line.replace('#', '\\#')
215           line = line.replace('^', '\\textasciicircum{}')
216           line = line.replace('%', '\\%')
217           line = line.replace('_', '\\_')
218           line = line.replace('$', '\\$')
219
220           # Do the LyX text --> LaTeX conversion
221           for rep in unicode_reps:
222             line = line.replace(rep[1], rep[0] + "{}")
223           line = line.replace(r'\backslash', r'\textbackslash{}')
224           line = line.replace(r'\series bold', r'\bfseries{}').replace(r'\series default', r'\mdseries{}')
225           line = line.replace(r'\shape italic', r'\itshape{}').replace(r'\shape smallcaps', r'\scshape{}')
226           line = line.replace(r'\shape slanted', r'\slshape{}').replace(r'\shape default', r'\upshape{}')
227           line = line.replace(r'\emph on', r'\em{}').replace(r'\emph default', r'\em{}')
228           line = line.replace(r'\noun on', r'\scshape{}').replace(r'\noun default', r'\upshape{}')
229           line = line.replace(r'\bar under', r'\underbar{').replace(r'\bar default', r'}')
230           line = line.replace(r'\family sans', r'\sffamily{}').replace(r'\family default', r'\normalfont{}')
231           line = line.replace(r'\family typewriter', r'\ttfamily{}').replace(r'\family roman', r'\rmfamily{}')
232           line = line.replace(r'\InsetSpace ', r'').replace(r'\SpecialChar ', r'')
233       content += line
234     return content
235
236
237 def latex_length(slen):
238     ''' 
239     Convert lengths to their LaTeX representation. Returns (bool, length),
240     where the bool tells us if it was a percentage, and the length is the
241     LaTeX representation.
242     '''
243     i = 0
244     percent = False
245     # the slen has the form
246     # ValueUnit+ValueUnit-ValueUnit or
247     # ValueUnit+-ValueUnit
248     # the + and - (glue lengths) are optional
249     # the + always precedes the -
250
251     # Convert relative lengths to LaTeX units
252     units = {"text%":"\\textwidth", "col%":"\\columnwidth",
253              "page%":"\\paperwidth", "line%":"\\linewidth",
254              "theight%":"\\textheight", "pheight%":"\\paperheight"}
255     for unit in units.keys():
256         i = slen.find(unit)
257         if i == -1:
258             continue
259         percent = True
260         minus = slen.rfind("-", 1, i)
261         plus = slen.rfind("+", 0, i)
262         latex_unit = units[unit]
263         if plus == -1 and minus == -1:
264             value = slen[:i]
265             value = str(float(value)/100)
266             end = slen[i + len(unit):]
267             slen = value + latex_unit + end
268         if plus > minus:
269             value = slen[plus + 1:i]
270             value = str(float(value)/100)
271             begin = slen[:plus + 1]
272             end = slen[i+len(unit):]
273             slen = begin + value + latex_unit + end
274         if plus < minus:
275             value = slen[minus + 1:i]
276             value = str(float(value)/100)
277             begin = slen[:minus + 1]
278             slen = begin + value + latex_unit
279
280     # replace + and -, but only if the - is not the first character
281     slen = slen[0] + slen[1:].replace("+", " plus ").replace("-", " minus ")
282     # handle the case where "+-1mm" was used, because LaTeX only understands
283     # "plus 1mm minus 1mm"
284     if slen.find("plus  minus"):
285         lastvaluepos = slen.rfind(" ")
286         lastvalue = slen[lastvaluepos:]
287         slen = slen.replace("  ", lastvalue + " ")
288     return (percent, slen)
289
290
291 def revert_flex_inset(lines, name, LaTeXname):
292   " Convert flex insets to TeX code "
293   i = 0
294   while True:
295     i = find_token(lines, '\\begin_inset Flex ' + name, i)
296     if i == -1:
297       return
298     z = find_end_of_inset(lines, i)
299     if z == -1:
300       document.warning("Can't find end of Flex " + name + " inset.")
301       i += 1
302       continue
303     # remove the \end_inset
304     lines[z - 2:z + 1] = put_cmd_in_ert("}")
305     # we need to reset character layouts if necessary
306     j = find_token(lines, '\\emph on', i, z)
307     k = find_token(lines, '\\noun on', i, z)
308     l = find_token(lines, '\\series', i, z)
309     m = find_token(lines, '\\family', i, z)
310     n = find_token(lines, '\\shape', i, z)
311     o = find_token(lines, '\\color', i, z)
312     p = find_token(lines, '\\size', i, z)
313     q = find_token(lines, '\\bar under', i, z)
314     r = find_token(lines, '\\uuline on', i, z)
315     s = find_token(lines, '\\uwave on', i, z)
316     t = find_token(lines, '\\strikeout on', i, z)
317     if j != -1:
318       lines.insert(z - 2, "\\emph default")
319     if k != -1:
320       lines.insert(z - 2, "\\noun default")
321     if l != -1:
322       lines.insert(z - 2, "\\series default")
323     if m != -1:
324       lines.insert(z - 2, "\\family default")
325     if n != -1:
326       lines.insert(z - 2, "\\shape default")
327     if o != -1:
328       lines.insert(z - 2, "\\color inherit")
329     if p != -1:
330       lines.insert(z - 2, "\\size default")
331     if q != -1:
332       lines.insert(z - 2, "\\bar default")
333     if r != -1:
334       lines.insert(z - 2, "\\uuline default")
335     if s != -1:
336       lines.insert(z - 2, "\\uwave default")
337     if t != -1:
338       lines.insert(z - 2, "\\strikeout default")
339     lines[i:i + 4] = put_cmd_in_ert(LaTeXname + "{")
340     i += 1
341
342
343 def revert_font_attrs(lines, name, LaTeXname):
344   " Reverts font changes to TeX code "
345   i = 0
346   changed = False
347   while True:
348     i = find_token(lines, name + ' on', i)
349     if i == -1:
350       return changed
351     j = find_token(lines, name + ' default', i)
352     k = find_token(lines, name + ' on', i + 1)
353     # if there is no default set, the style ends with the layout
354     # assure hereby that we found the correct layout end
355     if j != -1 and (j < k or k == -1):
356       lines[j:j + 1] = put_cmd_in_ert("}")
357     else:
358       j = find_token(lines, '\\end_layout', i)
359       lines[j:j] = put_cmd_in_ert("}")
360     lines[i:i + 1] = put_cmd_in_ert(LaTeXname + "{")
361     changed = True
362     i += 1
363
364
365 def revert_layout_command(lines, name, LaTeXname):
366   " Reverts a command from a layout to TeX code "
367   i = 0
368   while True:
369     i = find_token(lines, '\\begin_layout ' + name, i)
370     if i == -1:
371       return
372     k = -1
373     # find the next layout
374     j = i + 1
375     while k == -1:
376       j = find_token(lines, '\\begin_layout', j)
377       l = len(lines)
378       # if nothing was found it was the last layout of the document
379       if j == -1:
380         lines[l - 4:l - 4] = put_cmd_in_ert("}")
381         k = 0
382       # exclude plain layout because this can be TeX code or another inset
383       elif lines[j] != '\\begin_layout Plain Layout':
384         lines[j - 2:j - 2] = put_cmd_in_ert("}")
385         k = 0
386       else:
387         j += 1
388     lines[i] = '\\begin_layout Standard'
389     lines[i + 1:i + 1] = put_cmd_in_ert(LaTeXname + "{")
390     i += 1
391
392
393 def hex2ratio(s):
394   " Converts an RRGGBB-type hexadecimal string to a float in [0.0,1.0] "
395   try:
396     val = int(s, 16)
397   except:
398     val = 0
399   if val != 0:
400     val += 1
401   return str(val / 256.0)
402
403
404 def str2bool(s):
405   "'true' goes to True, case-insensitively, and we strip whitespace."
406   s = s.strip().lower()
407   return s == "true"