QuizGenerator 0.6.3__py3-none-any.whl → 0.7.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.
Files changed (31) hide show
  1. QuizGenerator/contentast.py +2191 -2193
  2. QuizGenerator/misc.py +1 -1
  3. QuizGenerator/mixins.py +64 -64
  4. QuizGenerator/premade_questions/basic.py +16 -16
  5. QuizGenerator/premade_questions/cst334/languages.py +26 -26
  6. QuizGenerator/premade_questions/cst334/math_questions.py +42 -42
  7. QuizGenerator/premade_questions/cst334/memory_questions.py +124 -124
  8. QuizGenerator/premade_questions/cst334/persistence_questions.py +48 -48
  9. QuizGenerator/premade_questions/cst334/process.py +38 -38
  10. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py +45 -45
  11. QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py +34 -34
  12. QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py +53 -53
  13. QuizGenerator/premade_questions/cst463/gradient_descent/misc.py +2 -2
  14. QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py +65 -65
  15. QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py +39 -39
  16. QuizGenerator/premade_questions/cst463/models/attention.py +36 -36
  17. QuizGenerator/premade_questions/cst463/models/cnns.py +26 -26
  18. QuizGenerator/premade_questions/cst463/models/rnns.py +36 -36
  19. QuizGenerator/premade_questions/cst463/models/text.py +32 -32
  20. QuizGenerator/premade_questions/cst463/models/weight_counting.py +15 -15
  21. QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py +124 -124
  22. QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py +161 -161
  23. QuizGenerator/question.py +41 -41
  24. QuizGenerator/quiz.py +7 -7
  25. QuizGenerator/regenerate.py +114 -13
  26. QuizGenerator/typst_utils.py +2 -2
  27. {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/METADATA +1 -1
  28. {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/RECORD +31 -31
  29. {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/WHEEL +0 -0
  30. {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/entry_points.txt +0 -0
  31. {quizgenerator-0.6.3.dist-info → quizgenerator-0.7.1.dist-info}/licenses/LICENSE +0 -0
QuizGenerator/question.py CHANGED
@@ -21,7 +21,7 @@ import yaml
21
21
  from typing import List, Dict, Any, Tuple, Optional
22
22
  import canvasapi.course, canvasapi.quiz
23
23
 
24
- from QuizGenerator.contentast import ContentAST, AnswerTypes
24
+ import QuizGenerator.contentast as ca
25
25
  from QuizGenerator.performance import timer, PerformanceTracker
26
26
 
27
27
  import logging
@@ -31,9 +31,9 @@ log = logging.getLogger(__name__)
31
31
  @dataclasses.dataclass
32
32
  class QuestionComponents:
33
33
  """Bundle of question parts generated during construction."""
34
- body: ContentAST.Element
35
- answers: List[ContentAST.Answer]
36
- explanation: ContentAST.Element
34
+ body: ca.Element
35
+ answers: List[ca.Answer]
36
+ explanation: ca.Element
37
37
 
38
38
 
39
39
  # Spacing presets for questions
@@ -344,15 +344,15 @@ class Question(abc.ABC):
344
344
  """
345
345
  Base class for all quiz questions with cross-format rendering support.
346
346
 
347
- CRITICAL: When implementing Question subclasses, ALWAYS use ContentAST elements
347
+ CRITICAL: When implementing Question subclasses, ALWAYS use content AST elements
348
348
  for all content in get_body() and get_explanation() methods.
349
349
 
350
- NEVER create manual LaTeX, HTML, or Markdown strings. The ContentAST system
350
+ NEVER create manual LaTeX, HTML, or Markdown strings. The content AST system
351
351
  ensures consistent rendering across PDF/LaTeX and Canvas/HTML formats.
352
352
 
353
353
  Required Methods:
354
- - _get_body(): Return Tuple[ContentAST.Section, List[ContentAST.Answer]] with body and answers
355
- - _get_explanation(): Return Tuple[ContentAST.Section, List[ContentAST.Answer]] with explanation
354
+ - _get_body(): Return Tuple[ca.Section, List[ca.Answer]] with body and answers
355
+ - _get_explanation(): Return Tuple[ca.Section, List[ca.Answer]] with explanation
356
356
 
357
357
  Note: get_body() and get_explanation() are provided for backward compatibility
358
358
  and call the _get_* methods, returning just the first element of the tuple.
@@ -361,28 +361,28 @@ class Question(abc.ABC):
361
361
  - VERSION (str): Question version number (e.g., "1.0")
362
362
  Increment when RNG logic changes to ensure reproducibility
363
363
 
364
- ContentAST Usage Examples:
364
+ Content AST Usage Examples:
365
365
  def _get_body(self):
366
- body = ContentAST.Section()
366
+ body = ca.Section()
367
367
  answers = []
368
- body.add_element(ContentAST.Paragraph(["Calculate the matrix:"]))
368
+ body.add_element(ca.Paragraph(["Calculate the matrix:"]))
369
369
 
370
- # Use ContentAST.Matrix for math, NOT manual LaTeX
370
+ # Use ca.Matrix for math, NOT manual LaTeX
371
371
  matrix_data = [[1, 2], [3, 4]]
372
- body.add_element(ContentAST.Matrix(data=matrix_data, bracket_type="b"))
372
+ body.add_element(ca.Matrix(data=matrix_data, bracket_type="b"))
373
373
 
374
- # Answer extends ContentAST.Leaf - add directly to body
375
- ans = ContentAST.Answer.integer("result", 42, label="Result")
374
+ # Answer extends ca.Leaf - add directly to body
375
+ ans = ca.Answer.integer("result", 42, label="Result")
376
376
  answers.append(ans)
377
377
  body.add_element(ans)
378
378
  return body, answers
379
379
 
380
- Common ContentAST Elements:
381
- - ContentAST.Paragraph: Text blocks
382
- - ContentAST.Equation: Mathematical expressions
383
- - ContentAST.Matrix: Matrices and vectors (use instead of manual LaTeX!)
384
- - ContentAST.Table: Data tables
385
- - ContentAST.OnlyHtml/OnlyLatex: Platform-specific content
380
+ Common Content AST Elements:
381
+ - ca.Paragraph: Text blocks
382
+ - ca.Equation: Mathematical expressions
383
+ - ca.Matrix: Matrices and vectors (use instead of manual LaTeX!)
384
+ - ca.Table: Data tables
385
+ - ca.OnlyHtml/OnlyLatex: Platform-specific content
386
386
 
387
387
  Versioning Guidelines:
388
388
  - Increment VERSION when changing:
@@ -473,7 +473,7 @@ class Question(abc.ABC):
473
473
  self.points_value = points_value
474
474
  self.topic = topic
475
475
  self.spacing = parse_spacing(kwargs.get("spacing", 0))
476
- self.answer_kind = ContentAST.Answer.CanvasAnswerKind.BLANK
476
+ self.answer_kind = ca.Answer.CanvasAnswerKind.BLANK
477
477
 
478
478
  # Support for multi-part questions (defaults to 1 for normal questions)
479
479
  self.num_subquestions = kwargs.get("num_subquestions", 1)
@@ -504,11 +504,11 @@ class Question(abc.ABC):
504
504
  with open(path_to_yaml) as fid:
505
505
  question_dicts = yaml.safe_load_all(fid)
506
506
 
507
- def get_question(self, **kwargs) -> ContentAST.Question:
507
+ def get_question(self, **kwargs) -> ca.Question:
508
508
  """
509
509
  Gets the question in AST format
510
510
  :param kwargs:
511
- :return: (ContentAST.Question) Containing question.
511
+ :return: (ca.Question) Containing question.
512
512
  """
513
513
  # Generate the question, retrying with incremented seeds until we get an interesting one
514
514
  with timer("question_refresh", question_name=self.name, question_type=self.__class__.__name__):
@@ -553,7 +553,7 @@ class Question(abc.ABC):
553
553
 
554
554
  # Store the actual seed used and question metadata for QR code generation
555
555
  actual_seed = None if base_seed is None else base_seed + backoff_counter - 1
556
- question_ast = ContentAST.Question(
556
+ question_ast = ca.Question(
557
557
  body=body,
558
558
  explanation=explanation,
559
559
  value=self.points_value,
@@ -575,31 +575,31 @@ class Question(abc.ABC):
575
575
  return question_ast
576
576
 
577
577
  @abc.abstractmethod
578
- def get_body(self, **kwargs) -> ContentAST.Section:
578
+ def get_body(self, **kwargs) -> ca.Section:
579
579
  """
580
580
  Gets the body of the question during generation
581
581
  :param kwargs:
582
- :return: (ContentAST.Section) Containing question body
582
+ :return: (ca.Section) Containing question body
583
583
  """
584
584
  pass
585
585
 
586
- def get_explanation(self, **kwargs) -> ContentAST.Section:
586
+ def get_explanation(self, **kwargs) -> ca.Section:
587
587
  """
588
588
  Gets the body of the question during generation (backward compatible wrapper).
589
589
  Calls _get_explanation() and returns just the explanation.
590
590
  :param kwargs:
591
- :return: (ContentAST.Section) Containing question explanation or None
591
+ :return: (ca.Section) Containing question explanation or None
592
592
  """
593
593
  # Try new pattern first
594
594
  if hasattr(self, '_get_explanation') and callable(getattr(self, '_get_explanation')):
595
595
  explanation, _ = self._get_explanation()
596
596
  return explanation
597
597
  # Fallback: default explanation
598
- return ContentAST.Section(
599
- [ContentAST.Text("[Please reach out to your professor for clarification]")]
598
+ return ca.Section(
599
+ [ca.Text("[Please reach out to your professor for clarification]")]
600
600
  )
601
601
 
602
- def _get_body(self) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
602
+ def _get_body(self) -> Tuple[ca.Element, List[ca.Answer]]:
603
603
  """
604
604
  Build question body and collect answers (new pattern).
605
605
  Questions should override this to return (body, answers) tuple.
@@ -611,7 +611,7 @@ class Question(abc.ABC):
611
611
  body = self.get_body()
612
612
  return body, []
613
613
 
614
- def _get_explanation(self) -> Tuple[ContentAST.Element, List[ContentAST.Answer]]:
614
+ def _get_explanation(self) -> Tuple[ca.Element, List[ca.Answer]]:
615
615
  """
616
616
  Build question explanation and collect answers (new pattern).
617
617
  Questions can override this to include answers in explanations.
@@ -619,8 +619,8 @@ class Question(abc.ABC):
619
619
  Returns:
620
620
  Tuple of (explanation_ast, answers_list)
621
621
  """
622
- return ContentAST.Section(
623
- [ContentAST.Text("[Please reach out to your professor for clarification]")]
622
+ return ca.Section(
623
+ [ca.Text("[Please reach out to your professor for clarification]")]
624
624
  ), []
625
625
 
626
626
  def build_question_components(self, **kwargs) -> QuestionComponents:
@@ -645,7 +645,7 @@ class Question(abc.ABC):
645
645
  explanation=explanation
646
646
  )
647
647
 
648
- def get_answers(self, *args, **kwargs) -> Tuple[ContentAST.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
648
+ def get_answers(self, *args, **kwargs) -> Tuple[ca.Answer.CanvasAnswerKind, List[Dict[str,Any]]]:
649
649
  """
650
650
  Return answers from cached components (new pattern) or self.answers dict (old pattern).
651
651
  """
@@ -663,7 +663,7 @@ class Question(abc.ABC):
663
663
  answers = self._components.answers
664
664
  if self.can_be_numerical():
665
665
  return (
666
- ContentAST.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
666
+ ca.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
667
667
  list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in answers]))
668
668
  )
669
669
  return (
@@ -675,7 +675,7 @@ class Question(abc.ABC):
675
675
  if len(self.answers.values()) > 0:
676
676
  if self.can_be_numerical():
677
677
  return (
678
- ContentAST.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
678
+ ca.Answer.CanvasAnswerKind.NUMERICAL_QUESTION,
679
679
  list(itertools.chain(*[a.get_for_canvas(single_answer=True) for a in self.answers.values()]))
680
680
  )
681
681
  return (
@@ -683,7 +683,7 @@ class Question(abc.ABC):
683
683
  list(itertools.chain(*[a.get_for_canvas() for a in self.answers.values()]))
684
684
  )
685
685
 
686
- return (ContentAST.Answer.CanvasAnswerKind.ESSAY, [])
686
+ return (ca.Answer.CanvasAnswerKind.ESSAY, [])
687
687
 
688
688
  def refresh(self, rng_seed=None, *args, **kwargs):
689
689
  """If it is necessary to regenerate aspects between usages, this is the time to do it.
@@ -750,7 +750,7 @@ class Question(abc.ABC):
750
750
 
751
751
  def can_be_numerical(self):
752
752
  if (len(self.answers.values()) == 1
753
- and isinstance(list(self.answers.values())[0], AnswerTypes.Float)
753
+ and isinstance(list(self.answers.values())[0], ca.AnswerTypes.Float)
754
754
  ):
755
755
  return True
756
756
  return False
@@ -806,4 +806,4 @@ class QuestionGroup():
806
806
  return attr(*args, **kwargs)
807
807
  return wrapped_method
808
808
 
809
- return attr
809
+ return attr
QuizGenerator/quiz.py CHANGED
@@ -15,7 +15,7 @@ import re
15
15
 
16
16
  import yaml
17
17
 
18
- from QuizGenerator.contentast import ContentAST
18
+ import QuizGenerator.contentast as ca
19
19
  from QuizGenerator.question import Question, QuestionRegistry, QuestionGroup
20
20
 
21
21
  log = logging.getLogger(__name__)
@@ -34,12 +34,12 @@ class Quiz:
34
34
  self.questions = questions
35
35
  self.instructions = kwargs.get("instructions", "")
36
36
 
37
- # Parse description with ContentAST if provided
37
+ # Parse description with content AST if provided
38
38
  raw_description = kwargs.get("description", None)
39
39
  if raw_description:
40
- # Create a ContentAST document from the description text
41
- desc_doc = ContentAST.Document()
42
- desc_doc.add_element(ContentAST.Paragraph([raw_description]))
40
+ # Create a content AST document from the description text
41
+ desc_doc = ca.Document()
42
+ desc_doc.add_element(ca.Paragraph([raw_description]))
43
43
  self.description = desc_doc.render("html")
44
44
  else:
45
45
  self.description = None
@@ -418,8 +418,8 @@ class Quiz:
418
418
 
419
419
  return optimized_questions
420
420
 
421
- def get_quiz(self, **kwargs) -> ContentAST.Document:
422
- quiz = ContentAST.Document(title=self.name)
421
+ def get_quiz(self, **kwargs) -> ca.Document:
422
+ quiz = ca.Document(title=self.name)
423
423
 
424
424
  # Extract master RNG seed (if provided) and remove from kwargs
425
425
  master_seed = kwargs.pop('rng_seed', None)
@@ -39,12 +39,13 @@ the exact question and answer without needing the original exam file.
39
39
  """
40
40
 
41
41
  import argparse
42
+ import base64
42
43
  import json
43
44
  import sys
44
45
  import logging
45
46
  import os
46
47
  from pathlib import Path
47
- from typing import Dict, Any, Optional, List
48
+ from typing import Dict, Any, Optional, List, Callable
48
49
 
49
50
  # Load environment variables from .env file
50
51
  try:
@@ -140,7 +141,39 @@ def parse_qr_data(qr_string: str) -> Dict[str, Any]:
140
141
  return {}
141
142
 
142
143
 
143
- def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
144
+ def _inline_image_upload(img_data) -> str:
145
+ img_data.seek(0)
146
+ b64 = base64.b64encode(img_data.read()).decode("ascii")
147
+ return f"data:image/png;base64,{b64}"
148
+
149
+
150
+ def _resolve_upload_func(
151
+ image_mode: str,
152
+ upload_func: Optional[Callable]
153
+ ) -> Optional[Callable]:
154
+ if image_mode == "inline":
155
+ return _inline_image_upload
156
+ if image_mode == "upload":
157
+ if upload_func is None:
158
+ raise ValueError("image_mode='upload' requires upload_func")
159
+ return upload_func
160
+ if image_mode == "none":
161
+ return None
162
+ raise ValueError(f"Unknown image_mode: {image_mode}")
163
+
164
+
165
+ def _render_html(element, upload_func=None, **kwargs) -> str:
166
+ if upload_func is None:
167
+ return element.render("html", **kwargs)
168
+ return element.render("html", upload_func=upload_func, **kwargs)
169
+
170
+
171
+ def regenerate_question_answer(
172
+ qr_data: Dict[str, Any],
173
+ *,
174
+ image_mode: str = "inline",
175
+ upload_func: Optional[Callable] = None
176
+ ) -> Optional[Dict[str, Any]]:
144
177
  """
145
178
  Regenerate question and extract answer using QR code metadata.
146
179
 
@@ -156,8 +189,9 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
156
189
  "seed": int,
157
190
  "version": str,
158
191
  "answers": dict,
159
- "explanation_markdown": str | None # Markdown explanation (None if not available)
160
- }
192
+ "explanation_markdown": str | None # Markdown explanation (None if not available)
193
+ "explanation_html": str | None # HTML explanation (None if not available)
194
+ }
161
195
  """
162
196
  question_num = qr_data.get('q')
163
197
  points = qr_data.get('pts')
@@ -220,8 +254,14 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
220
254
  # Also store the raw answer objects for easier access
221
255
  result['answer_objects'] = question.answers
222
256
 
257
+ resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
258
+
223
259
  # Generate HTML answer key for grading
224
- question_html = question_ast.body.render("html", show_answers=True)
260
+ question_html = _render_html(
261
+ question_ast.body,
262
+ show_answers=True,
263
+ upload_func=resolved_upload_func
264
+ )
225
265
  result['answer_key_html'] = question_html
226
266
 
227
267
  # Generate markdown explanation for students
@@ -232,6 +272,16 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
232
272
  else:
233
273
  result['explanation_markdown'] = explanation_markdown
234
274
 
275
+ # Generate HTML explanation (optional for web UIs)
276
+ explanation_html = _render_html(
277
+ question_ast.explanation,
278
+ upload_func=resolved_upload_func
279
+ )
280
+ if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
281
+ result["explanation_html"] = None
282
+ else:
283
+ result["explanation_html"] = explanation_html
284
+
235
285
  log.info(f" Successfully regenerated question with {len(canvas_answers)} answer(s)")
236
286
 
237
287
  return result
@@ -243,7 +293,13 @@ def regenerate_question_answer(qr_data: Dict[str, Any]) -> Optional[Dict[str, An
243
293
  return result
244
294
 
245
295
 
246
- def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[str, Any]:
296
+ def regenerate_from_encrypted(
297
+ encrypted_data: str,
298
+ points: float = 1.0,
299
+ *,
300
+ image_mode: str = "inline",
301
+ upload_func: Optional[Callable] = None
302
+ ) -> Dict[str, Any]:
247
303
  """
248
304
  Regenerate question answers from encrypted QR code data (RECOMMENDED API).
249
305
 
@@ -253,6 +309,8 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
253
309
  Args:
254
310
  encrypted_data: The encrypted 's' field from the QR code JSON
255
311
  points: Point value for the question (default: 1.0)
312
+ image_mode: "inline", "upload", or "none" for HTML image handling
313
+ upload_func: Optional upload function used when image_mode="upload"
256
314
 
257
315
  Returns:
258
316
  Dictionary with regenerated answers:
@@ -266,6 +324,7 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
266
324
  "answer_objects": dict, # Raw Answer objects with values/tolerances
267
325
  "answer_key_html": str, # HTML rendering of question with answers shown
268
326
  "explanation_markdown": str | None # Markdown explanation (None if not available)
327
+ "explanation_html": str | None # HTML explanation (None if not available)
269
328
  }
270
329
 
271
330
  Raises:
@@ -287,12 +346,21 @@ def regenerate_from_encrypted(encrypted_data: str, points: float = 1.0) -> Dict[
287
346
  kwargs = decrypted.get('config', {})
288
347
 
289
348
  # Use the existing regeneration logic
290
- return regenerate_from_metadata(question_type, seed, version, points, kwargs)
349
+ return regenerate_from_metadata(
350
+ question_type,
351
+ seed,
352
+ version,
353
+ points,
354
+ kwargs,
355
+ image_mode=image_mode,
356
+ upload_func=upload_func
357
+ )
291
358
 
292
359
 
293
360
  def regenerate_from_metadata(
294
361
  question_type: str, seed: int, version: str,
295
- points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None
362
+ points: float = 1.0, kwargs: Optional[Dict[str, Any]] = None,
363
+ *, image_mode: str = "inline", upload_func: Optional[Callable] = None
296
364
  ) -> Dict[str, Any]:
297
365
  """
298
366
  Regenerate question answers from explicit metadata fields.
@@ -306,6 +374,8 @@ def regenerate_from_metadata(
306
374
  points: Point value for the question (default: 1.0)
307
375
  kwargs: Optional dictionary of question-specific configuration parameters
308
376
  (e.g., {"num_bits_va": 32, "max_value": 100})
377
+ image_mode: "inline", "upload", or "none" for HTML image handling
378
+ upload_func: Optional upload function used when image_mode="upload"
309
379
 
310
380
  Returns:
311
381
  Dictionary with regenerated answers (same format as regenerate_from_encrypted)
@@ -335,8 +405,14 @@ def regenerate_from_metadata(
335
405
  # Extract answers
336
406
  answer_kind, canvas_answers = question.get_answers()
337
407
 
408
+ resolved_upload_func = _resolve_upload_func(image_mode, upload_func)
409
+
338
410
  # Generate HTML answer key for grading
339
- question_html = question_ast.body.render("html", show_answers=True)
411
+ question_html = _render_html(
412
+ question_ast.body,
413
+ show_answers=True,
414
+ upload_func=resolved_upload_func
415
+ )
340
416
 
341
417
  # Generate markdown explanation for students
342
418
  explanation_markdown = question_ast.explanation.render("markdown")
@@ -344,6 +420,13 @@ def regenerate_from_metadata(
344
420
  if not explanation_markdown or "[Please reach out to your professor for clarification]" in explanation_markdown:
345
421
  explanation_markdown = None
346
422
 
423
+ explanation_html = _render_html(
424
+ question_ast.explanation,
425
+ upload_func=resolved_upload_func
426
+ )
427
+ if not explanation_html or "[Please reach out to your professor for clarification]" in explanation_html:
428
+ explanation_html = None
429
+
347
430
  result = {
348
431
  "question_type": question_type,
349
432
  "seed": seed,
@@ -355,7 +438,8 @@ def regenerate_from_metadata(
355
438
  },
356
439
  "answer_objects": question.answers,
357
440
  "answer_key_html": question_html,
358
- "explanation_markdown": explanation_markdown
441
+ "explanation_markdown": explanation_markdown,
442
+ "explanation_html": explanation_html
359
443
  }
360
444
 
361
445
  # Include kwargs in result if provided
@@ -407,6 +491,9 @@ def display_answer_summary(question_data: Dict[str, Any]) -> None:
407
491
  if 'explanation_markdown' in question_data and question_data['explanation_markdown'] is not None:
408
492
  print("Markdown explanation available in result['explanation_markdown']")
409
493
 
494
+ if 'explanation_html' in question_data and question_data['explanation_html'] is not None:
495
+ print("HTML explanation available in result['explanation_html']")
496
+
410
497
  print("=" * 60)
411
498
 
412
499
 
@@ -445,6 +532,12 @@ def main():
445
532
  action='store_true',
446
533
  help='Enable verbose debug logging'
447
534
  )
535
+ parser.add_argument(
536
+ '--image-mode',
537
+ choices=['inline', 'none'],
538
+ default='inline',
539
+ help='HTML image handling (default: inline)'
540
+ )
448
541
 
449
542
  args = parser.parse_args()
450
543
 
@@ -467,7 +560,11 @@ def main():
467
560
  if args.encrypted_str:
468
561
  try:
469
562
  log.info(f"Decoding encrypted string (points={args.points})")
470
- result = regenerate_from_encrypted(args.encrypted_str, args.points)
563
+ result = regenerate_from_encrypted(
564
+ args.encrypted_str,
565
+ args.points,
566
+ image_mode=args.image_mode
567
+ )
471
568
 
472
569
  # Format result similar to regenerate_question_answer output
473
570
  question_data = {
@@ -479,7 +576,8 @@ def main():
479
576
  "answers": result["answers"],
480
577
  "answer_objects": result["answer_objects"],
481
578
  "answer_key_html": result["answer_key_html"],
482
- "explanation_markdown": result.get("explanation_markdown")
579
+ "explanation_markdown": result.get("explanation_markdown"),
580
+ "explanation_html": result.get("explanation_html")
483
581
  }
484
582
 
485
583
  if "kwargs" in result:
@@ -517,7 +615,10 @@ def main():
517
615
  continue
518
616
 
519
617
  # Regenerate question and answer
520
- question_data = regenerate_question_answer(qr_data)
618
+ question_data = regenerate_question_answer(
619
+ qr_data,
620
+ image_mode=args.image_mode
621
+ )
521
622
 
522
623
  if question_data:
523
624
  results.append(question_data)
@@ -12,7 +12,7 @@ import tempfile
12
12
  import textwrap
13
13
  from pathlib import Path
14
14
  from typing import Optional
15
- from QuizGenerator.contentast import ContentAST
15
+ import QuizGenerator.contentast as ca
16
16
 
17
17
  import logging
18
18
  log = logging.getLogger(__name__)
@@ -31,7 +31,7 @@ def measure_typst_content(typst_content: str, page_width_cm: float = 18.0) -> Op
31
31
  """
32
32
 
33
33
  # Get the Typst header which includes fillline and other helper functions
34
- typst_header = ContentAST.Document.TYPST_HEADER
34
+ typst_header = ca.Document.TYPST_HEADER
35
35
 
36
36
  # Create temporary Typst file with measurement wrapper
37
37
  typst_code = textwrap.dedent(f"""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: QuizGenerator
3
- Version: 0.6.3
3
+ Version: 0.7.1
4
4
  Summary: Generate randomized quiz questions for Canvas LMS and PDF exams
5
5
  Project-URL: Homepage, https://github.com/OtterDen-Lab/QuizGenerator
6
6
  Project-URL: Documentation, https://github.com/OtterDen-Lab/QuizGenerator/tree/main/documentation
@@ -1,50 +1,50 @@
1
1
  QuizGenerator/__init__.py,sha256=8EV-k90A3PNC8Cm2-ZquwNyVyvnwW1gs6u-nGictyhs,840
2
2
  QuizGenerator/__main__.py,sha256=Dd9w4R0Unm3RiXztvR4Y_g9-lkWp6FHg-4VN50JbKxU,151
3
3
  QuizGenerator/constants.py,sha256=AO-UWwsWPLb1k2JW6KP8rl9fxTcdT0rW-6XC6zfnDOs,4386
4
- QuizGenerator/contentast.py,sha256=vO6t1VrRApS2zwW0Pi0PFlcKW_Li5R2bpEcCQP6a9Dc,92331
4
+ QuizGenerator/contentast.py,sha256=8i7iF3cxcYbrVkpK89WdwhNtH4Bo25_rqDCxrSc0z8s,86281
5
5
  QuizGenerator/generate.py,sha256=AWzNL0QTYDTcJFKaYfHIoRHvhx9MYRAbsD6z7E5_c9k,15733
6
- QuizGenerator/misc.py,sha256=wtlrEpmEpoE6vNRmgjNUmuWnRdQKSCYfrqeoTagNaxg,464
7
- QuizGenerator/mixins.py,sha256=HEhdGdeghqGWoajADTAIdUjkzwDSYl1b65LAkUdV50U,19211
6
+ QuizGenerator/misc.py,sha256=MXrguUhhdrWSV4Hqdl4G21ktowODu1AcKy6-5mvy3aI,454
7
+ QuizGenerator/mixins.py,sha256=aFn2573APCh-XKn6U7Xy95Iyj1a-BsRXnorjpN1FT6E,18711
8
8
  QuizGenerator/performance.py,sha256=CM3zLarJXN5Hfrl4-6JRBqD03j4BU1B2QW699HAr1Ds,7002
9
9
  QuizGenerator/qrcode_generator.py,sha256=S3mzZDk2UiHiw6ipSCpWPMhbKvSRR1P5ordZJUTo6ug,10776
10
- QuizGenerator/question.py,sha256=FjiAJZn3LUBVr7nDazx5G0bASP0uXQTUP0rqphbvyLw,31714
11
- QuizGenerator/quiz.py,sha256=f2HLrawUlu3ULkNDzcihBWAt-e-49AIPz_l1edMAEQ0,21503
12
- QuizGenerator/regenerate.py,sha256=Uh4B9aKQvL3zD7PT-uH-GvrcSuUygV1BimvPVuErc-g,16525
13
- QuizGenerator/typst_utils.py,sha256=XtMEO1e4_Tg0G1zR9D1fmrYKlUfHenBPdGoCKR0DhZg,3154
10
+ QuizGenerator/question.py,sha256=CqKq_-HDzETrgEr-E8fQu1sRiMZv6ZoBAjig6gy34vw,31395
11
+ QuizGenerator/quiz.py,sha256=4W_3xZMLx-pMzB5mn8GOhbmE-7bYiIqYLtsVYxJSdVc,21463
12
+ QuizGenerator/regenerate.py,sha256=q8fGecZXx_sMUEpaMMmS8Zp3QW_m63WE9_5g6IKtKUk,19520
13
+ QuizGenerator/typst_utils.py,sha256=JGQn_u5bEHd8HAtjAHuZoVJwLkx-Rd4ZCBWffwFZa3o,3136
14
14
  QuizGenerator/canvas/__init__.py,sha256=TwFP_zgxPIlWtkvIqQ6mcvBNTL9swIH_rJl7DGKcvkQ,286
15
15
  QuizGenerator/canvas/canvas_interface.py,sha256=StMcdXgLvTA1EayQ44m_le2GXGQpDQnduYXVeUYsqW0,24618
16
16
  QuizGenerator/canvas/classes.py,sha256=v_tQ8t_JJplU9sv2p4YctX45Fwed1nQ2HC1oC9BnDNw,7594
17
17
  QuizGenerator/premade_questions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
- QuizGenerator/premade_questions/basic.py,sha256=vMyCIYU0IJBjQVE-XVzHr9axq_kZL2ka4K1MaqeQwXM,3428
18
+ QuizGenerator/premade_questions/basic.py,sha256=hZCZHZxyTAQSDr-Hwo3xIMlU3L0-tyzfEZqvz7B-P64,3317
19
19
  QuizGenerator/premade_questions/cst334/__init__.py,sha256=BTz-Os1XbwIRKqAilf2UIva2NlY0DbA_XbSIggO2Tdk,36
20
- QuizGenerator/premade_questions/cst334/languages.py,sha256=ENsgpup1uoGIVWEE_puDgLzWicguwdERsm-iiS-sh1E,15129
21
- QuizGenerator/premade_questions/cst334/math_questions.py,sha256=zwkm3OLOkDZ_fbPF1E12UhdF373aQv9QzjBfV1bfbyU,10242
22
- QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=PUyXIoyrGImXhucx6KBgoAEYmQCzTSCz0DWUu_xo6Kc,54371
20
+ QuizGenerator/premade_questions/cst334/languages.py,sha256=t4LEfVeJei0xqOHjUb9vV4yG1Oyp-LE-wmF4AY_oMbk,14909
21
+ QuizGenerator/premade_questions/cst334/math_questions.py,sha256=TW4VR6kmcYC8qMOn5vNwe1_7OCUKjPeOqJbwoKOWZ-s,9930
22
+ QuizGenerator/premade_questions/cst334/memory_questions.py,sha256=S_Fndsq-RqWoFeIuKmMmCrdO7R1dFeQ0HrGWu93RRiE,53749
23
23
  QuizGenerator/premade_questions/cst334/ostep13_vsfs.py,sha256=d9jjrynEw44vupAH_wKl57UoHooCNEJXaC5DoNYualk,16163
24
- QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=pb63H47WlSsHi_nHRVhbwUHeybF2zbWL8vXbwOguAL0,17474
25
- QuizGenerator/premade_questions/cst334/process.py,sha256=aZKbsa9Cvh20HooaRV7CXv5kFAGvTkiWmYpQn6J62Nk,39865
24
+ QuizGenerator/premade_questions/cst334/persistence_questions.py,sha256=eyCIwfy3pmSm-XEwIV-7i9uU0-kZcuq1D0Apm0m2SfA,17163
25
+ QuizGenerator/premade_questions/cst334/process.py,sha256=YrtrLZA8Uk63adpD2WOuXyIaJsFKoO5xZ0yYDzMFWqs,39593
26
26
  QuizGenerator/premade_questions/cst463/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  QuizGenerator/premade_questions/cst463/gradient_descent/__init__.py,sha256=sH2CUV6zK9FT3jWTn453ys6_JTrUKRtZnU8hK6RmImU,240
28
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=PdWAJjgsiwYQsxeLlQiVDd3m97RUjUY1KJTJxyrrdRI,13984
29
- QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=yc1wwqgsFh13peGDRJM74TaWmzQrFg-N7cyqIdf7G54,10874
30
- QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=ftaNkwIPuh-wjAYeAj0YvklS3-xjOcQS7HvUkcNRRYY,22257
31
- QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=sn70FLK_3LnhYhX7TVKB9oDwZMJybQIMtYLzxn5WZxg,2675
28
+ QuizGenerator/premade_questions/cst463/gradient_descent/gradient_calculation.py,sha256=6TzPi-NMI5CILcd3gbzRpEs3T_280SzA1kauTZMXC_g,13607
29
+ QuizGenerator/premade_questions/cst463/gradient_descent/gradient_descent_questions.py,sha256=fEpYB4jFOJfyDdli0oh-zpsarC_w8fIGZBOXCkMOIGQ,10604
30
+ QuizGenerator/premade_questions/cst463/gradient_descent/loss_calculations.py,sha256=Tq_-EVaVmuVz6-V9Wbcp-oIlc2BJ5ehN4MdCx8G_Dk4,21824
31
+ QuizGenerator/premade_questions/cst463/gradient_descent/misc.py,sha256=0R-nFeD3zsqJyde5CXWrF6Npjmpx6_HbzfCbThLi3os,2657
32
32
  QuizGenerator/premade_questions/cst463/math_and_data/__init__.py,sha256=EbIaUrx7_aK9j3Gd8Mk08h9GocTq_0OoNu2trfNwaU8,202
33
- QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=oU8Z36-P92TQ9OhJ2XK6ARlyM8I_qvp1guZLYVvU_l8,29116
34
- QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=3NCgxtU0OxGrnixBOyrGKObf2rOWwXuBv4TWfiC9jxQ,13226
33
+ QuizGenerator/premade_questions/cst463/math_and_data/matrix_questions.py,sha256=_oODuW4WMEVLuEJFmVjMWYTZn3RiE2MqlJSEIwRXNMo,28740
34
+ QuizGenerator/premade_questions/cst463/math_and_data/vector_questions.py,sha256=nNu4Pqvy-zXO2Ly9UtqXIbx-gw2fUC40yii-PhuK-_c,12927
35
35
  QuizGenerator/premade_questions/cst463/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
36
- QuizGenerator/premade_questions/cst463/models/attention.py,sha256=e_iPxGJUAXQ89zk8BXG9r1sGFKOJhBr4UaWaDQV6I7g,6100
37
- QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=8Z3vP6A-CxRP80C7pb9gWJ2SctDh4X5fy6V5Gzx31Pc,6337
36
+ QuizGenerator/premade_questions/cst463/models/attention.py,sha256=i49u_bTbc_x0uLkmO-w7zQtLeoBpejVd78opVUnP9sg,5803
37
+ QuizGenerator/premade_questions/cst463/models/cnns.py,sha256=yFqZJ-sKInGaFButUEuYiRRZkALqx8Tpv8I-fTfvQcU,6109
38
38
  QuizGenerator/premade_questions/cst463/models/matrices.py,sha256=H61_8cF1DGCt4Z4Ssoi4SMClf6tD5wHkOqY5bMdsSt4,699
39
- QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=hHe-7npGmoUpYYVy1gIlyTTBk8ra3Muw0yHxa3FrbS8,7291
40
- QuizGenerator/premade_questions/cst463/models/text.py,sha256=bwEebpYnHn52yCrzJGCNz8ebWIU03fdedI9JQmQ9I4I,6978
41
- QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=oXyDKQvfLdWE_-cd2CY9m6t3SI_s-SQGXP6zDSoGINI,7273
39
+ QuizGenerator/premade_questions/cst463/models/rnns.py,sha256=CXoXHMNIhJRD2MEZ0WNemTKQLQRV7ahTnGVRjOe77vU,6903
40
+ QuizGenerator/premade_questions/cst463/models/text.py,sha256=vf9Lp9dvYs4Eok_ljt5MEw94O7YdhpP0JaDfDaPL0Vs,6705
41
+ QuizGenerator/premade_questions/cst463/models/weight_counting.py,sha256=wz63jKluW3sFYOWUePgtcNQVpFJykyG3BxJzt0djrvY,7133
42
42
  QuizGenerator/premade_questions/cst463/neural-network-basics/__init__.py,sha256=pmyCezO-20AFEQC6MR7KnAsaU9TcgZYsGQOMVkRZ-U8,149
43
- QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=luWlTfj1UM1yQDQzs_tNzTV67qXhRUBwNt8QrV74XHs,46115
43
+ QuizGenerator/premade_questions/cst463/neural-network-basics/neural_network_questions.py,sha256=LTsUZqdP5zdG8VB10u5FYkQMm2Q1wvlaFAOOVQQesmg,45176
44
44
  QuizGenerator/premade_questions/cst463/tensorflow-intro/__init__.py,sha256=G1gEHtG4KakYgi8ZXSYYhX6bQRtnm2tZVGx36d63Nmo,173
45
- QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=8Wo38kTd_n0Oau2ERpvcudB9uJiOVDYYQNeWu9v4Tyo,33516
46
- quizgenerator-0.6.3.dist-info/METADATA,sha256=BfoHO7-H8rocvQsKEeyHig-wwhhb7VoLx6yMEPaZj-Q,7212
47
- quizgenerator-0.6.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
- quizgenerator-0.6.3.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
49
- quizgenerator-0.6.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
50
- quizgenerator-0.6.3.dist-info/RECORD,,
45
+ QuizGenerator/premade_questions/cst463/tensorflow-intro/tensorflow_questions.py,sha256=KcYSDQQfbZcwNJQrBbcAmqIp69pYro8bJAWa1djDGsE,32263
46
+ quizgenerator-0.7.1.dist-info/METADATA,sha256=OvjwBmgGzYvr9VhlQ2zgi-UzKdCY7qRn-h6VjmZZ4r4,7212
47
+ quizgenerator-0.7.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
48
+ quizgenerator-0.7.1.dist-info/entry_points.txt,sha256=aOIdRdw26xY8HkxOoKHBnUPe2mwGv5Ti3U1zojb6zxQ,98
49
+ quizgenerator-0.7.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
50
+ quizgenerator-0.7.1.dist-info/RECORD,,