[ Index ]
 

Code source de DokuWiki 2006-11-06

Accédez au Source d'autres logiciels libresSoutenez Angelica Josefina !

title

Body

[fermer]

/inc/parser/ -> lexer.php (source)

   1  <?php
   2  /**
   3  * Author Markus Baker: http://www.lastcraft.com
   4  * Version adapted from Simple Test: http://sourceforge.net/projects/simpletest/
   5  * For an intro to the Lexer see:
   6  * http://www.phppatterns.com/index.php/article/articleview/106/1/2/
   7  * @author Marcus Baker
   8  * @package Doku
   9  * @subpackage Lexer
  10  * @version $Id: lexer.php,v 1.1 2005/03/23 23:14:09 harryf Exp $
  11  */
  12  
  13  /**
  14  * Init path constant
  15  */
  16  if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/');
  17  
  18  /**#@+
  19   * lexer mode constant
  20   */
  21  define("DOKU_LEXER_ENTER", 1);
  22  define("DOKU_LEXER_MATCHED", 2);
  23  define("DOKU_LEXER_UNMATCHED", 3);
  24  define("DOKU_LEXER_EXIT", 4);
  25  define("DOKU_LEXER_SPECIAL", 5);
  26  /**#@-*/
  27  
  28  /**
  29   *    Compounded regular expression. Any of
  30   *    the contained patterns could match and
  31   *    when one does it's label is returned.
  32   *    @package Doku
  33   *    @subpackage Lexer
  34   */
  35  class Doku_LexerParallelRegex {
  36      var $_patterns;
  37      var $_labels;
  38      var $_regex;
  39      var $_case;
  40  
  41      /**
  42       *    Constructor. Starts with no patterns.
  43       *    @param boolean $case    True for case sensitive, false
  44       *                            for insensitive.
  45       *    @access public
  46       */
  47      function Doku_LexerParallelRegex($case) {
  48          $this->_case = $case;
  49          $this->_patterns = array();
  50          $this->_labels = array();
  51          $this->_regex = null;
  52      }
  53  
  54      /**
  55       *    Adds a pattern with an optional label.
  56       *    @param mixed $pattern       Perl style regex. Must be UTF-8
  57       *                                encoded. If its a string, the (, )
  58       *                                lose their meaning unless they
  59       *                                form part of a lookahead or
  60       *                                lookbehind assertation.
  61       *    @param string $label        Label of regex to be returned
  62       *                                on a match. Label must be ASCII
  63       *    @access public
  64       */
  65      function addPattern($pattern, $label = true) {
  66          $count = count($this->_patterns);
  67          $this->_patterns[$count] = $pattern;
  68          $this->_labels[$count] = $label;
  69          $this->_regex = null;
  70      }
  71  
  72      /**
  73       *    Attempts to match all patterns at once against
  74       *    a string.
  75       *    @param string $subject      String to match against.
  76       *    @param string $match        First matched portion of
  77       *                                subject.
  78       *    @return boolean             True on success.
  79       *    @access public
  80       */
  81      function match($subject, &$match) {
  82          if (count($this->_patterns) == 0) {
  83              return false;
  84          }
  85          if (! preg_match($this->_getCompoundedRegex(), $subject, $matches)) {
  86              $match = "";
  87              return false;
  88          }
  89  
  90          $match = $matches[0];
  91          $size = count($matches);
  92          for ($i = 1; $i < $size; $i++) {
  93              if ($matches[$i] && isset($this->_labels[$i - 1])) {
  94                  return $this->_labels[$i - 1];
  95              }
  96          }
  97          return true;
  98      }
  99  
 100      /**
 101       *    Attempts to split the string against all patterns at once
 102       *
 103       *    @param string $subject      String to match against.
 104       *    @param array $split         The split result: array containing, pre-match, match & post-match strings
 105       *    @return boolean             True on success.
 106       *    @access public
 107       *
 108       *    @author Christopher Smith <chris@jalakai.co.uk>
 109       */
 110      function split($subject, &$split) {
 111          if (count($this->_patterns) == 0) {
 112              return false;
 113          }
 114  
 115          if (! preg_match($this->_getCompoundedRegex(), $subject, $matches)) {
 116              $split = array($subject, "", "");
 117              return false;
 118          }
 119  
 120          $idx = count($matches)-2;
 121  
 122          list($pre, $post) = preg_split($this->_patterns[$idx].$this->_getPerlMatchingFlags(), $subject, 2);
 123  
 124          $split = array($pre, $matches[0], $post);
 125          return isset($this->_labels[$idx]) ? $this->_labels[$idx] : true;
 126      }
 127  
 128      /**
 129       *    Compounds the patterns into a single
 130       *    regular expression separated with the
 131       *    "or" operator. Caches the regex.
 132       *    Will automatically escape (, ) and / tokens.
 133       *    @param array $patterns    List of patterns in order.
 134       *    @access private
 135       */
 136      function _getCompoundedRegex() {
 137          if ($this->_regex == null) {
 138              $cnt = count($this->_patterns);
 139              for ($i = 0; $i < $cnt; $i++) {
 140  
 141                  // Replace lookaheads / lookbehinds with marker
 142                  $m = "\1\1";
 143                  $pattern = preg_replace(
 144                          array (
 145                              '/\(\?(i|m|s|x|U)\)/U',
 146                              '/\(\?(\-[i|m|s|x|U])\)/U',
 147                              '/\(\?\=(.*)\)/sU',
 148                              '/\(\?\!(.*)\)/sU',
 149                              '/\(\?\<\=(.*)\)/sU',
 150                              '/\(\?\<\!(.*)\)/sU',
 151                              '/\(\?\:(.*)\)/sU',
 152                          ),
 153                          array (
 154                              $m.'SO:\\1'.$m,
 155                              $m.'SOR:\\1'.$m,
 156                              $m.'LA:IS:\\1'.$m,
 157                              $m.'LA:NOT:\\1'.$m,
 158                              $m.'LB:IS:\\1'.$m,
 159                              $m.'LB:NOT:\\1'.$m,
 160                              $m.'GRP:\\1'.$m,
 161                          ),
 162                          $this->_patterns[$i]
 163                      );
 164                  // Quote the rest
 165                  $pattern = str_replace(
 166                      array('/', '(', ')'),
 167                      array('\/', '\(', '\)'),
 168                      $pattern
 169                      );
 170  
 171                  // Restore lookaheads / lookbehinds
 172                  $pattern = preg_replace(
 173                          array (
 174                              '/'.$m.'SO:(.{1})'.$m.'/',
 175                              '/'.$m.'SOR:(.{2})'.$m.'/',
 176                              '/'.$m.'LA:IS:(.*)'.$m.'/sU',
 177                              '/'.$m.'LA:NOT:(.*)'.$m.'/sU',
 178                              '/'.$m.'LB:IS:(.*)'.$m.'/sU',
 179                              '/'.$m.'LB:NOT:(.*)'.$m.'/sU',
 180                              '/'.$m.'GRP:(.*)'.$m.'/sU',
 181                          ),
 182                          array (
 183                              '(?\\1)',
 184                              '(?\\1)',
 185                              '(?=\\1)',
 186                              '(?!\\1)',
 187                              '(?<=\\1)',
 188                              '(?<!\\1)',
 189                              '(?:\\1)',
 190                          ),
 191                          $pattern
 192                  );
 193  
 194                  $this->_patterns[$i] = '('.$pattern.')';
 195              }
 196              $this->_regex = "/" . implode("|", $this->_patterns) . "/" . $this->_getPerlMatchingFlags();
 197          }
 198          return $this->_regex;
 199      }
 200  
 201      /**
 202       *    Accessor for perl regex mode flags to use.
 203       *    @return string       Perl regex flags.
 204       *    @access private
 205       */
 206      function _getPerlMatchingFlags() {
 207          return ($this->_case ? "msS" : "msSi");
 208      }
 209  }
 210  
 211  /**
 212   *    States for a stack machine.
 213   *    @package Lexer
 214   *    @subpackage Lexer
 215   */
 216  class Doku_LexerStateStack {
 217      var $_stack;
 218  
 219      /**
 220       *    Constructor. Starts in named state.
 221       *    @param string $start        Starting state name.
 222       *    @access public
 223       */
 224      function Doku_LexerStateStack($start) {
 225          $this->_stack = array($start);
 226      }
 227  
 228      /**
 229       *    Accessor for current state.
 230       *    @return string       State.
 231       *    @access public
 232       */
 233      function getCurrent() {
 234          return $this->_stack[count($this->_stack) - 1];
 235      }
 236  
 237      /**
 238       *    Adds a state to the stack and sets it
 239       *    to be the current state.
 240       *    @param string $state        New state.
 241       *    @access public
 242       */
 243      function enter($state) {
 244          array_push($this->_stack, $state);
 245      }
 246  
 247      /**
 248       *    Leaves the current state and reverts
 249       *    to the previous one.
 250       *    @return boolean    False if we drop off
 251       *                       the bottom of the list.
 252       *    @access public
 253       */
 254      function leave() {
 255          if (count($this->_stack) == 1) {
 256              return false;
 257          }
 258          array_pop($this->_stack);
 259          return true;
 260      }
 261  }
 262  
 263  /**
 264   *    Accepts text and breaks it into tokens.
 265   *    Some optimisation to make the sure the
 266   *    content is only scanned by the PHP regex
 267   *    parser once. Lexer modes must not start
 268   *    with leading underscores.
 269   *    @package Doku
 270   *    @subpackage Lexer
 271   */
 272  class Doku_Lexer {
 273      var $_regexes;
 274      var $_parser;
 275      var $_mode;
 276      var $_mode_handlers;
 277      var $_case;
 278  
 279      /**
 280       *    Sets up the lexer in case insensitive matching
 281       *    by default.
 282       *    @param Doku_Parser $parser  Handling strategy by
 283       *                                    reference.
 284       *    @param string $start            Starting handler.
 285       *    @param boolean $case            True for case sensitive.
 286       *    @access public
 287       */
 288      function Doku_Lexer(&$parser, $start = "accept", $case = false) {
 289          $this->_case = $case;
 290          $this->_regexes = array();
 291          $this->_parser = &$parser;
 292          $this->_mode = &new Doku_LexerStateStack($start);
 293          $this->_mode_handlers = array();
 294      }
 295  
 296      /**
 297       *    Adds a token search pattern for a particular
 298       *    parsing mode. The pattern does not change the
 299       *    current mode.
 300       *    @param string $pattern      Perl style regex, but ( and )
 301       *                                lose the usual meaning.
 302       *    @param string $mode         Should only apply this
 303       *                                pattern when dealing with
 304       *                                this type of input.
 305       *    @access public
 306       */
 307      function addPattern($pattern, $mode = "accept") {
 308          if (! isset($this->_regexes[$mode])) {
 309              $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
 310          }
 311          $this->_regexes[$mode]->addPattern($pattern);
 312      }
 313  
 314      /**
 315       *    Adds a pattern that will enter a new parsing
 316       *    mode. Useful for entering parenthesis, strings,
 317       *    tags, etc.
 318       *    @param string $pattern      Perl style regex, but ( and )
 319       *                                lose the usual meaning.
 320       *    @param string $mode         Should only apply this
 321       *                                pattern when dealing with
 322       *                                this type of input.
 323       *    @param string $new_mode     Change parsing to this new
 324       *                                nested mode.
 325       *    @access public
 326       */
 327      function addEntryPattern($pattern, $mode, $new_mode) {
 328          if (! isset($this->_regexes[$mode])) {
 329              $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
 330          }
 331          $this->_regexes[$mode]->addPattern($pattern, $new_mode);
 332      }
 333  
 334      /**
 335       *    Adds a pattern that will exit the current mode
 336       *    and re-enter the previous one.
 337       *    @param string $pattern      Perl style regex, but ( and )
 338       *                                lose the usual meaning.
 339       *    @param string $mode         Mode to leave.
 340       *    @access public
 341       */
 342      function addExitPattern($pattern, $mode) {
 343          if (! isset($this->_regexes[$mode])) {
 344              $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
 345          }
 346          $this->_regexes[$mode]->addPattern($pattern, "__exit");
 347      }
 348  
 349      /**
 350       *    Adds a pattern that has a special mode. Acts as an entry
 351       *    and exit pattern in one go, effectively calling a special
 352       *    parser handler for this token only.
 353       *    @param string $pattern      Perl style regex, but ( and )
 354       *                                lose the usual meaning.
 355       *    @param string $mode         Should only apply this
 356       *                                pattern when dealing with
 357       *                                this type of input.
 358       *    @param string $special      Use this mode for this one token.
 359       *    @access public
 360       */
 361      function addSpecialPattern($pattern, $mode, $special) {
 362          if (! isset($this->_regexes[$mode])) {
 363              $this->_regexes[$mode] = new Doku_LexerParallelRegex($this->_case);
 364          }
 365          $this->_regexes[$mode]->addPattern($pattern, "_$special");
 366      }
 367  
 368      /**
 369       *    Adds a mapping from a mode to another handler.
 370       *    @param string $mode        Mode to be remapped.
 371       *    @param string $handler     New target handler.
 372       *    @access public
 373       */
 374      function mapHandler($mode, $handler) {
 375          $this->_mode_handlers[$mode] = $handler;
 376      }
 377  
 378      /**
 379       *    Splits the page text into tokens. Will fail
 380       *    if the handlers report an error or if no
 381       *    content is consumed. If successful then each
 382       *    unparsed and parsed token invokes a call to the
 383       *    held listener.
 384       *    @param string $raw        Raw HTML text.
 385       *    @return boolean           True on success, else false.
 386       *    @access public
 387       */
 388      function parse($raw) {
 389          if (! isset($this->_parser)) {
 390              return false;
 391          }
 392          $initialLength = strlen($raw);
 393          $length = $initialLength;
 394          $pos = 0;
 395          while (is_array($parsed = $this->_reduce($raw))) {
 396              list($unmatched, $matched, $mode) = $parsed;
 397              $currentLength = strlen($raw);
 398              $matchPos = $initialLength - $currentLength - strlen($matched);
 399              if (! $this->_dispatchTokens($unmatched, $matched, $mode, $pos, $matchPos)) {
 400                  return false;
 401              }
 402              if ($currentLength == $length) {
 403                  return false;
 404              }
 405              $length = $currentLength;
 406              $pos = $initialLength - $currentLength;
 407          }
 408          if (!$parsed) {
 409              return false;
 410          }
 411          return $this->_invokeParser($raw, DOKU_LEXER_UNMATCHED, $pos);
 412      }
 413  
 414      /**
 415       *    Sends the matched token and any leading unmatched
 416       *    text to the parser changing the lexer to a new
 417       *    mode if one is listed.
 418       *    @param string $unmatched    Unmatched leading portion.
 419       *    @param string $matched      Actual token match.
 420       *    @param string $mode         Mode after match. A boolean
 421       *                                false mode causes no change.
 422       *    @param int $pos         Current byte index location in raw doc
 423       *                                thats being parsed
 424       *    @return boolean             False if there was any error
 425       *                                from the parser.
 426       *    @access private
 427       */
 428      function _dispatchTokens($unmatched, $matched, $mode = false, $initialPos, $matchPos) {
 429          if (! $this->_invokeParser($unmatched, DOKU_LEXER_UNMATCHED, $initialPos) ){
 430              return false;
 431          }
 432          if ($this->_isModeEnd($mode)) {
 433              if (! $this->_invokeParser($matched, DOKU_LEXER_EXIT, $matchPos)) {
 434                  return false;
 435              }
 436              return $this->_mode->leave();
 437          }
 438          if ($this->_isSpecialMode($mode)) {
 439              $this->_mode->enter($this->_decodeSpecial($mode));
 440              if (! $this->_invokeParser($matched, DOKU_LEXER_SPECIAL, $matchPos)) {
 441                  return false;
 442              }
 443              return $this->_mode->leave();
 444          }
 445          if (is_string($mode)) {
 446              $this->_mode->enter($mode);
 447              return $this->_invokeParser($matched, DOKU_LEXER_ENTER, $matchPos);
 448          }
 449          return $this->_invokeParser($matched, DOKU_LEXER_MATCHED, $matchPos);
 450      }
 451  
 452      /**
 453       *    Tests to see if the new mode is actually to leave
 454       *    the current mode and pop an item from the matching
 455       *    mode stack.
 456       *    @param string $mode    Mode to test.
 457       *    @return boolean        True if this is the exit mode.
 458       *    @access private
 459       */
 460      function _isModeEnd($mode) {
 461          return ($mode === "__exit");
 462      }
 463  
 464      /**
 465       *    Test to see if the mode is one where this mode
 466       *    is entered for this token only and automatically
 467       *    leaves immediately afterwoods.
 468       *    @param string $mode    Mode to test.
 469       *    @return boolean        True if this is the exit mode.
 470       *    @access private
 471       */
 472      function _isSpecialMode($mode) {
 473          return (strncmp($mode, "_", 1) == 0);
 474      }
 475  
 476      /**
 477       *    Strips the magic underscore marking single token
 478       *    modes.
 479       *    @param string $mode    Mode to decode.
 480       *    @return string         Underlying mode name.
 481       *    @access private
 482       */
 483      function _decodeSpecial($mode) {
 484          return substr($mode, 1);
 485      }
 486  
 487      /**
 488       *    Calls the parser method named after the current
 489       *    mode. Empty content will be ignored. The lexer
 490       *    has a parser handler for each mode in the lexer.
 491       *    @param string $content        Text parsed.
 492       *    @param boolean $is_match      Token is recognised rather
 493       *                                  than unparsed data.
 494       *    @param int $pos         Current byte index location in raw doc
 495       *                                thats being parsed
 496       *    @access private
 497       */
 498      function _invokeParser($content, $is_match, $pos) {
 499          if (($content === "") || ($content === false)) {
 500              return true;
 501          }
 502          $handler = $this->_mode->getCurrent();
 503          if (isset($this->_mode_handlers[$handler])) {
 504              $handler = $this->_mode_handlers[$handler];
 505          }
 506  
 507          // modes starting with plugin_ are all handled by the same
 508          // handler but with an additional parameter
 509          if(substr($handler,0,7)=='plugin_'){
 510            list($handler,$plugin) = split('_',$handler,2);
 511                return $this->_parser->$handler($content, $is_match, $pos, $plugin);
 512          }
 513  
 514              return $this->_parser->$handler($content, $is_match, $pos);
 515          }
 516  
 517      /**
 518       *    Tries to match a chunk of text and if successful
 519       *    removes the recognised chunk and any leading
 520       *    unparsed data. Empty strings will not be matched.
 521       *    @param string $raw         The subject to parse. This is the
 522       *                               content that will be eaten.
 523       *    @return array              Three item list of unparsed
 524       *                               content followed by the
 525       *                               recognised token and finally the
 526       *                               action the parser is to take.
 527       *                               True if no match, false if there
 528       *                               is a parsing error.
 529       *    @access private
 530       */
 531      function _reduce(&$raw) {
 532          if (! isset($this->_regexes[$this->_mode->getCurrent()])) {
 533              return false;
 534          }
 535          if ($raw === "") {
 536              return true;
 537          }
 538          if ($action = $this->_regexes[$this->_mode->getCurrent()]->split($raw, $split)) {
 539              list($unparsed, $match, $raw) = $split;
 540              return array($unparsed, $match, $action);
 541          }
 542          return true;
 543      }
 544  }
 545  
 546  /**
 547  * Escapes regex characters other than (, ) and /
 548  * @TODO
 549  */
 550  function Doku_Lexer_Escape($str) {
 551      //$str = addslashes($str);
 552      $chars = array(
 553          '/\\\\/',
 554          '/\./',
 555          '/\+/',
 556          '/\*/',
 557          '/\?/',
 558          '/\[/',
 559          '/\^/',
 560          '/\]/',
 561          '/\$/',
 562          '/\{/',
 563          '/\}/',
 564          '/\=/',
 565          '/\!/',
 566          '/\</',
 567          '/\>/',
 568          '/\|/',
 569          '/\:/'
 570          );
 571  
 572      $escaped = array(
 573          '\\\\\\\\',
 574          '\.',
 575          '\+',
 576          '\*',
 577          '\?',
 578          '\[',
 579          '\^',
 580          '\]',
 581          '\$',
 582          '\{',
 583          '\}',
 584          '\=',
 585          '\!',
 586          '\<',
 587          '\>',
 588          '\|',
 589          '\:'
 590          );
 591      return preg_replace($chars, $escaped, $str);
 592  }
 593  
 594  //Setup VIM: ex: et ts=4 enc=utf-8 :


Généré le : Tue Apr 3 20:47:31 2007 par Balluche grâce à PHPXref 0.7