violit 0.0.4__py3-none-any.whl → 0.0.5__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,413 +1,413 @@
1
- """Text widgets"""
2
-
3
- from typing import Union, Callable, Optional
4
- from ..component import Component
5
- from ..context import rendering_ctx
6
-
7
-
8
- class TextWidgetsMixin:
9
- def write(self, *args, tag: Optional[str] = "div", unsafe_allow_html: bool = False, **props):
10
- """Display content with automatic type detection"""
11
- from ..state import State
12
- import re
13
- import json
14
- import html as html_lib
15
-
16
- cid = self._get_next_cid("comp")
17
-
18
- def builder():
19
- def _has_markdown(text: str) -> bool:
20
- """Check if text contains markdown syntax"""
21
- markdown_patterns = [
22
- r'^#{1,6}\s', # Headers: # ## ###
23
- r'\*\*[^*]+\*\*', # Bold: **text**
24
- r'(?<!\*)\*[^*\n]+\*', # Italic: *text*
25
- r'`[^`]+`', # Code: `text`
26
- r'\[.+?\]\(.+?\)', # Links: [text](url)
27
- r'^[-*]\s', # Lists: - or *
28
- r'^\d+\.\s', # Numbered lists: 1. 2.
29
- ]
30
- for pattern in markdown_patterns:
31
- if re.search(pattern, text, re.MULTILINE):
32
- return True
33
- return False
34
-
35
- # Set rendering context once for entire builder
36
- token = rendering_ctx.set(cid)
37
- parts = []
38
-
39
- try:
40
- for arg in args:
41
- current_value = arg
42
-
43
- # State object: read value (registers dependency)
44
- if isinstance(arg, State):
45
- current_value = arg.value
46
-
47
- # Callable/Lambda: execute (registers dependency)
48
- elif callable(arg):
49
- current_value = arg()
50
-
51
- # DataFrame (pandas)
52
- try:
53
- import pandas as pd
54
- if isinstance(current_value, pd.DataFrame):
55
- parts.append(self._render_dataframe_html(current_value))
56
- continue
57
- except (ImportError, AttributeError):
58
- pass
59
-
60
- # Dict or List → JSON
61
- if isinstance(current_value, (dict, list, tuple)):
62
- json_str = json.dumps(current_value, indent=2, ensure_ascii=False)
63
- parts.append(f'<pre style="background:var(--sl-bg-card);padding:1rem;border-radius:0.5rem;border:1px solid var(--sl-border);overflow-x:auto;"><code style="color:var(--sl-text);font-family:monospace;">{html_lib.escape(json_str)}</code></pre>')
64
- continue
65
-
66
- # String with markdown → render as markdown
67
- text = str(current_value)
68
- if _has_markdown(text):
69
- parts.append(self._render_markdown(text))
70
- else:
71
- # Plain text
72
- parts.append(text)
73
-
74
- # Join all parts
75
- content = " ".join(parts)
76
-
77
- # Check if any HTML in content
78
- has_html = '<' in content and '>' in content
79
- return Component(tag, id=cid, content=content, escape_content=not (has_html or unsafe_allow_html), **props)
80
-
81
- finally:
82
- rendering_ctx.reset(token)
83
-
84
- self._register_component(cid, builder)
85
-
86
- def _render_markdown(self, text: str) -> str:
87
- """Render markdown to HTML (internal helper)"""
88
- import re
89
- lines = text.split('\n')
90
- result = []
91
- i = 0
92
-
93
- while i < len(lines):
94
- line = lines[i]
95
- stripped = line.strip()
96
-
97
- # Headers
98
- if stripped.startswith('### '):
99
- result.append(f'<h3>{stripped[4:]}</h3>')
100
- i += 1
101
- elif stripped.startswith('## '):
102
- result.append(f'<h2>{stripped[3:]}</h2>')
103
- i += 1
104
- elif stripped.startswith('# '):
105
- result.append(f'<h1>{stripped[2:]}</h1>')
106
- i += 1
107
- # Unordered lists
108
- elif stripped.startswith(('- ', '* ')):
109
- list_items = []
110
- while i < len(lines):
111
- curr = lines[i].strip()
112
- if curr.startswith(('- ', '* ')):
113
- list_items.append(f'<li>{curr[2:]}</li>')
114
- i += 1
115
- elif not curr:
116
- i += 1
117
- break
118
- else:
119
- break
120
- result.append('<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ul>')
121
- # Ordered lists
122
- elif re.match(r'^\d+\.\s', stripped):
123
- list_items = []
124
- while i < len(lines):
125
- curr = lines[i].strip()
126
- if re.match(r'^\d+\.\s', curr):
127
- clean_item = re.sub(r'^\d+\.\s', '', curr)
128
- list_items.append(f'<li>{clean_item}</li>')
129
- i += 1
130
- elif not curr:
131
- i += 1
132
- break
133
- else:
134
- break
135
- result.append('<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ol>')
136
- # Empty line
137
- elif not stripped:
138
- result.append('<br>')
139
- i += 1
140
- # Regular text
141
- else:
142
- result.append(line)
143
- i += 1
144
-
145
- html = '\n'.join(result)
146
-
147
- # Inline elements
148
- html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
149
- html = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<em>\1</em>', html)
150
- html = re.sub(r'`(.+?)`', r'<code style="background:var(--sl-bg-card);padding:0.2em 0.4em;border-radius:3px;">\1</code>', html)
151
- html = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2" style="color:var(--sl-primary);">\1</a>', html)
152
-
153
- return html
154
-
155
- def _render_dataframe_html(self, df) -> str:
156
- """Render pandas DataFrame as HTML table (internal helper)"""
157
- # Use pandas to_html with custom styling
158
- html = df.to_html(
159
- index=True,
160
- escape=True,
161
- classes='dataframe',
162
- border=0
163
- )
164
-
165
- # Add custom styling
166
- styled_html = f'''
167
- <div style="overflow-x: auto; margin: 1rem 0;">
168
- <style>
169
- .dataframe {{
170
- border-collapse: collapse;
171
- width: 100%;
172
- font-size: 0.9rem;
173
- }}
174
- .dataframe th {{
175
- background: var(--sl-color-primary-600);
176
- color: white;
177
- padding: 0.75rem;
178
- text-align: left;
179
- font-weight: 600;
180
- }}
181
- .dataframe td {{
182
- padding: 0.5rem 0.75rem;
183
- border-bottom: 1px solid var(--sl-color-neutral-200);
184
- }}
185
- .dataframe tr:hover {{
186
- background: var(--sl-color-neutral-50);
187
- }}
188
- </style>
189
- {html}
190
- </div>
191
- '''
192
- return styled_html
193
-
194
- def heading(self, text, level: int = 1, divider: bool = False):
195
- """Display heading (h1-h6)"""
196
- from ..state import State
197
- import html as html_lib
198
-
199
- cid = self._get_next_cid("heading")
200
- def builder():
201
- token = rendering_ctx.set(cid)
202
- if isinstance(text, State):
203
- content = text.value
204
- elif callable(text):
205
- content = text()
206
- else:
207
- content = text
208
- rendering_ctx.reset(token)
209
-
210
- # XSS protection: escape content
211
- escaped_content = html_lib.escape(str(content))
212
-
213
- grad = "gradient-text" if level == 1 else ""
214
- html_output = f'<h{level} class="{grad}">{escaped_content}</h{level}>'
215
- if divider: html_output += '<sl-divider class="divider"></sl-divider>'
216
- return Component("div", id=cid, content=html_output)
217
- self._register_component(cid, builder)
218
-
219
- def title(self, text: Union[str, Callable]):
220
- """Display title (h1 with gradient)"""
221
- self.heading(text, level=1, divider=False)
222
-
223
- def header(self, text: Union[str, Callable], divider: bool = True):
224
- """Display header (h2)"""
225
- self.heading(text, level=2, divider=divider)
226
-
227
- def subheader(self, text: Union[str, Callable], divider: bool = False):
228
- """Display subheader (h3)"""
229
- self.heading(text, level=3, divider=divider)
230
-
231
- def text(self, content, size: str = "medium", muted: bool = False):
232
- """Display text paragraph"""
233
- from ..state import State
234
-
235
- cid = self._get_next_cid("text")
236
- def builder():
237
- token = rendering_ctx.set(cid)
238
- if isinstance(content, State):
239
- val = content.value
240
- elif callable(content):
241
- val = content()
242
- else:
243
- val = content
244
- rendering_ctx.reset(token)
245
- cls = f"text-{size} {'text-muted' if muted else ''}"
246
- # XSS protection: enable content escaping
247
- return Component("p", id=cid, content=val, escape_content=True, class_=cls)
248
- self._register_component(cid, builder)
249
-
250
- def caption(self, text: Union[str, Callable]):
251
- """Display caption text (small, muted)"""
252
- self.text(text, size="small", muted=True)
253
-
254
- def markdown(self, text: Union[str, Callable], **props):
255
- """Display markdown-formatted text"""
256
- cid = self._get_next_cid("markdown")
257
- def builder():
258
- token = rendering_ctx.set(cid)
259
- content = text() if callable(text) else text
260
-
261
- # Enhanced markdown conversion - line-by-line processing
262
- import re
263
- lines = content.split('\n')
264
- result = []
265
- i = 0
266
-
267
- while i < len(lines):
268
- line = lines[i]
269
- stripped = line.strip()
270
-
271
- # Headers
272
- if stripped.startswith('### '):
273
- result.append(f'<h3>{stripped[4:]}</h3>')
274
- i += 1
275
- elif stripped.startswith('## '):
276
- result.append(f'<h2>{stripped[3:]}</h2>')
277
- i += 1
278
- elif stripped.startswith('# '):
279
- result.append(f'<h1>{stripped[2:]}</h1>')
280
- i += 1
281
- # Unordered lists
282
- elif stripped.startswith(('- ', '* ')):
283
- list_items = []
284
- while i < len(lines):
285
- curr = lines[i].strip()
286
- if curr.startswith(('- ', '* ')):
287
- list_items.append(f'<li>{curr[2:]}</li>')
288
- i += 1
289
- elif not curr: # Empty line
290
- i += 1
291
- break
292
- else:
293
- break
294
- result.append('<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ul>')
295
- # Ordered lists
296
- elif re.match(r'^\d+\.\s', stripped):
297
- list_items = []
298
- while i < len(lines):
299
- curr = lines[i].strip()
300
- if re.match(r'^\d+\.\s', curr):
301
- clean_item = re.sub(r'^\d+\.\s', '', curr)
302
- list_items.append(f'<li>{clean_item}</li>')
303
- i += 1
304
- elif not curr: # Empty line
305
- i += 1
306
- break
307
- else:
308
- break
309
- result.append('<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ol>')
310
- # Empty line
311
- elif not stripped:
312
- result.append('<br>')
313
- i += 1
314
- # Regular text
315
- else:
316
- result.append(line)
317
- i += 1
318
-
319
- html = '\n'.join(result)
320
-
321
- # Inline elements (bold, italic, code, links)
322
- # Bold **text** (before italic to avoid conflicts)
323
- html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
324
- # Italic *text* (avoid matching list markers)
325
- html = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<em>\1</em>', html)
326
- # Code `text`
327
- html = re.sub(r'`(.+?)`', r'<code style="background:var(--sl-bg-card);padding:0.2em 0.4em;border-radius:3px;">\1</code>', html)
328
- # Links [text](url)
329
- html = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2" style="color:var(--sl-primary);">\1</a>', html)
330
-
331
- rendering_ctx.reset(token)
332
- return Component("div", id=cid, content=html, class_="markdown", **props)
333
- self._register_component(cid, builder)
334
-
335
- def html(self, html_content: Union[str, Callable], **props):
336
- """Display raw HTML content
337
-
338
- Use this when you need to render HTML directly without markdown processing.
339
- For markdown formatting, use app.markdown() instead.
340
-
341
- Example:
342
- app.html('<div class="custom">Hello</div>')
343
- """
344
- cid = self._get_next_cid("html")
345
- def builder():
346
- token = rendering_ctx.set(cid)
347
- content = html_content() if callable(html_content) else html_content
348
- rendering_ctx.reset(token)
349
- return Component("div", id=cid, content=content, **props)
350
- self._register_component(cid, builder)
351
-
352
- def code(self, code: Union[str, Callable], language: Optional[str] = None, **props):
353
- """Display code block with syntax highlighting"""
354
- import html as html_lib
355
-
356
- cid = self._get_next_cid("code")
357
- def builder():
358
- token = rendering_ctx.set(cid)
359
- code_text = code() if callable(code) else code
360
- rendering_ctx.reset(token)
361
-
362
- # XSS protection: escape code content
363
- escaped_code = html_lib.escape(str(code_text))
364
-
365
- lang_class = f"language-{language}" if language else ""
366
- html_output = f'''
367
- <pre style="background:var(--sl-bg-card);padding:1rem;border-radius:0.5rem;border:1px solid var(--sl-border);overflow-x:auto;">
368
- <code class="{lang_class}" style="color:var(--sl-text);font-family:monospace;">{escaped_code}</code>
369
- </pre>
370
- '''
371
- return Component("div", id=cid, content=html_output, **props)
372
- self._register_component(cid, builder)
373
-
374
- def html(self, html_content: Union[str, Callable], **props):
375
- """Render raw HTML"""
376
- cid = self._get_next_cid("html")
377
- def builder():
378
- token = rendering_ctx.set(cid)
379
- content = html_content() if callable(html_content) else html_content
380
- rendering_ctx.reset(token)
381
- return Component("div", id=cid, content=content, **props)
382
- self._register_component(cid, builder)
383
-
384
- def divider(self):
385
- """Display horizontal divider"""
386
- cid = self._get_next_cid("divider")
387
- def builder():
388
- return Component("sl-divider", id=cid, class_="divider")
389
- self._register_component(cid, builder)
390
-
391
- def success(self, body, icon="check-circle"):
392
- """Display success message"""
393
- self._alert(body, "success", icon)
394
-
395
- def info(self, body, icon="info-circle"):
396
- """Display info message"""
397
- self._alert(body, "primary", icon)
398
-
399
- def warning(self, body, icon="exclamation-triangle"):
400
- """Display warning message"""
401
- self._alert(body, "warning", icon)
402
-
403
- def error(self, body, icon="exclamation-octagon"):
404
- """Display error message"""
405
- self._alert(body, "danger", icon)
406
-
407
- def _alert(self, body, variant, icon_name):
408
- cid = self._get_next_cid("alert")
409
- def builder():
410
- icon_html = f'<sl-icon slot="icon" name="{icon_name}"></sl-icon>'
411
- html = f'<sl-alert variant="{variant}" open style="margin-bottom:1rem;">{icon_html}{body}</sl-alert>'
412
- return Component("div", id=cid, content=html)
413
- self._register_component(cid, builder)
1
+ """Text widgets"""
2
+
3
+ from typing import Union, Callable, Optional
4
+ from ..component import Component
5
+ from ..context import rendering_ctx
6
+
7
+
8
+ class TextWidgetsMixin:
9
+ def write(self, *args, tag: Optional[str] = "div", unsafe_allow_html: bool = False, **props):
10
+ """Display content with automatic type detection"""
11
+ from ..state import State
12
+ import re
13
+ import json
14
+ import html as html_lib
15
+
16
+ cid = self._get_next_cid("comp")
17
+
18
+ def builder():
19
+ def _has_markdown(text: str) -> bool:
20
+ """Check if text contains markdown syntax"""
21
+ markdown_patterns = [
22
+ r'^#{1,6}\s', # Headers: # ## ###
23
+ r'\*\*[^*]+\*\*', # Bold: **text**
24
+ r'(?<!\*)\*[^*\n]+\*', # Italic: *text*
25
+ r'`[^`]+`', # Code: `text`
26
+ r'\[.+?\]\(.+?\)', # Links: [text](url)
27
+ r'^[-*]\s', # Lists: - or *
28
+ r'^\d+\.\s', # Numbered lists: 1. 2.
29
+ ]
30
+ for pattern in markdown_patterns:
31
+ if re.search(pattern, text, re.MULTILINE):
32
+ return True
33
+ return False
34
+
35
+ # Set rendering context once for entire builder
36
+ token = rendering_ctx.set(cid)
37
+ parts = []
38
+
39
+ try:
40
+ for arg in args:
41
+ current_value = arg
42
+
43
+ # State object: read value (registers dependency)
44
+ if isinstance(arg, State):
45
+ current_value = arg.value
46
+
47
+ # Callable/Lambda: execute (registers dependency)
48
+ elif callable(arg):
49
+ current_value = arg()
50
+
51
+ # DataFrame (pandas)
52
+ try:
53
+ import pandas as pd
54
+ if isinstance(current_value, pd.DataFrame):
55
+ parts.append(self._render_dataframe_html(current_value))
56
+ continue
57
+ except (ImportError, AttributeError):
58
+ pass
59
+
60
+ # Dict or List → JSON
61
+ if isinstance(current_value, (dict, list, tuple)):
62
+ json_str = json.dumps(current_value, indent=2, ensure_ascii=False)
63
+ parts.append(f'<pre style="background:var(--sl-bg-card);padding:1rem;border-radius:0.5rem;border:1px solid var(--sl-border);overflow-x:auto;"><code style="color:var(--sl-text);font-family:monospace;">{html_lib.escape(json_str)}</code></pre>')
64
+ continue
65
+
66
+ # String with markdown → render as markdown
67
+ text = str(current_value)
68
+ if _has_markdown(text):
69
+ parts.append(self._render_markdown(text))
70
+ else:
71
+ # Plain text
72
+ parts.append(text)
73
+
74
+ # Join all parts
75
+ content = " ".join(parts)
76
+
77
+ # Check if any HTML in content
78
+ has_html = '<' in content and '>' in content
79
+ return Component(tag, id=cid, content=content, escape_content=not (has_html or unsafe_allow_html), **props)
80
+
81
+ finally:
82
+ rendering_ctx.reset(token)
83
+
84
+ self._register_component(cid, builder)
85
+
86
+ def _render_markdown(self, text: str) -> str:
87
+ """Render markdown to HTML (internal helper)"""
88
+ import re
89
+ lines = text.split('\n')
90
+ result = []
91
+ i = 0
92
+
93
+ while i < len(lines):
94
+ line = lines[i]
95
+ stripped = line.strip()
96
+
97
+ # Headers
98
+ if stripped.startswith('### '):
99
+ result.append(f'<h3>{stripped[4:]}</h3>')
100
+ i += 1
101
+ elif stripped.startswith('## '):
102
+ result.append(f'<h2>{stripped[3:]}</h2>')
103
+ i += 1
104
+ elif stripped.startswith('# '):
105
+ result.append(f'<h1>{stripped[2:]}</h1>')
106
+ i += 1
107
+ # Unordered lists
108
+ elif stripped.startswith(('- ', '* ')):
109
+ list_items = []
110
+ while i < len(lines):
111
+ curr = lines[i].strip()
112
+ if curr.startswith(('- ', '* ')):
113
+ list_items.append(f'<li>{curr[2:]}</li>')
114
+ i += 1
115
+ elif not curr:
116
+ i += 1
117
+ break
118
+ else:
119
+ break
120
+ result.append('<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ul>')
121
+ # Ordered lists
122
+ elif re.match(r'^\d+\.\s', stripped):
123
+ list_items = []
124
+ while i < len(lines):
125
+ curr = lines[i].strip()
126
+ if re.match(r'^\d+\.\s', curr):
127
+ clean_item = re.sub(r'^\d+\.\s', '', curr)
128
+ list_items.append(f'<li>{clean_item}</li>')
129
+ i += 1
130
+ elif not curr:
131
+ i += 1
132
+ break
133
+ else:
134
+ break
135
+ result.append('<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ol>')
136
+ # Empty line
137
+ elif not stripped:
138
+ result.append('<br>')
139
+ i += 1
140
+ # Regular text
141
+ else:
142
+ result.append(line)
143
+ i += 1
144
+
145
+ html = '\n'.join(result)
146
+
147
+ # Inline elements
148
+ html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
149
+ html = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<em>\1</em>', html)
150
+ html = re.sub(r'`(.+?)`', r'<code style="background:var(--sl-bg-card);padding:0.2em 0.4em;border-radius:3px;">\1</code>', html)
151
+ html = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2" style="color:var(--sl-primary);">\1</a>', html)
152
+
153
+ return html
154
+
155
+ def _render_dataframe_html(self, df) -> str:
156
+ """Render pandas DataFrame as HTML table (internal helper)"""
157
+ # Use pandas to_html with custom styling
158
+ html = df.to_html(
159
+ index=True,
160
+ escape=True,
161
+ classes='dataframe',
162
+ border=0
163
+ )
164
+
165
+ # Add custom styling
166
+ styled_html = f'''
167
+ <div style="overflow-x: auto; margin: 1rem 0;">
168
+ <style>
169
+ .dataframe {{
170
+ border-collapse: collapse;
171
+ width: 100%;
172
+ font-size: 0.9rem;
173
+ }}
174
+ .dataframe th {{
175
+ background: var(--sl-color-primary-600);
176
+ color: white;
177
+ padding: 0.75rem;
178
+ text-align: left;
179
+ font-weight: 600;
180
+ }}
181
+ .dataframe td {{
182
+ padding: 0.5rem 0.75rem;
183
+ border-bottom: 1px solid var(--sl-color-neutral-200);
184
+ }}
185
+ .dataframe tr:hover {{
186
+ background: var(--sl-color-neutral-50);
187
+ }}
188
+ </style>
189
+ {html}
190
+ </div>
191
+ '''
192
+ return styled_html
193
+
194
+ def heading(self, text, level: int = 1, divider: bool = False):
195
+ """Display heading (h1-h6)"""
196
+ from ..state import State
197
+ import html as html_lib
198
+
199
+ cid = self._get_next_cid("heading")
200
+ def builder():
201
+ token = rendering_ctx.set(cid)
202
+ if isinstance(text, State):
203
+ content = text.value
204
+ elif callable(text):
205
+ content = text()
206
+ else:
207
+ content = text
208
+ rendering_ctx.reset(token)
209
+
210
+ # XSS protection: escape content
211
+ escaped_content = html_lib.escape(str(content))
212
+
213
+ grad = "gradient-text" if level == 1 else ""
214
+ html_output = f'<h{level} class="{grad}">{escaped_content}</h{level}>'
215
+ if divider: html_output += '<sl-divider class="divider"></sl-divider>'
216
+ return Component("div", id=cid, content=html_output)
217
+ self._register_component(cid, builder)
218
+
219
+ def title(self, text: Union[str, Callable]):
220
+ """Display title (h1 with gradient)"""
221
+ self.heading(text, level=1, divider=False)
222
+
223
+ def header(self, text: Union[str, Callable], divider: bool = True):
224
+ """Display header (h2)"""
225
+ self.heading(text, level=2, divider=divider)
226
+
227
+ def subheader(self, text: Union[str, Callable], divider: bool = False):
228
+ """Display subheader (h3)"""
229
+ self.heading(text, level=3, divider=divider)
230
+
231
+ def text(self, content, size: str = "medium", muted: bool = False):
232
+ """Display text paragraph"""
233
+ from ..state import State
234
+
235
+ cid = self._get_next_cid("text")
236
+ def builder():
237
+ token = rendering_ctx.set(cid)
238
+ if isinstance(content, State):
239
+ val = content.value
240
+ elif callable(content):
241
+ val = content()
242
+ else:
243
+ val = content
244
+ rendering_ctx.reset(token)
245
+ cls = f"text-{size} {'text-muted' if muted else ''}"
246
+ # XSS protection: enable content escaping
247
+ return Component("p", id=cid, content=val, escape_content=True, class_=cls)
248
+ self._register_component(cid, builder)
249
+
250
+ def caption(self, text: Union[str, Callable]):
251
+ """Display caption text (small, muted)"""
252
+ self.text(text, size="small", muted=True)
253
+
254
+ def markdown(self, text: Union[str, Callable], **props):
255
+ """Display markdown-formatted text"""
256
+ cid = self._get_next_cid("markdown")
257
+ def builder():
258
+ token = rendering_ctx.set(cid)
259
+ content = text() if callable(text) else text
260
+
261
+ # Enhanced markdown conversion - line-by-line processing
262
+ import re
263
+ lines = content.split('\n')
264
+ result = []
265
+ i = 0
266
+
267
+ while i < len(lines):
268
+ line = lines[i]
269
+ stripped = line.strip()
270
+
271
+ # Headers
272
+ if stripped.startswith('### '):
273
+ result.append(f'<h3>{stripped[4:]}</h3>')
274
+ i += 1
275
+ elif stripped.startswith('## '):
276
+ result.append(f'<h2>{stripped[3:]}</h2>')
277
+ i += 1
278
+ elif stripped.startswith('# '):
279
+ result.append(f'<h1>{stripped[2:]}</h1>')
280
+ i += 1
281
+ # Unordered lists
282
+ elif stripped.startswith(('- ', '* ')):
283
+ list_items = []
284
+ while i < len(lines):
285
+ curr = lines[i].strip()
286
+ if curr.startswith(('- ', '* ')):
287
+ list_items.append(f'<li>{curr[2:]}</li>')
288
+ i += 1
289
+ elif not curr: # Empty line
290
+ i += 1
291
+ break
292
+ else:
293
+ break
294
+ result.append('<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ul>')
295
+ # Ordered lists
296
+ elif re.match(r'^\d+\.\s', stripped):
297
+ list_items = []
298
+ while i < len(lines):
299
+ curr = lines[i].strip()
300
+ if re.match(r'^\d+\.\s', curr):
301
+ clean_item = re.sub(r'^\d+\.\s', '', curr)
302
+ list_items.append(f'<li>{clean_item}</li>')
303
+ i += 1
304
+ elif not curr: # Empty line
305
+ i += 1
306
+ break
307
+ else:
308
+ break
309
+ result.append('<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ol>')
310
+ # Empty line
311
+ elif not stripped:
312
+ result.append('<br>')
313
+ i += 1
314
+ # Regular text
315
+ else:
316
+ result.append(line)
317
+ i += 1
318
+
319
+ html = '\n'.join(result)
320
+
321
+ # Inline elements (bold, italic, code, links)
322
+ # Bold **text** (before italic to avoid conflicts)
323
+ html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
324
+ # Italic *text* (avoid matching list markers)
325
+ html = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<em>\1</em>', html)
326
+ # Code `text`
327
+ html = re.sub(r'`(.+?)`', r'<code style="background:var(--sl-bg-card);padding:0.2em 0.4em;border-radius:3px;">\1</code>', html)
328
+ # Links [text](url)
329
+ html = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2" style="color:var(--sl-primary);">\1</a>', html)
330
+
331
+ rendering_ctx.reset(token)
332
+ return Component("div", id=cid, content=html, class_="markdown", **props)
333
+ self._register_component(cid, builder)
334
+
335
+ def html(self, html_content: Union[str, Callable], **props):
336
+ """Display raw HTML content
337
+
338
+ Use this when you need to render HTML directly without markdown processing.
339
+ For markdown formatting, use app.markdown() instead.
340
+
341
+ Example:
342
+ app.html('<div class="custom">Hello</div>')
343
+ """
344
+ cid = self._get_next_cid("html")
345
+ def builder():
346
+ token = rendering_ctx.set(cid)
347
+ content = html_content() if callable(html_content) else html_content
348
+ rendering_ctx.reset(token)
349
+ return Component("div", id=cid, content=content, **props)
350
+ self._register_component(cid, builder)
351
+
352
+ def code(self, code: Union[str, Callable], language: Optional[str] = None, **props):
353
+ """Display code block with syntax highlighting"""
354
+ import html as html_lib
355
+
356
+ cid = self._get_next_cid("code")
357
+ def builder():
358
+ token = rendering_ctx.set(cid)
359
+ code_text = code() if callable(code) else code
360
+ rendering_ctx.reset(token)
361
+
362
+ # XSS protection: escape code content
363
+ escaped_code = html_lib.escape(str(code_text))
364
+
365
+ lang_class = f"language-{language}" if language else ""
366
+ html_output = f'''
367
+ <pre style="background:var(--sl-bg-card);padding:1rem;border-radius:0.5rem;border:1px solid var(--sl-border);overflow-x:auto;">
368
+ <code class="{lang_class}" style="color:var(--sl-text);font-family:monospace;">{escaped_code}</code>
369
+ </pre>
370
+ '''
371
+ return Component("div", id=cid, content=html_output, **props)
372
+ self._register_component(cid, builder)
373
+
374
+ def html(self, html_content: Union[str, Callable], **props):
375
+ """Render raw HTML"""
376
+ cid = self._get_next_cid("html")
377
+ def builder():
378
+ token = rendering_ctx.set(cid)
379
+ content = html_content() if callable(html_content) else html_content
380
+ rendering_ctx.reset(token)
381
+ return Component("div", id=cid, content=content, **props)
382
+ self._register_component(cid, builder)
383
+
384
+ def divider(self):
385
+ """Display horizontal divider"""
386
+ cid = self._get_next_cid("divider")
387
+ def builder():
388
+ return Component("sl-divider", id=cid, class_="divider")
389
+ self._register_component(cid, builder)
390
+
391
+ def success(self, body, icon="check-circle"):
392
+ """Display success message"""
393
+ self._alert(body, "success", icon)
394
+
395
+ def info(self, body, icon="info-circle"):
396
+ """Display info message"""
397
+ self._alert(body, "primary", icon)
398
+
399
+ def warning(self, body, icon="exclamation-triangle"):
400
+ """Display warning message"""
401
+ self._alert(body, "warning", icon)
402
+
403
+ def error(self, body, icon="exclamation-octagon"):
404
+ """Display error message"""
405
+ self._alert(body, "danger", icon)
406
+
407
+ def _alert(self, body, variant, icon_name):
408
+ cid = self._get_next_cid("alert")
409
+ def builder():
410
+ icon_html = f'<sl-icon slot="icon" name="{icon_name}"></sl-icon>'
411
+ html = f'<sl-alert variant="{variant}" open style="margin-bottom:1rem;">{icon_html}{body}</sl-alert>'
412
+ return Component("div", id=cid, content=html)
413
+ self._register_component(cid, builder)