The pLyX system (see http://wiki.lyx.org/Examples/Examples) provides a means of launching python scripts at the click of a few toolbar buttons; these scripts modify the current document. Given the speed of modern computers, the whole process feels built-in -- the document changes almost immediately "in front of one's eyes".

As part of this system, I've attached a find-&-replace script for LyX's native file format, & associated files:

findrepl.py -- the script that does the finding & replacing.
findrepl_help.py  -- a help script.
pLyXFindReplace(compressed).lyx -- an explanatory LyX document that needs to be saved in uncompressed format for the examples in it to work.

In the document I show how the script can be used to tackle some queries that have appeared on the users list over the past year:

document-wide changing the width of figures
document-wide centering of figures
converting chemical formulae written in math insets (for the sake of sub- and superscripts) to \mathrm clearing up "debris" after importing documents from, e.g., OpenOffice or Abiword, that have been exported as LaTeX (in other words getting rid of all those irritating left & right braces in ERT insets).

One way of tackling such problems is to open the document in a text editor and make appropriate changes there. The find-&-replace script means that is no longer necessary. It can be done from the comfort of the LyX GUI. There are usually other ways of tackling such matters (and perhaps more insightful in a LaTeX sense) but for someone pressed for time, this may provide a convenient fallback.

The script has a simple mode capable of finding & replacing possibly multiple lines of native LyX format code, and a powerful regular expression mode similarly capable, but also of condensing multiple simple searches into one regexp search (and, of course, with a health warning about crashing LyX, given the obscurity of regexps, but it's great fun).

Andrew
# Find & replace elements of the LyX source.
# Part of the pLyX.py system.
#
# Andrew Parsloe ([email protected])
# version 0.1 (19 November 2012)
#
# findrepl.py 
#
import argparse, re, sys

re_backslash = re.compile(r'\n?\\backslash\n')

flex_fr = r'\begin_inset Flex .find & repl'
flex_arg = r'\begin_inset Flex .[argument]'
begin_layout = r'\begin_layout'
end_layout = r'\end_layout'
begin_std = r'\begin_layout Standard'
begin_inset = r'\begin_inset'
end_inset = r'\end_inset'
begin_note = r'\begin_inset Note'
st_open = 'status open\n'
st_coll = 'status collapsed\n'
end_body = r'\end_body'
end_document = r'\end_document'
backslash = r'\backslash'

begin_msg = r'''\begin_inset Note Note
status open
\begin_layout Plain Layout
'''
end_msg = r'''
\end_layout
\end_inset
'''

######################################################
def main(infl, outfl, options, guff):

    def strip_outers(stuff):
        '''Strip enclosing layout statements.'''
        stuff = stuff.partition('\n')[2]
        stuff = stuff.rpartition(end_layout)[0]
        return stuff
        
    def inset_contents():
        '''Get contents of inset minus LyX paragraphing.'''
        
        contents = lines = ''
        layouts, insets = 0, 1
        status = True
        newpara = False

        for line in infl:
            lines += line
            if line == '\n':
                continue
            # assumes "status open|collapsed" is last status line
            elif status:
                if st_open == line or st_coll == line:
                    status = False
                    continue
            elif begin_layout in line:
                # exclude LyX paragraphing of contents
                layouts += 1
                if layouts > 1:
                    contents += line
                else:
                    newpara = True
            elif begin_inset in line:
                insets += 1
                contents += line
            elif backslash in line:
                if newpara:
                    contents += '\n' + line
                else:
                    contents += line
            elif end_layout in line:
                newpara = False
                # exclude LyX paragraphing of contents
                if layouts > 1:
                    contents += line
                layouts -= 1
            elif end_inset in line:
                newpara = False
                insets -= 1
                if insets == 0:
                    return contents, lines
                else:
                    contents += line
            else:
                newpara = False
                contents += line
                    
    def get_lines(n):
        temp = ''
        i = 0
        if n == 0:
            return ''
        else:
            for line in infl:
                if line != '\n':
                    temp += line
                    i += 1
                    if i == n:
                        break
            return temp

    def write_msg(msg):
        '''Write a yellow note error message'''
        outfl.write(begin_msg)
        outfl.write(msg)
        outfl.write(end_msg)

    def write_last_count(flines, count, suppress):
        if not suppress:
            temp = iter(flines.splitlines(True))
            for line in temp:
                if end_body in line:
                    outfl.write(begin_std + '\n')
                    write_msg(count_msg(count))
                    outfl.write(end_layout + '\n')
                outfl.write(line)
        else:
            outfl.write(flines)
        for line in infl:
            outfl.write(line)

    def count_msg(count):
        '''Build occurrence/replacement message.'''
        msg = str(count) + ' '
        if replacing:
            msg += 'replacement'
            if count != 1:
                msg += 's'
            msg += ' of "' + find0 + '" by "' + repl0 + '"\n'
        else:
            msg += 'occurrence'
            if count != 1:
                msg += 's'
            msg += ' of "' + find0 + '"\n'
        return msg

    def re_flags(params):
        '''Return reg. exp. flag value.'''
        fgs = count = 0
        for char in params.upper():
            if char == 'I': # ignorecase
                fgs = fgs|re.I
            elif char == 'L': # locale
                fgs = fgs|re.L
            elif char == 'M': # mulitline
                fgs = fgs|re.M
            elif char in 'SD': # dotall
                fgs = fgs|re.S
            elif char == 'U': # unicode
                fgs = fgs|re.U
            elif char.isdigit():
                count = 10*count + int(char)
        return fgs, count
            
    ######################################################                
    # write the prelims
    outfl.write(guff)
    guff = ''

    # get the options
    parser = argparse.ArgumentParser(description='Find & replace')

    parser.add_argument('-r', '--regexp', dest = 'r', action ='store_true', \
                        default = False, help='Use regular expressions')
    parser.add_argument('-s', '--suppress', dest = 's', action ='store_true', \
                        default = False, help='Suppress occurrence/replacement messages')
    
    regexp0 = parser.parse_args(options).r
    suppress = parser.parse_args(options).s

    lines = flines = params = ''
    count = status = 0
    finding = replacing = False
    for line in infl:
        if line in '\n':
            continue
        # scanning text, looking for f-&-r inset
        elif status == 0:
            if flex_fr in line:
                if finding:
                    outfl.write(flines)
                    flines = ''
                    # show count & turn off current search
                    if suppress == False:
                        msg = count_msg(count)
                        write_msg(msg)
                    count = 0
                    finding = False
                outfl.write(line)
                params, temp = inset_contents()
                if '-r' in params.lower():
                    regexp = True
                    params = re.sub(r'\-\-regexp', '', params, flags = re.I)
                    params = re.sub(r'\-r', '', params, flags = re.I)
                else:
                    regexp = regexp0
                outfl.write(temp)
                status += 1
            elif finding:
                flines += line
                lenfl += 1
                print '*',lenfl
                if end_body in line:
                    write_last_count(flines, count, suppress)
                elif (not regexp and find in flines) or \
                     (regexp and re_find.search(flines)):
                    count += 1
                    if replacing:
                        if regexp:
                            outfl.write(re_find.sub(repl, flines))
                        else:
                            outfl.write(flines.replace(find, repl))
                    else:
                        outfl.write(flines)
                    if flen > 1:
                        flines = get_lines(flen - 1)
                        lenfl = flen - 1
                        if end_body in flines:
                            write_last_count(flines, count, suppress)  
                    else:
                        flines = ''
                        lenfl = 0
                else:
                    if lenfl >= flenmax:
                        temp = flines.partition('\n')
                        outfl.write(temp[0] + '\n')
                        flines = temp[2]
                        lenfl -= 1
            # otherwise write to file
            else:
                outfl.write(line)

        # looking for an argument inset (find)
        elif status == 1:
            outfl.write(line)
            if flex_arg in line:
                find0, temp = inset_contents()
                outfl.write(temp)
                find = re_backslash.sub(r'\\', find0).strip()
                if find == '' and finding == False:
                    # nothing to find
                    write_msg('Nothing to find!')
                    status -= 1
                elif find == '' and finding == True:
                    # turn off current search
                    finding = False
                    status -= 1
                else:
                    finding = True
                    re_find = re.compile(find, flags = re_flags(params)[0])
                    # check for replace inset
                    status += 1
            else:
                status -= 1
                finding = False
           
        # looking for an argument inset (replace)
        elif status == 2:
            outfl.write(line)
            # get replacement string
            if flex_arg in line:
                repl0, temp = inset_contents()
                print repl0
                repl = re_backslash.sub(r'\\', repl0).strip()
                outfl.write(temp)
                replacing = True
            else:
                # no replacement inset
                replacing = False

            status = 0
            flen = len(find.splitlines(True))
            flenmax = max(re_flags(params)[1], flen)
            print flenmax
            if flen > 1:
                flines = get_lines(flen - 1)
                lenfl = flen - 1
            else:
                lenfl = 0
    
    return 1



                
                
            
            
    
def helpnote(hv):
    if hv > 1:
        return header + version
    else:
        return header + tail

header = r'''\begin_LyX-Code
\family roman
\series bold
.find & replace
\end_layout
'''

version = r'''\begin_layout LyX-Code
\family roman
Version 1.0 (19 Jan 2013) RE flags & search block size spec.
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.2 (13 Jan 2013) Regular expression searches.
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.1 (8 Jan 2013)
\end_layout
'''
tail = r'''\begin_layout LyX-Code
\family roman
Find & replace, or count the number of occurrences of,
 elements of LyX format code.
\end_layout
\begin_layout LyX-Code

\end_layout
\begin_layout LyX-Code
\family roman
\series bold
Global options
\end_layout
\begin_layout LyX-Code
\family roman
\series bold
-h --help  
\series default
show this help note.
\end_layout
\begin_layout LyX-Code
\family roman
\series bold
-v --version  
\series default
show version information.
\end_layout
\begin_layout LyX-Code
\family roman
\series bold
-r  
\series default
use regular expressions.
\end_layout
\begin_layout LyX-Code
\family roman
\series bold
-s  
\series default
suppress occurrence/replacement messages.
\end_layout
\begin_layout Itemize
\family roman
To count the number of occurrences of some (possibly multi-line) element of
 LyX format code, enter the code in an 
\family sans
.[argument]
\family roman
 inset immediately following a 
\family sans
.find & replace (LyX format)
\family roman
 inset inserted in the document.
 This is the point at which the count will begin.
 (You can copy code from the  
\family sans
View Source
\family roman
 window and use 
\family sans
Paste Special
\family roman
, in the 
\family sans
Edit
\family roman
 menu, to paste it into the 
\family sans
.[argument]
\family roman
 inset.) 
 Another 
\family sans
.find & replace (LyX format)
\family roman
 inset inserted later in the document will  stop the count
 at that point, where the total will be displayed in a (yellow) Note and,
 should there be a following 
\family sans
.[argument]
\family roman
 inset with content, start a new count.
 If there is no further inset, this new count will be displayed at the end
 of the document.
\end_layout
\begin_layout Itemize
\family roman
To find & replace some (possibly multi-line) element of LyX format code,
 enter the code to be found in an 
\family sans
.[argument]
\family roman
 inset immediately following a 
\family sans
.find & replace (LyX format)
\family roman
 inset inserted in the document.
 The replacement code is entered in another 
\family sans
.[argument]
\family roman
 inset immediately following the first.  (Again, you can copy code from the  
\family sans
View Source
\family roman
 window and use 
\family sans
Paste Special
\family roman
, in the 
\family sans
Edit
\family roman
 menu, to paste it into either inset.)
 Another 
\family sans
.find & replace (LyX format) 
\family roman
 inset inserted later in the document will stop the find & replace
 at that point, where the total number of replacements will be displayed
 in a (yellow) Note.
 Depending on whether this second 
\family sans
.find & replace (LyX format) 
\family roman
inset is followed by one or two 
\family sans
.[argument]
\family roman
 insets with content, a new count or a new find & replace may be initiated
 at this point.
\end_layout
\begin_layout Itemize
\family roman
The yellow Note messages detailing how many occurrences or replacements
 have been made can be suppressed with the global 
\series bold
-s
\series default
 option.
 The default is to show them.
\end_layout
\begin_layout Itemize
\family roman
Because LyX inserts line breaks into ordinary text (including sometimes in
 the middle of words), both count and find-&-replace for ordinary text may
 not give expected results (but LyX has its built-in find-&-replace
 for this context).
\end_layout
\begin_layout Itemize
\family roman
Regular expression searches can be conducted by using the global 
\series bold
-r
\series default
 (or 
\series bold
--regexp
\series default
) option, or by inserting this option in a local instance of a 
\family sans
.find & replace (LyX format)
\family roman
 inset. Regular expression flags and a search block size specification
 can also be inserted in this inset. See the accompanying documentation: 
\emph on
The pLyX system: find & replace (& count) LyX format code
\emph default
.
\end_layout
'''

Attachment: pLyXFindReplace(compressed).lyx
Description: Binary data

Reply via email to