
Rq Report Renderer
- 1 installs
- 43 repo stars
- Updated June 23, 2026
- ricequant/ricequant-skills
rq-report-renderer is an agent skill that applies a RiceQuant-style HTML/CSS report shell to quant and finance outputs.
About
rq-report-renderer is an agent skill for quant developers who need consistent, professional HTML report shells instead of ad-hoc notebooks or raw tables. It encodes a RiceQuant-inspired visual system—warm paper tones, serif body type, sticky outline sidebar, and structured surfaces for tables and code—so agents can wrap backtest summaries, factor research, or portfolio analytics in a single reusable template. Use it after you have numbers or narrative copy but before you email investors, post research notes, or archive a strategy review. It is not a data pipeline or charting library; it focuses on presentation layer markup and styling conventions that make quant output readable and citable. Intermediate developers familiar with HTML/CSS and quant workflows get the most value when pairing this with their own content generation or export steps.
- CSS design tokens for paper, accent, and table-head styling suited to financial write-ups
- Two-column layout with sticky sidebar for section navigation in long reports
- Noto Serif SC / sans pairing tuned for bilingual quant narratives
- Radial and linear page backgrounds for a distinct branded report shell
- Scroll-smooth anchors for in-document section jumps
Rq Report Renderer by the numbers
- 1 all-time installs (skills.sh)
- Ranked #909 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/ricequant/ricequant-skills --skill rq-report-rendererAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 43 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 23, 2026 |
| Repository | ricequant/ricequant-skills ↗ |
What it does
Turn quant research or backtest output into polished, print-ready HTML reports with RiceQuant-style typography and layout.
Who is it for?
quant developers and small funds who publish recurring strategy or performance write-ups in HTML.
Skip if: Skip if you only need Jupyter PDF export or and want interactive charting libraries without a fixed document template.
When should I use this skill?
You need a styled HTML report shell for quant or finance content after data and copy exist.
What you get
You get a styled, navigable HTML report page ready to fill with sections, metrics, and commentary.
- Styled HTML report page structure with sidebar and typography tokens
Files
RQ 股票研究 - HTML 报告渲染器
核心原则
- 输入必须是显式的 Markdown 文件路径,不能依赖固定目录
- skill 必须自包含,渲染逻辑、样式和输入契约都放在
report-renderer/内 - 不依赖仓库级
utils - 不要求外部 Markdown 库;脚本内置最小可用解析器,覆盖当前各 skill 的真实输出结构
- 输出必须保留标题、章节、表格、列表、代码样式与数据来源标注
- 若本机支持打开浏览器,应作为可选行为,而不是强制副作用
目标产出
- 单文件 HTML 报告
- 默认与输入 Markdown 同目录同名输出
- 适合桌面阅读与打印
目录结构
report-renderer/
├── SKILL.md
├── scripts/
│ └── render_report.py
├── assets/
│ └── report.css
└── references/
└── input_contract.md输入契约
- 主输入:一个 Markdown 文件
- 可选参数:
- 输出 HTML 路径
- 覆盖标题
--open:渲染后尝试在默认浏览器打开
完整输入格式与支持的 Markdown 子集见 references/input_contract.md。
工作流
步骤 1:准备输入文件
确认已有 Markdown 报告,例如:
INPUT_MD="skill-test-results/sector-overview/baijiu/output/sector_overview_baijiu_2026-04-03.md"
OUTPUT_HTML="${INPUT_MD%.md}.html"步骤 2:执行渲染
主入口:
python3 report-renderer/scripts/render_report.py "$INPUT_MD" "$OUTPUT_HTML"若希望渲染后尝试打开浏览器:
python3 report-renderer/scripts/render_report.py "$INPUT_MD" "$OUTPUT_HTML" --open步骤 3:验证输出
- HTML 文件存在
- HTML 中包含
<!DOCTYPE html> - 主标题、主要章节和表格都已渲染
- 不应残留原始 Markdown 表格分隔行(如
| --- | --- |) 数据来源:RQData,置信度5等来源标注仍保留
参数说明
- 第一个位置参数:输入 Markdown 文件
- 第二个位置参数:输出 HTML 文件,可省略
--title:覆盖封面标题--open:渲染完成后尝试打开浏览器
报告质量要求
- 输出必须是完整 HTML,而不是片段
- 章节层级必须可导航,至少生成二级目录
- 表格必须渲染成
<table> - 列表必须渲染成
<ul>/<ol> - 粗体、斜体、行内代码与
<br>必须保留 - 样式必须独立内联或由 skill 自带资源注入,不能依赖外部 CDN
常见错误
- 把完整渲染脚本直接塞回
SKILL.md,没有落地成可执行文件 - 只渲染段落,不支持 Markdown 表格
- 把列表项直接变成裸
<li>,没有外层<ul>/<ol> - 强依赖外部 Markdown 库,导致在最小环境里不可执行
- 依赖 repo 里的
utils/html_renderer.py
:root {
--paper: #fbf8f1;
--surface: #fffdf8;
--ink: #1d2430;
--muted: #5e6875;
--line: #d7d4ca;
--accent: #a6482f;
--accent-deep: #6b2f21;
--accent-soft: #efe2d7;
--table-head: #293241;
--code-bg: #f3efe8;
--quote-bg: #f5ede4;
--shadow: 0 24px 80px rgba(24, 31, 42, 0.12);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
background:
radial-gradient(circle at top left, rgba(166, 72, 47, 0.10), transparent 32%),
linear-gradient(180deg, #f3ecdf 0%, var(--paper) 18%, #f7f4ee 100%);
color: var(--ink);
font-family: "Noto Serif SC", "Source Han Serif SC", "Songti SC", "STSong", serif;
line-height: 1.75;
}
a {
color: inherit;
}
.page {
width: min(1200px, calc(100vw - 32px));
margin: 24px auto 64px;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 24px;
}
.sidebar {
position: sticky;
top: 24px;
align-self: start;
background: rgba(255, 253, 248, 0.94);
border: 1px solid rgba(107, 47, 33, 0.10);
border-radius: 20px;
padding: 24px 22px;
box-shadow: var(--shadow);
backdrop-filter: blur(16px);
}
.eyebrow {
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 10px;
}
.sidebar-title {
margin: 0;
font-size: 28px;
line-height: 1.25;
}
.meta-list {
list-style: none;
margin: 18px 0 0;
padding: 0;
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 13px;
color: var(--muted);
}
.meta-list li + li {
margin-top: 8px;
}
.toc {
margin-top: 24px;
border-top: 1px solid var(--line);
padding-top: 20px;
}
.toc-title {
margin: 0 0 10px;
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--muted);
}
.toc ul {
list-style: none;
margin: 0;
padding: 0;
}
.toc li + li {
margin-top: 8px;
}
.toc a {
display: block;
text-decoration: none;
color: var(--ink);
padding: 6px 8px;
border-radius: 8px;
transition: background-color 120ms ease, transform 120ms ease;
}
.toc a:hover {
background: var(--accent-soft);
transform: translateX(2px);
}
.toc .level-3 a,
.toc .level-4 a {
padding-left: 18px;
color: var(--muted);
}
.report {
background: var(--surface);
border: 1px solid rgba(41, 50, 65, 0.08);
border-radius: 28px;
overflow: hidden;
box-shadow: var(--shadow);
}
.cover {
padding: 56px 56px 44px;
background:
linear-gradient(135deg, rgba(166, 72, 47, 0.96), rgba(76, 33, 24, 0.95)),
linear-gradient(180deg, #784032 0%, #3d1e17 100%);
color: #fffaf3;
}
.cover-kicker {
margin: 0 0 10px;
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
letter-spacing: 0.16em;
text-transform: uppercase;
font-size: 12px;
opacity: 0.8;
}
.cover-title {
margin: 0;
font-size: clamp(32px, 4vw, 48px);
line-height: 1.18;
}
.cover-subtitle {
margin: 16px 0 0;
font-size: 17px;
line-height: 1.6;
max-width: 56ch;
opacity: 0.92;
}
.cover-meta {
margin-top: 24px;
display: flex;
flex-wrap: wrap;
gap: 12px;
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 13px;
}
.chip {
border: 1px solid rgba(255, 250, 243, 0.24);
border-radius: 999px;
padding: 6px 12px;
background: rgba(255, 250, 243, 0.08);
}
.content {
padding: 40px 56px 56px;
}
.content h1,
.content h2,
.content h3,
.content h4 {
scroll-margin-top: 24px;
}
.content h1 {
margin: 32px 0 16px;
font-size: 30px;
line-height: 1.28;
color: var(--accent-deep);
}
.content h2 {
margin: 36px 0 14px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(166, 72, 47, 0.22);
font-size: 24px;
line-height: 1.3;
color: var(--accent-deep);
}
.content h3 {
margin: 26px 0 10px;
font-size: 19px;
line-height: 1.35;
}
.content h4 {
margin: 18px 0 8px;
font-size: 16px;
line-height: 1.4;
}
.content p {
margin: 12px 0;
font-size: 15px;
}
.content ul,
.content ol {
margin: 12px 0 16px;
padding-left: 24px;
}
.content li + li {
margin-top: 6px;
}
.content hr {
border: 0;
border-top: 1px solid var(--line);
margin: 28px 0;
}
.content blockquote {
margin: 18px 0;
padding: 14px 18px;
border-left: 4px solid var(--accent);
background: var(--quote-bg);
color: var(--muted);
}
.content code {
background: var(--code-bg);
border-radius: 6px;
padding: 2px 6px;
font-size: 0.92em;
font-family: "SFMono-Regular", "Menlo", "Monaco", "Courier New", monospace;
}
.code-block {
overflow-x: auto;
background: #211f1c;
color: #f6efe7;
padding: 16px 18px;
border-radius: 16px;
font-size: 13px;
line-height: 1.65;
}
.content table {
width: 100%;
border-collapse: collapse;
margin: 18px 0 12px;
table-layout: fixed;
font-size: 14px;
}
.content thead th {
background: var(--table-head);
color: #f8fafc;
padding: 10px 12px;
text-align: left;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.content tbody td {
padding: 10px 12px;
border: 1px solid #ddd7cb;
vertical-align: top;
}
.content tbody tr:nth-child(even) td {
background: #fcf8f1;
}
.align-left {
text-align: left;
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.source-note {
color: var(--muted);
font-style: italic;
font-size: 13px;
}
.footer {
margin-top: 40px;
padding-top: 18px;
border-top: 1px solid var(--line);
color: var(--muted);
font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 12px;
}
@media (max-width: 980px) {
.page {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
}
}
@media (max-width: 640px) {
.page {
width: calc(100vw - 18px);
margin: 10px auto 32px;
gap: 12px;
}
.cover,
.content {
padding-left: 20px;
padding-right: 20px;
}
.content table {
display: block;
overflow-x: auto;
white-space: nowrap;
}
}
@media print {
body {
background: #fff;
}
.page {
width: auto;
margin: 0;
display: block;
}
.sidebar {
display: none;
}
.report {
box-shadow: none;
border: 0;
}
.cover {
page-break-after: always;
}
}
report-renderer 输入契约
report-renderer/scripts/render_report.py 接收一个 Markdown 文件并输出单文件 HTML。
1. 输入文件
推荐输入是由其他 research skill 生成的 Markdown 报告,例如:
morning-notethesis-trackeridea-generationearnings-previewsector-overview
2. 支持的 Markdown 子集
渲染器内置解析器,当前保证覆盖:
- ATX 标题:
#/##/###/#### - 无序列表:
- item/* item - 有序列表:
1. item - Markdown 表格
- 普通段落
- 分隔线:
--- - 块引用:
> quote - 代码块:````
`lang ...````` - 行内粗体:
**text** - 行内斜体:
*text* - 行内代码:`
code` - 表格单元格中的原生
<br>
3. 元数据抽取
渲染器会尝试从 Markdown 前几行提取:
- 主标题
- 报告日期
- 行业 / 公司 / 覆盖范围等摘要字段
若未识别到元数据:
- 标题退回到文件名
- 报告日期显示为
未提供
4. 输出约定
- 默认输出路径为输入文件同名
.html - 输出是完整 HTML5 文档
- CSS 由
assets/report.css内联注入,不依赖外部样式资源
5. 非目标能力
当前不保证完整支持:
- 嵌套列表
- 复杂表格合并单元格
- 任意 HTML 嵌入
- 脚注、任务列表、LaTeX
#!/usr/bin/env python3
"""Render Markdown research reports into standalone HTML."""
from __future__ import annotations
import argparse
import html
import re
import subprocess
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, List, Optional, Sequence
HEADING_RE = re.compile(r"^(#{1,4})\s+(.*\S)\s*$")
ORDERED_ITEM_RE = re.compile(r"^\d+\.\s+(.*\S)\s*$")
UNORDERED_ITEM_RE = re.compile(r"^[-*]\s+(.*\S)\s*$")
BLOCKQUOTE_RE = re.compile(r"^>\s?(.*)$")
REPORT_DATE_RE = re.compile(r"报告日期[::]\s*([^\n]+)")
META_LINE_RE = re.compile(r"^[-*]\s*([^::]{1,24})[::]\s*(.+)$")
@dataclass
class Heading:
level: int
text: str
anchor: str
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="将 Markdown 研究报告渲染为单文件 HTML")
parser.add_argument("input", help="输入 Markdown 文件路径")
parser.add_argument("output", nargs="?", help="输出 HTML 文件路径,默认与输入同名")
parser.add_argument("--title", help="覆盖封面标题")
parser.add_argument("--open", action="store_true", help="渲染完成后尝试在默认浏览器打开")
return parser.parse_args()
def load_css(script_path: Path) -> str:
css_path = script_path.resolve().parent.parent / "assets" / "report.css"
return css_path.read_text(encoding="utf-8")
def split_table_row(line: str) -> List[str]:
inner = line.strip().strip("|")
return [cell.strip() for cell in inner.split("|")]
def is_table_divider(cells: Sequence[str]) -> bool:
if not cells:
return False
for cell in cells:
stripped = cell.replace(":", "").replace("-", "").strip()
if stripped:
return False
return True
def cell_alignment(cell: str) -> str:
stripped = cell.strip()
if stripped.startswith(":") and stripped.endswith(":"):
return "align-center"
if stripped.endswith(":"):
return "align-right"
return "align-left"
def preserve_soft_breaks(text: str) -> str:
return text.replace("<br>", "<br>")
def render_inline(text: str) -> str:
escaped = preserve_soft_breaks(html.escape(text, quote=False))
def replace_code(match: re.Match[str]) -> str:
return f"<code>{match.group(1)}</code>"
def replace_link(match: re.Match[str]) -> str:
label = match.group(1)
url = html.escape(match.group(2), quote=True)
return f'<a href="{url}" target="_blank" rel="noreferrer">{label}</a>'
escaped = re.sub(r"`([^`]+)`", replace_code, escaped)
escaped = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", replace_link, escaped)
escaped = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", escaped)
escaped = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", escaped)
return escaped
def slugify(text: str, index: int) -> str:
compact = re.sub(r"\s+", "-", text.strip().lower())
compact = re.sub(r"[^0-9a-zA-Z\u4e00-\u9fff-]", "", compact)
compact = compact.strip("-")
return compact or f"section-{index}"
def parse_metadata(markdown_text: str, markdown_path: Path) -> dict[str, str]:
lines = markdown_text.splitlines()
title = markdown_path.stem
for line in lines:
match = HEADING_RE.match(line)
if match and len(match.group(1)) == 1:
title = match.group(2).strip()
break
report_date_match = REPORT_DATE_RE.search(markdown_text)
report_date = report_date_match.group(1).strip() if report_date_match else "未提供"
meta: dict[str, str] = {"title": title, "report_date": report_date}
for line in lines[:20]:
meta_match = META_LINE_RE.match(line.strip())
if meta_match:
key = meta_match.group(1).strip()
value = meta_match.group(2).strip()
if key and value:
meta[key] = value
return meta
def render_table(table_lines: Sequence[str]) -> str:
rows = [split_table_row(line) for line in table_lines if line.strip()]
if not rows:
return ""
header = rows[0]
divider = rows[1] if len(rows) > 1 and is_table_divider(rows[1]) else None
alignments = [cell_alignment(cell) for cell in divider] if divider else ["align-left"] * len(header)
body_rows = rows[2:] if divider else rows[1:]
head_html = "".join(f'<th class="{alignments[idx] if idx < len(alignments) else "align-left"}">{render_inline(cell)}</th>' for idx, cell in enumerate(header))
parts = ["<table>", f"<thead><tr>{head_html}</tr></thead>", "<tbody>"]
for row in body_rows:
cells = []
for idx, cell in enumerate(row):
align = alignments[idx] if idx < len(alignments) else "align-left"
cells.append(f'<td class="{align}">{render_inline(cell)}</td>')
parts.append("<tr>" + "".join(cells) + "</tr>")
parts.append("</tbody></table>")
return "".join(parts)
def render_list(list_lines: Sequence[str], ordered: bool) -> str:
tag = "ol" if ordered else "ul"
items = []
for line in list_lines:
match = ORDERED_ITEM_RE.match(line) if ordered else UNORDERED_ITEM_RE.match(line)
if match:
items.append(f"<li>{render_inline(match.group(1).strip())}</li>")
return f"<{tag}>" + "".join(items) + f"</{tag}>"
def render_code_block(code_lines: Sequence[str], language: str) -> str:
payload = html.escape("\n".join(code_lines), quote=False)
class_attr = f' data-language="{html.escape(language, quote=True)}"' if language else ""
return f'<pre class="code-block"{class_attr}><code>{payload}</code></pre>'
def paragraph_class(text: str) -> str:
plain = re.sub(r"<[^>]+>", "", text).strip()
if plain.startswith("数据来源:") or plain.startswith("*数据来源:"):
return " class=\"source-note\""
return ""
def render_markdown(markdown_text: str) -> tuple[str, List[Heading]]:
lines = markdown_text.splitlines()
parts: List[str] = []
headings: List[Heading] = []
paragraph: List[str] = []
unordered_items: List[str] = []
ordered_items: List[str] = []
table_lines: List[str] = []
quote_lines: List[str] = []
code_lines: List[str] = []
in_code_block = False
code_language = ""
heading_index = 0
def flush_paragraph() -> None:
nonlocal paragraph
if not paragraph:
return
text = render_inline(" ".join(item.strip() for item in paragraph))
parts.append(f"<p{paragraph_class(text)}>{text}</p>")
paragraph = []
def flush_unordered() -> None:
nonlocal unordered_items
if unordered_items:
parts.append(render_list(unordered_items, ordered=False))
unordered_items = []
def flush_ordered() -> None:
nonlocal ordered_items
if ordered_items:
parts.append(render_list(ordered_items, ordered=True))
ordered_items = []
def flush_table() -> None:
nonlocal table_lines
if table_lines:
parts.append(render_table(table_lines))
table_lines = []
def flush_quote() -> None:
nonlocal quote_lines
if quote_lines:
text = render_inline(" ".join(quote_lines))
parts.append(f"<blockquote><p>{text}</p></blockquote>")
quote_lines = []
for raw_line in lines:
line = raw_line.rstrip("\n")
if in_code_block:
if line.strip().startswith("```"):
parts.append(render_code_block(code_lines, code_language))
code_lines = []
in_code_block = False
code_language = ""
else:
code_lines.append(line)
continue
if line.strip().startswith("```"):
flush_paragraph()
flush_unordered()
flush_ordered()
flush_table()
flush_quote()
in_code_block = True
code_language = line.strip()[3:].strip()
code_lines = []
continue
if not line.strip():
flush_paragraph()
flush_unordered()
flush_ordered()
flush_table()
flush_quote()
continue
heading_match = HEADING_RE.match(line)
if heading_match:
flush_paragraph()
flush_unordered()
flush_ordered()
flush_table()
flush_quote()
heading_index += 1
level = len(heading_match.group(1))
text = heading_match.group(2).strip()
anchor = slugify(text, heading_index)
headings.append(Heading(level=level, text=text, anchor=anchor))
parts.append(f'<h{level} id="{anchor}">{render_inline(text)}</h{level}>')
continue
if line.strip() == "---":
flush_paragraph()
flush_unordered()
flush_ordered()
flush_table()
flush_quote()
parts.append("<hr>")
continue
if line.lstrip().startswith("|"):
flush_paragraph()
flush_unordered()
flush_ordered()
flush_quote()
table_lines.append(line)
continue
quote_match = BLOCKQUOTE_RE.match(line.strip())
if quote_match:
flush_paragraph()
flush_unordered()
flush_ordered()
flush_table()
quote_lines.append(quote_match.group(1).strip())
continue
if UNORDERED_ITEM_RE.match(line.strip()):
flush_paragraph()
flush_ordered()
flush_table()
flush_quote()
unordered_items.append(line.strip())
continue
if ORDERED_ITEM_RE.match(line.strip()):
flush_paragraph()
flush_unordered()
flush_table()
flush_quote()
ordered_items.append(line.strip())
continue
flush_unordered()
flush_ordered()
flush_table()
flush_quote()
paragraph.append(line.strip())
if in_code_block:
parts.append(render_code_block(code_lines, code_language))
flush_paragraph()
flush_unordered()
flush_ordered()
flush_table()
flush_quote()
return "\n".join(parts), headings
def build_toc(headings: Iterable[Heading]) -> str:
items = [heading for heading in headings if heading.level in (2, 3, 4)]
if not items:
return ""
lines = ['<nav class="toc">', '<p class="toc-title">Contents</p>', "<ul>"]
for heading in items:
lines.append(
f'<li class="level-{heading.level}"><a href="#{heading.anchor}">{html.escape(heading.text)}</a></li>'
)
lines.extend(["</ul>", "</nav>"])
return "\n".join(lines)
def render_meta_list(metadata: dict[str, str]) -> str:
preferred = ["报告日期", "行业", "公司", "覆盖范围"]
items = []
for key in preferred:
if key in metadata:
items.append((key, metadata[key]))
for key, value in metadata.items():
if key in {"title", "report_date"} or key in preferred:
continue
items.append((key, value))
if not items:
items = [("报告日期", metadata.get("report_date", "未提供"))]
return "<ul class=\"meta-list\">" + "".join(
f"<li><strong>{html.escape(key)}</strong> {render_inline(value)}</li>" for key, value in items
) + "</ul>"
def build_html_document(title: str, subtitle: str, report_date: str, metadata: dict[str, str], toc_html: str, body_html: str, css: str) -> str:
meta_chips = "".join(
f'<span class="chip">{html.escape(label)} {render_inline(value)}</span>'
for label, value in [("Report Date", report_date), ("Format", "Standalone HTML"), ("Source", "RQ Research Markdown")]
)
return f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{html.escape(title)}</title>
<style>
{css}
</style>
</head>
<body>
<div class="page">
<aside class="sidebar">
<p class="eyebrow">RQ Report Renderer</p>
<h1 class="sidebar-title">{html.escape(title)}</h1>
{render_meta_list(metadata)}
{toc_html}
</aside>
<main class="report">
<section class="cover">
<p class="cover-kicker">Research Output</p>
<h1 class="cover-title">{html.escape(title)}</h1>
<p class="cover-subtitle">{render_inline(subtitle)}</p>
<div class="cover-meta">{meta_chips}</div>
</section>
<section class="content">
{body_html}
<div class="footer">Generated by report-renderer/scripts/render_report.py</div>
</section>
</main>
</div>
</body>
</html>
"""
def resolve_output_path(args: argparse.Namespace, input_path: Path) -> Path:
return Path(args.output).expanduser() if args.output else input_path.with_suffix(".html")
def infer_subtitle(metadata: dict[str, str]) -> str:
for key in ("行业", "公司", "覆盖范围"):
if key in metadata:
return f"{key}:{metadata[key]}"
return "由 Markdown 研究报告自动渲染的单文件 HTML。"
def try_open(path: Path) -> None:
commands = (["xdg-open", str(path)], ["open", str(path)])
for command in commands:
try:
subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
return
except (FileNotFoundError, subprocess.CalledProcessError):
continue
print("警告:未找到可用的浏览器打开命令,已保留 HTML 文件。")
def main() -> None:
args = parse_args()
input_path = Path(args.input).expanduser()
if not input_path.exists():
raise FileNotFoundError(f"输入 Markdown 不存在:{input_path}")
markdown_text = input_path.read_text(encoding="utf-8")
metadata = parse_metadata(markdown_text, input_path)
title = args.title or metadata.get("title") or input_path.stem
report_date = metadata.get("report_date", "未提供")
subtitle = infer_subtitle(metadata)
body_html, headings = render_markdown(markdown_text)
toc_html = build_toc(headings)
css = load_css(Path(__file__))
output_html = build_html_document(title, subtitle, report_date, metadata, toc_html, body_html, css)
output_path = resolve_output_path(args, input_path)
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(output_html, encoding="utf-8")
print(f"✅ HTML 报告已生成:{output_path}")
if args.open:
try_open(output_path)
if __name__ == "__main__":
main()
Related skills
How it compares
Use as a presentation template skill rather than bolting charts into a generic docs generator or Markdown-to-PDF alone.
FAQ
Who is rq-report-renderer for?
Developers in quantitative finance who want agent-assisted HTML reports with a consistent RiceQuant-like visual identity.
When should I use rq-report-renderer?
After analytics or backtests in grow/analytics, when documenting strategies in build/docs, or when iterating operational reviews in operate/iterate—whenever you need a polished report frame.
Is rq-report-renderer safe to install?
Review the Security Audits panel on this Prism page and inspect the skill source in your repo before granting filesystem or network access to your agent.