PageRenderTime 127ms CodeModel.GetById 117ms app.highlight 4ms RepoModel.GetById 1ms app.codeStats 0ms


XML | 672 lines | 569 code | 82 blank | 21 comment | 0 complexity | 6daf095b0f4cfe75ee42421475ef2ed7 MD5 | raw file
  1<!-- jEdit 4.0 Macro Guide, (C) 2001, 2002 John Gellene                 -->
  2<!-- jEdit buffer-local properties:                                     -->
  3<!-- :indentSize=1:noTabs=yes:maxLineLen=72:tabSize=2:                  -->
  4<!-- :xml.root=users-guide.xml:                                         -->
  5<!-- This file contains an extended discussion of a                     -->
  6<!-- dialog-based macro example "Add_Prefix_and_Suffix.bsh"             -->
  7<!-- $Id: dialog-macro.xml 4777 2003-06-11 23:55:39Z spestov $      -->
  9<chapter id="dialog-macro"><title>A Dialog-Based Macro</title>
 10 <para>
 11  Now we will look at a more complicated macro which will demonstrate
 12  some useful techniques and BeanShell features.
 13 </para>
 15 <sect1 id="dialog-macro-intro"><title>Use of the Macro</title>
 16  <para>
 17    Our new example adds prefix and suffix text to a series of selected lines. This
 18    macro can be used to reduce typing for a series of text items that must be
 19    preceded and following by identical text. In Java, for example, if we are
 20    interested in making a series of calls to
 21    <function>StringBuffer.append()</function> to construct a lengthy, formatted
 22    string, we could type the parameter for each call on successive lines as follows:
 23  </para>
 34   <para>
 35    Our macro would ask for input for the common <quote>prefix</quote> and
 36    <quote>suffix</quote> to be applied to each line; in this case, the prefix is
 37    <userinput>ourStringBuffer.append(</userinput> and the suffix is
 38    <userinput>);</userinput>. After selecting these lines and
 39    running the macro, the resulting text would look like this:
 40   </para>
 50 </sect1>
 52<sect1 id="add-prefix-and-suffix"><title>Listing of the Macro</title>
 54   <para>
 55    The macro script follows. You can find it in the jEdit
 56    distribution in the <filename>Text</filename> subdirectory of the
 57    <filename>macros</filename> directory. You can also try it out by invoking
 58    <guimenu>Macros</guimenu>&gt;<guisubmenu>Text</guisubmenu>&gt;<guimenuitem>Add
 59    Prefix and Suffix</guimenuitem>.
 60   </para>
 63<!-- <title>Add_Prefix_and_Suffix.bsh</title> -->
 64<programlisting>// beginning of Add_Prefix_and_Suffix.bsh
 66<anchor id="imports"/>// import statement (see <xref linkend=
 68import javax.swing.border.*;
 70<anchor id="main-routine"/>// main routine
 71void prefixSuffixDialog()
 73<anchor id="create-dialog"/>    // create dialog object (see <xref
 74 linkend="explain-create-dialog"/>)
 75    title = <quote>Add prefix and suffix to selected lines</quote>;
 76    dialog = new JDialog(view, title, false);
 77    content = new JPanel(new BorderLayout());
 78    content.setBorder(new EmptyBorder(12, 12, 12, 12));
 79    content.setPreferredSize(new Dimension(320, 160));
 80    dialog.setContentPane(content);
 82<anchor id="fields-panel"/>    // add the text fields (see <xref linkend=
 84    fieldPanel = new JPanel(new GridLayout(4, 1, 0, 6));
 85    prefixField = new HistoryTextField(<quote>macro.add-prefix</quote>);
 86    prefixLabel = new JLabel(<quote>Prefix to add:</quote>);
 87    suffixField = new HistoryTextField(<quote>macro.add-suffix</quote>);
 88    suffixLabel = new JLabel(<quote>Suffix to add:</quote>);
 89    fieldPanel.add(prefixLabel);
 90    fieldPanel.add(prefixField);
 91    fieldPanel.add(suffixLabel);
 92    fieldPanel.add(suffixField);
 93    content.add(fieldPanel, <quote>Center</quote>);
 95<anchor id="button-panel"/>    // add a panel containing the buttons (see <xref
 96 linkend="explain-button-panel"/>)
 97    buttonPanel = new JPanel();
 98    buttonPanel.setLayout(new BoxLayout(buttonPanel,
 99        BoxLayout.X_AXIS));
