@@ -33,15 +33,131 @@ export function optimise(
3333 output . push ( "" ) ;
3434 }
3535
36- // 0: Early return for single string
36+ // Early return for single string
3737 if (
3838 stringyTextElements . length === 1 &&
3939 typeof stringyTextElements [ 0 ] === "string"
4040 ) {
4141 return stringyTextElements ;
4242 }
4343
44- // 1: Remove undefineds, flatten MinecraftText with only text
44+ output . push ( ...flattenMCText ( stringyTextElements ) ) ;
45+ output = mergeTextComponents ( output ) ;
46+
47+ // remove leading empty string if followed by a string
48+ if ( output . length >= 2 && output [ 0 ] === "" && typeof output [ 1 ] === "string" )
49+ output . shift ( ) ;
50+
51+ if ( shouldHaveLeadingEmptyString ( output ) ) {
52+ output . shift ( ) ;
53+ }
54+
55+ // if it is item lore then override
56+ if ( lore ) {
57+ output . unshift ( { italic : false , color : "white" , text : "" } ) ;
58+ }
59+ return output ;
60+ }
61+
62+ /**
63+ * Gets all shared style properties between two MinecraftText components
64+ *
65+ * @param a text component a to check
66+ * @param b text component b to check
67+ * @returns a record of shared style properties
68+ */
69+ function getSharedStyleProps (
70+ a : MinecraftText ,
71+ b : MinecraftText ,
72+ ) : Record < keyof MinecraftText , any > {
73+ const allSharedProps : Record < keyof MinecraftText , any > = { } as Record <
74+ keyof MinecraftText ,
75+ any
76+ > ;
77+ for ( const prop of styleProps ) {
78+ const p = prop as MCTextKey ;
79+ if (
80+ a [ p ] !== undefined &&
81+ b [ p ] !== undefined &&
82+ propsMatch ( a [ p ] , b [ p ] , prop )
83+ ) {
84+ allSharedProps [ p ] = a [ p ] ;
85+ }
86+ }
87+ return allSharedProps ;
88+ }
89+
90+ /**
91+ * Collects all MinecraftText components from the current index that share the same style properties
92+ *
93+ * @param i the current index into the array
94+ * @param group the current group of MinecraftText components being collected
95+ * @param output the current output array
96+ * @param allSharedProps a record of all shared properties between the group
97+ */
98+ function collectAllFromIndex (
99+ i : number ,
100+ group : MinecraftText [ ] ,
101+ output : StringyMCText [ ] ,
102+ allSharedProps : Record < keyof MinecraftText , any > ,
103+ ) {
104+ let j = i ;
105+ let sharedKeys = Object . keys ( allSharedProps ) as ( keyof MinecraftText ) [ ] ;
106+ while ( output [ j + 1 ] && typeof output [ j + 1 ] === "object" ) {
107+ const next = output [ j + 1 ] as MinecraftText ;
108+ let allPropertiesMatch = sharedKeys . every (
109+ ( prop ) =>
110+ next [ prop ] !== undefined &&
111+ propsMatch ( next [ prop ] , allSharedProps [ prop ] , prop ) ,
112+ ) ;
113+
114+ if ( ! allPropertiesMatch ) break ;
115+
116+ group . push ( next ) ;
117+ j ++ ;
118+ }
119+ }
120+
121+ /**
122+ * Checks if two properties match, considering interactive properties
123+ *
124+ * @param a property A
125+ * @param b property B
126+ * @param property the property to check for
127+ * @returns true if the properties match, false otherwise
128+ */
129+ function propsMatch ( a : any , b : any , property : string ) {
130+ return isAnInteractiveProp ( property )
131+ ? JSON . stringify ( a ) === JSON . stringify ( b )
132+ : a === b ;
133+ }
134+
135+ /**
136+ * Determines if the final output should have the leading empty string removed
137+ *
138+ * @param output the optimized output array
139+ * @returns true if the final output should have the leading empty string removed
140+ */
141+ function shouldHaveLeadingEmptyString ( output : StringyMCText [ ] ) : boolean {
142+ return (
143+ output . length >= 2 &&
144+ output [ 0 ] == "" &&
145+ ( typeof output [ 1 ] === "string" ||
146+ ( typeof output [ 1 ] === "object" &&
147+ ! styleProps . some (
148+ ( prop ) => output [ 1 ] [ prop as keyof StringyMCText ] !== undefined ,
149+ ) ) )
150+ ) ;
151+ }
152+
153+ /**
154+ * Flattens the text elements by converting objects with only text property to strings and removing undefined properties
155+ *
156+ * @param stringyTextElements the text elements
157+ * @returns the elements with strings becoming string literals and cleaned object properties
158+ */
159+ function flattenMCText ( stringyTextElements : StringyMCText [ ] ) : StringyMCText [ ] {
160+ const output : StringyMCText [ ] = [ ] ;
45161 for ( const component of stringyTextElements ) {
46162 if ( typeof component === "string" ) {
47163 // string, just add
@@ -63,8 +179,16 @@ export function optimise(
63179 Object . keys ( component ) . length === 1 ? component . text ! : component ,
64180 ) ;
65181 }
182+ return output ;
183+ }
66184
67- // 2: Merge adjacent strings and whitespace, group objects with shared style
185+ /**
186+ * Merges adjacent strings and whitespace, groups objects with shared style/interactivity properties
187+ *
188+ * @param output the optimized output step
189+ * @returns the optimized output step with merged strings, whitespace and group properties with shared styling
190+ */
191+ function mergeTextComponents ( output : StringyMCText [ ] ) {
68192 for ( let i = 0 ; i < output . length - 1 ; i ++ ) {
69193 const current = output [ i ] ,
70194 next = output [ i + 1 ] ;
@@ -108,21 +232,18 @@ export function optimise(
108232 // Find shared style/interactivity properties between consecutive objects
109233 if ( typeof current === "object" && typeof next === "object" ) {
110234 // Merge all properties in styleProps that are identical across the group
111- const allShared : Record < keyof MinecraftText , any > = getSharedStyleProps (
112- current ,
113- next ,
114- ) ;
235+ const sharedProperties = getSharedStyleProps ( current , next ) ;
115236
116- if ( Object . keys ( allShared ) . length > 0 ) {
237+ if ( Object . keys ( sharedProperties ) . length > 0 ) {
117238 // Find how many consecutive objects share these properties
118239 let group = [ current ] ;
119240
120- collectAllFromIndex ( i , group , output , allShared ) ;
241+ collectAllFromIndex ( i , group , output , sharedProperties ) ;
121242 if ( group . length > 1 ) {
122243 // Remove shared properties from each group member for "extra"
123244 let extras : StringyMCText [ ] = group . map ( ( comp ) => {
124245 const copy = { ...comp } ;
125- for ( const prop of Object . keys ( allShared ) ) {
246+ for ( const prop of Object . keys ( sharedProperties ) ) {
126247 delete copy [ prop as MCTextKey ] ;
127248 }
128249 return copy ;
@@ -131,13 +252,18 @@ export function optimise(
131252 // Optimise extra
132253 extras = optimise ( extras ) ;
133254 const first = extras . shift ( ) ;
134- let merged = { ...allShared } ;
255+ let merged = { ...sharedProperties } ;
256+
257+ // Rebuild merged component
135258 if ( typeof first == "string" ) {
136259 merged . text = first ;
137260 if ( extras . length > 0 ) merged . extra = extras ;
138261 } else {
139- merged = { ...allShared , ...first } ;
140- if ( extras . length > 0 ) merged . extra = extras ;
262+ Object . assign ( merged , { ...first } ) ;
263+ if ( extras . length > 0 ) {
264+ if ( ! merged . extra ) merged . extra = extras ;
265+ else merged . extra = merged . extra . concat ( extras ) ; // this single line cost me 20 minutes
266+ }
141267 }
142268 output . splice ( i , group . length , merged ) ;
143269 i -- ; // recheck at this position
@@ -146,74 +272,5 @@ export function optimise(
146272 }
147273 }
148274 }
149-
150- // 3: Remove leading empty string if followed by a string
151- if ( output . length >= 2 && output [ 0 ] === "" && typeof output [ 1 ] === "string" )
152- output . shift ( ) ;
153-
154- // 4: If out[1] is a string, or an object without any style properties, then remove out[0]
155- if (
156- output . length >= 2 &&
157- output [ 0 ] == "" &&
158- ( typeof output [ 1 ] === "string" ||
159- ( typeof output [ 1 ] === "object" &&
160- ! styleProps . some (
161- ( prop ) => output [ 1 ] [ prop as keyof StringyMCText ] !== undefined ,
162- ) ) )
163- ) {
164- output . shift ( ) ;
165- }
166-
167- // 5: If it is item lore then override
168- if ( lore ) {
169- output . unshift ( { italic : false , color : "white" , text : "" } ) ;
170- }
171275 return output ;
172276}
173-
174- function getSharedStyleProps ( a : MinecraftText , b : MinecraftText ) {
175- const allSharedProps : Record < keyof MinecraftText , any > = { } as Record <
176- keyof MinecraftText ,
177- any
178- > ;
179- for ( const prop of styleProps ) {
180- const p = prop as MCTextKey ;
181- if (
182- a [ p ] !== undefined &&
183- b [ p ] !== undefined &&
184- propsMatch ( a [ p ] , b [ p ] , prop )
185- ) {
186- allSharedProps [ p ] = a [ p ] ;
187- }
188- }
189- return allSharedProps ;
190- }
191-
192- function collectAllFromIndex (
193- i : number ,
194- group : MinecraftText [ ] ,
195- output : StringyMCText [ ] ,
196- allSharedProps : Record < keyof MinecraftText , any > ,
197- ) {
198- let j = i ;
199- let sharedKeys = Object . keys ( allSharedProps ) as ( keyof MinecraftText ) [ ] ;
200- while ( output [ j + 1 ] && typeof output [ j + 1 ] === "object" ) {
201- const next = output [ j + 1 ] as MinecraftText ;
202- let allPropertiesMatch = sharedKeys . every (
203- ( prop ) =>
204- next [ prop ] !== undefined &&
205- propsMatch ( next [ prop ] , allSharedProps [ prop ] , prop ) ,
206- ) ;
207-
208- if ( ! allPropertiesMatch ) break ;
209-
210- group . push ( next ) ;
211- j ++ ;
212- }
213- }
214-
215- function propsMatch ( a : any , b : any , property : string ) {
216- return isAnInteractiveProp ( property )
217- ? JSON . stringify ( a ) === JSON . stringify ( b )
218- : a === b ;
219- }
0 commit comments