feat(renderer): 添加 Jinja2 inline_asset 全局函数

- 新增 `RendererService._inline_asset_global` 方法,并注册为 Jinja2 全局函数 `inline_asset`。
- 允许模板通过 `{{ inline_asset('@namespace/path/to/asset.svg') }}` 直接内联已注册命名空间下的资源文件内容。
- 主要用于解决内联 SVG 时可能遇到的跨域安全问题。
- 【重构】优化 `ResourceResolver.resolve_asset_uri` 中对命名空间资源 (以 `@` 开头) 的解析逻辑,确保能够正确获取文件绝对路径并返回 URI。
- 改进 `RenderableComponent.get_extra_css`,使其在组件定义 `component_css` 时自动返回该 CSS 内容。
- 清理 `Renderable` 协议和 `RenderableComponent` 基类中已存在方法的 `[新增]` 标记。
This commit is contained in:
webjoin111 2025-11-11 21:42:01 +08:00
parent 3ee0f6f2b1
commit 2581e335af
4 changed files with 59 additions and 22 deletions

View File

@ -40,7 +40,7 @@ class Renderable(ABC):
@abstractmethod
def get_children(self) -> Iterable["Renderable"]:
"""
[新增] 返回一个包含所有直接子组件的可迭代对象
返回一个包含所有直接子组件的可迭代对象
这使得渲染服务能够递归地遍历整个组件树以执行依赖收集CSSJS等任务
非容器组件应返回一个空列表

View File

@ -75,6 +75,7 @@ class RendererService:
self._custom_globals: dict[str, Callable] = {}
self.filter("dump_json")(self._pydantic_tojson_filter)
self.global_function("inline_asset")(self._inline_asset_global)
def _create_jinja_env(self) -> Environment:
"""
@ -176,9 +177,24 @@ class RendererService:
return decorator
async def _inline_asset_global(self, namespaced_path: str) -> str:
"""
一个Jinja2全局函数用于读取并内联一个已注册命名空间下的资源文件内容
主要用于内联SVG以解决浏览器的跨域安全问题
"""
if not self._jinja_env or not self._jinja_env.loader:
return f"<!-- Error: Jinja env not ready for {namespaced_path} -->"
try:
source, _, _ = self._jinja_env.loader.get_source(
self._jinja_env, namespaced_path
)
return source
except TemplateNotFound:
return f"<!-- Asset not found: {namespaced_path} -->"
async def initialize(self):
"""
[新增] 延迟初始化方法 on_startup 钩子中调用
延迟初始化方法 on_startup 钩子中调用
负责初始化截图引擎和主题管理器确保在首次渲染前所有依赖都已准备就绪
使用锁来防止并发初始化

View File

@ -172,24 +172,45 @@ class ResourceResolver:
if asset_path.startswith("@"):
try:
full_asset_path = self.theme_manager.jinja_env.join_path(
asset_path, current_template_name
)
_source, file_abs_path, _uptodate = (
self.theme_manager.jinja_env.loader.get_source(
self.theme_manager.jinja_env, full_asset_path
if "/" not in asset_path:
raise TemplateNotFound(f"无效的命名空间路径: {asset_path}")
namespace, rel_path = asset_path.split("/", 1)
loader = self.theme_manager.jinja_env.loader
if (
isinstance(loader, ChoiceLoader)
and loader.loaders
and isinstance(loader.loaders[0], PrefixLoader)
):
prefix_loader = loader.loaders[0]
if namespace in prefix_loader.mapping:
loader_for_namespace = prefix_loader.mapping[namespace]
if isinstance(loader_for_namespace, FileSystemLoader):
base_path = Path(loader_for_namespace.searchpath[0])
file_abs_path = (base_path / rel_path).resolve()
if file_abs_path.is_file():
logger.debug(
f"Resolved namespaced asset"
f" '{asset_path}' -> '{file_abs_path}'"
)
return file_abs_path.as_uri()
else:
raise TemplateNotFound(asset_path)
else:
raise TemplateNotFound(
f"Unsupported loader type for namespace '{namespace}'."
)
else:
raise TemplateNotFound(f"Namespace '{namespace}' not found.")
else:
raise TemplateNotFound(
f"无法解析命名空间资源 '{asset_path}',加载器结构不符合预期。"
)
)
if file_abs_path:
logger.debug(
f"Jinja Loader resolved asset '{asset_path}'->'{file_abs_path}'"
)
return Path(file_abs_path).absolute().as_uri()
except TemplateNotFound:
logger.warning(
f"资源文件在命名空间中未找到: '{asset_path}'"
f"(在模板 '{current_template_name}' 中引用)"
)
logger.warning(f"资源文件在命名空间中未找到: '{asset_path}'")
return ""
search_paths: list[tuple[str, Path]] = []

View File

@ -64,13 +64,13 @@ class RenderableComponent(BaseModel, Renderable):
@compat_computed_field
def inline_style_str(self) -> str:
"""[新增] 一个辅助属性将内联样式字典转换为CSS字符串"""
"""一个辅助属性将内联样式字典转换为CSS字符串"""
if not self.inline_style:
return ""
return "; ".join(f"{k}: {v}" for k, v in self.inline_style.items())
def get_extra_css(self, context: Any) -> str | Awaitable[str]:
return ""
return self.component_css or ""
class ContainerComponent(RenderableComponent, ABC):
@ -86,7 +86,7 @@ class ContainerComponent(RenderableComponent, ABC):
raise NotImplementedError
def get_required_scripts(self) -> list[str]:
"""[新增] 聚合所有子组件的脚本依赖。"""
"""聚合所有子组件的脚本依赖。"""
scripts = set(super().get_required_scripts())
for child in self.get_children():
if child:
@ -94,7 +94,7 @@ class ContainerComponent(RenderableComponent, ABC):
return list(scripts)
def get_required_styles(self) -> list[str]:
"""[新增] 聚合所有子组件的样式依赖。"""
"""聚合所有子组件的样式依赖。"""
styles = set(super().get_required_styles())
for child in self.get_children():
if child: