No Description

Parser.php 26KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Component\Yaml;
  11. use Symfony\Component\Yaml\Exception\ParseException;
  12. /**
  13. * Parser parses YAML strings to convert them to PHP arrays.
  14. *
  15. * @author Fabien Potencier <fabien@symfony.com>
  16. */
  17. class Parser
  18. {
  19. const FOLDED_SCALAR_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
  20. private $offset = 0;
  21. private $lines = array();
  22. private $currentLineNb = -1;
  23. private $currentLine = '';
  24. private $refs = array();
  25. /**
  26. * Constructor.
  27. *
  28. * @param int $offset The offset of YAML document (used for line numbers in error messages)
  29. */
  30. public function __construct($offset = 0)
  31. {
  32. $this->offset = $offset;
  33. }
  34. /**
  35. * Parses a YAML string to a PHP value.
  36. *
  37. * @param string $value A YAML string
  38. * @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
  39. * @param bool $objectSupport true if object support is enabled, false otherwise
  40. * @param bool $objectForMap true if maps should return a stdClass instead of array()
  41. *
  42. * @return mixed A PHP value
  43. *
  44. * @throws ParseException If the YAML is not valid
  45. */
  46. public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
  47. {
  48. $this->currentLineNb = -1;
  49. $this->currentLine = '';
  50. $this->lines = explode("\n", $this->cleanup($value));
  51. if (!preg_match('//u', $value)) {
  52. throw new ParseException('The YAML value does not appear to be valid UTF-8.');
  53. }
  54. if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
  55. $mbEncoding = mb_internal_encoding();
  56. mb_internal_encoding('UTF-8');
  57. }
  58. $data = array();
  59. $context = null;
  60. $allowOverwrite = false;
  61. while ($this->moveToNextLine()) {
  62. if ($this->isCurrentLineEmpty()) {
  63. continue;
  64. }
  65. // tab?
  66. if ("\t" === $this->currentLine[0]) {
  67. throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  68. }
  69. $isRef = $mergeNode = false;
  70. if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) {
  71. if ($context && 'mapping' == $context) {
  72. throw new ParseException('You cannot define a sequence item when in a mapping');
  73. }
  74. $context = 'sequence';
  75. if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
  76. $isRef = $matches['ref'];
  77. $values['value'] = $matches['value'];
  78. }
  79. // array
  80. if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
  81. $c = $this->getRealCurrentLineNb() + 1;
  82. $parser = new Parser($c);
  83. $parser->refs = & $this->refs;
  84. $data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
  85. } else {
  86. if (isset($values['leadspaces'])
  87. && preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
  88. ) {
  89. // this is a compact notation element, add to next block and parse
  90. $c = $this->getRealCurrentLineNb();
  91. $parser = new Parser($c);
  92. $parser->refs = & $this->refs;
  93. $block = $values['value'];
  94. if ($this->isNextLineIndented()) {
  95. $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
  96. }
  97. $data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
  98. } else {
  99. $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap);
  100. }
  101. }
  102. } elseif (preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) {
  103. if ($context && 'sequence' == $context) {
  104. throw new ParseException('You cannot define a mapping item when in a sequence');
  105. }
  106. $context = 'mapping';
  107. // force correct settings
  108. Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
  109. try {
  110. $key = Inline::parseScalar($values['key']);
  111. } catch (ParseException $e) {
  112. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  113. $e->setSnippet($this->currentLine);
  114. throw $e;
  115. }
  116. if ('<<' === $key) {
  117. $mergeNode = true;
  118. $allowOverwrite = true;
  119. if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
  120. $refName = substr($values['value'], 1);
  121. if (!array_key_exists($refName, $this->refs)) {
  122. throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
  123. }
  124. $refValue = $this->refs[$refName];
  125. if (!is_array($refValue)) {
  126. throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  127. }
  128. foreach ($refValue as $key => $value) {
  129. if (!isset($data[$key])) {
  130. $data[$key] = $value;
  131. }
  132. }
  133. } else {
  134. if (isset($values['value']) && $values['value'] !== '') {
  135. $value = $values['value'];
  136. } else {
  137. $value = $this->getNextEmbedBlock();
  138. }
  139. $c = $this->getRealCurrentLineNb() + 1;
  140. $parser = new Parser($c);
  141. $parser->refs = & $this->refs;
  142. $parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
  143. if (!is_array($parsed)) {
  144. throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  145. }
  146. if (isset($parsed[0])) {
  147. // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
  148. // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
  149. // in the sequence override keys specified in later mapping nodes.
  150. foreach ($parsed as $parsedItem) {
  151. if (!is_array($parsedItem)) {
  152. throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
  153. }
  154. foreach ($parsedItem as $key => $value) {
  155. if (!isset($data[$key])) {
  156. $data[$key] = $value;
  157. }
  158. }
  159. }
  160. } else {
  161. // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
  162. // current mapping, unless the key already exists in it.
  163. foreach ($parsed as $key => $value) {
  164. if (!isset($data[$key])) {
  165. $data[$key] = $value;
  166. }
  167. }
  168. }
  169. }
  170. } elseif (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
  171. $isRef = $matches['ref'];
  172. $values['value'] = $matches['value'];
  173. }
  174. if ($mergeNode) {
  175. // Merge keys
  176. } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
  177. // hash
  178. // if next line is less indented or equal, then it means that the current value is null
  179. if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
  180. // Spec: Keys MUST be unique; first one wins.
  181. // But overwriting is allowed when a merge node is used in current block.
  182. if ($allowOverwrite || !isset($data[$key])) {
  183. $data[$key] = null;
  184. }
  185. } else {
  186. $c = $this->getRealCurrentLineNb() + 1;
  187. $parser = new Parser($c);
  188. $parser->refs = & $this->refs;
  189. $value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
  190. // Spec: Keys MUST be unique; first one wins.
  191. // But overwriting is allowed when a merge node is used in current block.
  192. if ($allowOverwrite || !isset($data[$key])) {
  193. $data[$key] = $value;
  194. }
  195. }
  196. } else {
  197. $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap);
  198. // Spec: Keys MUST be unique; first one wins.
  199. // But overwriting is allowed when a merge node is used in current block.
  200. if ($allowOverwrite || !isset($data[$key])) {
  201. $data[$key] = $value;
  202. }
  203. }
  204. } else {
  205. // multiple documents are not supported
  206. if ('---' === $this->currentLine) {
  207. throw new ParseException('Multiple documents are not supported.');
  208. }
  209. // 1-liner optionally followed by newline
  210. $lineCount = count($this->lines);
  211. if (1 === $lineCount || (2 === $lineCount && empty($this->lines[1]))) {
  212. try {
  213. $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
  214. } catch (ParseException $e) {
  215. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  216. $e->setSnippet($this->currentLine);
  217. throw $e;
  218. }
  219. if (is_array($value)) {
  220. $first = reset($value);
  221. if (is_string($first) && 0 === strpos($first, '*')) {
  222. $data = array();
  223. foreach ($value as $alias) {
  224. $data[] = $this->refs[substr($alias, 1)];
  225. }
  226. $value = $data;
  227. }
  228. }
  229. if (isset($mbEncoding)) {
  230. mb_internal_encoding($mbEncoding);
  231. }
  232. return $value;
  233. }
  234. switch (preg_last_error()) {
  235. case PREG_INTERNAL_ERROR:
  236. $error = 'Internal PCRE error.';
  237. break;
  238. case PREG_BACKTRACK_LIMIT_ERROR:
  239. $error = 'pcre.backtrack_limit reached.';
  240. break;
  241. case PREG_RECURSION_LIMIT_ERROR:
  242. $error = 'pcre.recursion_limit reached.';
  243. break;
  244. case PREG_BAD_UTF8_ERROR:
  245. $error = 'Malformed UTF-8 data.';
  246. break;
  247. case PREG_BAD_UTF8_OFFSET_ERROR:
  248. $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
  249. break;
  250. default:
  251. $error = 'Unable to parse.';
  252. }
  253. throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine);
  254. }
  255. if ($isRef) {
  256. $this->refs[$isRef] = end($data);
  257. }
  258. }
  259. if (isset($mbEncoding)) {
  260. mb_internal_encoding($mbEncoding);
  261. }
  262. return empty($data) ? null : $data;
  263. }
  264. /**
  265. * Returns the current line number (takes the offset into account).
  266. *
  267. * @return int The current line number
  268. */
  269. private function getRealCurrentLineNb()
  270. {
  271. return $this->currentLineNb + $this->offset;
  272. }
  273. /**
  274. * Returns the current line indentation.
  275. *
  276. * @return int The current line indentation
  277. */
  278. private function getCurrentLineIndentation()
  279. {
  280. return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
  281. }
  282. /**
  283. * Returns the next embed block of YAML.
  284. *
  285. * @param int $indentation The indent level at which the block is to be read, or null for default
  286. * @param bool $inSequence True if the enclosing data structure is a sequence
  287. *
  288. * @return string A YAML string
  289. *
  290. * @throws ParseException When indentation problem are detected
  291. */
  292. private function getNextEmbedBlock($indentation = null, $inSequence = false)
  293. {
  294. $oldLineIndentation = $this->getCurrentLineIndentation();
  295. if (!$this->moveToNextLine()) {
  296. return;
  297. }
  298. if (null === $indentation) {
  299. $newIndent = $this->getCurrentLineIndentation();
  300. $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem($this->currentLine);
  301. if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
  302. throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  303. }
  304. } else {
  305. $newIndent = $indentation;
  306. }
  307. $data = array();
  308. if ($this->getCurrentLineIndentation() >= $newIndent) {
  309. $data[] = substr($this->currentLine, $newIndent);
  310. } else {
  311. $this->moveToPreviousLine();
  312. return;
  313. }
  314. if ($inSequence && $oldLineIndentation === $newIndent && '-' === $data[0][0]) {
  315. // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
  316. // and therefore no nested list or mapping
  317. $this->moveToPreviousLine();
  318. return;
  319. }
  320. $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem($this->currentLine);
  321. // Comments must not be removed inside a string block (ie. after a line ending with "|")
  322. $removeCommentsPattern = '~'.self::FOLDED_SCALAR_PATTERN.'$~';
  323. $removeComments = !preg_match($removeCommentsPattern, $this->currentLine);
  324. while ($this->moveToNextLine()) {
  325. $indent = $this->getCurrentLineIndentation();
  326. if ($indent === $newIndent) {
  327. $removeComments = !preg_match($removeCommentsPattern, $this->currentLine);
  328. }
  329. if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem($this->currentLine) && $newIndent === $indent) {
  330. $this->moveToPreviousLine();
  331. break;
  332. }
  333. if ($this->isCurrentLineBlank()) {
  334. $data[] = substr($this->currentLine, $newIndent);
  335. continue;
  336. }
  337. if ($removeComments && $this->isCurrentLineComment()) {
  338. continue;
  339. }
  340. if ($indent >= $newIndent) {
  341. $data[] = substr($this->currentLine, $newIndent);
  342. } elseif (0 == $indent) {
  343. $this->moveToPreviousLine();
  344. break;
  345. } else {
  346. throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  347. }
  348. }
  349. return implode("\n", $data);
  350. }
  351. /**
  352. * Moves the parser to the next line.
  353. *
  354. * @return bool
  355. */
  356. private function moveToNextLine()
  357. {
  358. if ($this->currentLineNb >= count($this->lines) - 1) {
  359. return false;
  360. }
  361. $this->currentLine = $this->lines[++$this->currentLineNb];
  362. return true;
  363. }
  364. /**
  365. * Moves the parser to the previous line.
  366. */
  367. private function moveToPreviousLine()
  368. {
  369. $this->currentLine = $this->lines[--$this->currentLineNb];
  370. }
  371. /**
  372. * Parses a YAML value.
  373. *
  374. * @param string $value A YAML value
  375. * @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
  376. * @param bool $objectSupport True if object support is enabled, false otherwise
  377. * @param bool $objectForMap true if maps should return a stdClass instead of array()
  378. *
  379. * @return mixed A PHP value
  380. *
  381. * @throws ParseException When reference does not exist
  382. */
  383. private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap)
  384. {
  385. if (0 === strpos($value, '*')) {
  386. if (false !== $pos = strpos($value, '#')) {
  387. $value = substr($value, 1, $pos - 2);
  388. } else {
  389. $value = substr($value, 1);
  390. }
  391. if (!array_key_exists($value, $this->refs)) {
  392. throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine);
  393. }
  394. return $this->refs[$value];
  395. }
  396. if (preg_match('/^'.self::FOLDED_SCALAR_PATTERN.'$/', $value, $matches)) {
  397. $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
  398. return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers)));
  399. }
  400. try {
  401. return Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
  402. } catch (ParseException $e) {
  403. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  404. $e->setSnippet($this->currentLine);
  405. throw $e;
  406. }
  407. }
  408. /**
  409. * Parses a folded scalar.
  410. *
  411. * @param string $separator The separator that was used to begin this folded scalar (| or >)
  412. * @param string $indicator The indicator that was used to begin this folded scalar (+ or -)
  413. * @param int $indentation The indentation that was used to begin this folded scalar
  414. *
  415. * @return string The text value
  416. */
  417. private function parseFoldedScalar($separator, $indicator = '', $indentation = 0)
  418. {
  419. $notEOF = $this->moveToNextLine();
  420. if (!$notEOF) {
  421. return '';
  422. }
  423. $isCurrentLineBlank = $this->isCurrentLineBlank();
  424. $text = '';
  425. // leading blank lines are consumed before determining indentation
  426. while ($notEOF && $isCurrentLineBlank) {
  427. // newline only if not EOF
  428. if ($notEOF = $this->moveToNextLine()) {
  429. $text .= "\n";
  430. $isCurrentLineBlank = $this->isCurrentLineBlank();
  431. }
  432. }
  433. // determine indentation if not specified
  434. if (0 === $indentation) {
  435. if (preg_match('/^ +/', $this->currentLine, $matches)) {
  436. $indentation = strlen($matches[0]);
  437. }
  438. }
  439. if ($indentation > 0) {
  440. $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
  441. while (
  442. $notEOF && (
  443. $isCurrentLineBlank ||
  444. preg_match($pattern, $this->currentLine, $matches)
  445. )
  446. ) {
  447. if ($isCurrentLineBlank) {
  448. $text .= substr($this->currentLine, $indentation);
  449. } else {
  450. $text .= $matches[1];
  451. }
  452. // newline only if not EOF
  453. if ($notEOF = $this->moveToNextLine()) {
  454. $text .= "\n";
  455. $isCurrentLineBlank = $this->isCurrentLineBlank();
  456. }
  457. }
  458. } elseif ($notEOF) {
  459. $text .= "\n";
  460. }
  461. if ($notEOF) {
  462. $this->moveToPreviousLine();
  463. }
  464. // replace all non-trailing single newlines with spaces in folded blocks
  465. if ('>' === $separator) {
  466. preg_match('/(\n*)$/', $text, $matches);
  467. $text = preg_replace('/(?<!\n)\n(?!\n)/', ' ', rtrim($text, "\n"));
  468. $text .= $matches[1];
  469. }
  470. // deal with trailing newlines as indicated
  471. if ('' === $indicator) {
  472. $text = preg_replace('/\n+$/s', "\n", $text);
  473. } elseif ('-' === $indicator) {
  474. $text = preg_replace('/\n+$/s', '', $text);
  475. }
  476. return $text;
  477. }
  478. /**
  479. * Returns true if the next line is indented.
  480. *
  481. * @return bool Returns true if the next line is indented, false otherwise
  482. */
  483. private function isNextLineIndented()
  484. {
  485. $currentIndentation = $this->getCurrentLineIndentation();
  486. $EOF = !$this->moveToNextLine();
  487. while (!$EOF && $this->isCurrentLineEmpty()) {
  488. $EOF = !$this->moveToNextLine();
  489. }
  490. if ($EOF) {
  491. return false;
  492. }
  493. $ret = false;
  494. if ($this->getCurrentLineIndentation() > $currentIndentation) {
  495. $ret = true;
  496. }
  497. $this->moveToPreviousLine();
  498. return $ret;
  499. }
  500. /**
  501. * Returns true if the current line is blank or if it is a comment line.
  502. *
  503. * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
  504. */
  505. private function isCurrentLineEmpty()
  506. {
  507. return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
  508. }
  509. /**
  510. * Returns true if the current line is blank.
  511. *
  512. * @return bool Returns true if the current line is blank, false otherwise
  513. */
  514. private function isCurrentLineBlank()
  515. {
  516. return '' == trim($this->currentLine, ' ');
  517. }
  518. /**
  519. * Returns true if the current line is a comment line.
  520. *
  521. * @return bool Returns true if the current line is a comment line, false otherwise
  522. */
  523. private function isCurrentLineComment()
  524. {
  525. //checking explicitly the first char of the trim is faster than loops or strpos
  526. $ltrimmedLine = ltrim($this->currentLine, ' ');
  527. return $ltrimmedLine[0] === '#';
  528. }
  529. /**
  530. * Cleanups a YAML string to be parsed.
  531. *
  532. * @param string $value The input YAML string
  533. *
  534. * @return string A cleaned up YAML string
  535. */
  536. private function cleanup($value)
  537. {
  538. $value = str_replace(array("\r\n", "\r"), "\n", $value);
  539. // strip YAML header
  540. $count = 0;
  541. $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
  542. $this->offset += $count;
  543. // remove leading comments
  544. $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
  545. if ($count == 1) {
  546. // items have been removed, update the offset
  547. $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
  548. $value = $trimmedValue;
  549. }
  550. // remove start of the document marker (---)
  551. $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
  552. if ($count == 1) {
  553. // items have been removed, update the offset
  554. $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
  555. $value = $trimmedValue;
  556. // remove end of the document marker (...)
  557. $value = preg_replace('#\.\.\.\s*$#s', '', $value);
  558. }
  559. return $value;
  560. }
  561. /**
  562. * Returns true if the next line starts unindented collection.
  563. *
  564. * @return bool Returns true if the next line starts unindented collection, false otherwise
  565. */
  566. private function isNextLineUnIndentedCollection()
  567. {
  568. $currentIndentation = $this->getCurrentLineIndentation();
  569. $notEOF = $this->moveToNextLine();
  570. while ($notEOF && $this->isCurrentLineEmpty()) {
  571. $notEOF = $this->moveToNextLine();
  572. }
  573. if (false === $notEOF) {
  574. return false;
  575. }
  576. $ret = false;
  577. if (
  578. $this->getCurrentLineIndentation() == $currentIndentation
  579. &&
  580. $this->isStringUnIndentedCollectionItem($this->currentLine)
  581. ) {
  582. $ret = true;
  583. }
  584. $this->moveToPreviousLine();
  585. return $ret;
  586. }
  587. /**
  588. * Returns true if the string is un-indented collection item.
  589. *
  590. * @return bool Returns true if the string is un-indented collection item, false otherwise
  591. */
  592. private function isStringUnIndentedCollectionItem()
  593. {
  594. return (0 === strpos($this->currentLine, '- '));
  595. }
  596. }