UNPKG

6.97 kBJavaScriptView Raw
1'use strict';
2
3// A valid output which means nothing has been parsed.
4// Used as error return / invalid output
5const nothingHappend = {
6 prop: {},
7 eaten: '',
8};
9
10const defaultConfig = {
11 defaultValue: () => undefined, // Its a function
12};
13
14function parse(value, indexNext, userConfig) {
15// Main function
16 let letsEat = '';
17 let stopOnBrace = false;
18 let errorDetected = false;
19 const config = {...defaultConfig, ...userConfig};
20
21 // Make defaultValue a function if it isn't
22 if (typeof (config.defaultValue) !== 'function') {
23 const {defaultValue} = config;
24 config.defaultValue = () => defaultValue;
25 }
26
27 const prop = {};
28
29 /* They are at least one label and at best two */
30 /* ekqsdf <- one label
31 * qsdfqsfd=qsdfqsdf <- two */
32 let labelFirst = '';
33 let labelSecond;
34
35 if (indexNext === undefined) {
36 indexNext = 0;
37 }
38
39 /* 3 types :
40 * .azcv <- class
41 * #poi <- id
42 * dfgh=zert <- key
43 * jkj <- this is also a key but with a user defined value (default is undefined)
44 * jkj= <- this is also a key but with a empty value
45 */
46 let type;
47 const forbidenCharacters = '\n\r{}';
48
49 // A function that detect if it's time to end the parsing
50 const shouldStop = function () {
51 if (indexNext >= value.length || forbidenCharacters.indexOf(value[indexNext]) > -1) {
52 if (stopOnBrace && value[indexNext] !== '}') {
53 errorDetected = true;
54 }
55
56 return true;
57 }
58
59 return value[indexNext] === '}' && stopOnBrace;
60 };
61
62 let eaten = '';
63 // Couple of functions that parse same kinds of characters
64 // Used to parse spaces or identifiers
65 const eat = chars => {
66 eaten = '';
67
68 while (indexNext < value.length &&
69 forbidenCharacters.indexOf(value.charAt(indexNext)) < 0 &&
70 chars.indexOf(value.charAt(indexNext)) >= 0) {
71 letsEat += value.charAt(indexNext);
72 eaten += value.charAt(indexNext);
73 indexNext++;
74 }
75
76 return shouldStop();
77 };
78
79 const eatUntil = chars => {
80 eaten = '';
81
82 while (indexNext < value.length &&
83 forbidenCharacters.indexOf(value.charAt(indexNext)) < 0 &&
84 chars.indexOf(value.charAt(indexNext)) < 0) {
85 letsEat += value.charAt(indexNext);
86 eaten += value.charAt(indexNext);
87 indexNext++;
88 }
89
90 // Ugly but keep the main loop readable
91 // Set the label it should set
92 if (labelFirst) {
93 labelSecond = eaten;
94 } else {
95 labelFirst = eaten;
96 }
97
98 return shouldStop();
99 };
100
101 // In quote, every character is valid except the unescaped quotes and CR or LF
102 // Same function for single and double quote
103 const eatInQuote = quote => {
104 eaten = '';
105 // First check so value[indexNext-1] will always be valid
106 if (value[indexNext] === quote) {
107 return;
108 }
109
110 while (indexNext < value.length &&
111 !(quote === value[indexNext] && value[indexNext - 1] !== '\\') &&
112 value[indexNext] !== '\n' && value[indexNext] !== '\r') {
113 letsEat += value.charAt(indexNext);
114 eaten += value.charAt(indexNext);
115 indexNext++;
116 }
117
118 // If we encounter an EOL, there is an error
119 // We are waiting for a quote
120 if (value[indexNext] === '\n' || value[indexNext] === '\r' || indexNext >= value.length) {
121 errorDetected = true;
122 return true;
123 }
124
125 // Ugly but keep the main loop readable
126 if (labelFirst) {
127 labelSecond = eaten.replace(/\\"/g, '"');
128 } else {
129 labelFirst = eaten.replace(/\\"/g, '"');
130 }
131
132 return shouldStop();
133 };
134
135 // It's really common to eat only one character so let's make it a function
136 const eatOne = (c, skipStopCheck) => {
137 // Miam !
138 letsEat += c;
139 indexNext++;
140
141 return skipStopCheck ? false : shouldStop();
142 };
143
144 // Common parsing of quotes
145 const eatQuote = q => {
146 eatOne(q, true);
147 eatInQuote(q, true);
148
149 if (value.charAt(indexNext) !== q) {
150 return nothingHappend;
151 }
152
153 if (eatOne(q)) {
154 return -1;
155 }
156 };
157
158 let idSetByKey = false;
159 const addAttribute = () => {
160 switch (type) {
161 case 'id': // ID
162 if (idSetByKey) {
163 prop.id = labelFirst;
164 idSetByKey = false;
165 } else {
166 prop.id = prop.id || labelFirst;
167 }
168
169 break;
170 case 'class':
171 if (!prop.class) {
172 prop.class = [];
173 }
174
175 if (prop.class.indexOf(labelFirst) < 0) {
176 prop.class.push(labelFirst);
177 }
178
179 break;
180 case 'key':
181 if (!labelFirst) {
182 return nothingHappend;
183 }
184
185 if (!(labelFirst in prop)) {
186 if (labelSecond === undefined) { // Here, we have an attribute without value
187 // so it's user defined
188 prop[labelFirst] = config.defaultValue(labelFirst);
189 } else {
190 prop[labelFirst] = labelFirst === 'class' ? [labelSecond] : labelSecond;
191 }
192
193 if (labelFirst === 'id') {
194 idSetByKey = true;
195 }
196 } else if (labelFirst === 'class' && Boolean(labelSecond)) {
197 prop.class.push(labelSecond);
198 }
199
200 break;
201 default:
202 }
203
204 type = undefined;
205 labelFirst = '';
206 labelSecond = undefined;
207 };
208
209 /** *********************** Start parsing ************************ */
210
211 // Let's check for leading spaces first
212 eat(' \t\v');
213
214 if (value[indexNext] === '{') {
215 eatOne('{');
216 stopOnBrace = true;
217 }
218
219 while (!shouldStop()) { // Main loop which extract attributes
220 if (eat(' \t\v')) {
221 break;
222 }
223
224 if (value.charAt(indexNext) === '.') { // Classes
225 type = 'class';
226 if (eatOne('.')) {
227 errorDetected = true;
228 break;
229 }
230 } else if (value.charAt(indexNext) === '#') { // ID
231 type = 'id';
232 if (eatOne('#')) {
233 errorDetected = true;
234 break;
235 }
236 } else { // Key
237 type = 'key';
238 }
239
240 // Extract name
241 if (eatUntil('=\t\b\v  ') || !labelFirst) {
242 break;
243 }
244
245 if (value.charAt(indexNext) === '=' && type === 'key') { // Set labelSecond
246 if (eatOne('=')) {
247 break;
248 }
249
250 if (value.charAt(indexNext) === '"') {
251 const ret = eatQuote('"');
252 if (ret === -1) {
253 break;
254 } else if (ret === nothingHappend) {
255 return nothingHappend;
256 }
257 } else if (value.charAt(indexNext) === '\'') {
258 const ret = eatQuote('\'');
259 if (ret === -1) {
260 break;
261 } else if (ret === nothingHappend) {
262 return nothingHappend;
263 }
264 } else if (eatUntil(' \t\n\r\v=}')) {
265 break;
266 }
267 }
268
269 // Add the parsed attribute to the output prop with the ad hoc type
270 addAttribute();
271 }
272
273 addAttribute();
274 if (stopOnBrace) {
275 if (indexNext < value.length && value[indexNext] === '}') {
276 stopOnBrace = false;
277 eatOne('}');
278 } else {
279 return nothingHappend;
280 }
281 }
282
283 return errorDetected ? nothingHappend : {prop, eaten: letsEat};
284}
285
286module.exports = parse;