Parser.php 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308
  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. use Symfony\Component\Yaml\Tag\TaggedValue;
  13. /**
  14. * Parser parses YAML strings to convert them to PHP arrays.
  15. *
  16. * @author Fabien Potencier <fabien@symfony.com>
  17. *
  18. * @final
  19. */
  20. class Parser
  21. {
  22. public const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
  23. public const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
  24. private $filename;
  25. private $offset = 0;
  26. private $numberOfParsedLines = 0;
  27. private $totalNumberOfLines;
  28. private $lines = [];
  29. private $currentLineNb = -1;
  30. private $currentLine = '';
  31. private $refs = [];
  32. private $skippedLineNumbers = [];
  33. private $locallySkippedLineNumbers = [];
  34. private $refsBeingParsed = [];
  35. /**
  36. * Parses a YAML file into a PHP value.
  37. *
  38. * @param string $filename The path to the YAML file to be parsed
  39. * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
  40. *
  41. * @return mixed The YAML converted to a PHP value
  42. *
  43. * @throws ParseException If the file could not be read or the YAML is not valid
  44. */
  45. public function parseFile(string $filename, int $flags = 0)
  46. {
  47. if (!is_file($filename)) {
  48. throw new ParseException(sprintf('File "%s" does not exist.', $filename));
  49. }
  50. if (!is_readable($filename)) {
  51. throw new ParseException(sprintf('File "%s" cannot be read.', $filename));
  52. }
  53. $this->filename = $filename;
  54. try {
  55. return $this->parse(file_get_contents($filename), $flags);
  56. } finally {
  57. $this->filename = null;
  58. }
  59. }
  60. /**
  61. * Parses a YAML string to a PHP value.
  62. *
  63. * @param string $value A YAML string
  64. * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
  65. *
  66. * @return mixed A PHP value
  67. *
  68. * @throws ParseException If the YAML is not valid
  69. */
  70. public function parse(string $value, int $flags = 0)
  71. {
  72. if (false === preg_match('//u', $value)) {
  73. throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
  74. }
  75. $this->refs = [];
  76. $mbEncoding = null;
  77. if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
  78. $mbEncoding = mb_internal_encoding();
  79. mb_internal_encoding('UTF-8');
  80. }
  81. try {
  82. $data = $this->doParse($value, $flags);
  83. } finally {
  84. if (null !== $mbEncoding) {
  85. mb_internal_encoding($mbEncoding);
  86. }
  87. $this->lines = [];
  88. $this->currentLine = '';
  89. $this->numberOfParsedLines = 0;
  90. $this->refs = [];
  91. $this->skippedLineNumbers = [];
  92. $this->locallySkippedLineNumbers = [];
  93. $this->totalNumberOfLines = null;
  94. }
  95. return $data;
  96. }
  97. private function doParse(string $value, int $flags)
  98. {
  99. $this->currentLineNb = -1;
  100. $this->currentLine = '';
  101. $value = $this->cleanup($value);
  102. $this->lines = explode("\n", $value);
  103. $this->numberOfParsedLines = \count($this->lines);
  104. $this->locallySkippedLineNumbers = [];
  105. if (null === $this->totalNumberOfLines) {
  106. $this->totalNumberOfLines = $this->numberOfParsedLines;
  107. }
  108. if (!$this->moveToNextLine()) {
  109. return null;
  110. }
  111. $data = [];
  112. $context = null;
  113. $allowOverwrite = false;
  114. while ($this->isCurrentLineEmpty()) {
  115. if (!$this->moveToNextLine()) {
  116. return null;
  117. }
  118. }
  119. // Resolves the tag and returns if end of the document
  120. if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
  121. return new TaggedValue($tag, '');
  122. }
  123. do {
  124. if ($this->isCurrentLineEmpty()) {
  125. continue;
  126. }
  127. // tab?
  128. if ("\t" === $this->currentLine[0]) {
  129. throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  130. }
  131. Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);
  132. $isRef = $mergeNode = false;
  133. if ('-' === $this->currentLine[0] && self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
  134. if ($context && 'mapping' == $context) {
  135. throw new ParseException('You cannot define a sequence item when in a mapping.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  136. }
  137. $context = 'sequence';
  138. if (isset($values['value']) && '&' === $values['value'][0] && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
  139. $isRef = $matches['ref'];
  140. $this->refsBeingParsed[] = $isRef;
  141. $values['value'] = $matches['value'];
  142. }
  143. if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
  144. throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  145. }
  146. // array
  147. if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
  148. $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true) ?? '', $flags);
  149. } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
  150. $data[] = new TaggedValue(
  151. $subTag,
  152. $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
  153. );
  154. } else {
  155. if (
  156. isset($values['leadspaces'])
  157. && (
  158. '!' === $values['value'][0]
  159. || self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
  160. )
  161. ) {
  162. // this is a compact notation element, add to next block and parse
  163. $block = $values['value'];
  164. if ($this->isNextLineIndented()) {
  165. $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
  166. }
  167. $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
  168. } else {
  169. $data[] = $this->parseValue($values['value'], $flags, $context);
  170. }
  171. }
  172. if ($isRef) {
  173. $this->refs[$isRef] = end($data);
  174. array_pop($this->refsBeingParsed);
  175. }
  176. } elseif (
  177. self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:( ++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
  178. && (false === strpos($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"]))
  179. ) {
  180. if ($context && 'sequence' == $context) {
  181. throw new ParseException('You cannot define a mapping item when in a sequence.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
  182. }
  183. $context = 'mapping';
  184. try {
  185. $key = Inline::parseScalar($values['key']);
  186. } catch (ParseException $e) {
  187. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  188. $e->setSnippet($this->currentLine);
  189. throw $e;
  190. }
  191. if (!\is_string($key) && !\is_int($key)) {
  192. throw new ParseException(sprintf('%s keys are not supported. Quote your evaluable mapping keys instead.', is_numeric($key) ? 'Numeric' : 'Non-string'), $this->getRealCurrentLineNb() + 1, $this->currentLine);
  193. }
  194. // Convert float keys to strings, to avoid being converted to integers by PHP
  195. if (\is_float($key)) {
  196. $key = (string) $key;
  197. }
  198. if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
  199. $mergeNode = true;
  200. $allowOverwrite = true;
  201. if (isset($values['value'][0]) && '*' === $values['value'][0]) {
  202. $refName = substr(rtrim($values['value']), 1);
  203. if (!\array_key_exists($refName, $this->refs)) {
  204. if (false !== $pos = array_search($refName, $this->refsBeingParsed, true)) {
  205. throw new ParseException(sprintf('Circular reference [%s, %s] detected for reference "%s".', implode(', ', \array_slice($this->refsBeingParsed, $pos)), $refName, $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename);
  206. }
  207. throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  208. }
  209. $refValue = $this->refs[$refName];
  210. if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
  211. $refValue = (array) $refValue;
  212. }
  213. if (!\is_array($refValue)) {
  214. throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  215. }
  216. $data += $refValue; // array union
  217. } else {
  218. if (isset($values['value']) && '' !== $values['value']) {
  219. $value = $values['value'];
  220. } else {
  221. $value = $this->getNextEmbedBlock();
  222. }
  223. $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
  224. if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
  225. $parsed = (array) $parsed;
  226. }
  227. if (!\is_array($parsed)) {
  228. throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  229. }
  230. if (isset($parsed[0])) {
  231. // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
  232. // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
  233. // in the sequence override keys specified in later mapping nodes.
  234. foreach ($parsed as $parsedItem) {
  235. if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
  236. $parsedItem = (array) $parsedItem;
  237. }
  238. if (!\is_array($parsedItem)) {
  239. throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
  240. }
  241. $data += $parsedItem; // array union
  242. }
  243. } else {
  244. // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
  245. // current mapping, unless the key already exists in it.
  246. $data += $parsed; // array union
  247. }
  248. }
  249. } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match('#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u', $values['value'], $matches)) {
  250. $isRef = $matches['ref'];
  251. $this->refsBeingParsed[] = $isRef;
  252. $values['value'] = $matches['value'];
  253. }
  254. $subTag = null;
  255. if ($mergeNode) {
  256. // Merge keys
  257. } elseif (!isset($values['value']) || '' === $values['value'] || '#' === ($values['value'][0] ?? '') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
  258. // hash
  259. // if next line is less indented or equal, then it means that the current value is null
  260. if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
  261. // Spec: Keys MUST be unique; first one wins.
  262. // But overwriting is allowed when a merge node is used in current block.
  263. if ($allowOverwrite || !isset($data[$key])) {
  264. if (null !== $subTag) {
  265. $data[$key] = new TaggedValue($subTag, '');
  266. } else {
  267. $data[$key] = null;
  268. }
  269. } else {
  270. throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
  271. }
  272. } else {
  273. // remember the parsed line number here in case we need it to provide some contexts in error messages below
  274. $realCurrentLineNbKey = $this->getRealCurrentLineNb();
  275. $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
  276. if ('<<' === $key) {
  277. $this->refs[$refMatches['ref']] = $value;
  278. if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
  279. $value = (array) $value;
  280. }
  281. $data += $value;
  282. } elseif ($allowOverwrite || !isset($data[$key])) {
  283. // Spec: Keys MUST be unique; first one wins.
  284. // But overwriting is allowed when a merge node is used in current block.
  285. if (null !== $subTag) {
  286. $data[$key] = new TaggedValue($subTag, $value);
  287. } else {
  288. $data[$key] = $value;
  289. }
  290. } else {
  291. throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $realCurrentLineNbKey + 1, $this->currentLine);
  292. }
  293. }
  294. } else {
  295. $value = $this->parseValue(rtrim($values['value']), $flags, $context);
  296. // Spec: Keys MUST be unique; first one wins.
  297. // But overwriting is allowed when a merge node is used in current block.
  298. if ($allowOverwrite || !isset($data[$key])) {
  299. $data[$key] = $value;
  300. } else {
  301. throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
  302. }
  303. }
  304. if ($isRef) {
  305. $this->refs[$isRef] = $data[$key];
  306. array_pop($this->refsBeingParsed);
  307. }
  308. } elseif ('"' === $this->currentLine[0] || "'" === $this->currentLine[0]) {
  309. if (null !== $context) {
  310. throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  311. }
  312. try {
  313. return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs);
  314. } catch (ParseException $e) {
  315. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  316. $e->setSnippet($this->currentLine);
  317. throw $e;
  318. }
  319. } elseif ('{' === $this->currentLine[0]) {
  320. if (null !== $context) {
  321. throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  322. }
  323. try {
  324. $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs);
  325. while ($this->moveToNextLine()) {
  326. if (!$this->isCurrentLineEmpty()) {
  327. throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  328. }
  329. }
  330. return $parsedMapping;
  331. } catch (ParseException $e) {
  332. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  333. $e->setSnippet($this->currentLine);
  334. throw $e;
  335. }
  336. } elseif ('[' === $this->currentLine[0]) {
  337. if (null !== $context) {
  338. throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  339. }
  340. try {
  341. $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs);
  342. while ($this->moveToNextLine()) {
  343. if (!$this->isCurrentLineEmpty()) {
  344. throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  345. }
  346. }
  347. return $parsedSequence;
  348. } catch (ParseException $e) {
  349. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  350. $e->setSnippet($this->currentLine);
  351. throw $e;
  352. }
  353. } else {
  354. // multiple documents are not supported
  355. if ('---' === $this->currentLine) {
  356. throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
  357. }
  358. if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) {
  359. throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
  360. }
  361. // 1-liner optionally followed by newline(s)
  362. if (\is_string($value) && $this->lines[0] === trim($value)) {
  363. try {
  364. $value = Inline::parse($this->lines[0], $flags, $this->refs);
  365. } catch (ParseException $e) {
  366. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  367. $e->setSnippet($this->currentLine);
  368. throw $e;
  369. }
  370. return $value;
  371. }
  372. // try to parse the value as a multi-line string as a last resort
  373. if (0 === $this->currentLineNb) {
  374. $previousLineWasNewline = false;
  375. $previousLineWasTerminatedWithBackslash = false;
  376. $value = '';
  377. foreach ($this->lines as $line) {
  378. $trimmedLine = trim($line);
  379. if ('#' === ($trimmedLine[0] ?? '')) {
  380. continue;
  381. }
  382. // If the indentation is not consistent at offset 0, it is to be considered as a ParseError
  383. if (0 === $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) {
  384. throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  385. }
  386. if (false !== strpos($line, ': ')) {
  387. throw new ParseException('Mapping values are not allowed in multi-line blocks.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  388. }
  389. if ('' === $trimmedLine) {
  390. $value .= "\n";
  391. } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
  392. $value .= ' ';
  393. }
  394. if ('' !== $trimmedLine && '\\' === $line[-1]) {
  395. $value .= ltrim(substr($line, 0, -1));
  396. } elseif ('' !== $trimmedLine) {
  397. $value .= $trimmedLine;
  398. }
  399. if ('' === $trimmedLine) {
  400. $previousLineWasNewline = true;
  401. $previousLineWasTerminatedWithBackslash = false;
  402. } elseif ('\\' === $line[-1]) {
  403. $previousLineWasNewline = false;
  404. $previousLineWasTerminatedWithBackslash = true;
  405. } else {
  406. $previousLineWasNewline = false;
  407. $previousLineWasTerminatedWithBackslash = false;
  408. }
  409. }
  410. try {
  411. return Inline::parse(trim($value));
  412. } catch (ParseException $e) {
  413. // fall-through to the ParseException thrown below
  414. }
  415. }
  416. throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  417. }
  418. } while ($this->moveToNextLine());
  419. if (null !== $tag) {
  420. $data = new TaggedValue($tag, $data);
  421. }
  422. if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && 'mapping' === $context && !\is_object($data)) {
  423. $object = new \stdClass();
  424. foreach ($data as $key => $value) {
  425. $object->$key = $value;
  426. }
  427. $data = $object;
  428. }
  429. return empty($data) ? null : $data;
  430. }
  431. private function parseBlock(int $offset, string $yaml, int $flags)
  432. {
  433. $skippedLineNumbers = $this->skippedLineNumbers;
  434. foreach ($this->locallySkippedLineNumbers as $lineNumber) {
  435. if ($lineNumber < $offset) {
  436. continue;
  437. }
  438. $skippedLineNumbers[] = $lineNumber;
  439. }
  440. $parser = new self();
  441. $parser->offset = $offset;
  442. $parser->totalNumberOfLines = $this->totalNumberOfLines;
  443. $parser->skippedLineNumbers = $skippedLineNumbers;
  444. $parser->refs = &$this->refs;
  445. $parser->refsBeingParsed = $this->refsBeingParsed;
  446. return $parser->doParse($yaml, $flags);
  447. }
  448. /**
  449. * Returns the current line number (takes the offset into account).
  450. *
  451. * @internal
  452. *
  453. * @return int The current line number
  454. */
  455. public function getRealCurrentLineNb(): int
  456. {
  457. $realCurrentLineNumber = $this->currentLineNb + $this->offset;
  458. foreach ($this->skippedLineNumbers as $skippedLineNumber) {
  459. if ($skippedLineNumber > $realCurrentLineNumber) {
  460. break;
  461. }
  462. ++$realCurrentLineNumber;
  463. }
  464. return $realCurrentLineNumber;
  465. }
  466. /**
  467. * Returns the current line indentation.
  468. *
  469. * @return int The current line indentation
  470. */
  471. private function getCurrentLineIndentation(): int
  472. {
  473. if (' ' !== ($this->currentLine[0] ?? '')) {
  474. return 0;
  475. }
  476. return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
  477. }
  478. /**
  479. * Returns the next embed block of YAML.
  480. *
  481. * @param int|null $indentation The indent level at which the block is to be read, or null for default
  482. * @param bool $inSequence True if the enclosing data structure is a sequence
  483. *
  484. * @return string A YAML string
  485. *
  486. * @throws ParseException When indentation problem are detected
  487. */
  488. private function getNextEmbedBlock(int $indentation = null, bool $inSequence = false): string
  489. {
  490. $oldLineIndentation = $this->getCurrentLineIndentation();
  491. if (!$this->moveToNextLine()) {
  492. return '';
  493. }
  494. if (null === $indentation) {
  495. $newIndent = null;
  496. $movements = 0;
  497. do {
  498. $EOF = false;
  499. // empty and comment-like lines do not influence the indentation depth
  500. if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  501. $EOF = !$this->moveToNextLine();
  502. if (!$EOF) {
  503. ++$movements;
  504. }
  505. } else {
  506. $newIndent = $this->getCurrentLineIndentation();
  507. }
  508. } while (!$EOF && null === $newIndent);
  509. for ($i = 0; $i < $movements; ++$i) {
  510. $this->moveToPreviousLine();
  511. }
  512. $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
  513. if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
  514. throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  515. }
  516. } else {
  517. $newIndent = $indentation;
  518. }
  519. $data = [];
  520. if ($this->getCurrentLineIndentation() >= $newIndent) {
  521. $data[] = substr($this->currentLine, $newIndent);
  522. } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
  523. $data[] = $this->currentLine;
  524. } else {
  525. $this->moveToPreviousLine();
  526. return '';
  527. }
  528. if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
  529. // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
  530. // and therefore no nested list or mapping
  531. $this->moveToPreviousLine();
  532. return '';
  533. }
  534. $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
  535. $isItComment = $this->isCurrentLineComment();
  536. while ($this->moveToNextLine()) {
  537. if ($isItComment && !$isItUnindentedCollection) {
  538. $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
  539. $isItComment = $this->isCurrentLineComment();
  540. }
  541. $indent = $this->getCurrentLineIndentation();
  542. if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
  543. $this->moveToPreviousLine();
  544. break;
  545. }
  546. if ($this->isCurrentLineBlank()) {
  547. $data[] = substr($this->currentLine, $newIndent);
  548. continue;
  549. }
  550. if ($indent >= $newIndent) {
  551. $data[] = substr($this->currentLine, $newIndent);
  552. } elseif ($this->isCurrentLineComment()) {
  553. $data[] = $this->currentLine;
  554. } elseif (0 == $indent) {
  555. $this->moveToPreviousLine();
  556. break;
  557. } else {
  558. throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
  559. }
  560. }
  561. return implode("\n", $data);
  562. }
  563. private function hasMoreLines(): bool
  564. {
  565. return (\count($this->lines) - 1) > $this->currentLineNb;
  566. }
  567. /**
  568. * Moves the parser to the next line.
  569. */
  570. private function moveToNextLine(): bool
  571. {
  572. if ($this->currentLineNb >= $this->numberOfParsedLines - 1) {
  573. return false;
  574. }
  575. $this->currentLine = $this->lines[++$this->currentLineNb];
  576. return true;
  577. }
  578. /**
  579. * Moves the parser to the previous line.
  580. */
  581. private function moveToPreviousLine(): bool
  582. {
  583. if ($this->currentLineNb < 1) {
  584. return false;
  585. }
  586. $this->currentLine = $this->lines[--$this->currentLineNb];
  587. return true;
  588. }
  589. /**
  590. * Parses a YAML value.
  591. *
  592. * @param string $value A YAML value
  593. * @param int $flags A bit field of PARSE_* constants to customize the YAML parser behavior
  594. * @param string $context The parser context (either sequence or mapping)
  595. *
  596. * @return mixed A PHP value
  597. *
  598. * @throws ParseException When reference does not exist
  599. */
  600. private function parseValue(string $value, int $flags, string $context)
  601. {
  602. if ('*' === ($value[0] ?? '')) {
  603. if (false !== $pos = strpos($value, '#')) {
  604. $value = substr($value, 1, $pos - 2);
  605. } else {
  606. $value = substr($value, 1);
  607. }
  608. if (!\array_key_exists($value, $this->refs)) {
  609. if (false !== $pos = array_search($value, $this->refsBeingParsed, true)) {
  610. throw new ParseException(sprintf('Circular reference [%s, %s] detected for reference "%s".', implode(', ', \array_slice($this->refsBeingParsed, $pos)), $value, $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
  611. }
  612. throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
  613. }
  614. return $this->refs[$value];
  615. }
  616. if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
  617. $modifiers = $matches['modifiers'] ?? '';
  618. $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers));
  619. if ('' !== $matches['tag'] && '!' !== $matches['tag']) {
  620. if ('!!binary' === $matches['tag']) {
  621. return Inline::evaluateBinaryScalar($data);
  622. }
  623. return new TaggedValue(substr($matches['tag'], 1), $data);
  624. }
  625. return $data;
  626. }
  627. try {
  628. if ('' !== $value && '{' === $value[0]) {
  629. $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
  630. return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs);
  631. } elseif ('' !== $value && '[' === $value[0]) {
  632. $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
  633. return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs);
  634. }
  635. switch ($value[0] ?? '') {
  636. case '"':
  637. case "'":
  638. $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
  639. $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs);
  640. if (isset($this->currentLine[$cursor]) && preg_replace('/\s*(#.*)?$/A', '', substr($this->currentLine, $cursor))) {
  641. throw new ParseException(sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor)));
  642. }
  643. return $parsedValue;
  644. default:
  645. $lines = [];
  646. while ($this->moveToNextLine()) {
  647. // unquoted strings end before the first unindented line
  648. if (0 === $this->getCurrentLineIndentation()) {
  649. $this->moveToPreviousLine();
  650. break;
  651. }
  652. $lines[] = trim($this->currentLine);
  653. }
  654. for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
  655. if ('' === $lines[$i]) {
  656. $value .= "\n";
  657. $previousLineBlank = true;
  658. } elseif ($previousLineBlank) {
  659. $value .= $lines[$i];
  660. $previousLineBlank = false;
  661. } else {
  662. $value .= ' '.$lines[$i];
  663. $previousLineBlank = false;
  664. }
  665. }
  666. Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
  667. $parsedValue = Inline::parse($value, $flags, $this->refs);
  668. if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
  669. throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
  670. }
  671. return $parsedValue;
  672. }
  673. } catch (ParseException $e) {
  674. $e->setParsedLine($this->getRealCurrentLineNb() + 1);
  675. $e->setSnippet($this->currentLine);
  676. throw $e;
  677. }
  678. }
  679. /**
  680. * Parses a block scalar.
  681. *
  682. * @param string $style The style indicator that was used to begin this block scalar (| or >)
  683. * @param string $chomping The chomping indicator that was used to begin this block scalar (+ or -)
  684. * @param int $indentation The indentation indicator that was used to begin this block scalar
  685. */
  686. private function parseBlockScalar(string $style, string $chomping = '', int $indentation = 0): string
  687. {
  688. $notEOF = $this->moveToNextLine();
  689. if (!$notEOF) {
  690. return '';
  691. }
  692. $isCurrentLineBlank = $this->isCurrentLineBlank();
  693. $blockLines = [];
  694. // leading blank lines are consumed before determining indentation
  695. while ($notEOF && $isCurrentLineBlank) {
  696. // newline only if not EOF
  697. if ($notEOF = $this->moveToNextLine()) {
  698. $blockLines[] = '';
  699. $isCurrentLineBlank = $this->isCurrentLineBlank();
  700. }
  701. }
  702. // determine indentation if not specified
  703. if (0 === $indentation) {
  704. $currentLineLength = \strlen($this->currentLine);
  705. for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) {
  706. ++$indentation;
  707. }
  708. }
  709. if ($indentation > 0) {
  710. $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
  711. while (
  712. $notEOF && (
  713. $isCurrentLineBlank ||
  714. self::preg_match($pattern, $this->currentLine, $matches)
  715. )
  716. ) {
  717. if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
  718. $blockLines[] = substr($this->currentLine, $indentation);
  719. } elseif ($isCurrentLineBlank) {
  720. $blockLines[] = '';
  721. } else {
  722. $blockLines[] = $matches[1];
  723. }
  724. // newline only if not EOF
  725. if ($notEOF = $this->moveToNextLine()) {
  726. $isCurrentLineBlank = $this->isCurrentLineBlank();
  727. }
  728. }
  729. } elseif ($notEOF) {
  730. $blockLines[] = '';
  731. }
  732. if ($notEOF) {
  733. $blockLines[] = '';
  734. $this->moveToPreviousLine();
  735. } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
  736. $blockLines[] = '';
  737. }
  738. // folded style
  739. if ('>' === $style) {
  740. $text = '';
  741. $previousLineIndented = false;
  742. $previousLineBlank = false;
  743. for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) {
  744. if ('' === $blockLines[$i]) {
  745. $text .= "\n";
  746. $previousLineIndented = false;
  747. $previousLineBlank = true;
  748. } elseif (' ' === $blockLines[$i][0]) {
  749. $text .= "\n".$blockLines[$i];
  750. $previousLineIndented = true;
  751. $previousLineBlank = false;
  752. } elseif ($previousLineIndented) {
  753. $text .= "\n".$blockLines[$i];
  754. $previousLineIndented = false;
  755. $previousLineBlank = false;
  756. } elseif ($previousLineBlank || 0 === $i) {
  757. $text .= $blockLines[$i];
  758. $previousLineIndented = false;
  759. $previousLineBlank = false;
  760. } else {
  761. $text .= ' '.$blockLines[$i];
  762. $previousLineIndented = false;
  763. $previousLineBlank = false;
  764. }
  765. }
  766. } else {
  767. $text = implode("\n", $blockLines);
  768. }
  769. // deal with trailing newlines
  770. if ('' === $chomping) {
  771. $text = preg_replace('/\n+$/', "\n", $text);
  772. } elseif ('-' === $chomping) {
  773. $text = preg_replace('/\n+$/', '', $text);
  774. }
  775. return $text;
  776. }
  777. /**
  778. * Returns true if the next line is indented.
  779. *
  780. * @return bool Returns true if the next line is indented, false otherwise
  781. */
  782. private function isNextLineIndented(): bool
  783. {
  784. $currentIndentation = $this->getCurrentLineIndentation();
  785. $movements = 0;
  786. do {
  787. $EOF = !$this->moveToNextLine();
  788. if (!$EOF) {
  789. ++$movements;
  790. }
  791. } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  792. if ($EOF) {
  793. return false;
  794. }
  795. $ret = $this->getCurrentLineIndentation() > $currentIndentation;
  796. for ($i = 0; $i < $movements; ++$i) {
  797. $this->moveToPreviousLine();
  798. }
  799. return $ret;
  800. }
  801. /**
  802. * Returns true if the current line is blank or if it is a comment line.
  803. *
  804. * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
  805. */
  806. private function isCurrentLineEmpty(): bool
  807. {
  808. return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
  809. }
  810. /**
  811. * Returns true if the current line is blank.
  812. *
  813. * @return bool Returns true if the current line is blank, false otherwise
  814. */
  815. private function isCurrentLineBlank(): bool
  816. {
  817. return '' === $this->currentLine || '' === trim($this->currentLine, ' ');
  818. }
  819. /**
  820. * Returns true if the current line is a comment line.
  821. *
  822. * @return bool Returns true if the current line is a comment line, false otherwise
  823. */
  824. private function isCurrentLineComment(): bool
  825. {
  826. //checking explicitly the first char of the trim is faster than loops or strpos
  827. $ltrimmedLine = '' !== $this->currentLine && ' ' === $this->currentLine[0] ? ltrim($this->currentLine, ' ') : $this->currentLine;
  828. return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
  829. }
  830. private function isCurrentLineLastLineInDocument(): bool
  831. {
  832. return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
  833. }
  834. /**
  835. * Cleanups a YAML string to be parsed.
  836. *
  837. * @param string $value The input YAML string
  838. *
  839. * @return string A cleaned up YAML string
  840. */
  841. private function cleanup(string $value): string
  842. {
  843. $value = str_replace(["\r\n", "\r"], "\n", $value);
  844. // strip YAML header
  845. $count = 0;
  846. $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
  847. $this->offset += $count;
  848. // remove leading comments
  849. $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
  850. if (1 === $count) {
  851. // items have been removed, update the offset
  852. $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
  853. $value = $trimmedValue;
  854. }
  855. // remove start of the document marker (---)
  856. $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
  857. if (1 === $count) {
  858. // items have been removed, update the offset
  859. $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
  860. $value = $trimmedValue;
  861. // remove end of the document marker (...)
  862. $value = preg_replace('#\.\.\.\s*$#', '', $value);
  863. }
  864. return $value;
  865. }
  866. /**
  867. * Returns true if the next line starts unindented collection.
  868. *
  869. * @return bool Returns true if the next line starts unindented collection, false otherwise
  870. */
  871. private function isNextLineUnIndentedCollection(): bool
  872. {
  873. $currentIndentation = $this->getCurrentLineIndentation();
  874. $movements = 0;
  875. do {
  876. $EOF = !$this->moveToNextLine();
  877. if (!$EOF) {
  878. ++$movements;
  879. }
  880. } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
  881. if ($EOF) {
  882. return false;
  883. }
  884. $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
  885. for ($i = 0; $i < $movements; ++$i) {
  886. $this->moveToPreviousLine();
  887. }
  888. return $ret;
  889. }
  890. /**
  891. * Returns true if the string is un-indented collection item.
  892. *
  893. * @return bool Returns true if the string is un-indented collection item, false otherwise
  894. */
  895. private function isStringUnIndentedCollectionItem(): bool
  896. {
  897. return 0 === strncmp($this->currentLine, '- ', 2) || '-' === rtrim($this->currentLine);
  898. }
  899. /**
  900. * A local wrapper for "preg_match" which will throw a ParseException if there
  901. * is an internal error in the PCRE engine.
  902. *
  903. * This avoids us needing to check for "false" every time PCRE is used
  904. * in the YAML engine
  905. *
  906. * @throws ParseException on a PCRE internal error
  907. *
  908. * @see preg_last_error()
  909. *
  910. * @internal
  911. */
  912. public static function preg_match(string $pattern, string $subject, array &$matches = null, int $flags = 0, int $offset = 0): int
  913. {
  914. if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
  915. switch (preg_last_error()) {
  916. case \PREG_INTERNAL_ERROR:
  917. $error = 'Internal PCRE error.';
  918. break;
  919. case \PREG_BACKTRACK_LIMIT_ERROR:
  920. $error = 'pcre.backtrack_limit reached.';
  921. break;
  922. case \PREG_RECURSION_LIMIT_ERROR:
  923. $error = 'pcre.recursion_limit reached.';
  924. break;
  925. case \PREG_BAD_UTF8_ERROR:
  926. $error = 'Malformed UTF-8 data.';
  927. break;
  928. case \PREG_BAD_UTF8_OFFSET_ERROR:
  929. $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
  930. break;
  931. default:
  932. $error = 'Error.';
  933. }
  934. throw new ParseException($error);
  935. }
  936. return $ret;
  937. }
  938. /**
  939. * Trim the tag on top of the value.
  940. *
  941. * Prevent values such as "!foo {quz: bar}" to be considered as
  942. * a mapping block.
  943. */
  944. private function trimTag(string $value): string
  945. {
  946. if ('!' === $value[0]) {
  947. return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
  948. }
  949. return $value;
  950. }
  951. private function getLineTag(string $value, int $flags, bool $nextLineCheck = true): ?string
  952. {
  953. if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
  954. return null;
  955. }
  956. if ($nextLineCheck && !$this->isNextLineIndented()) {
  957. return null;
  958. }
  959. $tag = substr($matches['tag'], 1);
  960. // Built-in tags
  961. if ($tag && '!' === $tag[0]) {
  962. throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
  963. }
  964. if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
  965. return $tag;
  966. }
  967. throw new ParseException(sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
  968. }
  969. private function lexInlineQuotedString(int &$cursor = 0): string
  970. {
  971. $quotation = $this->currentLine[$cursor];
  972. $value = $quotation;
  973. ++$cursor;
  974. $previousLineWasNewline = true;
  975. $previousLineWasTerminatedWithBackslash = false;
  976. $lineNumber = 0;
  977. do {
  978. if (++$lineNumber > 1) {
  979. $cursor += strspn($this->currentLine, ' ', $cursor);
  980. }
  981. if ($this->isCurrentLineBlank()) {
  982. $value .= "\n";
  983. } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
  984. $value .= ' ';
  985. }
  986. for (; \strlen($this->currentLine) > $cursor; ++$cursor) {
  987. switch ($this->currentLine[$cursor]) {
  988. case '\\':
  989. if ("'" === $quotation) {
  990. $value .= '\\';
  991. } elseif (isset($this->currentLine[++$cursor])) {
  992. $value .= '\\'.$this->currentLine[$cursor];
  993. }
  994. break;
  995. case $quotation:
  996. ++$cursor;
  997. if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) {
  998. $value .= "''";
  999. break;
  1000. }
  1001. return $value.$quotation;
  1002. default:
  1003. $value .= $this->currentLine[$cursor];
  1004. }
  1005. }
  1006. if ($this->isCurrentLineBlank()) {
  1007. $previousLineWasNewline = true;
  1008. $previousLineWasTerminatedWithBackslash = false;
  1009. } elseif ('\\' === $this->currentLine[-1]) {
  1010. $previousLineWasNewline = false;
  1011. $previousLineWasTerminatedWithBackslash = true;
  1012. } else {
  1013. $previousLineWasNewline = false;
  1014. $previousLineWasTerminatedWithBackslash = false;
  1015. }
  1016. if ($this->hasMoreLines()) {
  1017. $cursor = 0;
  1018. }
  1019. } while ($this->moveToNextLine());
  1020. throw new ParseException('Malformed inline YAML string');
  1021. }
  1022. private function lexUnquotedString(int &$cursor): string
  1023. {
  1024. $offset = $cursor;
  1025. $cursor += strcspn($this->currentLine, '[]{},: ', $cursor);
  1026. return substr($this->currentLine, $offset, $cursor - $offset);
  1027. }
  1028. private function lexInlineMapping(int &$cursor = 0): string
  1029. {
  1030. return $this->lexInlineStructure($cursor, '}');
  1031. }
  1032. private function lexInlineSequence(int &$cursor = 0): string
  1033. {
  1034. return $this->lexInlineStructure($cursor, ']');
  1035. }
  1036. private function lexInlineStructure(int &$cursor, string $closingTag): string
  1037. {
  1038. $value = $this->currentLine[$cursor];
  1039. ++$cursor;
  1040. do {
  1041. $this->consumeWhitespaces($cursor);
  1042. while (isset($this->currentLine[$cursor])) {
  1043. switch ($this->currentLine[$cursor]) {
  1044. case '"':
  1045. case "'":
  1046. $value .= $this->lexInlineQuotedString($cursor);
  1047. break;
  1048. case ':':
  1049. case ',':
  1050. $value .= $this->currentLine[$cursor];
  1051. ++$cursor;
  1052. break;
  1053. case '{':
  1054. $value .= $this->lexInlineMapping($cursor);
  1055. break;
  1056. case '[':
  1057. $value .= $this->lexInlineSequence($cursor);
  1058. break;
  1059. case $closingTag:
  1060. $value .= $this->currentLine[$cursor];
  1061. ++$cursor;
  1062. return $value;
  1063. case '#':
  1064. break 2;
  1065. default:
  1066. $value .= $this->lexUnquotedString($cursor);
  1067. }
  1068. if ($this->consumeWhitespaces($cursor)) {
  1069. $value .= ' ';
  1070. }
  1071. }
  1072. if ($this->hasMoreLines()) {
  1073. $cursor = 0;
  1074. }
  1075. } while ($this->moveToNextLine());
  1076. throw new ParseException('Malformed inline YAML string');
  1077. }
  1078. private function consumeWhitespaces(int &$cursor): bool
  1079. {
  1080. $whitespacesConsumed = 0;
  1081. do {
  1082. $whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor);
  1083. $whitespacesConsumed += $whitespaceOnlyTokenLength;
  1084. $cursor += $whitespaceOnlyTokenLength;
  1085. if (isset($this->currentLine[$cursor])) {
  1086. return 0 < $whitespacesConsumed;
  1087. }
  1088. if ($this->hasMoreLines()) {
  1089. $cursor = 0;
  1090. }
  1091. } while ($this->moveToNextLine());
  1092. return 0 < $whitespacesConsumed;
  1093. }
  1094. }