@@ -265,25 +265,126 @@ function setWindowProgressBar(value) {
265265 } catch { }
266266}
267267
268+ function looksLikeHtml ( input ) {
269+ if ( ! input ) return false ;
270+ const s = String ( input ) ;
271+ if ( ! s . includes ( "<" ) || ! s . includes ( ">" ) ) return false ;
272+ // Be conservative: only treat the note as HTML if it contains common tags we expect from GitHub-rendered bodies.
273+ return / < ( p | d i v | b r | u l | o l | l i | a | s t r o n g | e m | t t | c o d e | p r e | h [ 1 - 6 ] ) \b / i. test ( s ) ;
274+ }
275+
276+ function htmlToPlainText ( html ) {
277+ if ( ! html ) return "" ;
278+
279+ let text = String ( html ) ;
280+
281+ // Drop script/style blocks entirely.
282+ text = text . replace ( / < s c r i p t [ ^ > ] * > [ \s \S ] * ?< \/ s c r i p t > / gi, "" ) ;
283+ text = text . replace ( / < s t y l e [ ^ > ] * > [ \s \S ] * ?< \/ s t y l e > / gi, "" ) ;
284+
285+ // Keep links readable after stripping tags.
286+ text = text . replace (
287+ / < a \s + [ ^ > ] * h r e f = ( [ " ' ] ) ( [ ^ " ' ] + ) \1[ ^ > ] * > ( [ \s \S ] * ?) < \/ a > / gi,
288+ ( _m , _q , href , inner ) => {
289+ const innerText = String ( inner ) . replace ( / < [ ^ > ] * > / g, "" ) . trim ( ) ;
290+ const url = String ( href || "" ) . trim ( ) ;
291+ if ( ! url ) return innerText ;
292+ if ( ! innerText ) return url ;
293+ return `${ innerText } (${ url } )` ;
294+ }
295+ ) ;
296+
297+ // Preserve line breaks / list structure before stripping remaining tags.
298+ text = text . replace ( / < \s * b r \s * \/ ? > / gi, "\n" ) ;
299+ text = text . replace ( / < \/ \s * ( p | d i v | h 1 | h 2 | h 3 | h 4 | h 5 | h 6 ) \s * > / gi, "\n" ) ;
300+ text = text . replace ( / < \s * l i [ ^ > ] * > / gi, "- " ) ;
301+ text = text . replace ( / < \/ \s * l i \s * > / gi, "\n" ) ;
302+ text = text . replace ( / < \/ \s * ( u l | o l ) \s * > / gi, "\n" ) ;
303+
304+ // Strip remaining tags.
305+ text = text . replace ( / < [ ^ > ] * > / g, "" ) ;
306+
307+ // Decode the handful of entities we commonly see from GitHub-rendered HTML.
308+ const named = {
309+ nbsp : " " ,
310+ amp : "&" ,
311+ lt : "<" ,
312+ gt : ">" ,
313+ quot : '"' ,
314+ apos : "'" ,
315+ "#39" : "'" ,
316+ } ;
317+ text = text . replace ( / & ( [ a - z 0 - 9 # ] + ) ; / gi, ( m , name ) => {
318+ const key = String ( name || "" ) . toLowerCase ( ) ;
319+ if ( named [ key ] != null ) return named [ key ] ;
320+
321+ // Numeric entities (decimal / hex).
322+ const decMatch = key . match ( / ^ # ( \d + ) $ / ) ;
323+ if ( decMatch ) {
324+ const n = Number ( decMatch [ 1 ] ) ;
325+ if ( Number . isFinite ( n ) && n >= 0 && n <= 0x10ffff ) {
326+ try {
327+ return String . fromCodePoint ( n ) ;
328+ } catch {
329+ return m ;
330+ }
331+ }
332+ return m ;
333+ }
334+
335+ const hexMatch = key . match ( / ^ # x ( [ 0 - 9 a - f ] + ) $ / i) ;
336+ if ( hexMatch ) {
337+ const n = Number . parseInt ( hexMatch [ 1 ] , 16 ) ;
338+ if ( Number . isFinite ( n ) && n >= 0 && n <= 0x10ffff ) {
339+ try {
340+ return String . fromCodePoint ( n ) ;
341+ } catch {
342+ return m ;
343+ }
344+ }
345+ return m ;
346+ }
347+
348+ return m ;
349+ } ) ;
350+
351+ // Normalize whitespace/newlines.
352+ text = text . replace ( / \r \n / g, "\n" ) ;
353+ text = text . replace ( / \n { 3 , } / g, "\n\n" ) ;
354+ return text . trim ( ) ;
355+ }
356+
268357function normalizeReleaseNotes ( releaseNotes ) {
269358 if ( ! releaseNotes ) return "" ;
270- if ( typeof releaseNotes === "string" ) return releaseNotes ;
359+
360+ const normalizeText = ( value ) => {
361+ if ( value == null ) return "" ;
362+ const raw = typeof value === "string" ? value : String ( value ) ;
363+ const trimmed = raw . trim ( ) ;
364+ if ( ! trimmed ) return "" ;
365+ if ( looksLikeHtml ( trimmed ) ) return htmlToPlainText ( trimmed ) ;
366+ return trimmed ;
367+ } ;
368+
369+ if ( typeof releaseNotes === "string" ) return normalizeText ( releaseNotes ) ;
271370 if ( Array . isArray ( releaseNotes ) ) {
272371 const parts = [ ] ;
273372 for ( const item of releaseNotes ) {
274373 const version = item ?. version ? String ( item . version ) : "" ;
275374 const note = item ?. note ;
276375 const noteText =
277376 typeof note === "string" ? note : note != null ? JSON . stringify ( note , null , 2 ) : "" ;
278- const block = [ version ? `v${ version } ` : "" , noteText ] . filter ( Boolean ) . join ( "\n" ) ;
377+ const block = [ version ? `v${ version } ` : "" , normalizeText ( noteText ) ]
378+ . filter ( Boolean )
379+ . join ( "\n" ) ;
279380 if ( block ) parts . push ( block ) ;
280381 }
281382 return parts . join ( "\n\n" ) ;
282383 }
283384 try {
284- return JSON . stringify ( releaseNotes , null , 2 ) ;
385+ return normalizeText ( JSON . stringify ( releaseNotes , null , 2 ) ) ;
285386 } catch {
286- return String ( releaseNotes ) ;
387+ return normalizeText ( releaseNotes ) ;
287388 }
288389}
289390
0 commit comments