violit 0.0.4.post1__py3-none-any.whl → 0.0.6__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.
- violit/app.py +2229 -1988
- violit/component.py +38 -38
- violit/context.py +1 -0
- violit/state.py +234 -0
- violit/widgets/__init__.py +30 -30
- violit/widgets/card_widgets.py +595 -595
- violit/widgets/chart_widgets.py +253 -253
- violit/widgets/data_widgets.py +547 -529
- violit/widgets/input_widgets.py +745 -745
- violit/widgets/layout_widgets.py +419 -419
- violit/widgets/status_widgets.py +308 -255
- violit/widgets/text_widgets.py +458 -413
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/METADATA +1 -1
- violit-0.0.6.dist-info/RECORD +26 -0
- violit-0.0.4.post1.dist-info/RECORD +0 -26
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/WHEEL +0 -0
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {violit-0.0.4.post1.dist-info → violit-0.0.6.dist-info}/top_level.txt +0 -0
violit/widgets/text_widgets.py
CHANGED
|
@@ -1,413 +1,458 @@
|
|
|
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,
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
<
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
.dataframe
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
</
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
self.
|
|
406
|
-
|
|
407
|
-
def
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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, **kwargs):
|
|
10
|
+
"""Magic write: displays arguments based on their type
|
|
11
|
+
|
|
12
|
+
Supported types:
|
|
13
|
+
- Strings, Numbers, State: Rendered as Markdown text
|
|
14
|
+
- Pandas DataFrame/Series: Rendered as interactive table
|
|
15
|
+
- Dict/List: Rendered as JSON tree
|
|
16
|
+
- Matplotlib/Plotly Figures: Rendered as charts
|
|
17
|
+
- Exceptions: Rendered as error trace
|
|
18
|
+
"""
|
|
19
|
+
from ..state import State, ComputedState
|
|
20
|
+
|
|
21
|
+
# Buffer for text-like arguments
|
|
22
|
+
text_buffer = []
|
|
23
|
+
|
|
24
|
+
def flush_buffer():
|
|
25
|
+
if text_buffer:
|
|
26
|
+
self.markdown(*text_buffer)
|
|
27
|
+
text_buffer.clear()
|
|
28
|
+
|
|
29
|
+
for arg in args:
|
|
30
|
+
# Unwrap state for type checking ONLY
|
|
31
|
+
check_val = arg.value if isinstance(arg, (State, ComputedState)) else arg
|
|
32
|
+
|
|
33
|
+
# 1. Pandas DataFrame / Series
|
|
34
|
+
is_df = False
|
|
35
|
+
try:
|
|
36
|
+
import pandas as pd
|
|
37
|
+
if isinstance(check_val, (pd.DataFrame, pd.Series, pd.Index)):
|
|
38
|
+
is_df = True
|
|
39
|
+
except ImportError: pass
|
|
40
|
+
|
|
41
|
+
if is_df:
|
|
42
|
+
flush_buffer()
|
|
43
|
+
if hasattr(self, 'dataframe'):
|
|
44
|
+
self.dataframe(arg)
|
|
45
|
+
else:
|
|
46
|
+
self.markdown(str(arg))
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# 2. Matplotlib Figure
|
|
50
|
+
is_pyplot = False
|
|
51
|
+
try:
|
|
52
|
+
import matplotlib.figure
|
|
53
|
+
if isinstance(check_val, matplotlib.figure.Figure):
|
|
54
|
+
is_pyplot = True
|
|
55
|
+
except ImportError: pass
|
|
56
|
+
|
|
57
|
+
if is_pyplot:
|
|
58
|
+
flush_buffer()
|
|
59
|
+
if hasattr(self, 'pyplot'):
|
|
60
|
+
self.pyplot(arg)
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
# 3. Plotly Figure
|
|
64
|
+
is_plotly = False
|
|
65
|
+
try:
|
|
66
|
+
if hasattr(check_val, 'to_plotly_json'):
|
|
67
|
+
is_plotly = True
|
|
68
|
+
except ImportError: pass
|
|
69
|
+
|
|
70
|
+
if is_plotly:
|
|
71
|
+
flush_buffer()
|
|
72
|
+
if hasattr(self, 'plotly_chart'):
|
|
73
|
+
self.plotly_chart(arg)
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
# 4. Dict / List (JSON)
|
|
77
|
+
if isinstance(check_val, (dict, list)):
|
|
78
|
+
flush_buffer()
|
|
79
|
+
if hasattr(self, 'json'):
|
|
80
|
+
self.json(arg)
|
|
81
|
+
else:
|
|
82
|
+
# Fallback if json widget logic is missing for State
|
|
83
|
+
# But wait, we need to fix json widget too.
|
|
84
|
+
self.json(arg)
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
# 5. Exception
|
|
88
|
+
if isinstance(check_val, Exception):
|
|
89
|
+
flush_buffer()
|
|
90
|
+
if hasattr(self, 'exception'):
|
|
91
|
+
self.exception(arg)
|
|
92
|
+
else:
|
|
93
|
+
self.error(str(arg))
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
# Default: Text-like (str, int, float, State, ComputedState)
|
|
97
|
+
text_buffer.append(arg)
|
|
98
|
+
|
|
99
|
+
# Flush remaining text
|
|
100
|
+
flush_buffer()
|
|
101
|
+
|
|
102
|
+
def _render_markdown(self, text: str) -> str:
|
|
103
|
+
"""Render markdown to HTML (internal helper)"""
|
|
104
|
+
import re
|
|
105
|
+
lines = text.split('\n')
|
|
106
|
+
result = []
|
|
107
|
+
i = 0
|
|
108
|
+
|
|
109
|
+
while i < len(lines):
|
|
110
|
+
line = lines[i]
|
|
111
|
+
stripped = line.strip()
|
|
112
|
+
|
|
113
|
+
# Headers
|
|
114
|
+
if stripped.startswith('### '):
|
|
115
|
+
result.append(f'<h3>{stripped[4:]}</h3>')
|
|
116
|
+
i += 1
|
|
117
|
+
elif stripped.startswith('## '):
|
|
118
|
+
result.append(f'<h2>{stripped[3:]}</h2>')
|
|
119
|
+
i += 1
|
|
120
|
+
elif stripped.startswith('# '):
|
|
121
|
+
result.append(f'<h1>{stripped[2:]}</h1>')
|
|
122
|
+
i += 1
|
|
123
|
+
# Unordered lists
|
|
124
|
+
elif stripped.startswith(('- ', '* ')):
|
|
125
|
+
list_items = []
|
|
126
|
+
while i < len(lines):
|
|
127
|
+
curr = lines[i].strip()
|
|
128
|
+
if curr.startswith(('- ', '* ')):
|
|
129
|
+
list_items.append(f'<li>{curr[2:]}</li>')
|
|
130
|
+
i += 1
|
|
131
|
+
elif not curr:
|
|
132
|
+
i += 1
|
|
133
|
+
break
|
|
134
|
+
else:
|
|
135
|
+
break
|
|
136
|
+
result.append('<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ul>')
|
|
137
|
+
# Ordered lists
|
|
138
|
+
elif re.match(r'^\d+\.\s', stripped):
|
|
139
|
+
list_items = []
|
|
140
|
+
while i < len(lines):
|
|
141
|
+
curr = lines[i].strip()
|
|
142
|
+
if re.match(r'^\d+\.\s', curr):
|
|
143
|
+
clean_item = re.sub(r'^\d+\.\s', '', curr)
|
|
144
|
+
list_items.append(f'<li>{clean_item}</li>')
|
|
145
|
+
i += 1
|
|
146
|
+
elif not curr:
|
|
147
|
+
i += 1
|
|
148
|
+
break
|
|
149
|
+
else:
|
|
150
|
+
break
|
|
151
|
+
result.append('<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ol>')
|
|
152
|
+
# Empty line
|
|
153
|
+
elif not stripped:
|
|
154
|
+
result.append('<br>')
|
|
155
|
+
i += 1
|
|
156
|
+
# Regular text
|
|
157
|
+
else:
|
|
158
|
+
result.append(line)
|
|
159
|
+
i += 1
|
|
160
|
+
|
|
161
|
+
html = '\n'.join(result)
|
|
162
|
+
|
|
163
|
+
# Inline elements
|
|
164
|
+
html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
|
|
165
|
+
html = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<em>\1</em>', html)
|
|
166
|
+
html = re.sub(r'`(.+?)`', r'<code style="background:var(--sl-bg-card);padding:0.2em 0.4em;border-radius:3px;">\1</code>', html)
|
|
167
|
+
html = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2" style="color:var(--sl-primary);">\1</a>', html)
|
|
168
|
+
|
|
169
|
+
return html
|
|
170
|
+
|
|
171
|
+
def _render_dataframe_html(self, df) -> str:
|
|
172
|
+
"""Render pandas DataFrame as HTML table (internal helper)"""
|
|
173
|
+
# Use pandas to_html with custom styling
|
|
174
|
+
html = df.to_html(
|
|
175
|
+
index=True,
|
|
176
|
+
escape=True,
|
|
177
|
+
classes='dataframe',
|
|
178
|
+
border=0
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Add custom styling
|
|
182
|
+
styled_html = f'''
|
|
183
|
+
<div style="overflow-x: auto; margin: 1rem 0;">
|
|
184
|
+
<style>
|
|
185
|
+
.dataframe {{
|
|
186
|
+
border-collapse: collapse;
|
|
187
|
+
width: 100%;
|
|
188
|
+
font-size: 0.9rem;
|
|
189
|
+
}}
|
|
190
|
+
.dataframe th {{
|
|
191
|
+
background: var(--sl-color-primary-600);
|
|
192
|
+
color: white;
|
|
193
|
+
padding: 0.75rem;
|
|
194
|
+
text-align: left;
|
|
195
|
+
font-weight: 600;
|
|
196
|
+
}}
|
|
197
|
+
.dataframe td {{
|
|
198
|
+
padding: 0.5rem 0.75rem;
|
|
199
|
+
border-bottom: 1px solid var(--sl-color-neutral-200);
|
|
200
|
+
}}
|
|
201
|
+
.dataframe tr:hover {{
|
|
202
|
+
background: var(--sl-color-neutral-50);
|
|
203
|
+
}}
|
|
204
|
+
</style>
|
|
205
|
+
{html}
|
|
206
|
+
</div>
|
|
207
|
+
'''
|
|
208
|
+
return styled_html
|
|
209
|
+
|
|
210
|
+
def heading(self, *args, level: int = 1, divider: bool = False):
|
|
211
|
+
"""Display heading (h1-h6)"""
|
|
212
|
+
from ..state import State, ComputedState
|
|
213
|
+
import html as html_lib
|
|
214
|
+
|
|
215
|
+
cid = self._get_next_cid("heading")
|
|
216
|
+
def builder():
|
|
217
|
+
token = rendering_ctx.set(cid)
|
|
218
|
+
|
|
219
|
+
parts = []
|
|
220
|
+
for arg in args:
|
|
221
|
+
if isinstance(arg, (State, ComputedState)):
|
|
222
|
+
parts.append(str(arg.value))
|
|
223
|
+
elif callable(arg):
|
|
224
|
+
parts.append(str(arg()))
|
|
225
|
+
else:
|
|
226
|
+
parts.append(str(arg))
|
|
227
|
+
|
|
228
|
+
content = " ".join(parts)
|
|
229
|
+
rendering_ctx.reset(token)
|
|
230
|
+
|
|
231
|
+
# XSS protection: escape content
|
|
232
|
+
escaped_content = html_lib.escape(str(content))
|
|
233
|
+
|
|
234
|
+
grad = "gradient-text" if level == 1 else ""
|
|
235
|
+
html_output = f'<h{level} class="{grad}">{escaped_content}</h{level}>'
|
|
236
|
+
if divider: html_output += '<sl-divider class="divider"></sl-divider>'
|
|
237
|
+
return Component("div", id=cid, content=html_output)
|
|
238
|
+
self._register_component(cid, builder)
|
|
239
|
+
|
|
240
|
+
def title(self, *args):
|
|
241
|
+
"""Display title (h1 with gradient)"""
|
|
242
|
+
self.heading(*args, level=1, divider=False)
|
|
243
|
+
|
|
244
|
+
def header(self, *args, divider: bool = True):
|
|
245
|
+
"""Display header (h2)"""
|
|
246
|
+
self.heading(*args, level=2, divider=divider)
|
|
247
|
+
|
|
248
|
+
def subheader(self, *args, divider: bool = False):
|
|
249
|
+
"""Display subheader (h3)"""
|
|
250
|
+
self.heading(*args, level=3, divider=divider)
|
|
251
|
+
|
|
252
|
+
def text(self, *args, size: str = "medium", muted: bool = False):
|
|
253
|
+
"""Display text paragraph
|
|
254
|
+
|
|
255
|
+
Supports multiple arguments which will be joined by spaces.
|
|
256
|
+
"""
|
|
257
|
+
from ..state import State, ComputedState
|
|
258
|
+
|
|
259
|
+
cid = self._get_next_cid("text")
|
|
260
|
+
def builder():
|
|
261
|
+
token = rendering_ctx.set(cid)
|
|
262
|
+
|
|
263
|
+
parts = []
|
|
264
|
+
for arg in args:
|
|
265
|
+
if isinstance(arg, (State, ComputedState)):
|
|
266
|
+
parts.append(str(arg.value))
|
|
267
|
+
elif callable(arg):
|
|
268
|
+
parts.append(str(arg()))
|
|
269
|
+
else:
|
|
270
|
+
parts.append(str(arg))
|
|
271
|
+
|
|
272
|
+
val = " ".join(parts)
|
|
273
|
+
rendering_ctx.reset(token)
|
|
274
|
+
|
|
275
|
+
cls = f"text-{size} {'text-muted' if muted else ''}"
|
|
276
|
+
# XSS protection: enable content escaping
|
|
277
|
+
return Component("p", id=cid, content=val, escape_content=True, class_=cls)
|
|
278
|
+
self._register_component(cid, builder)
|
|
279
|
+
|
|
280
|
+
def caption(self, *args):
|
|
281
|
+
"""Display caption text (small, muted)"""
|
|
282
|
+
self.text(*args, size="small", muted=True)
|
|
283
|
+
|
|
284
|
+
def markdown(self, *args, **props):
|
|
285
|
+
"""Display markdown-formatted text
|
|
286
|
+
|
|
287
|
+
Supports multiple arguments which will be joined by spaces.
|
|
288
|
+
"""
|
|
289
|
+
cid = self._get_next_cid("markdown")
|
|
290
|
+
def builder():
|
|
291
|
+
token = rendering_ctx.set(cid)
|
|
292
|
+
from ..state import State, ComputedState
|
|
293
|
+
|
|
294
|
+
parts = []
|
|
295
|
+
for arg in args:
|
|
296
|
+
if isinstance(arg, (State, ComputedState)):
|
|
297
|
+
parts.append(str(arg.value))
|
|
298
|
+
elif callable(arg):
|
|
299
|
+
parts.append(str(arg()))
|
|
300
|
+
else:
|
|
301
|
+
parts.append(str(arg))
|
|
302
|
+
|
|
303
|
+
content = " ".join(parts)
|
|
304
|
+
|
|
305
|
+
# Enhanced markdown conversion - line-by-line processing
|
|
306
|
+
import re
|
|
307
|
+
lines = content.split('\n')
|
|
308
|
+
result = []
|
|
309
|
+
i = 0
|
|
310
|
+
|
|
311
|
+
while i < len(lines):
|
|
312
|
+
line = lines[i]
|
|
313
|
+
stripped = line.strip()
|
|
314
|
+
|
|
315
|
+
# Headers
|
|
316
|
+
if stripped.startswith('### '):
|
|
317
|
+
result.append(f'<h3>{stripped[4:]}</h3>')
|
|
318
|
+
i += 1
|
|
319
|
+
elif stripped.startswith('## '):
|
|
320
|
+
result.append(f'<h2>{stripped[3:]}</h2>')
|
|
321
|
+
i += 1
|
|
322
|
+
elif stripped.startswith('# '):
|
|
323
|
+
result.append(f'<h1>{stripped[2:]}</h1>')
|
|
324
|
+
i += 1
|
|
325
|
+
# Unordered lists
|
|
326
|
+
elif stripped.startswith(('- ', '* ')):
|
|
327
|
+
list_items = []
|
|
328
|
+
while i < len(lines):
|
|
329
|
+
curr = lines[i].strip()
|
|
330
|
+
if curr.startswith(('- ', '* ')):
|
|
331
|
+
list_items.append(f'<li>{curr[2:]}</li>')
|
|
332
|
+
i += 1
|
|
333
|
+
elif not curr: # Empty line
|
|
334
|
+
i += 1
|
|
335
|
+
break
|
|
336
|
+
else:
|
|
337
|
+
break
|
|
338
|
+
result.append('<ul style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ul>')
|
|
339
|
+
# Ordered lists
|
|
340
|
+
elif re.match(r'^\d+\.\s', stripped):
|
|
341
|
+
list_items = []
|
|
342
|
+
while i < len(lines):
|
|
343
|
+
curr = lines[i].strip()
|
|
344
|
+
if re.match(r'^\d+\.\s', curr):
|
|
345
|
+
clean_item = re.sub(r'^\d+\.\s', '', curr)
|
|
346
|
+
list_items.append(f'<li>{clean_item}</li>')
|
|
347
|
+
i += 1
|
|
348
|
+
elif not curr: # Empty line
|
|
349
|
+
i += 1
|
|
350
|
+
break
|
|
351
|
+
else:
|
|
352
|
+
break
|
|
353
|
+
result.append('<ol style="margin: 0.5rem 0; padding-left: 1.5rem;">' + ''.join(list_items) + '</ol>')
|
|
354
|
+
# Empty line
|
|
355
|
+
elif not stripped:
|
|
356
|
+
result.append('<br>')
|
|
357
|
+
i += 1
|
|
358
|
+
# Regular text
|
|
359
|
+
else:
|
|
360
|
+
result.append(line)
|
|
361
|
+
i += 1
|
|
362
|
+
|
|
363
|
+
html = '\n'.join(result)
|
|
364
|
+
|
|
365
|
+
# Inline elements (bold, italic, code, links)
|
|
366
|
+
# Bold **text** (before italic to avoid conflicts)
|
|
367
|
+
html = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', html)
|
|
368
|
+
# Italic *text* (avoid matching list markers)
|
|
369
|
+
html = re.sub(r'(?<!\*)\*([^*\n]+?)\*(?!\*)', r'<em>\1</em>', html)
|
|
370
|
+
# Code `text`
|
|
371
|
+
html = re.sub(r'`(.+?)`', r'<code style="background:var(--sl-bg-card);padding:0.2em 0.4em;border-radius:3px;">\1</code>', html)
|
|
372
|
+
# Links [text](url)
|
|
373
|
+
html = re.sub(r'\[(.+?)\]\((.+?)\)', r'<a href="\2" style="color:var(--sl-primary);">\1</a>', html)
|
|
374
|
+
|
|
375
|
+
rendering_ctx.reset(token)
|
|
376
|
+
return Component("div", id=cid, content=html, class_="markdown", **props)
|
|
377
|
+
self._register_component(cid, builder)
|
|
378
|
+
|
|
379
|
+
def html(self, *args, **props):
|
|
380
|
+
"""Display raw HTML content
|
|
381
|
+
|
|
382
|
+
Use this when you need to render HTML directly without markdown processing.
|
|
383
|
+
For markdown formatting, use app.markdown() instead.
|
|
384
|
+
|
|
385
|
+
Example:
|
|
386
|
+
app.html('<div class="custom">', count, '</div>')
|
|
387
|
+
"""
|
|
388
|
+
cid = self._get_next_cid("html")
|
|
389
|
+
def builder():
|
|
390
|
+
from ..state import State, ComputedState
|
|
391
|
+
token = rendering_ctx.set(cid)
|
|
392
|
+
|
|
393
|
+
parts = []
|
|
394
|
+
for arg in args:
|
|
395
|
+
if isinstance(arg, (State, ComputedState)):
|
|
396
|
+
parts.append(str(arg.value))
|
|
397
|
+
elif callable(arg):
|
|
398
|
+
parts.append(str(arg()))
|
|
399
|
+
else:
|
|
400
|
+
parts.append(str(arg))
|
|
401
|
+
|
|
402
|
+
content = " ".join(parts)
|
|
403
|
+
rendering_ctx.reset(token)
|
|
404
|
+
return Component("div", id=cid, content=content, **props)
|
|
405
|
+
self._register_component(cid, builder)
|
|
406
|
+
|
|
407
|
+
def code(self, code: Union[str, Callable], language: Optional[str] = None, **props):
|
|
408
|
+
"""Display code block with syntax highlighting"""
|
|
409
|
+
import html as html_lib
|
|
410
|
+
|
|
411
|
+
cid = self._get_next_cid("code")
|
|
412
|
+
def builder():
|
|
413
|
+
token = rendering_ctx.set(cid)
|
|
414
|
+
code_text = code() if callable(code) else code
|
|
415
|
+
rendering_ctx.reset(token)
|
|
416
|
+
|
|
417
|
+
# XSS protection: escape code content
|
|
418
|
+
escaped_code = html_lib.escape(str(code_text))
|
|
419
|
+
|
|
420
|
+
lang_class = f"language-{language}" if language else ""
|
|
421
|
+
html_output = f'''
|
|
422
|
+
<pre style="background:var(--sl-bg-card);padding:1rem;border-radius:0.5rem;border:1px solid var(--sl-border);overflow-x:auto;">
|
|
423
|
+
<code class="{lang_class}" style="color:var(--sl-text);font-family:monospace;">{escaped_code}</code>
|
|
424
|
+
</pre>
|
|
425
|
+
'''
|
|
426
|
+
return Component("div", id=cid, content=html_output, **props)
|
|
427
|
+
self._register_component(cid, builder)
|
|
428
|
+
|
|
429
|
+
def divider(self):
|
|
430
|
+
"""Display horizontal divider"""
|
|
431
|
+
cid = self._get_next_cid("divider")
|
|
432
|
+
def builder():
|
|
433
|
+
return Component("sl-divider", id=cid, class_="divider")
|
|
434
|
+
self._register_component(cid, builder)
|
|
435
|
+
|
|
436
|
+
def success(self, body, icon="check-circle"):
|
|
437
|
+
"""Display success message"""
|
|
438
|
+
self._alert(body, "success", icon)
|
|
439
|
+
|
|
440
|
+
def info(self, body, icon="info-circle"):
|
|
441
|
+
"""Display info message"""
|
|
442
|
+
self._alert(body, "primary", icon)
|
|
443
|
+
|
|
444
|
+
def warning(self, body, icon="exclamation-triangle"):
|
|
445
|
+
"""Display warning message"""
|
|
446
|
+
self._alert(body, "warning", icon)
|
|
447
|
+
|
|
448
|
+
def error(self, body, icon="exclamation-octagon"):
|
|
449
|
+
"""Display error message"""
|
|
450
|
+
self._alert(body, "danger", icon)
|
|
451
|
+
|
|
452
|
+
def _alert(self, body, variant, icon_name):
|
|
453
|
+
cid = self._get_next_cid("alert")
|
|
454
|
+
def builder():
|
|
455
|
+
icon_html = f'<sl-icon slot="icon" name="{icon_name}"></sl-icon>'
|
|
456
|
+
html = f'<sl-alert variant="{variant}" open style="margin-bottom:1rem;">{icon_html}{body}</sl-alert>'
|
|
457
|
+
return Component("div", id=cid, content=html)
|
|
458
|
+
self._register_component(cid, builder)
|