@@ -9,6 +9,8 @@ use std::path::{Path, PathBuf};
99pub mod text_processing {
1010 use regex:: Regex ;
1111
12+ use crate :: cfp:: normalize_timeframe;
13+
1214 /// Process template content by replacing placeholders and removing notes
1315 pub fn process_template_content (
1416 content : & str ,
@@ -71,7 +73,12 @@ pub mod text_processing {
7173 let mut new_content = content. to_string ( ) ;
7274
7375 // Create the new section content with capitalized H
74- let capitalized_timeframe = format ! ( "{}H{}" , & timeframe[ 0 ..4 ] , & timeframe[ 5 ..] ) ;
76+ let capitalized_timeframe = if timeframe. len ( ) > 4 {
77+ format ! ( "{}H{}" , & timeframe[ 0 ..4 ] , & timeframe[ 5 ..] )
78+ } else {
79+ timeframe. to_string ( )
80+ } ;
81+
7582 let new_section_content = format ! (
7683 "# ⏳ {} goal process\n \n \
7784 - [Overview](./{}/README.md)\n \
@@ -134,9 +141,9 @@ pub mod text_processing {
134141 let new_section = format ! ( "\n {}" , new_section_content) ;
135142
136143 // Find a good place to insert the new section
137- // Look for the last timeframe section or insert at the beginning
138- // Match both lowercase and uppercase H
139- let re = Regex :: new ( r"# ⏳ \d{4}[hH][12] goal process" ) . unwrap ( ) ;
144+ // Look for the last timeframe section or insert after # Summary
145+ // Match both year-only (2027) and half-year (2026H1) formats
146+ let re = Regex :: new ( r"# ⏳ \d{4}( [hH][12])? goal process" ) . unwrap ( ) ;
140147
141148 if let Some ( last_match) = re. find_iter ( & content) . last ( ) {
142149 // Find the end of this section (next section or end of file)
@@ -148,8 +155,20 @@ pub mod text_processing {
148155 new_content. push_str ( & new_section) ;
149156 }
150157 } else {
151- // No existing timeframe sections, insert at the beginning
152- new_content = new_section + & content;
158+ // No existing ⏳ timeframe sections, find the first dated section (⏳ or ⚙️) and insert before it
159+ let dated_section_re = Regex :: new ( r"\n# (⏳|⚙️) \d{4}" ) . unwrap ( ) ;
160+ if let Some ( first_dated) = dated_section_re. find ( & content) {
161+ new_content. insert_str ( first_dated. start ( ) , & new_section) ;
162+ } else {
163+ // No dated sections at all, insert after "# Summary\n\n"
164+ if let Some ( summary_pos) = content. find ( "# Summary" ) {
165+ let insert_pos = summary_pos + "# Summary\n \n " . len ( ) ;
166+ new_content. insert_str ( insert_pos, & new_section) ;
167+ } else {
168+ // No # Summary found, prepend
169+ new_content = new_section + & content;
170+ }
171+ }
153172 }
154173
155174 new_content
@@ -163,28 +182,38 @@ pub mod text_processing {
163182 ) -> String {
164183 let mut new_content = content. to_string ( ) ;
165184
166- // Extract year and half from timeframe
167- let _year = & timeframe[ 0 ..4 ] ;
168- let half = & timeframe[ 4 ..] . to_lowercase ( ) ;
169-
170- // Determine the months based on the half
171- let ( start_month, end_month) = if half == "h1" {
172- ( "January" , "June" )
185+ // Create the new section to add with capitalized H
186+ let capitalized_timeframe = normalize_timeframe ( timeframe) ;
187+
188+ // Check if this is a year-only timeframe (no H1/H2)
189+ let new_section = if timeframe. len ( ) == 4 {
190+ // Year-only format - no date range
191+ format ! (
192+ "\n ## Next goal period ({})\n \n \
193+ The next goal period will be {}. \
194+ We are currently in the process of assembling goals. \
195+ [Click here](./{}/goals.md) to see the current list. \
196+ If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md).\n ",
197+ capitalized_timeframe, capitalized_timeframe, lowercase_timeframe
198+ )
173199 } else {
174- ( "July" , "December" )
200+ // Half-year format - include date range
201+ let half = & timeframe[ 4 ..] . to_lowercase ( ) ;
202+ let ( start_month, end_month) = if half == "h1" {
203+ ( "January" , "June" )
204+ } else {
205+ ( "July" , "December" )
206+ } ;
207+ format ! (
208+ "\n ## Next goal period ({})\n \n \
209+ The next goal period will be {}, running from the start of {} to the end of {}. \
210+ We are currently in the process of assembling goals. \
211+ [Click here](./{}/goals.md) to see the current list. \
212+ If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md).\n ",
213+ capitalized_timeframe, capitalized_timeframe, start_month, end_month, lowercase_timeframe
214+ )
175215 } ;
176216
177- // Create the new section to add with capitalized H
178- let capitalized_timeframe = format ! ( "{}H{}" , & timeframe[ 0 ..4 ] , & timeframe[ 5 ..] ) ;
179- let new_section = format ! (
180- "\n ## Next goal period ({})\n \n \
181- The next goal period will be {}, running from the start of {} to the end of {}. \
182- We are currently in the process of assembling goals. \
183- [Click here](./{}/goals.md) to see the current list. \
184- If you'd like to propose a goal, [instructions can be found here](./how_to/propose_a_goal.md).\n ",
185- capitalized_timeframe, capitalized_timeframe, start_month, end_month, lowercase_timeframe
186- ) ;
187-
188217 // First check for an existing entry for this specific timeframe
189218 let this_period_pattern = Regex :: new ( & format ! (
190219 r"## Next goal period(?:\s*\({}\))?\s*\n" ,
@@ -340,15 +369,23 @@ pub fn create_cfp(timeframe: &str, force: bool, dry_run: bool) -> Result<()> {
340369 Ok ( ( ) )
341370}
342371
343- /// Validates that the timeframe is in the correct format (e.g., "2025h1" or "2025H1")
372+ /// Validates that the timeframe is in the correct format (e.g., "2025h1" or "2025H1" or '2026' )
344373fn validate_timeframe ( timeframe : & str ) -> Result < ( ) > {
345- let re = Regex :: new ( r"^\d{4}[hH][12]$" ) . unwrap ( ) ;
374+ let re = Regex :: new ( r"^\d{4}( [hH][12])? $" ) . unwrap ( ) ;
346375 if !re. is_match ( timeframe) {
347376 return Err ( Error :: str ( "Invalid timeframe format. Expected format: YYYYhN or YYYYHN (e.g., 2025h1, 2025H1, 2025h2, or 2025H2" ) ) ;
348377 }
349378 Ok ( ( ) )
350379}
351380
381+ fn normalize_timeframe ( timeframe : & str ) -> String {
382+ if timeframe. len ( ) == 4 {
383+ timeframe. to_string ( )
384+ } else {
385+ format ! ( "{}H{}" , & timeframe[ 0 ..4 ] , & timeframe[ 5 ..] )
386+ }
387+ }
388+
352389/// Copies a template file to the destination and replaces placeholders
353390fn copy_and_process_template (
354391 template_path : & str ,
@@ -444,7 +481,7 @@ fn update_main_readme(timeframe: &str, lowercase_timeframe: &str, dry_run: bool)
444481 }
445482
446483 // Determine what kind of update was made for better logging
447- let capitalized_timeframe = format ! ( "{}H{}" , & timeframe[ 0 .. 4 ] , & timeframe [ 5 .. ] ) ;
484+ let capitalized_timeframe = normalize_timeframe ( timeframe) ;
448485 let specific_timeframe_pattern = format ! (
449486 r"## Next goal period(?:\s*\({}\))" ,
450487 regex:: escape( & capitalized_timeframe)
@@ -522,15 +559,39 @@ mod tests {
522559 }
523560
524561 #[ test]
525- fn test_process_summary_content_no_existing_section ( ) {
526- // Test adding a new section when no timeframe sections exist
527- let content = "# Summary\n \n [Introduction](./README.md)\n " ;
562+ fn test_process_summary_content_new_section ( ) {
563+ // Test adding a new section before an existing dated section
564+ let content = "# Summary\n \n [👋 Introduction](./README.md) \n \n # ⚙️ 2025H2 goal process \n \n - [Overview](./2025h2 /README.md)\n " ;
528565 let result = process_summary_content ( content, "2026h1" , "2026h1" ) ;
529566
530567 assert ! ( result. contains( "# ⏳ 2026H1 goal process" ) ) ;
531568 assert ! ( result. contains( "- [Overview](./2026h1/README.md)" ) ) ;
532569 assert ! ( result. contains( "- [Proposed goals](./2026h1/goals.md)" ) ) ;
533570 assert ! ( result. contains( "- [Goals not accepted](./2026h1/not_accepted.md)" ) ) ;
571+ // Verify the section is inserted after Introduction, before the existing dated section
572+ let intro_pos = result. find ( "[👋 Introduction]" ) . unwrap ( ) ;
573+ let goal_process_pos = result. find ( "# ⏳ 2026H1 goal process" ) . unwrap ( ) ;
574+ let existing_section_pos = result. find ( "# ⚙️ 2025H2" ) . unwrap ( ) ;
575+ assert ! ( intro_pos < goal_process_pos, "Goal process section should come after Introduction" ) ;
576+ assert ! ( goal_process_pos < existing_section_pos, "New section should come before existing dated section" ) ;
577+ }
578+
579+ #[ test]
580+ fn test_process_summary_content_year_only_timeframe ( ) {
581+ // Test adding a new section with year-only timeframe (no H1/H2)
582+ let content = "# Summary\n \n [👋 Introduction](./README.md)\n \n # ⚙️ 2025H2 goal process\n \n - [Overview](./2025h2/README.md)\n " ;
583+ let result = process_summary_content ( content, "2027" , "2027" ) ;
584+
585+ assert ! ( result. contains( "# ⏳ 2027 goal process" ) ) ;
586+ assert ! ( result. contains( "- [Overview](./2027/README.md)" ) ) ;
587+ assert ! ( result. contains( "- [Proposed goals](./2027/goals.md)" ) ) ;
588+ assert ! ( result. contains( "- [Goals not accepted](./2027/not_accepted.md)" ) ) ;
589+ // Verify the section is inserted after Introduction, before the existing dated section
590+ let intro_pos = result. find ( "[👋 Introduction]" ) . unwrap ( ) ;
591+ let goal_process_pos = result. find ( "# ⏳ 2027 goal process" ) . unwrap ( ) ;
592+ let existing_section_pos = result. find ( "# ⚙️ 2025H2" ) . unwrap ( ) ;
593+ assert ! ( intro_pos < goal_process_pos, "Goal process section should come after Introduction" ) ;
594+ assert ! ( goal_process_pos < existing_section_pos, "New section should come before existing dated section" ) ;
534595 }
535596
536597 #[ test]
@@ -630,4 +691,16 @@ mod tests {
630691 assert ! ( result. contains( "## Next goal period (2026H1)" ) ) ;
631692 assert ! ( result. contains( "running from the start of January to the end of June" ) ) ;
632693 }
694+
695+ #[ test]
696+ fn test_process_readme_content_year_only ( ) {
697+ // Test that year-only timeframes don't include date ranges
698+ let content = "# Project goals\n \n ## Current goal period (2026)\n \n The 2026 goal period." ;
699+ let result = process_readme_content ( content, "2027" , "2027" ) ;
700+
701+ assert ! ( result. contains( "## Next goal period (2027)" ) ) ;
702+ assert ! ( result. contains( "The next goal period will be 2027." ) ) ;
703+ assert ! ( !result. contains( "running from" ) ) ;
704+ assert ! ( result. contains( "[Click here](./2027/goals.md)" ) ) ;
705+ }
633706}
0 commit comments