There are three files attached to this posting:

txtmacs.py, txtmacs_help.py -- a script implementing a system of text macros in LyX, and its help file pLyXTexTMacros(compressed).lyx -- an explanatory document (with nearly 670 text macros just waiting to be expanded)

The explanatory document must be saved in *uncompressed* format for the examples in it to work. It explains where to put the python scripts and then launches into an extensive explanation of how to use the system.

While I was working on a table sorting script in September, there was a brief exchange on the Developer's list about text macros. It occurred to me that the scheme of exporting from .lyx to .lyx then doing a buffer-reload might lend itself to a text macro implementation. This started out as a system of expanding abbreviations (which could be nested one within another). Almost immediately I found that an 'abbreviation' could be essentially any LyX construct -- a word, a phrase, a paragraph, a number of paragraphs, an equation, inline or display, a picture, an inset, essentially anything.

Then the code generalised so that the abbreviations could take arguments. Thus a macro [hg] might, by itself, expand to 'hyperbolic geometry' but presented with an argument, [hg](H), would expand to 'Hyperbolic geometry', meaning it could be used within and at the start of sentences. With 3 well-chosen keyboard shortcuts, I found macros provided a more convenient way of inserting character styles than presently available (in my opinion) in LyX. Macros also provide the easy insertion of LyX menu selections (as in the LyX manuals) with their sans serif formatting and menu separation character. And they enable the insertion of a specified number of any given character -- 47 asterisks perhaps, or 34 smileys.

There are conditionals available which enlarge the scope of what is possible. In fact it is all very new to me and I don't know what is possible. But the basic system has proved undeniably useful, and I wouldn't consider undertaking any substantial writing task in LyX now without this facility to hand.

Andrew

# Define and expand text macros in a LyX document.
# Part of the pLyX.py system; not an independent script
#
# txtmacs.py 
#
# Andrew Parsloe (apars...@clear.net.nz)
#
######################################################

import re, argparse

bs1 = '\n\\backslash\n1\n'
macdict = {'toggle': [[], ''], 'if0': [[''], bs1], 'if1': [[''], bs1],
           'peel': [[''], bs1]}

