Skip to content

Commit 739e507

Browse files
e2tha-eBrian Muenzenmeyer
authored andcommitted
250 precursor (#293)
* more accurate paramToJson in parameter_hunter * updating unit tests for parameter_hunter paramToJson * minor edit * another param string parser * bugs fixed, unit tests working * variable name change * Added (failing for now) test to highlight the bug. * Added another test for another bug (parenthesis in value confuses patternhunter) * Added additional parameter hunter tests. * replacing JSON with JSON5 for better error messaging * working paramToJson * stricter unit tests * comment updates * simplfying paramToJson further * simplfying paramToJson further * comment update * tightened parameter key search * comment update * accommodating decimals and exponentials as valid numeric values * comment update * fixing array instead of number typo * more comments * spacing to pass eslint * removing unnecessary escape * fixing substring param typo * renaming json5 instances * comment update
1 parent b03b704 commit 739e507

File tree

8 files changed

+346
-93
lines changed

8 files changed

+346
-93
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
node_modules/
2+
npm-debug.log
23
.DS_Store
34
latest-change.txt
45
patternlab.json

core/lib/list_item_hunter.js

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
var list_item_hunter = function () {
1414

1515
var extend = require('util')._extend,
16+
JSON5 = require('json5'),
1617
pa = require('./pattern_assembler'),
1718
smh = require('./style_modifier_hunter'),
1819
pattern_assembler = new pa(),
@@ -44,7 +45,13 @@ var list_item_hunter = function () {
4445
}
4546

4647
//check for a local listitems.json file
47-
var listData = JSON.parse(JSON.stringify(patternlab.listitems));
48+
var listData;
49+
try {
50+
listData = JSON5.parse(JSON5.stringify(patternlab.listitems));
51+
} catch (err) {
52+
console.log('There was an error parsing JSON for ' + pattern.abspath);
53+
console.log(err);
54+
}
4855
listData = pattern_assembler.merge_data(listData, pattern.listitems);
4956

5057
//iterate over each copied block, rendering its contents along with pattenlab.listitems[i]
@@ -54,8 +61,15 @@ var list_item_hunter = function () {
5461

5562
//combine listItem data with pattern data with global data
5663
var itemData = listData['' + items.indexOf(loopNumberString)]; //this is a property like "2"
57-
var globalData = JSON.parse(JSON.stringify(patternlab.data));
58-
var localData = JSON.parse(JSON.stringify(pattern.jsonFileData));
64+
var globalData;
65+
var localData;
66+
try {
67+
globalData = JSON5.parse(JSON5.stringify(patternlab.data));
68+
localData = JSON5.parse(JSON5.stringify(pattern.jsonFileData));
69+
} catch (err) {
70+
console.log('There was an error parsing JSON for ' + pattern.abspath);
71+
console.log(err);
72+
}
5973

6074
var allData = pattern_assembler.merge_data(globalData, localData);
6175
allData = pattern_assembler.merge_data(allData, itemData !== undefined ? itemData[i] : {}); //itemData could be undefined if the listblock contains no partial, just markup
@@ -71,7 +85,13 @@ var list_item_hunter = function () {
7185
var partialPattern = pattern_assembler.get_pattern_by_key(partialName, patternlab);
7286

7387
//create a copy of the partial so as to not pollute it after the get_pattern_by_key call.
74-
var cleanPartialPattern = JSON.parse(JSON.stringify(partialPattern));
88+
var cleanPartialPattern;
89+
try {
90+
cleanPartialPattern = JSON5.parse(JSON5.stringify(partialPattern));
91+
} catch (err) {
92+
console.log('There was an error parsing JSON for ' + pattern.abspath);
93+
console.log(err);
94+
}
7595

7696
//if partial has style modifier data, replace the styleModifier value
7797
if (foundPartials[j].indexOf(':') > -1) {

core/lib/parameter_hunter.js

Lines changed: 203 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -13,110 +13,230 @@
1313
var parameter_hunter = function () {
1414

1515
var extend = require('util')._extend,
16+
JSON5 = require('json5'),
1617
pa = require('./pattern_assembler'),
1718
smh = require('./style_modifier_hunter'),
18-
style_modifier_hunter = new smh(),
19-
pattern_assembler = new pa();
20-
19+
pattern_assembler = new pa(),
20+
style_modifier_hunter = new smh();
21+
22+
/**
23+
* This function is really to accommodate the lax JSON-like syntax allowed by
24+
* Pattern Lab PHP for parameter submissions to partials. Unfortunately, no
25+
* easily searchable library was discovered for this. What we had to do was
26+
* write a custom script to crawl through the parameter string, and wrap the
27+
* keys and values in double-quotes as necessary.
28+
* The steps on a high-level are as follows:
29+
* * Further escape all escaped quotes and colons. Use the string
30+
* representation of their unicodes for this. This has the added bonus
31+
* of being interpreted correctly by JSON5.parse() without further
32+
* modification. This will be useful later in the function.
33+
* * Once escaped quotes are out of the way, we know the remaining quotes
34+
* are either key/value wrappers or wrapped within those wrappers. We know
35+
* that remaining commas and colons are either delimiters, or wrapped
36+
* within quotes to not be recognized as such.
37+
* * A do-while loop crawls paramString to write keys to a keys array and
38+
* values to a values array.
39+
* * Start by parsing the first key. Determine the type of wrapping quote,
40+
* if any.
41+
* * By knowing the open wrapper, we know that the next quote of that kind
42+
* (if the key is wrapped in quotes), HAS to be the close wrapper.
43+
* Similarly, if the key is unwrapped, we know the next colon HAS to be
44+
* the delimiter between key and value.
45+
* * Save the key to the keys array.
46+
* * Next, search for a value. It will either be the next block wrapped in
47+
* quotes, or a string of alphanumerics, decimal points, or minus signs.
48+
* * Save the value to the values array.
49+
* * The do-while loop truncates the paramString value while parsing. Its
50+
* condition for completion is when the paramString is whittled down to an
51+
* empty string.
52+
* * After the keys and values arrays are built, a for loop iterates through
53+
* them to build the final paramStringWellFormed string.
54+
* * No quote substitution had been done prior to this loop. In this loop,
55+
* all keys are ensured to be wrapped in double-quotes. String values are
56+
* also ensured to be wrapped in double-quotes.
57+
* * Unescape escaped unicodes except for double-quotes. Everything beside
58+
* double-quotes will be wrapped in double-quotes without need for escape.
59+
* * Return paramStringWellFormed.
60+
*
61+
* @param {string} pString
62+
* @returns {string} paramStringWellFormed
63+
*/
2164
function paramToJson(pString) {
22-
var paramStringWellFormed = '';
23-
var paramStringTmp;
24-
var colonPos;
25-
var delimitPos;
26-
var quotePos;
27-
var paramString = pString;
28-
65+
var colonPos = -1;
66+
var keys = [];
67+
var paramString = pString; // to not reassign param
68+
var paramStringWellFormed;
69+
var quotePos = -1;
70+
var regex;
71+
var values = [];
72+
var wrapper;
73+
74+
//replace all escaped double-quotes with escaped unicode
75+
paramString = paramString.replace(/\\"/g, '\\u0022');
76+
77+
//replace all escaped single-quotes with escaped unicode
78+
paramString = paramString.replace(/\\'/g, '\\u0027');
79+
80+
//replace all escaped colons with escaped unicode
81+
paramString = paramString.replace(/\\:/g, '\\u0058');
82+
83+
//with escaped chars out of the way, crawl through paramString looking for
84+
//keys and values
2985
do {
3086

31-
//if param key is wrapped in single quotes, replace with double quotes.
32-
paramString = paramString.replace(/(^\s*[\{|\,]\s*)'([^']+)'(\s*\:)/, '$1"$2"$3');
87+
//check if searching for a key
88+
if (paramString[0] === '{' || paramString[0] === ',') {
89+
paramString = paramString.substring(1, paramString.length).trim();
3390

34-
//if params key is not wrapped in any quotes, wrap in double quotes.
35-
paramString = paramString.replace(/(^\s*[\{|\,]\s*)([^\s"'\:]+)(\s*\:)/, '$1"$2"$3');
91+
//search for end quote if wrapped in quotes. else search for colon.
92+
//everything up to that position will be saved in the keys array.
93+
switch (paramString[0]) {
3694

37-
//move param key to paramStringWellFormed var.
38-
colonPos = paramString.indexOf(':');
95+
//need to search for end quote pos in case the quotes wrap a colon
96+
case '"':
97+
case '\'':
98+
wrapper = paramString[0];
99+
quotePos = paramString.indexOf(wrapper, 1);
100+
break;
39101

40-
//except to prevent infinite loops.
41-
if (colonPos === -1) {
42-
colonPos = paramString.length - 1;
43-
}
44-
else {
45-
colonPos += 1;
46-
}
47-
paramStringWellFormed += paramString.substring(0, colonPos);
48-
paramString = paramString.substring(colonPos, paramString.length).trim();
102+
default:
103+
colonPos = paramString.indexOf(':');
104+
}
49105

50-
//if param value is wrapped in single quotes, replace with double quotes.
51-
if (paramString[0] === '\'') {
52-
quotePos = paramString.search(/[^\\]'/);
106+
if (quotePos > -1) {
107+
keys.push(paramString.substring(0, quotePos + 1).trim());
53108

54-
//except for unclosed quotes to prevent infinite loops.
55-
if (quotePos === -1) {
56-
quotePos = paramString.length - 1;
57-
}
58-
else {
59-
quotePos += 2;
60-
}
109+
//truncate the beginning from paramString and look for a value
110+
paramString = paramString.substring(quotePos + 1, paramString.length).trim();
61111

62-
//prepare param value for move to paramStringWellFormed var.
63-
paramStringTmp = paramString.substring(0, quotePos);
112+
//unset quotePos
113+
quotePos = -1;
64114

65-
//unescape any escaped single quotes.
66-
paramStringTmp = paramStringTmp.replace(/\\'/g, '\'');
115+
} else if (colonPos > -1) {
116+
keys.push(paramString.substring(0, colonPos).trim());
67117

68-
//escape any double quotes.
69-
paramStringTmp = paramStringTmp.replace(/"/g, '\\"');
118+
//truncate the beginning from paramString and look for a value
119+
paramString = paramString.substring(colonPos, paramString.length);
70120

71-
//replace the delimiting single quotes with double quotes.
72-
paramStringTmp = paramStringTmp.replace(/^'/, '"');
73-
paramStringTmp = paramStringTmp.replace(/'$/, '"');
121+
//unset colonPos
122+
colonPos = -1;
74123

75-
//move param key to paramStringWellFormed var.
76-
paramStringWellFormed += paramStringTmp;
77-
paramString = paramString.substring(quotePos, paramString.length).trim();
124+
//if there are no more colons, and we're looking for a key, there is
125+
//probably a problem. stop any further processing.
126+
} else {
127+
paramString = '';
128+
break;
129+
}
78130
}
79131

80-
//if param value is wrapped in double quotes, just move to paramStringWellFormed var.
81-
else if (paramString[0] === '"') {
82-
quotePos = paramString.search(/[^\\]"/);
83-
84-
//except for unclosed quotes to prevent infinite loops.
85-
if (quotePos === -1) {
86-
quotePos = paramString.length - 1;
132+
//now, search for a value
133+
if (paramString[0] === ':') {
134+
paramString = paramString.substring(1, paramString.length).trim();
135+
136+
//the only reason we're using regexes here, instead of indexOf(), is
137+
//because we don't know if the next delimiter is going to be a comma or
138+
//a closing curly brace. since it's not much of a performance hit to
139+
//use regexes as sparingly as here, and it's much more concise and
140+
//readable, we'll use a regex for match() and replace() instead of
141+
//performing conditional logic with indexOf().
142+
switch (paramString[0]) {
143+
144+
//since a quote of same type as its wrappers would be escaped, and we
145+
//escaped those even further with their unicodes, it is safe to look
146+
//for wrapper pairs and conclude that their contents are values
147+
case '"':
148+
regex = /^"(.|\s)*?"/;
149+
break;
150+
case '\'':
151+
regex = /^'(.|\s)*?'/;
152+
break;
153+
154+
//if there is no value wrapper, regex for alphanumerics, decimal
155+
//points, and minus signs for exponential notation.
156+
default:
157+
regex = /^[\w\-\.]*/;
87158
}
88-
else {
89-
quotePos += 2;
159+
values.push(paramString.match(regex)[0].trim());
160+
161+
//truncate the beginning from paramString and continue either
162+
//looking for a key, or returning
163+
paramString = paramString.replace(regex, '').trim();
164+
165+
//exit do while if the final char is '}'
166+
if (paramString === '}') {
167+
paramString = '';
168+
break;
90169
}
91170

92-
//move param key to paramStringWellFormed var.
93-
paramStringWellFormed += paramString.substring(0, quotePos);
94-
paramString = paramString.substring(quotePos, paramString.length).trim();
171+
//if there are no more colons, and we're looking for a value, there is
172+
//probably a problem. stop any further processing.
173+
} else {
174+
paramString = '';
175+
break;
95176
}
177+
} while (paramString);
96178

97-
//if param value is not wrapped in quotes, move everthing up to the delimiting comma to paramStringWellFormed var.
98-
else {
99-
delimitPos = paramString.indexOf(',');
100-
101-
//except to prevent infinite loops.
102-
if (delimitPos === -1) {
103-
delimitPos = paramString.length - 1;
179+
//build paramStringWellFormed string for JSON parsing
180+
paramStringWellFormed = '{';
181+
for (var i = 0; i < keys.length; i++) {
182+
183+
//keys
184+
//replace single-quote wrappers with double-quotes
185+
if (keys[i][0] === '\'' && keys[i][keys[i].length - 1] === '\'') {
186+
paramStringWellFormed += '"';
187+
188+
//any enclosed double-quotes must be escaped
189+
paramStringWellFormed += keys[i].substring(1, keys[i].length - 1).replace(/"/g, '\\"');
190+
paramStringWellFormed += '"';
191+
} else {
192+
193+
//open wrap with double-quotes if no wrapper
194+
if (keys[i][0] !== '"' && keys[i][0] !== '\'') {
195+
paramStringWellFormed += '"';
196+
197+
//this is to clean up vestiges from Pattern Lab PHP's escaping scheme.
198+
//F.Y.I. Pattern Lab PHP would allow special characters like question
199+
//marks in parameter keys so long as the key was unwrapped and the
200+
//special character escaped with a backslash. In Node, we need to wrap
201+
//those keys and unescape those characters.
202+
keys[i] = keys[i].replace(/\\/g, '');
104203
}
105-
else {
106-
delimitPos += 1;
204+
205+
paramStringWellFormed += keys[i];
206+
207+
//close wrap with double-quotes if no wrapper
208+
if (keys[i][keys[i].length - 1] !== '"' && keys[i][keys[i].length - 1] !== '\'') {
209+
paramStringWellFormed += '"';
107210
}
108-
paramStringWellFormed += paramString.substring(0, delimitPos);
109-
paramString = paramString.substring(delimitPos, paramString.length).trim();
110211
}
111212

112-
//break at the end.
113-
if (paramString.length === 1) {
114-
paramStringWellFormed += paramString.trim();
115-
paramString = '';
116-
break;
213+
//colon delimiter.
214+
paramStringWellFormed += ':'; + values[i];
215+
216+
//values
217+
//replace single-quote wrappers with double-quotes
218+
if (values[i][0] === '\'' && values[i][values[i].length - 1] === '\'') {
219+
paramStringWellFormed += '"';
220+
221+
//any enclosed double-quotes must be escaped
222+
paramStringWellFormed += values[i].substring(1, values[i].length - 1).replace(/"/g, '\\"');
223+
paramStringWellFormed += '"';
224+
225+
//for everything else, just add the value however it's wrapped
226+
} else {
227+
paramStringWellFormed += values[i];
117228
}
118229

119-
} while (paramString);
230+
//comma delimiter
231+
if (i < keys.length - 1) {
232+
paramStringWellFormed += ',';
233+
}
234+
}
235+
paramStringWellFormed += '}';
236+
237+
//unescape escaped unicode except for double-quotes
238+
paramStringWellFormed = paramStringWellFormed.replace(/\\u0027/g, '\'');
239+
paramStringWellFormed = paramStringWellFormed.replace(/\\u0058/g, ':');
120240

121241
return paramStringWellFormed;
122242
}
@@ -140,7 +260,7 @@ var parameter_hunter = function () {
140260

141261
//strip out the additional data, convert string to JSON.
142262
var leftParen = pMatch.indexOf('(');
143-
var rightParen = pMatch.indexOf(')');
263+
var rightParen = pMatch.lastIndexOf(')');
144264
var paramString = '{' + pMatch.substring(leftParen + 1, rightParen) + '}';
145265
var paramStringWellFormed = paramToJson(paramString);
146266

@@ -149,11 +269,12 @@ var parameter_hunter = function () {
149269
var localData = {};
150270

151271
try {
152-
paramData = JSON.parse(paramStringWellFormed);
153-
globalData = JSON.parse(JSON.stringify(patternlab.data));
154-
localData = JSON.parse(JSON.stringify(pattern.jsonFileData || {}));
155-
} catch (e) {
156-
console.log(e);
272+
paramData = JSON5.parse(paramStringWellFormed);
273+
globalData = JSON5.parse(JSON5.stringify(patternlab.data));
274+
localData = JSON5.parse(JSON5.stringify(pattern.jsonFileData || {}));
275+
} catch (err) {
276+
console.log('There was an error parsing JSON for ' + pattern.abspath);
277+
console.log(err);
157278
}
158279

159280
var allData = pattern_assembler.merge_data(globalData, localData);

0 commit comments

Comments
 (0)