@@ -44,6 +44,14 @@ export interface TruncationOptions {
4444 maxBytes ?: number ;
4545}
4646
47+ interface ResolvedTruncationInput {
48+ lines : string [ ] ;
49+ totalLines : number ;
50+ totalBytes : number ;
51+ maxLines : number ;
52+ maxBytes : number ;
53+ }
54+
4755interface RuntimeBuffer {
4856 byteLength ( content : string , encoding : "utf8" ) : number ;
4957}
@@ -137,6 +145,50 @@ export function formatSize(bytes: number): string {
137145 return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 1 ) } MB` ;
138146}
139147
148+ function resolveTruncationInput (
149+ content : string ,
150+ options : TruncationOptions ,
151+ ) : ResolvedTruncationInput {
152+ const maxLines = options . maxLines ?? DEFAULT_MAX_LINES ;
153+ const maxBytes = options . maxBytes ?? DEFAULT_MAX_BYTES ;
154+ const totalBytes = utf8ByteLength ( content ) ;
155+ const lines = splitLinesForCounting ( content ) ;
156+ return {
157+ lines,
158+ totalLines : lines . length ,
159+ totalBytes,
160+ maxLines,
161+ maxBytes,
162+ } ;
163+ }
164+
165+ function buildTruncationResult (
166+ input : ResolvedTruncationInput ,
167+ params : {
168+ content : string ;
169+ truncated : boolean ;
170+ truncatedBy : TruncationResult [ "truncatedBy" ] ;
171+ outputLines : number ;
172+ outputBytes ?: number ;
173+ lastLinePartial ?: boolean ;
174+ firstLineExceedsLimit ?: boolean ;
175+ } ,
176+ ) : TruncationResult {
177+ return {
178+ content : params . content ,
179+ truncated : params . truncated ,
180+ truncatedBy : params . truncatedBy ,
181+ totalLines : input . totalLines ,
182+ totalBytes : input . totalBytes ,
183+ outputLines : params . outputLines ,
184+ outputBytes : params . outputBytes ?? utf8ByteLength ( params . content ) ,
185+ lastLinePartial : params . lastLinePartial ?? false ,
186+ firstLineExceedsLimit : params . firstLineExceedsLimit ?? false ,
187+ maxLines : input . maxLines ,
188+ maxBytes : input . maxBytes ,
189+ } ;
190+ }
191+
140192/**
141193 * Truncate content from the head (keep first N lines/bytes).
142194 * Suitable for file reads where you want to see the beginning.
@@ -145,58 +197,39 @@ export function formatSize(bytes: number): string {
145197 * returns empty content with firstLineExceedsLimit=true.
146198 */
147199export function truncateHead ( content : string , options : TruncationOptions = { } ) : TruncationResult {
148- const maxLines = options . maxLines ?? DEFAULT_MAX_LINES ;
149- const maxBytes = options . maxBytes ?? DEFAULT_MAX_BYTES ;
150-
151- const totalBytes = utf8ByteLength ( content ) ;
152- const lines = splitLinesForCounting ( content ) ;
153- const totalLines = lines . length ;
200+ const input = resolveTruncationInput ( content , options ) ;
154201
155- // Check if no truncation needed
156- if ( totalLines <= maxLines && totalBytes <= maxBytes ) {
157- return {
202+ if ( input . totalLines <= input . maxLines && input . totalBytes <= input . maxBytes ) {
203+ return buildTruncationResult ( input , {
158204 content,
159205 truncated : false ,
160206 truncatedBy : null ,
161- totalLines,
162- totalBytes,
163- outputLines : totalLines ,
164- outputBytes : totalBytes ,
165- lastLinePartial : false ,
166- firstLineExceedsLimit : false ,
167- maxLines,
168- maxBytes,
169- } ;
207+ outputLines : input . totalLines ,
208+ outputBytes : input . totalBytes ,
209+ } ) ;
170210 }
171211
172- // Check if first line alone exceeds byte limit
173- const firstLineBytes = utf8ByteLength ( lines [ 0 ] ) ;
174- if ( firstLineBytes > maxBytes ) {
175- return {
212+ const firstLineBytes = utf8ByteLength ( input . lines [ 0 ] ) ;
213+ if ( firstLineBytes > input . maxBytes ) {
214+ return buildTruncationResult ( input , {
176215 content : "" ,
177216 truncated : true ,
178217 truncatedBy : "bytes" ,
179- totalLines,
180- totalBytes,
181218 outputLines : 0 ,
182219 outputBytes : 0 ,
183- lastLinePartial : false ,
184220 firstLineExceedsLimit : true ,
185- maxLines,
186- maxBytes,
187- } ;
221+ } ) ;
188222 }
189223
190- // Collect complete lines that fit
191224 const outputLinesArr : string [ ] = [ ] ;
192225 let outputBytesCount = 0 ;
193- let truncatedBy : "lines" | "bytes" = totalLines > maxLines ? "lines" : "bytes" ;
226+ let truncatedBy : "lines" | "bytes" = input . totalLines > input . maxLines ? "lines" : "bytes" ;
194227
195- for ( let i = 0 ; i < lines . length && i < maxLines ; i ++ ) {
196- const line = lines [ i ] ;
228+ for ( let i = 0 ; i < input . lines . length && i < input . maxLines ; i ++ ) {
229+ const line = input . lines [ i ] ;
197230 const lineBytes = utf8ByteLength ( line ) + ( i > 0 ? 1 : 0 ) ; // +1 for newline
198231
199- if ( outputBytesCount + lineBytes > maxBytes ) {
232+ if ( outputBytesCount + lineBytes > input . maxBytes ) {
200233 truncatedBy = "bytes" ;
201234 break ;
202235 }
@@ -205,27 +238,22 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
205238 outputBytesCount += lineBytes ;
206239 }
207240
208- // If we exited due to line limit
209- if ( totalLines > maxLines && outputLinesArr . length >= maxLines && outputBytesCount <= maxBytes ) {
241+ if (
242+ input . totalLines > input . maxLines &&
243+ outputLinesArr . length >= input . maxLines &&
244+ outputBytesCount <= input . maxBytes
245+ ) {
210246 truncatedBy = "lines" ;
211247 }
212248
213249 const outputContent = outputLinesArr . join ( "\n" ) ;
214- const finalOutputBytes = utf8ByteLength ( outputContent ) ;
215250
216- return {
251+ return buildTruncationResult ( input , {
217252 content : outputContent ,
218253 truncated : true ,
219254 truncatedBy,
220- totalLines,
221- totalBytes,
222255 outputLines : outputLinesArr . length ,
223- outputBytes : finalOutputBytes ,
224- lastLinePartial : false ,
225- firstLineExceedsLimit : false ,
226- maxLines,
227- maxBytes,
228- } ;
256+ } ) ;
229257}
230258
231259/**
@@ -235,46 +263,33 @@ export function truncateHead(content: string, options: TruncationOptions = {}):
235263 * May return partial first line if the last line of original content exceeds byte limit.
236264 */
237265export function truncateTail ( content : string , options : TruncationOptions = { } ) : TruncationResult {
238- const maxLines = options . maxLines ?? DEFAULT_MAX_LINES ;
239- const maxBytes = options . maxBytes ?? DEFAULT_MAX_BYTES ;
240-
241- const totalBytes = utf8ByteLength ( content ) ;
242- const lines = splitLinesForCounting ( content ) ;
243- const totalLines = lines . length ;
266+ const input = resolveTruncationInput ( content , options ) ;
244267
245- // Check if no truncation needed
246- if ( totalLines <= maxLines && totalBytes <= maxBytes ) {
247- return {
268+ if ( input . totalLines <= input . maxLines && input . totalBytes <= input . maxBytes ) {
269+ return buildTruncationResult ( input , {
248270 content,
249271 truncated : false ,
250272 truncatedBy : null ,
251- totalLines,
252- totalBytes,
253- outputLines : totalLines ,
254- outputBytes : totalBytes ,
255- lastLinePartial : false ,
256- firstLineExceedsLimit : false ,
257- maxLines,
258- maxBytes,
259- } ;
273+ outputLines : input . totalLines ,
274+ outputBytes : input . totalBytes ,
275+ } ) ;
260276 }
261277
262- // Work backwards from the end
263278 const outputLinesArr : string [ ] = [ ] ;
264279 let outputBytesCount = 0 ;
265- let truncatedBy : "lines" | "bytes" = totalLines > maxLines ? "lines" : "bytes" ;
280+ let truncatedBy : "lines" | "bytes" = input . totalLines > input . maxLines ? "lines" : "bytes" ;
266281 let lastLinePartial = false ;
267282
268- for ( let i = lines . length - 1 ; i >= 0 && outputLinesArr . length < maxLines ; i -- ) {
269- const line = lines [ i ] ;
283+ for ( let i = input . lines . length - 1 ; i >= 0 && outputLinesArr . length < input . maxLines ; i -- ) {
284+ const line = input . lines [ i ] ;
270285 const lineBytes = utf8ByteLength ( line ) + ( outputLinesArr . length > 0 ? 1 : 0 ) ; // +1 for newline
271286
272- if ( outputBytesCount + lineBytes > maxBytes ) {
287+ if ( outputBytesCount + lineBytes > input . maxBytes ) {
273288 truncatedBy = "bytes" ;
274289 // Edge case: if we haven't added ANY lines yet and this line exceeds maxBytes,
275290 // take the end of the line (partial)
276291 if ( outputLinesArr . length === 0 ) {
277- const truncatedLine = truncateStringToBytesFromEnd ( line , maxBytes ) ;
292+ const truncatedLine = truncateStringToBytesFromEnd ( line , input . maxBytes ) ;
278293 outputLinesArr . unshift ( truncatedLine ) ;
279294 outputBytesCount = utf8ByteLength ( truncatedLine ) ;
280295 lastLinePartial = true ;
@@ -286,27 +301,23 @@ export function truncateTail(content: string, options: TruncationOptions = {}):
286301 outputBytesCount += lineBytes ;
287302 }
288303
289- // If we exited due to line limit
290- if ( totalLines > maxLines && outputLinesArr . length >= maxLines && outputBytesCount <= maxBytes ) {
304+ if (
305+ input . totalLines > input . maxLines &&
306+ outputLinesArr . length >= input . maxLines &&
307+ outputBytesCount <= input . maxBytes
308+ ) {
291309 truncatedBy = "lines" ;
292310 }
293311
294312 const outputContent = outputLinesArr . join ( "\n" ) ;
295- const finalOutputBytes = utf8ByteLength ( outputContent ) ;
296313
297- return {
314+ return buildTruncationResult ( input , {
298315 content : outputContent ,
299316 truncated : true ,
300317 truncatedBy,
301- totalLines,
302- totalBytes,
303318 outputLines : outputLinesArr . length ,
304- outputBytes : finalOutputBytes ,
305319 lastLinePartial,
306- firstLineExceedsLimit : false ,
307- maxLines,
308- maxBytes,
309- } ;
320+ } ) ;
310321}
311322
312323/**
0 commit comments