      1: <?php
      2: /*************************************************************************************/
      3: /*                                                                                   */
      4: /*      Thelia                                                                       */
      5: /*                                                                                   */
      6: /*      Copyright (c) OpenStudio                                                     */
      7: /*  email : info@thelia.net                                                      */
      8: /*      web : http://www.thelia.net                                                  */
      9: /*                                                                                   */
     10: /*      This program is free software; you can redistribute it and/or modify         */
     11: /*      it under the terms of the GNU General Public License as published by         */
     12: /*      the Free Software Foundation; either version 3 of the License                */
     13: /*                                                                                   */
     14: /*      This program is distributed in the hope that it will be useful,              */
     15: /*      but WITHOUT ANY WARRANTY; without even the implied warranty of               */
     16: /*      MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                */
     17: /*      GNU General Public License for more details.                                 */
     18: /*                                                                                   */
     19: /*      You should have received a copy of the GNU General Public License            */
     20: /*      along with this program. If not, see <http://www.gnu.org/licenses/>.     */
     21: /*                                                                                   */
     22: /*************************************************************************************/
     23:
     24: namespace Thelia\Tpex\Tokenizer;
     25:
     26: use Thelia\Tpex\TpexToken;
     27: use Thelia\Tpex\Element\ElementCollection;
     28: use Thelia\Tpex\Element\SimpleLoopElement;
     29: use Thelia\Tpex\Element\TextElement;
     30: use Thelia\Tpex\Element\RepeatLoopElement;
     31: use Thelia\Tpex\Element\ConditionalLoopElement;
     32: use Thelia\Tpex\Element\TestLoopElement;
     33: use Thelia\Tpex\Element\ConditionalVarLoopElement;
     34: use Thelia\Tpex\Element\VariablesTemplate;
     35: use Thelia\Tpex\Exception\TokenNotFoundException;
     36: use Thelia\Tpex\Exception\TokenSyntaxException;
     37:
     38: use Symfony\Component\HttpFoundation\Request;
     39: use Symfony\Component\EventDispatcher\EventDispatcherInterface;
     40:
     41: class Tokenize implements TokenizeInterface
     42: {
     43:     protected $in_comment;
     44:
     45:     /**
     46:      * @var \Symfony\Component\HttpFoundation\Request
     47:      */
     48:     protected $request;
     49:
     50:     /**
     51:      * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
     52:      */
     53:     protected $dispatcher;
     54:
     55:     /**
     56:      *
     57:      * @todo add filter here
     58:      *
     59:      * @var array $tags
     60:      */
     61:     protected $tags = array(
     62:         "THELIA_"   => TpexToken::OSL,
     63:         "/THELIA_"   => TpexToken::CSL,
     64:         "TEST_"     => TpexToken::OTL,
     65:         "/TEST_"    => TpexToken::ETL,
     66:         "//TEST_"   => TpexToken::CTL,
     67:         "T_"        => TpexToken::OCL,
     68:         "/T_"       => TpexToken::ECL,
     69:         "//T_"      => TpexToken::CCL,
     70:         "REM"       => TpexToken::OCM,
     71:         "/REM"      => TpexToken::CCM,
     72:         "REPEAT_"    => TpexToken::ORL,
     73:         "/REPEAT_"   => TpexToken::CRL,
     74:         "T:"        => TpexToken::OCVL,
     75:         "/T:"       => TpexToken::ECVL,
     76:         "//T:"      => TpexToken::CCVL
     77:     );
     78:
     79:     protected $tokens = array();
     80:
     81:     /**
     82:      * associative array containing information for loop execution
     83:      *
     84:      * key is loop name
     85:      * value is the class implementing/extending base loop classes
     86:      *
     87:      * ex :
     88:      *
     89:      * $loop = array(
     90:      *  "product" => "Thelia\Loop\Product",
     91:      *  "category" => "Thelia\Loop\Category",
     92:      *  "myLoop" => "My\Own\Loop"
     93:      * );
     94:      *
     95:      * @var Array
     96:      */
     97:     protected $loop = array();
     98:
     99:     /**
    100:      * associative array containing information for filter execution
    101:      *
    102:      * key is filter name
    103:      * value is the class implementing/extending base filter classes
    104:      *
    105:      * ex :
    106:      *
    107:      * $filter = array(
    108:      *  "upperCase" => "Thelia\Filter\UpperCase",
    109:      *  "lowerCase" => "Thelia\Filter\LowerCase",
    110:      *  "myFilter" => "My\Own\Filter"
    111:      * );
    112:      *
    113:      * @var Array
    114:      */
    115:     protected $filter = array();
    116:
    117:     /**
    118:      * associative array containing information for loop test execution
    119:      *
    120:      * key is test loop name
    121:      * value is the class implementing/extending base test loop classes
    122:      *
    123:      * ex :
    124:      *
    125:      * $testLoop = array(
    126:      *  "equal" => "Thelia\testLoop\Equal",
    127:      *  "myTestLoop" => "My\Own\TestLoop"
    128:      * );
    129:      *
    130:      * @var Array
    131:      */
    132:     protected $testLoop = array();
    133:
    134:     protected $loopIdStack = array();
    135:
    136:     protected $currentLoopStack = array();
    137:
    138:     public function __construct(Request $request, EventDispatcherInterface $dispatcher, $content = null)
    139:     {
    140:         if (is_null($content) === false) {
    141:             $this->setContent($content);
    142:         }
    143:
    144:         $this->dispatcher = $dispatcher;
    145:         $this->request = $request;
    146:     }
    147:
    148:     /**
    149:      *
    150:      * associative array containing information for loop execution
    151:      *
    152:      * key is loop name
    153:      * value is the class implementing/extending base loop classes
    154:      *
    155:      * ex :
    156:      *
    157:      * $loop = array(
    158:      *  "product" => "Thelia\Loop\Product",
    159:      *  "category" => "Thelia\Loop\Category",
    160:      *  "myLoop" => "My\Own\Loop"
    161:      * );
    162:      *
    163:      * @param  array                     $loops
    164:      * @throws \InvalidArgumentException if loop name already exists
    165:      */
    166:     public function setLoop(array $loops)
    167:     {
    168:         foreach ($loops as $name => $className) {
    169:             if (array_key_exists($name, $this->loop)) {
    170:                 throw new \InvalidArgumentException(sprintf("%s loop name already exists for %s class name", $name, $className));
    171:             }
    172:
    173:             $this->loop[$name] = $className;
    174:         }
    175:     }
    176:
    177:     /**
    178:      * @return array containing all loop. array's key are name loop and array value is the class of this loop
    179:      */
    180:     public function getLoop()
    181:     {
    182:         return $this->loop;
    183:     }
    184:
    185:     /**
    186:      * associative array containing information for filter execution
    187:      *
    188:      * key is filter name
    189:      * value is the class implementing/extending base loop classes
    190:      *
    191:      * ex :
    192:      *
    193:      * $filter = array(
    194:      *  "upperCase" => "Thelia\Filter\UpperCase",
    195:      *  "lowerCase" => "Thelia\Filter\LowerCase",
    196:      *  "myFilter" => "My\Own\Filter"
    197:      * );
    198:      *
    199:      * @param  array                     $filters
    200:      * @throws \InvalidArgumentException if filter name already exists
    201:      */
    202:     public function setFilter(array $filters)
    203:     {
    204:         foreach ($filters as $name => $className) {
    205:             if (array_key_exists($name, $this->filter)) {
    206:                 throw new \InvalidArgumentException(sprintf("%s filter name already exists for %s class name", $name, $className));
    207:             }
    208:
    209:             $this->filter[$name] = $className;
    210:         }
    211:     }
    212:
    213:     /**
    214:      * associative array containing information for loop test execution
    215:      *
    216:      * key is test loop name
    217:      * value is the class implementing/extending base test loop classes
    218:      *
    219:      * ex :
    220:      *
    221:      * $testLoop = array(
    222:      *  "equal" => "Thelia\testLoop\Equal",
    223:      *  "myTestLoop" => "My\Own\TestLoop"
    224:      * );
    225:      *
    226:      * @param  array                     $testLoops
    227:      * @throws \InvalidArgumentException
    228:      */
    229:     public function setTestLoop(array $testLoops)
    230:     {
    231:         foreach ($testLoops as $name => $className) {
    232:             if (array_key_exists($name, $this->filter)) {
    233:                 throw new \InvalidArgumentException(sprintf("%s test loop name already exists for %s class name",
    234:                     $name, $className));
    235:             }
    236:
    237:             $this->testLoop[$name] = $className;
    238:         }
    239:     }
    240:
    241:     public function getTestLoop()
    242:     {
    243:         return $this->testLoop;
    244:     }
    245:
    246:     public function getFilter()
    247:     {
    248:         return $this->filter;
    249:     }
    250:
    251:     public function setContent($content)
    252:     {
    253:         $this->tokens = preg_split("/( |\t|\n|\r|<|>|\\#|\")/", $content, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
    254:
    255:         // push blank at array start for using next in parseContent method
    256:         array_unshift($this->tokens, '');
    257:     }
    258:
    259:     /**
    260:      * @return \Thelia\Tpex\Element|ElementCollection
    261:      */
    262:     public function tokenize()
    263:     {
    264:         VariablesTemplate::init($this->getFilter(), $this->request);
    265:         $compileContent = $this->parseContent();
    266:
    267:         return $compileContent;
    268:     }
    269:
    270:     /**
    271:      * @return \Thelia\Tpex\Element\ElementCollection
    272:      */
    273:     protected function parseContent()
    274:     {
    275:         $collection = new ElementCollection();
    276:
    277:         while (true) {
    278:             $token = next($this->tokens);
    279:
    280:             if ($token === false) {
    281:
    282:                 break;
    283:             }
    284:
    285:             $res = $this->processToken($token);
    286:
    287:             if (empty($res))
    288:                 continue;
    289:             else if ($res == 'stop')
    290:                 break;
    291:             else
    292:                 $collection->addElement($res);
    293:         }
    294:
    295:         return $collection;
    296:     }
    297:
    298:     protected function parseArgs()
    299:     {
    300:         $args = "";
    301:
    302:         $in_quote = false;
    303:
    304:         while (1) {
    305:             $tok = next($this->tokens);
    306:
    307:             if ($tok == '#') { //we are in a value case
    308:                 $token = next($this->tokens);
    309:
    310:                 if (preg_match('/([A-Z0-9_]+)/', $token, $varname) > 0) {
    311:                     $tok = '#'.$varname[1];
    312:
    313:                 }
    314:
    315:                 $this->addVar($tok);
    316:
    317:                 // Il faut placer dans les args la valeur originale (sinon bug. Ex: #PROMO[X][Y])
    318:                 $tok = '#'.$token;
    319:             } elseif ($tok == '"') {
    320:                 $in_quote = ! $in_quote;
    321:             } elseif ($tok == '>') {
    322:                 if (! $in_quote) break;
    323:             } elseif ($tok === FALSE) {
    324:                 break;
    325:             }
    326:
    327:             $args .= $tok;
    328:         }
    329:
    330:         return $args;
    331:     }
    332:
    333:     protected function addVar($token)
    334:     {
    335:         // Ne pas prendre en compte les filtres et les get/set
    336:         if (strstr($token, 'GET{') !== FALSE || strstr($token, 'SET{') !== FALSE) return;
    337:
    338:         $count = count($this->currentLoopStack);
    339:
    340:         //there is already no loop open
    341:         if($count == 0) return;
    342:
    343:         $currentLoop = $this->currentLoopStack[$count-1];
    344:
    345:         //current loop is not a simple loop so we don't have to save variable
    346:         if($currentLoop->type() != TpexToken::TPEX_SIMPLE_LOOP) return;
    347:
    348:         if (preg_match('/([A-Z0-9_]+)/', $token, $varname) > 0) {
    349:                 $currentLoop->addVar($varname[1]);
    350:         }
    351:     }
    352:
    353:     protected function processToken($atoken)
    354:     {
    355:         $token_type = TpexToken::TXT;
    356:         $token = '';
    357:
    358:         if ($atoken == '<') {
    359:             $no_match = false;
    360:
    361:             $token = next($this->tokens);
    362:
    363:             if ($token[0] != 'T' && strpos($token, '/T') !== 0 && strpos($token, '//T') !== 0 && $token[0] != 'R' && strpos($token, '/R') !== 0) {
    364:                 $no_match = true;
    365:             } else {
    366:                 try {
    367:                     $token_type = $this->findTokenType($token);
    368:                 } catch (TokenNotFoundException $e) {
    369:                     $no_match = true;
    370:                 }
    371:             }
    372:
    373:             if ($no_match) {
    374:                 prev($this->tokens);
    375:
    376:                 $token = $atoken;
    377:             }
    378:         } elseif ($atoken == "#") {
    379:             //similar case as ##REF
    380:             $tmp = next($this->tokens);
    381:
    382:             if ($tmp == '#') {
    383:                 $token = '#';
    384:
    385:                 prev($this->tokens);
    386:             } else {
    387:                 $token = '#' . $tmp;
    388:
    389:                 $this->addVar($token);
    390:             }
    391:         } else {
    392:             $token = $atoken;
    393:         }
    394:
    395:         if ($this->in_comment && $token_type !== TpexToken::CCM) {
    396:            return;
    397:         }
    398:
    399:         if ($token_type === TpexToken::OSL) {
    400:             return  $this->initSimpleLoop($token);
    401:
    402:         } elseif ($token_type === TpexToken::CSL || $token_type === TpexToken::CRL) {
    403:             $this->controlEndLoop($token);
    404:
    405:             array_pop($this->currentLoopStack);
    406:
    407:             return 'stop';
    408:
    409:         } elseif ($token_type === TpexToken::ORL) {
    410:             return $this->initRepeatLoop($token);
    411:
    412:         } elseif ($token_type == TpexToken::OCL) {
    413:             return $this->initConditionalLoop($token);
    414:
    415:         } elseif ($token_type === TpexToken::OTL) {
    416:             return $this->initTestLoop($token);
    417:
    418:         } elseif ($token_type === TpexToken::OCM) {
    419:
    420:             $this->in_comment = true;
    421:
    422:             return;
    423:
    424:         } elseif ($token_type === TpexToken::OCVL) {
    425:             return $this->initConditionalVarLoop($token);
    426:         }else if (
    427:             $token_type === TpexToken::ECL ||
    428:             $token_type === TpexToken::CCL ||
    429:             $token_type === TpexToken::ETL ||
    430:             $token_type === TpexToken::CTL ||
    431:             $token_type === TpexToken::ECVL ||
    432:             $token_type === TpexToken::CCVL
    433: ) {
    434:
    435:             $this->controlEndLoop($token);
    436:
    437:             return "stop";
    438:
    439:         } elseif ($token_type === TpexToken::CCM) {
    440:             $this->in_comment = false;
    441:             $this->skipTo(">");
    442:
    443:             return ;
    444:         } elseif ($token !== '') {
    445:             return new TextElement($token);
    446:         } else {
    447:             return;
    448:         }
    449:     }
    450:
    451:     /**
    452:      *
    453:      * When end tag is found like /THELIA, Tpex control that this end tag
    454:      * is good and ended good loop.
    455:      *
    456:      * @param  string                                      $idLoop
    457:      * @throws \Thelia\Tpex\Exception\TokenSyntaxException
    458:      */
    459:     protected function controlEndLoop($idLoop)
    460:     {
    461:         $idStackLoop = array_pop($this->loopIdStack);
    462:
    463:         if ($idLoop != "/".$idStackLoop) {
    464:             if ($idStackLoop == '') {
    465:                 throw new TokenSyntaxException(sprintf("no end tag found for %s loop"), $idLoop);
    466:             } else {
    467:                 throw new TokenSyntaxException(sprintf("%s loop found, /%s expected", $idLoop, $idStackLoop));
    468:             }
    469:         }
    470:     }
    471:
    472:     /**
    473:      *
    474:      * Initialize a simpleLoopElement with arguments and content.
    475:      *
    476:      * name is the seconde part of THELIA_xxx tag (ex : for THELIA_foo foo is the loop name).
    477:      *
    478:      * $token and \Thelia\Tpex\Element\SimpleLoopElement are push into self::loopIdStack and self::currentLoopStack
    479:      * respectively
    480:      *
    481:      * @param $token
    482:      * @return \Thelia\Tpex\Element\SimpleLoopElement
    483:      */
    484:     protected function initSimpleLoop($token)
    485:     {
    486:
    487:         $loop = new SimpleLoopElement(substr($token, 7), $this->loop, $this->request, $this->dispatcher);
    488:
    489:         array_push($this->loopIdStack, $token);
    490:
    491:         $loop->setArgs($this->parseArgs());
    492:
    493:         array_push($this->currentLoopStack, $loop);
    494:
    495:         $loop->addContent($this->parseContent());
    496:
    497:         $this->skipTo(">");
    498:
    499:         return $loop;
    500:     }
    501:
    502:     protected function initRepeatLoop($token)
    503:     {
    504:         $loop = new RepeatLoopElement(substr($token, 7));
    505:
    506:         array_push($this->loopIdStack, $token);
    507:
    508:         $loop->setArgs($this->parseArgs());
    509:
    510:         array_push($this->currentLoopStack, $loop);
    511:
    512:         $loop->addContent($this->parseContent());
    513:
    514:         $this->skipTo(">");
    515:
    516:         return $loop;
    517:     }
    518:
    519:     protected function initConditionalLoop($token)
    520:     {
    521:         $loop = new ConditionalLoopElement(substr($token, 2));
    522:         $this->skipTo(">");
    523:
    524:         array_push($this->loopIdStack, $token);
    525:
    526:         $loop->addContent($this->parseContent());
    527:         $this->skipTo(">");
    528:
    529:         array_push($this->loopIdStack, "/".$token);
    530:
    531:         $loop->addContent($this->parseContent());
    532:         $this->skipTo(">");
    533:
    534:         return $loop;
    535:     }
    536:
    537:     protected function initConditionalVarLoop($token)
    538:     {
    539:         $var = substr($token, 2);
    540:
    541:         $this->addVar($var);
    542:
    543:         $loop = new ConditionalVarLoopElement($var);
    544:         $this->skipTo(">");
    545:
    546:         array_push($this->loopIdStack, $token);
    547:
    548:         $loop->addContent($this->parseContent());
    549:         $this->skipTo(">");
    550:
    551:         array_push($this->loopIdStack, "/".$token);
    552:
    553:         $loop->addContent($this->parseContent());
    554:         $this->skipTo(">");
    555:
    556:         return $loop;
    557:     }
    558:
    559:     protected function initTestLoop($token)
    560:     {
    561:         $loop = new TestLoopElement(substr($token, 5), $this->testLoop, $this->request, $this->dispatcher);
    562:         $loop->setArgs($this->parseArgs());
    563:
    564:
    565:         array_push($this->loopIdStack, $token);
    566:
    567:         $loop->addContent($this->parseContent());
    568:         $this->skipTo(">");
    569:
    570:         array_push($this->loopIdStack, "/".$token);
    571:
    572:         $loop->addContent($this->parseContent());
    573:         $this->skipTo(">");
    574:
    575:         return $loop;
    576:     }
    577:
    578:     /**
    579:      *
    580:      * skip to the next element containing in $val
    581:      *
    582:      * @param $val search value
    583:      */
    584:     protected function skipTo($val)
    585:     {
    586:         $ret = false;
    587:
    588:         while ( ( ($tok = next($this->tokens)) !== false) && $tok != $val ) {
    589:             $ret = $tok;
    590:         }
    591:     }
    592:
    593:
    594:
    595:     /**
    596:      *
    597:      * This function search if tag is knowing by Tpex and return is int code.
    598:      *
    599:      * An exception is thrown if token is unknown by Tpex
    600:      *
    601:      * @param $token the token to find
    602:      * @return int
    603:      * @throws \Thelia\Tpex\Exception\TokenNotFoundException
    604:      */
    605:     protected function findTokenType($token)
    606:     {
    607:         $token_type = false;
    608:
    609:         if (strpos($token, 'THELIA_') === 0) {
    610:             $token_type = $this->tags['THELIA_'];
    611:         } elseif (strpos($token, '/THELIA_') === 0) {
    612:             $token_type = $this->tags['/THELIA_'];
    613:         } elseif (strpos($token, 'TEST_') === 0) {
    614:             $token_type = $this->tags['TEST_'];
    615:         } elseif (strpos($token, '/TEST_') === 0) {
    616:             $token_type = $this->tags['/TEST_'];
    617:         } elseif (strpos($token, '//TEST_') === 0) {
    618:             $token_type = $this->tags['//TEST_'];
    619:         } elseif (strpos($token, 'T_') === 0) {
    620:             $token_type = $this->tags['T_'];
    621:         } elseif (strpos($token, '/T_') === 0) {
    622:             $token_type = $this->tags['/T_'];
    623:         } elseif (strpos($token, '//T_') === 0) {
    624:             $token_type = $this->tags['//T_'];
    625:         } elseif (strpos($token, 'REM') === 0) {
    626:             $token_type = $this->tags['REM'];
    627:         } elseif (strpos($token, '/REM') === 0) {
    628:             $token_type = $this->tags['/REM'];
    629:         } elseif (strpos($token, 'REPEAT_') === 0) {
    630:             $token_type = $this->tags['REPEAT_'];
    631:         } elseif (strpos($token, '/REPEAT_') === 0) {
    632:             $token_type = $this->tags['/REPEAT_'];
    633:         } elseif (strpos($token, 'T:') === 0) {
    634:             $token_type = $this->tags['T:'];
    635:         } elseif (strpos($token, '/T:') === 0) {
    636:             $token_type = $this->tags['/T:'];
    637:         } elseif (strpos($token, '//T:') === 0) {
    638:             $token_type = $this->tags['//T:'];
    639:         } else {
    640:             throw new TokenNotFoundException("Token not found in Tpex tags");
    641:         }
    642:
    643:         return $token_type;
    644:
    645:     }
    646:
    647: }
    648: