]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/lyx2lyx_tools.py
Don't use widest label for numerical citations.
[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 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, 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 space"):
208           line = line[18:].strip()
209           if line.startswith("\\hspace"):
210               # Account for both \hspace and \hspace*
211               hspace = line[:-2]
212               continue
213           elif line == "\\space{}":
214               line = "\\ "
215           elif line == "\\thinspace{}":
216               line = "\\,"
217       elif hspace != "":
218           # The LyX length is in line[8:], after the \length keyword
219           length = latex_length(line[8:])[1]
220           line = hspace + "{" + length + "}"
221           hspace = ""
222       elif line.isspace() or \
223             line.startswith("\\begin_layout") or \
224             line.startswith("\\end_layout") or \
225             line.startswith("\\begin_inset") or \
226             line.startswith("\\end_inset") or \
227             line.startswith("\\lang") or \
228             line.strip() == "status collapsed" or \
229             line.strip() == "status open":
230           #skip all that stuff
231           continue
232
233       # this needs to be added to the preamble because of cases like
234       # \textmu, \textbackslash, etc.
235       add_to_preamble(document, ['% added by lyx2lyx for converted index entries',
236                                  '\\@ifundefined{textmu}',
237                                  ' {\\usepackage{textcomp}}{}'])
238       # a lossless reversion is not possible
239       # try at least to handle some common insets and settings
240       if ert_end >= curline:
241           line = line.replace(r'\backslash', '\\')
242       else:
243           # No need to add "{}" after single-nonletter macros
244           line = line.replace('&', '\\&')
245           line = line.replace('#', '\\#')
246           line = line.replace('^', '\\textasciicircum{}')
247           line = line.replace('%', '\\%')
248           line = line.replace('_', '\\_')
249           line = line.replace('$', '\\$')
250
251           # Do the LyX text --> LaTeX conversion
252           for rep in unicode_reps:
253             line = line.replace(rep[1], rep[0] + "{}")
254           line = line.replace(r'\backslash', r'\textbackslash{}')
255           line = line.replace(r'\series bold', r'\bfseries{}').replace(r'\series default', r'\mdseries{}')
256           line = line.replace(r'\shape italic', r'\itshape{}').replace(r'\shape smallcaps', r'\scshape{}')
257           line = line.replace(r'\shape slanted', r'\slshape{}').replace(r'\shape default', r'\upshape{}')
258           line = line.replace(r'\emph on', r'\em{}').replace(r'\emph default', r'\em{}')
259           line = line.replace(r'\noun on', r'\scshape{}').replace(r'\noun default', r'\upshape{}')
260           line = line.replace(r'\bar under', r'\underbar{').replace(r'\bar default', r'}')
261           line = line.replace(r'\family sans', r'\sffamily{}').replace(r'\family default', r'\normalfont{}')
262           line = line.replace(r'\family typewriter', r'\ttfamily{}').replace(r'\family roman', r'\rmfamily{}')
263           line = line.replace(r'\InsetSpace ', r'').replace(r'\SpecialChar ', r'')
264       content += line
265     return content
266
267
268 def latex_length(slen):
269     ''' 
270     Convert lengths to their LaTeX representation. Returns (bool, length),
271     where the bool tells us if it was a percentage, and the length is the
272     LaTeX representation.
273     '''
274     i = 0
275     percent = False
276     # the slen has the form
277     # ValueUnit+ValueUnit-ValueUnit or
278     # ValueUnit+-ValueUnit
279     # the + and - (glue lengths) are optional
280     # the + always precedes the -
281
282     # Convert relative lengths to LaTeX units
283     units = {"text%":"\\textwidth", "col%":"\\columnwidth",
284              "page%":"\\paperwidth", "line%":"\\linewidth",
285              "theight%":"\\textheight", "pheight%":"\\paperheight"}
286     for unit in units.keys():
287         i = slen.find(unit)
288         if i == -1:
289             continue
290         percent = True
291         minus = slen.rfind("-", 1, i)
292         plus = slen.rfind("+", 0, i)
293         latex_unit = units[unit]
294         if plus == -1 and minus == -1:
295             value = slen[:i]
296             value = str(float(value)/100)
297             end = slen[i + len(unit):]
298             slen = value + latex_unit + end
299         if plus > minus:
300             value = slen[plus + 1:i]
301             value = str(float(value)/100)
302             begin = slen[:plus + 1]
303             end = slen[i+len(unit):]
304             slen = begin + value + latex_unit + end
305         if plus < minus:
306             value = slen[minus + 1:i]
307             value = str(float(value)/100)
308             begin = slen[:minus + 1]
309             slen = begin + value + latex_unit
310
311     # replace + and -, but only if the - is not the first character
312     slen = slen[0] + slen[1:].replace("+", " plus ").replace("-", " minus ")
313     # handle the case where "+-1mm" was used, because LaTeX only understands
314     # "plus 1mm minus 1mm"
315     if slen.find("plus  minus"):
316         lastvaluepos = slen.rfind(" ")
317         lastvalue = slen[lastvaluepos:]
318         slen = slen.replace("  ", lastvalue + " ")
319     return (percent, slen)
320
321
322 def revert_flex_inset(lines, name, LaTeXname):
323   " Convert flex insets to TeX code "
324   i = 0
325   while True:
326     i = find_token(lines, '\\begin_inset Flex ' + name, i)
327     if i == -1:
328       return
329     z = find_end_of_inset(lines, i)
330     if z == -1:
331       document.warning("Can't find end of Flex " + name + " inset.")
332       i += 1
333       continue
334     # remove the \end_inset
335     lines[z - 2:z + 1] = put_cmd_in_ert("}")
336     # we need to reset character layouts if necessary
337     j = find_token(lines, '\\emph on', i, z)
338     k = find_token(lines, '\\noun on', i, z)
339     l = find_token(lines, '\\series', i, z)
340     m = find_token(lines, '\\family', i, z)
341     n = find_token(lines, '\\shape', i, z)
342     o = find_token(lines, '\\color', i, z)
343     p = find_token(lines, '\\size', i, z)
344     q = find_token(lines, '\\bar under', i, z)
345     r = find_token(lines, '\\uuline on', i, z)
346     s = find_token(lines, '\\uwave on', i, z)
347     t = find_token(lines, '\\strikeout on', i, z)
348     if j != -1:
349       lines.insert(z - 2, "\\emph default")
350     if k != -1:
351       lines.insert(z - 2, "\\noun default")
352     if l != -1:
353       lines.insert(z - 2, "\\series default")
354     if m != -1:
355       lines.insert(z - 2, "\\family default")
356     if n != -1:
357       lines.insert(z - 2, "\\shape default")
358     if o != -1:
359       lines.insert(z - 2, "\\color inherit")
360     if p != -1:
361       lines.insert(z - 2, "\\size default")
362     if q != -1:
363       lines.insert(z - 2, "\\bar default")
364     if r != -1:
365       lines.insert(z - 2, "\\uuline default")
366     if s != -1:
367       lines.insert(z - 2, "\\uwave default")
368     if t != -1:
369       lines.insert(z - 2, "\\strikeout default")
370     lines[i:i + 4] = put_cmd_in_ert(LaTeXname + "{")
371     i += 1
372
373
374 def revert_font_attrs(lines, name, LaTeXname):
375   " Reverts font changes to TeX code "
376   i = 0
377   changed = False
378   while True:
379     i = find_token(lines, name + ' on', i)
380     if i == -1:
381       return changed
382     j = find_token(lines, name + ' default', i)
383     k = find_token(lines, name + ' on', i + 1)
384     # if there is no default set, the style ends with the layout
385     # assure hereby that we found the correct layout end
386     if j != -1 and (j < k or k == -1):
387       lines[j:j + 1] = put_cmd_in_ert("}")
388     else:
389       j = find_token(lines, '\\end_layout', i)
390       lines[j:j] = put_cmd_in_ert("}")
391     lines[i:i + 1] = put_cmd_in_ert(LaTeXname + "{")
392     changed = True
393     i += 1
394
395
396 def revert_layout_command(lines, name, LaTeXname):
397   " Reverts a command from a layout to TeX code "
398   i = 0
399   while True:
400     i = find_token(lines, '\\begin_layout ' + name, i)
401     if i == -1:
402       return
403     k = -1
404     # find the next layout
405     j = i + 1
406     while k == -1:
407       j = find_token(lines, '\\begin_layout', j)
408       l = len(lines)
409       # if nothing was found it was the last layout of the document
410       if j == -1:
411         lines[l - 4:l - 4] = put_cmd_in_ert("}")
412         k = 0
413       # exclude plain layout because this can be TeX code or another inset
414       elif lines[j] != '\\begin_layout Plain Layout':
415         lines[j - 2:j - 2] = put_cmd_in_ert("}")
416         k = 0
417       else:
418         j += 1
419     lines[i] = '\\begin_layout Standard'
420     lines[i + 1:i + 1] = put_cmd_in_ert(LaTeXname + "{")
421     i += 1
422
423
424 def hex2ratio(s):
425   " Converts an RRGGBB-type hexadecimal string to a float in [0.0,1.0] "
426   try:
427     val = int(s, 16)
428   except:
429     val = 0
430   if val != 0:
431     val += 1
432   return str(val / 256.0)
433
434
435 def str2bool(s):
436   "'true' goes to True, case-insensitively, and we strip whitespace."
437   s = s.strip().lower()
438   return s == "true"