100    buttonPanel.setBorder(new EmptyBorder(12, 50, 0, 50));
101    buttonPanel.add(Box.createGlue());
102    ok = new JButton(<quote>OK</quote>);
103    cancel = new JButton(<quote>Cancel</quote>);
104    ok.setPreferredSize(cancel.getPreferredSize());
105    dialog.getRootPane().setDefaultButton(ok);
106    buttonPanel.add(ok);
107    buttonPanel.add(Box.createHorizontalStrut(6));
108    buttonPanel.add(cancel);
109    buttonPanel.add(Box.createGlue());
110    content.add(buttonPanel, <quote>South</quote>);
112<anchor id="add-listeners"/>    // register this method as an ActionListener for
113    // the buttons and text fields (see <xref linkend=
115    ok.addActionListener(this);
116    cancel.addActionListener(this);
117    prefixField.addActionListener(this);
118    suffixField.addActionListener(this);
120<anchor id="set-visible"/>    // locate the dialog in the center of the
121    // editing pane and make it visible (see <xref linkend=
123    dialog.pack();
124    dialog.setLocationRelativeTo(view);
125    dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
126    dialog.setVisible(true);
128<anchor id="action-listener"/>    // this method will be called when a button is clicked
129    // or when ENTER is pressed (see <xref linkend=
131    void actionPerformed(e)
132    {
133        if(e.getSource() != cancel)
134        {
135            processText();
136        }
137        dialog.dispose();
138    }
140<anchor id="process-text"/>    // this is where the work gets done to insert
141    // the prefix and suffix (see <xref linkend=
143    void processText()
144    {
145        prefix = prefixField.getText();
146        suffix = suffixField.getText();
147        if(prefix.length() == 0 &amp;&amp; suffix.length() == 0)
148            return;
149        prefixField.addCurrentToHistory();
150        suffixField.addCurrentToHistory();
152<anchor id="jEdit-calls"/>        // text manipulation begins here using calls
153        // to jEdit methods  (see <xref linkend="explain-jedit-calls"/>)
154        buffer.beginCompoundEdit();
155        selectedLines = textArea.getSelectedLines();
156        for(i = 0; i &lt; selectedLines.length; ++i)
157        {
158            offsetBOL = textArea.getLineStartOffset(
159                selectedLines[i]);
160            textArea.setCaretPosition(offsetBOL);
161            textArea.goToStartOfWhiteSpace(false);
162            textArea.goToEndOfWhiteSpace(true);
163            text = textArea.getSelectedText();
164            if(text == null) text = &quot;&quot;;
165            textArea.setSelectedText(prefix + text + suffix);
166        }
167        buffer.endCompoundEdit();
168    }
171<anchor id="main"/>// this single line of code is the script's main routine
172// (see <xref linkend=
176// end of Add_Prefix_and_Suffix.bsh</programlisting>
180<sect1 id="macro-analysis"><title>Analysis of the Macro</title>
182<sect2 id="explain-imports"><title>Import Statements</title>
184<informalexample><programlisting>// import statement
185import javax.swing.border.*;</programlisting></informalexample>
187  <para>
188    This macro makes use of classes in the
189    <literal>javax.swing.border</literal> package, which is not
190    automatically imported. As we mentioned
191    previously (see <xref linkend="first-example" />), jEdit's implementation of
192    BeanShell causes a number of classes to be automatically imported. Classes
193    that are not automatically imported must be identified by a full qualified
194    name or be the subject of an <function>import</function> statement.
195  </para>
199<sect2 id="explain-create-dialog"><title>Create the Dialog</title>
201<informalexample><programlisting>// create dialog object
202title = <quote>Add prefix and suffix to selected lines</quote>;
203dialog = new JDialog(view, title, false);
204content = new JPanel(new BorderLayout());
205content.setBorder(new EmptyBorder(12, 12, 12, 12));
208  <para>
209    To get input for the macro, we need a dialog that provides for input of the prefix
210    and suffix strings, an <guibutton>OK</guibutton> button to perform text
211    insertion, and a <guibutton>Cancel</guibutton> button in case we change our
212    mind. We have decided to make the dialog window non-modal. This will allow us to
213    move around in the text buffer to find things we may need (including text to cut
214    and paste) while the macro is running and the dialog is visible.
215  </para>
217  <para>
218    The Java object we need is a <classname>JDialog</classname> object from
219    the Swing package. To construct one, we use the <function>new</function>
220    keyword and call a <glossterm>constructor</glossterm> function. The
221    constructor we use takes three parameters: the owner of the new dialog,
222    the title to be displayed in the dialog frame, and a
223    <classname>boolean</classname> parameter (<constant>true</constant> or
224    <constant>false</constant>) that specifies whether the dialog will be
225    modal or non-modal. We define the variable <varname>title</varname>
226    using a string literal, then use it immediately in the
227    <classname>JDialog</classname> constructor.
228  </para>
230  <para>
231    A <classname>JDialog</classname> object is a window containing a single object
232    called a <glossterm>content pane</glossterm>. The content pane in turn contains
233    the various visible components of the dialog. A
234    <classname>JDialog</classname> creates an empty content pane for itself as
235    during its construction.  However, to control the dialog's appearance
236    as much as possible, we will separately create our own content pane and
237    attach it to the <classname>JDialog</classname>. We do this by creating a
238    <classname>JPanel</classname> object. A <classname>JPanel</classname> is a
239    lightweight container for other components that can be set to a given size and
240    color. It also contains a <glossterm>layout</glossterm> scheme for arranging the
241    size and position of its components. Here we are constructing a
242    <classname>JPanel</classname> as a content pane with a
243    <classname>BorderLayout</classname>. We put a <classname>EmptyBorder</classname>
244    inside it to serve as a margin between the edge of the window and the components
245    inside. We then attach the <classname>JPanel</classname> as the dialog's content
246    pane, replacing the dialog's home-grown version.
247  </para>
249  <para>
250    A <classname>BorderLayout</classname> is one of the simpler layout
251    schemes available for container objects like <classname>JPanel</classname>.
252    A <classname>BorderLayout</classname>
253    divides the container into five sections: <quote>North</quote>,
254    <quote>South</quote>, <quote>East</quote>, <quote>West</quote> and
255    <quote>Center</quote>. Components are added to the layout using the
256    container's <function>add</function> method, specifying the component to
257    be added and the section to which it is assigned. Building a
258    component like our dialog window involves building a set of
259    nested containers and specifying the location of each of their
260    member components. We have taken the first step by creating a
261    <classname>JPanel</classname> as the dialog's content pane.
262  </para>
266<sect2 id="explain-fields-panel"><title>
267Create the Text Fields</title>
269<informalexample><programlisting>// add the text fields
270fieldPanel = new JPanel(new GridLayout(4, 1, 0, 6));
271prefixField = new HistoryTextField(&quot;macro.add-prefix&quot;);
272prefixLabel = new JLabel(<quote>Prefix to add</quote>:);
273suffixField = new HistoryTextField(<quote>macro.add-suffix</quote>);
274suffixLabel = new JLabel(<quote>Suffix to add:</quote>);
279content.add(fieldPanel, <quote>Center</quote>);</programlisting></informalexample>
281  <para>
282    Next we shall create a smaller panel containing two fields for
283    entering the prefix and suffix text and two labels identifying the
284    input fields.
285  </para>
287  <para>
288    For the text fields, we will use jEdit's
289    <ulink
290		url="../api/org/gjt/sp/jedit/gui/HistoryTextField.html">HistoryTextField</ulink>
291    class. It is derived from the Java Swing class
292    <classname>JTextField</classname>. This class offers the enhancement of a stored
293    list of prior values used as text input. When the component has input focus, the
294    up and down keys scroll through the prior values for the variable.
295    <!-- The prior values are stored in a file named
296    <filename>history</filename> located in the directory in which jEdit stores
297    various user data. -->
298  </para>
300  <para>
301    To create the <ulink
302		url="../api/org/gjt/sp/jedit/gui/HistoryTextField.html">HistoryTextField</ulink> objects we use a
303    constructor method that takes a single parameter: the name of the tag
304    under which history values will be stored. Here we choose names that are
305    not likely to conflict with existing jEdit history items.
306  </para>
308  <para>
309    The labels that accompany the text fields are
310    <classname>JLabel</classname> objects from the Java Swing
311    package. The constructor we use for both labels takes the label text
312    as a single <classname>String</classname> parameter.
313  </para>
315  <para>
316    We wish to arrange these four components from top to bottom,
317    one after the other. To achieve that, we use a
318    <classname>JPanel</classname> container object named
319    <varname>fieldPanel</varname> that
320    will be nested inside the dialog's content pane that we have
321    already created. In the constructor for <varname>fieldPanel</varname>,
322    we assign a new <classname>GridLayout</classname> with the indicated
323    parameters: four rows, one column, zero spacing between columns (a
324    meaningless element of a grid with only one column, but
325    nevertheless a required parameter) and spacing of six pixels between
326    rows. The spacing between rows spreads out the four <quote>grid</quote>
327    elements. After the components, the panel and the layout are
328    specified, the components are added to <varname>fieldPanel</varname>
329    top to bottom, one <quote>grid cell</quote> at a time. Finally, the complete
330    <varname>fieldPanel</varname> is added to the dialog's content pane to
331    occupy the <quote>Center</quote> section of the content pane.
332  </para>
335<sect2 id="explain-button-panel"><title>
336Create the Buttons</title>
338<informalexample><programlisting>// add the buttons
339buttonPanel = new JPanel();
340buttonPanel.setLayout(new BoxLayout(buttonPanel,
341    BoxLayout.X_AXIS));
342buttonPanel.setBorder(new EmptyBorder(12, 50, 0, 50));
344ok = new JButton(<quote>OK</quote>);
345cancel = new JButton(<quote>Cancel</quote>);
352content.add(buttonPanel, <quote>South</quote>);</programlisting></informalexample>
354  <para>
355    To create the dialog's buttons, we follow repeat the <quote>nested container</quote>
356    pattern we used in creating the text fields.
357    First, we create a new, nested panel. This time we use a <classname>BoxLayout</classname>
358    that places components either in a single row or
359    column, depending on the parameter passed to its constructor. This layout object
360    is more flexible than a <classname>GridLayout</classname> in that variable spacing
361    between elements can be specified easily. We put an
362    <classname>EmptyBorder</classname> in the new panel to set margins for placing
363    the buttons. Then we create the buttons, using a <classname>JButton</classname>
364    constructor that specifies the button text. After setting the size of the
365    <guilabel>OK</guilabel> button to equal the size of the
366    <guilabel>Cancel</guilabel> button, we designate the <guilabel>OK</guilabel>
367    button as the default button in the dialog. This causes the
368    <guilabel>OK</guilabel> button to be outlined when the dialog if first displayed.
369    Finally, we place the buttons side by side with a 6 pixel gap between them (for aesthetic
370    reasons), and place the completed <varname>buttonPanel</varname> in the
371    <quote>South</quote> section of the dialog's content pane.
372  </para>
375<sect2 id="explain-add-listeners">
376<title>Register the Action Listeners</title>
378<informalexample><programlisting>// register this method as an ActionListener for
379// the buttons and text fields
385  <para>
386    In order to specify the action to be taken upon clicking a
387    button or pressing the <keycap>Enter</keycap> key, we must register
388    an <classname>ActionListener</classname> for each of the four active
389    components of the dialog - the two
390    <ulink
391		url="../api/org/gjt/sp/jedit/HistoryTextField.html">HistoryTextField</ulink>
392    components and the two buttons. In Java, an
393    <classname>ActionListener</classname> is an <glossterm>interface</glossterm> - an
394    abstract specification for a derived class to implement. The
395    <classname>ActionListener</classname> interface contains a single method to
396    be implemented:
397  </para>
400 <funcprototype>
401  <funcdef>public void <function>actionPerformed</function></funcdef>
402  <paramdef>ActionEvent <parameter>e</parameter></paramdef>
403 </funcprototype>
406  <para>
407    BeanShell does not permit a script to create derived classes.
408    However, BeanShell offers a useful substitute: a method can be
409    used as a scripted object that can include nested methods implementing a
410    number of Java interfaces. The method
411    <function>prefixSuffixDialog()</function> that we are writing can thus be
412    treated as an <classname>ActionListener</classname> object. To accomplish this, we
413    call <function>addActionListener()</function> on each of the four
414    components specifying <varname>this</varname> as the
415    <classname>ActionListener</classname>. We still need to implement the
416    interface. We will do that shortly.
417  </para>
421<sect2 id="explain-set-visible">
422<title>Make the Dialog Visible</title>
424<informalexample><programlisting>// locate the dialog in the center of the
425// editing pane and make it visible
431  <para>
432    Here we do three things. First, we activate all the layout routines we have
433    established by calling the <function>pack()</function> method for the dialog as
434    the top-level window. Next we center the dialog's position in the active jEdit
435    <varname>view</varname> by calling <function>setLocationRelativeTo()</function>
436    on the dialog. We also call the <function>setDefaultCloseOperation()</function>
437    function to specify that the dialog box should be immediately disposed if the
438    user clicks the close box. Finally, we activate the dialog by calling
439    <function>setVisible()</function>with the state parameter set to
440    <constant>true</constant>.
441  </para>
443  <para>
444    At this point we have a decent looking dialog window that
445    doesn't do anything. Without more code, it will not respond to
446    user input and will not accomplish any text manipulation. The
447    remainder of the script deals with these two requirements.
448  </para>
452<sect2 id="explain-action-listener">
453<title>The Action Listener</title>
455<informalexample><programlisting>// this method will be called when a button is clicked
456// or when ENTER is pressed
457void actionPerformed(e)
459    if(e.getSource() != cancel)
460    {
461        processText();
462    }
463    dialog.dispose();
466  <para>
467    The method <function>actionPerformed()</function> nested inside
468    <function>prefixSuffixDialog()</function> implements the implicit
469    <classname>ActionListener</classname> interface. It looks at the source
470    of the <classname>ActionEvent</classname>, determined by a call to
471    <function>getSource()</function>. What we do with this return value is
472    straightforward: if the source is not the <guibutton>Cancel</guibutton> button, we
473    call the <function>processText()</function> method to insert the prefix
474    and suffix text. Then the dialog is closed by calling its
475    <function>dispose()</function> method.
476  </para>
478  <para>
479    The ability to implement interfaces like
480    <classname>ActionListener</classname> inside a BeanShell script is
481    one of the more powerful features of the BeanShell package. With an
482    <classname>ActionListener</classname> interface, which has only a
483    single method, implementation is simple.  When using other
484    interfaces with multiple methods, however, there are some details to
485    deal with that will vary depending on the version of the Java
486    platform that you are running.  These techniques are discussed in
487    the next chapter; see <xref linkend="macro-tips-BeanShell-interface" />.
488  </para>
492<sect2 id="explain-process-text">
493<title>Get the User's Input</title>
495<informalexample><programlisting>// this is where the work gets done to insert
496// the prefix and suffix
497void processText()
499    prefix = prefixField.getText();
500    suffix = suffixField.getText();
501    if(prefix.length() == 0 &amp;&amp; suffix.length() == 0)
502        return;
503    prefixField.addCurrentToHistory();
504    suffixField.addCurrentToHistory();</programlisting></informalexample>
506  <para>
507    The method <function>processText()</function> does the work of our
508    macro. First we obtain the input from the two text fields with a
509    call to their <function>getText()</function> methods. If they are both
510    empty, there is nothing to do, so the method returns. If there is
511    input, any text in the field is added to that field's stored
512    history list by calling <function>addCurrentToHistory()</function>.
513    We do not need to test the <varname>prefixField</varname> or
514    <varname>suffixField</varname> controls for <constant>null</constant>
515    or empty values because <function>addCurrentToHistory()</function>
516    does that internally.
517  </para>
521<sect2 id="explain-jedit-calls">
522<title>Call jEdit Methods to Manipulate Text</title>
524<informalexample><programlisting>    // text manipulation begins here using calls
525    // to jEdit methods
526    buffer.beginCompoundEdit();
527    selectedLines = textArea.getSelectedLines();
528    for(i = 0; i &lt; selectedLines.length; ++i)
529    {
530        offsetBOL = textArea.getLineStartOffset(
531            selectedLines[i]);
532        textArea.setCaretPosition(offsetBOL);
533        textArea.goToStartOfWhiteSpace(false);
534        textArea.goToEndOfWhiteSpace(true);
535        text = textArea.getSelectedText();
536        if(text == null) text = &quot;&quot;;
537        textArea.setSelectedText(prefix + text + suffix);
538    }
539    buffer.endCompoundEdit();
542  <para>
543    The text manipulation routine loops through each selected line
544    in the text buffer. We get the loop parameters by calling
545    <function>textArea.getSelectedLines()</function>, which returns an array
546    consisting of the line numbers of every selected line.  The array includes the
547    number of the current line, whether or not it is selected, and the line numbers
548    are sorted in increasing order.  We iterate through each member of the
549    <varname>selectedLines</varname> array, which represents the number of a
550    selected line, and apply the following routine:
551  </para>
555  <listitem>
556    <para>
557      Get the buffer position of the start of the line (expressed
558      as a zero-based index from the start of the buffer) by calling
559      <function>textArea.getLineStartOffset(selectedLines[i])</function>;
560    </para>
561  </listitem>
563  <listitem>
564    <para>
565      Move the caret to that position by calling
566      <function>textArea.setCaretPosition()</function>;
567    </para>
568  </listitem>
570  <listitem>
571    <para>
572      Find the first and last non-whitespace characters on the line
573      by calling <function>textArea.goToStartOfWhiteSpace()</function> and
574      <function>textArea.goToEndOfWhiteSpace()</function>;
575    </para>
577    <para>
578      The <function>goTo...</function> methods in
579      <ulink
580		url="../api/org/gjt/sp/jedit/textarea/JEditTextArea.html">JEditTextArea</ulink> take a single parameter which
581      tells jEdit whether the text between the current caret position and
582      the desired position should be selected. Here, we call
583      <function>textArea.goToStartOfWhiteSpace(false)</function> so that
584      no text is selected, then call
585      <function>textArea.goToEndOfWhiteSpace(true)</function> so that all of
586      the text between the beginning and ending whitespace is
587      selected.
588    </para>
589  </listitem>
591  <listitem>
592    <para>
593      Retrieve the selected text by storing the return value of
594      <function>textArea.getSelectedText()</function> in a new variable
595      <function>text</function>.
596    </para>
598    <para>
599      If the line is empty, <function>getSelectedText()</function> will
600      return <constant>null</constant>. In that case, we assign an empty
601      string to <varname>text</varname> to avoid calling methods on a
602      null object.
603    </para>
604  </listitem>
606  <listitem>
607    <para>
608      Change the selected text to <varname>prefix + text +
609      suffix</varname> by calling
610      <function>textArea.setSelectedText()</function>.
611      If there is no selected text (for example, if the line is empty),
612      the prefix and suffix will be inserted without any intervening
613      characters.
614    </para>
615  </listitem>
619<sidebar><title>Compound edits</title>
620 <para>
621  Note the <function>beginCompoundEdit()</function> and
622  <function>endCompoundEdit()</function> calls. These ensure that all edits
623  performed between the two calls can be undone in one step. Normally,
624  jEdit automatically wraps a macro call in these methods; however if
625  the macro shows a non-modal dialog box, as far as jEdit is concerned
626  the macro has finished executing by the time the dialog is shown,
627  since control returns to the event dispatch thread.
628 </para>
629 <para>
630  If you do not understand this, don't worry; just keep it in mind if
631  your macro needs to show a non-modal dialog box for some reason;
632  Most macros won't.
633 </para>
636  <!-- <para>
637    The loop routine is bracketed by calls to
638    <function>buffer.beginCompoundEdit()</function> and
639    <function>buffer.endCompoundEdit()</function>.  These methods
640    modify the way in which jEdit's undo facility performs.  Text
641    operations done between calls to these functions will be
642    treated as a single operation for undo purposes.  A single undo
643    command issued immediately after the macro completes will thus remove
644    the prefix and suffix text from all of the previously selected lines.
645  </para> -->
649<sect2 id="explain-main">
650<title>The Main Routine</title>
652<informalexample><programlisting>// this single line of code is the script's main routine
656  <para>
657    The call to <function>prefixSuffixDialog()</function>is the only line
658    in the macro that is not inside an enclosing block. BeanShell
659    treats such code as a top-level <function>main</function> method and
660    begins execution with it.
661  </para>
663  <para>
664    Our analysis of <filename>Add_Prefix_and_Suffix.bsh</filename> is now
665    complete. In the next section, we look at other ways in which a macro
666    can obtain user input, as well as other macro writing techniques.
667  </para>