Files

79 lines
3.4 KiB
Python

"""导出器共享工具:URL 协议校验、函数图像预算与占位 warning 文案。
导出器共享 URL 规则;HTML / DOCX 使用文档资源预算,PDF 不使用这些预算。
"""
from __future__ import annotations
from datetime import datetime
from urllib.parse import urlparse
# 链接/图片地址允许的协议;无 scheme 的相对地址视为安全,其余协议一律降级
ALLOWED_URL_SCHEMES = frozenset({"http", "https", "mailto"})
MERMAID_WARNING = "mermaid 需前端渲染,已保留为占位代码块"
RAW_HTML_WARNING = "原始 HTML 已按纯文本转义保留"
# DOCX 暂不支持静态渲染函数图像,统一回退源码占位
PLOT_PLACEHOLDER_WARNING = "函数图像:该格式暂不支持静态渲染,已保留为源码占位"
# 单篇文档允许的函数图像数量上限,超出部分回退占位,防止多图块并发采样耗尽内存/线程
MAX_FUNCTION_PLOTS = 16
# 单篇文档允许的函数图像累计 AST 节点预算,超出部分回退占位,防止组合复杂度(多图块
# × 多表达式 × 深表达式)在采样求值时长时间占满 CPU
MAX_TOTAL_PLOT_NODES = 8000
class FunctionPlotBudget:
"""函数图像文档级资源预算:数量上限 + 累计 AST 节点上限。
HTML 与 DOCX 导出器在渲染每个 function-plot 图块前先问预算,超限即回退源码占位,
不解析不采样,避免多图块组合复杂度耗尽内存/CPU。
"""
def __init__(self, max_plots: int | None = None, max_total_nodes: int | None = None) -> None:
# 默认读模块常量(便于测试 monkeypatch 常量后重新生效)
self.max_plots = MAX_FUNCTION_PLOTS if max_plots is None else max_plots
self.max_total_nodes = MAX_TOTAL_PLOT_NODES if max_total_nodes is None else max_total_nodes
self.count = 0
self.total_nodes = 0
def check_count(self) -> str | None:
"""图块数量 +1;超限返回 warning 文案,否则返回 None。"""
self.count += 1
if self.count > self.max_plots:
return f"函数图像:文档内函数图像数量超过上限 {self.max_plots},已回退为源码占位"
return None
def check_nodes(self, node_count: int) -> str | None:
"""累计节点预算校验;超限返回 warning 文案(不累加),否则累加并返回 None。"""
if self.total_nodes + node_count > self.max_total_nodes:
return f"函数图像:文档内函数图像累计复杂度超过上限 {self.max_total_nodes} 节点,已回退为源码占位"
self.total_nodes += node_count
return None
def format_plot_diagnostic(diag) -> str:
"""把解析诊断格式化为面向用户的 warning 文案。"""
loc = f"(第 {diag.line} 行)" if diag.line else ""
return f"函数图像:{diag.message}{loc}"
def safe_url(url: str) -> str | None:
"""校验 URL 协议;安全返回原串,不安全返回 None。"""
url = url.strip()
if not url:
return None
scheme = urlparse(url).scheme.lower()
if scheme and scheme not in ALLOWED_URL_SCHEMES:
return None
return url
def format_meta_value(value: object) -> str:
"""把元数据值转成可读文本:datetime 转 ISO、列表用逗号连接。"""
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, list):
return ", ".join(str(item) for item in value)
return str(value)