5bf3b20542
otherwise the closing } on non-empty objects is incorrectly indented
734 lines
16 KiB
C++
734 lines
16 KiB
C++
#pragma once
|
|
|
|
#include <vector>
|
|
|
|
#include "StarUnicode.hpp"
|
|
|
|
namespace Star {
|
|
|
|
struct JsonStream {
|
|
virtual ~JsonStream() {}
|
|
|
|
virtual void beginObject() = 0;
|
|
virtual void objectKey(char32_t const*, size_t) = 0;
|
|
virtual void endObject() = 0;
|
|
|
|
virtual void beginArray() = 0;
|
|
virtual void endArray() = 0;
|
|
|
|
virtual void putString(char32_t const*, size_t) = 0;
|
|
virtual void putDouble(char32_t const*, size_t) = 0;
|
|
virtual void putInteger(char32_t const*, size_t) = 0;
|
|
virtual void putBoolean(bool) = 0;
|
|
virtual void putNull() = 0;
|
|
|
|
virtual void putWhitespace(char32_t const*, size_t) = 0;
|
|
virtual void putColon() = 0;
|
|
virtual void putComma() = 0;
|
|
};
|
|
|
|
// Will parse JSON and output to a given JsonStream. Parses an *extension* to
|
|
// the JSON format that includes comments.
|
|
template <typename InputIterator>
|
|
class JsonParser {
|
|
public:
|
|
JsonParser(JsonStream& stream)
|
|
: m_line(0), m_column(0), m_stream(stream) {}
|
|
virtual ~JsonParser() {}
|
|
|
|
// Does not throw. On error, returned iterator will not be equal to end, and
|
|
// error() will be non-null. Set fragment to true to parse any JSON type
|
|
// rather than just object or array.
|
|
InputIterator parse(InputIterator begin, InputIterator end, bool fragment = false) {
|
|
init(begin, end);
|
|
|
|
try {
|
|
white();
|
|
if (fragment)
|
|
value();
|
|
else
|
|
top();
|
|
white();
|
|
} catch (ParsingException const&) {
|
|
}
|
|
|
|
return m_current;
|
|
}
|
|
|
|
// Human readable parsing error, does not include line or column info.
|
|
char const* error() const {
|
|
if (m_error.empty())
|
|
return nullptr;
|
|
else
|
|
return m_error.c_str();
|
|
}
|
|
|
|
size_t line() const {
|
|
return m_line + 1;
|
|
}
|
|
|
|
size_t column() const {
|
|
return m_column + 1;
|
|
}
|
|
|
|
private:
|
|
typedef std::basic_string<char32_t> CharArray;
|
|
|
|
// Thrown internally to abort parsing.
|
|
class ParsingException {};
|
|
|
|
void top() {
|
|
switch (m_char) {
|
|
case '{':
|
|
object();
|
|
break;
|
|
case '[':
|
|
array();
|
|
break;
|
|
default:
|
|
error("expected JSON object or array at top level");
|
|
return;
|
|
}
|
|
}
|
|
|
|
void value() {
|
|
switch (m_char) {
|
|
case '{':
|
|
object();
|
|
break;
|
|
case '[':
|
|
array();
|
|
break;
|
|
case '"':
|
|
string();
|
|
break;
|
|
case '-':
|
|
number();
|
|
break;
|
|
case 0:
|
|
error("unexpected end of stream parsing value");
|
|
return;
|
|
default:
|
|
m_char >= '0' && m_char <= '9' ? number() : word();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void object() {
|
|
if (m_char != '{')
|
|
error("bad object, should be '{'");
|
|
|
|
next();
|
|
m_stream.beginObject();
|
|
|
|
white();
|
|
if (m_char == '}') {
|
|
next();
|
|
m_stream.endObject();
|
|
return;
|
|
}
|
|
|
|
while (true) {
|
|
CharArray s = parseString();
|
|
m_stream.objectKey(s.c_str(), s.length());
|
|
|
|
white();
|
|
if (m_char != ':')
|
|
error("bad object, should be ':'");
|
|
next();
|
|
m_stream.putColon();
|
|
white();
|
|
|
|
value();
|
|
|
|
white();
|
|
if (m_char == '}') {
|
|
next();
|
|
m_stream.endObject();
|
|
return;
|
|
} else if (m_char == ',') {
|
|
next();
|
|
m_stream.putComma();
|
|
white();
|
|
} else if (m_char == 0) {
|
|
error("unexpected end of stream parsing object.");
|
|
} else {
|
|
error("bad object, should be '}' or ','");
|
|
}
|
|
}
|
|
}
|
|
|
|
void array() {
|
|
if (m_char == '[') {
|
|
next();
|
|
m_stream.beginArray();
|
|
white();
|
|
if (m_char == ']') {
|
|
next();
|
|
m_stream.endArray();
|
|
} else {
|
|
while (true) {
|
|
value();
|
|
white();
|
|
if (m_char == ']') {
|
|
next();
|
|
m_stream.endArray();
|
|
break;
|
|
} else if (m_char == ',') {
|
|
next();
|
|
m_stream.putComma();
|
|
white();
|
|
} else if (m_char == 0) {
|
|
error("unexpected end of stream parsing array.");
|
|
} else {
|
|
error("bad array, should be ',' or ']'");
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
error("bad array");
|
|
}
|
|
}
|
|
|
|
void string() {
|
|
CharArray s = parseString();
|
|
m_stream.putString(s.c_str(), s.length());
|
|
}
|
|
|
|
void number() {
|
|
std::basic_string<char32_t> buffer;
|
|
bool isDouble = false;
|
|
|
|
if (m_char == '-') {
|
|
buffer += '-';
|
|
next();
|
|
}
|
|
|
|
if (m_char == '0') {
|
|
buffer += '0';
|
|
next();
|
|
} else if (m_char > '0' && m_char <= '9') {
|
|
while (m_char >= '0' && m_char <= '9') {
|
|
buffer += m_char;
|
|
next();
|
|
}
|
|
} else {
|
|
error("bad number, must start with digit");
|
|
}
|
|
|
|
if (m_char == '.') {
|
|
isDouble = true;
|
|
buffer += '.';
|
|
next();
|
|
while (m_char >= '0' && m_char <= '9') {
|
|
buffer += m_char;
|
|
next();
|
|
}
|
|
}
|
|
|
|
if (m_char == 'e' || m_char == 'E') {
|
|
isDouble = true;
|
|
buffer += m_char;
|
|
next();
|
|
if (m_char == '-' || m_char == '+') {
|
|
buffer += m_char;
|
|
next();
|
|
}
|
|
while (m_char >= '0' && m_char <= '9') {
|
|
buffer += m_char;
|
|
next();
|
|
}
|
|
}
|
|
|
|
if (isDouble) {
|
|
try {
|
|
m_stream.putDouble(buffer.c_str(), buffer.length());
|
|
} catch (std::exception const& e) {
|
|
error(std::string("Bad double: ") + e.what());
|
|
}
|
|
} else {
|
|
try {
|
|
m_stream.putInteger(buffer.c_str(), buffer.length());
|
|
} catch (std::exception const& e) {
|
|
error(std::string("Bad integer: ") + e.what());
|
|
}
|
|
}
|
|
}
|
|
|
|
// true, false, or null
|
|
void word() {
|
|
switch (m_char) {
|
|
case 't':
|
|
next();
|
|
check('r');
|
|
check('u');
|
|
check('e');
|
|
m_stream.putBoolean(true);
|
|
break;
|
|
case 'f':
|
|
next();
|
|
check('a');
|
|
check('l');
|
|
check('s');
|
|
check('e');
|
|
m_stream.putBoolean(false);
|
|
break;
|
|
case 'n':
|
|
next();
|
|
check('u');
|
|
check('l');
|
|
check('l');
|
|
m_stream.putNull();
|
|
break;
|
|
default:
|
|
error("unexpected character parsing word");
|
|
return;
|
|
}
|
|
}
|
|
|
|
CharArray parseString() {
|
|
if (m_char != '"')
|
|
error("bad string, should be '\"'");
|
|
next();
|
|
|
|
CharArray str;
|
|
|
|
while (true) {
|
|
if (m_char == '\\') {
|
|
next();
|
|
if (m_char == 'u') {
|
|
std::string hexString;
|
|
next();
|
|
for (int i = 0; i < 4; ++i) {
|
|
hexString.push_back(m_char);
|
|
next();
|
|
}
|
|
char32_t codepoint = hexStringToUtf32(hexString);
|
|
if (isUtf16LeadSurrogate(codepoint)) {
|
|
check('\\');
|
|
check('u');
|
|
hexString.clear();
|
|
for (int i = 0; i < 4; ++i) {
|
|
hexString.push_back(m_char);
|
|
next();
|
|
}
|
|
codepoint = hexStringToUtf32(hexString, codepoint);
|
|
}
|
|
str += codepoint;
|
|
} else {
|
|
switch (m_char) {
|
|
case '"':
|
|
str += '"';
|
|
break;
|
|
case '\\':
|
|
str += '\\';
|
|
break;
|
|
case '/':
|
|
str += '/';
|
|
break;
|
|
case 'b':
|
|
str += '\b';
|
|
break;
|
|
case 'f':
|
|
str += '\f';
|
|
break;
|
|
case 'n':
|
|
str += '\n';
|
|
break;
|
|
case 'r':
|
|
str += '\r';
|
|
break;
|
|
case 't':
|
|
str += '\t';
|
|
break;
|
|
default:
|
|
error("bad string escape character");
|
|
break;
|
|
}
|
|
next();
|
|
}
|
|
} else if (m_char == '\"') {
|
|
next();
|
|
return str;
|
|
} else if (m_char == 0) {
|
|
error("unexpected end of stream reading string!");
|
|
} else {
|
|
str += m_char;
|
|
next();
|
|
}
|
|
}
|
|
error("parser bug");
|
|
return {};
|
|
}
|
|
|
|
// Checks current char then moves on to the next one
|
|
void check(char32_t c) {
|
|
if (m_char == 0)
|
|
error("unexpected end of stream parsing word");
|
|
if (m_char != c)
|
|
error("unexpected character in word");
|
|
next();
|
|
}
|
|
|
|
void init(InputIterator begin, InputIterator end) {
|
|
m_current = begin;
|
|
m_end = end;
|
|
m_line = 0;
|
|
m_column = 0;
|
|
|
|
if (m_current != m_end)
|
|
m_char = *m_current;
|
|
else
|
|
m_char = 0;
|
|
}
|
|
|
|
// Consumes next character.
|
|
void next() {
|
|
if (m_current == m_end)
|
|
return;
|
|
|
|
if (m_char == '\n') {
|
|
++m_line;
|
|
m_column = 0;
|
|
} else {
|
|
++m_column;
|
|
}
|
|
++m_current;
|
|
|
|
if (m_current != m_end)
|
|
m_char = *m_current;
|
|
else
|
|
m_char = 0;
|
|
}
|
|
|
|
// Will skip whitespace and comments between tokens.
|
|
void white() {
|
|
CharArray buffer;
|
|
while (m_current != m_end) {
|
|
if (m_char == '/') {
|
|
// Always consume '/' found in whitespace, because that is never valid
|
|
// JSON (other than comments)
|
|
buffer += m_char;
|
|
next();
|
|
if (m_current != m_end && m_char == '/') {
|
|
// eat "/"
|
|
buffer += m_char;
|
|
next();
|
|
|
|
// Read '//' style comments up until eol/eof.
|
|
while (m_current != m_end && m_char != '\n') {
|
|
buffer += m_char;
|
|
next();
|
|
}
|
|
} else if (m_current != m_end && m_char == '*') {
|
|
// eat "*"
|
|
buffer += m_char;
|
|
next();
|
|
|
|
// Read '/*' style comments up until '*/'.
|
|
while (m_current != m_end) {
|
|
if (m_char == '*') {
|
|
buffer += m_char;
|
|
next();
|
|
if (m_char == '/') {
|
|
buffer += m_char;
|
|
next();
|
|
break;
|
|
}
|
|
} else {
|
|
buffer += m_char;
|
|
next();
|
|
if (m_current == m_end)
|
|
error("/* comment has no matching */");
|
|
}
|
|
}
|
|
} else {
|
|
// The only allowed characters following / in whitespace are / and *
|
|
error("/ character in whitespace is not follwed by '/' or '*', invalid comment");
|
|
return;
|
|
}
|
|
} else if (isSpace(m_char)) {
|
|
buffer += m_char;
|
|
next();
|
|
} else {
|
|
if (buffer.size() != 0)
|
|
m_stream.putWhitespace(buffer.c_str(), buffer.length());
|
|
return;
|
|
}
|
|
}
|
|
if (buffer.size() != 0)
|
|
m_stream.putWhitespace(buffer.c_str(), buffer.length());
|
|
}
|
|
|
|
void error(std::string msg) {
|
|
m_error = std::move(msg);
|
|
throw ParsingException();
|
|
}
|
|
|
|
bool isSpace(char32_t c) {
|
|
// Only whitespace allowed by JSON
|
|
return c == 0x20 || // space
|
|
c == 0x09 || // horizontal tab
|
|
c == 0x0a || // newline
|
|
c == 0x0d || // carriage return
|
|
c == 0xfeff; // BOM or ZWNBSP
|
|
}
|
|
|
|
char32_t m_char;
|
|
InputIterator m_current;
|
|
InputIterator m_end;
|
|
size_t m_line;
|
|
size_t m_column;
|
|
std::string m_error;
|
|
JsonStream& m_stream;
|
|
};
|
|
|
|
// Write JSON through JsonStream interface.
|
|
template <typename OutputIterator>
|
|
class JsonWriter : public JsonStream {
|
|
public:
|
|
JsonWriter(OutputIterator out, unsigned pretty = 0)
|
|
: m_out(out), m_pretty(pretty) {}
|
|
|
|
void beginObject() {
|
|
startValue();
|
|
pushState(Object);
|
|
write('{');
|
|
}
|
|
|
|
void objectKey(char32_t const* s, size_t len) {
|
|
if (currentState() == ObjectElement) {
|
|
if (m_pretty)
|
|
write('\n');
|
|
indent();
|
|
} else {
|
|
pushState(ObjectElement);
|
|
if (m_pretty)
|
|
write('\n');
|
|
indent();
|
|
}
|
|
|
|
write('"');
|
|
char32_t c = *s;
|
|
while (c && len) {
|
|
write(c);
|
|
c = *++s;
|
|
--len;
|
|
}
|
|
write('"');
|
|
if (m_pretty)
|
|
write(' ');
|
|
}
|
|
|
|
void endObject() {
|
|
bool isNotEmpty = currentState() == ObjectElement;
|
|
popState(Object);
|
|
if (isNotEmpty) {
|
|
if (m_pretty)
|
|
write('\n');
|
|
indent();
|
|
}
|
|
write('}');
|
|
}
|
|
|
|
void beginArray() {
|
|
startValue();
|
|
pushState(Array);
|
|
write('[');
|
|
}
|
|
|
|
void endArray() {
|
|
popState(Array);
|
|
write(']');
|
|
}
|
|
|
|
void putString(char32_t const* s, size_t len) {
|
|
startValue();
|
|
|
|
write('"');
|
|
char32_t c = *s;
|
|
while (c && (len > 0)) {
|
|
if (!isPrintable(c)) {
|
|
switch (c) {
|
|
case '"':
|
|
write('\\');
|
|
write('"');
|
|
break;
|
|
case '\\':
|
|
write('\\');
|
|
write('\\');
|
|
break;
|
|
case '\b':
|
|
write('\\');
|
|
write('b');
|
|
break;
|
|
case '\f':
|
|
write('\\');
|
|
write('f');
|
|
break;
|
|
case '\n':
|
|
write('\\');
|
|
write('n');
|
|
break;
|
|
case '\r':
|
|
write('\\');
|
|
write('r');
|
|
break;
|
|
case '\t':
|
|
write('\\');
|
|
write('t');
|
|
break;
|
|
default:
|
|
auto hex = hexStringFromUtf32(c);
|
|
if (hex.size() == 4) {
|
|
write('\\');
|
|
write('u');
|
|
for (auto c : hex) {
|
|
write(c);
|
|
}
|
|
} else if (hex.size() == 8) {
|
|
write('\\');
|
|
write('u');
|
|
for (auto c : hex.substr(0, 4)) {
|
|
write(c);
|
|
}
|
|
write('\\');
|
|
write('u');
|
|
for (auto c : hex.substr(4)) {
|
|
write(c);
|
|
}
|
|
} else {
|
|
throw UnicodeException("Internal Error: Received invalid unicode hex from hexStringFromUtf32.");
|
|
}
|
|
break;
|
|
}
|
|
} else {
|
|
write(c);
|
|
}
|
|
c = *++s;
|
|
--len;
|
|
}
|
|
write('"');
|
|
}
|
|
|
|
void putDouble(char32_t const* s, size_t len) {
|
|
startValue();
|
|
for (size_t i = 0; i < len; ++i)
|
|
write(s[i]);
|
|
}
|
|
|
|
void putInteger(char32_t const* s, size_t len) {
|
|
startValue();
|
|
for (size_t i = 0; i < len; ++i)
|
|
write(s[i]);
|
|
}
|
|
|
|
void putBoolean(bool b) {
|
|
startValue();
|
|
if (b) {
|
|
write('t');
|
|
write('r');
|
|
write('u');
|
|
write('e');
|
|
} else {
|
|
write('f');
|
|
write('a');
|
|
write('l');
|
|
write('s');
|
|
write('e');
|
|
}
|
|
}
|
|
|
|
void putNull() {
|
|
startValue();
|
|
write('n');
|
|
write('u');
|
|
write('l');
|
|
write('l');
|
|
}
|
|
|
|
void putWhitespace(char32_t const* s, size_t len) {
|
|
// If m_pretty is true, extra spurious whitespace will be inserted.
|
|
for (size_t i = 0; i < len; ++i)
|
|
write(s[i]);
|
|
}
|
|
|
|
void putColon() {
|
|
write(':');
|
|
if (m_pretty)
|
|
write(' ');
|
|
}
|
|
|
|
void putComma() {
|
|
write(',');
|
|
}
|
|
|
|
private:
|
|
enum State {
|
|
Top,
|
|
Object,
|
|
ObjectElement,
|
|
Array,
|
|
ArrayElement
|
|
};
|
|
|
|
// Handles separating array elements if currently adding to an array
|
|
void startValue() {
|
|
if (currentState() == ArrayElement) {
|
|
if (m_pretty)
|
|
write(' ');
|
|
} else if (currentState() == Array) {
|
|
pushState(ArrayElement);
|
|
}
|
|
}
|
|
|
|
void indent() {
|
|
for (unsigned i = 0; i < m_state.size() / 2; ++i) {
|
|
for (unsigned j = 0; j < m_pretty; ++j) {
|
|
write(' ');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Push state onto stack.
|
|
void pushState(State state) {
|
|
m_state.push_back(state);
|
|
}
|
|
|
|
// Pop state stack down to given state.
|
|
void popState(State state) {
|
|
while (true) {
|
|
if (m_state.empty())
|
|
return;
|
|
|
|
State last = currentState();
|
|
m_state.pop_back();
|
|
if (last == state)
|
|
return;
|
|
}
|
|
}
|
|
|
|
State currentState() {
|
|
if (m_state.empty())
|
|
return Top;
|
|
else
|
|
return *prev(m_state.end());
|
|
}
|
|
|
|
void write(char32_t c) {
|
|
*m_out = c;
|
|
++m_out;
|
|
}
|
|
|
|
// Only chars that are unescaped according to JSON spec.
|
|
bool isPrintable(char32_t c) {
|
|
return (c >= 0x20 && c <= 0x21) || (c >= 0x23 && c <= 0x5b) || (c >= 0x5d && c <= 0x10ffff);
|
|
}
|
|
|
|
OutputIterator m_out;
|
|
unsigned m_pretty;
|
|
std::vector<State> m_state;
|
|
};
|
|
|
|
}
|