@@ -217,92 +217,80 @@ function buildAttributesMap(attrStr, jPath, tagName) {
217217 const len = matches . length ; //don't make it inline
218218 const attrs = { } ;
219219
220- if ( this . options . jPath === false ) {
221- // First pass: parse all attributes and update matcher with raw values
222- // This ensures the matcher has all attribute values when processors run
223- const rawAttrsForMatcher = { } ;
224- for ( let i = 0 ; i < len ; i ++ ) {
225- const attrName = this . resolveNameSpace ( matches [ i ] [ 1 ] ) ;
226- const oldVal = matches [ i ] [ 4 ] ;
227-
228- if ( attrName . length && oldVal !== undefined ) {
229- let parsedVal = oldVal ;
230- if ( this . options . trimValues ) {
231- parsedVal = parsedVal . trim ( ) ;
232- }
233- parsedVal = this . replaceEntitiesValue ( parsedVal , tagName , this . readonlyMatcher ) ;
234- rawAttrsForMatcher [ attrName ] = parsedVal ;
235- }
236- }
220+ // Pre-process values once: trim + entity replacement
221+ // Reused in both matcher update and second pass
222+ const processedVals = new Array ( len ) ;
223+ let hasRawAttrs = false ;
224+ const rawAttrsForMatcher = { } ;
225+
226+ for ( let i = 0 ; i < len ; i ++ ) {
227+ const attrName = this . resolveNameSpace ( matches [ i ] [ 1 ] ) ;
228+ const oldVal = matches [ i ] [ 4 ] ;
229+
230+ if ( attrName . length && oldVal !== undefined ) {
231+ let val = oldVal ;
232+ if ( this . options . trimValues ) val = val . trim ( ) ;
233+ val = this . replaceEntitiesValue ( val , tagName , this . readonlyMatcher ) ;
234+ processedVals [ i ] = val ;
237235
238- // Update matcher with raw attribute values BEFORE running processors
239- if ( Object . keys ( rawAttrsForMatcher ) . length > 0 && typeof jPath === 'object' && jPath . updateCurrent ) {
240- jPath . updateCurrent ( rawAttrsForMatcher ) ;
236+ rawAttrsForMatcher [ attrName ] = val ;
237+ hasRawAttrs = true ;
241238 }
242239 }
243240
244- // Second pass: now process attributes with matcher having full attribute context
241+ // Update matcher ONCE before second pass, if applicable
242+ if ( hasRawAttrs && typeof jPath === 'object' && jPath . updateCurrent ) {
243+ jPath . updateCurrent ( rawAttrsForMatcher ) ;
244+ }
245+
246+ // Hoist toString() once — path doesn't change during attribute processing
245247 const jPathStr = this . options . jPath ? jPath . toString ( ) : this . readonlyMatcher ;
246248
249+ // Second pass: apply processors, build final attrs
250+ let hasAttrs = false ;
247251 for ( let i = 0 ; i < len ; i ++ ) {
248252 const attrName = this . resolveNameSpace ( matches [ i ] [ 1 ] ) ;
249253
250- // Convert jPath to string if needed for ignoreAttributesFn
251- if ( this . ignoreAttributesFn ( attrName , jPathStr ) ) {
252- continue
253- }
254+ if ( this . ignoreAttributesFn ( attrName , jPathStr ) ) continue ;
254255
255- let oldVal = matches [ i ] [ 4 ] ;
256256 let aName = this . options . attributeNamePrefix + attrName ;
257257
258258 if ( attrName . length ) {
259259 if ( this . options . transformAttributeName ) {
260260 aName = this . options . transformAttributeName ( aName ) ;
261261 }
262- //if (aName === "__proto__") aName = "#__proto__";
263262 aName = sanitizeName ( aName , this . options ) ;
264263
265- if ( oldVal !== undefined ) {
266- if ( this . options . trimValues ) {
267- oldVal = oldVal . trim ( ) ;
268- }
269- oldVal = this . replaceEntitiesValue ( oldVal , tagName , this . readonlyMatcher ) ;
264+ if ( matches [ i ] [ 4 ] !== undefined ) {
265+ // Reuse already-processed value — no double entity replacement
266+ const oldVal = processedVals [ i ] ;
270267
271- // Pass jPath string or readonlyMatcher based on options.jPath setting
272- const jPathOrMatcher = this . options . jPath ? jPath . toString ( ) : this . readonlyMatcher ;
273- const newVal = this . options . attributeValueProcessor ( attrName , oldVal , jPathOrMatcher ) ;
268+ const newVal = this . options . attributeValueProcessor ( attrName , oldVal , jPathStr ) ;
274269 if ( newVal === null || newVal === undefined ) {
275- //don't parse
276270 attrs [ aName ] = oldVal ;
277271 } else if ( typeof newVal !== typeof oldVal || newVal !== oldVal ) {
278- //overwrite
279272 attrs [ aName ] = newVal ;
280273 } else {
281- //parse
282- attrs [ aName ] = parseValue (
283- oldVal ,
284- this . options . parseAttributeValue ,
285- this . options . numberParseOptions
286- ) ;
274+ attrs [ aName ] = parseValue ( oldVal , this . options . parseAttributeValue , this . options . numberParseOptions ) ;
287275 }
276+ hasAttrs = true ;
288277 } else if ( this . options . allowBooleanAttributes ) {
289278 attrs [ aName ] = true ;
279+ hasAttrs = true ;
290280 }
291281 }
292282 }
293283
294- if ( ! Object . keys ( attrs ) . length ) {
295- return ;
296- }
284+ if ( ! hasAttrs ) return ;
285+
297286 if ( this . options . attributesGroupName ) {
298287 const attrCollection = { } ;
299288 attrCollection [ this . options . attributesGroupName ] = attrs ;
300289 return attrCollection ;
301290 }
302- return attrs
291+ return attrs ;
303292 }
304293}
305-
306294const parseXml = function ( xmlData ) {
307295 xmlData = xmlData . replace ( / \r \n ? / g, "\n" ) ; //TODO: remove this line
308296 const xmlObj = new xmlNode ( '!xml' ) ;
0 commit comments