Created
February 28, 2022 16:54
-
-
Save hrdtbs/4bfc7bd2bc238703f5145803e1435ffe to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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