"""Render a CogEvol / OpenMAIC slide scene-graph JSON into a self-contained HTML page.
The contract implemented here follows the canonical OpenMAIC slide object model
(`packages/@openmaic/dsl/src/slides.ts`, MIT, THU-MAIC): a 1000x562 canvas with
absolutely-positioned `text | shape | line | image | table | chart | latex | video | code`
elements plus a `background` object.
"""
from __future__ import annotations
import html
import json
from typing import Any
CANVAS_W = 1000
CANVAS_H = 562
DEFAULT_THEME_COLORS = [
"#5b9bd5",
"#ed7d31",
"#a5a5a5",
"#ffc000",
"#4472c4",
"#70ad47",
"#264478",
"#9e480e",
]
FONT_STACK = (
"'Microsoft YaHei','PingFang SC','Hiragino Sans GB','Noto Sans CJK SC',"
"'Source Han Sans SC','WenQuanYi Micro Hei',Inter,Arial,sans-serif"
)
def _num(value: Any, default: float = 0.0) -> float:
try:
if isinstance(value, bool):
return default
return float(value)
except (TypeError, ValueError):
return default
def _esc(value: Any) -> str:
return html.escape("" if value is None else str(value), quote=True)
def _css_outline(outline: dict | None, fallback: str = "") -> str:
if not isinstance(outline, dict):
return fallback
width = _num(outline.get("width"), 1)
style = outline.get("style") or "solid"
color = outline.get("color") or "#000000"
if width <= 0:
return fallback
return f"border:{width}px {_esc(style)} {_esc(color)};"
def _shadow(shadow: dict | None) -> str:
if not isinstance(shadow, dict):
return ""
return (
f"filter:drop-shadow({_num(shadow.get('h'))}px {_num(shadow.get('v'))}px "
f"{_num(shadow.get('blur'))}px {_esc(shadow.get('color') or 'rgba(0,0,0,.3)')});"
)
def _base_box(el: dict, extra: str = "") -> str:
left, top = _num(el.get("left")), _num(el.get("top"))
width, height = _num(el.get("width"), 100), _num(el.get("height"), 40)
rotate = _num(el.get("rotate"))
style = (
f"position:absolute;left:{left}px;top:{top}px;"
f"width:{width}px;height:{height}px;"
)
if rotate:
style += f"transform:rotate({rotate}deg);"
opacity = el.get("opacity")
if isinstance(opacity, (int, float)) and opacity != 1:
style += f"opacity:{opacity};"
return style + extra
def _gradient_css(gradient: dict) -> str:
colors = gradient.get("colors") or []
stops = ", ".join(
f"{_esc(c.get('color') or '#ffffff')} {_num(c.get('pos'))}%"
for c in colors
if isinstance(c, dict)
)
if not stops:
return ""
if (gradient.get("type") or "linear") == "radial":
return f"radial-gradient(circle, {stops})"
return f"linear-gradient({_num(gradient.get('rotate'))}deg, {stops})"
# --------------------------------------------------------------------------- text
def _render_text(el: dict) -> str:
style = _base_box(el)
style += f"color:{_esc(el.get('defaultColor') or '#333333')};"
font = el.get("defaultFontName")
if font:
style += f"font-family:{_esc(font)},{FONT_STACK};"
if el.get("fill"):
style += f"background:{_esc(el['fill'])};"
style += _css_outline(el.get("outline"))
style += f"line-height:{_num(el.get('lineHeight'), 1.5)};"
if el.get("wordSpace"):
style += f"letter-spacing:{_num(el.get('wordSpace'))}px;"
style += _shadow(el.get("shadow"))
if el.get("vertical"):
style += "writing-mode:vertical-rl;"
valign = el.get("vAlign") or "top"
flex = {"top": "flex-start", "middle": "center", "bottom": "flex-end"}.get(valign, "flex-start")
style += f"display:flex;flex-direction:column;justify-content:{flex};overflow:visible;"
content = el.get("content") or ""
return f'
{content}
'
# -------------------------------------------------------------------------- shape
def _render_shape(el: dict, idx: int) -> str:
view_box = el.get("viewBox") or [200, 200]
if isinstance(view_box, (int, float)):
view_box = [view_box, view_box]
vb_w = _num(view_box[0] if len(view_box) > 0 else 200, 200)
vb_h = _num(view_box[1] if len(view_box) > 1 else vb_w, vb_w)
path = el.get("path") or "M 0 0 L 200 0 L 200 200 L 0 200 Z"
fill = el.get("fill") or "#5b9bd5"
defs = ""
fill_attr = _esc(fill)
gradient = el.get("gradient")
if isinstance(gradient, dict) and gradient.get("colors"):
gid = f"grad{idx}"
stops = "".join(
f''
for c in gradient["colors"]
if isinstance(c, dict)
)
if (gradient.get("type") or "linear") == "radial":
defs = f'{stops}'
else:
rot = _num(gradient.get("rotate"))
defs = (
f''
f"{stops}"
)
fill_attr = f"url(#{gid})"
outline = el.get("outline") or {}
stroke = ""
if isinstance(outline, dict) and outline.get("color"):
ow = _num(outline.get("width"), 1)
dash = {"dashed": "10 6", "dotted": "2 4"}.get(outline.get("style") or "solid", "")
stroke = f' stroke="{_esc(outline["color"])}" stroke-width="{ow}"'
if dash:
stroke += f' stroke-dasharray="{dash}"'
flip = ""
if el.get("flipH") or el.get("flipV"):
sx, sy = (-1 if el.get("flipH") else 1), (-1 if el.get("flipV") else 1)
flip = f"transform:scale({sx},{sy});"
svg = (
f''
)
inner = ""
text = el.get("text")
if isinstance(text, dict) and text.get("content"):
align = {"top": "flex-start", "middle": "center", "bottom": "flex-end"}.get(
text.get("align") or "middle", "center"
)
t_style = (
"position:absolute;inset:0;display:flex;flex-direction:column;"
f"justify-content:{align};padding:6px;box-sizing:border-box;"
f"color:{_esc(text.get('defaultColor') or '#333333')};"
f"line-height:{_num(text.get('lineHeight'), 1.5)};"
)
if text.get("defaultFontName"):
t_style += f"font-family:{_esc(text['defaultFontName'])},{FONT_STACK};"
inner = f'
{text["content"]}
'
style = _base_box(el, _shadow(el.get("shadow")))
return f'
{svg}{inner}
'
# --------------------------------------------------------------------------- line
def _render_line(el: dict, idx: int) -> str:
start = el.get("start") or [0, 0]
end = el.get("end") or [100, 0]
sx, sy = _num(start[0] if len(start) > 0 else 0), _num(start[1] if len(start) > 1 else 0)
ex, ey = _num(end[0] if len(end) > 0 else 0), _num(end[1] if len(end) > 1 else 0)
ctrl_points = []
for key in ("broken", "broken2", "curve"):
pt = el.get(key)
if isinstance(pt, (list, tuple)) and len(pt) >= 2:
ctrl_points.append((_num(pt[0]), _num(pt[1])))
cubic = el.get("cubic")
if isinstance(cubic, (list, tuple)) and len(cubic) == 2:
for pt in cubic:
if isinstance(pt, (list, tuple)) and len(pt) >= 2:
ctrl_points.append((_num(pt[0]), _num(pt[1])))
stroke_w = max(_num(el.get("width"), 2), 1)
xs = [sx, ex] + [p[0] for p in ctrl_points]
ys = [sy, ey] + [p[1] for p in ctrl_points]
pad = stroke_w * 3 + 6
svg_w = max(xs) - min(min(xs), 0) + pad
svg_h = max(ys) - min(min(ys), 0) + pad
if cubic and len(ctrl_points) >= 2:
c1, c2 = ctrl_points[-2], ctrl_points[-1]
d = f"M {sx} {sy} C {c1[0]} {c1[1]} {c2[0]} {c2[1]} {ex} {ey}"
elif el.get("curve") and ctrl_points:
c = ctrl_points[0]
d = f"M {sx} {sy} Q {c[0]} {c[1]} {ex} {ey}"
elif el.get("broken") and ctrl_points:
pts = " ".join(f"L {p[0]} {p[1]}" for p in ctrl_points)
d = f"M {sx} {sy} {pts} L {ex} {ey}"
else:
d = f"M {sx} {sy} L {ex} {ey}"
color = _esc(el.get("color") or "#333333")
dash = {"dashed": stroke_w * 5, "dotted": stroke_w * 2}.get(el.get("style") or "solid")
dash_attr = f' stroke-dasharray="{dash} {dash}"' if dash else ""
points = el.get("points") or ["", ""]
p_start = points[0] if len(points) > 0 else ""
p_end = points[1] if len(points) > 1 else ""
defs, marker_attrs = [], ""
for name, kind, is_start in (("s", p_start, True), ("e", p_end, False)):
if kind not in ("arrow", "dot"):
continue
mid = f"m{idx}{name}"
if kind == "arrow":
body = f''
if is_start:
body = f''
defs.append(
f'{body}'
)
else:
defs.append(
f''
)
marker_attrs += f' marker-{"start" if is_start else "end"}="url(#{mid})"'
defs_str = f"{''.join(defs)}" if defs else ""
left, top = _num(el.get("left")), _num(el.get("top"))
style = f"position:absolute;left:{left}px;top:{top}px;width:{svg_w}px;height:{svg_h}px;overflow:visible;"
return (
f'
'
f'
'
)
# -------------------------------------------------------------------------- table
def _render_table(el: dict) -> str:
data = el.get("data") or []
col_widths = el.get("colWidths") or []
n_cols = max((len(r) for r in data if isinstance(r, list)), default=1)
if len(col_widths) != n_cols or not col_widths:
col_widths = [1.0 / max(n_cols, 1)] * n_cols
total = sum(_num(w, 0) for w in col_widths) or 1.0
cols = "".join(f'
' for w in col_widths)
outline = el.get("outline") or {}
border = "1px solid #cbd5e1"
if isinstance(outline, dict):
border = (
f"{_num(outline.get('width'), 1)}px {_esc(outline.get('style') or 'solid')} "
f"{_esc(outline.get('color') or '#cbd5e1')}"
)
min_h = _num(el.get("cellMinHeight"), 36)
row_heights = el.get("rowHeights") or []
rows_html = []
for r_i, row in enumerate(data):
if not isinstance(row, list):
continue
h = _num(row_heights[r_i], min_h) if r_i < len(row_heights) else min_h
cells = []
for cell in row:
if not isinstance(cell, dict):
cell = {"text": str(cell)}
st = cell.get("style") or {}
css = f"border:{border};padding:{_esc(cell.get('padding') or '4px 8px')};"
css += f"vertical-align:{_esc(cell.get('vAlign') or 'middle')};"
if st.get("bold"):
css += "font-weight:700;"
if st.get("em"):
css += "font-style:italic;"
deco = []
if st.get("underline"):
deco.append("underline")
if st.get("strikethrough"):
deco.append("line-through")
if deco:
css += f"text-decoration:{' '.join(deco)};"
if st.get("color"):
css += f"color:{_esc(st['color'])};"
if st.get("backcolor"):
css += f"background:{_esc(st['backcolor'])};"
if st.get("fontsize"):
fs = str(st["fontsize"])
css += f"font-size:{_esc(fs if not fs.isdigit() else fs + 'px')};"
if st.get("fontname"):
css += f"font-family:{_esc(st['fontname'])},{FONT_STACK};"
if st.get("align"):
css += f"text-align:{_esc(st['align'])};"
span = ""
colspan = int(_num(cell.get("colspan"), 1)) or 1
rowspan = int(_num(cell.get("rowspan"), 1)) or 1
if colspan > 1:
span += f' colspan="{colspan}"'
if rowspan > 1:
span += f' rowspan="{rowspan}"'
text = str(cell.get("text", ""))
cells.append(f'
{text}
')
rows_html.append(f'
{"".join(cells)}
')
style = _base_box(el, "overflow:visible;")
return (
f'
'
f'
'
f"
{cols}
{''.join(rows_html)}
"
)
# -------------------------------------------------------------------------- other
def _render_image(el: dict) -> str:
src = el.get("src") or ""
style = _base_box(el, _css_outline(el.get("outline")) + _shadow(el.get("shadow")))
if el.get("radius"):
style += f"border-radius:{_num(el.get('radius'))}px;overflow:hidden;"
fit = "contain" if el.get("fixedRatio", True) else "fill"
if isinstance(src, str) and (src.startswith("http") or src.startswith("data:")):
return (
f'
'
f'
'
)
label = _esc(src) if src else "image"
return (
f'
'
f'🖼 {label}
'
)
def _render_video(el: dict) -> str:
src = el.get("src") or el.get("mediaRef") or ""
style = _base_box(el)
if isinstance(src, str) and src.startswith("http"):
return (
f'
'
f'
'
)
return f'
â–¶ {_esc(src) or "video"}
'
def _render_latex(el: dict, idx: int) -> str:
style = _base_box(el, "display:flex;align-items:center;overflow:visible;")
align = el.get("align") or "center"
justify = {"left": "flex-start", "center": "center", "right": "flex-end"}.get(align, "center")
style += f"justify-content:{justify};"
if el.get("color"):
style += f"color:{_esc(el['color'])};"
if el.get("html"):
return f'
{el["html"]}
'
return (
f'
'
f'
'
)
def _render_code(el: dict) -> str:
lines = el.get("lines") or []
text = "\n".join(
str(line.get("content", "")) if isinstance(line, dict) else str(line) for line in lines
)
style = _base_box(el, "overflow:auto;background:#1e293b;border-radius:6px;")
fs = _num(el.get("fontSize"), 14)
return (
f'
'
f'
{_esc(text)}
'
)
def _render_chart(el: dict, idx: int) -> tuple[str, dict | None]:
style = _base_box(el)
if el.get("fill"):
style += f"background:{_esc(el['fill'])};"
style += _css_outline(el.get("outline"))
div = f''
data = el.get("data") or {}
labels = [str(x) for x in (data.get("labels") or [])]
legends = [str(x) for x in (data.get("legends") or [])]
series = data.get("series") or []
if not isinstance(series, list) or not labels:
return div, None
series = [s if isinstance(s, list) else [s] for s in series]
colors = el.get("themeColors") or DEFAULT_THEME_COLORS
text_color = el.get("textColor") or "#333333"
line_color = el.get("lineColor") or "#d9d9d9"
chart_type = el.get("chartType") or "bar"
smooth = bool((el.get("options") or {}).get("lineSmooth"))
stack = "total" if (el.get("options") or {}).get("stack") else None
spec = {
"chartType": chart_type,
"labels": labels,
"legends": legends,
"series": series,
"colors": colors,
"textColor": text_color,
"lineColor": line_color,
"smooth": smooth,
"stack": stack,
"id": f"chart{idx}",
}
return div, spec
# ---------------------------------------------------------------------- assemble
_PAGE = """
__ELEMENTS__
"""
def _background_css(background: Any) -> str:
if not isinstance(background, dict):
return "background:#ffffff;"
b_type = background.get("type") or "solid"
if b_type == "gradient" and isinstance(background.get("gradient"), dict):
css = _gradient_css(background["gradient"])
if css:
return f"background:{css};"
if b_type == "image":
image = background.get("image")
src = image.get("src") if isinstance(image, dict) else background.get("imageSrc")
if isinstance(src, str) and (src.startswith("http") or src.startswith("data:")):
return f"background:#fff url('{_esc(src)}') center/cover no-repeat;"
return f"background:{_esc(background.get('color') or '#ffffff')};"
def render_slide(slide: dict) -> str:
"""Render a slide scene-graph dict into a standalone HTML document string."""
elements = slide.get("elements") or []
parts: list[str] = []
charts: list[dict] = []
for idx, el in enumerate(elements):
if not isinstance(el, dict):
continue
el_type = el.get("type")
try:
if el_type == "text":
parts.append(_render_text(el))
elif el_type == "shape":
parts.append(_render_shape(el, idx))
elif el_type == "line":
parts.append(_render_line(el, idx))
elif el_type == "table":
parts.append(_render_table(el))
elif el_type == "image":
parts.append(_render_image(el))
elif el_type == "video":
parts.append(_render_video(el))
elif el_type == "latex":
parts.append(_render_latex(el, idx))
elif el_type == "code":
parts.append(_render_code(el))
elif el_type == "chart":
div, spec = _render_chart(el, idx)
parts.append(div)
if spec:
charts.append(spec)
else:
parts.append(
f'
'
f"unsupported element: {_esc(el_type)}
"
)
except Exception as exc: # a malformed element must not kill the page
parts.append(
f'