@@ -809,6 +809,146 @@ def post_insert_images(
809809 return 0
810810
811811
812+ def apply_formatting (
813+ docs_service ,
814+ doc_id : str ,
815+ font_family : Optional [str ] = None ,
816+ font_size : Optional [float ] = None ,
817+ line_spacing : Optional [float ] = None ,
818+ ) -> None :
819+ """Apply font and spacing to the document body.
820+
821+ Font family and size are applied only to NORMAL_TEXT
822+ paragraphs so that headings, titles, and other named
823+ styles keep their distinct appearance. Bold, italic,
824+ and other inline formatting is preserved because the
825+ fields mask only touches font family and size.
826+
827+ Line spacing is applied to ALL paragraphs (including
828+ headings) using NEVER_COLLAPSE so the value is always
829+ respected.
830+
831+ Args:
832+ docs_service: Authenticated Google Docs service.
833+ doc_id: The Google Doc ID.
834+ font_family: Font name (e.g. "Arial").
835+ font_size: Font size in points (e.g. 10).
836+ line_spacing: Line spacing multiplier (e.g. 2.0
837+ for double spacing).
838+ """
839+ if not any ([font_family , font_size , line_spacing ]):
840+ return
841+
842+ doc = (
843+ docs_service .documents ()
844+ .get (documentId = doc_id )
845+ .execute ()
846+ )
847+ body = doc .get ("body" , {})
848+ content = body .get ("content" , [])
849+ if not content :
850+ return
851+
852+ end_index = content [- 1 ].get ("endIndex" , 1 )
853+ if end_index <= 1 :
854+ return
855+
856+ requests : list [dict ] = []
857+
858+ # Build text style dict once (used per-paragraph)
859+ text_style : dict = {}
860+ text_fields : list [str ] = []
861+ if font_family :
862+ text_style ["weightedFontFamily" ] = {
863+ "fontFamily" : font_family ,
864+ }
865+ text_fields .append ("weightedFontFamily" )
866+ if font_size :
867+ text_style ["fontSize" ] = {
868+ "magnitude" : font_size ,
869+ "unit" : "PT" ,
870+ }
871+ text_fields .append ("fontSize" )
872+
873+ # Iterate paragraphs to selectively apply styles
874+ for element in content :
875+ paragraph = element .get ("paragraph" )
876+ if not paragraph :
877+ continue
878+
879+ p_style = paragraph .get ("paragraphStyle" , {})
880+ named_style = p_style .get (
881+ "namedStyleType" , "NORMAL_TEXT"
882+ )
883+ start = element .get ("startIndex" , 0 )
884+ end = element .get ("endIndex" , 0 )
885+ if start >= end :
886+ continue
887+
888+ p_range = {
889+ "startIndex" : start ,
890+ "endIndex" : end ,
891+ }
892+
893+ # Font family/size: only on NORMAL_TEXT paragraphs
894+ # so headings keep their distinct style
895+ if text_fields and named_style == "NORMAL_TEXT" :
896+ requests .append (
897+ {
898+ "updateTextStyle" : {
899+ "range" : p_range ,
900+ "textStyle" : text_style ,
901+ "fields" : "," .join (text_fields ),
902+ }
903+ }
904+ )
905+
906+ # Line spacing: apply to all paragraphs
907+ if line_spacing :
908+ requests .append (
909+ {
910+ "updateParagraphStyle" : {
911+ "range" : p_range ,
912+ "paragraphStyle" : {
913+ "lineSpacing" : (
914+ line_spacing * 100
915+ ),
916+ "spacingMode" : (
917+ "NEVER_COLLAPSE"
918+ ),
919+ },
920+ "fields" : (
921+ "lineSpacing,spacingMode"
922+ ),
923+ }
924+ }
925+ )
926+
927+ if not requests :
928+ return
929+
930+ try :
931+ docs_service .documents ().batchUpdate (
932+ documentId = doc_id ,
933+ body = {"requests" : requests },
934+ ).execute ()
935+ parts = []
936+ if font_family :
937+ parts .append (f"font={ font_family } " )
938+ if font_size :
939+ parts .append (f"size={ font_size } pt" )
940+ if line_spacing :
941+ parts .append (f"spacing={ line_spacing } x" )
942+ console .print (
943+ f"[green]Applied formatting:[/green] "
944+ f"{ ', ' .join (parts )} "
945+ )
946+ except Exception as e :
947+ console .print (
948+ f"[red]Error applying formatting:[/red] { e } "
949+ )
950+
951+
812952def cleanup_temp_images (
813953 service , file_ids : list [str ]
814954) -> None :
@@ -878,6 +1018,7 @@ def main() -> None:
8781018 md2gdoc report.md --name "Q4 Summary"
8791019 md2gdoc report.md --on-existing version
8801020 md2gdoc report.md --max-image-width 5.0
1021+ md2gdoc report.md --font Arial --font-size 10 --line-spacing 2.0
8811022 """ ,
8821023 )
8831024
@@ -908,6 +1049,20 @@ def main() -> None:
9081049 help = "Max image display width in inches "
9091050 "(default: 6.5 = full page width)" ,
9101051 )
1052+ parser .add_argument (
1053+ "--font" , type = str , default = None ,
1054+ help = "Font family for body text "
1055+ "(e.g., 'Arial', 'Times New Roman')" ,
1056+ )
1057+ parser .add_argument (
1058+ "--font-size" , type = float , default = None ,
1059+ help = "Font size in points (e.g., 10, 12)" ,
1060+ )
1061+ parser .add_argument (
1062+ "--line-spacing" , type = float , default = None ,
1063+ help = "Line spacing multiplier "
1064+ "(e.g., 1.0, 1.5, 2.0 for double)" ,
1065+ )
9111066
9121067 args = parser .parse_args ()
9131068
@@ -1025,6 +1180,7 @@ def main() -> None:
10251180
10261181 # --- Post-insert images via Docs API ---
10271182 n_images = 0
1183+ docs_svc = None
10281184 if image_infos and doc_id :
10291185 max_w_pt = args .max_image_width * 72 # in→pt
10301186 docs_svc = get_docs_service ()
@@ -1044,6 +1200,29 @@ def main() -> None:
10441200 "placeholders."
10451201 )
10461202
1203+ # --- Apply formatting ---
1204+ has_formatting = any ([
1205+ args .font , args .font_size , args .line_spacing ,
1206+ ])
1207+ if has_formatting and doc_id :
1208+ fmt_svc = docs_svc or get_docs_service ()
1209+ if fmt_svc :
1210+ console .print (
1211+ "[cyan]Applying formatting...[/cyan]"
1212+ )
1213+ apply_formatting (
1214+ fmt_svc ,
1215+ doc_id ,
1216+ font_family = args .font ,
1217+ font_size = args .font_size ,
1218+ line_spacing = args .line_spacing ,
1219+ )
1220+ else :
1221+ console .print (
1222+ "[yellow]Warning:[/yellow] Could not "
1223+ "get Docs service — formatting skipped."
1224+ )
1225+
10471226 # --- Cleanup ---
10481227 if drive_file_ids :
10491228 console .print (
0 commit comments