TiH0 commited on
Commit
314740a
·
verified ·
1 Parent(s): ca49f0f

Update meta.py

Browse files
Files changed (1) hide show
  1. meta.py +184 -38
meta.py CHANGED
@@ -68,10 +68,6 @@ def set_two_column_layout(doc, add_separator_line=True, balance_columns=True):
68
  # Set space between columns (reduced for better space utilization)
69
  cols.set(qn('w:space'), '432') # 0.3 inch in twentieths of a point (was 708)
70
 
71
- # Add separator line between columns if requested
72
- if add_separator_line:
73
- cols.set(qn('w:sep'), '1') # This adds the vertical separator line
74
-
75
  # Enable column balancing if requested
76
  if balance_columns:
77
  cols.set(qn('w:equalWidth'), '1') # Equal width columns
@@ -137,27 +133,138 @@ def add_page_break(doc):
137
  doc.add_page_break()
138
 
139
 
140
- def create_course_title(doc, course_number, course_title, theme_color=None):
141
- """Create a course title section in the document and return the paragraph object"""
 
 
 
142
  if theme_color is None:
143
- theme_color = THEME_COLOR
144
 
145
  # Add minimal space before course title
146
  course_para = doc.add_paragraph()
147
  course_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
148
 
149
- # Reduce spacing before and after
150
- course_para.paragraph_format.space_before = Pt(6)
151
- course_para.paragraph_format.space_after = Pt(3)
152
- course_para.paragraph_format.keep_with_next = True # Keep course title with first question
153
- course_para.paragraph_format.keep_together = True # Prevent splitting within paragraph
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
 
155
- course_run = course_para.add_run(f"{course_number}. {course_title}")
156
- course_run.font.name = 'Montserrat'
157
- course_run.font.size = Pt(13)
158
- course_run.font.bold = True
159
- course_run.font.color.rgb = None
160
- course_run.font.color.rgb = theme_color
161
 
162
  return course_para
163
 
@@ -186,14 +293,14 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
186
  # Question number in Axiforma Black
187
  num_run = question_para.add_run(f"{question_num}. ")
188
  num_run.font.name = 'Inter ExtraBold'
189
- num_run.font.size = Pt(10)
190
  num_run.font.bold = True
191
  num_run.font.color.rgb = theme_color
192
 
193
  # Question text in SF UI Display Med
194
  text_run = question_para.add_run(question_text)
195
  text_run.font.name = 'Inter ExtraBold'
196
- text_run.font.size = Pt(10)
197
 
198
  # Display ALL choices for this question with minimal spacing
199
  choice_paragraphs = []
@@ -217,7 +324,7 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
217
 
218
  choice_run = choice_para.add_run(f"{choice_letter}- {choice_text}")
219
  choice_run.font.name = 'Inter Display Medium'
220
- choice_run.font.size = Pt(10)
221
 
222
  choice_paragraphs.append(choice_para)
223
 
