django-cfg 1.4.108__py3-none-any.whl → 1.4.110__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.
Files changed (34) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/modules/django_admin/__init__.py +6 -0
  3. django_cfg/modules/django_admin/base/pydantic_admin.py +91 -0
  4. django_cfg/modules/django_admin/config/__init__.py +5 -0
  5. django_cfg/modules/django_admin/config/admin_config.py +7 -0
  6. django_cfg/modules/django_admin/config/documentation_config.py +406 -0
  7. django_cfg/modules/django_admin/config/field_config.py +87 -0
  8. django_cfg/modules/django_admin/templates/django_admin/change_form_docs.html +23 -0
  9. django_cfg/modules/django_admin/templates/django_admin/change_list_docs.html +23 -0
  10. django_cfg/modules/django_admin/templates/django_admin/documentation_block.html +297 -0
  11. django_cfg/modules/django_admin/templates/django_admin/markdown_docs_block.html +37 -0
  12. django_cfg/modules/django_admin/utils/__init__.py +3 -0
  13. django_cfg/modules/django_admin/utils/html_builder.py +94 -1
  14. django_cfg/modules/django_admin/utils/markdown_renderer.py +344 -0
  15. django_cfg/pyproject.toml +2 -2
  16. django_cfg/static/frontend/admin.zip +0 -0
  17. {django_cfg-1.4.108.dist-info → django_cfg-1.4.110.dist-info}/METADATA +2 -1
  18. {django_cfg-1.4.108.dist-info → django_cfg-1.4.110.dist-info}/RECORD +21 -28
  19. django_cfg/modules/django_admin/CHANGELOG_CODE_METHODS.md +0 -153
  20. django_cfg/modules/django_admin/IMPORT_EXPORT_FIX.md +0 -72
  21. django_cfg/modules/django_admin/RESOURCE_CONFIG_ENHANCEMENT.md +0 -350
  22. django_cfg/modules/django_client/system/__init__.py +0 -24
  23. django_cfg/modules/django_client/system/base_generator.py +0 -123
  24. django_cfg/modules/django_client/system/generate_mjs_clients.py +0 -176
  25. django_cfg/modules/django_client/system/mjs_generator.py +0 -219
  26. django_cfg/modules/django_client/system/schema_parser.py +0 -199
  27. django_cfg/modules/django_client/system/templates/api_client.js.j2 +0 -87
  28. django_cfg/modules/django_client/system/templates/app_index.js.j2 +0 -13
  29. django_cfg/modules/django_client/system/templates/base_client.js.j2 +0 -166
  30. django_cfg/modules/django_client/system/templates/main_index.js.j2 +0 -80
  31. django_cfg/modules/django_client/system/templates/types.js.j2 +0 -24
  32. {django_cfg-1.4.108.dist-info → django_cfg-1.4.110.dist-info}/WHEEL +0 -0
  33. {django_cfg-1.4.108.dist-info → django_cfg-1.4.110.dist-info}/entry_points.txt +0 -0
  34. {django_cfg-1.4.108.dist-info → django_cfg-1.4.110.dist-info}/licenses/LICENSE +0 -0
@@ -254,3 +254,90 @@ class ImageField(FieldConfig):
254
254
  config['caption_template'] = self.caption_template
255
255
  config['alt_text'] = self.alt_text
256
256
  return config