def main(infl, outfl, options, guff):
    '''Expand text macros.'''
      
    flex_arg = r'\begin_inset Flex .[argument]'
    begin_layout = r'\begin_layout'
    end_layout = r'\end_layout'
    begin_inset = r'\begin_inset'
    end_inset = r'\end_inset'
    begin_note = r'\begin_inset Note Note'
    bkslash = '\\'
    backslash = '\\backslash\n'
    status_open = 'status open\n'
    status_coll = 'status collapsed\n'

    re_macro = re.compile(r'(\w+)\n*(\w*)\s*(\d*)\s*$')
    re_lyxcmds = re.compile(r'(\\\w+) \w+')
    underscores = set([begin_layout, begin_inset, end_layout, end_inset])
    charstyles = ['\\family', '\\series', '\\shape', '\\size', '\\emph', \
                  '\\noun', '\\underbar', '\\strikeout', '\\uuline', \
                  '\\uwave', '\\no_emph', '\\no_noun', '\\no_strikeout', \
                  '\\no_bar', '\\no_uuline', '\\no_uwave']

    def output(depth, L):
        # write to file only at top level
        if depth == 0:
            outfl.write(L)
            return ''
        else:
            return L

    def defaults(s):
        '''Return used charstyles to default states.'''
        temp = r''
        for y in (set(re_lyxcmds.findall(s)) - underscores):
            if y == r'\color':
                temp += y + ' inherit\n'
            elif y == r'\lang':
                temp += y + ' english\n'
            elif y.lower() in charstyles:
                temp += y + ' default\n'
        return temp
    
    def expand(mname, sup_args, depth):
        '''Expand the macro (recursively if necessary).'''
        # substitute values for placeholders
        macexp = macdict.get(mname)[1]
        macprms = macdict.get(mname)[0]
        num_params = len(macprms)
        macprms = ['\n'] + macprms[:num_params - len(sup_args)] + sup_args
        for i in range(num_params, -1, -1):            
            macexp = macexp.replace('\n\\backslash\n' + str(i), macprms[i])
        # replace macro & inset with its expansion
        #  & expand included macros
        store = scan(iter(macexp.splitlines(True)), depth + 1)
        # return char styles to defaults
        store += output(depth + 1, defaults(macexp))
        return store

    def strip_outers(stuff):
        '''Strip enclosing layout statements.'''
        stuff = stuff.partition('\n')[2]
        stuff = stuff.rpartition(end_layout)[0]
        return stuff
    
    def inset_contents(iterable, keepouters):
        '''Get contents of inset +/- outer layout statements.'''
        contents = lines = ''
        insets = 1
        status = True
        bslash = False
        
        for line in iterable:
            lines += line
            # lose empties & status line
            if line == '\n':
                continue
            elif status:
                if status_open == line or status_coll == line:
                    status = False
                    continue
            elif begin_inset in line:
                insets += 1
                bslash = True
                contents += line
            elif end_inset in line:
                insets -= 1
                bslash = True
                if insets == 0:
                    if keepouters:
                        return contents, lines
                    else:
                        # strip outermost layout statements
                        temp = strip_outers(contents)
                        return temp, lines
                else:
                    contents += line
            elif backslash == line:
                if bslash:
                    contents += '\n'
                    bslash = False
                contents += line
            elif bkslash == line[0]:
                bslash = True
                contents += line
            else:
                contents += line
                bslash = False

    def indx(look_for, given):
        '''Return index of look_for in given.'''
        if look_for in given:
            return given.index(look_for)
        else:
            return len(given)

    def scan(iterable, depth):
        '''Parse the iterable line by line & resolve into output.'''
        
        # the 2nd is needed for the "toggle" macro
        flex_macro = r'\begin_inset Flex .expand macro'
        flex_macr0 = r'\begin_inset Flex .expand macro'
        
        lines = store = ''
        status = 0
        pending = starting = False
        params = supplied_args = []
        for line in iterable:
            if line in '\n':
                continue
            # scanning text, looking for macro & note insets
            elif status == 0:
                # a macro inset?
                if flex_macro in line:
                    status += 1
                    lines = line # store the line
                    defining = False
                # a note inset?
                elif begin_note in line and not starting:
                    store += output(depth, line)
                    if not scanotes:
                        # don't expand macros in notes
                        toss, temp = inset_contents(iterable, True)
                        store += output(depth, temp)
                # looking for toggle macro
                elif starting:
                    lines += line
                    if 'toggle\n' == line:
                        # found it!
                        flex_macro = flex_macr0
                        status += 1
                        defining = False
                        starting = False
                        if toggle_keep:
                            store += output(depth, lines)
                            lines = ''
                            store += output(depth, end_layout + '\n' \
                                        + end_inset + '\n')
                    # not the toggle macro
                    elif end_inset in line:
                        starting = False
                        store += output(depth, lines)
                        lines = ''
                # macro expansion off; is this the toggle macro?
                elif flex_macr0 in line:
                    lines = line
                    starting = True
                # otherwise write to file
                else:
                    store += output(depth, line)
                                   
            # in a macro inset
            elif status == 1:
                # get macro
                if re_macro.match(line):
                    m = re_macro.match(line)
                    macname = m.group(1) + m.group(2)
                    if macname not in macdict:
                        # a defining inset
                        if m.group(3) != '':
                            nparams = int(m.group(3))
                        else:
                            nparams = 0
                        lines += line
                        store += output(depth, lines)
                        lines = line = ''
                        defining = True
                        ndflts = 0
                        params = [globdef for x in range(nparams)]
                    elif macname == 'toggle':
                        # turn off macro expansion by misnaming (a hack)
                        flex_macro = r'\penguin_insect .expand macro'
                        if toggle_keep:
                            defining = True
                            lines += line
                            store += output(depth, lines)
                            lines = line = ''
                    elif m.group(3) == '0':
                        # delete macro from dictionary
                        macdict.pop(macname)
                        lines += line
                        store += output(depth, lines)
                        lines = line = ''
                        defining = True
                    else:
                        # a 'using' use; expansion looming
                        pending = True
                elif begin_note in line:
                    store += output(depth, line)
                    toss, temp = inset_contents(iterable, True)
                    store += output(depth, temp)
                elif end_inset in line:
                    if defining:
                        defining = False
                        store += output(depth, end_layout + '\n')
                        store += output(depth, line)
                        status -= 1
                    else:
                        # get arguments
                        supplied_args = []
                        status += 1
                # get default parameters
                elif flex_arg in line:
                    store += output(depth, line)
                    params[ndflts], temp = inset_contents(iterable, False)
                    ndflts += 1
                    store += output(depth, temp)
                # get macro expansion
                elif flex_macro in line:
                    store += output(depth, line)
                    expansion, temp = inset_contents(iterable, False)
                    macdict[macname] = [params, expansion]
                    params = []
                    store += output(depth, temp)
                else:
                    lines += line

            # get arguments (if any); expand macro
            elif status == 2:
                # get arguments (but not too many!)
                if flex_arg in line and \
                   len(supplied_args) < len(macdict.get(macname)[0]):
                    arg, toss = inset_contents(iterable, False)
                    # check if arg contains a top-level macro
                    if indx(flex_macro, arg) < indx(flex_arg, arg):
                        arg = scan(iter(arg.splitlines(True)), depth + 1)
                    # strip nested argument insets (if they exist)
                    if macname == 'peel':
                        if flex_arg in arg[:29]:
                            arg, toss = inset_contents(arg.splitlines(True)[1:], False)
                    arg = arg.rstrip()
                    # block (some) args of the conditionals
                    if (macname == 'if0' and arg != '')  or \
                       (macname == 'if1' and arg == ''):
                        continue
                    else:
                        supplied_args.append(arg)
                # macro following hard on another
                elif flex_macro in line:
                    # expand previous macro
                    store += output(depth, expand(macname, supplied_args, depth))
                    pending = False
                    # now parse this one
                    status = 1
                    defining = False
                    lines = line
                else:
                    # all args found; expand
                    store += output(depth, expand(macname, supplied_args, depth))
                    store += output(depth, line)
                    pending = False
                    status = 0
            
        if pending:
            store += output(depth, expand(macname, supplied_args, depth))
        return store
                            
    ######################################################                
    # write the prelims
    outfl.write(guff)

    # get the options
    parser = argparse.ArgumentParser(description='Expand text macros')

    parser.add_argument('-n', action ='store_true', default = False, \
                        help='Expand macros in notes')
    parser.add_argument('-t', action ='store_true', default = False, \
                        help='Retain toggle macros in document')
    parser.add_argument('-g', action ='store', default = '', \
                        help='Set global default value')

    scanotes = parser.parse_args(options).n
    toggle_keep = parser.parse_args(options).t
    globdef = parser.parse_args(options).g
    
    depth = 0
    scan(infl, depth)

    return 1
                
            
            
    

