1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 | 'use strict';
|
21 |
|
22 | const startsWith = (s, c) => s.substr(0, c.length) === c;
|
23 | const endsWith = (s, c) => s.substr(s.length - c.length, c.length) === c;
|
24 | const unwrap = s => s.slice(1, -1);
|
25 | const isObjectString = s => startsWith(s, '{') && endsWith(s, '}');
|
26 | const isArrayString = s => startsWith(s, '[') && endsWith(s, ']');
|
27 | const isQuotedString = s => (startsWith(s, '\'') && endsWith(s, '\'')) || (startsWith(s, '"') && endsWith(s, '"'));
|
28 |
|
29 | const error = (token, string, originalString) => {
|
30 | throw new Error('Unexpected token \'' + token + '\' at position ' + (originalString.length - string.length - 1) + ' in string: \'' + originalString + '\'');
|
31 | };
|
32 |
|
33 | const processToken = (token, string, originalString) => {
|
34 | if (token === 'true' || token === 'false') {
|
35 | return token === 'true';
|
36 | } else if (isQuotedString(token)) {
|
37 | return unwrap(token);
|
38 | } else if (!isNaN(token)) {
|
39 | return +(token);
|
40 | } else if (isObjectString(token)) {
|
41 | return parseObject(unwrap(token));
|
42 | } else if (isArrayString(token)) {
|
43 | return parseArray(unwrap(token));
|
44 | } else {
|
45 | error(token, string, originalString);
|
46 | }
|
47 | };
|
48 |
|
49 | const nextToken = (string) => {
|
50 | string = string.trim();
|
51 | let limit = string.length;
|
52 |
|
53 | if (string[0] === ':' || string[0] === ',') {
|
54 |
|
55 | limit = 1;
|
56 |
|
57 | } else if (string[0] === '{' || string[0] === '[') {
|
58 |
|
59 | const c = string.charCodeAt(0);
|
60 | let nestedObject = 1;
|
61 | for (let i = 1; i < string.length; i++) {
|
62 | if (string.charCodeAt(i) === c) {
|
63 | nestedObject++;
|
64 | } else if (string.charCodeAt(i) === c + 2) {
|
65 | nestedObject--;
|
66 | if (nestedObject === 0) {
|
67 | limit = i + 1;
|
68 | break;
|
69 | }
|
70 | }
|
71 | }
|
72 |
|
73 | } else if (string[0] === '\'' || string[0] === '"') {
|
74 |
|
75 | for (let i = 1; i < string.length; i++) {
|
76 | if (string[i] === string[0]) {
|
77 | limit = i + 1;
|
78 | break;
|
79 | }
|
80 | }
|
81 |
|
82 | } else {
|
83 |
|
84 | for (let i = 1; i < string.length; i++) {
|
85 | if ([' ', ',', ':'].indexOf(string[i]) !== -1) {
|
86 | limit = i;
|
87 | break;
|
88 | }
|
89 | }
|
90 |
|
91 | }
|
92 |
|
93 | return string.slice(0, limit);
|
94 | };
|
95 |
|
96 | const parseObject = (string) => {
|
97 | const isValidKey = key => /^[A-Z_$][A-Z0-9_$]*$/i.test(key);
|
98 |
|
99 | string = string.trim();
|
100 | const originalString = string;
|
101 | const object = {};
|
102 | let readingKey = true, key, previousToken, token;
|
103 |
|
104 | while (string.length > 0) {
|
105 | previousToken = token;
|
106 | token = nextToken(string);
|
107 | string = string.slice(token.length, string.length).trim();
|
108 |
|
109 | if ((token === ':' && (!readingKey || !previousToken || previousToken === ','))
|
110 | || (token === ',' && readingKey)
|
111 | || (token !== ':' && token !== ',' && (previousToken && previousToken !== ',' && previousToken !== ':'))) {
|
112 | error(token, string, originalString);
|
113 | } else if (token === ':' && readingKey && previousToken) {
|
114 | previousToken = isQuotedString(previousToken) ? unwrap(previousToken) : previousToken;
|
115 | if (isValidKey(previousToken)) {
|
116 | key = previousToken;
|
117 | readingKey = false;
|
118 | } else {
|
119 | throw new Error('Invalid key token \'' + previousToken + '\' at position 0 in string: \'' + originalString + '\'');
|
120 | }
|
121 | } else if (token === ',' && !readingKey && previousToken) {
|
122 | object[key] = processToken(previousToken, string, originalString);
|
123 | readingKey = true;
|
124 | }
|
125 | }
|
126 |
|
127 | if (token) {
|
128 | object[key] = processToken(token, string, originalString);
|
129 | }
|
130 |
|
131 | return object;
|
132 | };
|
133 |
|
134 | const parseArray = (string) => {
|
135 | string = string.trim();
|
136 | const originalString = string;
|
137 | const array = [];
|
138 | let previousToken, token;
|
139 |
|
140 | while (string.length > 0) {
|
141 | previousToken = token;
|
142 | token = nextToken(string);
|
143 | string = string.slice(token.length, string.length).trim();
|
144 |
|
145 | if (token === ',' && (!previousToken || previousToken === ',')) {
|
146 | error(token, string, originalString);
|
147 | } else if (token === ',') {
|
148 | array.push(processToken(previousToken, string, originalString));
|
149 | }
|
150 | }
|
151 |
|
152 | if (token) {
|
153 | if (token !== ',') {
|
154 | array.push(processToken(token, string, originalString));
|
155 | } else {
|
156 | error(token, string, originalString);
|
157 | }
|
158 | }
|
159 |
|
160 | return array;
|
161 | };
|
162 |
|
163 | const parse = (string) => {
|
164 | string = string.trim();
|
165 |
|
166 | if (isObjectString(string)) {
|
167 | return parseObject(unwrap(string));
|
168 | } else if (isArrayString(string)) {
|
169 | return parseArray(unwrap(string));
|
170 | } else {
|
171 | throw new Error('Provided string must be object or array like: ' + string);
|
172 | }
|
173 | };
|
174 |
|
175 | export default parse;
|