1313var 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 ( / \\ u 0 0 2 7 / g, '\'' ) ;
239+ paramStringWellFormed = paramStringWellFormed . replace ( / \\ u 0 0 5 8 / 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