Coverage for server / validators / question_richtext.py: 87%
31 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-04 09:33 +0000
1"""
2Limits for question bodies that may contain rich HTML (editors, MathLive, MathML).
4Plain stems stay on the legacy short cap. Content from TinyMCE / MathLive (including
5rendered formula DOM) uses a much larger cap so `data-latex` + nested spans can be
6persisted.
7"""
9from __future__ import annotations
11import re
12from typing import Final
14from fastapi import HTTPException, status
16PLAIN_TEXT_SHORT_MAX: Final[int] = 1000
17RICH_EDITOR_HTML_MAX: Final[int] = 500_000
19_BASE64_IMG = re.compile(r"data:image/[^;]+;base64,[A-Za-z0-9+/=]+")
21_RICH_MARKERS: Final[tuple[str, ...]] = (
22 "mfe-formula",
23 "mfe-",
24 "data-latex",
25 "ml__",
26 "math-field",
27 "<math",
28 "katex",
29 "graph2d-embed",
30 "<p",
31 "<span",
32 "<div",
33 "<br",
34 "<img",
35 "<sub",
36 "<sup",
37 "<table",
38 "<strong",
39 "<em",
40 "<ul",
41 "<ol",
42 "<li",
43)
46def has_embedded_base64_image(s: str) -> bool:
47 return isinstance(s, str) and bool(_BASE64_IMG.search(s))
50def html_allows_large_content(s: str) -> bool:
51 """
52 True when `s` should use ``RICH_EDITOR_HTML_MAX`` instead of the plain cap.
53 """
54 if not isinstance(s, str) or not s:
55 return False
56 if has_embedded_base64_image(s):
57 return True
58 low = s.lower()
59 if any(marker in low for marker in _RICH_MARKERS):
60 return True
61 if "\\(" in s or "\\[" in s or "$$" in s:
62 return True
63 return False
66def enforce_text_length(s: str, field_label: str) -> str:
67 """Apply the plain-vs-rich character cap. Empty-string checks stay in the caller."""
68 if not isinstance(s, str):
69 return s
70 if html_allows_large_content(s):
71 if len(s) > RICH_EDITOR_HTML_MAX:
72 raise HTTPException(
73 status.HTTP_400_BAD_REQUEST,
74 detail=(
75 f"{field_label} should not exceed "
76 f"{RICH_EDITOR_HTML_MAX:,} characters when containing "
77 "embedded images, MathLive formulas, or editor HTML"
78 ),
79 )
80 return s
81 if len(s.strip()) > PLAIN_TEXT_SHORT_MAX:
82 raise HTTPException(
83 status.HTTP_400_BAD_REQUEST,
84 detail=(
85 f"{field_label} should not exceed "
86 f"{PLAIN_TEXT_SHORT_MAX} characters (send formula HTML with "
87 "`mfe-formula`, `data-latex`, MathLive markup, or editor tags "
88 "for a larger limit)"
89 ),
90 )
91 return s