@@ -7,6 +7,8 @@ use std::fs;
77use std:: path:: { Path , PathBuf } ;
88use walkdir:: WalkDir ;
99
10+ /// Example:
11+ /// notion-to-blog --input <input_folder> --output <output_folder> --output-name release-070
1012#[ derive( Parser ) ]
1113#[ command( author, version, about, long_about = None ) ]
1214struct Args {
@@ -17,6 +19,10 @@ struct Args {
1719 /// Output folder for transformed markdown
1820 #[ arg( short, long) ]
1921 output : PathBuf ,
22+
23+ /// The name of the markdown file to output
24+ #[ arg( short, long, default_value = "blog" ) ]
25+ output_name : String ,
2026}
2127
2228#[ tokio:: main]
@@ -27,6 +33,13 @@ async fn main() -> Result<()> {
2733 anyhow:: bail!( "Input folder does not exist: {}" , args. input. display( ) ) ;
2834 }
2935
36+ if args. input . is_file ( ) {
37+ anyhow:: bail!(
38+ "Input path must be a directory, not a file: {}" ,
39+ args. input. display( )
40+ ) ;
41+ }
42+
3043 // Create output directory if it doesn't exist
3144 fs:: create_dir_all ( & args. output ) . with_context ( || {
3245 format ! (
@@ -42,7 +55,7 @@ async fn main() -> Result<()> {
4255
4356 if path. extension ( ) . and_then ( |s| s. to_str ( ) ) == Some ( "md" ) {
4457 println ! ( "Processing: {}" , path. display( ) ) ;
45- process_markdown_file ( & args. input , & args. output , path) . await ?;
58+ process_markdown_file ( & args. input , & args. output , & args . output_name , path) . await ?;
4659 }
4760 }
4861
@@ -53,6 +66,7 @@ async fn main() -> Result<()> {
5366async fn process_markdown_file (
5467 input_base : & Path ,
5568 output_base : & Path ,
69+ output_name : & str ,
5670 md_path : & Path ,
5771) -> Result < ( ) > {
5872 // Read the markdown file
@@ -61,7 +75,7 @@ async fn process_markdown_file(
6175
6276 // Get the relative path from input base to maintain directory structure
6377 let relative_path = md_path. strip_prefix ( input_base) ?;
64- let output_md_path = output_base. join ( relative_path ) ;
78+ let output_md_path = output_base. join ( output_name ) . with_extension ( "md" ) ;
6579
6680 // Create parent directories if needed
6781 if let Some ( parent) = output_md_path. parent ( ) {
@@ -74,17 +88,18 @@ async fn process_markdown_file(
7488 . and_then ( |s| s. to_str ( ) )
7589 . unwrap_or ( "assets" ) ;
7690 let input_assets_folder = md_path. parent ( ) . unwrap ( ) . join ( assets_folder_name) ;
77- let output_assets_folder = output_md_path. parent ( ) . unwrap ( ) . join ( "assets" ) ;
91+ let output_assets_folder = output_md_path. parent ( ) . unwrap ( ) . join ( "assets" ) . join ( output_name ) ;
7892
7993 // Process images if assets folder exists
8094 let mut image_mapping = HashMap :: new ( ) ;
8195 if input_assets_folder. exists ( ) {
8296 fs:: create_dir_all ( & output_assets_folder) ?;
8397 image_mapping = process_images ( & input_assets_folder, & output_assets_folder) . await ?;
8498 }
99+ println ! ( "{:#?}" , image_mapping) ;
85100
86101 // Transform the markdown content
87- let transformed_content = transform_markdown ( & content, & image_mapping) ?;
102+ let transformed_content = transform_markdown ( & content, output_name , & image_mapping) ?;
88103
89104 // Write the transformed markdown
90105 fs:: write ( & output_md_path, transformed_content)
@@ -100,6 +115,7 @@ async fn process_images(
100115) -> Result < HashMap < String , String > > {
101116 let mut image_mapping = HashMap :: new ( ) ;
102117 let mut screenshot_counter = 1 ;
118+ let mut screen_recording_counter = 1 ;
103119
104120 for entry in WalkDir :: new ( input_folder) {
105121 let entry = entry?;
@@ -111,14 +127,19 @@ async fn process_images(
111127 }
112128
113129 if let Some ( extension) = path. extension ( ) . and_then ( |s| s. to_str ( ) ) {
114- let file_name = path. file_name ( ) . unwrap ( ) . to_str ( ) . unwrap ( ) ;
130+ let file_name = path. file_name ( ) . and_then ( |s| s. to_str ( ) ) . unwrap ( ) ;
131+ let file_stem = path. file_stem ( ) . unwrap ( ) . to_str ( ) . unwrap ( ) ;
115132
116133 if matches ! (
117134 extension. to_lowercase( ) . as_str( ) ,
118135 "png" | "jpg" | "jpeg" | "gif" | "webp"
119136 ) {
120137 // Convert images to AVIF
121- let new_name = generate_new_image_name ( file_name, & mut screenshot_counter) ;
138+ let new_name = generate_new_image_name (
139+ file_stem,
140+ & mut screenshot_counter,
141+ & mut screen_recording_counter,
142+ ) ;
122143 let new_name_avif = format ! (
123144 "{}.avif" ,
124145 new_name. trim_end_matches( & format!( ".{}" , extension) )
@@ -134,11 +155,16 @@ async fn process_images(
134155
135156 // Store mapping from original to new name (without ./assets/ prefix)
136157 image_mapping. insert ( file_name. to_string ( ) , new_name_avif. clone ( ) ) ;
137- println ! ( " ✓ Converted: {} -> {}" , file_name , new_name_avif ) ;
158+ println ! ( " ✓ Converted: {file_name } -> {new_name_avif}" ) ;
138159 } else {
139160 // Copy all other assets (videos, documents, etc.) as-is
140- let cleaned_name = clean_asset_name ( file_name) ;
141- let output_path = output_folder. join ( & cleaned_name) ;
161+ let cleaned_name = clean_asset_name (
162+ file_stem,
163+ & mut screenshot_counter,
164+ & mut screen_recording_counter,
165+ ) ;
166+ let cleaned_file = format ! ( "{}.{}" , cleaned_name, extension) ;
167+ let output_path = output_folder. join ( & cleaned_file) ;
142168
143169 fs:: copy ( path, & output_path) . with_context ( || {
144170 format ! (
@@ -149,24 +175,20 @@ async fn process_images(
149175 } ) ?;
150176
151177 // Store mapping from original to cleaned name (without ./assets/ prefix)
152- image_mapping. insert ( file_name. to_string ( ) , cleaned_name . clone ( ) ) ;
153- println ! ( " ✓ Copied: {} -> {}" , file_name , cleaned_name ) ;
178+ image_mapping. insert ( file_name. to_string ( ) , cleaned_file . clone ( ) ) ;
179+ println ! ( " ✓ Copied: {file_name } -> {cleaned_file}" ) ;
154180 }
155181 }
156182 }
157183
158184 Ok ( image_mapping)
159185}
160186
161- fn clean_asset_name ( original_name : & str ) -> String {
162- // Clean up asset names by removing spaces and URL encoding
163- original_name
164- . replace ( " " , "-" )
165- . replace ( "%20" , "-" )
166- . to_lowercase ( )
167- }
168-
169- fn generate_new_image_name ( original_name : & str , screenshot_counter : & mut i32 ) -> String {
187+ fn clean_asset_name (
188+ original_name : & str ,
189+ screenshot_counter : & mut i32 ,
190+ screen_recording_counter : & mut i32 ,
191+ ) -> String {
170192 let lower_name = original_name. to_lowercase ( ) ;
171193
172194 // Check if it's a screenshot
@@ -177,11 +199,29 @@ fn generate_new_image_name(original_name: &str, screenshot_counter: &mut i32) ->
177199 return name;
178200 }
179201
180- // For other images, just clean up the name
181- let cleaned = original_name
202+ // Check if it's a screen recording
203+ let screen_recording_regex =
204+ Regex :: new ( r"screen[_\s]*recording[_\s]*\d{4}[-_]\d{2}[-_]\d{2}" ) . unwrap ( ) ;
205+ if screen_recording_regex. is_match ( & lower_name) {
206+ let name = format ! ( "screen-recording-{}" , screen_recording_counter) ;
207+ * screen_recording_counter += 1 ;
208+ return name;
209+ }
210+
211+ // Clean up asset names by removing spaces and URL encoding
212+ original_name
182213 . replace ( " " , "-" )
183214 . replace ( "%20" , "-" )
184- . to_lowercase ( ) ;
215+ . to_lowercase ( )
216+ }
217+
218+ fn generate_new_image_name (
219+ original_name : & str ,
220+ screenshot_counter : & mut i32 ,
221+ screen_recording_counter : & mut i32 ,
222+ ) -> String {
223+ // For other images, just clean up the name
224+ let cleaned = clean_asset_name ( original_name, screenshot_counter, screen_recording_counter) ;
185225
186226 // Remove file extension to add it back later
187227 if let Some ( dot_pos) = cleaned. rfind ( '.' ) {
@@ -191,7 +231,11 @@ fn generate_new_image_name(original_name: &str, screenshot_counter: &mut i32) ->
191231 }
192232}
193233
194- fn transform_markdown ( content : & str , image_mapping : & HashMap < String , String > ) -> Result < String > {
234+ fn transform_markdown (
235+ content : & str ,
236+ asset_sub_directory : & str ,
237+ image_mapping : & HashMap < String , String > ,
238+ ) -> Result < String > {
195239 let parser = pulldown_cmark:: Parser :: new ( content) ;
196240 let mut events = Vec :: new ( ) ;
197241 let mut skip_until_after_heading = false ;
@@ -217,7 +261,8 @@ fn transform_markdown(content: &str, image_mapping: &HashMap<String, String>) ->
217261 title,
218262 id,
219263 } ) => {
220- let processed_url = process_image_url ( & dest_url, image_mapping) ;
264+ let processed_url =
265+ process_image_url ( & dest_url, asset_sub_directory, image_mapping) ;
221266
222267 // Check if this is a video file - if so, convert to image syntax
223268 let url_decoded = dest_url. replace ( "%20" , " " ) ;
@@ -239,7 +284,7 @@ fn transform_markdown(content: &str, image_mapping: &HashMap<String, String>) ->
239284 in_link = true ;
240285 events. push ( Event :: Start ( Tag :: Link {
241286 link_type,
242- dest_url : processed_url . into ( ) ,
287+ dest_url,
243288 title,
244289 id,
245290 } ) ) ;
@@ -260,7 +305,8 @@ fn transform_markdown(content: &str, image_mapping: &HashMap<String, String>) ->
260305 events. push ( Event :: Text ( text) ) ;
261306 } else {
262307 // Process image references in text only if not inside a link
263- let processed_text = process_image_references ( & text, image_mapping) ;
308+ let processed_text =
309+ process_image_references ( & text, asset_sub_directory, image_mapping) ;
264310 events. push ( Event :: Text ( processed_text. into ( ) ) ) ;
265311 }
266312 }
@@ -270,7 +316,8 @@ fn transform_markdown(content: &str, image_mapping: &HashMap<String, String>) ->
270316 title,
271317 id,
272318 } ) => {
273- let processed_url = process_image_url ( & dest_url, image_mapping) ;
319+ let processed_url =
320+ process_image_url ( & dest_url, asset_sub_directory, image_mapping) ;
274321 events. push ( Event :: Start ( Tag :: Image {
275322 link_type,
276323 dest_url : processed_url. into ( ) ,
@@ -414,7 +461,11 @@ fn transform_markdown(content: &str, image_mapping: &HashMap<String, String>) ->
414461 Ok ( output)
415462}
416463
417- fn process_image_references ( text : & str , image_mapping : & HashMap < String , String > ) -> String {
464+ fn process_image_references (
465+ text : & str ,
466+ asset_sub_directory : & str ,
467+ image_mapping : & HashMap < String , String > ,
468+ ) -> String {
418469 let mut result = text. to_string ( ) ;
419470
420471 // Only process if this text doesn't look like it's already part of a processed link
@@ -426,7 +477,7 @@ fn process_image_references(text: &str, image_mapping: &HashMap<String, String>)
426477 for ( original, new) in image_mapping {
427478 // Handle URL-encoded spaces and direct references
428479 let encoded_original = original. replace ( " " , "%20" ) ;
429- let new_with_prefix = format ! ( "./assets/{}" , new) ;
480+ let new_with_prefix = format ! ( "./assets/{asset_sub_directory}/{ new}" ) ;
430481
431482 // Check if this is a video file that should be treated as an image
432483 let is_video = is_media_file ( original) ;
@@ -452,18 +503,25 @@ fn process_image_references(text: &str, image_mapping: &HashMap<String, String>)
452503 result
453504}
454505
455- fn process_image_url ( url : & str , image_mapping : & HashMap < String , String > ) -> String {
506+ fn process_image_url (
507+ url : & str ,
508+ asset_sub_directory : & str ,
509+ image_mapping : & HashMap < String , String > ,
510+ ) -> String {
456511 // Extract filename from URL
457512 let url_decoded = url. replace ( "%20" , " " ) ;
458513
459514 if let Some ( filename) = Path :: new ( & url_decoded) . file_name ( ) . and_then ( |s| s. to_str ( ) ) {
460515 if let Some ( new_name) = image_mapping. get ( filename) {
461- return format ! ( "./assets/{}" , new_name) ;
516+ return format ! ( "./assets/{asset_sub_directory}/{ new_name}" ) ;
462517 }
463518 }
464519
465520 // Fallback: clean up the URL by removing URL encoding
466- format ! ( "./assets/{}" , url. replace( "%20" , "-" ) . to_lowercase( ) )
521+ format ! (
522+ "./assets/{asset_sub_directory}/{}" ,
523+ url. replace( "%20" , "-" ) . to_lowercase( )
524+ )
467525}
468526
469527fn is_media_file ( filename : & str ) -> bool {
0 commit comments