1 # This file is part of lyx2lyx
2 # -*- coding: utf-8 -*-
3 # Copyright (C) 2010 The LyX team
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.
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.
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.
19 " This modules offer several free functions to help with lyx2lyx'ing. "
22 from parser_tools import find_token
23 from unicode_symbols import unicode_reps
26 # This will accept either a list of lines or a single line.
27 # It is bad practice to pass something with embedded newlines,
28 # though we will handle that.
29 def add_to_preamble(document, text):
30 " Add text to the preamble if it is not already there. "
32 if not type(text) is list:
33 # split on \n just in case
34 # it'll give us the one element list we want
35 # if there's no \n, too
36 text = text.split('\n')
39 prelen = len(document.preamble)
41 i = find_token(document.preamble, text[0], i)
44 # we need a perfect match
47 if i >= prelen or line != document.preamble[i]:
54 document.preamble.extend(text)
57 # Note that text can be either a list of lines or a single line.
58 # It should really be a list.
59 def insert_to_preamble(index, document, text):
60 """ Insert text to the preamble at a given line"""
62 if not type(text) is list:
63 # split on \n just in case
64 # it'll give us the one element list we want
65 # if there's no \n, too
66 text = text.split('\n')
68 document.preamble[index:index] = text
71 # This routine wraps some content in an ERT inset.
73 # NOTE: The function accepts either a single string or a LIST of strings as
74 # argument. But it returns a LIST of strings, split on \n, so that it does
75 # not have embedded newlines.
77 # This is how lyx2lyx represents a LyX document: as a list of strings,
78 # each representing a line of a LyX file. Embedded newlines confuse
81 # A call to this routine will often go something like this:
82 # i = find_token('\\begin_inset FunkyInset', ...)
84 # j = find_end_of_inset(document.body, i)
85 # content = ...extract content from insets
86 # # that could be as simple as:
87 # # content = lyx2latex(document[i:j + 1])
88 # ert = put_cmd_in_ert(content)
89 # document.body[i:j] = ert
90 # Now, before we continue, we need to reset i appropriately. Normally,
93 # That puts us right after the ERT we just inserted.
95 def put_cmd_in_ert(arg):
96 ret = ["\\begin_inset ERT", "status collapsed", "\\begin_layout Plain Layout", ""]
97 # Despite the warnings just given, it will be faster for us to work
98 # with a single string internally. That way, we only go through the
99 # unicode_reps loop once.
100 if type(arg) is list:
104 for rep in unicode_reps:
105 s = s.replace(rep[1], rep[0].replace('\\\\', '\\'))
106 s = s.replace('\\', "\\backslash\n")
107 ret += s.splitlines()
108 ret += ["\\end_layout", "\\end_inset"]
112 def lyx2latex(document, lines):
113 'Convert some LyX stuff into corresponding LaTeX stuff, as best we can.'
114 # clean up multiline stuff
120 for curline in range(len(lines)):
121 line = lines[curline]
122 if line.startswith("\\begin_inset Note Note"):
123 # We want to skip LyX notes, so remember where the inset ends
124 note_end = find_end_of_inset(lines, curline + 1)
126 elif note_end >= curline:
129 elif line.startswith("\\begin_inset ERT"):
130 # We don't want to replace things inside ERT, so figure out
131 # where the end of the inset is.
132 ert_end = find_end_of_inset(lines, curline + 1)
134 elif line.startswith("\\begin_inset Formula"):
136 elif line.startswith("\\begin_inset Quotes"):
137 # For now, we do a very basic reversion. Someone who understands
138 # quotes is welcome to fix it up.
139 qtype = line[20:].strip()
153 elif line.startswith("\\begin_inset space"):
154 line = line[18:].strip()
155 if line.startswith("\\hspace"):
156 # Account for both \hspace and \hspace*
159 elif line == "\\space{}":
161 elif line == "\\thinspace{}":
164 # The LyX length is in line[8:], after the \length keyword
165 length = latex_length(line[8:])[1]
166 line = hspace + "{" + length + "}"
168 elif line.isspace() or \
169 line.startswith("\\begin_layout") or \
170 line.startswith("\\end_layout") or \
171 line.startswith("\\begin_inset") or \
172 line.startswith("\\end_inset") or \
173 line.startswith("\\lang") or \
174 line.strip() == "status collapsed" or \
175 line.strip() == "status open":
179 # this needs to be added to the preamble because of cases like
180 # \textmu, \textbackslash, etc.
181 add_to_preamble(document, ['% added by lyx2lyx for converted index entries',
182 '\\@ifundefined{textmu}',
183 ' {\\usepackage{textcomp}}{}'])
184 # a lossless reversion is not possible
185 # try at least to handle some common insets and settings
186 if ert_end >= curline:
187 line = line.replace(r'\backslash', '\\')
189 # No need to add "{}" after single-nonletter macros
190 line = line.replace('&', '\\&')
191 line = line.replace('#', '\\#')
192 line = line.replace('^', '\\textasciicircum{}')
193 line = line.replace('%', '\\%')
194 line = line.replace('_', '\\_')
195 line = line.replace('$', '\\$')
197 # Do the LyX text --> LaTeX conversion
198 for rep in unicode_reps:
199 line = line.replace(rep[1], rep[0] + "{}")
200 line = line.replace(r'\backslash', r'\textbackslash{}')
201 line = line.replace(r'\series bold', r'\bfseries{}').replace(r'\series default', r'\mdseries{}')
202 line = line.replace(r'\shape italic', r'\itshape{}').replace(r'\shape smallcaps', r'\scshape{}')
203 line = line.replace(r'\shape slanted', r'\slshape{}').replace(r'\shape default', r'\upshape{}')
204 line = line.replace(r'\emph on', r'\em{}').replace(r'\emph default', r'\em{}')
205 line = line.replace(r'\noun on', r'\scshape{}').replace(r'\noun default', r'\upshape{}')
206 line = line.replace(r'\bar under', r'\underbar{').replace(r'\bar default', r'}')
207 line = line.replace(r'\family sans', r'\sffamily{}').replace(r'\family default', r'\normalfont{}')
208 line = line.replace(r'\family typewriter', r'\ttfamily{}').replace(r'\family roman', r'\rmfamily{}')
209 line = line.replace(r'\InsetSpace ', r'').replace(r'\SpecialChar ', r'')
214 def latex_length(slen):
216 Convert lengths to their LaTeX representation. Returns (bool, length),
217 where the bool tells us if it was a percentage, and the length is the
218 LaTeX representation.
222 # the slen has the form
223 # ValueUnit+ValueUnit-ValueUnit or
224 # ValueUnit+-ValueUnit
225 # the + and - (glue lengths) are optional
226 # the + always precedes the -
228 # Convert relative lengths to LaTeX units
229 units = {"text%":"\\textwidth", "col%":"\\columnwidth",
230 "page%":"\\paperwidth", "line%":"\\linewidth",
231 "theight%":"\\textheight", "pheight%":"\\paperheight"}
232 for unit in units.keys():
237 minus = slen.rfind("-", 1, i)
238 plus = slen.rfind("+", 0, i)
239 latex_unit = units[unit]
240 if plus == -1 and minus == -1:
242 value = str(float(value)/100)
243 end = slen[i + len(unit):]
244 slen = value + latex_unit + end
246 value = slen[plus + 1:i]
247 value = str(float(value)/100)
248 begin = slen[:plus + 1]
249 end = slen[i+len(unit):]
250 slen = begin + value + latex_unit + end
252 value = slen[minus + 1:i]
253 value = str(float(value)/100)
254 begin = slen[:minus + 1]
255 slen = begin + value + latex_unit
257 # replace + and -, but only if the - is not the first character
258 slen = slen[0] + slen[1:].replace("+", " plus ").replace("-", " minus ")
259 # handle the case where "+-1mm" was used, because LaTeX only understands
260 # "plus 1mm minus 1mm"
261 if slen.find("plus minus"):
262 lastvaluepos = slen.rfind(" ")
263 lastvalue = slen[lastvaluepos:]
264 slen = slen.replace(" ", lastvalue + " ")
265 return (percent, slen)
268 def revert_flex_inset(document, name, LaTeXname, position):
269 " Convert flex insets to TeX code "
272 i = find_token(document.body, '\\begin_inset Flex ' + name, i)
275 z = find_end_of_inset(document.body, i)
277 document.warning("Malformed LyX document: Can't find end of Flex " + name + " inset.")
279 # remove the \end_inset
280 document.body[z - 2:z + 1] = put_cmd_in_ert("}")
281 # we need to reset character layouts if necessary
282 j = find_token(document.body, '\\emph on', i, z)
283 k = find_token(document.body, '\\noun on', i, z)
284 l = find_token(document.body, '\\series', i, z)
285 m = find_token(document.body, '\\family', i, z)
286 n = find_token(document.body, '\\shape', i, z)
287 o = find_token(document.body, '\\color', i, z)
288 p = find_token(document.body, '\\size', i, z)
289 q = find_token(document.body, '\\bar under', i, z)
290 r = find_token(document.body, '\\uuline on', i, z)
291 s = find_token(document.body, '\\uwave on', i, z)
292 t = find_token(document.body, '\\strikeout on', i, z)
294 document.body.insert(z - 2, "\\emph default")
296 document.body.insert(z - 2, "\\noun default")
298 document.body.insert(z - 2, "\\series default")
300 document.body.insert(z - 2, "\\family default")
302 document.body.insert(z - 2, "\\shape default")
304 document.body.insert(z - 2, "\\color inherit")
306 document.body.insert(z - 2, "\\size default")
308 document.body.insert(z - 2, "\\bar default")
310 document.body.insert(z - 2, "\\uuline default")
312 document.body.insert(z - 2, "\\uwave default")
314 document.body.insert(z - 2, "\\strikeout default")
315 document.body[i:i + 4] = put_cmd_in_ert(LaTeXname + "{")
319 def revert_font_attrs(document, name, LaTeXname):
320 " Reverts font changes to TeX code "
324 i = find_token(document.body, name + ' on', i)
327 j = find_token(document.body, name + ' default', i)
328 k = find_token(document.body, name + ' on', i + 1)
329 # if there is no default set, the style ends with the layout
330 # assure hereby that we found the correct layout end
331 if j != -1 and (j < k or k == -1):
332 document.body[j:j + 1] = put_cmd_in_ert("}")
334 j = find_token(document.body, '\\end_layout', i)
335 document.body[j:j] = put_cmd_in_ert("}")
336 document.body[i:i + 1] = put_cmd_in_ert(LaTeXname + "{")
341 def revert_layout_command(document, name, LaTeXname, position):
342 " Reverts a command from a layout to TeX code "
345 i = find_token(document.body, '\\begin_layout ' + name, i)
349 # find the next layout
352 j = find_token(document.body, '\\begin_layout', j)
353 l = len(document.body)
354 # if nothing was found it was the last layout of the document
356 document.body[l - 4:l - 4] = put_cmd_in_ert("}")
358 # exclude plain layout because this can be TeX code or another inset
359 elif document.body[j] != '\\begin_layout Plain Layout':
360 document.body[j - 2:j - 2] = put_cmd_in_ert("}")
364 document.body[i] = '\\begin_layout Standard'
365 document.body[i + 1:i + 1] = put_cmd_in_ert(LaTeXname + "{")
370 " Converts an RRGGBB-type hexadecimal string to a float in [0.0,1.0] "
377 return str(val / 256.0)
381 "'true' goes to True, case-insensitively, and we strip whitespace."
382 s = s.strip().lower()