<?php
// @(#) $Id: Textile.php,v 1.5 2005/03/21 15:33:01 jhriggs Exp $

/* This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
 */

/**
 * The Textile class serves as a wrapper for all Textile
 * functionality. It is not inherently necessary that Textile be a
 * class; however, this is as close as one can get to a namespace in
 * PHP. Wrapping the functionality in a class prevents name
 * collisions and dirtying of the global namespace. The Textile class
 * uses no global variables and will not have any side-effects on
 * other code.
 *
 * @brief Class wrapper for the Textile functionality.
 */
class Textile {
  /**
   * The @c array containing all of the Textile options for this
   * object.
   *
   * @private
   */
  var $options = array();

  /**
   * The @c string containing the regular expression pattern for a
   * URL. This variable is initialized by @c _create_re() which is
   * called in the contructor.
   *
   * @private
   */
  var $urlre;

  /**
   * The @c string containing the regular expression pattern for
   * punctuation characters. This variable is initialized by
   * @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $punct;

  /**
   * The @c string containing the regular expression pattern for the
   * valid vertical alignment codes. This variable is initialized by
   * @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $valignre;

  /**
   * The @c string containing the regular expression pattern for the
   * valid table alignment codes. This variable is initialized by
   * @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $tblalignre;

  /**
   * The @c string containing the regular expression pattern for the
   * valid horizontal alignment codes. This variable is initialized by
   * @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $halignre;

  /**
   * The @c string containing the regular expression pattern for the
   * valid alignment codes. This variable is initialized by
   * @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $alignre;

  /**
   * The @c string containing the regular expression pattern for the
   * valid image alignment codes. This variable is initialized by
   * @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $imgalignre;

  /**
   * The @c string containing the regular expression pattern for a
   * class, ID, and/or padding specification. This variable is
   * initialized by @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $clstypadre;

  /**
   * The @c string containing the regular expression pattern for a
   * class and/or ID specification. This variable is initialized by
   * @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $clstyre;

  /**
   * The @c string containing the regular expression pattern for a
   * class, ID, and/or filter specification. This variable is
   * initialized by @c _create_re() which is called in the contructor.
   *
   * @private
   */
  var $clstyfiltre;

  /**
   * The @c string containing the regular expression pattern for a
   * code block. This variable is initialized by @c _create_re() which
   * is called in the contructor.
   *
   * @private
   */
  var $codere;

  /**
   * The @c string containing the regular expression pattern for all
   * block tags. This variable is initialized by @c _create_re() which
   * is called in the contructor.
   *
   * @private
   */
  var $blocktags;

  /**
   * The @c array containing the list of lookup links.
   *
   * @private
   */
  var $links = array();

  /**
   * The @c array containing <code>array</code>s of replacement blocks
   * of text that are temporary removed from the input text to avoid
   * processing. Different functions use this replacement
   * functionality, and each shifts its own replacement array into
   * position 0 and removes it when finished. This avoids having
   * several replacement variables and/or functions clobbering
   * eachothers' replacement blocks.
   *
   * @private
   */
  var $repl = array();

  /**
   * The @c array containing temporary <code>string</code>s used in
   * replacement callbacks. *JHR*
   *
   * @private
   */
  var $tmp = array();

  /**
   * Instantiates a new Textile object. Optional options
   * can be passed to initialize the object. Attributes for the
   * options key are the same as the get/set method names
   * documented here.
   *
   * @param $options The @c array specifying the options to use for
   *        this object.
   *
   * @public
   */
  function Textile($options = array()) {
    $this->options = $options;
    $this->options['filters'] = ($this->options['filters'] ? $this->options['filters'] : array());
    $this->options['charset'] = ($this->options['charset'] ? $this->options['charset'] : 'iso-8859-1');
    $this->options['char_encoding'] = (isset($this->options['char_encoding']) ? $this->options['char_encoding'] : 1);
    $this->options['do_quotes'] = (isset($this->options['do_quotes']) ? $this->options['do_quotes'] : 1);
    $this->options['trim_spaces'] = (isset($this->options['trim_spaces']) ? $this->options['trim_spaces'] : 0);
    $this->options['smarty_mode'] = (isset($this->options['smarty_mode']) ? $this->options['smarty_mode'] : 1);
    $this->options['preserve_spaces'] = (isset($this->options['preserve_spaces']) ? $this->options['preserve_spaaces'] : 0);
    $this->options['head_offset'] = (isset($this->options['head_offset']) ? $this->options['head_offset'] : 0);

    if (is_array($this->options['css'])) {
      $this->css($this->options['css']);
    }
    $this->options['macros'] = ($this->options['macros'] ? $this->options['macros'] : $this->default_macros());
    if (isset($this->options['flavor'])) {
      $this->flavor($this->options['flavor']);
    } else {
      $this->flavor('xhtml1/css');
    }
    $this->_create_re();
  } // function Textile

  // getter/setter methods...

  /**
   * Used to set Textile attributes. Attribute names are the same
   * as the get/set method names documented here.
   *
   * @param $opt A @c string specifying the name of the option to
   *        change or an @c array specifying options and values.
   * @param $value The value for the provided option name.
   *
   * @public
   */
  function set($opt, $value = NULL) {
    if (is_array($opt)) {
      foreach ($opt as $opt => $value) {
        $this->set($opt, $value);
      }
    } else {
      // the following options have special set methods
      // that activate upon setting:
      if ($opt == 'charset') {
        $this->charset($value);
      } elseif ($opt == 'css') {
        $this->css($value);
      } elseif ($opt == 'flavor') {
        $this->flavor($value);
      } else {
        $this->options[$opt] = $value;
      }
    }
  } // function set

  /**
   * Used to get Textile attributes. Attribute names are the same
   * as the get/set method names documented here.
   *
   * @param $opt A @c string specifying the name of the option to get.
   *
   * @return The value for the provided option.
   *
   * @public
   */
  function get($opt) {
    return $this->options[$opt];
  } // function get

  /**
   * Gets or sets the "disable html" control, which allows you to
   * prevent HTML tags from being used within the text processed.
   * Any HTML tags encountered will be removed if disable html is
   * enabled. Default behavior is to allow HTML.
   *
   * @param $disable_html If provided, a @c bool indicating whether or
   *        not this object should disable HTML.
   *
   * @return A true value if this object disables HTML; a false value
   *         otherwise.
   *
   * @public
   */
  function disable_html($disable_html = NULL) {
    if ($disable_html != NULL) {
      $this->options['disable_html'] = $disable_html;
    }
    return ($this->options['disable_html'] ? $this->options['disable_html'] : 0);
  } // function disable_html

  /**
   * Gets or sets the relative heading offset, which allows you to
   * change the heading level used within the text processed. For
   * example, if the heading offset is '2' and the text contains an
   * 'h1' block, an \<h3\> block will be output.
   *
   * @param $head_offset If provided, an @c integer specifying the
   *        heading offset for this object.
   *
   * @return An @c integer containing the heading offset for this
   *         object.
   *
   * @public
   */
  function head_offset($head_offset = NULL) {
    if ($head_offset != NULL) {
      $this->options['head_offset'] = $head_offset;
    }
    return ($this->options['head_offset'] ? $this->options['head_offset'] : 0);
  } // function head_offset

  /**
   * Assigns the HTML flavor of output from Textile. Currently
   * these are the valid choices: html, xhtml (behaves like "xhtml1"),
   * xhtml1, xhtml2. Default flavor is "xhtml1".
   *
   * Note that the xhtml2 flavor support is experimental and incomplete
   * (and will remain that way until the XHTML 2.0 draft becomes a
   * proper recommendation).
   *
   * @param $flavor If provided, a @c string specifying the flavor to
   *        be used for this object.
   *
   * @return A @c string containing the flavor for this object.
   *
   * @public
   */
  function flavor($flavor = NULL) {
    if ($flavor != NULL) {
      $this->options['flavor'] = $flavor;
      if (preg_match('/^xhtml(\d)?(\D|$)/', $flavor, $matches)) {
        if ($matches[1] == '2') {
          $this->options['_line_open'] = '<l>';
          $this->options['_line_close'] = '</l>';
          $this->options['_blockcode_open'] = '<blockcode>';
          $this->options['_blockcode_close'] = '</blockcode>';
          $this->options['css_mode'] = 1;
        } else {
          // xhtml 1.x
          $this->options['_line_open'] = '';
          $this->options['_line_close'] = '<br />';
          $this->options['_blockcode_open'] = '<pre><code>';
          $this->options['_blockcode_close'] = '</code></pre>';
          $this->options['css_mode'] = 1;
        }
      } elseif (preg_match('/^html/', $flavor)) {
        $this->options['_line_open'] = '';
        $this->options['_line_close'] = '<br>';
        $this->options['_blockcode_open'] = '<pre><code>';
        $this->options['_blockcode_close'] = '</code></pre>';
        $this->options['css_mode'] = preg_match('/\/css/', $flavor);
      }
      if ($this->options['css_mode'] && !isset($this->options['css'])) { $this->_css_defaults(); }
    }
    return $this->options['flavor'];
  } // function flavor

  /**
   * Gets or sets the css support for Textile. If css is enabled,
   * Textile will emit CSS rules. You may pass a 1 or 0 to enable
   * or disable CSS behavior altogether. If you pass an associative array,
   * you may assign the CSS class names that are used by
   * Textile. The following key names for such an array are
   * recognized:
   *
   * <ul>
   * <li><b>class_align_right</b>
   *
   * defaults to 'right'</li>
   *
   * <li><b>class_align_left</b>
   *
   * defaults to 'left'</li>
   *
   * <li><b>class_align_center</b>
   *
   * defaults to 'center'</li>
   *
   * <li><b>class_align_top</b>
   *
   * defaults to 'top'</li>
   *
   * <li><b>class_align_bottom</b>
   *
   * defaults to 'bottom'</li>
   *
   * <li><b>class_align_middle</b>
   *
   * defaults to 'middle'</li>
   *
   * <li><b>class_align_justify</b>
   *
   * defaults to 'justify'</li>
   *
   * <li><b>class_caps</b>
   *
   * defaults to 'caps'</li>
   *
   * <li><b>class_footnote</b>
   *
   * defaults to 'footnote'</li>
   *
   * <li><b>id_footnote_prefix</b>
   *
   * defaults to 'fn'</li>
   *
   * </ul>
   *
   * @param $css If provided, either a @c bool indicating whether or
   *        not this object should use css or an associative @c array
   *        specifying class names to use.
   *
   * @return Either an associative @c array containing class names
   *         used by this object, or a true or false value indicating
   *         whether or not this object uses css.
   *
   * @public
   */
  function css($css = NULL) {
    if ($css != NULL) {
      if (is_array($css)) {
        $this->options['css'] = $css;
        $this->options['css_mode'] = 1;
      } else {
        $this->options['css_mode'] = $css;
        if ($this->options['css_mode'] && !isset($this->options['css'])) { $this->_css_defaults(); }
      }
    }
    return ($this->options['css_mode'] ? $this->options['css'] : 0);
  } // function css

  /**
   * Gets or sets the character set targetted for publication.
   * At this time, Textile only changes its behavior
   * if the 'utf-8' character set is assigned.
   *
   * Specifically, if utf-8 is requested, any special characters
   * created by Textile will be output as native utf-8 characters
   * rather than HTML entities.
   *
   * @param $charset If provided, a @c string specifying the
   *        characater set to be used for this object.
   *
   * @return A @c string containing the character set for this object.
   *
   * @public
   */
  function charset($charset = NULL) {
    if ($charset != NULL) {
        $this->options['charset'] = $charset;
        if (preg_match('/^utf-?8$/i', $this->options['charset'])) {
          $this->char_encoding(0);
        } else {
          $this->char_encoding(1);
        }
    }
    return $this->options['charset'];
  } // function charset

  /**
   * Gets or sets the physical file path to root of document files.
   * This path is utilized when images are referenced and size
   * calculations are needed (the getimagesize() function is used to read
   * the image dimensions).
   *
   * @param $docroot If provided, a @c string specifying the document
   *        root to use for this object.
   *
   * @return A @c string containing the docroot for this object.
   *
   * @public
   */
  function docroot($docroot = NULL) {
    if ($docroot != NULL) {
      $this->options['docroot'] = $docroot;
    }
    return $this->options['docroot'];
  } // function docroot

  /**
   * Gets or sets the 'trim spaces' control flag. If enabled, this
   * will clear any lines that have only spaces on them (the newline
   * itself will remain).
   *
   * @param $trim_spaces If provided, a @c bool indicating whether or
   *        not this object should trim spaces.
   *
   * @return A true value if this object trims spaces; a false value
   *         otherwise.
   *
   * @public
   */
  function trim_spaces($trim_spaces = NULL) {
    if ($trim_spaces != NULL) {
      $this->options['trim_spaces'] = $trim_spaces;
    }
    return $this->options['trim_spaces'];
  } // function trim_spaces

  /**
   * Gets or sets a parameter that is passed to filters.
   *
   * @param $filter_param If provided, a parameter that this object
   *        should pass to filters.
   *
   * @return The parameter this object passes to filters.
   *
   * @public
   */
  function filter_param($filter_param = NULL) {
    if ($filter_param != NULL) {
      $this->options['filter_param'] = $filter_param;
    }
    return $this->options['filter_param'];
  } // function filter_param

  /**
   * Gets or sets the 'preserve spaces' control flag. If enabled, this
   * will replace any double spaces within the paragraph data with the
   * \&amp;#8195; HTML entity (wide space). The default is 0. Spaces will
   * pass through to the browser unchanged and render as a single space.
   * Note that this setting has no effect on spaces within \<pre\>,
   * \<code\> blocks or \<script\> sections.
   *
   * @param $preserve_spaces If provided, a @c bool indicating whether
   *        or not this object should preserve spaces.
   *
   * @return A true value if this object preserves spaces; a false
   *         value otherwise.
   *
   * @public
   */
  function preserve_spaces($preserve_spaces = NULL) {
    if ($preserve_spaces != NULL) {
      $this->options['preserve_spaces'] = $preserve_spaces;
    }
    return $this->options['preserve_spaces'];
  } // function preserve_spaces

  /**
   * Gets or sets a list of filters to make available for
   * Textile to use. Returns a hash reference of the currently
   * assigned filters.
   *
   * @param $filters If provided, an @c array of filters to be used
   *        for this object.
   *
   * @return An @c array containing the filters for this object.
   *
   * @public
   */
  function filters($filters = NULL) {
    if ($filters != NULL) {
      $this->options['filters'] = $filters;
    }
    return $this->options['filters'];
  } // function filters

  /**
   * Gets or sets the character encoding logical flag. If character
   * encoding is enabled, the htmlentities function is used to
   * encode special characters. If character encoding is disabled,
   * only \<, \>, " and & are encoded to HTML entities.
   *
   * @param $char_encoding If provided, a @c bool indicating whether
   *        or not this object should encode special characters.
   *
   * @return A true value if this object encodes special characters; a
   *         false value otherwise.
   *
   * @public
   */
  function char_encoding($char_encoding = NULL) {
    if ($char_encoding != NULL) {
      $this->options['char_encoding'] = $char_encoding;
    }
    return $this->options['char_encoding'];
  } // function char_encoding

  /**
   * Gets or sets the "smart quoting" control flag. Returns the
   * current setting.
   *
   * @param $do_quotes If provided, a @c bool indicating whether or
   *        not this object should use smart quoting.
   *
   * @return A true value if this object uses smart quoting; a false
   *         value otherwise.
   *
   * @public
   */
  function handle_quotes($do_quotes = NULL) {
    if ($do_quotes != NULL) {
      $this->options['do_quotes'] = $do_quotes;
    }
    return $this->options['do_quotes'];
  } // function handle_quotes

  // end of getter/setter methods

