private function consumeChar($c) { // valid whitespace characters in JSON (from RFC4627 for JSON) include: // space, horizontal tab, line feed or new line, and carriage return. // thanks: http://stackoverflow.com/questions/16042274/definition-of-whitespace-in-json if (($c === " " || $c === "\t" || $c === "\n" || $c === "\r") && !($this->state === self::STATE_IN_STRING || $this->state === self::STATE_UNICODE || $this->state === self::STATE_START_ESCAPE || $this->state === self::STATE_IN_NUMBER || $this->state === self::STATE_START_DOCUMENT)) { return; } switch ($this->state) { case self::STATE_START_DOCUMENT: $this->listener->onDocumentStart(); if ($c === '[') { $this->startArray(); } elseif ($c === '{') { $this->startObject(); } else { throw new ParsingError($this->lineNumber, $this->charNumber, "Document must start with object or array."); } break; case self::STATE_IN_ARRAY: if ($c === ']') { $this->endArray(); } else { $this->startValue($c); } break; case self::STATE_IN_OBJECT: if ($c === '}') { $this->endObject(); } elseif ($c === '"') { $this->startKey(); } else { throw new ParsingError($this->lineNumber, $this->charNumber, "Start of string expected for object key. Instead got: " . $c); } break; case self::STATE_END_KEY: if ($c !== ':') { throw new ParsingError($this->lineNumber, $this->charNumber, "Expected ':' after key."); } $this->state = self::STATE_AFTER_KEY; break; case self::STATE_AFTER_KEY: $this->startValue($c); break; case self::STATE_IN_STRING: if ($c === '"') { $this->endString(); } elseif ($c === '\\') { $this->state = self::STATE_START_ESCAPE; } elseif ($c < "" || $c === "") { throw new ParsingError($this->lineNumber, $this->charNumber, "Unescaped control character encountered: " . $c); } else { $this->buffer .= $c; } break; case self::STATE_START_ESCAPE: $this->processEscapeCharacter($c); break; case self::STATE_UNICODE: $this->processUnicodeCharacter($c); break; case self::STATE_AFTER_VALUE: $within = end($this->stack); if ($within === self::STACK_OBJECT) { if ($c === '}') { $this->endObject(); } elseif ($c === ',') { $this->state = self::STATE_IN_OBJECT; } else { throw new ParsingError($this->lineNumber, $this->charNumber, "Expected ',' or '}' while parsing object. Got: " . $c); } } elseif ($within === self::STACK_ARRAY) { if ($c === ']') { $this->endArray(); } elseif ($c === ',') { $this->state = self::STATE_IN_ARRAY; } else { throw new ParsingError($this->lineNumber, $this->charNumber, "Expected ',' or ']' while parsing array. Got: " . $c); } } else { throw new ParsingError($this->lineNumber, $this->charNumber, "Finished a literal, but unclear what state to move to. Last state: " . $within); } break; case self::STATE_IN_NUMBER: if (preg_match('/\\d/', $c)) { $this->buffer .= $c; } elseif ($c === '.') { if (strpos($this->buffer, '.') !== false) { throw new ParsingError($this->lineNumber, $this->charNumber, "Cannot have multiple decimal points in a number."); } elseif (stripos($this->buffer, 'e') !== false) { throw new ParsingError($this->lineNumber, $this->charNumber, "Cannot have a decimal point in an exponent."); } $this->buffer .= $c; } elseif ($c === 'e' || $c === 'E') { if (stripos($this->buffer, 'e') !== false) { throw new ParsingError($this->lineNumber, $this->charNumber, "Cannot have multiple exponents in a number."); } $this->buffer .= $c; } elseif ($c === '+' || $c === '-') { $last = mb_substr($this->buffer, -1); if (!($last === 'e' || $last === 'E')) { throw new ParsingError($this->lineNumber, $this->charNumber, "Can only have '+' or '-' after the 'e' or 'E' in a number."); } $this->buffer .= $c; } else { $this->endNumber(); // we have consumed one beyond the end of the number $this->consumeChar($c); } break; case self::STATE_IN_TRUE: $this->buffer .= $c; if (mb_strlen($this->buffer) === 4) { $this->endTrue(); } break; case self::STATE_IN_FALSE: $this->buffer .= $c; if (mb_strlen($this->buffer) === 5) { $this->endFalse(); } break; case self::STATE_IN_NULL: $this->buffer .= $c; if (mb_strlen($this->buffer) === 4) { $this->endNull(); } break; case self::STATE_DONE: throw new ParsingError($this->lineNumber, $this->charNumber, "Expected end of document."); default: throw new ParsingError($this->lineNumber, $this->charNumber, "Internal error. Reached an unknown state: " . $this->state); } }