chgksuite 0.26.0b10__py3-none-any.whl → 0.26.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import os
2
+ import re
2
3
  import shutil
3
4
  import sys
4
5
  import tempfile
@@ -12,8 +13,15 @@ from docx.oxml.ns import qn
12
13
  from docx.shared import Inches
13
14
  from docx.shared import Pt as DocxPt
14
15
 
15
- from chgksuite.common import log_wrap, replace_escaped
16
- from chgksuite.composer.composer_common import BaseExporter, backtick_replace, parseimg
16
+ import chgksuite.typotools as typotools
17
+ from chgksuite.common import DummyLogger, log_wrap, replace_escaped
18
+ from chgksuite.composer.composer_common import (
19
+ BaseExporter,
20
+ _parse_4s_elem,
21
+ backtick_replace,
22
+ parseimg,
23
+ remove_accents_standalone,
24
+ )
17
25
 
18
26
  WHITEN = {
19
27
  "handout": False,
@@ -59,6 +67,430 @@ def replace_font_in_docx(template_path, new_font):
59
67
  return temp_template
60
68
 
61
69
 
70
+ def replace_no_break_standalone(s, replace_spaces=True, replace_hyphens=True):
71
+ """Standalone version of _replace_no_break"""
72
+ return typotools.replace_no_break(s, spaces=replace_spaces, hyphens=replace_hyphens)
73
+
74
+
75
+ def get_label_standalone(
76
+ question, field, labels, language="ru", only_question_number=False, number=None
77
+ ):
78
+ """Standalone version of get_label"""
79
+ if field == "question" and only_question_number:
80
+ return str(question.get("number") or number)
81
+ if field in ("question", "tour"):
82
+ lbl = (question.get("overrides") or {}).get(field) or labels["question_labels"][
83
+ field
84
+ ]
85
+ num = question.get("number") or number
86
+ if language in ("uz", "uz_cyr"):
87
+ return f"{num} – {lbl}"
88
+ elif language == "kz":
89
+ return f"{num}-{lbl}"
90
+ else:
91
+ return f"{lbl} {num}"
92
+ if field in (question.get("overrides") or {}):
93
+ return question["overrides"][field]
94
+ if field == "source" and isinstance(question.get("source" or ""), list):
95
+ return labels["question_labels"]["sources"]
96
+ return labels["question_labels"][field]
97
+
98
+
99
+ def remove_square_brackets_standalone(s, labels):
100
+ """Standalone version of remove_square_brackets"""
101
+ hs = labels["question_labels"]["handout_short"]
102
+ s = s.replace("\\[", "LEFTSQUAREBRACKET")
103
+ s = s.replace("\\]", "RIGHTSQUAREBRACKET")
104
+ s = re.sub(f"\\[{hs}(.+?)\\]", "{" + hs + "\\1}", s, flags=re.DOTALL)
105
+ i = 0
106
+ while "[" in s and "]" in s and i < 10:
107
+ s = re.sub(" *\\[.+?\\]", "", s, flags=re.DOTALL)
108
+ s = s.strip()
109
+ i += 1
110
+ if i == 10:
111
+ sys.stderr.write(
112
+ f"Error replacing square brackets on question: {s}, retries exceeded\n"
113
+ )
114
+ s = re.sub("\\{" + hs + "(.+?)\\}", "[" + hs + "\\1]", s, flags=re.DOTALL)
115
+ s = s.replace("LEFTSQUAREBRACKET", "[")
116
+ s = s.replace("RIGHTSQUAREBRACKET", "]")
117
+ return s
118
+
119
+
120
+ def add_hyperlink_to_docx(doc, paragraph, text, url):
121
+ """Standalone version of add_hyperlink"""
122
+ run = paragraph.add_run(text)
123
+ run.style = doc.styles["Hyperlink"]
124
+ part = paragraph.part
125
+ r_id = part.relate_to(
126
+ url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True
127
+ )
128
+ hyperlink = docx.oxml.shared.OxmlElement("w:hyperlink")
129
+ hyperlink.set(docx.oxml.shared.qn("r:id"), r_id)
130
+ hyperlink.append(run._r)
131
+ paragraph._p.append(hyperlink)
132
+ return hyperlink
133
+
134
+
135
+ def format_docx_element(
136
+ doc,
137
+ el,
138
+ para,
139
+ whiten,
140
+ spoilers="none",
141
+ logger=None,
142
+ labels=None,
143
+ language="ru",
144
+ remove_accents=False,
145
+ remove_brackets=False,
146
+ replace_no_break_spaces=False,
147
+ **kwargs,
148
+ ):
149
+ """
150
+ Standalone version of docx_format that can be used outside DocxExporter.
151
+
152
+ Args:
153
+ doc: docx Document object
154
+ el: Element to format
155
+ para: Paragraph object to add content to
156
+ whiten: Whether to apply whitening
157
+ spoilers: Spoiler handling mode ("none", "whiten", "dots", "pagebreak")
158
+ logger: Logger instance
159
+ labels: Labels dictionary
160
+ language: Language code
161
+ remove_accents: Whether to remove accents
162
+ remove_brackets: Whether to remove square brackets
163
+ replace_no_break_spaces: Whether to replace non-breaking spaces
164
+ **kwargs: Additional arguments (tmp_dir, targetdir, etc.)
165
+ """
166
+ if logger is None:
167
+ logger = DummyLogger()
168
+
169
+ if isinstance(el, list):
170
+ if len(el) > 1 and isinstance(el[1], list):
171
+ format_docx_element(
172
+ doc,
173
+ el[0],
174
+ para,
175
+ whiten,
176
+ spoilers,
177
+ logger,
178
+ labels,
179
+ language,
180
+ remove_accents,
181
+ remove_brackets,
182
+ replace_no_break_spaces,
183
+ **kwargs,
184
+ )
185
+ licount = 0
186
+ for li in el[1]:
187
+ licount += 1
188
+ para.add_run("\n{}. ".format(licount))
189
+ format_docx_element(
190
+ doc,
191
+ li,
192
+ para,
193
+ whiten,
194
+ spoilers,
195
+ logger,
196
+ labels,
197
+ language,
198
+ remove_accents,
199
+ remove_brackets,
200
+ replace_no_break_spaces,
201
+ **kwargs,
202
+ )
203
+ else:
204
+ licount = 0
205
+ for li in el:
206
+ licount += 1
207
+ para.add_run("\n{}. ".format(licount))
208
+ format_docx_element(
209
+ doc,
210
+ li,
211
+ para,
212
+ whiten,
213
+ spoilers,
214
+ logger,
215
+ labels,
216
+ language,
217
+ remove_accents,
218
+ remove_brackets,
219
+ replace_no_break_spaces,
220
+ **kwargs,
221
+ )
222
+
223
+ if isinstance(el, str):
224
+ logger.debug("parsing element {}:".format(log_wrap(el)))
225
+
226
+ if remove_accents and labels:
227
+ el = remove_accents_standalone(el, labels)
228
+ if remove_brackets and labels:
229
+ el = remove_square_brackets_standalone(el, labels)
230
+ else:
231
+ el = replace_escaped(el)
232
+
233
+ el = backtick_replace(el)
234
+
235
+ for run in _parse_4s_elem(el, logger=logger):
236
+ if run[0] == "pagebreak":
237
+ if spoilers == "dots":
238
+ for _ in range(30):
239
+ para = doc.add_paragraph()
240
+ para.add_run(".")
241
+ para = doc.add_paragraph()
242
+ else:
243
+ para = doc.add_page_break()
244
+ elif run[0] == "linebreak":
245
+ para.add_run("\n")
246
+ elif run[0] == "screen":
247
+ if remove_accents or remove_brackets:
248
+ text = run[1]["for_screen"]
249
+ else:
250
+ text = run[1]["for_print"]
251
+ if replace_no_break_spaces:
252
+ text = replace_no_break_standalone(text)
253
+ r = para.add_run(text)
254
+ elif run[0] == "hyperlink" and not (whiten and spoilers == "whiten"):
255
+ r = add_hyperlink_to_docx(doc, para, run[1], run[1])
256
+ elif run[0] == "img":
257
+ if run[1].endswith(".shtml"):
258
+ r = para.add_run("(ТУТ БЫЛА ССЫЛКА НА ПРОТУХШУЮ КАРТИНКУ)\n")
259
+ continue
260
+ parsed_image = parseimg(
261
+ run[1],
262
+ dimensions="inches",
263
+ tmp_dir=kwargs.get("tmp_dir"),
264
+ targetdir=kwargs.get("targetdir"),
265
+ )
266
+ imgfile = parsed_image["imgfile"]
267
+ width = parsed_image["width"]
268
+ height = parsed_image["height"]
269
+ inline = parsed_image["inline"]
270
+ if inline:
271
+ r = para.add_run("")
272
+ else:
273
+ r = para.add_run("\n")
274
+
275
+ try:
276
+ if inline:
277
+ r.add_picture(imgfile, height=Inches(1.0 / 6))
278
+ else:
279
+ r.add_picture(
280
+ imgfile, width=Inches(width), height=Inches(height)
281
+ )
282
+ except UnrecognizedImageError:
283
+ sys.stderr.write(
284
+ f"python-docx can't recognize header for {imgfile}\n"
285
+ )
286
+ if not inline:
287
+ r = para.add_run("\n")
288
+ continue
289
+ else:
290
+ text = run[1]
291
+ if replace_no_break_spaces:
292
+ text = replace_no_break_standalone(text)
293
+ r = para.add_run(text)
294
+ if "italic" in run[0]:
295
+ r.italic = True
296
+ if "bold" in run[0]:
297
+ r.bold = True
298
+ if "underline" in run[0]:
299
+ r.underline = True
300
+ if run[0] == "strike":
301
+ r.font.strike = True
302
+ if run[0] == "sc":
303
+ r.small_caps = True
304
+ if whiten and spoilers == "whiten":
305
+ r.style = "Whitened"
306
+
307
+
308
+ def add_question_to_docx(
309
+ doc,
310
+ question_data,
311
+ labels,
312
+ qcount=None,
313
+ skip_qcount=False,
314
+ screen_mode=False,
315
+ external_para=None,
316
+ noparagraph=False,
317
+ noanswers=False,
318
+ spoilers="none",
319
+ language="ru",
320
+ only_question_number=False,
321
+ add_question_label=True,
322
+ logger=None,
323
+ **kwargs,
324
+ ):
325
+ """
326
+ Standalone function to add a question to a docx document.
327
+
328
+ Args:
329
+ doc: docx Document object
330
+ question_data: Dictionary containing question data
331
+ labels: Labels dictionary
332
+ qcount: Current question count (will be incremented if not skip_qcount)
333
+ skip_qcount: Whether to skip incrementing question count
334
+ screen_mode: Whether to use screen mode formatting
335
+ external_para: External paragraph to use instead of creating new ones
336
+ noparagraph: Whether to skip paragraph breaks
337
+ noanswers: Whether to skip adding answers
338
+ spoilers: Spoiler handling mode ("none", "whiten", "dots", "pagebreak")
339
+ language: Language code
340
+ only_question_number: Whether to show only question numbers
341
+ logger: Logger instance
342
+ **kwargs: Additional arguments passed to format_docx_element
343
+
344
+ Returns:
345
+ Updated question count
346
+ """
347
+ if not kwargs.get("tmp_dir"):
348
+ kwargs["tmp_dir"] = tempfile.mkdtemp()
349
+ if not kwargs.get("targetdir"):
350
+ kwargs["targetdir"] = os.getcwd()
351
+ if logger is None:
352
+ logger = DummyLogger()
353
+
354
+ q = question_data
355
+ if external_para is None:
356
+ p = doc.add_paragraph()
357
+ else:
358
+ p = external_para
359
+ if add_question_label:
360
+ p.paragraph_format.space_before = DocxPt(18)
361
+ p.paragraph_format.keep_together = True
362
+
363
+ # Handle question numbering
364
+ if qcount is None:
365
+ qcount = 1
366
+ if "number" not in q and not skip_qcount:
367
+ qcount += 1
368
+ if "setcounter" in q:
369
+ qcount = int(q["setcounter"])
370
+
371
+ # Add question label
372
+ if add_question_label:
373
+ question_label = get_label_standalone(
374
+ q,
375
+ "question",
376
+ labels,
377
+ language,
378
+ only_question_number,
379
+ number=qcount if "number" not in q else q["number"],
380
+ )
381
+ p.add_run(f"{question_label}. ").bold = True
382
+
383
+ # Add handout if present
384
+ if "handout" in q:
385
+ handout_label = get_label_standalone(q, "handout", labels, language)
386
+ p.add_run(f"\n[{handout_label}: ")
387
+ format_docx_element(
388
+ doc,
389
+ q["handout"],
390
+ p,
391
+ WHITEN["handout"],
392
+ spoilers,
393
+ logger,
394
+ labels,
395
+ language,
396
+ remove_accents=screen_mode,
397
+ remove_brackets=screen_mode,
398
+ **kwargs,
399
+ )
400
+ p.add_run("\n]")
401
+
402
+ if not noparagraph:
403
+ p.add_run("\n")
404
+
405
+ # Add question text
406
+ format_docx_element(
407
+ doc,
408
+ q["question"],
409
+ p,
410
+ False,
411
+ spoilers,
412
+ logger,
413
+ labels,
414
+ language,
415
+ remove_accents=screen_mode,
416
+ remove_brackets=screen_mode,
417
+ replace_no_break_spaces=True,
418
+ **kwargs,
419
+ )
420
+
421
+ # Add answers and other fields if not disabled
422
+ if not noanswers:
423
+ if spoilers == "pagebreak":
424
+ p = doc.add_page_break()
425
+ elif spoilers == "dots":
426
+ for _ in range(30):
427
+ if external_para is None:
428
+ p = doc.add_paragraph()
429
+ else:
430
+ p.add_run("\n")
431
+ p.add_run(".")
432
+ if external_para is None:
433
+ p = doc.add_paragraph()
434
+ else:
435
+ p.add_run("\n")
436
+ else:
437
+ if external_para is None:
438
+ p = doc.add_paragraph()
439
+ else:
440
+ p.add_run("\n")
441
+
442
+ p.paragraph_format.keep_together = True
443
+ p.paragraph_format.space_before = DocxPt(6)
444
+
445
+ # Add answer
446
+ answer_label = get_label_standalone(q, "answer", labels, language)
447
+ p.add_run(f"{answer_label}: ").bold = True
448
+ format_docx_element(
449
+ doc,
450
+ q["answer"],
451
+ p,
452
+ True,
453
+ spoilers,
454
+ logger,
455
+ labels,
456
+ language,
457
+ remove_accents=screen_mode,
458
+ replace_no_break_spaces=True,
459
+ **kwargs,
460
+ )
461
+
462
+ # Add other fields
463
+ for field in ["zachet", "nezachet", "comment", "source", "author"]:
464
+ if field in q:
465
+ if field == "source":
466
+ if external_para is None:
467
+ p = doc.add_paragraph()
468
+ p.paragraph_format.keep_together = True
469
+ else:
470
+ p.add_run("\n")
471
+ else:
472
+ p.add_run("\n")
473
+
474
+ field_label = get_label_standalone(q, field, labels, language)
475
+ p.add_run(f"{field_label}: ").bold = True
476
+ format_docx_element(
477
+ doc,
478
+ q[field],
479
+ p,
480
+ WHITEN[field],
481
+ spoilers,
482
+ logger,
483
+ labels,
484
+ language,
485
+ remove_accents=screen_mode,
486
+ remove_brackets=screen_mode,
487
+ replace_no_break_spaces=field != "source",
488
+ **kwargs,
489
+ )
490
+
491
+ return qcount
492
+
493
+
62
494
  class DocxExporter(BaseExporter):
63
495
  def __init__(self, *args, **kwargs):
64
496
  super().__init__(*args, **kwargs)
@@ -76,230 +508,53 @@ class DocxExporter(BaseExporter):
76
508
 
77
509
  def _docx_format(self, *args, **kwargs):
78
510
  kwargs.update(self.dir_kwargs)
79
- return self.docx_format(*args, **kwargs)
511
+ return format_docx_element(
512
+ self.doc,
513
+ *args,
514
+ spoilers=self.args.spoilers,
515
+ logger=self.logger,
516
+ labels=self.labels,
517
+ language=self.args.language,
518
+ **kwargs,
519
+ )
80
520
 
81
521
  def docx_format(self, el, para, whiten, **kwargs):
82
- if isinstance(el, list):
83
- if len(el) > 1 and isinstance(el[1], list):
84
- self.docx_format(el[0], para, whiten, **kwargs)
85
- licount = 0
86
- for li in el[1]:
87
- licount += 1
88
-
89
- para.add_run("\n{}. ".format(licount))
90
- self.docx_format(li, para, whiten, **kwargs)
91
- else:
92
- licount = 0
93
- for li in el:
94
- licount += 1
95
-
96
- para.add_run("\n{}. ".format(licount))
97
- self.docx_format(li, para, whiten, **kwargs)
98
-
99
- if isinstance(el, str):
100
- self.logger.debug("parsing element {}:".format(log_wrap(el)))
101
-
102
- if kwargs.get("remove_accents"):
103
- el = el.replace("\u0301", "")
104
- if kwargs.get("remove_brackets"):
105
- el = self.remove_square_brackets(el)
106
- else:
107
- el = replace_escaped(el)
108
-
109
- el = backtick_replace(el)
110
-
111
- for run in self.parse_4s_elem(el):
112
- if run[0] == "pagebreak":
113
- if self.args.spoilers == "dots":
114
- for _ in range(30):
115
- para = self.doc.add_paragraph()
116
- para.add_run(".")
117
- para = self.doc.add_paragraph()
118
- else:
119
- para = self.doc.add_page_break()
120
- elif run[0] == "linebreak":
121
- para.add_run("\n")
122
- elif run[0] == "screen":
123
- if kwargs.get("remove_accents") or kwargs.get("remove_brackets"):
124
- text = run[1]["for_screen"]
125
- else:
126
- text = run[1]["for_print"]
127
- if kwargs.get("replace_no_break_spaces"):
128
- text = self._replace_no_break(text)
129
- r = para.add_run(text)
130
- elif run[0] == "hyperlink" and not (
131
- whiten and self.args.spoilers == "whiten"
132
- ):
133
- r = self.add_hyperlink(para, run[1], run[1])
134
- elif run[0] == "img":
135
- if run[1].endswith(".shtml"):
136
- r = para.add_run(
137
- "(ТУТ БЫЛА ССЫЛКА НА ПРОТУХШУЮ КАРТИНКУ)\n"
138
- ) # TODO: добавить возможность пропускать кривые картинки опцией
139
- continue
140
- parsed_image = parseimg(
141
- run[1],
142
- dimensions="inches",
143
- tmp_dir=kwargs.get("tmp_dir"),
144
- targetdir=kwargs.get("targetdir"),
145
- )
146
- imgfile = parsed_image["imgfile"]
147
- width = parsed_image["width"]
148
- height = parsed_image["height"]
149
- inline = parsed_image["inline"]
150
- if inline:
151
- r = para.add_run("")
152
- else:
153
- r = para.add_run("\n")
154
-
155
- try:
156
- if inline:
157
- r.add_picture(
158
- imgfile,
159
- height=Inches(
160
- 1.0 / 6
161
- ), # Height is based on docx template
162
- )
163
- else:
164
- r.add_picture(
165
- imgfile, width=Inches(width), height=Inches(height)
166
- )
167
- except UnrecognizedImageError:
168
- sys.stderr.write(
169
- f"python-docx can't recognize header for {imgfile}\n"
170
- )
171
- if not inline:
172
- r = para.add_run("\n")
173
- continue
174
- else:
175
- text = run[1]
176
- if kwargs.get("replace_no_break_spaces"):
177
- text = self._replace_no_break(text)
178
- r = para.add_run(text)
179
- if "italic" in run[0]:
180
- r.italic = True
181
- if "bold" in run[0]:
182
- r.bold = True
183
- if "underline" in run[0]:
184
- r.underline = True
185
- if run[0] == "strike":
186
- r.font.strike = True
187
- if run[0] == "sc":
188
- r.small_caps = True
189
- if whiten and self.args.spoilers == "whiten":
190
- r.style = "Whitened"
522
+ # Redirect to standalone function
523
+ return format_docx_element(
524
+ self.doc,
525
+ el,
526
+ para,
527
+ whiten,
528
+ spoilers=self.args.spoilers,
529
+ logger=self.logger,
530
+ labels=self.labels,
531
+ language=self.args.language,
532
+ **kwargs,
533
+ )
191
534
 
192
535
  def add_hyperlink(self, paragraph, text, url):
193
- # adapted from https://github.com/python-openxml/python-docx/issues/610
194
- doc = self.doc
195
- run = paragraph.add_run(text)
196
- run.style = doc.styles["Hyperlink"]
197
- part = paragraph.part
198
- r_id = part.relate_to(
199
- url, docx.opc.constants.RELATIONSHIP_TYPE.HYPERLINK, is_external=True
200
- )
201
- hyperlink = docx.oxml.shared.OxmlElement("w:hyperlink")
202
- hyperlink.set(docx.oxml.shared.qn("r:id"), r_id)
203
- hyperlink.append(run._r)
204
- paragraph._p.append(hyperlink)
205
- return hyperlink
536
+ return add_hyperlink_to_docx(self.doc, paragraph, text, url)
206
537
 
207
538
  def add_question(
208
539
  self, element, skip_qcount=False, screen_mode=False, external_para=None
209
540
  ):
210
- q = element[1]
211
- if external_para is None:
212
- p = self.doc.add_paragraph()
213
- else:
214
- p = external_para
215
- p.paragraph_format.space_before = DocxPt(18)
216
- p.paragraph_format.keep_together = True
217
- if "number" not in q and not skip_qcount:
218
- self.qcount += 1
219
- if "setcounter" in q:
220
- self.qcount = int(q["setcounter"])
221
- p.add_run(
222
- "{question}. ".format(
223
- question=self.get_label(
224
- q,
225
- "question",
226
- number=self.qcount if "number" not in q else q["number"],
227
- )
228
- )
229
- ).bold = True
230
-
231
- if "handout" in q:
232
- p.add_run("\n[{handout}: ".format(handout=self.get_label(q, "handout")))
233
- self._docx_format(
234
- q["handout"],
235
- p,
236
- WHITEN["handout"],
237
- remove_accents=screen_mode,
238
- remove_brackets=screen_mode,
239
- )
240
- p.add_run("\n]")
241
- if not self.args.noparagraph:
242
- p.add_run("\n")
243
-
244
- self._docx_format(
245
- q["question"],
246
- p,
247
- False,
248
- remove_accents=screen_mode,
249
- remove_brackets=screen_mode,
250
- replace_no_break_spaces=True,
541
+ self.qcount = add_question_to_docx(
542
+ self.doc,
543
+ element[1],
544
+ self.labels,
545
+ self.qcount,
546
+ skip_qcount,
547
+ screen_mode,
548
+ external_para,
549
+ self.args.noparagraph,
550
+ self.args.noanswers,
551
+ self.args.spoilers,
552
+ self.args.language,
553
+ self.args.only_question_number,
554
+ self.logger,
555
+ **self.dir_kwargs,
251
556
  )
252
557
 
253
- if not self.args.noanswers:
254
- if self.args.spoilers == "pagebreak":
255
- p = self.doc.add_page_break()
256
- elif self.args.spoilers == "dots":
257
- for _ in range(30):
258
- if external_para is None:
259
- p = self.doc.add_paragraph()
260
- else:
261
- p.add_run("\n")
262
- p.add_run(".")
263
- if external_para is None:
264
- p = self.doc.add_paragraph()
265
- else:
266
- p.add_run("\n")
267
- else:
268
- if external_para is None:
269
- p = self.doc.add_paragraph()
270
- else:
271
- p.add_run("\n")
272
- p.paragraph_format.keep_together = True
273
- p.paragraph_format.space_before = DocxPt(6)
274
- p.add_run(f"{self.get_label(q, 'answer')}: ").bold = True
275
- self._docx_format(
276
- q["answer"],
277
- p,
278
- True,
279
- remove_accents=screen_mode,
280
- replace_no_break_spaces=True,
281
- )
282
-
283
- for field in ["zachet", "nezachet", "comment", "source", "author"]:
284
- if field in q:
285
- if field == "source":
286
- if external_para is None:
287
- p = self.doc.add_paragraph()
288
- p.paragraph_format.keep_together = True
289
- else:
290
- p.add_run("\n")
291
- else:
292
- p.add_run("\n")
293
- p.add_run(f"{self.get_label(q, field)}: ").bold = True
294
- self._docx_format(
295
- q[field],
296
- p,
297
- WHITEN[field],
298
- remove_accents=screen_mode,
299
- remove_brackets=screen_mode,
300
- replace_no_break_spaces=field != "source",
301
- )
302
-
303
558
  def _add_question_columns(self, element):
304
559
  table = self.doc.add_table(rows=1, cols=2)
305
560
  table.autofit = True
@@ -332,82 +587,6 @@ class DocxExporter(BaseExporter):
332
587
 
333
588
  self.doc.add_paragraph()
334
589
 
335
- def _add_question_content(self, q, p, skip_qcount=False, screen_mode=False):
336
- """Helper method to add question content to a paragraph"""
337
- if "number" not in q and not skip_qcount:
338
- self.qcount += 1
339
- if "setcounter" in q:
340
- self.qcount = int(q["setcounter"])
341
- p.add_run(
342
- "{question}. ".format(
343
- question=self.get_label(
344
- q,
345
- "question",
346
- number=self.qcount if "number" not in q else q["number"],
347
- )
348
- )
349
- ).bold = True
350
-
351
- if "handout" in q:
352
- p.add_run("\n[{handout}: ".format(handout=self.get_label(q, "handout")))
353
- self._docx_format(
354
- q["handout"],
355
- p,
356
- WHITEN["handout"],
357
- remove_accents=screen_mode,
358
- remove_brackets=screen_mode,
359
- )
360
- p.add_run("\n]")
361
- if not self.args.noparagraph:
362
- p.add_run("\n")
363
-
364
- self._docx_format(
365
- q["question"],
366
- p,
367
- False,
368
- remove_accents=screen_mode,
369
- remove_brackets=screen_mode,
370
- replace_no_break_spaces=True,
371
- )
372
-
373
- if not self.args.noanswers:
374
- if self.args.spoilers == "pagebreak":
375
- p = self.doc.add_page_break()
376
- elif self.args.spoilers == "dots":
377
- for _ in range(30):
378
- p = self.doc.add_paragraph()
379
- p.add_run(".")
380
- p = self.doc.add_paragraph()
381
- else:
382
- p = self.doc.add_paragraph()
383
- p.paragraph_format.keep_together = True
384
- p.paragraph_format.space_before = DocxPt(6)
385
- p.add_run(f"{self.get_label(q, 'answer')}: ").bold = True
386
- self._docx_format(
387
- q["answer"],
388
- p,
389
- True,
390
- remove_accents=screen_mode,
391
- replace_no_break_spaces=True,
392
- )
393
-
394
- for field in ["zachet", "nezachet", "comment", "source", "author"]:
395
- if field in q:
396
- if field == "source":
397
- p = self.doc.add_paragraph()
398
- p.paragraph_format.keep_together = True
399
- else:
400
- p.add_run("\n")
401
- p.add_run(f"{self.get_label(q, field)}: ").bold = True
402
- self._docx_format(
403
- q[field],
404
- p,
405
- WHITEN[field],
406
- remove_accents=screen_mode,
407
- remove_brackets=screen_mode,
408
- replace_no_break_spaces=field != "source",
409
- )
410
-
411
590
  def export(self, outfilename):
412
591
  self.logger.debug(self.args.docx_template)
413
592
  self.doc = Document(self.args.docx_template)
@@ -473,3 +652,53 @@ class DocxExporter(BaseExporter):
473
652
 
474
653
  self.doc.save(outfilename)
475
654
  self.logger.info("Output: {}".format(outfilename))
655
+
656
+
657
+ # Example usage of the extracted DOCX functions:
658
+ """
659
+ from docx import Document
660
+ import toml
661
+ from chgksuite.composer.docx import add_question_to_docx, format_docx_element
662
+
663
+ # Load labels
664
+ with open("labels.toml", encoding="utf8") as f:
665
+ labels = toml.load(f)
666
+
667
+ # Create a new document
668
+ doc = Document()
669
+
670
+ # Example question data
671
+ question_data = {
672
+ "question": "What is the capital of France?",
673
+ "answer": "Paris",
674
+ "comment": "This is a basic geography question",
675
+ "source": "World Geography Book"
676
+ }
677
+
678
+ # Add question to document
679
+ qcount = add_question_to_docx(
680
+ doc=doc,
681
+ question_data=question_data,
682
+ labels=labels,
683
+ qcount=0, # Starting question count
684
+ noanswers=False, # Include answers
685
+ spoilers="none", # No spoiler handling
686
+ language="en",
687
+ only_question_number=False
688
+ )
689
+
690
+ # Or use the lower-level formatting function directly
691
+ paragraph = doc.add_paragraph()
692
+ format_docx_element(
693
+ doc=doc,
694
+ el="This is **bold text** and _italic text_",
695
+ para=paragraph,
696
+ whiten=False,
697
+ spoilers="none",
698
+ labels=labels,
699
+ language="en"
700
+ )
701
+
702
+ # Save the document
703
+ doc.save("example_output.docx")
704
+ """