def helpnote(hv):
    if hv > 1:
        return header + version
    else:
        return header + tail
    
header = r'''\begin_layout LyX-Code
\family roman
\series bold
.expand macros           
\end_layout
'''
version = r'''\begin_layout LyX-Code
\family roman
Version 1.0 (30 December 2012)
 'invisible punctuator'; reversal of substitution order; built-in 'peel' macro.  
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.6 (20 December 2012)
 expand arguments containing top-level macros before passing to the parent macro.
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.5 (c.1 December 2012)
 built-in 'if0' and 'if1' macros.
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.4 (10 November 2012)
 built-in 'toggle' macro.
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.3 (21 October 2012)
 first version for the pLyX system.
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.2 (18 October 2012)
 first functioning script using insets instead of markers.
\end_layout
\begin_layout LyX-Code
\family roman
Version 0.1 (13 October 2012)
 first script for expanding abbreviations.
\end_layout
'''
tail = r'''\begin_layout LyX-Code
\family roman
Define and expand text macros.
\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
-g  
\series default
 set global default value for arguments; the default is the empty string.
 E.g. 
\series bold
-g *
\series default
 sets the global default to *.
\end_layout
\begin_layout LyX-Code
\family roman
\series bold
-n   
\series default
make macros within (yellow) notes expandable; default 
\series bold
False
\series default
.
\end_layout
\begin_layout LyX-Code

\end_layout
\begin_layout LyX-Code
\family roman
\series bold
Defining macros
\end_layout
\begin_layout LyX-Code
\family roman
\emph on
Example 1:
\end_layout
\begin_layout LyX-Code
\family roman
\begin_inset Flex .expand macro|txtmacs
status open
\begin_layout Plain Layout
\family roman
Lp
\begin_inset Flex .expand macro|txtmacs
status open

\begin_layout Plain Layout
\begin_inset Formula ${\displaystyle \left(\frac{\partial^{2}}{\partial x^{2}}+\frac{\partial^{2}}{\partial y^{2}}+\frac{\partial^{2}}{\partial z^{2}}\right)\phi=0}$
\end_inset
\end_layout
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout LyX-Code
\family roman
defines a 
\begin_inset Quotes els
\end_inset

pure abbreviation
\begin_inset Quotes ers
\end_inset

 macro 
\series bold
Lp
\series default
 (one with no parameters) which expands to Laplace's equation in mathematical
 display format: 
\family default

\begin_inset Flex .expand macro|txtmacs
status open

\begin_layout Plain Layout

\family roman
Lp
\end_layout

\end_inset


\end_layout

\begin_layout LyX-Code

\family roman
\emph on
Example 2: 
\end_layout

\begin_layout LyX-Code

\family roman
\begin_inset Flex .expand macro|txtmacs
status open

\begin_layout Plain Layout

\family roman
hg 1 
\begin_inset Flex .[argument]
status collapsed

\begin_layout Plain Layout

\family roman
h
\end_layout

\end_inset

 
\begin_inset Flex .expand macro|txtmacs
status open

\begin_layout Plain Layout

\family roman

\backslash
1yperbolic geometry
\end_layout

\end_inset


\end_layout

\end_inset


\end_layout

\begin_layout LyX-Code

\family roman
shows the definition of a macro 
\series bold
hg
\series default
 with one parameter.
 The default value of the parameter is 
\begin_inset Quotes els
\end_inset

h
\begin_inset Quotes ers
\end_inset

. Using 
\begin_inset Quotes els
\end_inset

=>
\begin_inset Quotes ers
\end_inset

 to mean 
\begin_inset Quotes els
\end_inset

expands to
\begin_inset Quotes ers
\end_inset

,
\end_layout

\begin_layout LyX-Code

\family roman
\begin_inset Flex .expand macro|txtmacs
status collapsed

\begin_layout Plain Layout

\family roman
hg
\end_layout

\end_inset

 => hyperbolic geometry
\end_layout

\begin_layout LyX-Code

\family roman
\begin_inset Flex .expand macro|txtmacs
status collapsed

\begin_layout Plain Layout

\family roman
hg
\end_layout

\end_inset


\begin_inset Flex .[argument]
status collapsed

\begin_layout Plain Layout

\family roman
H
\end_layout

\end_inset

 => Hyperbolic geometry
\end_layout

\begin_layout LyX-Code

\family roman
so that the latter is appropriate for use at the start of a sentence.
\end_layout

\begin_layout LyX-Code

\family roman
\emph on
Example 3:
\end_layout

\begin_layout LyX-Code

\family roman
\begin_inset Flex .expand macro|txtmacs
status open

\begin_layout Plain Layout

\family roman
tp 2 
\family default

\begin_inset Flex .[argument]
status collapsed

\begin_layout Plain Layout
File Handling
\end_layout

\end_inset


\family roman

\begin_inset Flex .[argument]
status collapsed

\begin_layout Plain Layout
File Formats
\end_layout

\end_inset


\family sans

\begin_inset Flex .expand macro|txtmacs
status open

\begin_layout Plain Layout

\family sans
Tools \SpecialChar \menuseparator
 Preferences \SpecialChar \menuseparator
 
\backslash
1 \SpecialChar \menuseparator
 
\backslash
2
\end_layout

\end_inset


\end_layout

\end_inset


\end_layout

\begin_layout LyX-Code

\family roman
defines the macro 
\series bold
tp
\series default
 with two parameters, for both of which default values are given (but specifying
 default values for some or all parameters is not essential).
 Thus 
\end_layout

\begin_layout LyX-Code

\family roman
\begin_inset Flex .expand macro|txtmacs
status collapsed

\begin_layout Plain Layout

\family roman
tp
\end_layout

\end_inset

 => 
\family sans
Tools \SpecialChar \menuseparator
 
Preferences \SpecialChar \menuseparator
 File Handling \SpecialChar \menuseparator
 File Formats
 
\end_layout

\begin_layout LyX-Code

\family roman
\begin_inset Flex .expand macro|txtmacs
status collapsed

\begin_layout Plain Layout

\family roman
tp
\end_layout

\end_inset


\family default

\begin_inset Flex .[argument]
status collapsed

\begin_layout Plain Layout

\family roman
Converters
\end_layout

\end_inset


\family roman
 =>  
\family sans
Tools \SpecialChar \menuseparator
 Preferences \SpecialChar \menuseparator
 File Handling \SpecialChar \menuseparator
 Converters
 
\end_layout

\begin_layout LyX-Code

\family roman
\begin_inset Flex .expand macro|txtmacs
status collapsed

\begin_layout Plain Layout

\family roman
tp
\end_layout

\end_inset


\begin_inset Flex .[argument]
status collapsed

\begin_layout Plain Layout
Editing
\end_layout

\end_inset


\family default

\begin_inset Flex .[argument]
status collapsed

\begin_layout Plain Layout
Shortcuts
\end_layout

\end_inset


\family roman
 => 
\family sans
Tools \SpecialChar \menuseparator
 Preferences \SpecialChar \menuseparator
 Editing \SpecialChar \menuseparator
 Shortcuts
\end_layout

\begin_layout LyX-Code

\end_layout

\begin_layout LyX-Code

\family roman
\emph on
Parameters
\end_layout

\begin_layout LyX-Code

\family roman
Parameters are entered in  an 
\family sans
.[argument]
\family roman
 inset: the contents of an 
\family sans
.[argument]
\family roman
 inset are invisible to LaTeX and have no effect on the pdf.
 Parameters are numbered from 
\backslash
1,
\backslash
2, ...
\end_layout

\begin_layout Itemize
The contents of both macro and argument insets are invisible to LaTeX.
\end_layout

\begin_layout Itemize

\family roman
More than one macro may be defined in a macro inset, but each must start
 on a new line in native LyX format (check
\family sans
View Source
\family roman.
\end_layout

\begin_layout Itemize

\family roman
An expansion may include other macros.
\end_layout

\begin_layout Itemize

\family roman
An expansion may be a word or phrase, or a multi-paragraph passage, containing
 text and character formatting, an equation, a graphic, indeed anything
 that can be displayed in LyX. It may contain parameters and other macros.
\end_layout

\begin_layout Itemize
A macro must be defined 
\emph on
before
\emph default
 its first use in the text (but the order of definition within an inset
 is immaterial).
\end_layout

\begin_layout Itemize
Use the built-in macro
\series bold
 toggle
\series default
 to stop macro expansion thereafter or, used again, to start it again.
\end_layout

\begin_layout LyX-Code

\family roman
\series bold
Shortcuts
\end_layout

\begin_layout LyX-Code

\family roman
Convenient keyboard shortcuts are needed for the 
\family sans
Macro
\family roman
 and 
\family sans
.[argument]
\family roman
 insets, and also for 
\begin_inset Quotes els
\end_inset

jumping
\begin_inset Quotes ers
\end_inset

 out of an inset (to the right), so that the cursor is correctly placed
 for further input.
 Perhaps (after removing the current assignment of 
\family sans
 Ctrl+K
\family roman
):
\end_layout

\begin_layout Itemize

\series bold
flex-insert 
\begin_inset Quotes eld
\end_inset

.expand macro|txtmacs
\begin_inset Quotes erd
\end_inset


\series default
 <=> 
\family sans
Ctrl+K
\family roman
 (or 
\family sans
 Ctrl+H
\family roman
)
\end_layout

\begin_layout Itemize

\series bold
flex-insert .[argument]
\series default
 <=> 
\family sans
Ctrl+;
\end_layout

\begin_layout Itemize

\series bold
command-sequence line-end; char-right; toggle-inset;
\series default
 <=> 
\family sans
Ctrl+J 
\family default
(J as in Jump)
\end_layout
'''

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

Reply via email to