]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/lyx2lyx_tools.py
Support units in external inset bbox
[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 re
63 import string
64 from parser_tools import find_token, find_end_of_inset
65 from unicode_symbols import unicode_reps
66
67
68 # This will accept either a list of lines or a single line.
69 # It is bad practice to pass something with embedded newlines,
70 # though we will handle that.
71 def add_to_preamble(document, text):
72     " Add text to the preamble if it is not already there. "
73
74     if not type(text) is list:
75       # split on \n just in case
76       # it'll give us the one element list we want
77       # if there's no \n, too
78       text = text.split('\n')
79
80     i = 0
81     prelen = len(document.preamble)
82     while True:
83       i = find_token(document.preamble, text[0], i)
84       if i == -1:
85         break
86       # we need a perfect match
87       matched = True
88       for line in text:
89         if i >= prelen or line != document.preamble[i]:
90           matched = False
91           break
92         i += 1
93       if matched:
94         return
95
96     document.preamble.extend(["% Added by lyx2lyx"])
97     document.preamble.extend(text)
98
99
100 # Note that text can be either a list of lines or a single line.
101 # It should really be a list.
102 def insert_to_preamble(document, text, index = 0):
103     """ Insert text to the preamble at a given line"""
104     
105     if not type(text) is list:
106       # split on \n just in case
107       # it'll give us the one element list we want
108       # if there's no \n, too
109       text = text.split('\n')
110     
111     text.insert(0, "% Added by lyx2lyx")
112     document.preamble[index:index] = text
113
114
115 def put_cmd_in_ert(arg):
116     '''
117     arg should be a list of lines we want to wrap in ERT.
118     Returns a list of strings, with the lines so wrapped.
119     '''
120     
121     ret = ["\\begin_inset ERT", "status collapsed", "", "\\begin_layout Plain Layout", ""]
122     # It will be faster for us to work with a single string internally. 
123     # That way, we only go through the unicode_reps loop once.
124     if type(arg) is list:
125       s = "\n".join(arg)
126     else:
127       s = arg
128     for rep in unicode_reps:
129       s = s.replace(rep[1], rep[0])
130     s = s.replace('\\', "\\backslash\n")
131     ret += s.splitlines()
132     ret += ["\\end_layout", "", "\\end_inset"]
133     return ret
134
135
136 def get_ert(lines, i):
137     'Convert an ERT inset into LaTeX.'
138     if not lines[i].startswith("\\begin_inset ERT"):
139         return ""
140     j = find_end_of_inset(lines, i)
141     if j == -1:
142         return ""
143     while i < j and not lines[i].startswith("status"):
144         i = i + 1
145     i = i + 1
146     ret = ""
147     first = True
148     while i < j:
149         if lines[i] == "\\begin_layout Plain Layout":
150             if first:
151                 first = False
152             else:
153                 ret = ret + "\n"
154             while i + 1 < j and lines[i+1] == "":
155                 i = i + 1
156         elif lines[i] == "\\end_layout":
157             while i + 1 < j and lines[i+1] == "":
158                 i = i + 1
159         elif lines[i] == "\\backslash":
160             ret = ret + "\\"
161         else:
162             ret = ret + lines[i]
163         i = i + 1
164     return ret
165
166
167 def lyx2latex(document, lines):
168     'Convert some LyX stuff into corresponding LaTeX stuff, as best we can.'
169
170     content = ""
171     ert_end = 0
172     note_end = 0
173     hspace = ""
174
175     for curline in range(len(lines)):
176       line = lines[curline]
177       if line.startswith("\\begin_inset Note Note"):
178           # We want to skip LyX notes, so remember where the inset ends
179           note_end = find_end_of_inset(lines, curline + 1)
180           continue
181       elif note_end >= curline:
182           # Skip LyX notes
183           continue
184       elif line.startswith("\\begin_inset ERT"):
185           # We don't want to replace things inside ERT, so figure out
186           # where the end of the inset is.
187           ert_end = find_end_of_inset(lines, curline + 1)
188           continue
189       elif line.startswith("\\begin_inset Formula"):
190           line = line[20:]
191       elif line.startswith("\\begin_inset Quotes"):
192           # For now, we do a very basic reversion. Someone who understands
193           # quotes is welcome to fix it up.
194           qtype = line[20:].strip()
195           # lang = qtype[0]
196           side = qtype[1]
197           dbls = qtype[2]
198           if side == "l":
199               if dbls == "d":
200                   line = "``"
201               else:
202                   line = "`"
203           else:
204               if dbls == "d":
205                   line = "''"
206               else:
207                   line = "'"
208       elif line.startswith("\\begin_inset Newline newline"):
209           line = "\\\\ "
210       elif line.startswith("\\noindent"):
211           line = "\\noindent " # we need the space behind the command
212       elif line.startswith("\\begin_inset space"):
213           line = line[18:].strip()
214           if line.startswith("\\hspace"):
215               # Account for both \hspace and \hspace*
216               hspace = line[:-2]
217               continue
218           elif line == "\\space{}":
219               line = "\\ "
220           elif line == "\\thinspace{}":
221               line = "\\,"
222       elif hspace != "":
223           # The LyX length is in line[8:], after the \length keyword
224           length = latex_length(line[8:])[1]
225           line = hspace + "{" + length + "}"
226           hspace = ""
227       elif line.isspace() or \
228             line.startswith("\\begin_layout") or \
229             line.startswith("\\end_layout") or \
230             line.startswith("\\begin_inset") or \
231             line.startswith("\\end_inset") or \
232             line.startswith("\\lang") or \
233             line.strip() == "status collapsed" or \
234             line.strip() == "status open":
235           #skip all that stuff
236           continue
237
238       # this needs to be added to the preamble because of cases like
239       # \textmu, \textbackslash, etc.
240       add_to_preamble(document, ['% added by lyx2lyx for converted index entries',
241                                  '\\@ifundefined{textmu}',
242                                  ' {\\usepackage{textcomp}}{}'])
243       # a lossless reversion is not possible
244       # try at least to handle some common insets and settings
245       if ert_end >= curline:
246           line = line.replace(r'\backslash', '\\')
247       else:
248           # No need to add "{}" after single-nonletter macros
249           line = line.replace('&', '\\&')
250           line = line.replace('#', '\\#')
251           line = line.replace('^', '\\textasciicircum{}')
252           line = line.replace('%', '\\%')
253           line = line.replace('_', '\\_')
254           line = line.replace('$', '\\$')
255
256           # Do the LyX text --> LaTeX conversion
257           for rep in unicode_reps:
258               line = line.replace(rep[1], rep[0])
259           line = line.replace(r'\backslash', r'\textbackslash{}')
260           line = line.replace(r'\series bold', r'\bfseries{}').replace(r'\series default', r'\mdseries{}')
261           line = line.replace(r'\shape italic', r'\itshape{}').replace(r'\shape smallcaps', r'\scshape{}')
262           line = line.replace(r'\shape slanted', r'\slshape{}').replace(r'\shape default', r'\upshape{}')
263           line = line.replace(r'\emph on', r'\em{}').replace(r'\emph default', r'\em{}')
264           line = line.replace(r'\noun on', r'\scshape{}').replace(r'\noun default', r'\upshape{}')
265           line = line.replace(r'\bar under', r'\underbar{').replace(r'\bar default', r'}')
266           line = line.replace(r'\family sans', r'\sffamily{}').replace(r'\family default', r'\normalfont{}')
267           line = line.replace(r'\family typewriter', r'\ttfamily{}').replace(r'\family roman', r'\rmfamily{}')
268           line = line.replace(r'\InsetSpace ', r'').replace(r'\SpecialChar ', r'')
269       content += line
270     return content
271
272
273 def latex_length(slen):
274     ''' 
275     Convert lengths to their LaTeX representation. Returns (bool, length),
276     where the bool tells us if it was a percentage, and the length is the
277     LaTeX representation.
278     '''
279     i = 0
280     percent = False
281     # the slen has the form
282     # ValueUnit+ValueUnit-ValueUnit or
283     # ValueUnit+-ValueUnit
284     # the + and - (glue lengths) are optional
285     # the + always precedes the -
286
287     # Convert relative lengths to LaTeX units
288     units = {"text%":"\\textwidth", "col%":"\\columnwidth",
289              "page%":"\\paperwidth", "line%":"\\linewidth",
290              "theight%":"\\textheight", "pheight%":"\\paperheight"}
291     for unit in list(units.keys()):
292         i = slen.find(unit)
293         if i == -1:
294             continue
295         percent = True
296         minus = slen.rfind("-", 1, i)
297         plus = slen.rfind("+", 0, i)
298         latex_unit = units[unit]
299         if plus == -1 and minus == -1:
300             value = slen[:i]
301             value = str(float(value)/100)
302             end = slen[i + len(unit):]
303             slen = value + latex_unit + end
304         if plus > minus:
305             value = slen[plus + 1:i]
306             value = str(float(value)/100)
307             begin = slen[:plus + 1]
308             end = slen[i+len(unit):]
309             slen = begin + value + latex_unit + end
310         if plus < minus:
311             value = slen[minus + 1:i]
312             value = str(float(value)/100)
313             begin = slen[:minus + 1]
314             slen = begin + value + latex_unit
315
316     # replace + and -, but only if the - is not the first character
317     slen = slen[0] + slen[1:].replace("+", " plus ").replace("-", " minus ")
318     # handle the case where "+-1mm" was used, because LaTeX only understands
319     # "plus 1mm minus 1mm"
320     if slen.find("plus  minus"):
321         lastvaluepos = slen.rfind(" ")
322         lastvalue = slen[lastvaluepos:]
323         slen = slen.replace("  ", lastvalue + " ")
324     return (percent, slen)
325
326
327 def length_in_bp(length):
328     " Convert a length in LyX format to its value in bp units "
329
330     em_width = 10.0 / 72.27 # assume 10pt font size
331     text_width = 8.27 / 1.7 # assume A4 with default margins
332     # scale factors are taken from Length::inInch()
333     scales = {"bp"       : 1.0,
334               "cc"       : (72.0 / (72.27 / (12.0 * 0.376 * 2.845))),
335               "cm"       : (72.0 / 2.54),
336               "dd"       : (72.0 / (72.27 / (0.376 * 2.845))),
337               "em"       : (72.0 * em_width),
338               "ex"       : (72.0 * em_width * 0.4305),
339               "in"       : 72.0,
340               "mm"       : (72.0 / 25.4),
341               "mu"       : (72.0 * em_width / 18.0),
342               "pc"       : (72.0 / (72.27 / 12.0)),
343               "pt"       : (72.0 / (72.27)),
344               "sp"       : (72.0 / (72.27 * 65536.0)),
345               "text%"    : (72.0 * text_width / 100.0),
346               "col%"     : (72.0 * text_width / 100.0), # assume 1 column
347               "page%"    : (72.0 * text_width * 1.7 / 100.0),
348               "line%"    : (72.0 * text_width / 100.0),
349               "theight%" : (72.0 * text_width * 1.787 / 100.0),
350               "pheight%" : (72.0 * text_width * 2.2 / 100.0)}
351
352     rx = re.compile(r'^\s*([^a-zA-Z%]+)([a-zA-Z%]+)\s*$')
353     m = rx.match(length)
354     if not m:
355         document.warning("Invalid length value: " + length + ".")
356         return 0
357     value = m.group(1)
358     unit = m.group(2)
359     if not unit in scales.keys():
360         document.warning("Unknown length unit: " + unit + ".")
361         return value
362     return "%g" % (float(value) * scales[unit])
363
364
365 def revert_flex_inset(lines, name, LaTeXname):
366   " Convert flex insets to TeX code "
367   i = 0
368   while True:
369     i = find_token(lines, '\\begin_inset Flex ' + name, i)
370     if i == -1:
371       return
372     z = find_end_of_inset(lines, i)
373     if z == -1:
374       document.warning("Can't find end of Flex " + name + " inset.")
375       i += 1
376       continue
377     # remove the \end_inset
378     lines[z - 2:z + 1] = put_cmd_in_ert("}")
379     # we need to reset character layouts if necessary
380     j = find_token(lines, '\\emph on', i, z)
381     k = find_token(lines, '\\noun on', i, z)
382     l = find_token(lines, '\\series', i, z)
383     m = find_token(lines, '\\family', i, z)
384     n = find_token(lines, '\\shape', i, z)
385     o = find_token(lines, '\\color', i, z)
386     p = find_token(lines, '\\size', i, z)
387     q = find_token(lines, '\\bar under', i, z)
388     r = find_token(lines, '\\uuline on', i, z)
389     s = find_token(lines, '\\uwave on', i, z)
390     t = find_token(lines, '\\strikeout on', i, z)
391     if j != -1:
392       lines.insert(z - 2, "\\emph default")
393     if k != -1:
394       lines.insert(z - 2, "\\noun default")
395     if l != -1:
396       lines.insert(z - 2, "\\series default")
397     if m != -1:
398       lines.insert(z - 2, "\\family default")
399     if n != -1:
400       lines.insert(z - 2, "\\shape default")
401     if o != -1:
402       lines.insert(z - 2, "\\color inherit")
403     if p != -1:
404       lines.insert(z - 2, "\\size default")
405     if q != -1:
406       lines.insert(z - 2, "\\bar default")
407     if r != -1:
408       lines.insert(z - 2, "\\uuline default")
409     if s != -1:
410       lines.insert(z - 2, "\\uwave default")
411     if t != -1:
412       lines.insert(z - 2, "\\strikeout default")
413     lines[i:i + 4] = put_cmd_in_ert(LaTeXname + "{")
414     i += 1
415
416
417 def revert_font_attrs(lines, name, LaTeXname):
418   " Reverts font changes to TeX code "
419   i = 0
420   changed = False
421   while True:
422     i = find_token(lines, name + ' on', i)
423     if i == -1:
424       return changed
425     j = find_token(lines, name + ' default', i)
426     k = find_token(lines, name + ' on', i + 1)
427     # if there is no default set, the style ends with the layout
428     # assure hereby that we found the correct layout end
429     if j != -1 and (j < k or k == -1):
430       lines[j:j + 1] = put_cmd_in_ert("}")
431     else:
432       j = find_token(lines, '\\end_layout', i)
433       lines[j:j] = put_cmd_in_ert("}")
434     lines[i:i + 1] = put_cmd_in_ert(LaTeXname + "{")
435     changed = True
436     i += 1
437
438
439 def revert_layout_command(lines, name, LaTeXname):
440   " Reverts a command from a layout to TeX code "
441   i = 0
442   while True:
443     i = find_token(lines, '\\begin_layout ' + name, i)
444     if i == -1:
445       return
446     k = -1
447     # find the next layout
448     j = i + 1
449     while k == -1:
450       j = find_token(lines, '\\begin_layout', j)
451       l = len(lines)
452       # if nothing was found it was the last layout of the document
453       if j == -1:
454         lines[l - 4:l - 4] = put_cmd_in_ert("}")
455         k = 0
456       # exclude plain layout because this can be TeX code or another inset
457       elif lines[j] != '\\begin_layout Plain Layout':
458         lines[j - 2:j - 2] = put_cmd_in_ert("}")
459         k = 0
460       else:
461         j += 1
462     lines[i] = '\\begin_layout Standard'
463     lines[i + 1:i + 1] = put_cmd_in_ert(LaTeXname + "{")
464     i += 1
465
466
467 def hex2ratio(s):
468   " Converts an RRGGBB-type hexadecimal string to a float in [0.0,1.0] "
469   try:
470     val = int(s, 16)
471   except:
472     val = 0
473   if val != 0:
474     val += 1
475   return str(val / 256.0)
476
477
478 def str2bool(s):
479   "'true' goes to True, case-insensitively, and we strip whitespace."
480   s = s.strip().lower()
481   return s == "true"