Merge branch 'MDL-57742_master' of git://github.com/markn86/moodle
[moodle.git] / lib / php-css-parser / Parser.php
blob65ea2f0d0b986634597cbbaa7c0750e411e49fb8
1 <?php
3 namespace Sabberworm\CSS;
5 use Sabberworm\CSS\CSSList\CSSList;
6 use Sabberworm\CSS\CSSList\Document;
7 use Sabberworm\CSS\CSSList\KeyFrame;
8 use Sabberworm\CSS\Parsing\SourceException;
9 use Sabberworm\CSS\Property\AtRule;
10 use Sabberworm\CSS\Property\Import;
11 use Sabberworm\CSS\Property\Charset;
12 use Sabberworm\CSS\Property\CSSNamespace;
13 use Sabberworm\CSS\RuleSet\AtRuleSet;
14 use Sabberworm\CSS\CSSList\AtRuleBlockList;
15 use Sabberworm\CSS\RuleSet\DeclarationBlock;
16 use Sabberworm\CSS\Value\CSSFunction;
17 use Sabberworm\CSS\Value\RuleValueList;
18 use Sabberworm\CSS\Value\Size;
19 use Sabberworm\CSS\Value\Color;
20 use Sabberworm\CSS\Value\URL;
21 use Sabberworm\CSS\Value\CSSString;
22 use Sabberworm\CSS\Rule\Rule;
23 use Sabberworm\CSS\Parsing\UnexpectedTokenException;
24 use Sabberworm\CSS\Comment\Comment;
26 /**
27 * Parser class parses CSS from text into a data structure.
29 class Parser {
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;
41 /**
42 * Parser constructor.
43 * Note that that iLineNo starts from 1 and not 0
45 * @param $sText
46 * @param Settings|null $oParserSettings
47 * @param int $iLineNo
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();
56 $this->oParserSettings = $oParserSettings;
57 $this->blockRules = explode('/', AtRule::BLOCK_RULES);
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();
64 $this->aSizeUnits[$iSize][strtolower($val)] = $val;
66 ksort($this->aSizeUnits, SORT_NUMERIC);
69 public function setCharset($sCharset) {
70 $this->sCharset = $sCharset;
71 $this->aText = $this->strsplit($this->sText);
72 $this->iLength = count($this->aText);
75 public function getCharset() {
76 return $this->sCharset;
79 public function parse() {
80 $this->setCharset($this->oParserSettings->sDefaultCharset);
81 $oResult = new Document($this->iLineNo);
82 $this->parseDocument($oResult);
83 return $oResult;
86 private function parseDocument(Document $oDocument) {
87 $this->parseList($oDocument, true);
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;
100 } else {
101 $oListItem = $this->parseListItem($oList, $bIsRoot);
103 if($oListItem === null) {
104 // List parsing finished
105 return;
107 if($oListItem) {
108 $oListItem->setComments($comments);
109 $oList->append($oListItem);
112 if (!$bIsRoot) {
113 throw new SourceException("Unexpected end of document", $this->iLineNo);
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);
124 if(count($oList->getContents()) > 0) {
125 throw new UnexpectedTokenException('@charset must be the first parseable token in a document', '', 'custom', $this->iLineNo);
127 $this->setCharset($oAtRule->getCharset()->getString());
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;
137 } else {
138 return $this->parseSelector();
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(';');
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();
174 $this->consume(';');
175 if ($sPrefix !== null && !is_string($sPrefix)) {
176 throw new UnexpectedTokenException('Wrong namespace prefix', $sPrefix, 'custom', $iIdentifierLineNum);
178 if (!($mUrl instanceof CSSString || $mUrl instanceof URL)) {
179 throw new UnexpectedTokenException('Wrong namespace url of invalid type', $mUrl, 'custom', $iIdentifierLineNum);
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;
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);
199 return $oAtRule;
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);
208 $sCharacter = null;
209 while (($sCharacter = $this->parseCharacter(true)) !== null) {
210 $sResult .= $sCharacter;
212 if ($bIgnoreCase) {
213 $sResult = $this->strtolower($sResult);
215 if ($bAllowFunctions && $this->comes('(')) {
216 $this->consume('(');
217 $aArguments = $this->parseValue(array('=', ' ', ','));
218 $sResult = new CSSFunction($sResult, $aArguments, ',', $this->iLineNo);
219 $this->consume(')');
221 return $sResult;
224 private function parseStringValue() {
225 $sBegin = $this->peek();
226 $sQuote = null;
227 if ($sBegin === "'") {
228 $sQuote = "'";
229 } else if ($sBegin === '"') {
230 $sQuote = '"';
232 if ($sQuote !== null) {
233 $this->consume($sQuote);
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);
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);
248 $sResult .= $sContent;
250 $this->consume($sQuote);
252 return new CSSString($sResult, $this->iLineNo);
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;
261 $this->consume('\\');
262 if ($this->comes('\n') || $this->comes('\r')) {
263 return '';
265 if (preg_match('/[0-9a-fA-F]/Su', $this->peek()) === 0) {
266 return $this->consume(1);
268 $sUnicode = $this->consumeExpression('/^[0-9a-fA-F]{1,6}/u', 6);
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);
279 $iUnicode = intval($sUnicode, 16);
280 $sUtf32 = "";
281 for ($i = 0; $i < 4; ++$i) {
282 $sUtf32 .= chr($iUnicode & 0xff);
283 $iUnicode = $iUnicode >> 8;
285 return iconv('utf-32le', $this->sCharset, $sUtf32);
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);
298 } else {
299 return $this->consume(1);
301 return null;
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;
313 private function parseRuleSet($oRuleSet) {
314 while ($this->comes(';')) {
315 $this->consume(';');
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(';');
333 } catch (UnexpectedTokenException $e) {
334 // We’ve reached the end of the document. Just close the RuleSet.
335 return;
338 } else {
339 $oRule = $this->parseRule();
341 if($oRule) {
342 $oRuleSet->addRule($oRule);
345 $this->consume('}');
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();
363 if ($this->comes('!')) {
364 $this->consume('!');
365 $this->consumeWhiteSpace();
366 $this->consume('important');
367 $oRule->setIsImportant(true);
369 while ($this->comes(';')) {
370 $this->consume(';');
372 return $oRule;
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;
390 if (!$bFoundDelimiter) {
391 //Whitespace was the list delimiter
392 array_push($aStack, ' ');
395 array_push($aStack, $this->parsePrimitiveValue());
396 $this->consumeWhiteSpace();
398 //Convert the list to list objects
399 foreach ($aListDelimiters as $sDelimiter) {
400 if (count($aStack) === 1) {
401 return $aStack[0];
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;
411 $oList = new RuleValueList($sDelimiter, $this->iLineNo);
412 for ($i = $iStartPosition - 1; $i - $iStartPosition + 1 < $iLength * 2; $i+=2) {
413 $oList->addListComponent($aStack[$i]);
415 array_splice($aStack, $iStartPosition - 1, $iLength * 2 - 1, array($oList));
418 return $aStack[0];
421 private static function listDelimiterForRule($sRule) {
422 if (preg_match('/^font($|-)/', $sRule)) {
423 return array(',', '/', ' ');
425 return array(',', ' ', '/');
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);
444 $this->consumeWhiteSpace();
445 return $oValue;
448 private function parseNumericValue($bForColor = false) {
449 $sSize = '';
450 if ($this->comes('-')) {
451 $sSize .= $this->consume('-');
453 while (is_numeric($this->peek()) || $this->comes('.')) {
454 if ($this->comes('.')) {
455 $sSize .= $this->consume('.');
456 } else {
457 $sSize .= $this->consume(1);
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;
471 return new Size(floatval($sSize), $sUnit, $bForColor, $this->iLineNo);
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];
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(',');
496 $this->consume(')');
498 return new Color($aColor, $this->iLineNo);
501 private function parseMicrosoftFilter() {
502 $sFunction = $this->consumeUntil('(', false, true);
503 $aArguments = $this->parseValue(array(',', '='));
504 return new CSSFunction($sFunction, $aArguments, ',', $this->iLineNo);
507 private function parseURLValue() {
508 $bUseUrl = $this->comes('url', true);
509 if ($bUseUrl) {
510 $this->consume('url');
511 $this->consumeWhiteSpace();
512 $this->consume('(');
514 $this->consumeWhiteSpace();
515 $oResult = new URL($this->parseStringValue(), $this->iLineNo);
516 if ($bUseUrl) {
517 $this->consumeWhiteSpace();
518 $this->consume(')');
520 return $oResult;
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.
526 private function identifierIs($sIdentifier, $sMatch) {
527 return (strcasecmp($sIdentifier, $sMatch) === 0)
528 ?: preg_match("/^(-\\w+-)?$sMatch$/i", $sIdentifier) === 1;
531 private function comes($sString, $bCaseInsensitive = false) {
532 $sPeek = $this->peek(strlen($sString));
533 return ($sPeek == '')
534 ? false
535 : $this->streql($sPeek, $sString, $bCaseInsensitive);
538 private function peek($iLength = 1, $iOffset = 0) {
539 $iOffset += $this->iCurrentPosition;
540 if ($iOffset >= $this->iLength) {
541 return '';
543 return $this->substr($iOffset, $iLength);
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);
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);
560 $sResult = $this->substr($this->iCurrentPosition, $mValue);
561 $iLineCount = substr_count($sResult, "\n");
562 $this->iLineNo += $iLineCount;
563 $this->iCurrentPosition += $mValue;
564 return $sResult;
568 private function consumeExpression($mExpression, $iMaxLength = null) {
569 $aMatches = null;
570 $sInput = $iMaxLength !== null ? $this->peek($iMaxLength) : $this->inputLeft();
571 if (preg_match($mExpression, $sInput, $aMatches, PREG_OFFSET_CAPTURE) === 1) {
572 return $this->consume($aMatches[0][0]);
574 throw new UnexpectedTokenException($mExpression, $this->peek(5), 'expression', $this->iLineNo);
577 private function consumeWhiteSpace() {
578 $comments = array();
579 do {
580 while (preg_match('/\\s/isSu', $this->peek()) === 1) {
581 $this->consume(1);
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;
591 } else {
592 $oComment = $this->consumeComment();
594 if ($oComment !== false) {
595 $comments[] = $oComment;
597 } while($oComment !== false);
598 return $comments;
602 * @return false|Comment
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;
619 if ($mComment !== false) {
620 // We skip the * which was included in the comment.
621 return new Comment(substr($mComment, 1), $iLineNo);
624 return $mComment;
627 private function isEnd() {
628 return $this->iCurrentPosition >= $this->iLength;
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;
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);
643 return $out;
645 $out .= $char;
646 if ($comment = $this->consumeComment()) {
647 $comments[] = $comment;
651 $this->iCurrentPosition = $start;
652 throw new UnexpectedTokenException('One of ("'.implode('","', $aEnd).'")', $this->peek(5), 'search', $this->iLineNo);
655 private function inputLeft() {
656 return $this->substr($this->iCurrentPosition, -1);
659 private function substr($iStart, $iLength) {
660 if ($iLength < 0) {
661 $iLength = $this->iLength - $iStart + $iLength;
663 if ($iStart + $iLength > $this->iLength) {
664 $iLength = $this->iLength - $iStart;
666 $sResult = '';
667 while ($iLength > 0) {
668 $sResult .= $this->aText[$iStart];
669 $iStart++;
670 $iLength--;
672 return $sResult;
675 private function strlen($sString) {
676 if ($this->oParserSettings->bMultibyteSupport) {
677 return mb_strlen($sString, $this->sCharset);
678 } else {
679 return strlen($sString);
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;
691 private function strtolower($sString) {
692 if ($this->oParserSettings->bMultibyteSupport) {
693 return mb_strtolower($sString, $this->sCharset);
694 } else {
695 return strtolower($sString);
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);
709 return $aResult;
711 } else {
712 if($sString === '') {
713 return array();
714 } else {
715 return str_split($sString);
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);