@@ -90,6 +90,11 @@ export interface StaxXmlWriterOptions {
9090 * This writer provides efficient streaming XML generation using WritableStream for handling
9191 * large XML documents with automatic buffering, backpressure management, and namespace support.
9292 *
93+ * This is an optimized implementation with:
94+ * - Optimization 1: Regex caching for entity escaping
95+ * - Optimization 2: Attribute string batching
96+ * - Optimization 3: Early entity check before regex execution
97+ *
9398 * @remarks
9499 * The writer supports streaming output with configurable buffering, automatic entity encoding,
95100 * pretty printing with customizable indentation, and comprehensive namespace handling.
@@ -124,6 +129,16 @@ export interface StaxXmlWriterOptions {
124129 * @public
125130 */
126131export class StaxXmlWriter {
132+ // OPTIMIZATION 1: Static cached regex and entity map for basic entities
133+ private static readonly BASIC_ENTITY_MAP : Record < string , string > = {
134+ '&' : '&' ,
135+ '<' : '<' ,
136+ '>' : '>' ,
137+ '"' : '"' ,
138+ '\'' : '''
139+ } ;
140+ private static readonly BASIC_ENTITY_REGEX = / [ & < > " ' ] / g;
141+
127142 private writer : WritableStreamDefaultWriter < Uint8Array > ;
128143 private encoder : TextEncoder ;
129144 private buffer : Uint8Array ;
@@ -137,7 +152,11 @@ export class StaxXmlWriter {
137152 private readonly options : Required < StaxXmlWriterOptions > ;
138153 private currentIndentLevel : number = 0 ;
139154 private needsIndent : boolean = false ;
140- private entityMap : Record < string , string > = { } ;
155+
156+ // OPTIMIZATION 1: Instance fields for custom entity handling (if any)
157+ private customEntityRegex ?: RegExp ;
158+ private fullEntityMap ?: Record < string , string > ;
159+ private customEntityKeys ?: string [ ] ; // For fast early checking
141160
142161 // Performance metrics
143162 private metrics = {
@@ -177,17 +196,28 @@ export class StaxXmlWriter {
177196 // Initialize namespace stack
178197 this . namespaceStack = [ new Map < string , string > ( ) ] ;
179198
180- // Initialize entity map
181- this . _initializeEntityMap ( ) ;
182- }
199+ // OPTIMIZATION 1: Build custom entity map and regex at construction time
200+ if ( this . options . addEntities && this . options . addEntities . length > 0 ) {
201+ this . fullEntityMap = {
202+ ...StaxXmlWriter . BASIC_ENTITY_MAP ,
203+ ...this . options . addEntities . reduce ( ( map , entity ) => {
204+ if ( entity . entity && entity . value ) {
205+ map [ entity . entity ] = entity . value ;
206+ }
207+ return map ;
208+ } , { } as Record < string , string > )
209+ } ;
183210
184- private _initializeEntityMap ( ) : void {
185- if ( this . options . addEntities ) {
186- for ( const entity of this . options . addEntities ) {
187- if ( entity . entity && entity . value ) {
188- this . entityMap [ entity . entity ] = entity . value ;
189- }
190- }
211+ // Build regex with proper escaping
212+ const escapedKeys = Object . keys ( this . fullEntityMap ) . map ( k =>
213+ k . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' )
214+ ) ;
215+ this . customEntityRegex = new RegExp ( escapedKeys . join ( '|' ) , 'g' ) ;
216+
217+ // Store custom entity keys (excluding basic ones) for early check
218+ this . customEntityKeys = Object . keys ( this . fullEntityMap ) . filter (
219+ k => ! ( k in StaxXmlWriter . BASIC_ENTITY_MAP )
220+ ) ;
191221 }
192222 }
193223
@@ -319,27 +349,33 @@ export class StaxXmlWriter {
319349 currentNamespaces . set ( prefix , uri ) ;
320350 }
321351
322- // Attribute processing
352+ // OPTIMIZATION 2: Attribute string batching
353+ // Build entire attribute string first, then single _writeToBuffer call
323354 if ( attributes ) {
355+ let attrString = '' ;
324356 for ( const [ key , value ] of Object . entries ( attributes ) ) {
325357 if ( typeof value === 'string' ) {
326- await this . _writeToBuffer ( ` ${ key } ="${ this . _escapeXml ( value ) } "` ) ;
358+ // Simple string attribute
359+ attrString += ` ${ key } ="${ this . _escapeXml ( value ) } "` ;
327360 } else {
361+ // AttributeInfo object - attribute with prefix
328362 const attrPrefix = value . prefix ;
329363 const attrValue = value . value ;
330364
331365 if ( attrPrefix ) {
366+ // Check if prefix is defined in namespace
332367 if ( ! currentNamespaces . has ( attrPrefix ) ) {
333- throw new Error ( `Namespace prefix '${ attrPrefix } ' is not defined` ) ;
368+ throw new Error ( `Namespace prefix '${ attrPrefix } ' is not defined for attribute ' ${ key } ' ` ) ;
334369 }
335- await this . _writeToBuffer (
336- ` ${ attrPrefix } :${ key } ="${ this . _escapeXml ( attrValue ) } "`
337- ) ;
370+ attrString += ` ${ attrPrefix } :${ key } ="${ this . _escapeXml ( attrValue ) } "` ;
338371 } else {
339- await this . _writeToBuffer ( ` ${ key } ="${ this . _escapeXml ( attrValue ) } "` ) ;
372+ attrString += ` ${ key } ="${ this . _escapeXml ( attrValue ) } "` ;
340373 }
341374 }
342375 }
376+ if ( attrString ) {
377+ await this . _writeToBuffer ( attrString ) ;
378+ }
343379 }
344380
345381 if ( selfClosing ) {
@@ -517,6 +553,16 @@ export class StaxXmlWriter {
517553 }
518554 }
519555
556+ /**
557+ * Escapes XML text.
558+ * OPTIMIZED with:
559+ * - Cached regex patterns (Optimization 1)
560+ * - Early entity check to skip regex when not needed (Optimization 3)
561+ * - Fast path for no custom entities case (most common)
562+ * @param text Text to escape
563+ * @returns Escaped text
564+ * @private
565+ */
520566 private _escapeXml ( text : string ) : string {
521567 if ( ! text ) {
522568 return '' ; // Return empty string as-is
@@ -525,32 +571,35 @@ export class StaxXmlWriter {
525571 return text ; // Return original text if automatic entity encoding is disabled
526572 }
527573
528- let entityMap : Record < string , string > = {
529- '&' : '&' , // During write process, & does not conflict with other entities
530- '<' : '<' ,
531- '>' : '>' ,
532- '"' : '"' ,
533- '\'' : ''' ,
534- ...this . options . addEntities ?. reduce ( ( map , entity ) => {
535- if ( entity . entity && entity . value ) {
536- map [ entity . entity ] = entity . value ;
537- }
538- return map ;
539- } , { } as Record < string , string > )
540- } ;
541-
542- // Convert entityMap keys to regex for escaping
543- const regex = new RegExp ( Object . keys ( entityMap ) . join ( '|' ) , 'g' ) ;
544- // Escape processing
545- return text . replace ( regex , ( match ) => {
546- // If character is defined in entityMap, return mapped value
547- if ( entityMap [ match ] ) {
548- return entityMap [ match ] ;
574+ // Fast path: No custom entities case (most common)
575+ if ( ! this . customEntityRegex ) {
576+ // Early exit: Check if text contains basic entities
577+ if ( ! text . includes ( '&' ) && ! text . includes ( '<' ) && ! text . includes ( '>' ) &&
578+ ! text . includes ( '"' ) && ! text . includes ( "'" ) ) {
579+ return text ; // No escaping needed
549580 }
550- else {
551- // Return undefined characters as-is
552- return match ;
553- }
554- } ) ;
581+
582+ // Use cached basic entity regex
583+ return text . replace ( StaxXmlWriter . BASIC_ENTITY_REGEX ,
584+ ( match ) => StaxXmlWriter . BASIC_ENTITY_MAP [ match ] || match ) ;
585+ }
586+
587+ // Slow path: Custom entities exist
588+ // OPTIMIZATION 3: Early exit check (including custom entities)
589+ const hasBasicEntities = text . includes ( '&' ) || text . includes ( '<' ) || text . includes ( '>' ) ||
590+ text . includes ( '"' ) || text . includes ( "'" ) ;
591+
592+ let hasCustomEntities = false ;
593+ if ( this . customEntityKeys && this . customEntityKeys . length > 0 ) {
594+ hasCustomEntities = this . customEntityKeys . some ( entity => text . includes ( entity ) ) ;
595+ }
596+
597+ // If no entities present, return original text
598+ if ( ! hasBasicEntities && ! hasCustomEntities ) {
599+ return text ;
600+ }
601+
602+ // OPTIMIZATION 1: Use cached custom entity regex
603+ return text . replace ( this . customEntityRegex , ( match ) => this . fullEntityMap ! [ match ] || match ) ;
555604 }
556605}
0 commit comments