@@ -235,22 +342,22 @@ def format_question_block(doc, question_num, question_text, choices, correct_ans
235
  # Source
236
  source_run = source_para.add_run(f"Source:")
237
  source_run.font.name = 'Inter SemiBold'
238
- source_run.font.size = Pt(8)
239
  source_run.font.bold = True
240
  source_run.font.underline = True
241
 
242
  source_value_run = source_para.add_run(f" {source}")
243
  source_value_run.font.name = 'Inter Display Medium'
244
- source_value_run.font.size = Pt(8)
245
  source_value_run.font.color.rgb = None
246
  source_value_run.font.color.rgb = theme_color
247
 
248
  empty_para = doc.add_paragraph(' ', style='TinySpace')
249
  empty_para.paragraph_format.space_before = Pt(0)
250
  empty_para.paragraph_format.space_after = Pt(0)
251
- empty_para.paragraph_format.line_spacing = Pt(5)
252
  empty_run = empty_para.add_run(' ')
253
- empty_run.font.size = Pt(5)
254
 
255
  # Add comment if exists
256
  if comment and str(comment).strip() and str(comment).lower() != 'nan':
@@ -276,7 +383,7 @@ def add_page_numbers(doc, theme_hex=None):
276
  # ===== HEADER (keep existing text like module name) =====
277
  header = section.header
278
  header.is_linked_to_previous = False
279
- section.header_distance = Cm(0.6)
280
 
281
  # If header is empty, add a blank paragraph
282
  if not header.paragraphs:
@@ -339,7 +446,7 @@ def add_page_numbers(doc, theme_hex=None):
339
  run._r.append(fldChar2)
340
 
341
  run.font.name = 'Montserrat'
342
- run.font.size = Pt(12)
343
  run.font.bold = True
344
  run.font.color.rgb = RGBColor(0, 0, 0)
345
 
@@ -362,7 +469,7 @@ def add_page_numbers(doc, theme_hex=None):
362
  <w:r>
363
  <w:rPr>
364
  <w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>
365
- <w:sz w:val="28"/>
366
  <w:color w:val="{theme_hex}"/>
367
  <w:u w:val="single"/>
368
  </w:rPr>
@@ -372,7 +479,7 @@ def add_page_numbers(doc, theme_hex=None):
372
  <w:rPr>
373
  <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
374
  <w:b/>
375
- <w:sz w:val="18"/>
376
  <w:color w:val="{theme_hex}"/>
377
  <w:u w:val="single"/>
378
  </w:rPr>
@@ -980,7 +1087,7 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
980
  <w:rPr>
981
  <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
982
  <w:b/>
983
- <w:sz w:val="20"/>
984
  <w:color w:val="{theme_hex}"/>
985
  </w:rPr>
986
  <w:t>{module_name_str}</w:t>
@@ -1011,7 +1118,7 @@ def create_flexible_header(section, module_name, sheet_name, display_name=None,
1011
  <w:rPr>
1012
  <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
1013
  <w:b/>
1014
- <w:sz w:val="20"/>
1015
  <w:color w:val="{theme_hex}"/>
1016
  </w:rPr>
1017
  <w:t>{sheet_name_str}</w:t>
@@ -1084,6 +1191,45 @@ def extract_display_name_from_excel(excel_file_path):
1084
  return os.path.splitext(os.path.basename(excel_file_path))[0]
1085
 
1086
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1087
  def process_excel_to_word(excel_file_path, output_word_path, display_name=None, use_two_columns=True, add_separator_line=True, balance_method="dynamic", theme_hex=None):
1088
  """Main function to process Excel and create a Word document with TOC on the first page"""
1089
 
@@ -1171,9 +1317,9 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
1171
  # ========================================
1172
  # ADD THREE EMPTY PAGES AT THE BEGINNING
1173
  # ========================================
1174
- for i in range(4):
1175
  doc.add_paragraph() # Add empty paragraph
1176
- if i < 3: # Add page breaks for first 2 pages (3rd page leads to TOC)
1177
  doc.add_page_break()
1178
 
1179
  # TOC helpers
@@ -1374,8 +1520,6 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
1374
  sectPr.append(cols)
1375
  cols.set(qn('w:num'), '2')
1376
  cols.set(qn('w:space'), '432')
1377
- if add_separator_line:
1378
- cols.set(qn('w:sep'), '1')
1379
  cols.set(qn('w:equalWidth'), '1')
1380
 
1381
  if use_two_columns:
@@ -1386,13 +1530,15 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
1386
  sectPr.append(cols)
1387
  cols.set(qn('w:num'), '2')
1388
  cols.set(qn('w:space'), '432')
1389
- if add_separator_line:
1390
- cols.set(qn('w:sep'), '1')
1391
  cols.set(qn('w:equalWidth'), '1')
1392
 
1393
  # Use the new flexible header function
1394
  create_flexible_header(section, module_name, first_sheet_name, display_name, theme_hex=theme_hex)
1395
 
 
 
 
 
1396
  # ========== CUSTOMIZE MODULE TITLE APPEARANCE HERE ==========
1397
  MODULE_HEIGHT = 31 # Frame height in points
1398
  MODULE_ROUNDNESS = 50 # Corner roundness % (0=square, 50=pill)
@@ -1461,7 +1607,7 @@ def process_excel_to_word(excel_file_path, output_word_path, display_name=None,
1461
  course_question_count = 1
1462
 
1463
  course_title = cours_titles.get(cours_num, f"COURSE {cours_num}")
1464
- course_para = create_course_title(doc, natural_num, course_title, theme_color)
1465
 
1466
  bm_course_name = sanitize_bookmark_name(f"COURSE_{module_name}_{cours_num}")
1467
  add_bookmark_to_paragraph(course_para, bm_course_name, bookmark_id)
 
68
  # Set space between columns (reduced for better space utilization)
69
  cols.set(qn('w:space'), '432') # 0.3 inch in twentieths of a point (was 708)
70
 
 
 
 
 
71
  # Enable column balancing if requested
72
  if balance_columns:
73
  cols.set(qn('w:equalWidth'), '1') # Equal width columns
 
133
  doc.add_page_break()
134
 
135
 
136
+ def create_course_title(doc, course_number, course_title, theme_color=None, theme_hex=None):
137
+ """Create a course title section with rounded frame (unfilled) matching module style
138
+ Automatically wraps to two lines and doubles height if text is too long"""
139
+ if theme_hex is None:
140
+ theme_hex = THEME_COLOR_HEX
141
  if theme_color is None:
142
+ theme_color = RGBColor.from_string(theme_hex)
143
 
144
  # Add minimal space before course title
145
  course_para = doc.add_paragraph()
146
  course_para.alignment = WD_ALIGN_PARAGRAPH.CENTER
147
 
148
+ # Remove all spacing before and after
149
+ course_para.paragraph_format.space_before = Pt(0)
150
+ course_para.paragraph_format.space_after = Pt(0)
151
+ course_para.paragraph_format.keep_with_next = True
152
+ course_para.paragraph_format.keep_together = True
153
+
154
+ # Format the text
155
+ full_text = f"{course_number}. {course_title}"
156
+ text_length = len(full_text)
157
+
158
+ # ========== CUSTOMIZE COURSE TITLE APPEARANCE HERE ==========
159
+ MAX_CHARS_SINGLE_LINE = 40 # Threshold for wrapping to two lines
160
+ SINGLE_LINE_HEIGHT = 31 # Frame height for single line
161
+ DOUBLE_LINE_HEIGHT = 55 # Frame height for two lines (almost double)
162
+ COURSE_ROUNDNESS = 50 # Corner roundness %
163
+ COURSE_FONT_SIZE = 26 # Font size in half-points (26=13pt)
164
+ COURSE_TEXT_COLOR = theme_hex
165
+ COURSE_STROKE_COLOR = theme_hex
166
+ COURSE_STROKE_WEIGHT = "2pt"
167
+ MAX_WIDTH_PT = 280 # Maximum width in points for the frame
168
+ # ============================================================
169
+
170
+ # Determine if we need two lines
171
+ needs_two_lines = text_length > MAX_CHARS_SINGLE_LINE
172
+
173
+ if needs_two_lines:
174
+ # Split text intelligently at a good breaking point
175
+ words = course_title.split()
176
+ mid_point = len(words) // 2
177
+
178
+ # Try to split at middle, but prefer breaking after shorter first line
179
+ first_line = f"{course_number}. " + " ".join(words[:mid_point])
180
+ second_line = " ".join(words[mid_point:])
181
+
182
+ # Adjust if first line is too long
183
+ while len(first_line) > MAX_CHARS_SINGLE_LINE and mid_point > 1:
184
+ mid_point -= 1
185
+ first_line = f"{course_number}. " + " ".join(words[:mid_point])
186
+ second_line = " ".join(words[mid_point:])
187
+
188
+ text_escaped_line1 = html.escape(first_line)
189
+ text_escaped_line2 = html.escape(second_line)
190
+
191
+ # Use max of both lines for width calculation
192
+ max_line_length = max(len(first_line), len(second_line))
193
+ estimated_width = min((max_line_length * 8), MAX_WIDTH_PT)
194
+ frame_height = DOUBLE_LINE_HEIGHT
195
+
196
+ print(f'Text: {course_title}, charachters: {text_length}')
197
+ print(f'split: {first_line}, {len(first_line)}, {second_line}, {len(second_line)}')
198
+
199
+ # Two-line XML
200
+ text_content = f'''
201
+ <w:r>
202
+ <w:rPr>
203
+ <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
204
+ <w:b/>
205
+ <w:sz w:val="{COURSE_FONT_SIZE}"/>
206
+ <w:color w:val="{COURSE_TEXT_COLOR}"/>
207
+ </w:rPr>
208
+ <w:t>{text_escaped_line1}</w:t>
209
+ </w:r>
210
+ <w:r>
211
+ <w:br/>
212
+ </w:r>
213
+ <w:r>
214
+ <w:rPr>
215
+ <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
216
+ <w:b/>
217
+ <w:sz w:val="{COURSE_FONT_SIZE}"/>
218
+ <w:color w:val="{COURSE_TEXT_COLOR}"/>
219
+ </w:rPr>
220
+ <w:t>{text_escaped_line2}</w:t>
221
+ </w:r>'''
222
+ else:
223
+ # Single line
224
+ estimated_width = min((text_length * 9) + 20, MAX_WIDTH_PT)
225
+ frame_height = SINGLE_LINE_HEIGHT
226
+ text_escaped = html.escape(full_text)
227
+
228
+ print(f'Text: {text_escaped}, charachters: {text_length}')
229
+
230
+ text_content = f'''
231
+ <w:r>
232
+ <w:rPr>
233
+ <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
234
+ <w:b/>
235
+ <w:sz w:val="{COURSE_FONT_SIZE}"/>
236
+ <w:color w:val="{COURSE_TEXT_COLOR}"/>
237
+ </w:rPr>
238
+ <w:t>{text_escaped}</w:t>
239
+ </w:r>'''
240
+
241
+ # Create rounded rectangle shape (UNFILLED with stroke)
242
+ shape_xml = f'''
243
+ <w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
244
+ xmlns:v="urn:schemas-microsoft-com:vml">
245
+ <w:pict>
246
+ <v:roundrect style="width:{estimated_width}pt;height:{frame_height}pt"
247
+ arcsize="{COURSE_ROUNDNESS}%"
248
+ filled="f"
249
+ strokecolor="#{COURSE_STROKE_COLOR}"
250
+ strokeweight="{COURSE_STROKE_WEIGHT}">
251
+ <v:textbox inset="0pt,3pt,0pt,3pt" style="v-text-anchor:middle">
252
+ <w:txbxContent>
253
+ <w:p>
254
+ <w:pPr>
255
+ <w:jc w:val="center"/>
256
+ <w:spacing w:before="0" w:after="0"/>
257
+ </w:pPr>{text_content}
258
+ </w:p>
259
+ </w:txbxContent>
260
+ </v:textbox>
261
+ </v:roundrect>
262
+ </w:pict>
263
+ </w:r>
264
+ '''
265
 
266
+ shape_element = parse_xml(shape_xml)
267
+ course_para._p.append(shape_element)
 
 
 
 
268
 
269
  return course_para
270
 
 
293
  # Question number in Axiforma Black
294
  num_run = question_para.add_run(f"{question_num}. ")
295
  num_run.font.name = 'Inter ExtraBold'
296
+ num_run.font.size = Pt(11)
297
  num_run.font.bold = True
298
  num_run.font.color.rgb = theme_color
299
 
300
  # Question text in SF UI Display Med
301
  text_run = question_para.add_run(question_text)
302
  text_run.font.name = 'Inter ExtraBold'
303
+ text_run.font.size = Pt(11)
304
 
305
  # Display ALL choices for this question with minimal spacing
306
  choice_paragraphs = []
 
324
 
325
  choice_run = choice_para.add_run(f"{choice_letter}- {choice_text}")
326
  choice_run.font.name = 'Inter Display Medium'
327
+ choice_run.font.size = Pt(11)
328
 
329
  choice_paragraphs.append(choice_para)
330
 
 
342
  # Source
343
  source_run = source_para.add_run(f"Source:")
344
  source_run.font.name = 'Inter SemiBold'
345
+ source_run.font.size = Pt(9)
346
  source_run.font.bold = True
347
  source_run.font.underline = True
348
 
349
  source_value_run = source_para.add_run(f" {source}")
350
  source_value_run.font.name = 'Inter Display Medium'
351
+ source_value_run.font.size = Pt(9)
352
  source_value_run.font.color.rgb = None
353
  source_value_run.font.color.rgb = theme_color
354
 
355
  empty_para = doc.add_paragraph(' ', style='TinySpace')
356
  empty_para.paragraph_format.space_before = Pt(0)
357
  empty_para.paragraph_format.space_after = Pt(0)
358
+ empty_para.paragraph_format.line_spacing = Pt(7)
359
  empty_run = empty_para.add_run(' ')
360
+ empty_run.font.size = Pt(7)
361
 
362
  # Add comment if exists
363
  if comment and str(comment).strip() and str(comment).lower() != 'nan':
 
383
  # ===== HEADER (keep existing text like module name) =====
384
  header = section.header
385
  header.is_linked_to_previous = False
386
+ section.header_distance = Cm(0.3)
387
 
388
  # If header is empty, add a blank paragraph
389
  if not header.paragraphs:
 
446
  run._r.append(fldChar2)
447
 
448
  run.font.name = 'Montserrat'
449
+ run.font.size = Pt(14)
450
  run.font.bold = True
451
  run.font.color.rgb = RGBColor(0, 0, 0)
452
 
 
469
  <w:r>
470
  <w:rPr>
471
  <w:rFonts w:ascii="Aptos" w:hAnsi="Aptos"/>
472
+ <w:sz w:val="44"/>
473
  <w:color w:val="{theme_hex}"/>
474
  <w:u w:val="single"/>
475
  </w:rPr>
 
479
  <w:rPr>
480
  <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
481
  <w:b/>
482
+ <w:sz w:val="28"/>
483
  <w:color w:val="{theme_hex}"/>
484
  <w:u w:val="single"/>
485
  </w:rPr>
 
1087
  <w:rPr>
1088
  <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
1089
  <w:b/>
1090
+ <w:sz w:val="26"/>
1091
  <w:color w:val="{theme_hex}"/>
1092
  </w:rPr>
1093
  <w:t>{module_name_str}</w:t>
 
1118
  <w:rPr>
1119
  <w:rFonts w:ascii="Montserrat" w:hAnsi="Montserrat"/>
1120
  <w:b/>
1121
+ <w:sz w:val="26"/>
1122
  <w:color w:val="{theme_hex}"/>
1123
  </w:rPr>
1124
  <w:t>{sheet_name_str}</w:t>
 
1191
  return os.path.splitext(os.path.basename(excel_file_path))[0]
1192
 
1193
 
1194
+ def add_colored_column_separator(section, theme_hex=None):
1195
+ """Add a custom colored vertical line between columns"""
1196
+ if theme_hex is None:
1197
+ theme_hex = THEME_COLOR_HEX
1198
+
1199
+ header = section.header
1200
+
1201
+ # Find or create the first paragraph in header
1202
+ if not header.paragraphs:
1203
+ header.add_paragraph()
1204
+
1205
+ header_para = header.paragraphs[0]
1206
+
1207
+ # Create a vertical line using VML shape
1208
+ # The line starts AFTER the header and goes to the bottom
1209
+ # Adjust the "from" value to start below header (e.g., 0.8in from top)
1210
+ line_xml = f'''
1211
+ <w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
1212
+ xmlns:v="urn:schemas-microsoft-com:vml"
1213
+ xmlns:o="urn:schemas-microsoft-com:office:office">
1214
+ <w:pict>
1215
+ <v:line id="columnSeparator"
1216
+ style="position:absolute;left:0;text-align:left;z-index:-1;
1217
+ mso-position-horizontal:center;
1218
+ mso-position-horizontal-relative:margin;
1219
+ mso-position-vertical-relative:page"
1220
+ from="0,0.49in" to="0,11.05in"
1221
+ strokecolor="#{theme_hex}"
1222
+ strokeweight="0.75pt">
1223
+ <o:lock v:ext="edit" aspectratio="f"/>
1224
+ </v:line>
1225
+ </w:pict>
1226
+ </w:r>
1227
+ '''
1228
+
1229
+ line_element = parse_xml(line_xml)
1230
+ header_para._p.append(line_element)
1231
+
1232
+
1233
  def process_excel_to_word(excel_file_path, output_word_path, display_name=None, use_two_columns=True, add_separator_line=True, balance_method="dynamic", theme_hex=None):
1234
  """Main function to process Excel and create a Word document with TOC on the first page"""
1235
 
 
1317
  # ========================================
1318
  # ADD THREE EMPTY PAGES AT THE BEGINNING
1319
  # ========================================
1320
+ for i in range(3):
1321
  doc.add_paragraph() # Add empty paragraph
1322
+ if i < 2: # Add page breaks for first 2 pages (3rd page leads to TOC)
1323
  doc.add_page_break()
1324
 
1325
  # TOC helpers
 
1520
  sectPr.append(cols)
1521
  cols.set(qn('w:num'), '2')
1522
  cols.set(qn('w:space'), '432')
 
 
1523
  cols.set(qn('w:equalWidth'), '1')
1524
 
1525
  if use_two_columns:
 
1530
  sectPr.append(cols)
1531
  cols.set(qn('w:num'), '2')
1532
  cols.set(qn('w:space'), '432')
 
 
1533
  cols.set(qn('w:equalWidth'), '1')
1534
 
1535
  # Use the new flexible header function
1536
  create_flexible_header(section, module_name, first_sheet_name, display_name, theme_hex=theme_hex)
1537
 
1538
+ # ADD THE COLORED SEPARATOR
1539
+ if add_separator_line:
1540
+ add_colored_column_separator(section, theme_hex)
1541
+
1542
  # ========== CUSTOMIZE MODULE TITLE APPEARANCE HERE ==========
1543
  MODULE_HEIGHT = 31 # Frame height in points
1544
  MODULE_ROUNDNESS = 50 # Corner roundness % (0=square, 50=pill)
 
1607
  course_question_count = 1
1608
 
1609
  course_title = cours_titles.get(cours_num, f"COURSE {cours_num}")
1610
+ course_para = create_course_title(doc, natural_num, course_title, theme_color, theme_hex=theme_hex)
1611
 
1612
  bm_course_name = sanitize_bookmark_name(f"COURSE_{module_name}_{cours_num}")
1613
  add_bookmark_to_paragraph(course_para, bm_course_name, bookmark_id)