caspian-utils 0.0.12__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.
- casp/__init__.py +0 -0
- casp/auth.py +537 -0
- casp/cache_handler.py +180 -0
- casp/caspian_config.py +441 -0
- casp/component_decorator.py +183 -0
- casp/components_compiler.py +293 -0
- casp/html_attrs.py +93 -0
- casp/layout.py +474 -0
- casp/loading.py +25 -0
- casp/rpc.py +230 -0
- casp/scripts_type.py +21 -0
- casp/state_manager.py +134 -0
- casp/string_helpers.py +18 -0
- casp/tw.py +31 -0
- casp/validate.py +747 -0
- caspian_utils-0.0.12.dist-info/METADATA +214 -0
- caspian_utils-0.0.12.dist-info/RECORD +19 -0
- caspian_utils-0.0.12.dist-info/WHEEL +5 -0
- caspian_utils-0.0.12.dist-info/top_level.txt +1 -0
casp/layout.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Callable
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import hashlib
|
|
6
|
+
import importlib.util
|
|
7
|
+
import inspect
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
|
+
from jinja2 import Environment, BaseLoader, select_autoescape
|
|
11
|
+
from markupsafe import Markup
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LayoutEngine:
|
|
15
|
+
"""
|
|
16
|
+
Single source of truth for:
|
|
17
|
+
- Jinja environment (Caspian delimiters)
|
|
18
|
+
- Template loading (file + relative-to-py)
|
|
19
|
+
- Rendering (string + page + layout)
|
|
20
|
+
- Nested layout system (Next.js style layout discovery and wrapping)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.env = Environment(
|
|
25
|
+
loader=BaseLoader(),
|
|
26
|
+
autoescape=select_autoescape(["html", "xml"]),
|
|
27
|
+
variable_start_string="[[",
|
|
28
|
+
variable_end_string="]]",
|
|
29
|
+
comment_start_string="[#",
|
|
30
|
+
comment_end_string="#]",
|
|
31
|
+
block_start_string="[%", # NOTE: matches your existing config
|
|
32
|
+
block_end_string="%]",
|
|
33
|
+
)
|
|
34
|
+
self.env.filters["json"] = self._to_safe_json
|
|
35
|
+
self.env.filters["dump"] = self._to_safe_json
|
|
36
|
+
|
|
37
|
+
# Legacy alias expected elsewhere
|
|
38
|
+
self.string_env = self.env
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------
|
|
41
|
+
# Jinja helpers
|
|
42
|
+
# ---------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def _to_safe_json(value: Any) -> Markup:
|
|
46
|
+
"""Safely serialize to JSON for HTML/JS embedding."""
|
|
47
|
+
s = json.dumps(value, ensure_ascii=False, default=str)
|
|
48
|
+
escaped = (
|
|
49
|
+
s.replace("</", "<\\/")
|
|
50
|
+
.replace("<!--", "<\\!--")
|
|
51
|
+
.replace("\u2028", "\\u2028")
|
|
52
|
+
.replace("\u2029", "\\u2029")
|
|
53
|
+
)
|
|
54
|
+
return Markup(escaped)
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------
|
|
57
|
+
# Core template loading & rendering (moved from render.py)
|
|
58
|
+
# ---------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
def load_template_file(self, file_path: str) -> str:
|
|
61
|
+
"""Load template file directly by path."""
|
|
62
|
+
if not os.path.exists(file_path):
|
|
63
|
+
raise FileNotFoundError(f"Template not found: {file_path}")
|
|
64
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
65
|
+
return f.read()
|
|
66
|
+
|
|
67
|
+
def load_template(self, py_file: str, template_name: Optional[str] = None) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Load HTML template relative to a Python file.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
py_file: Pass __file__ from calling module
|
|
73
|
+
template_name: Template filename (auto-detected if None)
|
|
74
|
+
"""
|
|
75
|
+
dir_path = os.path.dirname(os.path.abspath(py_file))
|
|
76
|
+
|
|
77
|
+
if template_name is None:
|
|
78
|
+
template_name = os.path.basename(py_file).replace(".py", ".html")
|
|
79
|
+
|
|
80
|
+
html_path = os.path.join(dir_path, template_name)
|
|
81
|
+
return self.load_template_file(html_path)
|
|
82
|
+
|
|
83
|
+
def render(
|
|
84
|
+
self,
|
|
85
|
+
py_file: str,
|
|
86
|
+
context: Optional[Dict[str, Any]] = None,
|
|
87
|
+
template_name: Optional[str] = None,
|
|
88
|
+
) -> str:
|
|
89
|
+
"""Unified render function for pages and layouts."""
|
|
90
|
+
context = context or {}
|
|
91
|
+
template_str = self.load_template(py_file, template_name)
|
|
92
|
+
template = self.env.from_string(template_str)
|
|
93
|
+
return template.render(**context)
|
|
94
|
+
|
|
95
|
+
def render_page(self, py_file: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
96
|
+
"""Render index.html with context."""
|
|
97
|
+
return self.render(py_file, context, "index.html")
|
|
98
|
+
|
|
99
|
+
def render_layout(self, py_file: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
100
|
+
"""Render layout.html with context."""
|
|
101
|
+
if context is None:
|
|
102
|
+
# Optimization: No context? Just load the raw file (preserves all tags)
|
|
103
|
+
return self.load_template(py_file, "layout.html")
|
|
104
|
+
|
|
105
|
+
# Preserve [[ children ]] tag if caller didn't provide children
|
|
106
|
+
if "children" not in context:
|
|
107
|
+
context["children"] = Markup("[[ children ]]")
|
|
108
|
+
|
|
109
|
+
return self.render(py_file, context, "layout.html")
|
|
110
|
+
|
|
111
|
+
def render_string(self, template_str: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
112
|
+
"""Render a template string directly."""
|
|
113
|
+
context = context or {}
|
|
114
|
+
template = self.env.from_string(template_str)
|
|
115
|
+
return template.render(**context)
|
|
116
|
+
|
|
117
|
+
# ---------------------------------------------------------------------
|
|
118
|
+
# Layout system (existing behavior, now using this engine as source)
|
|
119
|
+
# ---------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class LayoutInfo:
|
|
123
|
+
dir_path: str
|
|
124
|
+
has_py: bool
|
|
125
|
+
has_html: bool
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def layout_id(self) -> str:
|
|
129
|
+
return hashlib.md5(self.dir_path.encode()).hexdigest()[:8]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def py_path(self) -> str:
|
|
133
|
+
return os.path.join(self.dir_path, "layout.py")
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def html_path(self) -> str:
|
|
137
|
+
return os.path.join(self.dir_path, "layout.html")
|
|
138
|
+
|
|
139
|
+
@dataclass
|
|
140
|
+
class LayoutResult:
|
|
141
|
+
content: str
|
|
142
|
+
props: Dict[str, Any] = field(default_factory=dict)
|
|
143
|
+
|
|
144
|
+
def discover_layouts(self, route_dir: str) -> List["LayoutEngine.LayoutInfo"]:
|
|
145
|
+
"""Find all layouts from root to route (Next.js style)."""
|
|
146
|
+
route_dir = route_dir.replace("\\", "/")
|
|
147
|
+
rel_path = os.path.relpath(route_dir, "src/app").replace("\\", "/")
|
|
148
|
+
|
|
149
|
+
layouts: List[LayoutEngine.LayoutInfo] = []
|
|
150
|
+
|
|
151
|
+
def check_dir(dir_path: str) -> Optional[LayoutEngine.LayoutInfo]:
|
|
152
|
+
dir_path = dir_path.replace("\\", "/")
|
|
153
|
+
html_exists = os.path.exists(os.path.join(dir_path, "layout.html"))
|
|
154
|
+
py_exists = os.path.exists(os.path.join(dir_path, "layout.py"))
|
|
155
|
+
|
|
156
|
+
if html_exists or py_exists:
|
|
157
|
+
return LayoutEngine.LayoutInfo(
|
|
158
|
+
dir_path=dir_path, has_py=py_exists, has_html=html_exists
|
|
159
|
+
)
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# Root layout
|
|
163
|
+
root = check_dir("src/app")
|
|
164
|
+
if root:
|
|
165
|
+
layouts.append(root)
|
|
166
|
+
|
|
167
|
+
# Path segments
|
|
168
|
+
current = "src/app"
|
|
169
|
+
for segment in rel_path.split("/"):
|
|
170
|
+
if segment in (".", "src", "app", ""):
|
|
171
|
+
continue
|
|
172
|
+
current = os.path.join(current, segment).replace("\\", "/")
|
|
173
|
+
found = check_dir(current)
|
|
174
|
+
if found:
|
|
175
|
+
layouts.append(found)
|
|
176
|
+
|
|
177
|
+
return layouts
|
|
178
|
+
|
|
179
|
+
def load_layout_module(self, py_path: str):
|
|
180
|
+
"""Dynamically load layout.py module."""
|
|
181
|
+
if not os.path.exists(py_path):
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
module_name = f"layout_{hashlib.md5(py_path.encode()).hexdigest()}"
|
|
185
|
+
spec = importlib.util.spec_from_file_location(module_name, py_path)
|
|
186
|
+
|
|
187
|
+
if spec and spec.loader:
|
|
188
|
+
module = importlib.util.module_from_spec(spec)
|
|
189
|
+
spec.loader.exec_module(module)
|
|
190
|
+
return module
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
def resolve_layout(self, layout: "LayoutEngine.LayoutInfo", context: Dict[str, Any]) -> "LayoutEngine.LayoutResult":
|
|
194
|
+
"""
|
|
195
|
+
Resolve layout content:
|
|
196
|
+
1. layout.py exists → execute it (MUST have layout() function)
|
|
197
|
+
2. layout.html only → load it directly
|
|
198
|
+
3. Neither → passthrough
|
|
199
|
+
"""
|
|
200
|
+
passthrough = "[[ children | safe ]]"
|
|
201
|
+
|
|
202
|
+
# Priority 1: layout.py
|
|
203
|
+
if layout.has_py:
|
|
204
|
+
module = self.load_layout_module(layout.py_path)
|
|
205
|
+
|
|
206
|
+
if module and not hasattr(module, "layout"):
|
|
207
|
+
raise AttributeError(
|
|
208
|
+
f"The file '{layout.py_path}' is missing the required 'def layout():' function."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if module:
|
|
212
|
+
# Auto-extract module-level variables (Single Source of Truth)
|
|
213
|
+
module_props: Dict[str, Any] = {}
|
|
214
|
+
|
|
215
|
+
if hasattr(module, "title"):
|
|
216
|
+
module_props["title"] = getattr(module, "title")
|
|
217
|
+
if hasattr(module, "description"):
|
|
218
|
+
module_props["description"] = getattr(
|
|
219
|
+
module, "description")
|
|
220
|
+
|
|
221
|
+
if hasattr(module, "metadata") and isinstance(module.metadata, dict):
|
|
222
|
+
module_props.update(module.metadata)
|
|
223
|
+
|
|
224
|
+
sig = inspect.signature(module.layout)
|
|
225
|
+
result = module.layout(
|
|
226
|
+
context) if sig.parameters else module.layout()
|
|
227
|
+
|
|
228
|
+
if result is None:
|
|
229
|
+
if layout.has_html:
|
|
230
|
+
return LayoutEngine.LayoutResult(self.load_template_file(layout.html_path), module_props)
|
|
231
|
+
return LayoutEngine.LayoutResult(passthrough, module_props)
|
|
232
|
+
|
|
233
|
+
# Case A: User returns string (HTML)
|
|
234
|
+
if isinstance(result, str):
|
|
235
|
+
return LayoutEngine.LayoutResult(result, module_props)
|
|
236
|
+
|
|
237
|
+
# Case B: User returns tuple (HTML, props)
|
|
238
|
+
if isinstance(result, tuple) and len(result) == 2:
|
|
239
|
+
combined_props = {**module_props, **(result[1] or {})}
|
|
240
|
+
return LayoutEngine.LayoutResult(result[0], combined_props)
|
|
241
|
+
|
|
242
|
+
# Case C: User returns dict (Context for html file)
|
|
243
|
+
if isinstance(result, dict):
|
|
244
|
+
content = self.load_template_file(
|
|
245
|
+
layout.html_path) if layout.has_html else passthrough
|
|
246
|
+
return LayoutEngine.LayoutResult(content, {**module_props, **result})
|
|
247
|
+
|
|
248
|
+
if layout.has_html:
|
|
249
|
+
return LayoutEngine.LayoutResult(self.load_template_file(layout.html_path))
|
|
250
|
+
return LayoutEngine.LayoutResult(passthrough)
|
|
251
|
+
|
|
252
|
+
# Priority 2: layout.html only
|
|
253
|
+
if layout.has_html:
|
|
254
|
+
return LayoutEngine.LayoutResult(self.load_template_file(layout.html_path))
|
|
255
|
+
|
|
256
|
+
return LayoutEngine.LayoutResult(passthrough)
|
|
257
|
+
|
|
258
|
+
def load_layout(self, py_or_html_path: str) -> str:
|
|
259
|
+
"""Load layout or page HTML file."""
|
|
260
|
+
if py_or_html_path.endswith(".html"):
|
|
261
|
+
return self.load_template_file(py_or_html_path)
|
|
262
|
+
|
|
263
|
+
dir_path = os.path.dirname(os.path.abspath(py_or_html_path))
|
|
264
|
+
base_name = os.path.splitext(os.path.basename(py_or_html_path))[0]
|
|
265
|
+
html_path = os.path.join(dir_path, f"{base_name}.html")
|
|
266
|
+
|
|
267
|
+
return self.load_template_file(html_path)
|
|
268
|
+
|
|
269
|
+
def get_loading_files(self) -> str:
|
|
270
|
+
"""Get loading indicator HTML."""
|
|
271
|
+
try:
|
|
272
|
+
from .loading import get_loading_files as _get_loading
|
|
273
|
+
return _get_loading()
|
|
274
|
+
except ImportError:
|
|
275
|
+
return ""
|
|
276
|
+
|
|
277
|
+
def _inject_meta(self, html: str, layout_id: str) -> str:
|
|
278
|
+
"""Inject root layout meta tag."""
|
|
279
|
+
meta = f'<meta name="pp-root-layout" content="{layout_id}">'
|
|
280
|
+
if "<head>" in html:
|
|
281
|
+
return html.replace("<head>", f"<head>\n {meta}")
|
|
282
|
+
return html
|
|
283
|
+
|
|
284
|
+
def render_with_nested_layouts(
|
|
285
|
+
self,
|
|
286
|
+
children: str,
|
|
287
|
+
route_dir: str,
|
|
288
|
+
title: Optional[str] = None,
|
|
289
|
+
description: Optional[str] = None,
|
|
290
|
+
context_data: Optional[Dict[str, Any]] = None,
|
|
291
|
+
transform_fn=None,
|
|
292
|
+
component_compiler: Optional[Callable] = None,
|
|
293
|
+
) -> Tuple[str, str]:
|
|
294
|
+
"""
|
|
295
|
+
Render page wrapped in nested layouts with strict precedence:
|
|
296
|
+
Page > Layout > Default
|
|
297
|
+
"""
|
|
298
|
+
context = context_data or {}
|
|
299
|
+
|
|
300
|
+
layouts = self.discover_layouts(route_dir)
|
|
301
|
+
root_id = layouts[0].layout_id if layouts else "default"
|
|
302
|
+
|
|
303
|
+
if not layouts:
|
|
304
|
+
html = children + self.get_loading_files()
|
|
305
|
+
if transform_fn:
|
|
306
|
+
html = transform_fn(html)
|
|
307
|
+
return html, root_id
|
|
308
|
+
|
|
309
|
+
current_html = children
|
|
310
|
+
accumulated_props: Dict[str, Any] = {}
|
|
311
|
+
|
|
312
|
+
# Render leaf → root
|
|
313
|
+
for i, layout in enumerate(reversed(layouts)):
|
|
314
|
+
is_root = (i == len(layouts) - 1)
|
|
315
|
+
|
|
316
|
+
result = self.resolve_layout(layout, context)
|
|
317
|
+
layer_content = result.content
|
|
318
|
+
|
|
319
|
+
if component_compiler:
|
|
320
|
+
layer_content = component_compiler(
|
|
321
|
+
layer_content, base_dir=layout.dir_path)
|
|
322
|
+
|
|
323
|
+
# Child layouts override parent layouts
|
|
324
|
+
accumulated_props = {**result.props, **accumulated_props}
|
|
325
|
+
|
|
326
|
+
# Page > Layout > Default
|
|
327
|
+
effective_title = title if title is not None else accumulated_props.get(
|
|
328
|
+
"title", "App")
|
|
329
|
+
effective_desc = description if description is not None else accumulated_props.get(
|
|
330
|
+
"description", "")
|
|
331
|
+
|
|
332
|
+
layer_context = {
|
|
333
|
+
**accumulated_props,
|
|
334
|
+
"title": effective_title,
|
|
335
|
+
"description": effective_desc,
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if is_root:
|
|
339
|
+
current_html += self.get_loading_files()
|
|
340
|
+
layer_content = self._inject_meta(layer_content, root_id)
|
|
341
|
+
|
|
342
|
+
template = self.env.from_string(layer_content)
|
|
343
|
+
current_html = template.render(
|
|
344
|
+
children=Markup(current_html),
|
|
345
|
+
layout=layer_context,
|
|
346
|
+
**context,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if transform_fn:
|
|
350
|
+
current_html = transform_fn(current_html)
|
|
351
|
+
|
|
352
|
+
return current_html, root_id
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
# -----------------------------------------------------------------------------
|
|
356
|
+
# Singleton + module-level backward-compatible API
|
|
357
|
+
# -----------------------------------------------------------------------------
|
|
358
|
+
|
|
359
|
+
_engine = LayoutEngine()
|
|
360
|
+
|
|
361
|
+
# Jinja env exports
|
|
362
|
+
env = _engine.env
|
|
363
|
+
string_env = _engine.string_env
|
|
364
|
+
|
|
365
|
+
# Render exports (moved here from render.py)
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def load_template_file(file_path: str) -> str:
|
|
369
|
+
return _engine.load_template_file(file_path)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def load_template(py_file: str, template_name: Optional[str] = None) -> str:
|
|
373
|
+
return _engine.load_template(py_file, template_name)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def render(py_file: str, context: Optional[Dict[str, Any]] = None, template_name: Optional[str] = None) -> str:
|
|
377
|
+
return _engine.render(py_file, context, template_name)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def render_page(py_file: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
381
|
+
return _engine.render_page(py_file, context)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def render_layout(py_file: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
385
|
+
return _engine.render_layout(py_file, context)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def render_string(template_str: str, context: Optional[Dict[str, Any]] = None) -> str:
|
|
389
|
+
return _engine.render_string(template_str, context)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# Layout system exports
|
|
393
|
+
LayoutInfo = LayoutEngine.LayoutInfo
|
|
394
|
+
LayoutResult = LayoutEngine.LayoutResult
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def discover_layouts(route_dir: str) -> List[LayoutInfo]:
|
|
398
|
+
return _engine.discover_layouts(route_dir)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def load_layout_module(py_path: str):
|
|
402
|
+
return _engine.load_layout_module(py_path)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def resolve_layout(layout: LayoutInfo, context: Dict[str, Any]) -> LayoutResult:
|
|
406
|
+
return _engine.resolve_layout(layout, context)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_loading_files() -> str:
|
|
410
|
+
return _engine.get_loading_files()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def render_with_nested_layouts(
|
|
414
|
+
children: str,
|
|
415
|
+
route_dir: str,
|
|
416
|
+
title: Optional[str] = None,
|
|
417
|
+
description: Optional[str] = None,
|
|
418
|
+
context_data: Optional[Dict[str, Any]] = None,
|
|
419
|
+
transform_fn=None,
|
|
420
|
+
component_compiler: Optional[Callable] = None,
|
|
421
|
+
) -> Tuple[str, str]:
|
|
422
|
+
return _engine.render_with_nested_layouts(
|
|
423
|
+
children=children,
|
|
424
|
+
route_dir=route_dir,
|
|
425
|
+
title=title,
|
|
426
|
+
description=description,
|
|
427
|
+
context_data=context_data,
|
|
428
|
+
transform_fn=transform_fn,
|
|
429
|
+
component_compiler=component_compiler,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# Back-compat alias (your old __all__ referenced this name)
|
|
434
|
+
render_with_layouts = render_with_nested_layouts
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def load_layout(py_or_html_path: str) -> str:
|
|
438
|
+
return _engine.load_layout(py_or_html_path)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def load_page(py_file_path):
|
|
442
|
+
"""Load HTML file next to the Python file"""
|
|
443
|
+
dir_path = os.path.dirname(py_file_path)
|
|
444
|
+
html_path = os.path.join(dir_path, 'index.html')
|
|
445
|
+
with open(html_path, 'r', encoding='utf-8') as f:
|
|
446
|
+
return f.read()
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
__all__ = [
|
|
450
|
+
# Engine (optional export)
|
|
451
|
+
"LayoutEngine",
|
|
452
|
+
|
|
453
|
+
# Jinja env
|
|
454
|
+
"env",
|
|
455
|
+
"string_env",
|
|
456
|
+
|
|
457
|
+
# Render (single truth)
|
|
458
|
+
"load_template_file",
|
|
459
|
+
"load_template",
|
|
460
|
+
"render",
|
|
461
|
+
"render_page",
|
|
462
|
+
"render_layout",
|
|
463
|
+
"render_string",
|
|
464
|
+
|
|
465
|
+
# Layout system
|
|
466
|
+
"LayoutInfo",
|
|
467
|
+
"LayoutResult",
|
|
468
|
+
"discover_layouts",
|
|
469
|
+
"resolve_layout",
|
|
470
|
+
"load_layout_module",
|
|
471
|
+
"render_with_nested_layouts",
|
|
472
|
+
"render_with_layouts",
|
|
473
|
+
"load_layout",
|
|
474
|
+
]
|
casp/loading.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .caspian_config import get_files_index
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
ROUTE_FILES_PATH = './settings/files-list.json'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_route_files():
|
|
9
|
+
if os.path.exists(ROUTE_FILES_PATH):
|
|
10
|
+
with open(ROUTE_FILES_PATH, 'r', encoding='utf-8') as f:
|
|
11
|
+
return json.load(f)
|
|
12
|
+
return []
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_loading_files():
|
|
16
|
+
idx = get_files_index()
|
|
17
|
+
loading_content = ''
|
|
18
|
+
|
|
19
|
+
for loading in idx.loadings:
|
|
20
|
+
if os.path.exists(loading.file.lstrip('./')):
|
|
21
|
+
with open(loading.file.lstrip('./'), 'r', encoding='utf-8') as f:
|
|
22
|
+
content = f.read()
|
|
23
|
+
loading_content += f'<div pp-loading-url="{loading.url_scope}">{content}</div>'
|
|
24
|
+
|
|
25
|
+
return f'<div style="display: none;" id="loading-file-1B87E">{loading_content}</div>' if loading_content else ''
|