django-cfg 1.4.109__py3-none-any.whl → 1.4.111__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/modules/django_admin/__init__.py +6 -0
- django_cfg/modules/django_admin/base/pydantic_admin.py +101 -0
- django_cfg/modules/django_admin/config/__init__.py +5 -0
- django_cfg/modules/django_admin/config/admin_config.py +7 -0
- django_cfg/modules/django_admin/config/documentation_config.py +406 -0
- django_cfg/modules/django_admin/config/field_config.py +87 -0
- django_cfg/modules/django_admin/templates/django_admin/change_form_docs.html +23 -0
- django_cfg/modules/django_admin/templates/django_admin/change_list_docs.html +23 -0
- django_cfg/modules/django_admin/templates/django_admin/documentation_block.html +303 -0
- django_cfg/modules/django_admin/templates/django_admin/markdown_docs_block.html +37 -0
- django_cfg/modules/django_admin/utils/__init__.py +3 -0
- django_cfg/modules/django_admin/utils/html_builder.py +94 -1
- django_cfg/modules/django_admin/utils/markdown_renderer.py +360 -0
- django_cfg/modules/django_admin/utils/mermaid_plugin.py +288 -0
- django_cfg/pyproject.toml +2 -2
- {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/METADATA +2 -1
- {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/RECORD +21 -17
- django_cfg/modules/django_admin/CHANGELOG_CODE_METHODS.md +0 -153
- django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md +0 -72
- django_cfg/modules/django_admin/RESOURCE_CONFIG_ENHANCEMENT.md +0 -350
- {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.109.dist-info → django_cfg-1.4.111.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Markdown rendering service for Django Admin.
|
|
3
|
+
|
|
4
|
+
Provides utilities for rendering markdown content from strings or files
|
|
5
|
+
with beautiful Tailwind CSS styling and collapsible sections.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import os
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Optional, Union
|
|
12
|
+
|
|
13
|
+
from django.utils.html import escape, format_html
|
|
14
|
+
from django.utils.safestring import SafeString, mark_safe
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import mistune
|
|
20
|
+
MISTUNE_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
MISTUNE_AVAILABLE = False
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class MarkdownRenderer:
|
|
26
|
+
"""
|
|
27
|
+
Markdown rendering service with file/string support and collapsible UI.
|
|
28
|
+
|
|
29
|
+
Features:
|
|
30
|
+
- Render markdown from strings or .md files
|
|
31
|
+
- Auto-detect content type (file path vs markdown string)
|
|
32
|
+
- Beautiful Tailwind CSS styling with dark mode
|
|
33
|
+
- Collapsible sections for documentation
|
|
34
|
+
- Support for all markdown features (tables, code blocks, etc.)
|
|
35
|
+
|
|
36
|
+
Usage:
|
|
37
|
+
# In admin.py
|
|
38
|
+
from django_cfg.modules.django_admin.utils import MarkdownRenderer
|
|
39
|
+
|
|
40
|
+
class MyAdmin(admin.ModelAdmin):
|
|
41
|
+
def documentation(self, obj):
|
|
42
|
+
# From file
|
|
43
|
+
return MarkdownRenderer.render(obj.docs_file_path, collapsible=True)
|
|
44
|
+
|
|
45
|
+
# From string
|
|
46
|
+
return MarkdownRenderer.render(obj.markdown_content, collapsible=True)
|
|
47
|
+
|
|
48
|
+
# From field or string with custom title
|
|
49
|
+
return MarkdownRenderer.render(
|
|
50
|
+
obj.description,
|
|
51
|
+
collapsible=True,
|
|
52
|
+
title="Description"
|
|
53
|
+
)
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
# Singleton markdown parser instances
|
|
57
|
+
_md_with_plugins = None
|
|
58
|
+
_md_without_plugins = None
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def _get_markdown_parser(cls, enable_plugins: bool = True, enable_mermaid: bool = True):
|
|
62
|
+
"""
|
|
63
|
+
Get or create markdown parser instance (singleton pattern).
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
enable_plugins: Enable standard mistune plugins
|
|
67
|
+
enable_mermaid: Enable Mermaid diagram support
|
|
68
|
+
"""
|
|
69
|
+
if not MISTUNE_AVAILABLE:
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
if enable_plugins:
|
|
73
|
+
if cls._md_with_plugins is None:
|
|
74
|
+
# Import Mermaid plugin
|
|
75
|
+
from .mermaid_plugin import mermaid_plugin
|
|
76
|
+
|
|
77
|
+
# Create markdown with standard plugins
|
|
78
|
+
md = mistune.create_markdown(
|
|
79
|
+
plugins=['strikethrough', 'table', 'url', 'task_lists', 'def_list']
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Add Mermaid plugin if enabled
|
|
83
|
+
if enable_mermaid:
|
|
84
|
+
md = mermaid_plugin(md)
|
|
85
|
+
|
|
86
|
+
cls._md_with_plugins = md
|
|
87
|
+
return cls._md_with_plugins
|
|
88
|
+
else:
|
|
89
|
+
if cls._md_without_plugins is None:
|
|
90
|
+
cls._md_without_plugins = mistune.create_markdown()
|
|
91
|
+
return cls._md_without_plugins
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def _load_from_file(cls, file_path: Union[str, Path]) -> Optional[str]:
|
|
95
|
+
"""
|
|
96
|
+
Load markdown content from file.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
file_path: Path to .md file
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
File content or None if error
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
path = Path(file_path)
|
|
106
|
+
if not path.exists():
|
|
107
|
+
logger.warning(f"Markdown file not found: {file_path}")
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
if not path.suffix.lower() in ['.md', '.markdown']:
|
|
111
|
+
logger.warning(f"File is not a markdown file: {file_path}")
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
with open(path, 'r', encoding='utf-8') as f:
|
|
115
|
+
return f.read()
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Error loading markdown file {file_path}: {e}")
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def _is_file_path(cls, content: str) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
Check if content is a file path.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content: String to check
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if looks like a file path
|
|
130
|
+
"""
|
|
131
|
+
# Check if it's a valid path and file exists
|
|
132
|
+
if '\n' in content:
|
|
133
|
+
return False # Multi-line content is not a file path
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
path = Path(content)
|
|
137
|
+
return path.exists() and path.is_file() and path.suffix.lower() in ['.md', '.markdown']
|
|
138
|
+
except (OSError, ValueError):
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def render_markdown(
|
|
143
|
+
cls,
|
|
144
|
+
text: str,
|
|
145
|
+
css_class: str = "",
|
|
146
|
+
max_height: Optional[str] = None,
|
|
147
|
+
enable_plugins: bool = True
|
|
148
|
+
) -> SafeString:
|
|
149
|
+
"""
|
|
150
|
+
Render markdown text to HTML with beautiful Tailwind styling.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
text: Markdown content
|
|
154
|
+
css_class: Additional CSS classes
|
|
155
|
+
max_height: Max height with scrolling (e.g., "400px", "20rem")
|
|
156
|
+
enable_plugins: Enable mistune plugins (tables, strikethrough, etc.)
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
SafeString with rendered HTML
|
|
160
|
+
"""
|
|
161
|
+
if not MISTUNE_AVAILABLE:
|
|
162
|
+
return format_html(
|
|
163
|
+
'<div class="text-warning-600 dark:text-warning-400 p-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-700 rounded">'
|
|
164
|
+
'<strong>⚠️ Mistune not installed:</strong> Install with: pip install mistune>=3.1.4'
|
|
165
|
+
'</div>'
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if not text:
|
|
169
|
+
return format_html(
|
|
170
|
+
'<span class="text-font-subtle-light dark:text-font-subtle-dark">No content</span>'
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Get markdown parser
|
|
174
|
+
md = cls._get_markdown_parser(enable_plugins)
|
|
175
|
+
if not md:
|
|
176
|
+
return format_html(
|
|
177
|
+
'<div class="text-danger-600 dark:text-danger-400">Error: Could not initialize markdown parser</div>'
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Render markdown to HTML
|
|
181
|
+
html_content = md(str(text))
|
|
182
|
+
|
|
183
|
+
# Beautiful Tailwind prose styles
|
|
184
|
+
base_classes = (
|
|
185
|
+
"prose prose-sm dark:prose-invert max-w-none "
|
|
186
|
+
"prose-headings:font-semibold prose-headings:text-font-default-light dark:prose-headings:text-font-default-dark "
|
|
187
|
+
"prose-h1:text-2xl prose-h1:mb-4 prose-h1:mt-6 prose-h1:border-b prose-h1:border-base-200 dark:prose-h1:border-base-700 prose-h1:pb-2 "
|
|
188
|
+
"prose-h2:text-xl prose-h2:mb-3 prose-h2:mt-5 prose-h2:border-b prose-h2:border-base-200 dark:prose-h2:border-base-700 prose-h2:pb-1 "
|
|
189
|
+
"prose-h3:text-lg prose-h3:mb-2 prose-h3:mt-4 "
|
|
190
|
+
"prose-h4:text-base prose-h4:mb-2 prose-h4:mt-3 "
|
|
191
|
+
"prose-p:mb-3 prose-p:leading-relaxed prose-p:text-font-default-light dark:prose-p:text-font-default-dark "
|
|
192
|
+
"prose-a:text-primary-600 dark:prose-a:text-primary-400 prose-a:no-underline hover:prose-a:underline prose-a:font-medium "
|
|
193
|
+
"prose-strong:text-font-default-light dark:prose-strong:text-font-default-dark prose-strong:font-semibold "
|
|
194
|
+
"prose-code:bg-base-100 dark:prose-code:bg-base-800 prose-code:text-danger-600 dark:prose-code:text-danger-400 "
|
|
195
|
+
"prose-code:px-1.5 prose-code:py-0.5 prose-code:rounded prose-code:text-xs prose-code:font-mono "
|
|
196
|
+
"prose-code:before:content-none prose-code:after:content-none "
|
|
197
|
+
"prose-pre:bg-base-50 dark:prose-pre:bg-base-900 prose-pre:border prose-pre:border-base-200 dark:prose-pre:border-base-700 prose-pre:rounded-lg "
|
|
198
|
+
"prose-blockquote:border-l-4 prose-blockquote:border-primary-300 dark:prose-blockquote:border-primary-600 "
|
|
199
|
+
"prose-blockquote:bg-base-50 dark:prose-blockquote:bg-base-900 prose-blockquote:pl-4 prose-blockquote:pr-4 prose-blockquote:py-2 "
|
|
200
|
+
"prose-blockquote:italic prose-blockquote:text-font-subtle-light dark:prose-blockquote:text-font-subtle-dark "
|
|
201
|
+
"prose-ul:list-disc prose-ul:pl-6 prose-ul:mb-3 prose-ul:text-font-default-light dark:prose-ul:text-font-default-dark "
|
|
202
|
+
"prose-ol:list-decimal prose-ol:pl-6 prose-ol:mb-3 prose-ol:text-font-default-light dark:prose-ol:text-font-default-dark "
|
|
203
|
+
"prose-li:mb-1 "
|
|
204
|
+
"prose-table:border-collapse prose-table:w-full prose-table:text-sm "
|
|
205
|
+
"prose-th:bg-base-100 dark:prose-th:bg-base-800 prose-th:border prose-th:border-base-300 dark:prose-th:border-base-600 "
|
|
206
|
+
"prose-th:px-4 prose-th:py-2 prose-th:text-left prose-th:font-semibold prose-th:text-font-default-light dark:prose-th:text-font-default-dark "
|
|
207
|
+
"prose-td:border prose-td:border-base-200 dark:prose-td:border-base-700 prose-td:px-4 prose-td:py-2 "
|
|
208
|
+
"prose-td:text-font-default-light dark:prose-td:text-font-default-dark "
|
|
209
|
+
"prose-img:rounded-lg prose-img:shadow-md prose-img:border prose-img:border-base-200 dark:prose-img:border-base-700 "
|
|
210
|
+
"prose-hr:border-base-200 dark:prose-hr:border-base-700 prose-hr:my-6 "
|
|
211
|
+
"prose-em:text-font-default-light dark:prose-em:text-font-default-dark "
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Combine with custom classes
|
|
215
|
+
classes = f"{base_classes} {css_class}".strip()
|
|
216
|
+
|
|
217
|
+
# Add container div with max-height if specified
|
|
218
|
+
style = ""
|
|
219
|
+
if max_height:
|
|
220
|
+
style = f'max-height: {max_height}; overflow-y: auto;'
|
|
221
|
+
|
|
222
|
+
return format_html(
|
|
223
|
+
'<div class="{}" {}>{}</div>',
|
|
224
|
+
classes,
|
|
225
|
+
mark_safe(f'style="{style}"') if style else '',
|
|
226
|
+
mark_safe(html_content)
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
@classmethod
|
|
230
|
+
def render(
|
|
231
|
+
cls,
|
|
232
|
+
content: Union[str, Path],
|
|
233
|
+
collapsible: bool = False,
|
|
234
|
+
title: str = "Documentation",
|
|
235
|
+
icon: str = "description",
|
|
236
|
+
max_height: Optional[str] = "500px",
|
|
237
|
+
enable_plugins: bool = True,
|
|
238
|
+
default_open: bool = False
|
|
239
|
+
) -> SafeString:
|
|
240
|
+
"""
|
|
241
|
+
Universal markdown renderer - auto-detects file vs string content.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
content: Markdown string, file path, or Path object
|
|
245
|
+
collapsible: Wrap in collapsible Tailwind details/summary
|
|
246
|
+
title: Title for collapsible section
|
|
247
|
+
icon: Material icon name for title
|
|
248
|
+
max_height: Max height for scrolling (None = no limit)
|
|
249
|
+
enable_plugins: Enable markdown plugins
|
|
250
|
+
default_open: If collapsible, open by default
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
Rendered HTML as SafeString
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
# Simple render from string
|
|
257
|
+
MarkdownRenderer.render("# Hello\\n\\nThis is **bold**")
|
|
258
|
+
|
|
259
|
+
# From file
|
|
260
|
+
MarkdownRenderer.render("/path/to/docs.md")
|
|
261
|
+
|
|
262
|
+
# Collapsible documentation
|
|
263
|
+
MarkdownRenderer.render(
|
|
264
|
+
obj.description,
|
|
265
|
+
collapsible=True,
|
|
266
|
+
title="API Documentation",
|
|
267
|
+
icon="api"
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# From file with collapse
|
|
271
|
+
MarkdownRenderer.render(
|
|
272
|
+
"docs/README.md",
|
|
273
|
+
collapsible=True,
|
|
274
|
+
title="Project Documentation",
|
|
275
|
+
default_open=True
|
|
276
|
+
)
|
|
277
|
+
"""
|
|
278
|
+
if not content:
|
|
279
|
+
return format_html(
|
|
280
|
+
'<span class="text-font-subtle-light dark:text-font-subtle-dark text-sm">No documentation available</span>'
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# Convert to string if Path object
|
|
284
|
+
content_str = str(content)
|
|
285
|
+
|
|
286
|
+
# Try to load from file if it looks like a file path
|
|
287
|
+
markdown_text = content_str
|
|
288
|
+
is_from_file = False
|
|
289
|
+
|
|
290
|
+
if cls._is_file_path(content_str):
|
|
291
|
+
file_content = cls._load_from_file(content_str)
|
|
292
|
+
if file_content:
|
|
293
|
+
markdown_text = file_content
|
|
294
|
+
is_from_file = True
|
|
295
|
+
else:
|
|
296
|
+
return format_html(
|
|
297
|
+
'<div class="text-warning-600 dark:text-warning-400 p-3 bg-warning-50 dark:bg-warning-900/20 border border-warning-200 dark:border-warning-700 rounded">'
|
|
298
|
+
'<strong>⚠️ File not found:</strong> {}'
|
|
299
|
+
'</div>',
|
|
300
|
+
escape(content_str)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Render markdown
|
|
304
|
+
rendered_html = cls.render_markdown(
|
|
305
|
+
markdown_text,
|
|
306
|
+
max_height=max_height if not collapsible else None,
|
|
307
|
+
enable_plugins=enable_plugins
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Return without collapse if not requested
|
|
311
|
+
if not collapsible:
|
|
312
|
+
return rendered_html
|
|
313
|
+
|
|
314
|
+
# Wrap in beautiful collapsible section
|
|
315
|
+
open_attr = 'open' if default_open else ''
|
|
316
|
+
|
|
317
|
+
return format_html(
|
|
318
|
+
'<details class="group border border-base-200 dark:border-base-700 rounded-lg overflow-hidden bg-white dark:bg-base-800 shadow-sm hover:shadow-md transition-shadow" {}>'
|
|
319
|
+
'<summary class="cursor-pointer px-4 py-3 bg-base-50 dark:bg-base-900 hover:bg-base-100 dark:hover:bg-base-800 transition-colors flex items-center gap-2 select-none">'
|
|
320
|
+
'<span class="material-symbols-outlined text-base text-primary-600 dark:text-primary-400 group-open:rotate-90 transition-transform">{}</span>'
|
|
321
|
+
'<span class="font-semibold text-sm text-font-default-light dark:text-font-default-dark">{}</span>'
|
|
322
|
+
'<span class="text-xs text-font-subtle-light dark:text-font-subtle-dark ml-auto">{}</span>'
|
|
323
|
+
'</summary>'
|
|
324
|
+
'<div class="p-4 bg-white dark:bg-base-800" {}>'
|
|
325
|
+
'{}'
|
|
326
|
+
'</div>'
|
|
327
|
+
'</details>',
|
|
328
|
+
mark_safe(open_attr),
|
|
329
|
+
'chevron_right', # Arrow icon
|
|
330
|
+
escape(title),
|
|
331
|
+
'Click to expand' if not default_open else 'Click to collapse',
|
|
332
|
+
mark_safe(f'style="max-height: {max_height}; overflow-y: auto;"') if max_height else '',
|
|
333
|
+
rendered_html
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def render_docs(
|
|
338
|
+
cls,
|
|
339
|
+
content: Union[str, Path],
|
|
340
|
+
**kwargs
|
|
341
|
+
) -> SafeString:
|
|
342
|
+
"""
|
|
343
|
+
Shorthand for rendering documentation (always collapsible by default).
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
content: Markdown string or file path
|
|
347
|
+
**kwargs: Additional arguments passed to render()
|
|
348
|
+
|
|
349
|
+
Returns:
|
|
350
|
+
Rendered collapsible documentation
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
def documentation(self, obj):
|
|
354
|
+
return MarkdownRenderer.render_docs(obj.docs_path)
|
|
355
|
+
"""
|
|
356
|
+
# Set collapsible=True by default for docs
|
|
357
|
+
if 'collapsible' not in kwargs:
|
|
358
|
+
kwargs['collapsible'] = True
|
|
359
|
+
|
|
360
|
+
return cls.render(content, **kwargs)
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mermaid diagram plugin for Mistune markdown parser.
|
|
3
|
+
|
|
4
|
+
Renders ```mermaid code blocks as interactive diagrams using Mermaid.js.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def mermaid_plugin(md):
|
|
12
|
+
"""
|
|
13
|
+
Mistune plugin to render Mermaid diagrams.
|
|
14
|
+
|
|
15
|
+
Detects code fences with 'mermaid' language and renders them as
|
|
16
|
+
Mermaid diagram containers that will be processed by Mermaid.js.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
```mermaid
|
|
20
|
+
graph TD
|
|
21
|
+
A[Start] --> B{Decision}
|
|
22
|
+
B -->|Yes| C[OK]
|
|
23
|
+
B -->|No| D[Cancel]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
md: Mistune markdown instance
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def render_mermaid(text: str, **attrs: Any) -> str:
|
|
31
|
+
"""
|
|
32
|
+
Render Mermaid diagram HTML.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
text: Mermaid diagram code
|
|
36
|
+
**attrs: Additional attributes
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
HTML with Mermaid container
|
|
40
|
+
"""
|
|
41
|
+
# Generate unique ID for this diagram
|
|
42
|
+
import hashlib
|
|
43
|
+
diagram_id = f"mermaid-{hashlib.md5(text.encode()).hexdigest()[:8]}"
|
|
44
|
+
|
|
45
|
+
# Escape HTML special characters but preserve Mermaid syntax
|
|
46
|
+
escaped_text = text.strip()
|
|
47
|
+
|
|
48
|
+
# Return HTML container with Mermaid code
|
|
49
|
+
return f'''<div class="mermaid-container">
|
|
50
|
+
<div class="mermaid-wrapper">
|
|
51
|
+
<pre class="mermaid" id="{diagram_id}">
|
|
52
|
+
{escaped_text}
|
|
53
|
+
</pre>
|
|
54
|
+
</div>
|
|
55
|
+
</div>'''
|
|
56
|
+
|
|
57
|
+
# Override code block renderer for mermaid language
|
|
58
|
+
original_code = md.renderer.block_code
|
|
59
|
+
|
|
60
|
+
def patched_code(code: str, info: str = None, **attrs: Any) -> str:
|
|
61
|
+
"""
|
|
62
|
+
Patched code block renderer that checks for mermaid language.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
code: Code content
|
|
66
|
+
info: Language info
|
|
67
|
+
**attrs: Additional attributes
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Rendered code block (either Mermaid or normal code)
|
|
71
|
+
"""
|
|
72
|
+
if info and info.strip().lower() == 'mermaid':
|
|
73
|
+
return render_mermaid(code, **attrs)
|
|
74
|
+
return original_code(code, info, **attrs)
|
|
75
|
+
|
|
76
|
+
md.renderer.block_code = patched_code
|
|
77
|
+
|
|
78
|
+
return md
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_mermaid_styles() -> str:
|
|
82
|
+
"""
|
|
83
|
+
Get CSS styles for Mermaid diagrams with Unfold semantic colors.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
CSS string for Mermaid container styling
|
|
87
|
+
"""
|
|
88
|
+
return """
|
|
89
|
+
<style>
|
|
90
|
+
/* Mermaid container styles with Unfold semantic colors */
|
|
91
|
+
.mermaid-container {
|
|
92
|
+
margin: 1.5rem 0;
|
|
93
|
+
padding: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.mermaid-wrapper {
|
|
97
|
+
border: 1px solid rgb(var(--color-base-200));
|
|
98
|
+
border-radius: 0.5rem;
|
|
99
|
+
padding: 1.5rem;
|
|
100
|
+
background: rgb(var(--color-base-50));
|
|
101
|
+
overflow-x: auto;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Dark mode styles with semantic colors */
|
|
105
|
+
.dark .mermaid-wrapper {
|
|
106
|
+
border-color: rgb(var(--color-base-700));
|
|
107
|
+
background: rgb(var(--color-base-900));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* Mermaid diagram */
|
|
111
|
+
.mermaid {
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
background: transparent !important;
|
|
115
|
+
border: none !important;
|
|
116
|
+
padding: 0 !important;
|
|
117
|
+
margin: 0 !important;
|
|
118
|
+
font-family: inherit !important;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Ensure diagrams are centered */
|
|
122
|
+
.mermaid svg {
|
|
123
|
+
max-width: 100%;
|
|
124
|
+
height: auto;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/* Loading state with semantic colors */
|
|
128
|
+
.mermaid[data-processed="false"] {
|
|
129
|
+
color: rgb(var(--color-base-400));
|
|
130
|
+
text-align: center;
|
|
131
|
+
padding: 2rem;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.dark .mermaid[data-processed="false"] {
|
|
135
|
+
color: rgb(var(--color-base-500));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Error state with semantic colors */
|
|
139
|
+
.mermaid.error {
|
|
140
|
+
color: rgb(239, 68, 68);
|
|
141
|
+
border: 1px solid rgb(252, 165, 165);
|
|
142
|
+
background: rgb(254, 242, 242);
|
|
143
|
+
padding: 1rem;
|
|
144
|
+
border-radius: 0.375rem;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.dark .mermaid.error {
|
|
148
|
+
color: rgb(248, 113, 113);
|
|
149
|
+
border-color: rgb(153, 27, 27);
|
|
150
|
+
background: rgb(127, 29, 29);
|
|
151
|
+
}
|
|
152
|
+
</style>
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_mermaid_script(theme: str = "default") -> str:
|
|
157
|
+
"""
|
|
158
|
+
Get Mermaid.js initialization script with Unfold semantic colors.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
theme: Mermaid theme ('default', 'dark', 'forest', 'neutral')
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
HTML script tag with Mermaid.js and initialization
|
|
165
|
+
"""
|
|
166
|
+
return f"""
|
|
167
|
+
<script type="module">
|
|
168
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
169
|
+
|
|
170
|
+
// Helper to get CSS variable value
|
|
171
|
+
function getCSSVar(name) {{
|
|
172
|
+
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
|
|
173
|
+
// Convert "R, G, B" format to "#RRGGBB"
|
|
174
|
+
if (value.includes(',')) {{
|
|
175
|
+
const [r, g, b] = value.split(',').map(x => parseInt(x.trim()));
|
|
176
|
+
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
|
|
177
|
+
}}
|
|
178
|
+
return value;
|
|
179
|
+
}}
|
|
180
|
+
|
|
181
|
+
// Auto-detect dark mode
|
|
182
|
+
const isDarkMode = document.documentElement.classList.contains('dark') ||
|
|
183
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
184
|
+
|
|
185
|
+
// Get Unfold semantic colors
|
|
186
|
+
function getThemeColors() {{
|
|
187
|
+
if (isDarkMode) {{
|
|
188
|
+
return {{
|
|
189
|
+
primaryColor: '#3b82f6',
|
|
190
|
+
primaryTextColor: getCSSVar('--color-base-100'),
|
|
191
|
+
primaryBorderColor: getCSSVar('--color-base-700'),
|
|
192
|
+
lineColor: getCSSVar('--color-base-600'),
|
|
193
|
+
secondaryColor: getCSSVar('--color-base-800'),
|
|
194
|
+
tertiaryColor: getCSSVar('--color-base-700'),
|
|
195
|
+
background: getCSSVar('--color-base-900'),
|
|
196
|
+
mainBkg: getCSSVar('--color-base-800'),
|
|
197
|
+
secondBkg: getCSSVar('--color-base-700'),
|
|
198
|
+
border1: getCSSVar('--color-base-700'),
|
|
199
|
+
border2: getCSSVar('--color-base-600'),
|
|
200
|
+
note: getCSSVar('--color-base-800'),
|
|
201
|
+
noteText: getCSSVar('--color-base-200'),
|
|
202
|
+
noteBorder: getCSSVar('--color-base-600'),
|
|
203
|
+
text: getCSSVar('--color-base-200'),
|
|
204
|
+
critical: '#ef4444',
|
|
205
|
+
done: '#10b981',
|
|
206
|
+
active: '#3b82f6',
|
|
207
|
+
}};
|
|
208
|
+
}} else {{
|
|
209
|
+
return {{
|
|
210
|
+
primaryColor: '#2563eb',
|
|
211
|
+
primaryTextColor: getCSSVar('--color-base-900'),
|
|
212
|
+
primaryBorderColor: getCSSVar('--color-base-300'),
|
|
213
|
+
lineColor: getCSSVar('--color-base-400'),
|
|
214
|
+
secondaryColor: getCSSVar('--color-base-100'),
|
|
215
|
+
tertiaryColor: '#ffffff',
|
|
216
|
+
background: '#ffffff',
|
|
217
|
+
mainBkg: getCSSVar('--color-base-50'),
|
|
218
|
+
secondBkg: '#ffffff',
|
|
219
|
+
border1: getCSSVar('--color-base-300'),
|
|
220
|
+
border2: getCSSVar('--color-base-200'),
|
|
221
|
+
note: '#fef3c7',
|
|
222
|
+
noteText: getCSSVar('--color-base-900'),
|
|
223
|
+
noteBorder: '#fbbf24',
|
|
224
|
+
text: getCSSVar('--color-base-900'),
|
|
225
|
+
critical: '#dc2626',
|
|
226
|
+
done: '#059669',
|
|
227
|
+
active: '#2563eb',
|
|
228
|
+
}};
|
|
229
|
+
}}
|
|
230
|
+
}}
|
|
231
|
+
|
|
232
|
+
// Initialize Mermaid with Unfold semantic colors
|
|
233
|
+
mermaid.initialize({{
|
|
234
|
+
startOnLoad: true,
|
|
235
|
+
theme: 'base',
|
|
236
|
+
securityLevel: 'loose',
|
|
237
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
238
|
+
themeVariables: getThemeColors()
|
|
239
|
+
}});
|
|
240
|
+
|
|
241
|
+
// Listen for dark mode changes and re-render
|
|
242
|
+
const observer = new MutationObserver((mutations) => {{
|
|
243
|
+
mutations.forEach((mutation) => {{
|
|
244
|
+
if (mutation.attributeName === 'class') {{
|
|
245
|
+
// Re-initialize with new theme colors
|
|
246
|
+
mermaid.initialize({{
|
|
247
|
+
startOnLoad: true,
|
|
248
|
+
theme: 'base',
|
|
249
|
+
securityLevel: 'loose',
|
|
250
|
+
fontFamily: 'ui-sans-serif, system-ui, sans-serif',
|
|
251
|
+
themeVariables: getThemeColors()
|
|
252
|
+
}});
|
|
253
|
+
// Re-render all diagrams
|
|
254
|
+
mermaid.run({{
|
|
255
|
+
querySelector: '.mermaid',
|
|
256
|
+
}});
|
|
257
|
+
}}
|
|
258
|
+
}});
|
|
259
|
+
}});
|
|
260
|
+
|
|
261
|
+
observer.observe(document.documentElement, {{
|
|
262
|
+
attributes: true,
|
|
263
|
+
attributeFilter: ['class'],
|
|
264
|
+
}});
|
|
265
|
+
|
|
266
|
+
// Error handling
|
|
267
|
+
window.addEventListener('error', (event) => {{
|
|
268
|
+
if (event.message && event.message.includes('mermaid')) {{
|
|
269
|
+
console.error('Mermaid error:', event);
|
|
270
|
+
const mermaidElements = document.querySelectorAll('.mermaid[data-processed="false"]');
|
|
271
|
+
mermaidElements.forEach(el => {{
|
|
272
|
+
el.classList.add('error');
|
|
273
|
+
el.textContent = 'Error rendering diagram. Check console for details.';
|
|
274
|
+
}});
|
|
275
|
+
}}
|
|
276
|
+
}});
|
|
277
|
+
</script>
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def get_mermaid_resources() -> str:
|
|
282
|
+
"""
|
|
283
|
+
Get complete Mermaid resources (styles + script).
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
HTML string with styles and script for Mermaid support
|
|
287
|
+
"""
|
|
288
|
+
return get_mermaid_styles() + get_mermaid_script()
|
django_cfg/pyproject.toml
CHANGED
|
@@ -4,13 +4,13 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-cfg"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.111"
|
|
8
8
|
description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
|
|
11
11
|
classifiers = [ "Development Status :: 4 - Beta", "Framework :: Django", "Framework :: Django :: 5.2", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Systems Administration", "Typing :: Typed",]
|
|
12
12
|
requires-python = ">=3.12,<3.14"
|
|
13
|
-
dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "rearq>=0.2.0,<1.0", "setuptools>=75.0.0; python_version>='3.13'", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0",]
|
|
13
|
+
dependencies = [ "pydantic>=2.11.0,<3.0", "pydantic[email]>=2.11.0,<3.0", "PyYAML>=6.0,<7.0", "click>=8.2.0,<9.0", "questionary>=2.1.0,<3.0", "rich>=14.0.0,<15.0", "cloudflare>=4.3.0,<5.0", "loguru>=0.7.0,<1.0", "colorlog>=6.9.0,<7.0", "cachetools>=5.3.0,<7.0", "toml>=0.10.2,<0.11.0", "ngrok>=1.5.1; python_version>='3.12'", "psycopg[binary,pool]>=3.2.0,<4.0", "dj-database-url>=3.0.0,<4.0", "whitenoise>=6.8.0,<7.0", "django-cors-headers>=4.7.0,<5.0", "djangorestframework>=3.16.0,<4.0", "djangorestframework-simplejwt>=5.5.0,<6.0", "djangorestframework-simplejwt[token-blacklist]>=5.5.0,<6.0", "drf-nested-routers>=0.94.0,<1.0", "django-filter>=25.0,<26.0", "django-ratelimit>=4.1.0,<5.0.0", "drf-spectacular>=0.28.0,<1.0", "drf-spectacular-sidecar>=2025.8.0,<2026.0", "django-json-widget>=2.0.0,<3.0", "django-import-export>=4.3.0,<5.0", "django-extensions>=4.1.0,<5.0", "django-constance>=4.3.0,<5.0", "django-unfold>=0.64.0,<1.0", "django-redis>=6.0.0,<7.0", "redis>=6.4.0,<7.0", "hiredis>=2.0.0,<4.0", "rearq>=0.2.0,<1.0", "setuptools>=75.0.0; python_version>='3.13'", "pyTelegramBotAPI>=4.28.0,<5.0", "coolname>=2.2.0,<3.0", "django-admin-rangefilter>=0.13.0,<1.0", "python-json-logger>=3.3.0,<4.0", "requests>=2.32.0,<3.0", "tiktoken>=0.11.0,<1.0", "openai>=1.107.0,<2.0", "twilio>=9.8.0,<10.0", "sendgrid>=6.12.0,<7.0", "beautifulsoup4>=4.13.0,<5.0", "lxml>=6.0.0,<7.0", "pgvector>=0.4.0,<1.0", "tenacity>=9.1.2,<10.0.0", "mypy (>=1.18.2,<2.0.0)", "django-tailwind[reload] (>=4.2.0,<5.0.0)", "jinja2 (>=3.1.6,<4.0.0)", "django-axes[ipware] (>=8.0.0,<9.0.0)", "pydantic-settings (>=2.11.0,<3.0.0)", "pytz>=2025.1", "httpx>=0.28.1,<1.0", "mistune>=3.1.4,<4.0",]
|
|
14
14
|
[[project.authors]]
|
|
15
15
|
name = "Django-CFG Team"
|
|
16
16
|
email = "info@djangocfg.com"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-cfg
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.111
|
|
4
4
|
Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
|
|
5
5
|
Project-URL: Homepage, https://djangocfg.com
|
|
6
6
|
Project-URL: Documentation, https://djangocfg.com
|
|
@@ -58,6 +58,7 @@ Requires-Dist: httpx<1.0,>=0.28.1
|
|
|
58
58
|
Requires-Dist: jinja2<4.0.0,>=3.1.6
|
|
59
59
|
Requires-Dist: loguru<1.0,>=0.7.0
|
|
60
60
|
Requires-Dist: lxml<7.0,>=6.0.0
|
|
61
|
+
Requires-Dist: mistune<4.0,>=3.1.4
|
|
61
62
|
Requires-Dist: mypy<2.0.0,>=1.18.2
|
|
62
63
|
Requires-Dist: ngrok>=1.5.1; python_version >= '3.12'
|
|
63
64
|
Requires-Dist: openai<2.0,>=1.107.0
|