Skip to content

Instantly share code, notes, and snippets.

@hrdtbs
Created February 28, 2022 16:54
Show Gist options
  • Save hrdtbs/4bfc7bd2bc238703f5145803e1435ffe to your computer and use it in GitHub Desktop.
Save hrdtbs/4bfc7bd2bc238703f5145803e1435ffe to your computer and use it in GitHub Desktop.
const WHITESPACE = [" ", "\t", "\b", "\n", "\r"];
const NUMERICAL_CHARACTERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"];
const EXP_CHARACTERS = ["e", "E"];
//@see https://www.json.org/json-en.html
export const parse = (value: string) => {
let index = 0;
const skip = () => {
index++;
};
const peek = () => {
const char = value[index];
return char;
};
const next = () => {
const char = peek();
skip();
return char;
};
const peekTrimed = () => {
while (index < value.length) {
const char = value[index];
if (WHITESPACE.includes(char)) {
skip();
continue;
}
return char;
}
throw new Error("Not found characters");
};
const nextTrimed = () => {
const char = peekTrimed();
skip();
return char;
};
const parseString = () => {
const char = next();
if (char !== '"') {
throw new Error("String must starts with double quote");
}
let result = "";
while (index < value.length) {
const char = next();
if (char === '"') {
return result;
}
if (char === "\\") {
const char = next();
if (["\\", "/", '"'].includes(char)) {
result += char;
continue;
}
if (char === "b") {
result += "\u{0008}";
continue;
}
if (char === "f") {
result += "\u{000c}";
continue;
}
if (char === "n") {
result += "\n";
continue;
}
if (char === "r") {
result += "\r";
continue;
}
if (char === "t") {
result += "\t";
continue;
}
if (char === "u") {
throw new Error(`Not implemented '\\u'`);
}
throw new Error(`Invalid escaped character: '\\${char}'`);
}
result += char;
}
throw new Error(`String must ends with '"'`);
};
const parseArray = () => {
const start = next();
if (start !== "[") {
throw new Error(`Array must starts with '['`);
}
let result: any[] = [];
const char = peekTrimed();
if (char === "]") {
skip();
return result;
}
while (index < value.length) {
result.push(parseAny());
const char = nextTrimed();
if (char === "]") {
return result;
}
if (char === ",") {
continue;
}
throw new Error(
`',' or ']' is expected for array but actually found '${char}'`
);
}
throw new Error(`Array must ends with ']'`);
};
const parseObject = () => {
const start = next();
if (start !== "{") {
throw new Error(`Object must starts with '{'`);
}
let result: { [key: string]: any } = {};
const char = peekTrimed();
if (char === "}") {
skip();
return result;
}
while (index < value.length) {
const key = parseAny();
if (typeof key !== "string") {
throw new Error("Key must be string");
}
const colon = nextTrimed();
if (colon !== ":") {
throw new Error(
`':' is expected after key of object but actually found '${colon}'`
);
}
result[key] = parseAny();
const char = nextTrimed();
if (char === ",") {
continue;
}
if (char === "}") {
return result;
}
throw new Error(
`',' or '}' is expected for objet but actually found '${char}'`
);
}
throw new Error(`Object must starts with '}'`);
};
const parseNumber = () => {
let result = "";
const char = peek();
if (char === "-") {
result += "-";
skip();
}
let hasDot = false;
let hasExp = false;
while (index < value.length) {
const char = peek();
if (NUMERICAL_CHARACTERS.includes(char)) {
result += char;
skip();
continue;
}
if (char === ".") {
hasDot = true;
break;
}
if (EXP_CHARACTERS.includes(char)) {
hasExp = true;
break;
}
break;
}
if (result === "-" || result.length === 0) {
throw new Error("Integer part does not exist");
}
if (result.startsWith("0") && result.length > 1) {
throw new Error("Integer part must not start with zero, except for '0'");
}
if (hasDot) {
result += next();
while (index < value.length) {
const char = peek();
if (NUMERICAL_CHARACTERS.includes(char)) {
result += char;
skip();
continue;
}
if (EXP_CHARACTERS.includes(char)) {
hasExp = true;
break;
}
break;
}
if (result.endsWith(".")) {
throw new Error("The fractional part of a number must not be empty");
}
}
if (hasExp) {
result += next();
const char = peek();
if (char === "+" || char === "-") {
result += char;
skip();
}
let hasDigit = false;
while (index < value.length) {
const char = peek();
if (NUMERICAL_CHARACTERS.includes(char)) {
result += char;
skip();
hasDigit = true;
continue;
}
break;
}
if (hasDigit === false) {
throw new Error("Exponent part must not be empty in number literal");
}
}
return Number(result);
};
const parseConstant = (constant: string) => {
throw new Error(`Not implemented ${constant}`);
};
const parseTrue = () => {
return parseConstant("true");
};
const parseFalse = () => {
return parseConstant("false");
};
const parseNull = () => {
return parseConstant("null");
};
const parseAny = () => {
const char = peekTrimed();
if (char === '"') {
return parseString();
}
if (NUMERICAL_CHARACTERS.includes(char) || char === "-") {
return parseNumber();
}
if (char === "[") {
return parseArray();
}
if (char === "{") {
return parseObject();
}
if (char === "t") {
return parseTrue();
}
if (char === "f") {
return parseFalse();
}
if (char === "n") {
return parseNull();
}
};
const result = parseAny();
return result;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment