MDL-58646 lib: import latest sabberworm/PHP-CSS-Parser
[moodle.git] / lib / php-css-parser / Parser.php
CommitLineData
fbe18cc0
FM
1<?php
2
3namespace Sabberworm\CSS;
4
5use Sabberworm\CSS\CSSList\CSSList;
6use Sabberworm\CSS\CSSList\Document;
7use Sabberworm\CSS\CSSList\KeyFrame;
8use Sabberworm\CSS\Parsing\SourceException;
9use Sabberworm\CSS\Property\AtRule;
10use Sabberworm\CSS\Property\Import;
11use Sabberworm\CSS\Property\Charset;
12use Sabberworm\CSS\Property\CSSNamespace;
13use Sabberworm\CSS\RuleSet\AtRuleSet;
14use Sabberworm\CSS\CSSList\AtRuleBlockList;
15use Sabberworm\CSS\RuleSet\DeclarationBlock;
16use Sabberworm\CSS\Value\CSSFunction;
17use Sabberworm\CSS\Value\RuleValueList;
18use Sabberworm\CSS\Value\Size;
19use Sabberworm\CSS\Value\Color;
20use Sabberworm\CSS\Value\URL;
21use Sabberworm\CSS\Value\CSSString;
22use Sabberworm\CSS\Rule\Rule;
23use Sabberworm\CSS\Parsing\UnexpectedTokenException;
24use Sabberworm\CSS\Comment\Comment;
25
26/**
27 * Parser class parses CSS from text into a data structure.
28 */
29class Parser {
30
31 private $sText;
32 private $aText;
33 private $iCurrentPosition;
34 private $oParserSettings;
35 private $sCharset;
36 private $iLength;
37 private $blockRules;
38 private $aSizeUnits;
39 private $iLineNo;
40
41 /**
42 * Parser constructor.
43 * Note that that iLineNo starts from 1 and not 0
44 *
45 * @param $sText
46 * @param Settings|null $oParserSettings
47 * @param int $iLineNo
48 */
49 public function __construct($sText, Settings $oParserSettings = null, $iLineNo = 1) {
50 $this->sText = $sText;
51 $this->iCurrentPosition = 0;
52 $this->iLineNo = $iLineNo;
53 if ($oParserSettings === null) {
54 $oParserSettings = Settings::create();
55 }
56 $this->oParserSettings = $oParserSettings;
57 $this->blockRules = explode('/', AtRule::BLOCK_RULES);
58
59 foreach (explode('/', Size::ABSOLUTE_SIZE_UNITS.'/'.Size::RELATIVE_SIZE_UNITS.'/'.Size::NON_SIZE_UNITS) as $val) {
60 $iSize = strlen($val);
61 if(!isset($this->aSizeUnits[$iSize])) {
62 $this->aSizeUnits[$iSize] = array();
63 }
64 $this->aSizeUnits[$iSize][strtolower($val)] = $val;
65 }
66 ksort($this->aSizeUnits, SORT_NUMERIC);
67 }
68
69 public function setCharset($sCharset) {
70 $this->sCharset = $sCharset;
71 $this->aText = $this->strsplit($this->sText);
72 $this->iLength = count($this->aText);
73 }
74
75 public function getCharset() {
76 return $this->sCharset;
77 }
78
79 public function parse() {
80 $this->setCharset($this->oParserSettings->sDefaultCharset);
81 $oResult = new Document($this->iLineNo);
82 $this->parseDocument($oResult);
83 return $oResult;
84 }
85
86 private function parseDocument(Document $oDocument) {
87 $this->parseList($oDocument, true);
88 }
89
90 private function parseList(CSSList $oList, $bIsRoot = false) {
91 while (!$this->isEnd()) {
92 $comments = $this->consumeWhiteSpace();
93 $oListItem = null;
94 if($this->oParserSettings->bLenientParsing) {
95 try {
96 $oListItem = $this->parseListItem($oList, $bIsRoot);
97 } catch (UnexpectedTokenException $e) {
98 $oListItem = false;
99 }
100 } else {
101 $oListItem = $this->parseListItem($oList, $bIsRoot);
102 }
103 if($oListItem === null) {
104 // List parsing finished
105 return;
106 }
107 if($oListItem) {
108 $oListItem->setComments($comments);
109 $oList->append($oListItem);
110 }
111 }
112 if (!$bIsRoot) {
113 throw new SourceException("Unexpected end of document", $this->iLineNo);
114 }
115 }
116
117 private function parseListItem(CSSList $oList, $bIsRoot = false) {
118 if ($this->comes('@')) {
119 $oAtRule = $this->parseAtRule();
120 if($oAtRule instanceof Charset) {
121 if(!$bIsRoot) {
122 throw new UnexpectedTokenException('@charset may only occur in root document', '', 'custom', $this->iLineNo);
123 }
124 if(count($oList->getContents()) > 0) {
125 throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $this->iLineNo);
126 }
127 $this->setCharset($oAtRule->getCharset()->getString());
128 }
129 return $oAtRule;
130 } else if ($this->comes('}')) {
131 $this->consume('}');
132 if ($bIsRoot) {
133 throw new SourceException("Unopened {", $this->iLineNo);
134 } else {
135 return null;
136 }
137 } else {
138 return $this->parseSelector();
139 }
140 }
141
142 private function parseAtRule() {
143 $this->consume('@');
144 $sIdentifier = $this->parseIdentifier(false);
145 $iIdentifierLineNum = $this->iLineNo;
146 $this->consumeWhiteSpace();
147 if ($sIdentifier === 'import') {
148 $oLocation = $this->parseURLValue();
149 $this->consumeWhiteSpace();
150 $sMediaQuery = null;
151 if (!$this->comes(';')) {
152 $sMediaQuery = $this->consumeUntil(';');
153 }
154 $this->consume(';');
155 return new Import($oLocation, $sMediaQuery, $iIdentifierLineNum);
156 } else if ($sIdentifier === 'charset') {
157 $sCharset = $this->parseStringValue();
158 $this->consumeWhiteSpace();
159 $this->consume(';');
160 return new Charset($sCharset, $iIdentifierLineNum);
161 } else if ($this->identifierIs($sIdentifier, 'keyframes')) {
162 $oResult = new KeyFrame($iIdentifierLineNum);
163 $oResult->setVendorKeyFrame($sIdentifier);
164 $oResult->setAnimationName(trim($this->consumeUntil('{', false, true)));
165 $this->parseList($oResult);
166 return $oResult;
167 } else if ($sIdentifier === 'namespace') {
168 $sPrefix = null;
169 $mUrl = $this->parsePrimitiveValue();
170 if (!$this->comes(';')) {
171 $sPrefix = $mUrl;
172 $mUrl = $this->parsePrimitiveValue();
173 }
174 $this->consume(';');
175 if ($sPrefix !== null && !is_string($sPrefix)) {
176 throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
177 }
178 if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
179 throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
180 }
181 return new CSSNamespace($mUrl, $sPrefix, $iIdentifierLineNum);
182 } else {
183 //Unknown other at rule (font-face or such)
184 $sArgs = trim($this->consumeUntil('{', false, true));
185 $bUseRuleSet = true;
186 foreach($this->blockRules as $sBlockRuleName) {
187 if($this->identifierIs($sIdentifier, $sBlockRuleName)) {
188 $bUseRuleSet = false;
189 break;
190 }
191 }
192 if($bUseRuleSet) {
193 $oAtRule = new AtRuleSet($sIdentifier, $sArgs, $iIdentifierLineNum);
194 $this->parseRuleSet($oAtRule);
195 } else {
196 $oAtRule = new AtRuleBlockList($sIdentifier, $sArgs, $iIdentifierLineNum);
197 $this->parseList($oAtRule);
198 }
199 return $oAtRule;
200 }
201 }
202
203 private function parseIdentifier($bAllowFunctions = true, $bIgnoreCase = true) {
204 $sResult = $this->parseCharacter(true);
205 if ($sResult === null) {
206 throw new UnexpectedTokenException($sResult, $this->peek(5), 'identifier', $this->iLineNo);
207 }
208 $sCharacter = null;
209 while (($sCharacter = $this->parseCharacter(true)) !== null) {
210 $sResult .= $sCharacter;
211 }
212 if ($bIgnoreCase) {
213 $sResult = $this->strtolower($sResult);
214 }
215 if ($bAllowFunctions && $this->comes('(')) {
216 $this->consume('(');
217 $aArguments = $this->parseValue(array('=', ' ', ','));
218 $sResult = new CSSFunction($sResult, $aArguments, ',', $this->iLineNo);
219 $this->consume(')');
220 }
221 return $sResult;
222 }
223
224 private function parseStringValue() {
225 $sBegin = $this->peek();
226 $sQuote = null;
227 if ($sBegin === "'") {
228 $sQuote = "'";
229 } else if ($sBegin === '"') {
230 $sQuote = '"';
231 }
232 if ($sQuote !== null) {
233 $this->consume($sQuote);
234 }
235 $sResult = "";
236 $sContent = null;
237 if ($sQuote === null) {
238 //Unquoted strings end in whitespace or with braces, brackets, parentheses
239 while (!preg_match('/[\\s{}()<>\\[\\]]/isu', $this->peek())) {
240 $sResult .= $this->parseCharacter(false);
241 }
242 } else {
243 while (!$this->comes($sQuote)) {
244 $sContent = $this->parseCharacter(false);
245 if ($sContent === null) {
246 throw new SourceException("Non-well-formed quoted string {$this->peek(3)}", $this->iLineNo);
247 }
248 $sResult .= $sContent;
249 }
250 $this->consume($sQuote);
251 }
252 return new CSSString($sResult, $this->iLineNo);
253 }
254
255 private function parseCharacter($bIsForIdentifier) {
256 if ($this->peek() === '\\') {
257 if ($bIsForIdentifier && $this->oParserSettings->bLenientParsing && ($this->comes('\0') || $this->comes('\9'))) {
258 // Non-strings can contain \0 or \9 which is an IE hack supported in lenient parsing.
259 return null;
260 }
261 $this->consume('\\');
262 if ($this->comes('\n') || $this->comes('\r')) {
263 return '';
264 }
265 if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
266 return $this->consume(1);
267 }
2fe51fa3 268 $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
fbe18cc0
FM
269 if ($this->strlen($sUnicode) < 6) {
270 //Consume whitespace after incomplete unicode escape
271 if (preg_match('/\\s/isSu', $this->peek())) {
272 if ($this->comes('\r\n')) {
273 $this->consume(2);
274 } else {
275 $this->consume(1);
276 }
277 }
278 }
279 $iUnicode = intval($sUnicode, 16);
280 $sUtf32 = "";
281 for ($i = 0; $i < 4; ++$i) {
282 $sUtf32 .= chr($iUnicode & 0xff);
283 $iUnicode = $iUnicode >> 8;
284 }
285 return iconv('utf-32le', $this->sCharset, $sUtf32);
286 }
287 if ($bIsForIdentifier) {
288 $peek = ord($this->peek());
289 // Ranges: a-z A-Z 0-9 - _
290 if (($peek >= 97 && $peek <= 122) ||
291 ($peek >= 65 && $peek <= 90) ||
292 ($peek >= 48 && $peek <= 57) ||
293 ($peek === 45) ||
294 ($peek === 95) ||
295 ($peek > 0xa1)) {
296 return $this->consume(1);
297 }
298 } else {
299 return $this->consume(1);
300 }
301 return null;
302 }
303
304 private function parseSelector() {
305 $aComments = array();
306 $oResult = new DeclarationBlock($this->iLineNo);
307 $oResult->setSelector($this->consumeUntil('{', false, true, $aComments));
308 $oResult->setComments($aComments);
309 $this->parseRuleSet($oResult);
310 return $oResult;
311 }
312
313 private function parseRuleSet($oRuleSet) {
314 while ($this->comes(';')) {
315 $this->consume(';');
316 }
317 while (!$this->comes('}')) {
318 $oRule = null;
319 if($this->oParserSettings->bLenientParsing) {
320 try {
321 $oRule = $this->parseRule();
322 } catch (UnexpectedTokenException $e) {
323 try {
324 $sConsume = $this->consumeUntil(array("\n", ";", '}'), true);
325 // We need to “unfind” the matches to the end of the ruleSet as this will be matched later
326 if($this->streql(substr($sConsume, -1), '}')) {
327 --$this->iCurrentPosition;
328 } else {
329 while ($this->comes(';')) {
330 $this->consume(';');
331 }
332 }
333 } catch (UnexpectedTokenException $e) {
334 // We’ve reached the end of the document. Just close the RuleSet.
335 return;
336 }
337 }
338 } else {
339 $oRule = $this->parseRule();
340 }
341 if($oRule) {
342 $oRuleSet->addRule($oRule);
343 }
344 }
345 $this->consume('}');
346 }
347
348 private function parseRule() {
349 $aComments = $this->consumeWhiteSpace();
350 $oRule = new Rule($this->parseIdentifier(), $this->iLineNo);
351 $oRule->setComments($aComments);
352 $oRule->addComments($this->consumeWhiteSpace());
353 $this->consume(':');
354 $oValue = $this->parseValue(self::listDelimiterForRule($oRule->getRule()));
355 $oRule->setValue($oValue);
356 if ($this->oParserSettings->bLenientParsing) {
357 while ($this->comes('\\')) {
358 $this->consume('\\');
359 $oRule->addIeHack($this->consume());
360 $this->consumeWhiteSpace();
361 }
362 }
363 if ($this->comes('!')) {
364 $this->consume('!');
365 $this->consumeWhiteSpace();
366 $this->consume('important');
367 $oRule->setIsImportant(true);
368 }
369 while ($this->comes(';')) {
370 $this->consume(';');
371 }
372 return $oRule;
373 }
374
375 private function parseValue($aListDelimiters) {
376 $aStack = array();
377 $this->consumeWhiteSpace();
378 //Build a list of delimiters and parsed values
379 while (!($this->comes('}') || $this->comes(';') || $this->comes('!') || $this->comes(')') || $this->comes('\\'))) {
380 if (count($aStack) > 0) {
381 $bFoundDelimiter = false;
382 foreach ($aListDelimiters as $sDelimiter) {
383 if ($this->comes($sDelimiter)) {
384 array_push($aStack, $this->consume($sDelimiter));
385 $this->consumeWhiteSpace();
386 $bFoundDelimiter = true;
387 break;
388 }
389 }
390 if (!$bFoundDelimiter) {
391 //Whitespace was the list delimiter
392 array_push($aStack, ' ');
393 }
394 }
395 array_push($aStack, $this->parsePrimitiveValue());
396 $this->consumeWhiteSpace();
397 }
398 //Convert the list to list objects
399 foreach ($aListDelimiters as $sDelimiter) {
400 if (count($aStack) === 1) {
401 return $aStack[0];
402 }
403 $iStartPosition = null;
404 while (($iStartPosition = array_search($sDelimiter, $aStack, true)) !== false) {
405 $iLength = 2; //Number of elements to be joined
406 for ($i = $iStartPosition + 2; $i < count($aStack); $i+=2, ++$iLength) {
407 if ($sDelimiter !== $aStack[$i]) {
408 break;
409 }
410 }
411 $oList = new RuleValueList($sDelimiter, $this->iLineNo);
412 for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
413 $oList->addListComponent($aStack[$i]);
414 }
415 array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
416 }
417 }
418 return $aStack[0];
419 }
420
421 private static function listDelimiterForRule($sRule) {
422 if (preg_match('/^font($|-)/', $sRule)) {
423 return array(',', '/', ' ');
424 }
425 return array(',', ' ', '/');
426 }
427
428 private function parsePrimitiveValue() {
429 $oValue = null;
430 $this->consumeWhiteSpace();
431 if (is_numeric($this->peek()) || ($this->comes('-.') && is_numeric($this->peek(1, 2))) || (($this->comes('-') || $this->comes('.')) && is_numeric($this->peek(1, 1)))) {
432 $oValue = $this->parseNumericValue();
433 } else if ($this->comes('#') || $this->comes('rgb', true) || $this->comes('hsl', true)) {
434 $oValue = $this->parseColorValue();
435 } else if ($this->comes('url', true)) {
436 $oValue = $this->parseURLValue();
437 } else if ($this->comes("'") || $this->comes('"')) {
438 $oValue = $this->parseStringValue();
439 } else if ($this->comes("progid:") && $this->oParserSettings->bLenientParsing) {
440 $oValue = $this->parseMicrosoftFilter();
441 } else {
442 $oValue = $this->parseIdentifier(true, false);
443 }
444 $this->consumeWhiteSpace();
445 return $oValue;
446 }
447
448 private function parseNumericValue($bForColor = false) {
449 $sSize = '';
450 if ($this->comes('-')) {
451 $sSize .= $this->consume('-');
452 }
453 while (is_numeric($this->peek()) || $this->comes('.')) {
454 if ($this->comes('.')) {
455 $sSize .= $this->consume('.');
456 } else {
457 $sSize .= $this->consume(1);
458 }
459 }
460
461 $sUnit = null;
462 foreach ($this->aSizeUnits as $iLength => &$aValues) {
463 $sKey = strtolower($this->peek($iLength));
464 if(array_key_exists($sKey, $aValues)) {
465 if (($sUnit = $aValues[$sKey]) !== null) {
466 $this->consume($iLength);
467 break;
468 }
469 }
470 }
471 return new Size(floatval($sSize), $sUnit, $bForColor, $this->iLineNo);
472 }
473
474 private function parseColorValue() {
475 $aColor = array();
476 if ($this->comes('#')) {
477 $this->consume('#');
478 $sValue = $this->parseIdentifier(false);
479 if ($this->strlen($sValue) === 3) {
480 $sValue = $sValue[0] . $sValue[0] . $sValue[1] . $sValue[1] . $sValue[2] . $sValue[2];
481 }
482 $aColor = array('r' => new Size(intval($sValue[0] . $sValue[1], 16), null, true, $this->iLineNo), 'g' => new Size(intval($sValue[2] . $sValue[3], 16), null, true, $this->iLineNo), 'b' => new Size(intval($sValue[4] . $sValue[5], 16), null, true, $this->iLineNo));
483 } else {
484 $sColorMode = $this->parseIdentifier(false);
485 $this->consumeWhiteSpace();
486 $this->consume('(');
487 $iLength = $this->strlen($sColorMode);
488 for ($i = 0; $i < $iLength; ++$i) {
489 $this->consumeWhiteSpace();
490 $aColor[$sColorMode[$i]] = $this->parseNumericValue(true);
491 $this->consumeWhiteSpace();
492 if ($i < ($iLength - 1)) {
493 $this->consume(',');
494 }
495 }
496 $this->consume(')');
497 }
498 return new Color($aColor, $this->iLineNo);
499 }
500
501 private function parseMicrosoftFilter() {
502 $sFunction = $this->consumeUntil('(', false, true);
503 $aArguments = $this->parseValue(array(',', '='));
504 return new CSSFunction($sFunction, $aArguments, ',', $this->iLineNo);
505 }
506
507 private function parseURLValue() {
508 $bUseUrl = $this->comes('url', true);
509 if ($bUseUrl) {
510 $this->consume('url');
511 $this->consumeWhiteSpace();
512 $this->consume('(');
513 }
514 $this->consumeWhiteSpace();
515 $oResult = new URL($this->parseStringValue(), $this->iLineNo);
516 if ($bUseUrl) {
517 $this->consumeWhiteSpace();
518 $this->consume(')');
519 }
520 return $oResult;
521 }
522
523 /**
524 * Tests an identifier for a given value. Since identifiers are all keywords, they can be vendor-prefixed. We need to check for these versions too.
525 */
526 private function identifierIs($sIdentifier, $sMatch) {
527 return (strcasecmp($sIdentifier, $sMatch) === 0)
528 ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
529 }
530
531 private function comes($sString, $bCaseInsensitive = false) {
532 $sPeek = $this->peek(strlen($sString));
533 return ($sPeek == '')
534 ? false
535 : $this->streql($sPeek, $sString, $bCaseInsensitive);
536 }
537
538 private function peek($iLength = 1, $iOffset = 0) {
539 $iOffset += $this->iCurrentPosition;
540 if ($iOffset >= $this->iLength) {
541 return '';
542 }
543 return $this->substr($iOffset, $iLength);
544 }
545
546 private function consume($mValue = 1) {
547 if (is_string($mValue)) {
548 $iLineCount = substr_count($mValue, "\n");
549 $iLength = $this->strlen($mValue);
550 if (!$this->streql($this->substr($this->iCurrentPosition, $iLength), $mValue)) {
551 throw new UnexpectedTokenException($mValue, $this->peek(max($iLength, 5)), $this->iLineNo);
552 }
553 $this->iLineNo += $iLineCount;
554 $this->iCurrentPosition += $this->strlen($mValue);
555 return $mValue;
556 } else {
557 if ($this->iCurrentPosition + $mValue > $this->iLength) {
558 throw new UnexpectedTokenException($mValue, $this->peek(5), 'count', $this->iLineNo);
559 }
560 $sResult = $this->substr($this->iCurrentPosition, $mValue);
561 $iLineCount = substr_count($sResult, "\n");
562 $this->iLineNo += $iLineCount;
563 $this->iCurrentPosition += $mValue;
564 return $sResult;
565 }
566 }
567
2fe51fa3 568 private function consumeExpression($mExpression, $iMaxLength = null) {
fbe18cc0 569 $aMatches = null;
2fe51fa3
DP
570 $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
571 if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
fbe18cc0
FM
572 return $this->consume($aMatches[0][0]);
573 }
574 throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
575 }
576
577 private function consumeWhiteSpace() {
578 $comments = array();
579 do {
580 while (preg_match('/\\s/isSu', $this->peek()) === 1) {
581 $this->consume(1);
582 }
583 if($this->oParserSettings->bLenientParsing) {
584 try {
585 $oComment = $this->consumeComment();
586 } catch(UnexpectedTokenException $e) {
587 // When we can’t find the end of a comment, we assume the document is finished.
588 $this->iCurrentPosition = $this->iLength;
589 return;
590 }
591 } else {
592 $oComment = $this->consumeComment();
593 }
594 if ($oComment !== false) {
595 $comments[] = $oComment;
596 }
597 } while($oComment !== false);
598 return $comments;
599 }
600
601 /**
602 * @return false|Comment
603 */
604 private function consumeComment() {
605 $mComment = false;
606 if ($this->comes('/*')) {
607 $iLineNo = $this->iLineNo;
608 $this->consume(1);
609 $mComment = '';
610 while (($char = $this->consume(1)) !== '') {
611 $mComment .= $char;
612 if ($this->comes('*/')) {
613 $this->consume(2);
614 break;
615 }
616 }
617 }
618
619 if ($mComment !== false) {
620 // We skip the * which was included in the comment.
621 return new Comment(substr($mComment, 1), $iLineNo);
622 }
623
624 return $mComment;
625 }
626
627 private function isEnd() {
628 return $this->iCurrentPosition >= $this->iLength;
629 }
630
631 private function consumeUntil($aEnd, $bIncludeEnd = false, $consumeEnd = false, array &$comments = array()) {
632 $aEnd = is_array($aEnd) ? $aEnd : array($aEnd);
633 $out = '';
634 $start = $this->iCurrentPosition;
635
636 while (($char = $this->consume(1)) !== '') {
637 if (in_array($char, $aEnd)) {
638 if ($bIncludeEnd) {
639 $out .= $char;
640 } elseif (!$consumeEnd) {
641 $this->iCurrentPosition -= $this->strlen($char);
642 }
643 return $out;
644 }
645 $out .= $char;
646 if ($comment = $this->consumeComment()) {
647 $comments[] = $comment;
648 }
649 }
650
651 $this->iCurrentPosition = $start;
652 throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
653 }
654
655 private function inputLeft() {
656 return $this->substr($this->iCurrentPosition, -1);
657 }
658
659 private function substr($iStart, $iLength) {
660 if ($iLength < 0) {
661 $iLength = $this->iLength - $iStart + $iLength;
662 }
663 if ($iStart + $iLength > $this->iLength) {
664 $iLength = $this->iLength - $iStart;
665 }
666 $sResult = '';
667 while ($iLength > 0) {
668 $sResult .= $this->aText[$iStart];
669 $iStart++;
670 $iLength--;
671 }
672 return $sResult;
673 }
674
675 private function strlen($sString) {
676 if ($this->oParserSettings->bMultibyteSupport) {
677 return mb_strlen($sString, $this->sCharset);
678 } else {
679 return strlen($sString);
680 }
681 }
682
683 private function streql($sString1, $sString2, $bCaseInsensitive = true) {
684 if($bCaseInsensitive) {
685 return $this->strtolower($sString1) === $this->strtolower($sString2);
686 } else {
687 return $sString1 === $sString2;
688 }
689 }
690
691 private function strtolower($sString) {
692 if ($this->oParserSettings->bMultibyteSupport) {
693 return mb_strtolower($sString, $this->sCharset);
694 } else {
695 return strtolower($sString);
696 }
697 }
698
699 private function strsplit($sString) {
700 if ($this->oParserSettings->bMultibyteSupport) {
701 if ($this->streql($this->sCharset, 'utf-8')) {
702 return preg_split('//u', $sString, null, PREG_SPLIT_NO_EMPTY);
703 } else {
704 $iLength = mb_strlen($sString, $this->sCharset);
705 $aResult = array();
706 for ($i = 0; $i < $iLength; ++$i) {
707 $aResult[] = mb_substr($sString, $i, 1, $this->sCharset);
708 }
709 return $aResult;
710 }
711 } else {
712 if($sString === '') {
713 return array();
714 } else {
715 return str_split($sString);
716 }
717 }
718 }
719
720 private function strpos($sString, $sNeedle, $iOffset) {
721 if ($this->oParserSettings->bMultibyteSupport) {
722 return mb_strpos($sString, $sNeedle, $iOffset, $this->sCharset);
723 } else {
724 return strpos($sString, $sNeedle, $iOffset);
725 }
726 }
727
728}