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

1""" 

2Limits for question bodies that may contain rich HTML (editors, MathLive, MathML). 

3 

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""" 

8 

9from __future__ import annotations 

10 

11import re 

12from typing import Final 

13 

14from fastapi import HTTPException, status 

15 

16PLAIN_TEXT_SHORT_MAX: Final[int] = 1000 

17RICH_EDITOR_HTML_MAX: Final[int] = 500_000 

18 

19_BASE64_IMG = re.compile(r"data:image/[^;]+;base64,[A-Za-z0-9+/=]+") 

20 

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) 

44 

45 

46def has_embedded_base64_image(s: str) -> bool: 

47 return isinstance(s, str) and bool(_BASE64_IMG.search(s)) 

48 

49 

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 

64 

65 

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