Coverage for src / lexigram / admin / schema / text_area.py: 36%

72 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 22:14 +0800

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from typing import Any 

5 

6from lexigram.admin.schema.text import TextField 

7from lexigram.ui import Element, MarkdownEditor, RichEditor, TextArea, raw 

8 

9 

10@dataclass(frozen=True, kw_only=True) 

11class TextAreaField(TextField): 

12 """A multi-line text input field. 

13 

14 Renders a ``<textarea>`` form control. Column display truncates 

15 long content at 200 characters. 

16 """ 

17 

18 rows: int = 5 

19 

20 def render_form( 

21 self, value: str | None, *, errors: list[str] | None = None 

22 ) -> Element: 

23 kwargs: dict[str, Any] = { 

24 "name": self.name, 

25 "value": value if value is not None else "", 

26 "rows": self.rows, 

27 } 

28 if self.placeholder is not None: 

29 kwargs["placeholder"] = self.placeholder 

30 if self.label is not None: 

31 kwargs["label"] = self.label 

32 if errors: 

33 kwargs["error"] = errors[0] 

34 return TextArea(**kwargs).render() 

35 

36 def render_column(self, record: Any, value: str | None) -> Element: 

37 if value is None: 

38 return Element("span", "\u2014", class_="text-muted") 

39 if len(value) > 200: 

40 return Element("span", value[:200] + "...") 

41 return Element("span", value) 

42 

43 

44@dataclass(frozen=True, kw_only=True) 

45class MarkdownField(TextAreaField): 

46 """A markdown editor field with optional preview toggle. 

47 

48 Renders a ``MarkdownEditor`` form control with Alpine.js-powered 

49 preview mode switching. Column display strips markdown syntax 

50 for plain-text preview. 

51 """ 

52 

53 preview: bool = True 

54 min_height: int = 300 

55 

56 def render_form( 

57 self, value: str | None, *, errors: list[str] | None = None 

58 ) -> Element: 

59 kwargs: dict[str, Any] = { 

60 "name": self.name, 

61 "value": value if value is not None else "", 

62 "rows": self.rows, 

63 "preview": self.preview, 

64 "min_height": self.min_height, 

65 } 

66 if self.placeholder is not None: 

67 kwargs["placeholder"] = self.placeholder 

68 if self.label is not None: 

69 kwargs["label"] = self.label 

70 if errors: 

71 kwargs["error"] = errors[0] 

72 return MarkdownEditor(**kwargs).render() 

73 

74 def render_column(self, record: Any, value: str | None) -> Element: 

75 if value is None: 

76 return Element("span", "\u2014", class_="text-muted") 

77 text = value 

78 text = text.replace("**", "").replace("__", "") 

79 text = text.replace("*", "").replace("_", "") 

80 text = text.replace("```", "").replace("`", "") 

81 text = text.replace("#", "") 

82 text = text.replace("> ", "") 

83 text = text.replace("[", "").replace("]", "") 

84 text = text.replace("(", "").replace(")", "") 

85 return Element("span", text.strip()) 

86 

87 

88@dataclass(frozen=True, kw_only=True) 

89class RichTextField(TextAreaField): 

90 """A WYSIWYG rich-text field. 

91 

92 Renders a ``RichEditor`` form control backed by Trix.js. 

93 Column display renders the HTML value as trusted (unescaped) 

94 content. 

95 

96 Use ``render_assets()`` to include the required Trix.js CSS and 

97 JS when this field appears on a page. 

98 """ 

99 

100 toolbar: str | None = None 

101 min_height: int = 300 

102 

103 _trix_css_url: str = "https://unpkg.com/trix@2.0.8/dist/trix.css" 

104 _trix_js_url: str = "https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js" 

105 

106 @classmethod 

107 def render_assets(cls) -> Element: 

108 """Render the Trix.js CSS and ``<script>`` tags. 

109 

110 Include the result in ``<head>`` once per page to enable 

111 RichEditor instances. 

112 """ 

113 return Element( 

114 "div", 

115 Element( 

116 "link", 

117 rel="stylesheet", 

118 href=cls._trix_css_url, 

119 ), 

120 Element( 

121 "script", 

122 src=cls._trix_js_url, 

123 defer="", 

124 ), 

125 ) 

126 

127 def render_form( 

128 self, value: str | None, *, errors: list[str] | None = None 

129 ) -> Element: 

130 kwargs: dict[str, Any] = { 

131 "name": self.name, 

132 "value": value if value is not None else "", 

133 "min_height": self.min_height, 

134 } 

135 if self.placeholder is not None: 

136 kwargs["placeholder"] = self.placeholder 

137 if self.label is not None: 

138 kwargs["label"] = self.label 

139 if self.toolbar is not None: 

140 kwargs["toolbar"] = self.toolbar 

141 if errors: 

142 kwargs["error"] = errors[0] 

143 return RichEditor(**kwargs).render() 

144 

145 def render_column(self, record: Any, value: str | None) -> Element: 

146 if value is None: 

147 return Element("span", "\u2014", class_="text-muted") 

148 return Element("div", raw(value))