]> git.lyx.org Git - lyx.git/blob - lib/lyx2lyx/parser_tools.py
New lyx2lyx tools.
[lyx.git] / lib / lyx2lyx / parser_tools.py
1 # This file is part of lyx2lyx
2 # -*- coding: utf-8 -*-
3 # Copyright (C) 2002-2011 Dekel Tsur <dekel@lyx.org>,
4 # José Matos <jamatos@lyx.org>, Richard Heck <rgheck@comcast.net>
5 #
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
19
20
21 """
22 This module offers several free functions to help parse lines.
23 More documentaton is below, but here is a quick guide to what
24 they do. Optional arguments are marked by brackets.
25
26 find_token(lines, token, start[, end[, ignorews]]):
27   Returns the first line i, start <= i < end, on which
28   token is found at the beginning. Returns -1 if not
29   found.
30   If ignorews is (given and) True, then differences
31   in whitespace do not count, except that there must be no
32   extra whitespace following token itself.
33
34 find_token_exact(lines, token, start[, end]):
35   As find_token, but with ignorews set to True.
36
37 find_tokens(lines, tokens, start[, end[, ignorews]]):
38   Returns the first line i, start <= i < end, on which
39   one of the tokens in tokens is found at the beginning.
40   Returns -1 if not found.
41   If ignorews is (given and) True, then differences
42   in whitespace do not count, except that there must be no
43   extra whitespace following token itself.
44
45 find_tokens_exact(lines, token, start[, end]):
46   As find_tokens, but with ignorews True.
47
48 find_token_backwards(lines, token, start):
49 find_tokens_backwards(lines, tokens, start):
50   As before, but look backwards.
51
52 find_re(lines, rexp, start[, end]):
53   As find_token, but rexp is a regular expression object,
54   so it has to be passed as e.g.: re.compile(r'...').
55
56 get_value(lines, token, start[, end[, default]):
57   Similar to find_token, but it returns what follows the
58   token on the found line. Example:
59     get_value(document.header, "\\use_xetex", 0)
60   will find a line like:
61     \\use_xetex true
62   and, in that case, return "true". (Note that whitespace
63   is stripped.) The final argument, default, defaults to "",
64   and is what is returned if we do not find anything. So you
65   can use that to set a default.
66
67 get_quoted_value(lines, token, start[, end[, default]]):
68   Similar to get_value, but it will strip quotes off the
69   value, if they are present. So use this one for cases
70   where the value is normally quoted.
71
72 get_option_value(line, option):
73   This assumes we have a line with something like:
74       option="value"
75   and returns value. Returns "" if not found.
76
77 get_bool_value(lines, token, start[, end[, default]]):
78   Like get_value, but returns a boolean.
79
80 del_token(lines, token, start[, end]):
81   Like find_token, but deletes the line if it finds one.
82   Returns True if a line got deleted, otherwise False.
83
84 find_beginning_of(lines, i, start_token, end_token):
85   Here, start_token and end_token are meant to be a matching
86   pair, like "\\begin_layout" and "\\end_layout". We look for
87   the start_token that pairs with the end_token that occurs
88   on or after line i. Returns -1 if not found.
89   So, in the layout case, this would find the \\begin_layout
90   for the layout line i is in.
91   Example:
92     ec = find_token(document.body, "</cell", i)
93     bc = find_beginning_of(document.body, ec, \
94         "<cell", "</cell")
95   Now, assuming no -1s, bc-ec wraps the cell for line i.
96
97 find_end_of(lines, i, start_token, end_token):
98   Like find_beginning_of, but looking for the matching
99   end_token. This might look like:
100     bc = find_token_(document.body, "<cell", i)
101     ec = find_end_of(document.body, bc,  "<cell", "</cell")
102   Now, assuming no -1s, bc-ec wrap the next cell.
103
104 find_end_of_inset(lines, i):
105   Specialization of find_end_of for insets.
106
107 find_end_of_layout(lines, i):
108   Specialization of find_end_of for layouts.
109
110 find_end_of_sequence(lines, i):
111   Find the end of the sequence of layouts of the same kind.
112   Considers nesting. If the last paragraph in sequence is nested,
113   the position of the last \end_deeper is returned, else
114   the position of the last \end_layout.
115
116 is_in_inset(lines, i, inset):
117   Checks if line i is in an inset of the given type.
118   If so, returns starting and ending lines. Otherwise,
119   returns False.
120   Example:
121     is_in_inset(document.body, i, "\\begin_inset Tabular")
122   returns False unless i is within a table. If it is, then
123   it returns the line on which the table begins and the one
124   on which it ends. Note that this pair will evaulate to
125   boolean True, so
126     if is_in_inset(...):
127   will do what you expect.
128
129 get_containing_inset(lines, i):
130   Finds out what kind of inset line i is within. Returns a
131   list containing what follows \begin_inset on the line
132   on which the inset begins, plus the starting and ending line.
133   Returns False on any kind of error or if it isn't in an inset.
134   So get_containing_inset(document.body, i) might return:
135     ("CommandInset ref", 300, 306)
136   if i is within an InsetRef beginning on line 300 and ending
137   on line 306.
138
139 get_containing_layout(lines, i):
140   As get_containing_inset, but for layout. Additionally returns the
141   position of real paragraph start (after par params) as 4th value.
142
143 find_nonempty_line(lines, start[, end):
144   Finds the next non-empty line.
145
146 check_token(line, token):
147   Does line begin with token?
148
149 is_nonempty_line(line):
150   Does line contain something besides whitespace?
151
152 count_pars_in_inset(lines, i):
153   Counts the paragraphs inside an inset.
154
155 """
156
157 import re
158
159 # Utilities for one line
160 def check_token(line, token):
161     """ check_token(line, token) -> bool
162
163     Return True if token is present in line and is the first element
164     else returns False.
165
166     Deprecated. Use line.startswith(token).
167     """
168     return line.startswith(token)
169
170
171 def is_nonempty_line(line):
172     """ is_nonempty_line(line) -> bool
173
174     Return False if line is either empty or it has only whitespaces,
175     else return True."""
176     return bool(line.strip())
177
178
179 # Utilities for a list of lines
180 def find_token(lines, token, start=0, end=0, ignorews=False):
181     """ find_token(lines, token, start[[, end], ignorews]) -> int
182
183     Return the lowest line where token is found, and is the first
184     element, in lines[start, end].
185
186     If ignorews is True (default is False), then differences in
187     whitespace are ignored, but there must be whitespace following
188     token itself.
189
190     Return -1 on failure."""
191
192     if end == 0 or end > len(lines):
193         end = len(lines)
194     if ignorews:
195         y = token.split()
196     for i in range(start, end):
197         if ignorews:
198             x = lines[i].split()
199             if len(x) < len(y):
200                 continue
201             if x[:len(y)] == y:
202                 return i
203         else:
204             if lines[i].startswith(token):
205                 return i
206     return -1
207
208
209 def find_token_exact(lines, token, start=0, end=0):
210     return find_token(lines, token, start, end, True)
211
212
213 def find_tokens(lines, tokens, start=0, end=0, ignorews=False):
214     """ find_tokens(lines, tokens, start[[, end], ignorews]) -> int
215
216     Return the lowest line where one token in tokens is found, and is
217     the first element, in lines[start, end].
218
219     Return -1 on failure."""
220     if end == 0 or end > len(lines):
221         end = len(lines)
222
223     for i in range(start, end):
224         for token in tokens:
225             if ignorews:
226                 x = lines[i].split()
227                 y = token.split()
228                 if len(x) < len(y):
229                     continue
230                 if x[:len(y)] == y:
231                     return i
232             else:
233                 if lines[i].startswith(token):
234                     return i
235     return -1
236
237
238 def find_tokens_exact(lines, tokens, start=0, end=0):
239     return find_tokens(lines, tokens, start, end, True)
240
241
242 def find_re(lines, rexp, start=0, end=0):
243     """ find_re(lines, rexp, start[, end]) -> int
244
245     Return the lowest line where rexp, a regular expression, is found
246     in lines[start, end].
247
248     Return -1 on failure."""
249
250     if end == 0 or end > len(lines):
251         end = len(lines)
252     for i in range(start, end):
253         if rexp.match(lines[i]):
254                 return i
255     return -1
256
257
258 def find_token_backwards(lines, token, start):
259     """ find_token_backwards(lines, token, start) -> int
260
261     Return the highest line where token is found, and is the first
262     element, in lines[start, end].
263
264     Return -1 on failure."""
265     for i in range(start, -1, -1):
266         if lines[i].startswith(token):
267             return i
268     return -1
269
270
271 def find_tokens_backwards(lines, tokens, start):
272     """ find_tokens_backwards(lines, token, start) -> int
273
274     Return the highest line where token is found, and is the first
275     element, in lines[end, start].
276
277     Return -1 on failure."""
278     for i in range(start, -1, -1):
279         line = lines[i]
280         for token in tokens:
281             if line.startswith(token):
282                 return i
283     return -1
284
285
286 def find_complete_lines(lines, sublines, start=0, end=0):
287     """Find first occurence of sequence `sublines` in list `lines`.
288     Return index of first line or -1 on failure.
289
290     Efficient search for a sub-list in a large list. Works for any values.
291
292     >>> find_complete_lines([1, 2, 3, 1, 1, 2], [1, 2])
293     0
294
295     The `start` and `end` arguments work similar to list.index()
296
297     >>> find_complete_lines([1, 2, 3, 1, 1 ,2], [1, 2], start=1)
298     4
299     >>> find_complete_lines([1, 2, 3, 1, 1 ,2], [1, 2], start=1, end=4)
300     -1
301
302     The return value can be used to substitute the sub-list.
303     Take care to check before use:
304
305     >>> l = [1, 1, 2]
306     >>> s = find_complete_lines(l, [1, 2])
307     >>> if s != -1:
308     ...     l[s:s+2] = [3]; l
309     [1, 3]
310
311     See also del_complete_lines().
312     """
313     if not sublines:
314         return start
315     end = end or len(lines)
316     N = len(sublines)
317     try:
318         while True:
319             for j, value in enumerate(sublines):
320                 i = lines.index(value, start, end)
321                 if j and i != start:
322                     start = i-j
323                     break
324                 start = i + 1
325             else:
326                 return i +1 - N
327     except ValueError: # `sublines` not found
328         return -1
329
330
331 def find_across_lines(lines, sub, start=0, end=0):
332     sublines = sub.splitlines()
333     if len(sublines) > 2:
334         # at least 3 lines: the middle one(s) are complete -> use index search
335         i = find_complete_lines(lines, sublines[1:-1], start+1, end-1)
336         if i < start+1:
337             return -1
338         try:
339             if (lines[i-1].endswith(sublines[0]) and
340                 lines[i+len(sublines)].startswith(sublines[-1])):
341                 return i-1
342         except IndexError:
343             pass
344     elif len(sublines) > 1:
345         # last subline must start a line
346         i = find_token(lines, sublines[-1], start, end)
347         if i < start + 1:
348             return -1
349         if lines[i-1].endswith(sublines[0]):
350             return i-1
351     else: # no line-break, may be in the middle of a line
352         if end == 0 or end > len(lines):
353             end = len(lines)
354         for i in range(start, end):
355             if sub in lines[i]:
356                 return i
357     return -1
358
359
360 def get_value(lines, token, start=0, end=0, default=""):
361     """ get_value(lines, token, start[[, end], default]) -> string
362
363     Find the next line that looks like:
364       token followed by other stuff
365     Returns "followed by other stuff" with leading and trailing
366     whitespace removed.
367     """
368     i = find_token_exact(lines, token, start, end)
369     if i == -1:
370         return default
371     # TODO: establish desired behaviour, eventually change to
372     #  return lines.pop(i)[len(token):].strip() # or default
373     # see test_parser_tools.py
374     l = lines[i].split(None, 1)
375     if len(l) > 1:
376         return l[1].strip()
377     return default
378
379
380 def get_quoted_value(lines, token, start=0, end=0, default=""):
381     """ get_quoted_value(lines, token, start[[, end], default]) -> string
382
383     Find the next line that looks like:
384       token "followed by other stuff"
385     Returns "followed by other stuff" with leading and trailing
386     whitespace and quotes removed. If there are no quotes, that is OK too.
387     So use get_value to preserve possible quotes, this one to remove them,
388     if they are there.
389     Note that we will NOT strip quotes from default!
390     """
391     val = get_value(lines, token, start, end, "")
392     if not val:
393       return default
394     return val.strip('"')
395
396
397 def get_bool_value(lines, token, start=0, end=0, default=None):
398     """ get_bool_value(lines, token, start[[, end], default]) -> string
399
400     Find the next line that looks like:
401       token bool_value
402
403     Returns True if bool_value is 1 or true and
404     False if bool_value is 0 or false
405     """
406
407     val = get_quoted_value(lines, token, start, end, "")
408
409     if val == "1" or val == "true":
410         return True
411     if val == "0" or val == "false":
412         return False
413     return default
414
415
416 def get_option_value(line, option):
417     rx = option + '\s*=\s*"([^"]+)"'
418     rx = re.compile(rx)
419     m = rx.search(line)
420     if not m:
421       return ""
422     return m.group(1)
423
424
425 def set_option_value(line, option, value):
426     rx = '(' + option + '\s*=\s*")[^"]+"'
427     rx = re.compile(rx)
428     m = rx.search(line)
429     if not m:
430         return line
431     return re.sub(rx, '\g<1>' + value + '"', line)
432
433
434 def del_token(lines, token, start=0, end=0):
435     """ del_token(lines, token, start, end) -> int
436
437     Find the first line in lines where token is the first element
438     and delete that line. Returns True if we deleted a line, False
439     if we did not."""
440
441     k = find_token_exact(lines, token, start, end)
442     if k == -1:
443         return False
444     del lines[k]
445     return True
446
447 def del_complete_lines(lines, sublines, start=0, end=0):
448     """Delete first occurence of `sublines` in list `lines`.
449
450     Efficient deletion of a sub-list in a list. Works for any values.
451     The `start` and `end` arguments work similar to list.index()
452
453     Returns True if a deletion was done and False if not.
454
455     >>> l = [1, 0, 1, 1, 1, 2]
456     >>> del_complete_lines(l, [0, 1, 1])
457     True
458     >>> l
459     [1, 1, 2]
460     """
461     i = find_complete_lines(lines, sublines, start, end)
462     if i == -1:
463         return False
464     del(lines[i:i+len(sublines)])
465     return True
466
467
468 def del_value(lines, token, start=0, end=0, default=None):
469     """
470     Find the next line that looks like:
471       token followed by other stuff
472     Delete that line and return "followed by other stuff"
473     with leading and trailing whitespace removed.
474
475     If token is not found, return `default`.
476     """
477     i = find_token_exact(lines, token, start, end)
478     if i == -1:
479         return default
480     return lines.pop(i)[len(token):].strip()
481
482
483 def find_beginning_of(lines, i, start_token, end_token):
484     count = 1
485     while i > 0:
486         i = find_tokens_backwards(lines, [start_token, end_token], i-1)
487         if i == -1:
488             return -1
489         if lines[i].startswith(end_token):
490             count = count+1
491         else:
492             count = count-1
493         if count == 0:
494             return i
495     return -1
496
497
498 def find_end_of(lines, i, start_token, end_token):
499     count = 1
500     n = len(lines)
501     while i < n:
502         i = find_tokens(lines, [end_token, start_token], i+1)
503         if i == -1:
504             return -1
505         if lines[i].startswith(start_token):
506             count = count+1
507         else:
508             count = count-1
509         if count == 0:
510             return i
511     return -1
512
513
514 def find_nonempty_line(lines, start=0, end=0):
515     if end == 0:
516         end = len(lines)
517     for i in range(start, end):
518         if lines[i].strip():
519             return i
520     return -1
521
522
523 def find_end_of_inset(lines, i):
524     " Find end of inset, where lines[i] is included."
525     return find_end_of(lines, i, "\\begin_inset", "\\end_inset")
526
527
528 def find_end_of_layout(lines, i):
529     " Find end of layout, where lines[i] is included."
530     return find_end_of(lines, i, "\\begin_layout", "\\end_layout")
531
532
533 def is_in_inset(lines, i, inset):
534     '''
535     Checks if line i is in an inset of the given type.
536     If so, returns starting and ending lines.
537     Otherwise, returns False.
538     Example:
539       is_in_inset(document.body, i, "\\begin_inset Tabular")
540     returns False unless i is within a table. If it is, then
541     it returns the line on which the table begins and the one
542     on which it ends. Note that this pair will evaulate to
543     boolean True, so
544       if is_in_inset(...):
545     will do what you expect.
546     '''
547     defval = (-1, -1)
548     stins = find_token_backwards(lines, inset, i)
549     if stins == -1:
550       return defval
551     endins = find_end_of_inset(lines, stins)
552     # note that this includes the notfound case.
553     if endins < i:
554       return defval
555     return (stins, endins)
556
557
558 def get_containing_inset(lines, i):
559   '''
560   Finds out what kind of inset line i is within. Returns a
561   list containing (i) what follows \begin_inset on the line
562   on which the inset begins, plus the starting and ending line.
563   Returns False on any kind of error or if it isn't in an inset.
564   '''
565   j = i
566   while True:
567       stins = find_token_backwards(lines, "\\begin_inset", j)
568       if stins == -1:
569           return False
570       endins = find_end_of_inset(lines, stins)
571       if endins > j:
572           break
573       j = stins - 1
574
575   if endins < i:
576       return False
577
578   inset = get_value(lines, "\\begin_inset", stins)
579   if inset == "":
580       # shouldn't happen
581       return False
582   return (inset, stins, endins)
583
584
585 def get_containing_layout(lines, i):
586   '''
587   Finds out what kind of layout line i is within. Returns a
588   list containing what follows \begin_layout on the line
589   on which the layout begins, plus the starting and ending line
590   and the start of the paragraph (after all params). I.e, returns:
591     (layoutname, layoutstart, layoutend, startofcontent)
592   Returns False on any kind of error.
593   '''
594   j = i
595   while True:
596       stlay = find_token_backwards(lines, "\\begin_layout", j)
597       if stlay == -1:
598           return False
599       endlay = find_end_of_layout(lines, stlay)
600       if endlay > i:
601           break
602       j = stlay - 1
603
604   if endlay < i:
605       return False
606
607   lay = get_value(lines, "\\begin_layout", stlay)
608   if lay == "":
609       # shouldn't happen
610       return False
611   par_params = ["\\noindent", "\\indent", "\\indent-toggle", "\\leftindent",
612                 "\\start_of_appendix", "\\paragraph_spacing", "\\align",
613                 "\\labelwidthstring"]
614   stpar = stlay
615   while True:
616       stpar += 1
617       if lines[stpar].split(' ', 1)[0] not in par_params:
618           break
619   return (lay, stlay, endlay, stpar)
620
621
622 def count_pars_in_inset(lines, i):
623   '''
624   Counts the paragraphs within this inset
625   '''
626   ins = get_containing_inset(lines, i)
627   if ins == -1:
628       return -1
629   pars = 0
630   for j in range(ins[1], ins[2]):
631       m = re.match(r'\\begin_layout (.*)', lines[j])
632       if m and get_containing_inset(lines, j)[0] == ins[0]:
633           pars += 1
634
635   return pars
636
637
638 def find_end_of_sequence(lines, i):
639   '''
640   Returns the end of a sequence of identical layouts.
641   '''
642   lay = get_containing_layout(lines, i)
643   if lay == False:
644       return -1
645   layout = lay[0]
646   endlay = lay[2]
647   i = endlay
648   while True:
649       m = re.match(r'\\begin_layout (.*)', lines[i])
650       if m and m.group(1) != layout:
651           return endlay
652       elif lines[i] == "\\begin_deeper":
653           j = find_end_of(lines, i, "\\begin_deeper", "\\end_deeper")
654           if j != -1:
655               i = j
656               endlay = j
657               continue
658       if m and m.group(1) == layout:
659           endlay = find_end_of_layout(lines, i)
660           i = endlay
661           continue
662       if i == len(lines) - 1:
663           break
664       i = i + 1
665
666   return endlay