@@ -17,6 +17,11 @@ interface ParagraphRange {
1717 end : number ;
1818}
1919
20+ interface ListItemRange {
21+ start : number ;
22+ end : number ;
23+ }
24+
2025const getFenceRanges = ( content : string ) : FenceRange [ ] => {
2126 const fenceRanges : FenceRange [ ] = [ ] ;
2227 const fenceRegex = / ^ ( [ ` ~ ] { 3 , } ) [ ^ \n ] * (?: \n | $ ) / gm;
@@ -115,10 +120,55 @@ const getLastParagraphRangeBeforeCutOff = (
115120 return cutPoint ;
116121} ;
117122
123+ const getListItemRanges = ( content : string , fenceRanges : FenceRange [ ] ) : ListItemRange [ ] => {
124+ const listItemRanges : ListItemRange [ ] = [ ] ;
125+ const listItemRegex = / ^ \s * (?: [ - + * ] \s + | \d + [ . ) ] \s + ) / ;
126+ let activeListItemRange : ListItemRange | undefined ;
127+ let lineStart = 0 ;
128+
129+ while ( lineStart < content . length ) {
130+ const lineBreak = content . indexOf ( "\n" , lineStart ) ;
131+ const lineEnd = lineBreak === - 1 ? content . length : lineBreak ;
132+ const nextLineStart = lineBreak === - 1 ? content . length : lineBreak + 1 ;
133+ const line = content . slice ( lineStart , lineEnd ) ;
134+ const isListItem = listItemRegex . test ( line ) && ! isInsideFence ( fenceRanges , lineStart ) ;
135+ const isListContinuation = Boolean ( activeListItemRange && line . trim ( ) && / ^ \s + / . test ( line ) ) ;
136+
137+ if ( isListItem ) {
138+ if ( activeListItemRange ) {
139+ listItemRanges . push ( activeListItemRange ) ;
140+ }
141+ activeListItemRange = { start : lineStart , end : nextLineStart } ;
142+ } else if ( activeListItemRange && isListContinuation ) {
143+ activeListItemRange . end = nextLineStart ;
144+ } else if ( activeListItemRange ) {
145+ listItemRanges . push ( activeListItemRange ) ;
146+ activeListItemRange = undefined ;
147+ }
148+
149+ lineStart = nextLineStart ;
150+ }
151+
152+ if ( activeListItemRange ) {
153+ listItemRanges . push ( activeListItemRange ) ;
154+ }
155+
156+ return listItemRanges ;
157+ } ;
158+
159+ const getLastListItemRangeEndBeforeCutOff = ( listItemRanges : ListItemRange [ ] , cutOff : number ) : number => {
160+ let cutPoint = - 1 ;
161+ for ( const listItemRange of listItemRanges ) {
162+ if ( listItemRange . end > cutOff ) break ;
163+ cutPoint = listItemRange . end ;
164+ }
165+ return cutPoint ;
166+ } ;
167+
118168/**
119169 * Truncates a Markdown string at a safe raw boundary.
120- * It keeps links atomic and closes partial fenced code blocks; display-length refinement is handled by
121- * `truncateMarkdownDisplay`.
170+ * It keeps links atomic, prefers boundaries outside structured blocks, and closes a partial fenced code block only
171+ * when no safer boundary exists. Display-length refinement is handled by `truncateMarkdownDisplay`.
122172 */
123173export const truncateMarkdown = ( content : string , cutOff : number , suffix ?: string ) : string => {
124174 if ( ! cutOff || cutOff <= 0 || content . length <= cutOff ) {
@@ -128,17 +178,13 @@ export const truncateMarkdown = (content: string, cutOff: number, suffix?: strin
128178 const appendSuffix = ( truncated : string ) => ( suffix ? `${ truncated . trimEnd ( ) } \n\n${ suffix } ` : truncated . trimEnd ( ) ) ;
129179 const fenceRanges = getFenceRanges ( content ) ;
130180 const linkRanges = getLinkRanges ( content , fenceRanges ) ;
181+ const listItemRanges = getListItemRanges ( content , fenceRanges ) ;
131182 const safeCutOff = moveCutOffOutsideLink ( cutOff , linkRanges ) ;
132183
133184 if ( safeCutOff >= content . length ) {
134185 return content ;
135186 }
136187
137- const activeFence = fenceRanges . find ( ( { start, end } ) => safeCutOff > start && safeCutOff < end ) ;
138- if ( activeFence ) {
139- return appendSuffix ( truncateActiveFence ( content , safeCutOff , activeFence ) ) ;
140- }
141-
142188 if ( linkRanges . some ( ( { start } ) => safeCutOff === start ) ) {
143189 return appendSuffix ( content . slice ( 0 , safeCutOff ) ) ;
144190 }
@@ -148,6 +194,18 @@ export const truncateMarkdown = (content: string, cutOff: number, suffix?: strin
148194 }
149195
150196 let cutPoint = getLastParagraphRangeBeforeCutOff ( getParagraphRanges ( content ) , safeCutOff , fenceRanges ) ;
197+ const listBoundary = getLastListItemRangeEndBeforeCutOff ( listItemRanges , safeCutOff ) ;
198+ if ( listBoundary > cutPoint ) {
199+ cutPoint = listBoundary ;
200+ }
201+
202+ const activeListItem = listItemRanges . find ( ( { start, end } ) => safeCutOff > start && safeCutOff < end ) ;
203+ if ( activeListItem ) {
204+ const cutBeforeListItem = activeListItem . start > 0 && content . slice ( 0 , activeListItem . start ) . trim ( ) . length > 0 ;
205+ if ( cutPoint !== - 1 || cutBeforeListItem ) {
206+ return appendSuffix ( content . slice ( 0 , cutBeforeListItem ? activeListItem . start : cutPoint ) ) ;
207+ }
208+ }
151209
152210 if ( cutPoint === - 1 ) {
153211 const lineBoundary = content . lastIndexOf ( "\n" , safeCutOff ) ;
@@ -156,6 +214,16 @@ export const truncateMarkdown = (content: string, cutOff: number, suffix?: strin
156214 }
157215 }
158216
217+ const activeFence = fenceRanges . find ( ( { start, end } ) => safeCutOff > start && safeCutOff < end ) ;
218+ if ( activeFence ) {
219+ const cutBeforeFence = activeFence . start > 0 && content . slice ( 0 , activeFence . start ) . trim ( ) . length > 0 ;
220+ if ( cutPoint !== - 1 || cutBeforeFence ) {
221+ return appendSuffix ( content . slice ( 0 , cutPoint !== - 1 ? cutPoint : activeFence . start ) ) ;
222+ }
223+
224+ return appendSuffix ( truncateActiveFence ( content , safeCutOff , activeFence ) ) ;
225+ }
226+
159227 if ( cutPoint === - 1 ) {
160228 const lastSpace = content . lastIndexOf ( " " , safeCutOff ) ;
161229 cutPoint = lastSpace > 0 ? lastSpace : safeCutOff ;
0 commit comments