@@ -191,3 +191,181 @@ fn is_at_end_of_line_content_at_end_of_text() {
191191 let result = TextProcessor :: is_at_end_of_line_content ( text, 5 , & line_starts, & [ ] ) ;
192192 assert ! ( !result) ; // Out of bounds
193193}
194+
195+ // ============================================
196+ // should_skip_character - middle newline
197+ // ============================================
198+
199+ #[ test]
200+ fn should_skip_character_newline_in_middle ( ) {
201+ let text = "hello\n world" ;
202+ let line_starts = TextProcessor :: calculate_line_starts ( text) ;
203+ // Newline at position 5 is NOT the final newline, so should NOT be skipped
204+ let result = TextProcessor :: should_skip_character ( text, 5 , & line_starts, & [ ] ) ;
205+ assert ! ( !result) ;
206+ }
207+
208+ #[ test]
209+ fn should_skip_character_out_of_bounds ( ) {
210+ let text = "hello" ;
211+ let line_starts = TextProcessor :: calculate_line_starts ( text) ;
212+ let result = TextProcessor :: should_skip_character ( text, 100 , & line_starts, & [ ] ) ;
213+ assert ! ( !result) ;
214+ }
215+
216+ // ============================================
217+ // should_skip_character - whitespace before comment
218+ // ============================================
219+
220+ #[ test]
221+ fn should_skip_character_whitespace_before_comment ( ) {
222+ // "code // comment" where comment starts at position 6
223+ let text = "code // comment" ;
224+ let line_starts = TextProcessor :: calculate_line_starts ( text) ;
225+ let comment_ranges = vec ! [ ( 6 , 16 ) ] ;
226+ // Position 4 is space before the comment
227+ let result = TextProcessor :: should_skip_character ( text, 4 , & line_starts, & comment_ranges) ;
228+ assert ! ( result) ; // whitespace before comment should be skipped
229+ }
230+
231+ #[ test]
232+ fn should_skip_character_whitespace_not_before_comment ( ) {
233+ let text = "code more" ;
234+ let line_starts = TextProcessor :: calculate_line_starts ( text) ;
235+ // Position 4 is space, but no comment follows
236+ let result = TextProcessor :: should_skip_character ( text, 4 , & line_starts, & [ ] ) ;
237+ assert ! ( !result) ;
238+ }
239+
240+ // ============================================
241+ // process_challenge_text_with_comment_mapping - with comment ranges
242+ // ============================================
243+
244+ #[ test]
245+ fn process_with_comment_mapping_maps_comments_correctly ( ) {
246+ let text = "code // comment\n next line" ;
247+ let comment_ranges = vec ! [ ( 5 , 15 ) ] ;
248+ let ( processed, mapped_ranges) =
249+ TextProcessor :: process_challenge_text_with_comment_mapping ( text, & comment_ranges) ;
250+ assert_eq ! ( processed, "code // comment\n next line" ) ;
251+ assert ! ( !mapped_ranges. is_empty( ) ) ;
252+ // The comment range should be mapped to the same positions in processed text
253+ let ( start, end) = mapped_ranges[ 0 ] ;
254+ assert_eq ! ( start, 5 ) ;
255+ assert ! ( end > start) ;
256+ }
257+
258+ #[ test]
259+ fn process_with_comment_mapping_removes_empty_lines_and_adjusts_ranges ( ) {
260+ let text = "code // comment\n \n next line" ;
261+ let comment_ranges = vec ! [ ( 5 , 15 ) ] ;
262+ let ( processed, mapped_ranges) =
263+ TextProcessor :: process_challenge_text_with_comment_mapping ( text, & comment_ranges) ;
264+ // Empty line should be removed
265+ assert_eq ! ( processed, "code // comment\n next line" ) ;
266+ // Comment range should still be valid in the processed text
267+ assert ! ( !mapped_ranges. is_empty( ) ) ;
268+ }
269+
270+ #[ test]
271+ fn process_with_comment_mapping_comment_in_removed_line ( ) {
272+ // Comment is entirely in an empty/removed section
273+ let text = "\n // only comment\n code" ;
274+ // First line is empty, gets removed. Comment is on second line (positions 1-16)
275+ let comment_ranges = vec ! [ ( 1 , 17 ) ] ;
276+ let ( processed, mapped_ranges) =
277+ TextProcessor :: process_challenge_text_with_comment_mapping ( text, & comment_ranges) ;
278+ // "// only comment" line is not empty so should be kept
279+ assert ! ( processed. contains( "code" ) ) ;
280+ // mapped_ranges should still have the comment
281+ let _ = mapped_ranges; // just checking it doesn't panic
282+ }
283+
284+ #[ test]
285+ fn process_with_comment_mapping_preserve_empty_with_comments ( ) {
286+ let text = "code // comment\n \n next line" ;
287+ let comment_ranges = vec ! [ ( 5 , 15 ) ] ;
288+ let ( processed, mapped_ranges) =
289+ TextProcessor :: process_challenge_text_with_comment_mapping_preserve_empty (
290+ text,
291+ & comment_ranges,
292+ true ,
293+ ) ;
294+ // With preserve_empty = true, empty line should be kept
295+ assert ! ( processed. lines( ) . count( ) >= 3 ) ;
296+ assert ! ( !mapped_ranges. is_empty( ) ) ;
297+ }
298+
299+ // ============================================
300+ // is_at_end_of_line_content - with comment ranges
301+ // ============================================
302+
303+ #[ test]
304+ fn is_at_end_of_line_content_before_comment ( ) {
305+ let text = "code // comment\n next" ;
306+ let line_starts = TextProcessor :: calculate_line_starts ( text) ;
307+ let comment_ranges = vec ! [ ( 5 , 15 ) ] ;
308+ // Position 4 is space before comment - rest of line is comment only
309+ let result = TextProcessor :: is_at_end_of_line_content ( text, 4 , & line_starts, & comment_ranges) ;
310+ assert ! ( result) ;
311+ }
312+
313+ #[ test]
314+ fn is_at_end_of_line_content_before_code ( ) {
315+ let text = "code more_code" ;
316+ let line_starts = TextProcessor :: calculate_line_starts ( text) ;
317+ let result = TextProcessor :: is_at_end_of_line_content ( text, 4 , & line_starts, & [ ] ) ;
318+ assert ! ( !result) ;
319+ }
320+
321+ // ============================================
322+ // is_rest_of_line_comment_only - edge cases
323+ // ============================================
324+
325+ #[ test]
326+ fn is_rest_of_line_comment_only_whitespace_then_comment ( ) {
327+ let text = "code // comment" ;
328+ let comment_ranges = vec ! [ ( 7 , 17 ) ] ;
329+ // Position 4 has spaces then comment
330+ let result = TextProcessor :: is_rest_of_line_comment_only ( text, 4 , & comment_ranges) ;
331+ assert ! ( result) ;
332+ }
333+
334+ #[ test]
335+ fn is_rest_of_line_comment_only_out_of_bounds ( ) {
336+ let result = TextProcessor :: is_rest_of_line_comment_only ( "hello" , 100 , & [ ] ) ;
337+ assert ! ( !result) ;
338+ }
339+
340+ #[ test]
341+ fn is_rest_of_line_comment_only_at_newline_boundary ( ) {
342+ let text = "code // comment\n next" ;
343+ let comment_ranges = vec ! [ ( 5 , 15 ) ] ;
344+ let result = TextProcessor :: is_rest_of_line_comment_only ( text, 5 , & comment_ranges) ;
345+ assert ! ( result) ;
346+ }
347+
348+ // ============================================
349+ // process_challenge_text - more edge cases
350+ // ============================================
351+
352+ #[ test]
353+ fn process_challenge_text_only_whitespace_lines ( ) {
354+ let text = " \n \n " ;
355+ let result = TextProcessor :: process_challenge_text ( text) ;
356+ assert_eq ! ( result, "" ) ;
357+ }
358+
359+ #[ test]
360+ fn process_challenge_text_mixed_empty_and_content ( ) {
361+ let text = "\n line1\n \n \n line2\n " ;
362+ let result = TextProcessor :: process_challenge_text ( text) ;
363+ assert_eq ! ( result, "line1\n line2" ) ;
364+ }
365+
366+ #[ test]
367+ fn should_skip_final_newline_non_newline_char ( ) {
368+ let text = "hello" ;
369+ let result = TextProcessor :: should_skip_final_newline ( text, 4 ) ;
370+ assert ! ( !result) ; // 'o' is not a newline
371+ }
0 commit comments