257
+
258
+
259
+ class MarkdownField(FieldConfig):
260
+ """
261
+ Markdown documentation widget configuration.
262
+
263
+ Renders markdown content from field value or external file with beautiful styling.
264
+ Auto-detects whether content is a file path or markdown string.
265
+
266
+ Examples:
267
+ # From model field (markdown string)
268
+ MarkdownField(
269
+ name="description",
270
+ title="Documentation",
271
+ collapsible=True
272
+ )
273
+
274
+ # From file path field
275
+ MarkdownField(
276
+ name="docs_path",
277
+ title="User Guide",
278
+ collapsible=True,
279
+ default_open=True
280
+ )
281
+
282
+ # Static file (same for all objects)
283
+ MarkdownField(
284
+ name="static_doc", # method that returns file path
285
+ title="API Documentation",
286
+ source_file="docs/api.md", # relative to app root
287
+ max_height="500px"
288
+ )
289
+
290
+ # Dynamic markdown with custom title
291
+ MarkdownField(
292
+ name="get_help_text", # method that generates markdown
293
+ title="Help",
294
+ collapsible=True,
295
+ enable_plugins=True
296
+ )
297
+ """
298
+
299
+ ui_widget: Literal["markdown"] = "markdown"
300
+
301
+ # Display options
302
+ collapsible: bool = Field(True, description="Wrap in collapsible details/summary")
303
+ default_open: bool = Field(False, description="Open by default if collapsible")
304
+ max_height: Optional[str] = Field("500px", description="Max height with scrolling (e.g., '500px', None for no limit)")
305
+
306
+ # Content source
307
+ source_file: Optional[str] = Field(
308
+ None,
309
+ description="Static file path relative to app root (e.g., 'docs/api.md')"
310
+ )
311
+ source_field: Optional[str] = Field(
312
+ None,
313
+ description="Alternative field name for content (defaults to 'name' field)"
314
+ )
315
+
316
+ # Markdown options
317
+ enable_plugins: bool = Field(
318
+ True,
319
+ description="Enable mistune plugins (tables, strikethrough, task lists, etc.)"
320
+ )
321
+
322
+ # Custom icon for collapsible header
323
+ header_icon: Optional[str] = Field(
324
+ "description",
325
+ description="Material icon for collapsible header"
326
+ )
327
+
328
+ def get_widget_config(self) -> Dict[str, Any]:
329
+ """Extract markdown widget configuration."""
330
+ config = super().get_widget_config()
331
+ config['collapsible'] = self.collapsible
332
+ config['default_open'] = self.default_open
333
+ config['max_height'] = self.max_height
334
+ config['enable_plugins'] = self.enable_plugins
335
+
336
+ if self.source_file is not None:
337
+ config['source_file'] = self.source_file
338
+ if self.source_field is not None:
339
+ config['source_field'] = self.source_field
340
+ if self.header_icon is not None:
341
+ config['header_icon'] = self.header_icon
342
+
343
+ return config
@@ -0,0 +1,23 @@
1
+ {% extends "admin/change_form.html" %}
2
+ {% load static %}
3
+
4
+ {% block field_sets %}
5
+ {# Markdown documentation before fieldsets #}
6
+ {% if adminform.model_admin.documentation_config %}
7
+ {% with config=adminform.model_admin.documentation_config %}
8
+ {% if config.show_on_changeform %}
9
+ <div class="mb-6">
10
+ {% include "django_admin/markdown_docs_block.html" with
11
+ content=config.content
12
+ title=config.title
13
+ collapsible=config.collapsible
14
+ default_open=config.default_open
15
+ max_height=config.max_height
16
+ %}
17
+ </div>
18
+ {% endif %}
19
+ {% endwith %}
20
+ {% endif %}
21
+
22
+ {{ block.super }}
23
+ {% endblock %}
@@ -0,0 +1,23 @@
1
+ {% extends "admin/change_list.html" %}
2
+ {% load static %}
3
+
4
+ {% block content_title %}
5
+ {{ block.super }}
6
+
7
+ {# Markdown documentation above the list #}
8
+ {% if cl.model_admin.documentation_config %}
9
+ {% with config=cl.model_admin.documentation_config %}
10
+ {% if config.show_on_changelist %}
11
+ <div class="mt-4">
12
+ {% include "django_admin/markdown_docs_block.html" with
13
+ content=config.content
14
+ title=config.title
15
+ collapsible=config.collapsible
16
+ default_open=config.default_open
17
+ max_height=config.max_height
18
+ %}
19
+ </div>
20
+ {% endif %}
21
+ {% endwith %}
22
+ {% endif %}
23
+ {% endblock %}
@@ -0,0 +1,297 @@
1
+ {% load static %}
2
+ {# Multi-Section Markdown Documentation Block with Management Commands - Used with unfold hooks #}
3
+
4
+ {% if documentation_config and documentation_sections or management_commands %}
5
+ <div class="mb-6">
6
+ <div class="{% if not is_popup %}max-w-full{% endif %}">
7
+ {# Main documentation container with unfold semantic classes #}
8
+ <div class="bg-base-50 dark:bg-base-950 border border-base-200 dark:border-base-700 rounded-default shadow-xs overflow-hidden">
9
+
10
+ {# Header with semantic font colors #}
11
+ <div class="px-5 py-4 border-b border-base-200 dark:border-base-800 bg-base-100 dark:bg-base-900">
12
+ <div class="flex items-center gap-3">
13
+ <span class="material-symbols-outlined text-xl text-primary-600 dark:text-primary-400">description</span>
14
+ <h2 class="text-base font-semibold text-font-important-light dark:text-font-important-dark m-0">
15
+ {{ documentation_config.title }}
16
+ </h2>
17
+ {% if documentation_sections %}
18
+ <span class="ml-auto text-xs font-medium text-font-subtle-light dark:text-font-subtle-dark px-2.5 py-1 bg-base-100 dark:bg-base-800 rounded-full">
19
+ {{ documentation_sections|length }} section{{ documentation_sections|length|pluralize }}
20
+ </span>
21
+ {% endif %}
22
+ </div>
23
+ </div>
24
+
25
+ {# Management Commands Section (if any) #}
26
+ {% if management_commands %}
27
+ <details class="group border-b border-base-200 dark:border-base-800">
28
+ <summary class="cursor-pointer px-5 py-3.5 bg-white dark:bg-base-900 hover:bg-base-700/[.04] dark:hover:bg-white/[.04] transition-all duration-200 flex items-center gap-3 select-none list-none">
29
+ {# Terminal icon #}
30
+ <span class="material-symbols-outlined text-base text-font-default-light dark:text-font-default-dark group-open:rotate-90 group-open:text-primary-600 dark:group-open:text-primary-400 transition-all duration-200">
31
+ terminal
32
+ </span>
33
+
34
+ {# Title #}
35
+ <span class="text-sm font-medium text-font-default-light dark:text-font-default-dark flex-1">
36
+ Management Commands
37
+ </span>
38
+
39
+ {# Badge #}
40
+ <span class="text-xs font-medium text-font-subtle-light dark:text-font-subtle-dark px-2.5 py-1 bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400 rounded-full">
41
+ {{ management_commands|length }} command{{ management_commands|length|pluralize }}
42
+ </span>
43
+
44
+ {# Expand/collapse hint #}
45
+ <span class="text-xs text-font-subtle-light dark:text-font-subtle-dark group-open:hidden">
46
+ Click to expand
47
+ </span>
48
+ <span class="text-xs text-font-subtle-light dark:text-font-subtle-dark hidden group-open:inline">
49
+ Click to collapse
50
+ </span>
51
+ </summary>
52
+
53
+ {# Commands list #}
54
+ <div class="px-5 py-4 bg-white dark:bg-base-900">
55
+ <div class="flex flex-col gap-3">
56
+ {% for cmd in management_commands %}
57
+ <div class="border border-base-200 dark:border-base-700 rounded-default p-4 bg-base-50 dark:bg-base-950 hover:border-primary-300 dark:hover:border-primary-700 transition-colors duration-200">
58
+ {# Command name #}
59
+ <div class="flex items-start gap-3 mb-2">
60
+ <span class="material-symbols-outlined text-sm text-primary-600 dark:text-primary-400 mt-0.5">code</span>
61
+ <div class="flex-1 min-w-0">
62
+ <code class="text-sm font-mono font-semibold text-font-important-light dark:text-font-important-dark">
63
+ python manage.py {{ cmd.name }}
64
+ </code>
65
+ {% if cmd.help %}
66
+ <p class="text-xs text-font-default-light dark:text-font-default-dark mt-1">
67
+ {{ cmd.help }}
68
+ </p>
69
+ {% endif %}
70
+ </div>
71
+ </div>
72
+
73
+ {# Arguments #}
74
+ {% if cmd.arguments %}
75
+ <div class="mt-3 pt-3 border-t border-base-200 dark:border-base-700">
76
+ <div class="text-xs font-medium text-font-subtle-light dark:text-font-subtle-dark mb-2">Arguments:</div>
77
+ <div class="space-y-1.5">
78
+ {% for arg in cmd.arguments %}
79
+ <div class="flex items-start gap-2 text-xs">
80
+ <code class="font-mono text-primary-600 dark:text-primary-400 font-medium">{{ arg.name }}</code>
81
+ {% if arg.required %}
82
+ <span class="px-1.5 py-0.5 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded text-[10px] font-medium">required</span>
83
+ {% endif %}
84
+ {% if arg.help %}
85
+ <span class="text-font-subtle-light dark:text-font-subtle-dark">- {{ arg.help }}</span>
86
+ {% endif %}
87
+ {% if arg.default %}
88
+ <span class="text-font-subtle-light dark:text-font-subtle-dark ml-auto">default: <code class="text-[10px]">{{ arg.default }}</code></span>
89
+ {% endif %}
90
+ </div>
91
+ {% endfor %}
92
+ </div>
93
+ </div>
94
+ {% endif %}
95
+ </div>
96
+ {% endfor %}
97
+ </div>
98
+ </div>
99
+ </details>
100
+ {% endif %}
101
+
102
+ {# Documentation Sections #}
103
+ {% if documentation_sections %}
104
+ <div class="divide-y divide-base-200 dark:divide-base-800">
105
+ {% for section in documentation_sections %}
106
+ {% if documentation_config.collapsible %}
107
+ {# Collapsible section #}
108
+ <details class="group"{% if section.default_open %} open{% endif %}>
109
+ <summary class="cursor-pointer px-5 py-3.5 bg-white dark:bg-base-900 hover:bg-base-700/[.04] dark:hover:bg-white/[.04] transition-all duration-200 flex items-center gap-3 select-none list-none">
110
+ {# Chevron icon #}
111
+ <span class="material-symbols-outlined text-base text-font-default-light dark:text-font-default-dark group-open:rotate-90 group-open:text-primary-600 dark:group-open:text-primary-400 transition-all duration-200">
112
+ chevron_right
113
+ </span>
114
+
115
+ {# Section title #}
116
+ <span class="text-sm font-medium text-font-default-light dark:text-font-default-dark flex-1">
117
+ {{ section.title }}
118
+ </span>
119
+
120
+ {# Expand/collapse hint #}
121
+ <span class="text-xs text-font-subtle-light dark:text-font-subtle-dark group-open:hidden">
122
+ Click to expand
123
+ </span>
124
+ <span class="text-xs text-font-subtle-light dark:text-font-subtle-dark hidden group-open:inline">
125
+ Click to collapse
126
+ </span>
127
+ </summary>
128
+
129
+ {# Section content #}
130
+ <div class="p-5 bg-white dark:bg-base-900">
131
+ <div class="border border-base-200 dark:border-base-700 rounded-default p-4 bg-white dark:bg-base-950 prose prose-sm dark:prose-invert max-w-none overflow-auto"
132
+ {% if documentation_config.max_height %}style="max-height: {{ documentation_config.max_height }};"{% endif %}>
133
+ {{ section.content|safe }}
134
+ </div>
135
+ </div>
136
+ </details>
137
+ {% else %}
138
+ {# Non-collapsible section #}
139
+ <div class="bg-white dark:bg-base-900">
140
+ {% if section.title != documentation_config.title %}
141
+ <div class="px-5 py-3 bg-base-50 dark:bg-base-950 border-b border-base-200 dark:border-base-800">
142
+ <h3 class="text-sm font-medium text-font-important-light dark:text-font-important-dark m-0">
143
+ {{ section.title }}
144
+ </h3>
145
+ </div>
146
+ {% endif %}
147
+ <div class="p-5">
148
+ <div class="border border-base-200 dark:border-base-700 rounded-default p-4 bg-white dark:bg-base-950 prose prose-sm dark:prose-invert max-w-none overflow-auto"
149
+ {% if documentation_config.max_height %}style="max-height: {{ documentation_config.max_height }};"{% endif %}>
150
+ {{ section.content|safe }}
151
+ </div>
152
+ </div>
153
+ </div>
154
+ {% endif %}
155
+ {% endfor %}
156
+ </div>
157
+ {% endif %}
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ {# Enhanced dark mode prose styles with unfold semantic colors #}
163
+ <style>
164
+ /* Unfold semantic dark mode prose styles */
165
+ .dark .prose-invert {
166
+ --tw-prose-body: rgb(var(--color-base-300));
167
+ --tw-prose-headings: rgb(var(--color-base-100));
168
+ --tw-prose-lead: rgb(var(--color-base-400));
169
+ --tw-prose-links: rgb(96 165 250);
170
+ --tw-prose-bold: rgb(var(--color-base-100));
171
+ --tw-prose-counters: rgb(var(--color-base-400));
172
+ --tw-prose-bullets: rgb(var(--color-base-500));
173
+ --tw-prose-hr: rgb(var(--color-base-700));
174
+ --tw-prose-quotes: rgb(var(--color-base-100));
175
+ --tw-prose-quote-borders: rgb(var(--color-base-700));
176
+ --tw-prose-captions: rgb(var(--color-base-400));
177
+ --tw-prose-code: rgb(var(--color-base-100));
178
+ --tw-prose-pre-code: rgb(var(--color-base-200));
179
+ --tw-prose-pre-bg: rgb(var(--color-base-950));
180
+ --tw-prose-th-borders: rgb(var(--color-base-700));
181
+ --tw-prose-td-borders: rgb(var(--color-base-700));
182
+ }
183
+
184
+ /* Light mode pre blocks */
185
+ .prose pre {
186
+ background-color: rgb(249, 250, 251) !important;
187
+ border: 1px solid rgb(229, 231, 235) !important;
188
+ color: rgb(17, 24, 39) !important;
189
+ }
190
+
191
+ .prose code {
192
+ background-color: rgb(243, 244, 246) !important;
193
+ color: rgb(17, 24, 39) !important;
194
+ padding: 0.125rem 0.25rem;
195
+ border-radius: 0.25rem;
196
+ font-size: 0.875em;
197
+ }
198
+
199
+ .prose pre code {
200
+ background-color: transparent !important;
201
+ padding: 0;
202
+ color: inherit !important;
203
+ }
204
+
205
+ /* Dark mode pre blocks */
206
+ .dark .prose-invert pre {
207
+ background-color: rgb(3, 7, 18) !important;
208
+ border: 1px solid rgb(55, 65, 81) !important;
209
+ color: rgb(229, 231, 235) !important;
210
+ }
211
+
212
+ .dark .prose-invert code {
213
+ background-color: rgb(31, 41, 55) !important;
214
+ color: rgb(243, 244, 246) !important;
215
+ padding: 0.125rem 0.25rem;
216
+ border-radius: 0.25rem;
217
+ font-size: 0.875em;
218
+ }
219
+
220
+ .dark .prose-invert pre code {
221
+ background-color: transparent !important;
222
+ padding: 0;
223
+ color: inherit !important;
224
+ }
225
+
226
+ /* Tables with semantic colors */
227
+ .dark .prose-invert table {
228
+ border-color: rgb(var(--color-base-700));
229
+ }
230
+
231
+ .dark .prose-invert thead {
232
+ border-bottom-color: rgb(var(--color-base-700));
233
+ background-color: rgb(var(--color-base-800));
234
+ }
235
+
236
+ .dark .prose-invert th,
237
+ .dark .prose-invert td {
238
+ border-color: rgb(var(--color-base-700));
239
+ }
240
+
241
+ .dark .prose-invert tbody tr {
242
+ border-bottom-color: rgb(var(--color-base-700));
243
+ }
244
+
245
+ .dark .prose-invert tbody tr:hover {
246
+ background-color: rgb(var(--color-base-800));
247
+ }
248
+
249
+ /* Links */
250
+ .dark .prose-invert a {
251
+ color: rgb(96 165 250) !important;
252
+ }
253
+
254
+ .dark .prose-invert a:hover {
255
+ color: rgb(147 197 253) !important;
256
+ }
257
+
258
+ /* Blockquotes with semantic colors */
259
+ .dark .prose-invert blockquote {
260
+ border-left-color: rgb(var(--color-base-700));
261
+ background-color: rgb(var(--color-base-800));
262
+ padding: 1rem;
263
+ border-radius: 0.375rem;
264
+ }
265
+
266
+ /* Lists */
267
+ .dark .prose-invert ul > li::marker,
268
+ .dark .prose-invert ol > li::marker {
269
+ color: rgb(var(--color-base-500));
270
+ }
271
+
272
+ /* Horizontal rules */
273
+ .dark .prose-invert hr {
274
+ border-color: rgb(var(--color-base-700));
275
+ }
276
+
277
+ /* Custom scrollbar with semantic colors */
278
+ .dark .prose-invert::-webkit-scrollbar {
279
+ width: 8px;
280
+ height: 8px;
281
+ }
282
+
283
+ .dark .prose-invert::-webkit-scrollbar-track {
284
+ background: rgb(var(--color-base-800));
285
+ border-radius: 4px;
286
+ }
287
+
288
+ .dark .prose-invert::-webkit-scrollbar-thumb {
289
+ background: rgb(var(--color-base-700));
290
+ border-radius: 4px;
291
+ }
292
+
293
+ .dark .prose-invert::-webkit-scrollbar-thumb:hover {
294
+ background: rgb(var(--color-base-600));
295
+ }
296
+ </style>
297
+ {% endif %}
@@ -0,0 +1,37 @@
1
+ {% load static %}
2
+ {# Markdown Documentation Block Template #}
3
+ {#
4
+ Usage:
5
+ {% include "django_admin/markdown_docs_block.html" with
6
+ content=markdown_content
7
+ title="Documentation"
8
+ collapsible=True
9
+ default_open=False
10
+ %}
11
+ #}
12
+
13
+ <div class="mb-6">
14
+ {% if collapsible %}
15
+ <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"{% if default_open %} open{% endif %}>
16
+ <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">
17
+ <span class="material-symbols-outlined text-base text-primary-600 dark:text-primary-400 group-open:rotate-90 transition-transform">chevron_right</span>
18
+ <span class="font-semibold text-sm text-font-default-light dark:text-font-default-dark">{{ title|default:"Documentation" }}</span>
19
+ <span class="text-xs text-font-subtle-light dark:text-font-subtle-dark ml-auto">{% if default_open %}Click to collapse{% else %}Click to expand{% endif %}</span>
20
+ </summary>
21
+ <div class="p-4 bg-white dark:bg-base-800"{% if max_height %} style="max-height: {{ max_height }}; overflow-y: auto;"{% endif %}>
22
+ {{ content|safe }}
23
+ </div>
24
+ </details>
25
+ {% else %}
26
+ <div class="border border-base-200 dark:border-base-700 rounded-lg overflow-hidden bg-white dark:bg-base-800 shadow-sm">
27
+ {% if title %}
28
+ <div class="px-4 py-3 bg-base-50 dark:bg-base-900 border-b border-base-200 dark:border-base-700">
29
+ <h3 class="font-semibold text-sm text-font-default-light dark:text-font-default-dark">{{ title }}</h3>
30
+ </div>
31
+ {% endif %}
32
+ <div class="p-4"{% if max_height %} style="max-height: {{ max_height }}; overflow-y: auto;"{% endif %}>
33
+ {{ content|safe }}
34
+ </div>
35
+ </div>
36
+ {% endif %}
37
+ </div>
@@ -11,6 +11,7 @@ from .decorators import (
11
11
  )
12
12
  from .displays import DateTimeDisplay, MoneyDisplay, UserDisplay
13
13
  from .html_builder import HtmlBuilder
14
+ from .markdown_renderer import MarkdownRenderer
14
15
 
15
16
  __all__ = [
16
17
  # Display utilities
@@ -23,6 +24,8 @@ __all__ = [
23
24
  "CounterBadge",
24
25
  # HTML Builder
25
26
  "HtmlBuilder",
27
+ # Markdown Renderer
28
+ "MarkdownRenderer",
26
29
  # Decorators
27
30
  "computed_field",
28
31
  "badge_field",
@@ -2,17 +2,19 @@
2
2
  Universal HTML builder for Django Admin display methods.
3
3
  """
4
4
 
5
+ from pathlib import Path
5
6
  from typing import Any, List, Optional, Union
6
7
 
7
8
  from django.utils.html import escape, format_html
8
9
  from django.utils.safestring import SafeString
9
10
 
10
11
  from ..icons import Icons
12
+ from .markdown_renderer import MarkdownRenderer
11
13
 
12
14
 
13
15
  class HtmlBuilder:
14
16
  """
15
- Universal HTML builder with Material Icons support.
17
+ Universal HTML builder with Material Icons support and Markdown rendering.
16
18
 
17
19
  Usage in admin methods:
18
20
  def stats(self, obj):
@@ -20,6 +22,9 @@ class HtmlBuilder:
20
22
  self.html.icon_text(Icons.EDIT, obj.posts_count),
21
23
  self.html.icon_text(Icons.CHAT, obj.comments_count),
22
24
  ])
25
+
26
+ def documentation(self, obj):
27
+ return self.html.markdown_docs(obj.docs_path)
23
28
  """
24
29
 
25
30
  @staticmethod
@@ -276,3 +281,91 @@ class HtmlBuilder:
276
281
  style,
277
282
  escape(str(text))
278
283
  )
284
+
285
+ @staticmethod
286
+ def markdown(
287
+ text: str,
288
+ css_class: str = "",
289
+ max_height: Optional[str] = None,
290
+ enable_plugins: bool = True
291
+ ) -> SafeString:
292
+ """
293
+ Render markdown text to beautifully styled HTML.
294
+
295
+ Delegates to MarkdownRenderer.render_markdown() for actual rendering.
296
+
297
+ Args:
298
+ text: Markdown content
299
+ css_class: Additional CSS classes
300
+ max_height: Max height with scrolling (e.g., "400px", "20rem")
301
+ enable_plugins: Enable mistune plugins (tables, strikethrough, etc.)
302
+
303
+ Usage:
304
+ # Simple markdown rendering
305
+ html.markdown("# Hello\\n\\nThis is **bold** text")
306
+
307
+ # With custom styling
308
+ html.markdown(obj.description, css_class="my-custom-class")
309
+
310
+ # With max height for long content
311
+ html.markdown(obj.documentation, max_height="500px")
312
+
313
+ Returns:
314
+ SafeString with rendered HTML
315
+ """
316
+ return MarkdownRenderer.render_markdown(
317
+ text=text,
318
+ css_class=css_class,
319
+ max_height=max_height,
320
+ enable_plugins=enable_plugins
321
+ )
322
+
323
+ @staticmethod
324
+ def markdown_docs(
325
+ content: Union[str, Path],
326
+ collapsible: bool = True,
327
+ title: str = "Documentation",
328
+ icon: str = "description",
329
+ max_height: Optional[str] = "500px",
330
+ enable_plugins: bool = True,
331
+ default_open: bool = False
332
+ ) -> SafeString:
333
+ """
334
+ Render markdown documentation from string or file with collapsible UI.
335
+
336
+ Auto-detects whether content is a file path or markdown string.
337
+
338
+ Args:
339
+ content: Markdown string or path to .md file
340
+ collapsible: Wrap in collapsible details/summary
341
+ title: Title for collapsible section
342
+ icon: Material icon name for title
343
+ max_height: Max height for scrolling
344
+ enable_plugins: Enable markdown plugins
345
+ default_open: Open by default if collapsible
346
+
347
+ Usage:
348
+ # From string with collapse
349
+ html.markdown_docs(obj.description, title="Description")
350
+
351
+ # From file
352
+ html.markdown_docs("docs/api.md", title="API Documentation")
353
+
354
+ # Simple, no collapse
355
+ html.markdown_docs(obj.notes, collapsible=False)
356
+
357
+ # Open by default
358
+ html.markdown_docs(obj.readme, default_open=True)
359
+
360
+ Returns:
361
+ Rendered markdown with beautiful Tailwind styling
362
+ """
363
+ return MarkdownRenderer.render(
364
+ content=content,
365
+ collapsible=collapsible,
366
+ title=title,
367
+ icon=icon,
368
+ max_height=max_height,
369
+ enable_plugins=enable_plugins,
370
+ default_open=default_open
371
+ )