  /**
   * Creates the class variable regular expression patterns used by
   * Textile. They are not initialized in the declaration, because
   * some rely on the others, requiring a @c $this reference.
   *
   * PHP does not have the Perl qr operator to quote or precompile
   * patterns, so to avoid escaping and matching problems, all
   * patterns must use the same delimiter; this implementation uses
   * {}. Every use of these patterns within this class has been
   * changed to use these delimiters. *JHR*
   *
   * @private
   */
  function _create_re() {
    // a URL discovery regex. This is from Mastering Regex from O'Reilly.
    // Some modifications by Brad Choate <brad at bradchoate dot com>
    $this->urlre = '(?:
    # Must start out right...
    (?=[a-zA-Z0-9./#])
    # Match the leading part (proto://hostname, or just hostname)
    (?:
        # ftp://, http://, or https:// leading part
        (?:ftp|https?|telnet|nntp)://(?:\w+(?::\w+)?@)?[-\w]+(?:\.\w[-\w]*)+
        |
        (?:mailto:)?[-\+\w]+@[-\w]+(?:\.\w[-\w]*)+
        |
        # or, try to find a hostname with our more specific sub-expression
        (?i: [a-z0-9] (?:[-a-z0-9]*[a-z0-9])? \. )+ # sub domains
        # Now ending .com, etc. For these, require lowercase
        (?-i: com\b
            | edu\b
            | biz\b
            | gov\b
            | in(?:t|fo)\b # .int or .info
            | mil\b
            | net\b
            | org\b
            | museum\b
            | aero\b
            | coop\b
            | name\b
            | pro\b
            | [a-z][a-z]\b # two-letter country codes
        )
    )?

    # Allow an optional port number
    (?: : \d+ )?

    # The rest of the URL is optional, and begins with / . . .
    (?:
     /?
     # The rest are heuristics for what seems to work well
     [^.!,?;:"\'<>()\[\]{}\s\x7F-\xFF]*
     (?:
        [.!,?;:]+  [^.!,?;:"\'<>()\[\]{}\s\x7F-\xFF]+ #\'"
     )*
    )?
)';

    $this->punct = '[\!"\#\$%&\'()\*\+,\-\./:;<=>\?@\[\\\\\]\^_`{\|}\~]';
    $this->valignre = '[\-^~]';
    $this->tblalignre = '[<>=]';
    $this->halignre = '(?:<>|[<>=])';
    $this->alignre = '(?:(?:' . $this->valignre . '|<>' . $this->valignre . '?|' . $this->valignre . '?<>|' . $this->valignre . '?' . $this->halignre . '?|' . $this->halignre . '?' . $this->valignre . '?)(?!\w))';
    $this->imgalignre = '(?:(?:[<>]|' . $this->valignre . '){1,2})';

    $this->clstypadre = '(?:
  (?:\([A-Za-z0-9_\- \#]+\))
  |
  (?:{
      (?: \( [^)]+ \) | [^\}] )+
     })
  |
  (?:\(+? (?![A-Za-z0-9_\-\#]) )
  |
  (?:\)+?)
  |
  (?: \[ [a-zA-Z\-]+? \] )
)';

    $this->clstyre = '(?:
  (?:\([A-Za-z0-9_\- \#]+\))
  |
  (?:{
      [A-Za-z0-9_\-](?: \( [^)]+ \) | [^\}] )+
     })
  |
  (?: \[ [a-zA-Z\-]+? \] )
)';

    $this->clstyfiltre = '(?:
  (?:\([A-Za-z0-9_\- \#]+\))
  |
  (?:{
      [A-Za-z0-9_\-](?: \( [^)]+ \) | [^\}] )+
     })
  |
  (?:\|[^\|]+\|)
  |
  (?:\(+?(?![A-Za-z0-9_\-\#]))
  |
  (?:\)+)
  |
  (?: \[ [a-zA-Z]+? \] )
)';

    $this->codere = '(?:
    (?:
      [\[{]
      @                           # opening
      (?:\[([A-Za-z0-9]+)\])?     # $1: language id
      (.+?)                       # $2: code
      @                           # closing
      [\]}]
    )
    |
    (?:
      (?:^|(?<=[\s\(]))
      @                           # opening
      (?:\[([A-Za-z0-9]+)\])?     # $3: language id
      ([^\s].+?[^\s])             # $4: code itself
      @                           # closing
      (?:$|(?=' . $this->punct . '{1,2}|\s))
    )
)';

    $this->blocktags = '
    <
    (( /? ( h[1-6]
     | p
     | pre
     | div
     | table
     | t[rdh]
     | [ou]l
     | li
     | block(?:quote|code)
     | form
     | input
     | select
     | option
     | textarea
     )
    [ >]
    )
    | !--
    )
';
  } // function _create_re

  /**
   * Transforms the provided text using Textile markup rules.
   *
   * @param $str The @c string specifying the text to process.
   *
   * @return A @c string containing the processed (X)HTML.
   *
   * @public
   */
  function process($str) {
    /*
     * Function names in PHP are case insensitive, so function
     * textile() cannot be redefined.  Thus, this PHP implementation
     * will only use process().
     *
     *   return $this->textile($str);
     * } // function process
     *
     * function textile($str) {
     */

    // quick translator for abbreviated block names
    // to their tag
    $macros = array('bq' => 'blockquote');

    // an array to hold any portions of the text to be preserved
    // without further processing by Textile
    array_unshift($this->repl, array());

    // strip out extra newline characters. we're only matching for \n herein
    //$str = preg_replace('!(?:\r?\n|\r)!', "\n", $str);
    $str = preg_replace('!(?:\015?\012|\015)!', "\n", $str);

    // optionally remove trailing spaces
    if ($this->options['trim_spaces']) { $str = preg_replace('/ +$/m', '', $str); }

    // preserve contents of the '==', 'pre', 'blockcode' sections
    $str = preg_replace_callback('{(^|\n\n)==(.+?)==($|\n\n)}s',
                                 $this->_cb('"$m[1]\n\n" . $me->_repl($me->repl[0], $me->format_block(array("text" => $m[2]))) . "\n\n$m[3]"'), $str);

    if (!$this->disable_html()) {
      // preserve style, script tag contents
      $str = preg_replace_callback('!(<(style|script)(?:>| .+?>).*?</\2>)!s', $this->_cb('$me->_repl($me->repl[0], $m[1])'), $str);

      // preserve HTML comments
      $str = preg_replace_callback('|(<!--.+?-->)|s', $this->_cb('$me->_repl($me->repl[0], $m[1])'), $str);

      // preserve pre block contents, encode contents by default
      $pre_start = count($this->repl[0]);
      $str = preg_replace_callback('{(<pre(?: [^>]*)?>)(.+?)(</pre>)}s',
                                   $this->_cb('"\n\n" . $me->_repl($me->repl[0], $m[1] . $me->encode_html($m[2], 1) . $m[3]) . "\n\n"'), $str);
      // fix code tags within pre blocks we just saved.
      for ($i = $pre_start; $i < count($this->repl[0]); $i++) {
        $this->repl[0][$i] = preg_replace('|&lt;(/?)code(.*?)&gt;|s', '<$1code$2>', $this->repl[0][$i]);
      }

      // preserve code blocks by default, encode contents
      $str = preg_replace_callback('{(<code(?: [^>]+)?>)(.+?)(</code>)}s',
                                   $this->_cb('$me->_repl($me->repl[0], $m[1] . $me->encode_html($m[2], 1) . $m[3])'), $str);

      // encode blockcode tag (an XHTML 2 tag) and encode it's
      // content by default
      $str = preg_replace_callback('{(<blockcode(?: [^>]+)?>)(.+?)(</blockcode>)}s',
                                   $this->_cb('"\n\n" . $me->_repl($me->repl[0], $m[1] . $me->encode_html($m[2], 1) . $m[3]) . "\n\n"'), $str);

      // preserve PHPish, ASPish code
      $str = preg_replace_callback('!(<([\?%]).*?(\2)>)!s', $this->_cb('$me->_repl($me->repl[0], $m[1])'), $str);
    }

    // pass through and remove links that follow this format
    // [id_without_spaces (optional title text)]url
    // lines like this are stripped from the content, and can be
    // referred to using the "link text":id_without_spaces syntax
    //$links = array();
    $str = preg_replace_callback('{(?:\n|^) [ ]* \[ ([^ ]+?) [ ]*? (?:\( (.+?) \) )?  \] ((?:(?:ftp|https?|telnet|nntp)://|/)[^ ]+?) [ ]* (\n|$)}mx',
                                 $this->_cb('($me->links[$m[1]] = array("url" => $m[3], "title" => $m[2])) ? $m[4] : $m[4]'), $str);
    //$this->links = $links;

    // eliminate starting/ending blank lines
    $str = preg_replace('/^\n+/s', '', $str, 1);
    $str = preg_replace('/\n+$/s', '', $str, 1);

    // split up text into paragraph blocks, capturing newlines too
    $para = preg_split('/(\n{2,})/', $str, -1, PREG_SPLIT_DELIM_CAPTURE);
    unset($block, $bqlang, $filter, $class, $sticky, $lines,
          $style, $stickybuff, $lang, $clear);

    $out = '';

    foreach ($para as $para) {
      if (preg_match('/^\n+$/s', $para)) {
        if ($sticky && $stickybuff) {
          $stickybuff .= $para;
        } else {
          $out .= $para;
        }
        continue;
      }

      if ($sticky) {
        $sticky++;
      } else {
        unset($block);
        unset($class);
        $style = '';
        unset($lang);
      }

      unset($id, $cite, $align, $padleft, $padright, $lines, $buffer);
      if (preg_match('{^(h[1-6]|p|bq|bc|fn\d+)
                        ((?:' . $this->clstyfiltre . '*|' . $this->halignre . ')*)
                        (\.\.?)
                        (?::(\d+|' . $this->urlre . '))?\ (.*)$}sx', $para, $matches)) {
        if ($sticky) {
          if ($block == 'bc') {
            // close our blockcode section
            $out = preg_replace('/\n\n$/', '', $out, 1);
            $out .= $this->options['_blockcode_close'] . "\n\n";
          } elseif ($block == 'bq') {
            $out = preg_replace('/\n\n$/', '', $out, 1);
            $out .= '</blockquote>' . "\n\n";
          } elseif ($block == 'table') {
            $table_out = $this->format_table(array('text' => $stickybuff));
            if (!$table_out) { $table_out = ''; }
            $out .= $table_out;
            unset($stickybuff);
          } elseif ($block == 'dl') {
            $dl_out = $this->format_deflist(array('text' => $stickybuff));
            if (!$dl_out) { $dl_out = ''; }
            $out .= $dl_out;
            unset($stickybuff);
          }
          $sticky = 0;
        }
        // block macros: h[1-6](class)., bq(class)., bc(class)., p(class).
        //warn "paragraph: [[$para]]\n\tblock: $1\n\tparams: $2\n\tcite: $4";
        $block = $matches[1];
        $params = $matches[2];
        $cite = $matches[4];
        if ($matches[3] == '..') {
          $sticky = 1;
        } else {
          $sticky = 0;
          unset($class);
          unset($bqlang);
          unset($lang);
          $style = '';
          unset($filter);
        }
        if (preg_match('/^h([1-6])$/', $block, $matches2)) {
          if ($this->options['head_offset']) {
            $block = 'h' . ($matches2[1] + $this->options['head_offset']);
          }
        }
        if (preg_match('{(' . $this->halignre . '+)}', $params, $matches2)) {
          $align = $matches2[1];
          $params = preg_replace('{' . $this->halignre . '+}', '', $params, 1);
        }
        if ($params) {
          if (preg_match('/\|(.+)\|/', $params, $matches2)) {
            $filter = $matches2[1];
            $params = preg_replace('/\|.+?\|/', '', $params, 1);
          }
          if (preg_match('/{([^}]+)}/', $params, $matches2)) {
            $style = $matches2[1];
            $style = preg_replace('/\n/', ' ', $style);
            $params = preg_replace('/{[^}]+}/', '', $params);
          }
          if (preg_match('/\(([A-Za-z0-9_\-\ ]+?)(?:\#(.+?))?\)/', $params, $matches2) ||
              preg_match('/\(([A-Za-z0-9_\-\ ]+?)?(?:\#(.+?))\)/', $params, $matches2)) {
            if ($matches2[1] || $matches2[2]) {
              $class = $matches2[1];
              $id = $matches2[2];
              if ($class) {
                $params = preg_replace('/\([A-Za-z0-9_\-\ ]+?(#.*?)?\)/', '', $params);
              } elseif ($id) {
                $params = preg_replace('/\(#.+?\)/', '', $params);
              }
            }
          }
          if (preg_match('/(\(+)/', $params, $matches2)) {
            $padleft = strlen($matches2[1]);
            $params = preg_replace('/\(+/', '', $params, 1);
          }
          if (preg_match('/(\)+)/', $params, $matches2)) {
            $padright = strlen($matches2[1]);
            $params = preg_replace('/\)+/', '', $params, 1);
          }
          if (preg_match('/\[(.+?)\]/', $params, $matches2)) {
            $lang = $matches2[1];
            if ($block == 'bc') {
              $bqlang = $lang;
              unset($lang);
            }
            $params = preg_replace('/\[.+?\]/', '', $params, 1);
          }
        }
        // warn "settings:\n\tblock: $block\n\tpadleft: $padleft\n\tpadright: $padright\n\tclass: $class\n\tstyle: $style\n\tid: $id\n\tfilter: $filter\n\talign: $align\n\tlang: $lang\n\tsticky: $sticky";
        $para = $matches[5];
      } elseif (preg_match('|^<textile#(\d+)>$|', $para, $matches)) {
        $buffer = $this->repl[0][$matches[1] - 1];
      } elseif (preg_match('/^clear([<>]+)?\.$/', $para, $matches)) {
        if ($matches[1] == '<') {
          $clear = 'left';
        } elseif ($matches[1] == '>') {
          $clear = 'right';
        } else {
          $clear = 'both';
        }
        continue;
      } elseif ($sticky && $stickybuff &&
                ($block == 'table' || $block == 'dl')) {
        $stickybuff .= $para;
        continue;
      } elseif (preg_match('{^(?:' . $this->halignre . '|' . $this->clstypadre . '*)*
                              [\*\#]
                              (?:' . $this->halignre . '|' . $this->clstypadre . '*)*
                              \ }x', $para)) {
        // '*', '#' prefix means a list
        $buffer = $this->format_list(array('text' => $para));
      } elseif (preg_match('{^(?:table(?:' . $this->tblalignre . '|' . $this->clstypadre . '*)*
                              (\.\.?)\s+)?
                              (?:_|' . $this->alignre . '|' . $this->clstypadre . '*)*\|}x', $para, $matches)) {
        // handle wiki-style tables
        if ($matches[1] && ($matches[1] == '..')) {
          $block = 'table';
          $stickybuff = $para;
          $sticky = 1;
          continue;
        } else {
          $buffer = $this->format_table(array('text' => $para));
        }
      } elseif (preg_match('{^(?:dl(?:' . $this->clstyre . ')*(\.\.?)\s+)}x', $para, $matches)) {
        // handle definition lists
        if ($matches[1] && ($matches[1] == '..')) {
          $block = 'dl';
          $stickybuff = $para;
          $sticky = 1;
          continue;
        } else {
          $buffer = $this->format_deflist(array('text' => $para));
        }
      }
      if ($buffer) {
        $out .= $buffer;
        continue;
      }
      $lines = preg_split('/\n/', $para);
      if ((count($lines) == 1) && ($lines[0] == '')) {
        continue;
      }

      $block = ($block ? $block : 'p');

      $buffer = '';
      $pre = '';
      $post = '';

      if ($block == 'bc') {
        if ($sticky <= 1) {
          $pre .= $this->options['_blockcode_open'];
          $pre = preg_replace('/>$/s', '', $pre, 1);
          if ($bqlang) { $pre .= " language=\"$bqlang\""; }
          if ($align) {
            $alignment = $this->_halign($align);
            if ($this->options['css_mode']) {
              if (($padleft || $padright) &&
                  (($alignment == 'left') || ($alignment == 'right'))) {
                $style .= ';float:' . $alignment;
              } else {
                $style .= ';text-align:' . $alignment;
              }
              $class .= ' ' . ($this->options['css']["class_align_$alignment"] ? $this->options['css']["class_align_$alignment"] : $alignment);
            } else {
              if ($alignment) { $pre .= " align=\"$alignment\""; }
            }
          }
          if ($padleft) { $style .= ";padding-left:${padleft}em"; }
          if ($padright) { $style .= ";padding-right:${padright}em"; }
          if ($clear) { $style .= ";clear:${clear}"; }
          if ($class) { $class = preg_replace('/^ /', '', $class, 1); }
          if ($class) { $pre .= " class=\"$class\""; }
          if ($id) { $pre .= " id=\"$id\""; }
          if ($style) { $style = preg_replace('/^;/', '', $style, 1); }
          if ($style) { $pre .= " style=\"$style\""; }
          if ($lang) { $pre .= " lang=\"$lang\""; }
          $pre .= '>';
          unset($lang);
          unset($bqlang);
          unset($clear);
        }
        $para = preg_replace_callback('{(?:^|(?<=[\s>])|([{[]))
                                        ==(.+?)==
                                        (?:$|([\]}])|(?=' . $this->punct . '{1,2}|\s))}sx',
                                      $this->_cb('$me->_repl($me->repl[0], $me->format_block(array("text" => $m[2], "inline" => 1, "pre" => $m[1], "post" => $m[3])))'), $para);
        $buffer .= $this->encode_html_basic($para, 1);
        $buffer = preg_replace('/&lt;textile#(\d+)&gt;/', '<textile#$1>', $buffer);
        if ($sticky == 0) {
          $post .= $this->options['_blockcode_close'];
        }
        $out .= $pre . $buffer . $post;
        continue;
      } elseif ($block == 'bq') {
        if ($sticky <= 1) {
          $pre .= '<blockquote';
          if ($align) {
            $alignment = $this->_halign($align);
            if ($this->options['css_mode']) {
              if (($padleft || $padright) &&
                  (($alignment == 'left') || ($alignment == 'right'))) {
                $style .= ';float:' . $alignment;
              } else {
                $style .= ';text-align:' . $alignment;
              }
              $class .= ' ' . ($this->options['css']["class_align_$alignment"] ? $this->options['css']["class_align_$alignment"] : $alignment);
            } else {
              if ($alignment) { $pre .= " align=\"$alignment\""; }
            }
          }
          if ($padleft) { $style .= ";padding-left:${padleft}em"; }
          if ($padright) { $style .= ";padding-right:${padright}em"; }
          if ($clear) { $style .= ";clear:${clear}"; }
          if ($class) { $class = preg_replace('/^ /', '', $class, 1); }
          if ($class) { $pre .= " class=\"$class\""; }
          if ($id) { $pre .= " id=\"$id\""; }
          if ($style) { $style = preg_replace('/^;/', '', $style, 1); }
          if ($style) { $pre .= " style=\"$style\""; }
          if ($lang) { $pre .= " lang=\"$lang\""; }
          if ($cite) { $pre .= ' cite="' . $this->format_url(array('url' => $cite)) . '"'; }
          $pre .= '>';
          unset($clear);
        }
        $pre .= '<p>';
      } elseif (preg_match('/fn(\d+)/', $block, $matches)) {
        $fnum = $matches[1];
        $pre .= '<p';
        if ($this->options['css']['class_footnote']) { $class .= ' ' . $this->options['css']['class_footnote']; }
        if ($align) {
          $alignment = $this->_halign($align);
          if ($this->options['css_mode']) {
            if (($padleft || $padright) &&
                (($alignment == 'left') || ($alignment == 'right'))) {
              $style .= ';float:' . $alignment;
            } else {
              $style .= ';text-align:' . $alignment;
            }
            $class .= ($this->options['css']["class_align_$alignment"] ? $this->options['css']["class_align_$alignment"] : $alignment);
          } else {
            $pre .= " align=\"$alignment\"";
          }
        }
        if ($padleft) { $style .= ";padding-left:${padleft}em"; }
        if ($padright) { $style .= ";padding-right:${padright}em"; }
        if ($clear) { $style .= ";clear:${clear}"; }
        if ($class) { $class = preg_replace('/^ /', '', $class, 1); }
        if ($class) { $pre .= " class=\"$class\""; }
        $pre .= ' id="' . ($this->options['css']['id_footnote_prefix'] ? $this->options['css']['id_footnote_prefix'] : 'fn') . $fnum . '"';
        if ($style) { $style = preg_replace('/^;/', '', $style, 1); }
        if ($style) { $pre .= " style=\"$style\""; }
        if ($lang) { $pre .= " lang=\"$lang\""; }
        $pre .= '>';
        $pre .= '<sup>' . $fnum . '</sup> ';
        // we can close like a regular paragraph tag now
        $block = 'p';
        unset($clear);
      } else {
        $pre .= '<' . ($macros[$block] ? $macros[$block] : $block);
        if ($align) {
          $alignment = $this->_halign($align);
          if ($this->options['css_mode']) {
            if (($padleft || $padright) &&
                (($alignment == 'left') || ($alignment == 'right'))) {
              $style .= ';float:' . $alignment;
            } else {
              $style .= ';text-align:' . $alignment;
            }
            $class .= ' ' . ($this->options['css']["class_align_$alignment"] ? $this->options['css']["class_align_$alignment"] : $alignment);
          } else {
            $pre .= " align=\"$alignment\"";
          }
        }
        if ($padleft) { $style .= ";padding-left:${padleft}em"; }
        if ($padright) { $style .= ";padding-right:${padright}em"; }
        if ($clear) { $style .= ";clear:${clear}"; }
        if ($class) { $class = preg_replace('/^ /', '', $class, 1); }
        if ($class) { $pre .= " class=\"$class\""; }
        if ($id) { $pre .= " id=\"$id\""; }
        if ($style) { $style = preg_replace('/^;/', '', $style, 1); }
        if ($style) { $pre .= " style=\"$style\""; }
        if ($lang) { $pre .= " lang=\"$lang\""; }
        if ($cite && ($block == 'bq')) { $pre .= ' cite="' . $this->format_url(array('url' => $cite)) . '"'; }
        $pre .= '>';
        unset($clear);
      }

      $buffer = $this->format_paragraph(array('text' => $para));

      if ($block == 'bq') {
        if (!preg_match('/<p[ >]/', $buffer)) { $post .= '</p>'; }
        if ($sticky == 0) {
          $post .= '</blockquote>';
        }
      } else {
        $post .= '</' . $block . '>';
      }

      if (preg_match('{' . $this->blocktags . '}x', $buffer)) {
        $buffer = preg_replace('/^\n\n/s', '', $buffer, 1);
        $out .= $buffer;
      } else {
        if ($filter) { $buffer = $this->format_block(array('text' => "|$filter|" . $buffer, 'inline' => 1)); }
        $out .= $pre . $buffer . $post;
      }
    }

    if ($sticky) {
      if ($block == 'bc') {
        // close our blockcode section
        $out .= $this->options['_blockcode_close']; // . "\n\n";
      } elseif ($block == 'bq') {
        $out .= '</blockquote>'; // . "\n\n";
      } elseif (($block == 'table') && $stickybuff) {
        $table_out = $this->format_table(array('text' => $stickybuff));
        if ($table_out) { $out .= $table_out; }
      } elseif (($block == 'dl') && $stickybuff) {
        $dl_out = $this->format_deflist(array('text' => $stickybuff));
        if ($dl_out) { $out .= $dl_out; }
      }
    }

    // cleanup-- restore preserved blocks
    for ($i = count($this->repl[0]); $i > 0; $i--) {
      $out = preg_replace('!(?:<|&lt;)textile#' . $i . '(?:>|&gt;)!', str_replace('$', '\\$', $this->repl[0][$i - 1]), $out, 1);
    }
    array_shift($this->repl);

    // scan for br, hr tags that are not closed and close them
    // only for xhtml! just the common ones -- don't fret over input
    // and the like.
    if (preg_match('/^xhtml/i', $this->flavor())) {
      $out = preg_replace('/(<(?:img|br|hr)[^>]*?(?<!\/))>/', '$1 />', $out);
    }

    return $out;
  } // function process

  /**
   * Processes a single paragraph. The following attributes are
   * allowed:
   *
   * <ul>
   *
   * <li><b>text</b>
   *
   * The text to be processed.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the paragraph.
   *
   * @return A @c string containing the formatted paragraph.
   *
   * @private
   */
  function format_paragraph($args) {
    $buffer = (isset($args['text']) ? $args['text'] : '');

    array_unshift($this->repl, array());
    $buffer = preg_replace_callback('{(?:^|(?<=[\s>])|([{[]))
                                      ==(.+?)==
                                      (?:$|([\]}])|(?=' . $this->punct . '{1,2}|\s))}sx',
                                    $this->_cb('$me->_repl($me->repl[0], $me->format_block(array("text" => $m[2], "inline" => 1, "pre" => $m[1], "post" => $m[3])))'), $buffer);

    unset($tokens);
    if (preg_match('/</', $buffer) && (!$this->disable_html())) {  // optimization -- no point in tokenizing if we
                                       // have no tags to tokenize
      $tokens = $this->_tokenize($buffer);
    } else {
      $tokens = array(array('text', $buffer));
    }
    $result = '';
    foreach ($tokens as $token) {
      $text = $token[1];
      if ($token[0] == 'tag') {
        $text = preg_replace('/&(?!amp;)/', '&amp;', $text);
        $result .= $text;
      } else {
        $text = $this->format_inline(array('text' => $text));
        $result .= $text;
      }
    }

    // now, add line breaks for lines that contain plaintext
    $lines = preg_split('/\n/', $result);
    $result = '';
    $needs_closing = 0;
    foreach ($lines as $line) {
      if (!preg_match('{(' . $this->blocktags . ')}x', $line)
          && ((preg_match('/^[^<]/', $line) || preg_match('/>[^<]/', $line))
              || !preg_match('/<img /', $line))) {
        if ($this->options['_line_open']) {
          if ($result != '') { $result .= "\n"; }
          $result .= $this->options['_line_open'] . $line . $this->options['_line_close'];
        } else {
          if ($needs_closing) {
            $result .= $this->options['_line_close'] . "\n";
          } else {
            $needs_closing = 1;
            if ($result != '') { $result .= "\n"; }
          }
          $result .= $line;
        }
      } else {
        if ($needs_closing) {
          $result .= $this->options['_line_close'] . "\n";
        } else {
          if ($result != '') { $result .= "\n"; }
        }
        $result .= $line;
        $needs_closing = 0;
      }
    }

    // at this point, we will restore the \001's to \n's (reversing
    // the step taken in _tokenize).
    //$result = preg_replace('/\r/', "\n", $result);
    $result = preg_replace('/\001/', "\n", $result);

    for ($i = count($this->repl[0]); $i > 0; $i--) {
      $result = preg_replace("|<textile#$i>|", str_replace('$', '\\$', $this->repl[0][$i - 1]), $result, 1);
    }
    array_shift($this->repl);

    // quotalize
    if ($this->options['do_quotes']) {
      $result = $this->process_quotes($result);
    }

    return $result;
  } // function format_paragraph

  /**
   * Processes an inline string (plaintext) for Textile syntax.
   * The following attributes are allowed:
   *
   * <ul>
   *
   * <li><b>text</b>
   *
   * The text to be processed.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the inline string.
   *
   * @return A @c string containing the formatted inline string.
   *
   * @private
   */
  function format_inline($args) {
    $qtags = array(array('**', 'b',      '(?<!\*)\*\*(?!\*)', '\*'),
                   array('__', 'i',      '(?<!_)__(?!_)', '_'),
                   array('??', 'cite',   '\?\?(?!\?)', '\?'),
                   array('*',  'strong', '(?<!\*)\*(?!\*)', '\*'),
                   array('_',  'em',     '(?<!_)_(?!_)', '_'),
                   array('-',  'del',    '(?<!\-)\-(?!\-)', '-'),
                   array('+',  'ins',    '(?<!\+)\+(?!\+)', '\+'),
                   array('++', 'big',    '(?<!\+)\+\+(?!\+)', '\+\+'),
                   array('--', 'small',  '(?<!\-)\-\-(?!\-)', '\-\-'),
                   array('~',  'sub',    '(?<!\~)\~(?![\\\\/~])', '\~'));
    $text = (isset($args['text']) ? $args['text'] : '');

    array_unshift($this->repl, array());

    $text = preg_replace_callback('{' . $this->codere . '}mx', $this->_cb('$me->_repl($me->repl[0], $me->format_code(array("text" => $m[2] . $m[4], "lang" => $m[1] . $m[3])))'), $text);

    // images must be processed before encoding the text since they might
    // have the <, > alignment specifiers...

    // !blah (alt)! -> image
    $text = preg_replace_callback('{(?:^|(?<=[\s>])|([{[]))      # $1: open brace/bracket
                                    !                            # opening
                                    (' . $this->imgalignre . '?) # $2: optional alignment
                                    (' . $this->clstypadre . '*) # $3: optional CSS class/id
                                    (' . $this->imgalignre . '?) # $4: optional alignment
                                    (?:\s*)                      # space between alignment/css stuff
                                    ([^\s\(!]+)                  # $5: filename
                                    (\s*[^\(!]*(?:\([^\)]+\))?[^!]*) # $6: extras (alt text)
                                    !                            # closing
                                    (?::(\d+|' . $this->urlre . '))? # $7: optional URL
                                    (?:$|([\]}])|(?=' . $this->punct . '{1,2}|\s)) # $8: closing brace/bracket
                                   }mx', $this->_cb('$me->_repl($me->repl[0], $me->format_image(array("pre" => $m[1], "src" => $m[5], "align" => ($m[2] ? $m[2] : $m[4]), "extra" => $m[6], "url" => $m[7], "clsty" => $m[3], "post" => $m[8])))'), $text);

    $text = preg_replace_callback('{(?:^|(?<=[\s>])|([{[]))     # $1: open brace/bracket
                                    %                           # opening
                                    (' . $this->halignre . '?)  # $2: optional alignment
                                    (' . $this->clstyre . '*)   # $3: optional CSS class/id
                                    (' . $this->halignre . '?)  # $4: optional alignment
                                    (?:\s*)                     # spacing
                                    ([^%]+?)                    # $5: text
                                    %                           # closing
                                    (?::(\d+|' . $this->urlre . '))? # $6: optional URL
                                    (?:$|([]}])|(?=' . $this->punct . '{1,2}|\s)) # $7: closing brace/bracket
                                   }mx', $this->_cb('$me->_repl($me->repl[0], $me->format_span(array("pre" => $m[1], "text" => $m[5], "align" => ($m[2] ? $m[2] : $m[4]), "cite" => $m[6], "clsty" => $m[3], "post" => $m[7])))'), $text);

    $text = $this->encode_html($text);
    $text = preg_replace('!&lt;textile#(\d+)&gt;!', '<textile#$1>', $text);
    $text = preg_replace('!&amp;quot;!', '&#34;', $text);
    $text = preg_replace('!&amp;(([a-z]+|#\d+);)!', '&$1', $text);
    $text = preg_replace('!&quot;!', '"', $text);

    // These create markup with entities. Do first and 'save' result for later:
    // "text":url -> hyperlink
    // links with brackets surrounding
    $parenre = '\( (?: [^()] )* \)';
    $text = preg_replace_callback('{(
                                    [{[]
                                    (?:
                                        (?:"                                         # quote character
                                           (' . $this->clstyre . '*)?                # $2: optional CSS class/id
                                           ([^"]+?)                                  # $3: link text
                                           (?:\( ( (?:[^()]|' . $parenre . ')*) \))? # $4: optional link title
                                           "                                         # closing quote
                                        )
                                        |
                                        (?:\'                                        # open single quote
                                           (' . $this->clstyre . '*)?                # $5: optional CSS class/id
                                           ([^\']+?)                                 # $6: link text
                                           (?:\( ( (?:[^()]|' . $parenre . ')*) \))? # $7: optional link title
                                           \'                                        # closing quote
                                        )
                                    )
                                    :(.+?)                                           # $8: URL suffix
                                    [\]}]
                                   )
                                   }mx', $this->_cb('$me->_repl($me->repl[0], $me->format_link(array("text" => $m[1], "linktext" => $m[3] . $m[6], "title" => $me->encode_html_basic($m[4] . $m[7]), "url" => $m[8], "clsty" => $m[2] . $m[5])))'), $text);

    $text = preg_replace_callback('{((?:^|(?<=[\s>\(]))                              # $1: open brace/bracket
                                    (?: (?:"                                         # quote character "
                                           (' . $this->clstyre . '*)?                # $2: optional CSS class/id
                                           ([^"]+?)                                  # $3: link text "
                                           (?:\( ( (?:[^()]|' . $parenre . ')*) \))? # $4: optional link title
                                           "                                         # closing quote # "
                                        )
                                        |
                                        (?:\'                                        # open single quote \'
                                           (' . $this->clstyre . '*)?                # $5: optional CSS class/id
                                           ([^\']+?)                                 # $6: link text \'
                                           (?:\( ( (?:[^()]|' . $parenre . ')*) \))? # $7: optional link title
                                           \'                                        # closing quote \'
                                        )
                                    )
                                    :(\d+|' . $this->urlre . ')                      # $8: URL suffix
                                    (?:$|(?=' . $this->punct . '{1,2}|\s)))          # $9: closing brace/bracket
                                   }mx', $this->_cb('$me->_repl($me->repl[0], $me->format_link(array("text" => $m[1], "linktext" => $m[3] . $m[6], "title" => $me->encode_html_basic($m[4] . $m[7]), "url" => $m[8], "clsty" => $m[2] . $m[5])))'), $text);

    if (preg_match('/^xhtml2/', $this->flavor())) {
      // citation with cite link
      $text = preg_replace_callback('{(?:^|(?<=[\s>\'"\(])|([{[]))                   # $1: open brace/bracket \'
                                      \?\?                                           # opening \'??\'
                                      ([^\?]+?)                                      # $2: characters (can\'t contain \'?\')
                                      \?\?                                           # closing \'??\'
                                      :(\d+|' . $this->urlre . ')                    # $3: optional citation URL
                                      (?:$|([\]}])|(?=' . $this->punct . '{1,2}|\s)) # $4: closing brace/bracket
                                     }mx', $this->_cb('$me->_repl($me->repl[0], $me->format_cite(array("pre" => $m[1], "text" => $m[2], "cite" => $m[3], "post" => $m[4])))'), $text);
    }

    // footnotes
    if (preg_match('/[^ ]\[\d+\]/', $text)) {
      $fntag = '<sup';
      if ($this->options['css']['class_footnote']) { $fntag .= ' class="' . $this->options['css']['class_footnote'] . '"'; }
      $fntag .= '><a href="#' . ($this->options['css']['id_footnote_prefix'] ? $this->options['css']['id_footnote_prefix'] : 'fn');
      $text = preg_replace('|([^ ])\[(\d+)\]|', '$1' . $fntag . '$2">$2</a></sup>', $text);
    }

    // translate macros:
    $text = preg_replace_callback('{(\{)(.+?)(\})}x',
                                  $this->_cb('$me->format_macro(array("pre" => $m[1], "post" => $m[3], "macro" => $m[2]))'), $text);

    // these were present with textile 1 and are common enough
    // to not require macro braces...
    // (tm) -> &trade;
    $text = preg_replace('|[\(\[]TM[\)\]]|i', '&#8482;', $text);
    // (c) -> &copy;
    $text = preg_replace('|[\(\[]C[\)\]]|i', '&#169;', $text);
    // (r) -> &reg;
    $text = preg_replace('|[\(\[]R[\)\]]|i', '&#174;', $text);

    if ($this->preserve_spaces()) {
      // replace two spaces with an em space
      $text = preg_replace('/(?<!\s)\ \ (?!=\s)/', '&#8195;', $text);
    }

    $redo = preg_match('/[\*_\?\-\+\^\~]/', $text);
    $last = $text;
    while ($redo) {
      // simple replacements...
      $redo = 0;
      foreach ($qtags as $tag) {
        list ($this->tmp['f'][], $this->tmp['r'][], $qf, $cls) = $tag;
        if ($last != ($text = preg_replace_callback('{(?:^|(?<=[\s>\'"])|([{[]))                     # "\' $1 - pre
                                                      ' . $qf . '                                    #
                                                      (?:(' . $this->clstyre . '*))?                 # $2 - attributes
                                                      ([^' . $cls . '\s].*?)                         # $3 - content
                                                      (?<=\S)' . $qf . '                             #
                                                      (?:$|([\]}])|(?=' . $this->punct . '{1,2}|\s)) # $4 - post
                                                     }mx', $this->_cb('$me->format_tag(array("tag" => end($me->tmp["r"]), "marker" => end($me->tmp["f"]), "pre" => $m[1], "text" => $m[3], "clsty" => $m[2], "post" => $m[4]))'), $text))) {
          $redo = ($redo || ($last != $text));
          $last = $text;
        }
        array_pop($this->tmp['f']); array_pop($this->tmp['r']);
      }
    }

    // superscript is an even simpler replacement...
    $text = preg_replace('/(?<!\^)\^(?!\^)(.+?)(?<!\^)\^(?!\^)/', '<sup>$1</sup>', $text);

    // ABC(Aye Bee Cee) -> acronym
    $text = preg_replace_callback('{\b([A-Z][A-Za-z0-9]*?[A-Z0-9]+?)\b(?:[(]([^)]*)[)])}',
                                  $this->_cb('$me->_repl($me->repl[0],"<acronym title=\"" . $me->encode_html_basic($m[2]) . "\">$m[1]</acronym>")'), $text);

    // ABC -> 'capped' span
    if ($this->tmp['caps'][] = $this->options['css']['class_caps']) {
      $text = preg_replace_callback('/(^|[^"][>\s])  # "
                                      ((?:[A-Z](?:[A-Z0-9\.,\']|\&amp;){2,}\ *)+?) # \'
                                      (?=[^A-Z\.0-9]|$)
                                     /mx', $this->_cb('$m[1] . $me->_repl($me->repl[0], "<span class=\"" . end($me->tmp["caps"]) . "\">$m[2]</span>")'), $text);
    }
    array_pop($this->tmp['caps']);

    // nxn -> n&times;n
    $text = preg_replace('!((?:[0-9\.]0|[1-9]|\d[\'"])\ ?)x(\ ?\d)!', '$1&#215;$2', $text);

    // translate these entities to the Unicode equivalents:
    $text = preg_replace('/&#133;/', '&#8230;', $text);
    $text = preg_replace('/&#145;/', '&#8216;', $text);
    $text = preg_replace('/&#146;/', '&#8217;', $text);
    $text = preg_replace('/&#147;/', '&#8220;', $text);
    $text = preg_replace('/&#148;/', '&#8221;', $text);
    $text = preg_replace('/&#150;/', '&#8211;', $text);
    $text = preg_replace('/&#151;/', '&#8212;', $text);

    // Restore replacements done earlier:
    for ($i = count($this->repl[0]); $i > 0; $i--) {
      $text = preg_replace("|<textile#$i>|", str_replace('$', '\\$', $this->repl[0][$i - 1]), $text);
    }
    array_shift($this->repl);

    // translate entities to characters for highbit stuff since
    // we're using utf8
    // removed for backward compatability with older versions of Perl
    //if (preg_match('/^utf-?8$/i', $this->options['charset'])) {
    //    // translate any unicode entities to native UTF-8
    //    $text = preg_replace('/\&\#(\d+);/e', '($1 > 127) ? pack('U', $1) : chr($1)', $text);
    //}

    return $text;
  } // function format_inline

  /**
   * Responsible for processing a particular macro. Arguments passed
   * include:
   *
   * <ul>
   *
   * <li><b>pre</b>
   *
   * open brace character</li>
   *
   * <li><b>post</b>
   *
   * close brace character</li>
   *
   * <li><b>macro</b>
   *
   * the macro to be executed</li>
   *
   * </ul>
   *
   * The return value from this method would be the replacement
   * text for the macro given. If the macro is not defined, it will
   * return pre + macro + post, thereby preserving the original
   * macro string.
   *
   * @param $attrs An @c array containing the attributes for
   *        formatting the macro.
   *
   * @return A @c string containing the formatted macro.
   *
   * @private
   */
  function format_macro($attrs) {
    $macro = $attrs['macro'];
    if ($this->options['macros'][$macro]) {
      return $this->options['macros'][$macro];
    }

    return $attrs['pre'] . $macro . $attrs['post'];
  } // function format_macro

  /**
   * Processes text for a citation tag. The following attributes
   * are allowed:
   *
   * <ul>
   *
   * <li><b>pre</b>
   *
   * Any text that comes before the citation.</li>
   *
   * <li><b>text</b>
   *
   * The text that is being cited.</li>
   *
   * <li><b>cite</b>
   *
   * The URL of the citation.</li>
   *
   * <li><b>post</b>
   *
   * Any text that follows the citation.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the citation.
   *
   * @return A @c string containing the formatted citation.
   *
   * @private
   */
  function format_cite($args) {
    $pre = (isset($args['pre']) ? $args['pre'] : '');
    $text = (isset($args['text']) ? $args['text'] : '');
    $cite = $args['cite'];
    $post = (isset($args['post']) ? $args['post'] : '');
    $this->_strip_borders($pre, $post);
    $tag = $pre . '<cite';
    if (preg_match('/^xhtml2/', $this->flavor()) && $cite) {
      $cite = $this->format_url(array('url' => $cite));
      $tag .= " cite=\"$cite\"";
    } else {
      $post .= ':';
    }
    $tag .= '>';
    return $tag . $this->format_inline(array('text' => $text)) . '</cite>' . $post;
  } // function format_cite

  /**
   * Processes '@...@' type blocks (code snippets). The following
   * attributes are allowed:
   *
   * <ul>
   *
   * <li><b>text</b>
   *
   * The text of the code itself.</li>
   *
   * <li><b>lang</b>
   *
   * The language (programming language) for the code.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the code.
   *
   * @return A @c string containing the formatted code.
   *
   * @private
   */
  function format_code($args) {
    $code = (isset($args['text']) ? $args['text'] : '');
    $lang = $args['lang'];
    $code = $this->encode_html($code, 1);
    $code = preg_replace('/&lt;textile#(\d+)&gt;/', '<textile#$1>', $code);
    $tag = '<code';
    if ($lang) { $tag .= " language=\"$lang\""; }
    return $tag . '>' . $code . '</code>';
  } // function format_code

  /**
   * Returns a string of tag attributes to accomodate the class,
   * style and symbols present in @c $clsty.
   *
   * @c $clsty is checked for:
   *
   * <ul>
   *
   * <li><b><code>{...}</code></b>
   *
   * style rules. If present, they are appended to <code>$style</code>.</li>
   *
   * <li><b><code>(...#...)</code></b>
   *
   * class and/or ID name declaration</li>
   *
   * <li><b><code>(</code> (one or more)</b>
   *
   * pad left characters</li>
   *
   * <li><b><code>)</code> (one or more)</b>
   *
   * pad right characters</li>
   *
   * <li><b><code>[ll]</code></b>
   *
   * language declaration</li>
   *
   * </ul>
   *
   * The attribute string returned will contain any combination
   * of class, id, style and/or lang attributes.
   *
   * @param $clsty A @c string specifying the class/style to process.
   * @param $class A @c string specifying the predetermined class.
   * @param $style A @c string specifying the predetermined style.
   *
   * @return A @c string containing the formatted class, ID, style,
   *         and/or language.
   *
   * @private
   */
  function format_classstyle($clsty = NULL, $class = NULL, $style = NULL) {
    $class = preg_replace('/^ /', '', $class, 1);

    unset($lang, $padleft, $padright, $id);
    if ($clsty && preg_match('/{([^}]+)}/', $clsty, $matches)) {
      $_style = $matches[1];
      $_style = preg_replace('/\n/', ' ', $_style);
      $style .= ';' . $_style;
      $clsty = preg_replace('/{[^}]+}/', '', $clsty);
    }
    if ($clsty && (preg_match('/\(([A-Za-z0-9_\- ]+?)(?:#(.+?))?\)/', $clsty, $matches) ||
                   preg_match('/\(([A-Za-z0-9_\- ]+?)?(?:#(.+?))\)/', $clsty, $matches))) {
      if ($matches[1] || $matches[2]) {
        if ($class) {
          $class = $matches[1] . ' ' . $class;
        } else {
          $class = $matches[1];
        }
        $id = $matches[2];
        if ($class) {
          $clsty = preg_replace('/\([A-Za-z0-9_\- ]+?(#.*?)?\)/', '', $clsty);
        }
        if ($id) {
          $clsty = preg_replace('/\(#.+?\)/', '', $clsty);
        }
      }
    }
    if ($clsty && preg_match('/(\(+)/', $clsty, $matches)) {
      $padleft = strlen($matches[1]);
      $clsty = preg_replace('/\(+/', '', $clsty, 1);
    }
    if ($clsty && preg_match('/(\)+)/', $clsty, $matches)) {
      $padright = strlen($matches[1]);
      $clsty = preg_replace('/\)+/', '', $clsty, 1);
    }
    if ($clsty && preg_match('/\[(.+?)\]/', $clsty, $matches)) {
      $lang = $matches[1];
      $clsty = preg_replace('/\[.+?\]/', '', $clsty);
    }
    $attrs = '';
    if ($padleft) { $style .= ";padding-left:${padleft}em"; }
    if ($padright) { $style .= ";padding-right:${padright}em"; }
    $style = preg_replace('/^;/', '', $style, 1);
    $class = preg_replace('/^ /', '', $class, 1);
    $class = preg_replace('/ $/', '', $class, 1);
    if ($class) { $attrs .= " class=\"$class\""; }
    if ($id) { $attrs .= " id=\"$id\""; }
    if ($style) { $attrs .= " style=\"$style\""; }
    if ($lang) { $attrs .= " lang=\"$lang\""; }
    $attrs = preg_replace('/^ /', '', $attrs, 1);
    return $attrs;
  } // function format_classstyle

  /**
   * Constructs an HTML tag. Accepted arguments:
   *
   * <ul>
   *
   * <li><b>tag</b>
   *
   * the tag to produce</li>
   *
   * <li><b>text</b>
   *
   * the text to output inside the tag</li>
   *
   * <li><b>pre</b>
   *
   * text to produce before the tag</li>
   *
   * <li><b>post</b>
   *
   * text to produce following the tag</li>
   *
   * <li><b>clsty</b>
   *
   * class and/or style attributes that should be assigned to the tag.</li>
   *
   * </ul>
   *
   * @param $args @c array specifying the attributes for formatting
   *        the tag.
   *
   * @return A @c string containing the formatted tag.
   *
   * @private
   */
  function format_tag($args) {
    $tagname = $args['tag'];
    $text = (isset($args['text']) ? $args['text'] : '');
    $pre = (isset($args['pre']) ? $args['pre'] : '');
    $post = (isset($args['post']) ? $args['post'] : '');
    $clsty = (isset($args['clsty']) ? $args['clsty'] : '');
    $this->_strip_borders($pre, $post);
    $tag = "<$tagname";
    $attr = $this->format_classstyle($clsty);
    if ($attr) { $tag .= " $attr"; }
    $tag .= ">$text</$tagname>";
    return $pre . $tag . $post;
  } // function format_tag

  /**
   * Takes a Textile formatted definition list and
   * returns the markup for it. Arguments accepted:
   *
   * <ul>
   *
   * <li><b>text</b>
   *
   * The text to be processed.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the definition list.
   *
   * @return A @c string containing the formatted definition list.
   *
   * @private
   */
  function format_deflist($args) {
    $str = (isset($args['text']) ? $args['text'] : '');
    unset($clsty);
    $lines = preg_split('/\n/', $str);
    if (preg_match('{^(dl(' . $this->clstyre . '*?)\.\.?(?:\ +|$))}x', $lines[0], $matches)) {
      $clsty = $matches[2];
      $lines[0] = substr($lines[0], strlen($matches[1]));
    }

    unset($dt, $dd);
    $out = '';
    foreach ($lines as $line) {
      if (preg_match('{^((?:' . $this->clstyre . '*)(?:[^\ ].*?)(?<!["\'\ ])):([^\ \/].*)$}x', $line, $matches)) {
        if ($dt && $dd) { $out .= $this->add_term($dt, $dd); }
        $dt = $matches[1];
        $dd = $matches[2];
      } else {
        $dd .= "\n" . $line;
      }
    }
    if ($dt && $dd) { $out .= $this->add_term($dt, $dd); }

    $tag = '<dl';
    if ($clsty) { $attr = $this->format_classstyle($clsty); }
    if ($attr) { $tag .= " $attr"; }
    $tag .= '>' . "\n";

    return $tag . $out . "</dl>\n";
  } // function format_deflist

  /**
   * Processes a single definition list item from the provided term
   * and definition.
   *
   * @param $dt A @c string specifying the term to be defined.
   * @param $dd A @c string specifying the definition for the term.
   *
   * @return A @c string containing the formatted definition list
   *         item.
   *
   * @private
   */
  function add_term($dt, $dd) {
    unset($dtattr, $ddattr);
    unset($dtlang);
    if (preg_match('{^(' . $this->clstyre . '*)}x', $dt, $matches)) {
      $param = $matches[1];
      $dtattr = $this->format_classstyle($param);
      if (preg_match('/\[([A-Za-z]+?)\]/', $param, $matches)) {
        $dtlang = $matches[1];
      }
      $dt = substr($dt, strlen($param));
    }
    if (preg_match('{^(' . $this->clstyre . '*)}x', $dd, $matches)) {
      $param = $matches[1];
      // if the language was specified for the term,
      // then apply it to the definition as well (unless
      // already specified of course)
      if ($dtlang && preg_match('/\[([A-Za-z]+?)\]/', $param)) {
        unset($dtlang);
      }
      $ddattr = $this->format_classstyle(($dtlang ? "[$dtlang]" : '') . $param);
      $dd = substr($dd, strlen($param));
    }
    $out = '<dt';
    if ($dtattr) { $out .= " $dtattr"; }
    $out .= '>' . $this->format_inline(array('text' => $dt)) . '</dt>' . "\n";
    if (preg_match('/\n\n/', $dd)) {
      if (preg_match('/\n\n/', $dd)) { $dd = $this->process($dd); }
    } else {
      $dd = $this->format_paragraph(array('text' => $dd));
    }
    $out .= '<dd';
    if ($ddattr) { $out .= " $ddattr"; }
    $out .= '>' . $dd . '</dd>' . "\n";
    return $out;
  } // function add_term

  /**
   * Takes a Textile formatted list (numeric or bulleted) and
   * returns the markup for it. Text that is passed in requires
   * substantial parsing, so the @c format_list method is a little
   * involved. But it should always produce a proper ordered
   * or unordered list. If it cannot (due to misbalanced input),
   * it will return the original text. Arguments accepted:
   *
   * <ul>
   *
   * <li><b>text</b>
   *
   * The text to be processed.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the list.
   *
   * @return A @c string containing the formatted list.
   *
   * @private
   */
  function format_list($args) {
    $str = (isset($args['text']) ? $args['text'] : '');

    $list_tags = array('*' => 'ul', '#' => 'ol');

    $lines = preg_split('/\n/', $str);

    unset($stack);
    $last_depth = 0;
    $item = '';
    $out = '';
    foreach ($lines as $line) {
      if (preg_match('{^((?:' . $this->clstypadre . '*|' . $this->halignre . ')*)
                       ([\#\*]+)
                       ((?:' . $this->halignre . '|' . $this->clstypadre . '*)*)
                       \ (.+)$}x', $line, $matches)) {
        if ($item != '') {
          if (preg_match('/\n/', $item)) {
            if ($this->options['_line_open']) {
              $item = preg_replace('/(<li[^>]*>|^)/m', '$1' . $this->options['_line_open'], $item);
              $item = preg_replace('/(\n|$)/s', $this->options['_line_close'] . '$1', $item);
            } else {
              $item = preg_replace('/(\n)/s', $this->options['_line_close'] . '$1', $item);
            }
          }
          $out .= $item;
          $item = '';
        }
        $type = substr($matches[2], 0, 1);
        $depth = strlen($matches[2]);
        $blockparam = $matches[1];
        $itemparam = $matches[3];
        $line = $matches[4];
        unset ($blockclsty, $blockalign, $blockattr, $itemattr, $itemclsty,
               $itemalign);
        if (preg_match('{(' . $this->clstypadre . '+)}x', $blockparam, $matches)) {
          $blockclsty = $matches[1];
        }
        if (preg_match('{(' . $this->halignre . '+)}', $blockparam, $matches)) {
          $blockalign = $matches[1];
        }
        if (preg_match('{(' . $this->clstypadre . '+)}x', $itemparam, $matches)) {
          $itemclsty = $matches[1];
        }
        if (preg_match('{(' . $this->halignre . '+)}', $itemparam, $matches)) {
          $itemalign = $matches[1];
        }
        if ($itemclsty) { $itemattr = $this->format_classstyle($itemclsty); }
        if ($depth > $last_depth) {
          for ($j = $last_depth; $j < $depth; $j++) {
            $out .= "\n<$list_tags[$type]";
            $stack[] = $type;
            if ($blockclsty) {
              $blockattr = $this->format_classstyle($blockclsty);
              if ($blockattr) { $out .= ' ' . $blockattr; }
            }
            $out .= ">\n<li";
            if ($itemattr) { $out .= " $itemattr"; }
            $out .= ">";
          }
        } elseif ($depth < $last_depth) {
          for ($j = $depth; $j < $last_depth; $j++) {
            if ($j == $depth) { $out .= "</li>\n"; }
            $type = array_pop($stack);
            $out .= "</$list_tags[$type]>\n</li>\n";
          }
          if ($depth) {
            $out .= '<li';
            if ($itemattr) { $out .= " $itemattr"; }
            $out .= '>';
          }
        } else {
          $out .= "</li>\n<li";
          if ($itemattr) { $out .= " $itemattr"; }
          $out .= '>';
        }
        $last_depth = $depth;
      }
      if ($item != '') { $item .= "\n"; }
      $item .= $this->format_paragraph(array('text' => $line));
    }

    if (preg_match('/\n/', $item, $matches)) {
      if ($this->options['_line_open']) {
        $item = preg_replace('/(<li[^>]*>|^)/m', '$1' . $this->options['_line_open'], $item);
        $item = preg_replace('/(\n|$)/s', $this->options['_line_close'] . '$1', $item);
      } else {
        $item = preg_replace('/(\n)/s', $this->options['_line_close'] . '$1', $item);
      }
    }
    $out .= $item;

    for ($j = 1; $j <= $last_depth; $j++) {
      if ($j == 1) { $out .= '</li>'; }
      $type = array_pop($stack);
      $out .= "\n" . '</' . $list_tags[$type] . '>' . "\n";
      if ($j != $last_depth) { $out .= '</li>'; }
    }

    return $out . "\n";
  } // function format_list

  /**
   * Processes '==xxxxx==' type blocks for filters. A filter
   * would follow the open '==' sequence and is specified within
   * pipe characters, like so:
   * <pre>
   *     ==|filter|text to be filtered==
   * </pre>
   * You may specify multiple filters in the filter portion of
   * the string. Simply comma delimit the filters you desire
   * to execute. Filters are defined using the filters method.
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the block.
   *
   * @return A @c string containing the formatted block.
   *
   * @private
   */
  function format_block($args) {
    $str = (isset($args['text']) ? $args['text'] : '');
    $inline = $args['inline'];
    $pre = (isset($args['pre']) ? $args['pre'] : '');
    $post = (isset($args['post']) ? $args['post'] : '');
    $this->_strip_borders($pre, $post);
    $filters = (preg_match('/^(\|(?:(?:[a-z0-9_\-]+)\|)+)/', $str, $matches) ? $matches[1] : '');
    if ($filters) {
      $filtreg = preg_replace('/[^A-Za-z0-9]/', '\\\\$1', $filters);
      $str = preg_replace('/^' . $filtreg . '/', '', $str, 1);
      $filters = preg_replace('/^\|/', '', $filters, 1);
      $filters = preg_replace('/\|$/', '', $filter, 1);
      $filters = preg_split('/\|/', $filters);
      $str = $this->apply_filters(array('text' => $str, 'filters' => $filters));
      $count = count($filters);
      if ($str = preg_replace('!(<p>){' . $count . '}!se', '(++$i ? "$1" : "$1")', $str) && $i) {
        $str = preg_replace('!(</p>){' . $count . '}!s', '$1', $str);
        $str = preg_replace('!(<br( /)?>){' . $count . '}!s', '$1', $str);
      }
    }
    if ($inline) {
      // strip off opening para, closing para, since we're
      // operating within an inline block
      $str = preg_replace('/^\s*<p[^>]*>/', '', $str, 1);
      $str = preg_replace('/<\/p>\s*$/', '', $str, 1);
    }
    return $pre . $str . $post;
  } // function format_block

  /**
   * Takes the Textile link attributes and transforms them into
   * a hyperlink.
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the link.
   *
   * @return A @c string containing the formatted link.
   *
   * @private
   */
  function format_link($args) {
    $text = (isset($args['text']) ? $args['text'] : '');
    $linktext = (isset($args['linktext']) ? $args['linktext'] : '');
    $title = $args['title'];
    $url = $args['url'];
    $clsty = $args['clsty'];

    if (!$url || ($url == '')) {
      return $text;
    }
    if (isset($this->links) && isset($this->links[$url])) {
      $title = ($title ? $title : $this->links[$url]['title']);
      $url = $this->links[$url]['url'];
    }
    $linktext = preg_replace('/ +$/', '', $linktext, 1);
    $linktext = $this->format_paragraph(array('text' => $linktext));
    $url = $this->format_url(array('linktext' => $linktext, 'url' => $url));
    $tag = "<a href=\"$url\"";
    $attr = $this->format_classstyle($clsty);
    if ($attr) { $tag .= " $attr"; }
    if ($title) {
      $title = preg_replace('/^\s+/', '', $title, 1);
      if (strlen($title)) { $tag .= " title=\"$title\""; }
    }
    $tag .= ">$linktext</a>";
    return $tag;
  } // function format_link

  /**
   * Takes the given @c $url and transforms it appropriately.
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the url.
   *
   * @return A @c string containing the formatted url.
   *
   * @private
   */
  function format_url($args) {
    $url = ($args['url'] ? $args['url'] : '');
    if (preg_match('/^(mailto:)?([-\+\w]+@[-\w]+(\.\w[-\w]*)+)$/', $url, $matches)) {
      $url = 'mailto:' . $this->mail_encode($matches[2]);
    }
    if (!preg_match('!^(/|\./|\.\./|#)!', $url)) {
      if (!preg_match('!^(https?|ftp|mailto|nntp|telnet)!', $url)) { $url = "http://$url"; }
    }
    $url = preg_replace('/&(?!amp;)/', '&amp;', $url);
    $url = preg_replace('/\ /', '+', $url);
    $url = preg_replace_callback('/^((?:.+?)\?)(.+)$/', $this->_cb('$m[1] . $me->encode_url($m[2])'), $url);
    return $url;
  } // function format_url

  /**
   * Takes a Textile formatted span and returns the markup for it.
   *
   * @return A @c string containing the formatted span.
   *
   * @private
   */
  function format_span($args) {
    $text = (isset($args['text']) ? $args['text'] : '');
    $pre = (isset($args['pre']) ? $args['pre'] : '');
    $post = (isset($args['post']) ? $args['post'] : '');
    $align = $args['align'];
    $cite = (isset($args['cite']) ? $args['cite'] : '');
    $clsty = $args['clsty'];
    $this->_strip_borders($pre, $post);
    unset($class, $style);
    $tag  = "<span";
    $style = '';
    if ($align) {
      if ($self->options['css_mode']) {
        $alignment = $this->_halign($align);
        if ($alignment) { $style .= ";float:$alignment"; }
        if ($alignment) { $class .= ' ' . $this->options['css']["class_align_$alignment"]; }
      } else {
        $alignment = ($this->_halign($align) ? $this->_halign($align) : $this->_valign($align));
        if ($alignment) { $tag .= " align=\"$alignment\""; }
      }
    }
    $attr = $this->format_classstyle($clsty, $class, $style);
    if ($attr) { $tag .= " $attr"; }
    if ($cite) {
      $cite = preg_replace('/^:/', '', $cite, 1);
      $cite = $this->format_url(array('url' => $cite));
      $tag .= " cite=\"$cite\"";
    }
    return $pre . $tag . '>' . $this->format_paragraph(array('text' => $text)) . '</span>' . $post;
  } // function format_span

  /**
   * Returns markup for the given image. @c $src is the location of
   * the image, @c $extra contains the optional height/width and/or
   * alt text. @c $url is an optional hyperlink for the image. @c $class
   * holds the optional CSS class attribute.
   *
   * Arguments you may pass:
   *
   * <ul>
   *
   * <li><b>src</b>
   *
   * The 'src' (URL) for the image. This may be a local path,
   * ideally starting with a '/'. Images can be located within
   * the file system if the docroot method is used to specify
   * where the docroot resides. If the image can be found, the
   * image_size method is used to determine the dimensions of
   * the image.</li>
   *
   * <li><b>extra</b>
   *
   * Additional parameters for the image. This would include
   * alt text, height/width specification or scaling instructions.</li>
   *
   * <li><b>align</b>
   *
   * Alignment attribute.</li>
   *
   * <li><b>pre</b>
   *
   * Text to produce prior to the tag.</li>
   *
   * <li><b>post</b>
   *
   * Text to produce following the tag.</li>
   *
   * <li><b>link</b>
   *
   * Optional URL to connect with the image tag.</li>
   *
   * <li><b>clsty</b>
   *
   * Class and/or style attributes.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the image.
   *
   * @return A @c string containing the formatted image.
   *
   * @private
   */
  function format_image($args) {
    $src = (isset($args['src']) ? $args['src'] : '');
    $extra = $args['extra'];
    $align = $args['align'];
    $pre = (isset($args['pre']) ? $args['pre'] : '');
    $post = (isset($args['post']) ? $args['post'] : '');
    $link = $args['url'];
    $clsty = $args['clsty'];
    $this->_strip_borders($pre, $post);
    if (strlen($src) == 0) { return $pre . '!!' . $post; }
    unset($tag);
    if (preg_match('/^xhtml2/', $this->options['flavor'])) {
      unset($type); // poor man's mime typing. need to extend this externally
      if (preg_match('/(?:\.jpeg|\.jpg)$/i', $src)) {
        $type = 'image/jpeg';
      } elseif (preg_match('/\.gif$/i', $src)) {
        $type = 'image/gif';
      } elseif (preg_match('/\.png$/i', $src)) {
        $type = 'image/png';
      } elseif (preg_match('/\.tiff$/i', $src)) {
        $type = 'image/tiff';
      }
      $tag = "<object";
      if ($type) { $tag .= " type=\"$type\""; }
      $tag .= " data=\"$src\"";
    } else {
      $tag = "<img src=\"$src\"";
    }
    unset($class, $style);
    if ($align) {
      if ($this->options['css_mode']) {
        $alignment = $this->_halign($align);
        if ($alignment) { $style .= ";float:$alignment"; }
        if ($alignment) { $class .= ' ' . $alignment; }
        $alignment = $this->_valign($align);
        if ($alignment) {
          $imgvalign = (preg_match('/(top|bottom)/', $alignment) ? 'text-' . $alignment : $alignment);
          if ($imgvalign) { $style .= ";vertical-align:$imgvalign"; }
          if ($alignment) { $class .= ' ' . $this->options['css']["class_align_$alignment"]; }
        }
      } else {
        $alignment = ($this->_halign($align) ? $this->_halign($align) : $this->_valign($align));
        if ($alignment) { $tag .= " align=\"$alignment\""; }
      }
    }
    unset($pctw, $pcth, $w, $h, $alt);
    if ($extra) {
      $alt = (preg_match('/\(([^\)]+)\)/', $extra, $matches) ? $matches[1] : '');
      $extra = preg_replace('/\([^\)]+\)/', '', $extra, 1);
      $pct = (preg_match('/(^|\s)(\d+)%(\s|$)/', $extra, $matches) ? $matches[2] : '');
      if (!$pct) {
        list($pctw, $pcth) = (preg_match('/(^|\s)(\d+)%x(\d+)%(\s|$)/', $extra, $matches) ? array($matches[2], $matches[3]) : NULL);
      } else {
        $pctw = $pcth = $pct;
      }
      if (!$pctw && !$pcth) {
        list($w,$h) = (preg_match('/(^|\s)(\d+|\*)x(\d+|\*)(\s|$)/', $extra, $matches) ? array($matches[2], $matches[3]) : NULL);
        if ($w == '*') { $w = ''; }
        if ($h == '*') { $h = ''; }
        if (!$w) {
          $w = (preg_match('/(^|[,\s])(\d+)w([\s,]|$)/', $extra, $matches) ? $matches[2] : '');
        }
        if (!$h) {
          $h = (preg_match('/(^|[,\s])(\d+)h([\s,]|$)/', $extra, $matches) ? $matches[2] : '');
        }
      }
    }
    $alt = ($alt ? $alt : '');
    if (!preg_match('/^xhtml2/', $this->options['flavor'])) {
      $tag .= ' alt="' . $this->encode_html_basic($alt) . '"';
    }
    if ($w && $h) {
      if (!preg_match('/^xhtml2/', $this->options['flavor'])) {
        $tag .= " height=\"$h\" width=\"$w\"";
      } else {
        $style .= ";height:${h}px;width:${w}px";
      }
    } else {
      list($image_w, $image_h) = $this->image_size($src);
      if (($image_w && $image_h) && ($w || $h)) {
        // image size determined, but only width or height specified
        if ($w && !$h) {
          // width defined, scale down height proportionately
          $h = intval($image_h * ($w / $image_w));
        } elseif ($h && !$w) {
          $w = intval($image_w * ($h / $image_h));
        }
      } else {
        $w = $image_w;
        $h = $image_h;
      }
      if ($w && $h) {
        if ($pctw || $pcth) {
          $w = intval($w * $pctw / 100);
          $h = intval($h * $pcth / 100);
        }
        if (!preg_match('/^xhtml2/', $this->options['flavor'])) {
          $tag .= " height=\"$h\" width=\"$w\"";
        } else {
          $style .= ";height:{$h}px;width:{$w}px";
        }
      }
    }
    $attr = $this->format_classstyle($clsty, $class, $style);
    if ($attr) { $tag .= " $attr"; }
    if (preg_match('/^xhtml2/', $this->options['flavor'])) {
      $tag .= '><p>' . $this->encode_html_basic($alt) . '</p></object>';
    } elseif (preg_match('/^xhtml/', $this->options['flavor'])) {
      $tag .= ' />';
    } else {
      $tag .= '>';
    }
    if ($link) {
      $link = preg_replace('/^:/', '', $link, 1);
      $link = $this->format_url(array('url' => $link));
      $tag = '<a href="' . $link . '">' . $tag . '</a>';
    }
    return $pre . $tag . $post;
  } // function format_image

  /**
   * Takes a Wiki-ish string of data and transforms it into a full
   * table.
   *
   * @param $args An @c array specifying the attributes for formatting
   *        the table.
   *
   * @return A @c string containing the formatted table.
   *
   * @private
   */
  function format_table($args) {
    $str = (isset($args['text']) ? $args['text'] : '');

    $lines = preg_split('/\n/', $str);
    unset($rows);
    $line_count = count($lines);
    for ($i = 0; $i < $line_count; $i++) {
      if (!preg_match('/\|\s*$/', $lines[$i])) {
        if ($i + 1 < $line_count) {
          if ($i + 1 <= count($lines) - 1) { $lines[$i + 1] = $lines[$i] . "\n" . $lines[$i + 1]; }
        } else {
          $rows[] = $lines[$i];
        }
      } else {
        $rows[] = $lines[$i];
      }
    }
    unset($tid, $tpadl, $tpadr, $tlang);
    $tclass = '';
    $tstyle = '';
    $talign = '';
    if (preg_match('/^table[^\.]/', $rows[0])) {
      $row = $rows[0];
      $row = preg_replace('/^table/', '', $row, 1);
      $params = 1;
      // process row parameters until none are left
      while ($params) {
        if (preg_match('{^(' . $this->tblalignre . ')}', $row, $matches)) {
          // found row alignment
          $talign .= $matches[1];
          if ($matches[1]) { $row = substr($row, strlen($matches[1])); }
          if ($matches[1]) { continue; }
        }
        if (preg_match('{^(' . $this->clstypadre . ')}x', $row, $matches)) {
          // found a class/id/style/padding indicator
          $clsty = $matches[1];
          if ($clsty) { $row = substr($row, strlen($clsty)); }
          if (preg_match('/{([^}]+)}/', $clsty, $matches)) {
            $tstyle = $matches[1];
            $clsty = preg_replace('/{([^}]+)}/', '', $clsty, 1);
            if ($tstyle) { continue; }
          }
          if (preg_match('/\(([A-Za-z0-9_\- ]+?)(?:#(.+?))?\)/', $clsty, $matches) ||
              preg_match('/\(([A-Za-z0-9_\- ]+?)?(?:#(.+?))\)/', $clsty, $matches)) {
            if ($matches[1] || $matches[2]) {
              $tclass = $matches[1];
              $tid = $matches[2];
              continue;
            }
          }
          if (preg_match('/(\(+)/', $clsty, $matches)) { $tpadl = strlen($matches[1]); }
          if (preg_match('/(\)+)/', $clsty, $matches)) { $tpadr = strlen($matches[1]); }
          if (preg_match('/\[(.+?)\]/', $clsty, $matches)) { $tlang = $matches[1]; }
          if ($clsty) { continue; }
        }
        $params = 0;
      }
      $row = preg_replace('/\.\s+/', '', $row, 1);
      $rows[0] = $row;
    }
    $out = '';
    $cols = preg_split('/\|/', $rows[0] . ' ');
    unset($colaligns, $rowspans);
    foreach ($rows as $row) {
      $cols = preg_split('/\|/', $row . ' ');
      $colcount = count($cols) - 1;
      array_pop($cols);
      $colspan = 0;
      $row_out = '';
      unset($rowclass, $rowid, $rowalign, $rowstyle, $rowheader);
      if (!$cols[0]) { $cols[0] = ''; }
      if (preg_match('/_/', $cols[0])) {
        $cols[0] = preg_replace('/_/', '', $cols[0]);
        $rowheader = 1;
      }
      if (preg_match('/{([^}]+)}/', $cols[0], $matches)) {
        $rowstyle = $matches[1];
        $cols[0] = preg_replace('/{[^}]+}/', '', $cols[0]);
      }
      if (preg_match('/\(([^\#]+?)?(#(.+))?\)/', $cols[0], $matches)) {
        $rowclass = $matches[1];
        $rowid = $matches[3];
        $cols[0] = preg_replace('/\([^\)]+\)/', '', $cols[0]);
      }
      if (preg_match('{(' . $this->alignre . ')}', $cols[0], $matches)) { $rowalign = $matches[1]; }
      for ($c = $colcount - 1; $c > 0; $c--) {
        if ($rowspans[$c]) {
          $rowspans[$c]--;
          if ($rowspans[$c] > 1) { continue; }
        }
        unset($colclass, $colid, $header, $colparams, $colpadl, $colpadr, $collang);
        $colstyle = '';
        $colalign = $colaligns[$c];
        $col = array_pop($cols);
        $col = ($col ? $col : '');
        $attrs = '';
        if (preg_match('{^(((_|[/\\\\]\d+|' . $this->alignre . '|' . $this->clstypadre . ')+)\.\ )}x', $col, $matches)) {
          $colparams = $matches[2];
          $col = substr($col, strlen($matches[1]));
          $params = 1;
          // keep processing column parameters until there
          // are none left...
          while ($params) {
            if (preg_match('{^(_|' . $this->alignre . ')(.*)$}', $colparams, $matches)) {
              // found alignment or heading indicator
              $attrs .= $matches[1];
              if ($matches[1]) { $colparams = $matches[2]; }
              if ($matches[1]) { continue; }
            }
            if (preg_match('{^(' . $this->clstypadre . ')(.*)$}x', $colparams, $matches)) {
              // found a class/id/style/padding marker
              $clsty = $matches[1];
              if ($clsty) { $colparams = $matches[2]; }
              if (preg_match('/{([^}]+)}/', $clsty, $matches)) {
                $colstyle = $matches[1];
                $clsty = preg_replace('/{([^}]+)}/', '', $clsty, 1);
              }
              if (preg_match('/\(([A-Za-z0-9_\- ]+?)(?:#(.+?))?\)/', $clsty, $matches) ||
                  preg_match('/\(([A-Za-z0-9_\- ]+?)?(?:#(.+?))\)/', $clsty, $matches)) {
                if ($matches[1] || $matches[2]) {
                  $colclass = $matches[1];
                  $colid = $matches[2];
                  if ($colclass) {
                    $clsty = preg_replace('/\([A-Za-z0-9_\- ]+?(#.*?)?\)/', '', $clsty);
                  } elseif ($colid) {
                    $clsty = preg_replace('/\(#.+?\)/', '', $clsty);
                  }
                }
              }
              if (preg_match('/(\(+)/', $clsty, $matches)) {
                $colpadl = strlen($matches[1]);
                $clsty = preg_replace('/\(+/', '', $clsty, 1);
              }
              if (preg_match('/(\)+)/', $clsty, $matches)) {
                $colpadr = strlen($matches[1]);
                $clsty = preg_replace('/\)+/', '', $clsty, 1);
              }
              if (preg_match('/\[(.+?)\]/', $clsty, $matches)) {
                $collang = $matches[1];
                $clsty = preg_replace('/\[.+?\]/', '', $clsty, 1);
              }
              if ($clsty) { continue; }
            }
            if (preg_match('/^\\\\(\d+)/', $colparams, $matches)) {
              $colspan = $matches[1];
              $colparams = substr($colparams, strlen($matches[1]) + 1);
              if ($matches[1]) { continue; }
            }
            if (preg_match('/\/(\d+)/', $colparams, $matches)) {
              if ($matches[1]) { $rowspans[$c] = $matches[1]; }
              $colparams = substr($colparams, strlen($matches[1]) + 1);
              if ($matches[1]) { continue; }
            }
            $params = 0;
          }
        }
        if (strlen($attrs)) {
          if (preg_match('/_/', $attrs)) { $header = 1; }
          if (preg_match('{(' . $this->alignre . ')}', $attrs, $matches) && strlen($matches[1])) { $colalign = ''; }
          // determine column alignment
          if (preg_match('/<>/', $attrs)) {
            $colalign .= '<>';
          } elseif (preg_match('/</', $attrs)) {
            $colalign .= '<';
          } elseif (preg_match('/=/', $attrs)) {
            $colalign = '=';
          } elseif (preg_match('/>/', $attrs)) {
            $colalign = '>';
          }
          if (preg_match('/\^/', $attrs)) {
            $colalign .= '^';
          } elseif (preg_match('/~/', $attrs)) {
            $colalign .= '~';
          } elseif (preg_match('/-/', $attrs)) {
            $colalign .= '-';
          }
        }
        if ($rowheader) { $header = 1; }
        if ($header) { $colaligns[$c] = $colalign; }
        $col = preg_replace('/^ +/', '', $col, 1); $col = preg_replace('/ +$/', '', $col, 1);
        if (strlen($col)) {
          // create one cell tag
          $rowspan = ($rowspans[$c] ? $rowspans[$c] : 0);
          $col_out = '<' . ($header ? 'th' : 'td');
          if ($colalign) {
            // horizontal, vertical alignment
            $halign = $this->_halign($colalign);
            if ($halign) { $col_out .= " align=\"$halign\""; }
            $valign = $this->_valign($colalign);
            if ($valign) { $col_out .= " valign=\"$valign\""; }
          }
          // apply css attributes, row, column spans
          if ($colpadl) { $colstyle .= ";padding-left:${colpadl}em"; }
          if ($colpadr) { $colstyle .= ";padding-right:${colpadr}em"; }
          if ($colclass) { $col_out .= " class=\"$colclass\""; }
          if ($colid) { $col_out .= " id=\"$colid\""; }
          if ($colstyle) { $colstyle = preg_replace('/^;/', '', $colstyle, 1); }
          if ($colstyle) { $col_out .= " style=\"$colstyle\""; }
          if ($collang) { $col_out .= " lang=\"$collang\""; }
          if ($colspan > 1) { $col_out .= " colspan=\"$colspan\""; }
          if ($rowspan > 1) { $col_out .= " rowspan=\"$rowspan\""; }
          $col_out .= '>';
          // if the content of this cell has newlines OR matches
          // our paragraph block signature, process it as a full-blown
          // textile document
          if (preg_match('/\n\n/', $col) ||
              preg_match('{^(?:' . $this->halignre . '|' . $this->clstypadre . '*)*
                            [\*\#]
                            (?:' . $this->clstypadre . '*|' . $this->halignre . ')*\ }x', $col)) {
            $col_out .= $this->process($col);
          } else {
            $col_out .= $this->format_paragraph(array('text' => $col));
          }
          $col_out .= '</' . ($header ? 'th' : 'td') . '>';
          $row_out = $col_out . $row_out;
          if ($colspan) { $colspan = 0; }
        } else {
          if ($colspan == 0) { $colspan = 1; }
          $colspan++;
        }
      }
      if ($colspan > 1) {
        // handle the spanned column if we came up short
        $colspan--;
        $row_out = "<td"
                 . ($colspan > 1 ? " colspan=\"$colspan\"" : '')
                 . "></td>$row_out";
      }

      // build one table row
      $out .= "<tr";
      if ($rowalign) {
        $valign = $this->_valign($rowalign);
        if ($valign) { $out .= " valign=\"$valign\""; }
      }
      if ($rowclass) { $out .= " class=\"$rowclass\""; }
      if ($rowid) { $out .= " id=\"$rowid\""; }
      if ($rowstyle) { $out .= " style=\"$rowstyle\""; }
      $out .= ">$row_out</tr>";
    }

    // now, form the table tag itself
    $table = '';
    $table .= "<table";
    if ($talign) {
      if ($this->options['css_mode']) {
        // horizontal alignment
        $alignment = $this->_halign($talign);
        if ($talign == '=') {
          $tstyle .= ';margin-left:auto;margin-right:auto';
        } else {
          if ($alignment) { $tstyle .= ';float:' . $alignment; }
        }
        if ($alignment) { $tclass .= ' ' . $alignment; }
      } else {
        $alignment = $this->_halign($talign);
        if ($alignment) { $table .= " align=\"$alignment\""; }
      }
    }
    if ($tpadl) { $tstyle .= ";padding-left:${tpadl}em"; }
    if ($tpadr) { $tstyle .= ";padding-right:${tpadr}em"; }
    if ($tclass) { $tclass = preg_replace('/^ /', '', $tclass, 1); }
    if ($tclass) { $table .= " class=\"$tclass\""; }
    if ($tid) { $table .= " id=\"$tid\""; }
    if ($tstyle) { $tstyle = preg_replace('/^;/', '', $tstyle, 1); }
    if ($tstyle) { $table .= " style=\"$tstyle\""; }
    if ($tlang) { $table .= " lang=\"$tlang\""; }
    if ($tclass || $tid || $tstyle) { $table .= " cellspacing=\"0\""; }
    $table .= ">$out</table>";

    if (preg_match('|<tr></tr>|', $table)) {
      // exception -- something isn't right so return fail case
      return NULL;
    }

    return $table;
  } // function format_table

  /**
   * The following attributes are allowed:
   *
   * <ul>
   *
   * <li><b>text</b>
   *
   * The text to be processed.</li>
   *
   * <li><b>filters</b>
   *
   * An array reference of filter names to run for the given text.</li>
   *
   * </ul>
   *
   * @param $args An @c array specifying the text and filters to
   *        apply.
   *
   * @return A @c string containing the filtered text.
   *
   * @private
   */
  function apply_filters($args) {
    $text = $args['text'];
    if (!$text) { return ''; }
    $list = $args['filters'];
    $filters = $this->options['filters'];
    if (!is_array($filters)) { return $text; }

    $param = $this->filter_param();
    foreach ($list as $filter) {
      if (!isset($filters[$filter])) { continue; }
      if (is_string($filters[$filter])) {
        $text = (($f = create_function('$text, $param', $filters[$filter])) ? $f($text, $param) : $text);
      }
    }
    return $text;
  } // function apply_filters

  // minor utility / formatting routines

  var $Have_Entities = 1;

  /**
   * Encodes input @c $html string, escaping characters as needed
   * to HTML entities. This relies on the @c htmlentities function
   * for full effect. If unavailable, @c encode_html_basic is used
   * as a fallback technique. If the "char_encoding" flag is
   * set to false, @c encode_html_basic is used exclusively.
   *
   * @param $html A @c string specifying the HTML to be encoded.
   * @param $can_double_encode If provided, a @c bool indicating
   *        whether or not ampersand characters should be
   *        unconditionally encoded.
   *
   * @return A @c string containing the encoded HTML.
   *
   * @private
   */
  function encode_html($html, $can_double_encode = FALSE) {
    if (!$html) { return ''; }
    if ($this->Have_Entities && $this->options['char_encoding']) {
      $html = htmlentities($html);
    } else {
      $html = $this->encode_html_basic($html, $can_double_encode);
    }
    return $html;
  } // function encode_html

  /**
   * Decodes HTML entities in @c $html to their natural character
   * equivalents.
   *
   * @param $html A @c string specifying the HTML to be decoded.
   *
   * @return A @c string containing the decode HTML
   *
   * @private
   */
  function decode_html($html) {
    $html = preg_replace('!&quot;!', '"', $html);
    $html = preg_replace('!&amp;!', '&', $html);
    $html = preg_replace('!&lt;!', '<', $html);
    $html = preg_replace('!&gt;!', '>', $html);
    return $html;
  } // function decode_html

  /**
   * Encodes the input @c $html string for the following characters:
   * \<, \>, & and ". If @c $can_double_encode is true, all
   * ampersand characters are escaped even if they already were.
   * If @c $can_double_encode is false, ampersands are only escaped
   * when they aren't part of a HTML entity already.
   *
   * @param $html A @c string specifying the HTML to be encoded.
   * @param $can_double_encode If provided, a @c bool indicating
   *        whether or not ampersand characters should be
   * unconditionally encoded.
   *
   * @return A @c string containing the encoded HTML.
   *
   * @private
   */
  function encode_html_basic($html, $can_double_encode = FALSE) {
    if (!$html) { return ''; }
    if (!preg_match('/[^\w\s]/', $html)) { return $html; }
    if ($can_double_encode) {
      $html = preg_replace('!&!', '&amp;', $html);
    } else {
      // Encode any & not followed by something that looks like
      // an entity, numeric or otherwise.
      $html = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w{1,8});)/', '&amp;', $html);
    }
    $html = preg_replace('!"!', '&quot;', $html);
    $html = preg_replace('!<!', '&lt;', $html);
    $html = preg_replace('!>!', '&gt;', $html);
    return $html;
  } // function encode_html_basic

  /**
   * Returns the size for the image identified in @c $file. This
   * method relies upon the @c getimagesize function. If unavailable,
   * @c image_size will return @c NULL. Otherwise, the expected return
   * value is an array of the width and height (in that order), in
   * pixels.
   *
   * @param $file A @c string specifying the path or URL for the image
   *        file.
   *
   * @return An @c array containing the width and height
   *         (respectively) of the image.
   *
   * @private
   */
  function image_size($file) {
    $Have_ImageSize = function_exists('getimagesize');

    if ($Have_ImageSize) {
      if (file_exists($file)) {
        return @getimagesize($file);
      } else {
        if ($docroot = ($this->docroot() ? $this->docroot() : $_SERVER['DOCUMENT_ROOT'])) {
          $fullpath = $docroot . preg_replace('|^/*(.*)$|', '/$1', $file);
          if (file_exists($fullpath)) {
            return @getimagesize($fullpath);
          }
        }
      }
    }
    return @getimagesize($file);
  } // function image_size

  /**
   * Encodes the query portion of a URL, escaping characters
   * as necessary.
   *
   * @param $str A @c string specifying the URL to be encoded.
   *
   * @return A @c string containing the encoded URL.
   *
   * @private
   */
  function encode_url($str) {
    $str = preg_replace_callback('!([^A-Za-z0-9_\.\-\+\&=%;])!x',
                            $this->_cb('ord($m[1]) > 255 ? \'%u\' . sprintf("%04X", ord($m[1]))
                                                       : \'%\'  . sprintf("%02X", ord($m[1]))'), $str);
    return $str;
  } // function encode_url

  /**
   * Encodes the email address in @c $addr for 'mailto:' links.
   *
   * @param $addr A @c string specifying the email address to encode.
   *
   * @return A @c string containing the encoded email address.
   *
   * @private
   */
  function mail_encode($addr) {
    // granted, this is simple, but it gives off warm fuzzies
    $addr = preg_replace_callback('!([^\$])!x',
                             $this->_cb('ord($m[1]) > 255 ? \'%u\' . sprintf("%04X", ord($m[1]))
                                                        : \'%\'  . sprintf("%02X", ord($m[1]))'), $addr);
    return $addr;
  } // function mail_encode

  /**
   * Processes string, formatting plain quotes into curly quotes.
   *
   * @param $str A @c string specifying the text to process.
   *
   * @return A @c string containing the processed text.
   *
   * @private
   */
  function process_quotes($str) {
    // stub routine for now. subclass and implement.
    return $str;
  } // function process_quotes

  // a default set of macros for the {...} macro syntax
  // just a handy way to write a lot of the international characters
  // and some commonly used symbols

  /**
   * Returns an associative @c array of macros that are assigned to be processed by
   * default within the @c format_inline method.
   *
   * @return An @c array containing the default macros.
   *
   * @private
   */
  function default_macros() {
    // <, >, " must be html entities in the macro text since
    // those values are escaped by the time they are processed
    // for macros.
    return array(
      'c|' => '&#162;', // CENT SIGN
      '|c' => '&#162;', // CENT SIGN
      'L-' => '&#163;', // POUND SIGN
      '-L' => '&#163;', // POUND SIGN
      'Y=' => '&#165;', // YEN SIGN
      '=Y' => '&#165;', // YEN SIGN
      '(c)' => '&#169;', // COPYRIGHT SIGN
      '&lt;&lt;' => '&#171;', // LEFT-POINTING DOUBLE ANGLE QUOTATION
      '(r)' => '&#174;', // REGISTERED SIGN
      '+_' => '&#177;', // PLUS-MINUS SIGN
      '_+' => '&#177;', // PLUS-MINUS SIGN
      '&gt;&gt;' => '&#187;', // RIGHT-POINTING DOUBLE ANGLE QUOTATION
      '1/4' => '&#188;', // VULGAR FRACTION ONE QUARTER
      '1/2' => '&#189;', // VULGAR FRACTION ONE HALF
      '3/4' => '&#190;', // VULGAR FRACTION THREE QUARTERS
      'A`' => '&#192;', // LATIN CAPITAL LETTER A WITH GRAVE
      '`A' => '&#192;', // LATIN CAPITAL LETTER A WITH GRAVE
      'A\'' => '&#193;', // LATIN CAPITAL LETTER A WITH ACUTE
      '\'A' => '&#193;', // LATIN CAPITAL LETTER A WITH ACUTE
      'A^' => '&#194;', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX
      '^A' => '&#194;', // LATIN CAPITAL LETTER A WITH CIRCUMFLEX
      'A~' => '&#195;', // LATIN CAPITAL LETTER A WITH TILDE
      '~A' => '&#195;', // LATIN CAPITAL LETTER A WITH TILDE
      'A"' => '&#196;', // LATIN CAPITAL LETTER A WITH DIAERESIS
      '"A' => '&#196;', // LATIN CAPITAL LETTER A WITH DIAERESIS
      'Ao' => '&#197;', // LATIN CAPITAL LETTER A WITH RING ABOVE
      'oA' => '&#197;', // LATIN CAPITAL LETTER A WITH RING ABOVE
      'AE' => '&#198;', // LATIN CAPITAL LETTER AE
      'C,' => '&#199;', // LATIN CAPITAL LETTER C WITH CEDILLA
      ',C' => '&#199;', // LATIN CAPITAL LETTER C WITH CEDILLA
      'E`' => '&#200;', // LATIN CAPITAL LETTER E WITH GRAVE
      '`E' => '&#200;', // LATIN CAPITAL LETTER E WITH GRAVE
      'E\'' => '&#201;', // LATIN CAPITAL LETTER E WITH ACUTE
      '\'E' => '&#201;', // LATIN CAPITAL LETTER E WITH ACUTE
      'E^' => '&#202;', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX
      '^E' => '&#202;', // LATIN CAPITAL LETTER E WITH CIRCUMFLEX
      'E"' => '&#203;', // LATIN CAPITAL LETTER E WITH DIAERESIS
      '"E' => '&#203;', // LATIN CAPITAL LETTER E WITH DIAERESIS
      'I`' => '&#204;', // LATIN CAPITAL LETTER I WITH GRAVE
      '`I' => '&#204;', // LATIN CAPITAL LETTER I WITH GRAVE
      'I\'' => '&#205;', // LATIN CAPITAL LETTER I WITH ACUTE
      '\'I' => '&#205;', // LATIN CAPITAL LETTER I WITH ACUTE
      'I^' => '&#206;', // LATIN CAPITAL LETTER I WITH CIRCUMFLEX
      '^I' => '&#206;', // LATIN CAPITAL LETTER I WITH CIRCUMFLEX
      'I"' => '&#207;', // LATIN CAPITAL LETTER I WITH DIAERESIS
      '"I' => '&#207;', // LATIN CAPITAL LETTER I WITH DIAERESIS
      'D-' => '&#208;', // LATIN CAPITAL LETTER ETH
      '-D' => '&#208;', // LATIN CAPITAL LETTER ETH
      'N~' => '&#209;', // LATIN CAPITAL LETTER N WITH TILDE
      '~N' => '&#209;', // LATIN CAPITAL LETTER N WITH TILDE
      'O`' => '&#210;', // LATIN CAPITAL LETTER O WITH GRAVE
      '`O' => '&#210;', // LATIN CAPITAL LETTER O WITH GRAVE
      'O\'' => '&#211;', // LATIN CAPITAL LETTER O WITH ACUTE
      '\'O' => '&#211;', // LATIN CAPITAL LETTER O WITH ACUTE
      'O^' => '&#212;', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX
      '^O' => '&#212;', // LATIN CAPITAL LETTER O WITH CIRCUMFLEX
      'O~' => '&#213;', // LATIN CAPITAL LETTER O WITH TILDE
      '~O' => '&#213;', // LATIN CAPITAL LETTER O WITH TILDE
      'O"' => '&#214;', // LATIN CAPITAL LETTER O WITH DIAERESIS
      '"O' => '&#214;', // LATIN CAPITAL LETTER O WITH DIAERESIS
      'O/' => '&#216;', // LATIN CAPITAL LETTER O WITH STROKE
      '/O' => '&#216;', // LATIN CAPITAL LETTER O WITH STROKE
      'U`' =>  '&#217;', // LATIN CAPITAL LETTER U WITH GRAVE
      '`U' =>  '&#217;', // LATIN CAPITAL LETTER U WITH GRAVE
      'U\'' => '&#218;', // LATIN CAPITAL LETTER U WITH ACUTE
      '\'U' => '&#218;', // LATIN CAPITAL LETTER U WITH ACUTE
      'U^' => '&#219;', // LATIN CAPITAL LETTER U WITH CIRCUMFLEX
      '^U' => '&#219;', // LATIN CAPITAL LETTER U WITH CIRCUMFLEX
      'U"' => '&#220;', // LATIN CAPITAL LETTER U WITH DIAERESIS
      '"U' => '&#220;', // LATIN CAPITAL LETTER U WITH DIAERESIS
      'Y\'' => '&#221;', // LATIN CAPITAL LETTER Y WITH ACUTE
      '\'Y' => '&#221;', // LATIN CAPITAL LETTER Y WITH ACUTE
      'a`' => '&#224;', // LATIN SMALL LETTER A WITH GRAVE
      '`a' => '&#224;', // LATIN SMALL LETTER A WITH GRAVE
      'a\'' => '&#225;', // LATIN SMALL LETTER A WITH ACUTE
      '\'a' => '&#225;', // LATIN SMALL LETTER A WITH ACUTE
      'a^' => '&#226;', // LATIN SMALL LETTER A WITH CIRCUMFLEX
      '^a' => '&#226;', // LATIN SMALL LETTER A WITH CIRCUMFLEX
      'a~' => '&#227;', // LATIN SMALL LETTER A WITH TILDE
      '~a' => '&#227;', // LATIN SMALL LETTER A WITH TILDE
      'a"' => '&#228;', // LATIN SMALL LETTER A WITH DIAERESIS
      '"a' => '&#228;', // LATIN SMALL LETTER A WITH DIAERESIS
      'ao' => '&#229;', // LATIN SMALL LETTER A WITH RING ABOVE
      'oa' => '&#229;', // LATIN SMALL LETTER A WITH RING ABOVE
      'ae' => '&#230;', // LATIN SMALL LETTER AE
      'c,' => '&#231;', // LATIN SMALL LETTER C WITH CEDILLA
      ',c' => '&#231;', // LATIN SMALL LETTER C WITH CEDILLA
      'e`' => '&#232;', // LATIN SMALL LETTER E WITH GRAVE
      '`e' => '&#232;', // LATIN SMALL LETTER E WITH GRAVE
      'e\'' => '&#233;', // LATIN SMALL LETTER E WITH ACUTE
      '\'e' => '&#233;', // LATIN SMALL LETTER E WITH ACUTE
      'e^' => '&#234;', // LATIN SMALL LETTER E WITH CIRCUMFLEX
      '^e' => '&#234;', // LATIN SMALL LETTER E WITH CIRCUMFLEX
      'e"' => '&#235;', // LATIN SMALL LETTER E WITH DIAERESIS
      '"e' => '&#235;', // LATIN SMALL LETTER E WITH DIAERESIS
      'i`' => '&#236;', // LATIN SMALL LETTER I WITH GRAVE
      '`i' => '&#236;', // LATIN SMALL LETTER I WITH GRAVE
      'i\'' => '&#237;', // LATIN SMALL LETTER I WITH ACUTE
      '\'i' => '&#237;', // LATIN SMALL LETTER I WITH ACUTE
      'i^' => '&#238;', // LATIN SMALL LETTER I WITH CIRCUMFLEX
      '^i' => '&#238;', // LATIN SMALL LETTER I WITH CIRCUMFLEX
      'i"' => '&#239;', // LATIN SMALL LETTER I WITH DIAERESIS
      '"i' => '&#239;', // LATIN SMALL LETTER I WITH DIAERESIS
      'n~' => '&#241;', // LATIN SMALL LETTER N WITH TILDE
      '~n' => '&#241;', // LATIN SMALL LETTER N WITH TILDE
      'o`' => '&#242;', // LATIN SMALL LETTER O WITH GRAVE
      '`o' => '&#242;', // LATIN SMALL LETTER O WITH GRAVE
      'o\'' => '&#243;', // LATIN SMALL LETTER O WITH ACUTE
      '\'o' => '&#243;', // LATIN SMALL LETTER O WITH ACUTE
      'o^' => '&#244;', // LATIN SMALL LETTER O WITH CIRCUMFLEX
      '^o' => '&#244;', // LATIN SMALL LETTER O WITH CIRCUMFLEX
      'o~' => '&#245;', // LATIN SMALL LETTER O WITH TILDE
      '~o' => '&#245;', // LATIN SMALL LETTER O WITH TILDE
      'o"' => '&#246;', // LATIN SMALL LETTER O WITH DIAERESIS
      '"o' => '&#246;', // LATIN SMALL LETTER O WITH DIAERESIS
      ':-' => '&#247;', // DIVISION SIGN
      '-:' => '&#247;', // DIVISION SIGN
      'o/' => '&#248;', // LATIN SMALL LETTER O WITH STROKE
      '/o' => '&#248;', // LATIN SMALL LETTER O WITH STROKE
      'u`' => '&#249;', // LATIN SMALL LETTER U WITH GRAVE
      '`u' => '&#249;', // LATIN SMALL LETTER U WITH GRAVE
      'u\'' => '&#250;', // LATIN SMALL LETTER U WITH ACUTE
      '\'u' => '&#250;', // LATIN SMALL LETTER U WITH ACUTE
      'u^' => '&#251;', // LATIN SMALL LETTER U WITH CIRCUMFLEX
      '^u' => '&#251;', // LATIN SMALL LETTER U WITH CIRCUMFLEX
      'u"' => '&#252;', // LATIN SMALL LETTER U WITH DIAERESIS
      '"u' => '&#252;', // LATIN SMALL LETTER U WITH DIAERESIS
      'y\'' => '&#253;', // LATIN SMALL LETTER Y WITH ACUTE
      '\'y' => '&#253;', // LATIN SMALL LETTER Y WITH ACUTE
      'y"' => '&#255;', // LATIN SMALL LETTER Y WITH DIAERESIS
      '"y' => '&#255;', // LATIN SMALL LETTER Y WITH DIAERESIS
      'OE' => '&#338;', // LATIN CAPITAL LIGATURE OE
      'oe' => '&#339;', // LATIN SMALL LIGATURE OE
      '*' => '&#8226;', // BULLET
      'Fr' => '&#8355;', // FRENCH FRANC SIGN
      'L=' => '&#8356;', // LIRA SIGN
      '=L' => '&#8356;', // LIRA SIGN
      'Rs' => '&#8360;', // RUPEE SIGN
      'C=' => '&#8364;', // EURO SIGN
      '=C' => '&#8364;', // EURO SIGN
      'tm' => '&#8482;', // TRADE MARK SIGN
      '&lt;-' => '&#8592;', // LEFTWARDS ARROW
      '-&gt;' => '&#8594;', // RIGHTWARDS ARROW
      '&lt;=' => '&#8656;', // LEFTWARDS DOUBLE ARROW
      '=&gt;' => '&#8658;', // RIGHTWARDS DOUBLE ARROW
      '=/' => '&#8800;', // NOT EQUAL TO
      '/=' => '&#8800;', // NOT EQUAL TO
      '&lt;_' => '&#8804;', // LESS-THAN OR EQUAL TO
      '_&lt;' => '&#8804;', // LESS-THAN OR EQUAL TO
      '&gt;_' => '&#8805;', // GREATER-THAN OR EQUAL TO
      '_&gt;' => '&#8805;', // GREATER-THAN OR EQUAL TO
      ':(' => '&#9785;', // WHITE FROWNING FACE
      ':)' => '&#9786;', // WHITE SMILING FACE
      'spade' => '&#9824;', // BLACK SPADE SUIT
      'club' => '&#9827;', // BLACK CLUB SUIT
      'heart' => '&#9829;', // BLACK HEART SUIT
      'diamond' => '&#9830;', // BLACK DIAMOND SUIT
    );
  } // function default_macros

  // "private", internal routines

  /**
   * Sets the default CSS names for CSS controlled markup. This
   * is an internal function that should not be called directly.
   *
   * @private
   */
  function _css_defaults() {
    $css_defaults = array(
      'class_align_right' => 'right',
      'class_align_left' => 'left',
      'class_align_center' => 'center',
      'class_align_top' => 'top',
      'class_align_bottom' => 'bottom',
      'class_align_middle' => 'middle',
      'class_align_justify' => 'justify',
      'class_caps' => 'caps',
      'class_footnote' => 'footnote',
      'id_footnote_prefix' => 'fn',
    );
    $this->css($css_defaults);
  } // function _css_defaults

  /**
   * Returns the alignment keyword depending on the symbol passed.
   *
   * <ul>
   *
   * <li><b><code>\<\></code></b>
   *
   * becomes 'justify'</li>
   *
   * <li><b><code>\<</code></b>
   *
   * becomes 'left'</li>
   *
   * <li><b><code>\></code></b>
   *
   * becomes 'right'</li>
   *
   * <li><b><code>=</code></b>
   *
   * becomes 'center'</li>
   *
   * </ul>
   *
   * @param $align A @c string specifying the alignment code.
   *
   * @return A @c string containing the alignment text.
   *
   * @private
   */
  function _halign($align) {
    if (preg_match('/<>/', $align)) {
      return 'justify';
    } elseif (preg_match('/</', $align)) {
      return 'left';
    } elseif (preg_match('/>/', $align)) {
      return 'right';
    } elseif (preg_match('/=/', $align)) {
      return 'center';
    }
    return '';
  } // function _halign

  /**
   * Returns the alignment keyword depending on the symbol passed.
   *
   * <ul>
   *
   * <li><b><code>^</code></b>
   *
   * becomes 'top'</li>
   *
   * <li><b><code>~</code></b>
   *
   * becomes 'bottom'</li>
   *
   * <li><b><code>-</code></b>
   *
   * becomes 'middle'</li>
   *
   * </ul>
   *
   * @param $align A @c string specifying the alignment code.
   *
   * @return A @c string containing the alignment text.
   *
   * @private
   */
  function _valign($align) {
    if (preg_match('/\^/', $align)) {
      return 'top';
    } elseif (preg_match('/~/', $align)) {
      return 'bottom';
    } elseif (preg_match('/-/', $align)) {
      return 'middle';
    }
    return '';
  } // function _valign

  /**
   * Returns the alignment keyword depending on the symbol passed.
   * The following alignment symbols are recognized, and given
   * preference in the order listed:
   *
   * <ul>
   *
   * <li><b><code>^</code></b>
   *
   * becomes 'top'</li>
   *
   * <li><b><code>~</code></b>
   *
   * becomes 'bottom'</li>
   *
   * <li><b><code>-</code></b>
   *
   * becomes 'middle'</li>
   *
   * <li><b><code>\<</code></b>
   *
   * becomes 'left'</li>
   *
   * <li><b><code>\></code></b>
   *
   * becomes 'right'</li>
   *
   * </ul>
   *
   * @param $align A @c string containing the alignment code.
   *
   * @return A @c string containing the alignment text.
   *
   * @private
   */
  function _imgalign($align) {
    $align = preg_replace('/(<>|=)/', '', $align);
    return ($this->_valign($align) ? $this->_valign($align) : $this->_halign($align));
  } // function _imgalign

  /**
   * This utility routine will take 'border' characters off of
   * the given @c $pre and @c $post strings if they match one of these
   * conditions:
   * <pre>
   *     $pre starts with '[', $post ends with ']'
   *     $pre starts with '{', $post ends with '}'
   * </pre>
   * If neither condition is met, then the @c $pre and @c $post
   * values are left untouched.
   *
   * @param $pre A @c string specifying the prefix.
   * @param $post A @c string specifying the postfix.
   *
   * @private
   */
  function _strip_borders(&$pre, &$post) {
    if ($post && $pre && preg_match('/[{[]/', ($open = substr($pre, 0, 1)))) {
      $close = substr($post, 0, 1);
      if ((($open == '{') && ($close == '}')) ||
          (($open == '[') && ($close == ']'))) {
        $pre = substr($pre, 1);
        $post = substr($post, 1);
      } else {
        if (!preg_match('/[}\]]/', $close)) { $close = substr($post, -1, 1); }
        if ((($open == '{') && ($close == '}')) ||
            (($open == '[') && ($close == ']'))) {
          $pre = substr($pre, 1);
          $post = substr($post, 0, strlen($post) - 1);
        }
      }
    }
  } // function _strip_borders

  /**
   * An internal routine that takes a string and appends it to an array.
   * It returns a marker that is used later to restore the preserved
   * string.
   *
   * @param $array The @c array in which to store the replacement
   *        text.
   * @param $str A @c string specifying the replacement text.
   *
   * @return A @c string containing a temporary marker for the
   *         replacement.
   *
   * @private
   */
  function _repl(&$array, $str) {
    $array[] = $str;
    return '<textile#' . count($array) . '>';
  } // function _repl

  /**
   * An internal routine responsible for breaking up a string into
   * individual tag and plaintext elements.
   *
   * @param $str A @c string specifying the text to tokenize.
   *
   * @return An @c array containing the tag and text tokens.
   *
   * @private
   */
  function _tokenize($str) {
    $pos = 0;
    $len = strlen($str);
    unset($tokens);

    $depth = 6;
    $nested_tags = substr(str_repeat('(?:</?[A-Za-z0-9:]+ \s? (?:[^<>]|', $depth), 0, -1)
      . str_repeat(')*>)', $depth);
    $match = '(?s: <! ( -- .*? -- \s* )+ > )|  # comment
              (?s: <\? .*? \?> )|              # processing instruction
              (?s: <% .*? %> )|                # ASP-like
              (?:' . $nested_tags . ')|
              (?:' . $this->codere . ')';     // nested tags

    while (preg_match('{(' . $match . ')}x', substr($str, $pos), $matches, PREG_OFFSET_CAPTURE)) {
      $whole_tag = $matches[1][0];
      $sec_start = $pos + $matches[1][1] + strlen($whole_tag);
      $tag_start = $sec_start - strlen($whole_tag);
      if ($pos < $tag_start) {
        $tokens[] = array('text', substr($str, $pos, $tag_start - $pos));
      }
      if (preg_match('/^[[{]?@/', $whole_tag)) {
        $tokens[] = array('text', $whole_tag);
      } else {
        // this clever hack allows us to preserve \n within tags.
        // this is restored at the end of the format_paragraph method
        //$whole_tag = preg_replace('/\n/', "\r", $whole_tag);
        $whole_tag = preg_replace('/\n/', "\001", $whole_tag);
        $tokens[] = array('tag', $whole_tag);
      }
      $pos = $sec_start;
    }
    if ($pos < $len) { $tokens[] = array('text', substr($str, $pos, $len - $pos)); }
    return $tokens;
  } // function _tokenize

  /**
   * Returns the version of this release of Textile.php. *JHR*
   *
   * @return An @c array with keys 'text' and 'build' containing the
   *         text version and build ID of this release, respectively.
   *
   * @static
   */
  function version() {
    /* Why text and an ID?  Well, the text is easier for the user to
     * read and understand while the build ID, being a number (a date
     * with a serial, specifically), is easier for the developer to
     * use to determine newer/older versions for upgrade and
     * installation purposes.
     */
    return array("text" => "2.0.8", "build" => 2005032100);
  } // function version

/**
   * Creates a custom callback function from the provided PHP
   * code. The result is used as the callback in
   * @c preg_replace_callback calls. *JHR*
   *
   * @param $function A @c string specifying the PHP code for the
   *        function body.
   *
   * @return A @c function to be used for the callback.
   *
   * @private
   */
  function _cb($function) {
    $current =& Textile::_current_store($this);
    return create_function('$m', '$me =& Textile::_current(); return ' . $function . ';');
  } // function _cb

  /**
   * Stores a static variable for the Textile class. This helper
   * function is used by @c _current to simulate a static
   * class variable in PHP. *JHR*
   *
   * @param $new If a non-@c NULL object reference, the Textile object
   *        to be set as the current object.
   *
   * @return The @c array containing a reference to the current
   *         Textile object at index 0. An array is used because PHP
   *         does not allow static variables to be references.
   *
   * @static
   * @private
   */
 /* static */ function &_current_store(&$new) {
   static $current = array();

   if ($new != NULL) {
     $current = array(&$new);
   }

   return $current;
 } // function _current_store

  /**
   * Returns the "current" Textile object. This is used within
   * anonymous callback functions which cannot have the scope of a
   * specific object. *JHR*
   *
   * @return An @c object reference to the current Textile object.
   *
   * @static
   * @private
   */
 /* static */ function &_current() {
   $current =& Textile::_current_store($null = NULL);
   return $current[0];
 } // function _current
} // class Textile

/**
 * Brad Choate's mttextile Movable Type plugin adds some additional
 * functionality to the Textile.pm Perl module. This includes optional
 * "SmartyPants" processing of text to produce smart quotes, dashes,
 * etc., code colorizing using Beautifier, and some special lookup
 * links (imdb, google, dict, and amazon). The @c MTLikeTextile class
 * is a subclass of @c Textile that provides an MT-like implementation
 * of Textile to produce results similar to that of the mttextile
 * plugin. Currently only the SmartyPants and special lookup links are
 * implemented.
 *
 * Using the @c MTLikeTextile class is exactly the same as using @c
 * Textile. Simply use <code>$textile = new MTLikeTextile;</code>
 * instead of <code>$textile = new Textile;</code> to create a Textile
 * object.  This will enable the special lookup links.  To enable
 * SmartyPants processing, you must install the SmartyPants-PHP
 * implementation available at
 * <a
 * href="http://monauraljerk.org/smartypants-php/">http://monauraljerk.org/smartypants-php/</a>
 * and include the
 * SmartyPants-PHP.inc file.
 *
 * <pre><code>
 * include_once("Textile.php");
 * include_once("SmartyPants-PHP.inc");
 * $text = \<\<\<EOT
 * h1. Heading
 *
 * A _simple_ demonstration of Textile markup.
 *
 * * One
 * * Two
 * * Three
 *
 * "More information":http://www.textism.com/tools/textile is available.
 * EOT;
 *
 * $textile = new MTLikeTextile;
 * $html = $textile->process($text);
 * print $html;
 * </code></pre>
 *
 * @brief A Textile implementation providing additional
 *        Movable-Type-like formatting to produce results similar to
 *        the mttextile plugin.
 *
 * @author Jim Riggs \<textile at jimandlisa dot com\>
 */
class MTLikeTextile extends Textile {
  /**
   * Instantiates a new MTLikeTextile object. Optional options
   * can be passed to initialize the object. Attributes for the
   * options key are the same as the get/set method names
   * documented here.
   *
   * @param $options The @c array specifying the options to use for
   *        this object.
   *
   * @public
   */
  function MTLikeTextile($options = array()) {
    parent::Textile($options);
  } // function MTLikeTextile

  /**
   * @private
   */
  function process_quotes($str) {
    if (!$this->options['do_quotes'] || !function_exists('SmartyPants')) {
      return $str;
    }

    return SmartyPants($str, $this->options['smarty_mode']);
  } // function process_quotes

  /**
   * @private
   */
  function format_url($args) {
    $url = ($args['url'] ? $args['url'] : '');

    if (preg_match('/^(imdb|google|dict|amazon)(:(.+))?$/x', $url, $matches)) {
      $term = $matches[3];
      $term = ($term ? $term : strip_tags($args['linktext']));

      switch ($matches[1]) {
        case 'imdb':
          $args['url'] = 'http://www.imdb.com/Find?for=' . $term;
          break;

        case 'google':
          $args['url'] = 'http://www.google.com/search?q=' . $term;
          break;

        case 'dict':
          $args['url'] = 'http://www.dictionary.com/search?q=' . $term;
          break;

        case 'amazon':
          $args['url'] = 'http://www.amazon.com/exec/obidos/external-search?index=blended&keyword=' . $term;
          break;
      }
    }

    return parent::format_url($args);
  } // function format_url
} // class MTLikeTextile

/**
 * @mainpage
 * Textile - A Humane Web Text Generator.
 *
 * @section synopsis SYNOPSIS
 *
 * <pre><code>
 * include_once("Textile.php");
 * $text = \<\<\<EOT
 * h1. Heading
 *
 * A _simple_ demonstration of Textile markup.
 *
 * * One
 * * Two
 * * Three
 *
 * "More information":http://www.textism.com/tools/textile is available.
 * EOT;
 *
 * $textile = new Textile;
 * $html = $textile->process($text);
 * print $html;
 * </code></pre>
 *
 * @section abstract ABSTRACT
 *
 * Textile.php is a PHP-based implementation of Dean Allen's Textile
 * syntax. Textile is shorthand for doing common formatting tasks.
 *
 * @section syntax SYNTAX
 *
 * Textile processes text in units of blocks and lines.
 * A block might also be considered a paragraph, since blocks
 * are separated from one another by a blank line. Blocks
 * can begin with a signature that helps identify the rest
 * of the block content. Block signatures include:
 *
 * <ul>
 *
 * <li><b>p</b>
 *
 * A paragraph block. This is the default signature if no
 * signature is explicitly given. Paragraphs are formatted
 * with all the inline rules (see inline formatting) and
 * each line receives the appropriate markup rules for
 * the flavor of HTML in use. For example, newlines for XHTML
 * content receive a \<br /\> tag at the end of the line
 * (with the exception of the last line in the paragraph).
 * Paragraph blocks are enclosed in a \<p\> tag.</li>
 *
 * <li><b>pre</b>
 *
 * A pre-formatted block of text. Textile will not add any
 * HTML tags for individual lines. Whitespace is also preserved.
 * 
 * Note that within a "pre" block, \< and \> are
 * translated into HTML entities automatically.</li>
 *
 * <li><b>bc</b>
 *
 * A "bc" signature is short for "block code", which implies
 * a preformatted section like the 'pre' block, but it also
 * gets a \<code\> tag (or for XHTML 2, a \<blockcode\>
 * tag is used instead).
 * 
 * Note that within a "bc" block, \< and \> are
 * translated into HTML entities automatically.</li>
 *
 * <li><b>table</b>
 *
 * For composing HTML tables. See the "TABLES" section for more
 * information.</li>
 *
 * <li><b>bq</b>
 *
 * A "bq" signature is short for "block quote". Paragraph text
 * formatting is applied to these blocks and they are enclosed
 * in a \<blockquote\> tag as well as \<p\> tags
 * within.</li>
 *
 * <li><b>h1, h2, h3, h4, h5, h6</b>
 *
 * Headline signatures that produce \<h1\>, etc. tags.
 * You can adjust the relative output of these using the
 * head_offset attribute.</li>
 *
 * <li><b>clear</b>
 *
 * A 'clear' signature is simply used to indicate that the next
 * block should emit a CSS style attribute that clears any
 * floating elements. The default behavior is to clear "both",
 * but you can use the left (\< or right \>) alignment
 * characters to indicate which side to clear.</li>
 *
 * <li><b>dl</b>
 *
 * A "dl" signature is short for "definition list". See the
 * "LISTS" section for more information.</li>
 *
 * <li><b>fn</b>
 *
 * A "fn" signature is short for "footnote". You add a number
 * following the "fn" keyword to number the footnote. Footnotes
 * are output as paragraph tags but are given a special CSS
 * class name which can be used to style them as you see fit.</li>
 *
 * </ul>
 *
 * All signatures should end with a period and be followed
 * with a space. Inbetween the signature and the period, you
 * may use several parameters to further customize the block.
 * These include:
 *
 * <ul>
 *
 * <li><b><code>{style rule}</code></b>
 *
 * A CSS style rule. Style rules can span multiple lines.</li>
 *
 * <li><b><code>[ll]</code></b>
 *
 * A language identifier (for a "lang" attribute).</li>
 *
 * <li><b><code>(class)</code> or <code>(#id)</code> or <code>(class#id)</code></b>
 *
 * For CSS class and id attributes.</li>
 *
 * <li><b><code>\></code>, <code>\<</code>, <code>=</code>, <code>\<\></code></b>
 *
 * Modifier characters for alignment. Right-justification, left-justification,
 * centered, and full-justification.</li>
 *
 * <li><b><code>(</code> (one or more)</b>
 *
 * Adds padding on the left. 1em per "(" character is applied.
 * When combined with the align-left or align-right modifier,
 * it makes the block float.</li>
 *
 * <li><b><code>)</code> (one or more)</b>
 *
 * Adds padding on the right. 1em per ")" character is applied.
 * When combined with the align-left or align-right modifier,
 * it makes the block float.</li>
 *
 * <li><b><code>|filter|</code> or <code>|filter|filter|filter|</code></b>
 *
 * A filter may be invoked to further format the text for this
 * signature. If one or more filters are identified, the text
 * will be processed first using the filters and then by
 * Textile's own block formatting rules.</li>
 *
 * </ul>
 *
 * @subsection extendedblocks Extended Blocks
 *
 * Normally, a block ends with the first blank line encountered.
 * However, there are situations where you may want a block to continue
 * for multiple paragraphs of text. To cause a given block signature
 * to stay active, use two periods in your signature instead of one.
 * This will tell Textile to keep processing using that signature
 * until it hits the next signature is found.
 *
 * For example:
 * <pre>
 *     bq.. This is paragraph one of a block quote.
 *
 *     This is paragraph two of a block quote.
 *
 *     p. Now we're back to a regular paragraph.
 * </pre>
 * You can apply this technique to any signature (although for
 * some it doesn't make sense, like "h1" for example). This is
 * especially useful for "bc" blocks where your code may
 * have many blank lines scattered through it.
 *
 * @subsection escaping Escaping
 *
 * Sometimes you want Textile to just get out of the way and
 * let you put some regular HTML markup in your document. You
 * can disable Textile formatting for a given block using the '=='
 * escape mechanism:
 * <pre>
 *     p. Regular paragraph
 *
 *     ==
 *     Escaped portion -- will not be formatted
 *     by Textile at all
 *     ==
 *
 *     p. Back to normal.
 * </pre>
 * You can also use this technique within a Textile block,
 * temporarily disabling the inline formatting functions:
 * <pre>
 *     p. This is ==*a test*== of escaping.
 * </pre>
 * @subsection inlineformatting Inline Formatting
 *
 * Formatting within a block of text is covered by the "inline"
 * formatting rules. These operators must be placed up against
 * text/punctuation to be recognized. These include:
 *
 * <ul>
 *
 * <li><b><code>*strong*</code></b>
 *
 * Translates into \<strong\>strong\</strong\>.</li>
 *
 * <li><b>_emphasis_</b>
 *
 * Translates into \<em\>emphasis\</em\>.</li>
 *
 * <li><b><code>**bold**</code></b>
 *
 * Translates into \<b\>bold\</b\>.</li>
 *
 * <li><b><code>__italics__</code></b>
 *
 * Translates into \<i\>italics\</i\>.</li>
 *
 * <li><b><code>++bigger++</code></b>
 *
 * Translates into \<big\>bigger\</big\>.</li>
 *
 * <li><b><code>--smaller--</code></b>
 *
 * Translates into: \<small\>smaller\</small\>.</li>
 *
 * <li><b><code>-deleted text-</code></b>
 *
 * Translates into \<del\>deleted text\</del\>.</li>
 *
 * <li><b><code>+inserted text+</code></b>
 *
 * Translates into \<ins\>inserted text\</ins\>.</li>
 *
 * <li><b><code>^superscript^</code></b>
 *
 * Translates into \<sup\>superscript\</sup\>.</li>
 *
 * <li><b><code>~subscript~</code></b>
 *
 * Translates into \<sub\>subscript\</sub\>.</li>
 *
 * <li><b><code>\%span\%</code></b>
 *
 * Translates into \<span\>span\</span\>.</li>
 *
 * <li><b><code>\@code\@</code></b>
 *
 * Translates into \<code\>code\</code\>. Note
 * that within a '\@...\@' section, \< and \> are
 * translated into HTML entities automatically.</li>
 *
 * </ul>
 *
 * Inline formatting operators accept the following modifiers:
 *
 * <ul>
 *
 * <li><b><code>{style rule}</code></b>
 *
 * A CSS style rule.</li>
 *
 * <li><b><code>[ll]</code></b>
 *
 * A language identifier (for a "lang" attribute).</li>
 *
 * <li><b><code>(class)</code> or <code>(#id)</code> or <code>(class#id)</code></b>
 *
 * For CSS class and id attributes.</li>
 *
 * </ul>
 *
 * @subsubsection examples Examples
 * <pre>
 *     Textile is *way* cool.
 *
 *     Textile is *_way_* cool.
 * </pre>
 * Now this won't work, because the formatting
 * characters need whitespace before and after
 * to be properly recognized.
 * <pre>
 *     Textile is way c*oo*l.
 * </pre>
 * However, you can supply braces or brackets to
 * further clarify that you want to format, so
 * this would work:
 * <pre>
 *     Textile is way c[*oo*]l.
 * </pre>
 * @subsection footnotes Footnotes
 *
 * You can create footnotes like this:
 * <pre>
 *     And then he went on a long trip[1].
 * </pre>
 * By specifying the brackets with a number inside, Textile will
 * recognize that as a footnote marker. It will replace that with
 * a construct like this:
 * <pre>
 *     And then he went on a long
 *     trip<sup class="footnote"><a href="#fn1">1</a></sup>
 * </pre>
 * To supply the content of the footnote, place it at the end of your
 * document using a "fn" block signature:
 * <pre>
 *     fn1. And there was much rejoicing.
 * </pre>
 * Which creates a paragraph that looks like this:
 * <pre>
 *     <p class="footnote" id="fn1"><sup>1</sup> And there was
 *     much rejoicing.</p>
 * </pre>
 * @subsection links Links
 *
 * Textile defines a shorthand for formatting hyperlinks.
 * The format looks like this:
 * <pre>
 *     "Text to display":http://example.com
 * </pre>
 * In addition to this, you can add 'title' text to your link:
 * <pre>
 *     "Text to display (Title text)":http://example.com
 * </pre>
 * The URL portion of the link supports relative paths as well
 * as other protocols like ftp, mailto, news, telnet, etc.
 * <pre>
 *     "E-mail me please":mailto:someone\@example.com
 * </pre>
 * You can also use single quotes instead of double-quotes if
 * you prefer. As with the inline formatting rules, a hyperlink
 * must be surrounded by whitespace to be recognized (an
 * exception to this is common punctuation which can reside
 * at the end of the URL). If you have to place a URL next to
 * some other text, use the bracket or brace trick to do that:
 * <pre>
 *     You["gotta":http://example.com]seethis!
 * </pre>
 * Textile supports an alternate way to compose links. You can
 * optionally create a lookup list of links and refer to them
 * separately. To do this, place one or more links in a block
 * of it's own (it can be anywhere within your document):
 * <pre>
 *     [excom]http://example.com
 *     [exorg]http://example.org
 * </pre>
 * For a list like this, the text in the square brackets is
 * used to uniquely identify the link given. To refer to that
 * link, you would specify it like this:
 * <pre>
 *     "Text to display":excom
 * </pre>
 * Once you've defined your link lookup table, you can use
 * the identifiers any number of times.
 *
 * @subsection images Images
 *
 * Images are identified by the following pattern:
 * <pre>
 *     !/path/to/image!
 * </pre>
 * Image attributes may also be specified:
 * <pre>
 *     !/path/to/image 10x20!
 * </pre>
 * Which will render an image 10 pixels wide and 20 pixels high.
 * Another way to indicate width and height:
 * <pre>
 *     !/path/to/image 10w 20h!
 * </pre>
 * You may also redimension the image using a percentage.
 * <pre>
 *     !/path/to/image 20%x40%!
 * </pre>
 * Which will render the image at 20% of it's regular width
 * and 40% of it's regular height.
 *
 * Or specify one percentage to resize proprotionately:
 * <pre>
 *     !/path/to/image 20%!
 * </pre>
 * Alt text can be given as well:
 * <pre>
 *     !/path/to/image (Alt text)!
 * </pre>
 * The path of the image may refer to a locally hosted image or
 * can be a full URL.
 *
 * You can also use the following modifiers after the opening '!'
 * character:
 *
 * <ul>
 *
 * <li><b><code>\<</code></b>
 *
 * Align the image to the left (causes the image to float if
 * CSS options are enabled).</li>
 *
 * <li><b><code>\></code></b>
 *
 * Align the image to the right (causes the image to float if
 * CSS options are enabled).</li>
 *
 * <li><b><code>-</code> (dash)</b>
 *
 * Aligns the image to the middle.</li>
 *
 * <li><b><code>^</code></b>
 *
 * Aligns the image to the top.</li>
 *
 * <li><b><code>~</code> (tilde)</b>
 *
 * Aligns the image to the bottom.</li>
 *
 * <li><b><code>{style rule}</code></b>
 *
 * Applies a CSS style rule to the image.</li>
 *
 * <li><b><code>(class)</code> or <code>(#id)</code> or <code>(class#id)</code></b>
 *
 * Applies a CSS class and/or id to the image.</li>
 *
 * <li><b><code>(</code> (one or more)</b>
 *
 * Pads 1em on the left for each '(' character.</li>
 *
 * <li><b><code>)</code> (one or more)</b>
 *
 * Pads 1em on the right for each ')' character.</li>
 *
 * </ul>
 *
 * @subsection characterreplacements Character Replacements
 *
 * A few simple, common symbols are automatically replaced:
 * <pre>
 *     (c)
 *     (r)
 *     (tm)
 * </pre>
 * In addition to these, there are a whole set of character
 * macros that are defined by default. All macros are enclosed
 * in curly braces. These include:
 * <pre>
 *     {c|} or {|c} cent sign
 *     {L-} or {-L} pound sign
 *     {Y=} or {=Y} yen sign
 * </pre>
 * Many of these macros can be guessed. For example:
 * <pre>
 *     {A'} or {'A}
 *     {a"} or {"a}
 *     {1/4}
 *     {*}
 *     {:)}
 *     {:(}
 * </pre>
 * @subsection lists Lists
 *
 * Textile also supports ordered and unordered lists.
 * You simply place an asterisk or pound sign, followed
 * with a space at the start of your lines.
 *
 * Simple lists:
 * <pre>
 *     * one
 *     * two
 *     * three
 * </pre>
 * Multi-level lists:
 * <pre>
 *     * one
 *     ** one A
 *     ** one B
 *     *** one B1
 *     * two
 *     ** two A
 *     ** two B
 *     * three
 * </pre>
 * Ordered lists:
 * <pre>
 *     # one
 *     # two
 *     # three
 * </pre>
 * Styling lists:
 * <pre>
 *     (class#id)* one
 *     * two
 *     * three
 * </pre>
 * The above sets the class and id attributes for the \<ul\>
 * tag.
 * <pre>
 *     *(class#id) one
 *     * two
 *     * three
 * </pre>
 * The above sets the class and id attributes for the first \<li\>
 * tag.
 *
 * Definition lists:
 * <pre>
 *     dl. textile:a cloth, especially one manufactured by weaving
 *     or knitting; a fabric
 *     format:the arrangement of data for storage or display.
 * </pre>
 * Note that there is no space between the term and definition. The
 * term must be at the start of the line (or following the "dl"
 * signature as shown above).
 *
 * @subsection tables Tables
 *
 * Textile supports tables. Tables must be in their own block and
 * must have pipe characters delimiting the columns. An optional
 * block signature of "table" may be used, usually for applying
 * style, class, id or other options to the table element itself.
 *
 * From the simple:
 * <pre>
 *     |a|b|c|
 *     |1|2|3|
 * </pre>
 * To the complex:
 * <pre>
 *     table(fig). {color:red}_|Top|Row|
 *     {color:blue}|/2. Second|Row|
 *     |_{color:green}. Last|
 * </pre>
 * Modifiers can be specified for the table signature itself,
 * for a table row (prior to the first '|' character) and
 * for any cell (following the '|' for that cell). Note that for
 * cells, a period followed with a space must be placed after
 * any modifiers to distinguish the modifier from the cell content.
 *
 * Modifiers allowed are:
 *
 * <ul>
 *
 * <li><b><code>{style rule}</code></b>
 *
 * A CSS style rule.</li>
 *
 * <li><b><code>(class)</code> or <code>(#id)</code> or <code>(class#id)</code></b>
 *
 * A CSS class and/or id attribute.</li>
 *
 * <li><b><code>(</code> (one or more)</b>
 *
 * Adds 1em of padding to the left for each '(' character.</li>
 *
 * <li><b><code>)</code> (one or more)</b>
 *
 * Adds 1em of padding to the right for each ')' character.</li>
 *
 * <li><b><code>\<</code></b>
 *
 * Aligns to the left (floats to left for tables if combined with the
 * ')' modifier).</li>
 *
 * <li><b><code>\></code></b>
 *
 * Aligns to the right (floats to right for tables if combined with
 * the '(' modifier).</li>
 *
 * <li><b><code>=</code></b>
 *
 * Aligns to center (sets left, right margins to 'auto' for tables).</li>
 *
 * <li><b><code>\<\></code></b>
 *
 * For cells only. Justifies text.</li>
 *
 * <li><b><code>^</code></b>
 *
 * For rows and cells only. Aligns to the top.</li>
 *
 * <li><b><code>~</code> (tilde)</b>
 *
 * For rows and cells only. Aligns to the bottom.</li>
 *
 * <li><b><code>_</code> (underscore)</b>
 *
 * Can be applied to a table row or cell to indicate a header
 * row or cell.</li>
 *
 * <li><b><code>\\2</code> or <code>\\3</code> or <code>\\4</code>, etc.</b>
 *
 * Used within cells to indicate a colspan of 2, 3, 4, etc. columns.
 * When you see "\\", think "push forward".</li>
 *
 * <li><b><code>/2</code> or <code>/3</code> or <code>/4</code>, etc.</b>
 *
 * Used within cells to indicate a rowspan of 2, 3, 4, etc. rows.
 * When you see "/", think "push downward".</li>
 *
 * </ul>
 *
 * When a cell is identified as a header cell and an alignment
 * is specified, that becomes the default alignment for
 * cells below it. You can always override this behavior by
 * specifying an alignment for one of the lower cells.
 *
 * @subsection cssnotes CSS Notes
 *
 * When CSS is enabled (and it is by default), CSS class names
 * are automatically applied in certain situations.
 *
 * <ul>
 *
 * <li>Aligning a block or span or other element to
 * left, right, etc.
 *
 * "left" for left justified, "right" for right justified,
 * "center" for centered text, "justify" for full-justified
 * text.</li>
 *
 * <li>Aligning an image to the top or bottom
 *
 * "top" for top alignment, "bottom" for bottom alignment,
 * "middle" for middle alignment.</li>
 *
 * <li>Footnotes
 *
 * "footnote" is applied to the paragraph tag for the
 * footnote text itself. An id of "fn" plus the footnote
 * number is placed on the paragraph for the footnote as
 * well. For the footnote superscript tag, a class of
 * "footnote" is used.</li>
 *
 * <li>Capped text
 *
 * For a series of characters that are uppercased, a
 * span is placed around them with a class of "caps".</li>
 *
 * </ul>
 *
 * @subsection miscellaneous Miscellaneous
 *
 * Textile tries to do it's very best to ensure proper XHTML
 * syntax. It will even attempt to fix errors you may introduce
 * writing in HTML yourself. Unescaped '&' characters within
 * URLs will be properly escaped. Singlet tags such as br, img
 * and hr are checked for the '/' terminator (and it's added
 * if necessary). The best way to make sure you produce valid
 * XHTML with Textile is to not use any HTML markup at all--
 * use the Textile syntax and let it produce the markup for you.
 *
 * @section license LICENSE
 *
 * Text::Textile is licensed under the same terms as Perl
 * itself. Textile.php is licensed under the terms of the GNU General
 * Public License.
 *
 * @section authorandcopyright AUTHOR & COPYRIGHT
 *
 * Text::Textile was written by Brad Choate, \<brad at bradchoate dot com\>.
 * It is an adaptation of Textile, developed by Dean Allen of Textism.com.
 *
 * Textile.php is a PHP port of Brad Choate's Text::Textile
 * (Textile.pm) Perl module.
 *
 * Textile.php was ported by Jim Riggs \<textile at jimandlissa dot
 * com\>. Great care has been taken to leave the Perl code in much the
 * same form as Textile.pm. While changes were required due to
 * syntactical differences between Perl and PHP, much of the code was
 * left intact (even if alternative syntax or code optimizations could
 * have been made in PHP), even to the point where one can compare
 * functions/subroutines side by side between the two implementations.
 * This has been done to ensure compatibility, reduce the possibility
 * of introducing errors, and simplify maintainance as one version or
 * the other is updated.
 *
 * @author Jim Riggs \<textile at jimandlissa dot com\>
 * @author Brad Choate \<brad at bradchoate dot com\>
 * @copyright Copyright &copy; 2004 Jim Riggs and Brad Choate
 * @version @(#) $Id: Textile.php,v 1.5 2005/03/21 15:33:01 jhriggs Exp $
 */
?>
