Initial commit: merged, deduplicated, and vetted skill collection

Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:

- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
  ../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
  zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
  - docx family: kept docx (official, full toolchain) + docx-cn
    (GB/T 9704 Chinese official-document constants),
    dropped docx_writer (no scripts, name collided with docx)
  - humanizer family: kept humanizer-zh (6 zh reference docs),
    dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
  ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
  claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
  live credentials

Result: 25 skills, 572 files, ~7.5 MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
wangyitong 2026-07-13 14:47:12 +08:00
commit a65adcc2e5
572 changed files with 118507 additions and 0 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Editor / IDE
.DS_Store
.idea/
.vscode/
*.swp
*~
# Python
__pycache__/
*.pyc
*.pyo
.venv/
venv/
# Node
node_modules/
# Local secrets
.env
.env.local
*.local
# Skill-generated artifacts
**/output/
**/.agent/runs/

84
README.md Normal file
View File

@ -0,0 +1,84 @@
# Skills
Claude Code 技能集 — 金融数据、文档处理、数据可视化、联网搜索、写作与元技能。
## 目录
### 金融数据与研究
| 技能 | 说明 |
|------|------|
| [windpy-sdk](windpy-sdk/) | Wind 金融终端 Python API — 17 个函数、常驻服务、13 份参考文档,覆盖 A 股/债券/基金/期货/期权/外汇/宏观 |
| [tushare-data-1.0.5](tushare-data-1.0.5/) | Tushare 财经数据接口 — 股票/基金/期货/数字货币行情 + 公司财务/基金经理基本面 |
| [ifind-http-api](ifind-http-api/) | 同花顺 iFinD HTTP API — 18 个端点参考手册,支持股票/基金/债券/期货/期权/宏观数据 |
| [gangtise](gangtise/) | 冈底斯港股/A 股研报知识库 + 投研 AI Agent + 经济指标查询 |
| [annuity-disclosure-crawler](annuity-disclosure-crawler/) | 企业年金季度管理报告全量爬虫 — 22 家投资管理人官网披露栏目 |
| [asset-monitor](asset-monitor/) | 资产异常波动日频监控 — Z-Score 检测偏离 > 2σ 的异常,自动生成 Excel 报告 |
| [technical-analyst](technical-analyst/) | 周线 K 线技术分析 — 趋势识别、支撑阻力位、形态、概率场景推演(纯图表) |
| [portfolio-analyzer](portfolio-analyzer/) | 投资组合多维分析 — 8 项风险指标、Brinson 三因素归因、6-sheet Excel 报告 |
| [financial-report-writing](financial-report-writing/) | A 股上市公司投研报告写作 — 数据获取、分析、Word 输出全流程 |
### 文档与可视化
| 技能 | 说明 |
|------|------|
| [docx](docx/) | Word 文档创建/编辑 — 修订追踪、批注、格式保留、文本提取,含 docx-js + ooxml 工具链 |
| [docx-cn](docx-cn/) | 中文公文 Word 文档(GB/T 9704-2012) — 依赖 `docx` skill,提供公文常量与排版规范 |
| [xlsx](xlsx/) | Excel 电子表格 — 公式、格式、数据分析、可视化、公式重算 |
| [pdf](pdf/) | PDF 处理工具包 — 文本/表格提取、创建、合并/拆分、表单填写 |
| [pptx](pptx/) | PowerPoint 创建/编辑/分析 — 布局、母版、图表、演讲备注,含 OOXML schema |
| [ppt-station-skill](ppt-station-skill/) | 数据 → 金融报告 PPT — 12 种页面布局、10 主题、ChartJunkCleaner、DeckLinter 合规审计 |
| [fund-slides](fund-slides/) | 零依赖 HTML 演示 — 智能内容-可视化映射、ECharts 内联、中文字体支持 |
| [plotly](plotly/) | Plotly 交互式可视化 — 散点/折线/热力/3D/地图/金融图表,输出 HTML 或静态图 |
### 联网搜索与工具箱
| 技能 | 说明 |
|------|------|
| [agentrs-search](agentrs-search/) | 京东 agentrs 网关四搜索引擎 — tavily / cloudsway(Bing)/ searchpro(搜狗)/ jdcloud RAG |
| [agentrs-map](agentrs-map/) | 高德地图 6 工具 — POI / 地理编码 / 行政区 / IP 定位 / 步行导航 / 坐标转换 |
| [agentrs-weather](agentrs-weather/) | 高德天气 — 实况 + 未来预报,支持 adcode 或地址自动解析 |
| [zhipu-websearch](zhipu-websearch/) | 智谱 AI Web Search — 双引擎、时间过滤、域名过滤 |
### 写作与协作
| 技能 | 说明 |
|------|------|
| [humanizer-zh](humanizer-zh/) | 中文去 AI 痕迹 — 交互式工作流,33 种模式分 6 篇 reference 按需加载,支持学术/公文/商业报告 |
| [ai-pair](ai-pair/) | 多模型协同 — 一个创作(Author),多个审阅(可配 Codex/Gemini/Kimi),覆盖不同 review 维度 |
### 元技能
| 技能 | 说明 |
|------|------|
| [skill-creator](skill-creator/) | 技能创建指南 — 帮助创建和优化 Claude Code skill |
| [find-skills](find-skills/) | 技能发现与安装 — 从开源技能生态中搜索和安装 skill |
## 使用方式
将技能目录放入项目的 `.claude/skills/` 下,Claude Code 自动识别加载。
```bash
# 克隆整个仓库
git clone https://codehub.ablemind.cc/public/skills.git .claude/skills
# 或只安装单个技能
cp -r windpy-sdk <your-project>/.claude/skills/windpy-sdk
```
## 依赖关系
```
asset-monitor ── uses ── WindPy SDK ── ref ── windpy-sdk
portfolio-analyzer ── uses ── WindPy SDK ── ref ── windpy-sdk
└─ uses ── xlsx(recalc.py 公式验证)
docx-cn ── uses ── docx(unpack/pack/validate 工具链)
其余 skill 均独立,无交叉依赖
```
需要外部凭证的 skill(首次使用时会自动检测 env / .env,未命中则通过 AskUserQuestion 询问):
- `windpy-sdk` / `tushare-data-1.0.5` / `ifind-http-api` — 各自数据源账号
- `gangtise``GANGTISE_TOKEN`
- `agentrs-*``JD_AGENTRS_TOKEN`
- `zhipu-websearch``ZHIPU_API_KEY`

48
agentrs-map/SKILL.md Normal file
View File

@ -0,0 +1,48 @@
---
name: agentrs-map
description: >
高德地图 / 地理工具箱 —— 通过京东 agentrs 代理网关,用单一 Bearer Token 调 6 个高德(Amap)工具:
POI 地点检索、地理编码(地址↔经纬度/adcode)、行政区域查询、IP 定位、步行路径规划、坐标转换。
当需要查地点经纬度与 adcode、找附近地点(POI)、做步行导航、转换坐标系、
按 IP 定位、查行政区划时使用。触发词高德、地图、经纬度、坐标、adcode、地理编码、
POI、附近、步行路线、导航、坐标转换、IP定位、行政区、省市区。
---
# agentrs-map —— 高德地图 / 地理6 工具)
通过 `https://agentrs.jd.com/api/saas/proxy-k/v1/<tool>`,单一 Bearer Token 调 6 个高德工具。
## 鉴权与调用约定(先读)
- 环境变量取密钥,**不硬编码**`export JD_AGENTRS_TOKEN='你的token'`
- **网关只认 JSON body**:文档标 GET/POST 都不影响query string 会报"缺少必要输入参数"。
- HTTP 恒 200业务看 body`1001`=token 未授权;`9999 缺少必要输入参数`=参数错;`9999 用户积分不足`=余额不足(重试常能通)。
## 6 个工具
| 工具 | 入参 | 返回要点 |
|---|---|---|
| `poi` | `{keywords(必填), city(拼音如 beijing)}` | POI 文本检索(名虽 around 但**非按经纬度周边**`pois[]` 含 rating/人均/营业时间/招牌菜/电话/location |
| `geocode` | `{address, city}` | 地址→坐标;`geocodes[]` 含 location/adcode/level/district |
| `region` | `{keywords, subdistrict, offset, page}` | 行政区树;`districts[]` 含 adcode/center/level/下级 |
| `ip` | `{ip}`(留空取请求 IP仅国内 | province/city/adcode/rectangle |
| `walk` | `{origin:"lon,lat", destination:"lon,lat"}` | `route.paths[]` 含 distance/duration/steps[] 分段导航 |
| `coord` | `{locations:"lon,lat", coordsys: gps/baidu/mapbar/autonavi}` | 转成高德坐标 |
## 用法
```bash
export JD_AGENTRS_TOKEN='...'
python3 scripts/amap.py poi --keywords 故宫 --city beijing
python3 scripts/amap.py geocode --address "北京市朝阳区建国路88号"
python3 scripts/amap.py region --keywords 北京 --subdistrict 1
python3 scripts/amap.py ip --ip 114.247.50.2
python3 scripts/amap.py walk --origin 116.397455,39.909187 --destination 116.417854,39.914888
python3 scripts/amap.py coord --locations 116.481499,39.990475 --coordsys gps
```
打印规整 JSON遇 1001/9999 在 stderr 给可读提示。
## 组合范式
- **"附近有什么 / 某地点信息"**`poi --keywords X --city Y`。
- **"A 到 B 步行怎么走"**:两端各 `geocode`/`poi` 拿经纬度 → `walk`
- **坐标系不一致**GPS/百度坐标接入高德):先 `coord` 统一到高德。
- **查天气**`geocode` 拿到的 `adcode` 即天气入参 → 见姊妹 skill **agentrs-weather**
> 联网搜索见 **agentrs-search**;天气见 **agentrs-weather**。三者共用同一个 JD_AGENTRS_TOKEN。

107
agentrs-map/scripts/amap.py Normal file
View File

@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
agentrs-map 京东 agentrs 代理网关的高德地图 / 地理 / 天气客户端
封装 6 个高德(Amap)工具
poi(POI检索) / geocode(地理编码) / region(行政区) / ip(IP定位) /
walk(步行路径) / coord(坐标转换)
端点https://agentrs.jd.com/api/saas/proxy-k/v1/<tool>
鉴权环境变量 JD_AGENTRS_TOKEN不硬编码密钥
约定网关一律从 JSON body 取参query string 不生效HTTP 200业务看 body
天气查询见姊妹 skill agentrs-weathergeocode 拿到的 adcode 即天气入参
用法
python3 amap.py poi --keywords 故宫 --city beijing
python3 amap.py geocode --address "北京市朝阳区建国路88号"
python3 amap.py region --keywords 北京 --subdistrict 1
python3 amap.py ip --ip 114.247.50.2
python3 amap.py walk --origin 116.397455,39.909187 --destination 116.417854,39.914888
python3 amap.py coord --locations 116.481499,39.990475 --coordsys gps
"""
import os, sys, json, argparse, urllib.request, urllib.error
BASE = "https://agentrs.jd.com/api/saas/proxy-k/v1"
def _token():
tok = os.environ.get("JD_AGENTRS_TOKEN")
if not tok:
sys.exit("错误:未设置 JD_AGENTRS_TOKEN。\n export JD_AGENTRS_TOKEN='你的token'")
return tok
def call(tool, body, timeout=30):
payload = {k: v for k, v in body.items() if v not in ("", None)}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(f"{BASE}/{tool}", data=data, method="POST",
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {_token()}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
raw = r.read().decode("utf-8")
except urllib.error.URLError as e:
return {"_error": f"网络错误: {e}"}
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"_raw": raw}
def biz_error(resp):
if not isinstance(resp, dict):
return None
code, msg = resp.get("code"), resp.get("msg") or ""
if code in (1001, "1001"):
return f"[未授权 1001] 该 token 未开通此工具:{msg}"
if code in (9999, "9999") or "积分不足" in msg or "缺少必要输入参数" in msg:
return f"[业务失败 {code}] {msg}"
return None
# 7 个高德工具 -------------------------------------------------
def poi(keywords, city="", **k):
return call("search_around_gaode", dict(keywords=keywords, city=city))
def geocode(address, city="", **k):
return call("geocoding", dict(address=address, city=city))
def region(keywords, subdistrict="", page="", offset="", extensions="", filter="", **k):
return call("administrative_region_inquiry", dict(keywords=keywords,
subdistrict=subdistrict, page=page, offset=offset,
extensions=extensions, filter=filter))
def ip(ip="", **k):
return call("ip_locating", dict(ip=ip))
def walk(origin, destination, origin_id="", destination_id="", **k):
return call("pedestrian_path_planning", dict(origin=origin, destination=destination,
origin_id=origin_id, destination_id=destination_id))
def coord(locations, coordsys="gps", **k):
return call("coordinate_transformation", dict(locations=locations, coordsys=coordsys))
TOOLS = {"poi": poi, "geocode": geocode, "region": region, "ip": ip,
"walk": walk, "coord": coord}
def main():
p = argparse.ArgumentParser(description="京东 agentrs 高德地图/地理6 工具)")
p.add_argument("tool", choices=list(TOOLS))
for opt in ("keywords", "city", "address", "subdistrict", "page", "offset",
"extensions", "filter", "ip", "origin", "destination",
"origin_id", "destination_id", "locations", "coordsys"):
p.add_argument(f"--{opt}", default=None)
a = p.parse_args()
kw = {k: v for k, v in vars(a).items() if k != "tool" and v is not None}
resp = TOOLS[a.tool](**kw)
err = biz_error(resp)
if err:
print(err, file=sys.stderr)
print(json.dumps(resp, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

50
agentrs-search/SKILL.md Normal file
View File

@ -0,0 +1,50 @@
---
name: agentrs-search
description: >
联网搜索工具箱 —— 通过京东 agentrs 代理网关,用单一 Bearer Token 调用 4 个搜索引擎:
tavily(AI搜索)、cloudsway(必应/Bing,可拿网页全文)、SearchPro(搜狗,实时新闻时效最强)、
jdcloud_search(京东云RAG,返回整页正文)。当需要联网检索资讯、新闻、研报、实时信息,
或要抓取网页正文做后续分析时使用。触发词:联网搜索、实时搜索、查资讯、查新闻、
搜一下、tavily、bing、cloudsway、搜狗、京东搜索、web search、抓网页正文。
---
# agentrs-search —— 联网搜索4 引擎)
通过 `https://agentrs.jd.com/api/saas/proxy-k/v1/<tool>`,单一 Bearer Token 调 4 个搜索引擎。
## 鉴权与调用约定(先读)
- 环境变量取密钥,**不硬编码**`export JD_AGENTRS_TOKEN='你的token'`
- **网关只认 JSON body**:文档标 GET/POST 都不影响query string 会报"缺少必要输入参数"。
- HTTP 恒 200业务看 body`1001`=token 未授权(充值/补参无用);`9999 缺少必要输入参数`=参数错;`9999 用户积分不足`=余额不足(**重试常能通**)。
## 4 个引擎怎么选
| 引擎 | 底层 | 入参 | 结果路径 | 选用场景 |
|---|---|---|---|---|
| `tavily` | Tavily | `{query}` | `results[]` | 通用 AI 搜索,默认首选 |
| `cloudsway` | Bing/必应 | `{q,count,freshness,enableContent,contentType,mainText,sites,blockWebsites}` | `webPages.value[]` | 中文好;`enableContent=true` 直接拿网页**正文**`sites/blockWebsites` 站点过滤;`freshness=Day/Week/Month` 时间过滤 |
| `searchpro` | 搜狗 | `{Query,Cnt,Site,Industry,...}` | `Response.Pages[]`**元素是字符串化JSON需二次 parse** | **实时新闻/天气,时效性最强** |
| `jdcloud` | 京东云 RAG | `{query}` | `data[]``page_content` 整页正文 + `recall_distance` | 要**整页全文**做分析时 |
## 用法
```bash
export JD_AGENTRS_TOKEN='...'
python3 scripts/search.py tavily "人工智能最新趋势"
python3 scripts/search.py cloudsway "中信建投证券" --count 5 --enableContent true --mainText true
python3 scripts/search.py searchpro "今天北京天气"
python3 scripts/search.py jdcloud "AI agent 发展"
```
打印规整 JSON遇 1001/9999 在 stderr 给可读提示。
## 直接 curl
```bash
curl -s https://agentrs.jd.com/api/saas/proxy-k/v1/tavily_search \
-X POST -H "Content-Type: application/json" \
-H "Authorization: Bearer $JD_AGENTRS_TOKEN" \
-d '{"query":"搜索关键词"}'
```
工具名换 `cloudsway`/`SearchPro`/`jdcloud_search`、body 换对应入参即可。
## 选用范式
- 通用检索 → `tavily`;要最新时效 → `searchpro`;要网页正文做分析 → `jdcloud`(或 `cloudsway --enableContent true`);要限定站点 → `cloudsway --sites baijiahao.baidu.com`
> 地图/经纬度/POI/路径 等见姊妹 skill **agentrs-map**;天气见 **agentrs-weather**。三者共用同一个 JD_AGENTRS_TOKEN。

View File

@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""
agentrs-search 京东 agentrs 代理网关的联网搜索客户端
封装 4 个搜索引擎tavily / cloudsway(Bing) / SearchPro(搜狗) / jdcloud_search(京东云RAG)
端点https://agentrs.jd.com/api/saas/proxy-k/v1/<tool>
鉴权环境变量 JD_AGENTRS_TOKEN不硬编码密钥
export JD_AGENTRS_TOKEN='...'
约定网关一律从 JSON body 取参query string 不生效HTTP 200业务看 body
1001=工具未授权 | 9999"缺少必要输入参数"=参数错 | 9999"积分不足"=余额不足(可重试)
用法
python3 search.py tavily "人工智能最新趋势"
python3 search.py cloudsway "中信建投证券" --count 5 --enableContent true --mainText true
python3 search.py searchpro "今天北京天气"
python3 search.py jdcloud "AI agent 发展"
"""
import os, sys, json, argparse, urllib.request, urllib.error
BASE = "https://agentrs.jd.com/api/saas/proxy-k/v1"
def _token():
tok = os.environ.get("JD_AGENTRS_TOKEN")
if not tok:
sys.exit("错误:未设置 JD_AGENTRS_TOKEN。\n export JD_AGENTRS_TOKEN='你的token'")
return tok
def call(tool, body, timeout=30):
payload = {k: v for k, v in body.items() if v not in ("", None)}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(f"{BASE}/{tool}", data=data, method="POST",
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {_token()}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
raw = r.read().decode("utf-8")
except urllib.error.URLError as e:
return {"_error": f"网络错误: {e}"}
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"_raw": raw}
def biz_error(resp):
if not isinstance(resp, dict):
return None
code, msg = resp.get("code"), resp.get("msg") or ""
if code in (1001, "1001"):
return f"[未授权 1001] 该 token 未开通此工具:{msg}"
if code in (9999, "9999") or "积分不足" in msg or "缺少必要输入参数" in msg:
return f"[业务失败 {code}] {msg}"
return None
# 4 个搜索引擎 -------------------------------------------------
def tavily(query, **k):
return call("tavily_search", {"query": query})
def cloudsway(q, count="", freshness="", offset="", enableContent="",
contentType="", mainText="", sites="", blockWebsites="", **k):
return call("cloudsway", dict(q=q, count=count, freshness=freshness, offset=offset,
enableContent=enableContent, contentType=contentType, mainText=mainText,
sites=sites, blockWebsites=blockWebsites))
def searchpro(query, region="", mode="", site="", cnt="", industry="", **k):
return call("SearchPro", dict(Query=query, Region=region, Mode=mode,
site=site, Cnt=cnt, Industry=industry))
def jdcloud(query, **k):
return call("jdcloud_search", {"query": query})
TOOLS = {"tavily": tavily, "cloudsway": cloudsway,
"searchpro": searchpro, "jdcloud": jdcloud}
def main():
p = argparse.ArgumentParser(description="京东 agentrs 联网搜索4 引擎)")
p.add_argument("engine", choices=list(TOOLS))
p.add_argument("query", nargs="?", help="查询词")
for opt in ("count", "freshness", "offset", "enableContent", "contentType",
"mainText", "sites", "blockWebsites", "region", "mode",
"site", "cnt", "industry"):
p.add_argument(f"--{opt}", default=None)
a = p.parse_args()
kw = {k: v for k, v in vars(a).items()
if k not in ("engine", "query") and v is not None}
if a.query:
kw["q" if a.engine == "cloudsway" else "query"] = a.query
resp = TOOLS[a.engine](**kw)
err = biz_error(resp)
if err:
print(err, file=sys.stderr)
print(json.dumps(resp, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

51
agentrs-weather/SKILL.md Normal file
View File

@ -0,0 +1,51 @@
---
name: agentrs-weather
description: >
天气查询工具 —— 通过京东 agentrs 代理网关调用高德天气,返回城市实况(气温/天气/湿度/风力)
或未来预报。支持直接传高德 adcode也支持传城市名/地址自动解析 adcode。当用户问某地天气、
气温、今天/明天下不下雨、出门带不带伞、未来几天天气这类问题时使用。
触发词天气、气温、下雨、温度、湿度、风力、今天天气、明天天气、未来天气、weather、带伞。
---
# agentrs-weather —— 高德天气(实况 / 预报)
通过 `https://agentrs.jd.com/api/saas/proxy-k/v1/weather_inquiry` 查高德天气。
## 鉴权与调用约定(先读)
- 环境变量取密钥,**不硬编码**`export JD_AGENTRS_TOKEN='你的token'`
- **网关只认 JSON body**query string 不生效HTTP 恒 200业务看 body`1001`=未授权 / `9999 缺少必要输入参数`=参数错 / `9999 用户积分不足`=余额不足(重试常能通)。
## 关键:入参是 adcode
高德天气的 `city` 参数是**高德 adcode**(如北京=110100**不是经纬度,也不是和风天气的 LocationID**。
本 skill 的脚本内置「城市名→adcode」自动解析借地理编码所以两种方式都行
- 已知 adcode`--city 110100`
- 只知城市名:`--address 北京` / `--address 上海市黄浦区`(自动解析)
| 入参 | 说明 |
|---|---|
| `city` | 高德 adcode`address` 二选一) |
| `address` | 城市名/地址,脚本自动解析成 adcode |
| `extensions` | `base`=实况(默认) / `all`=预报 |
返回 `lives[]`(实况)含 weather/temperature/humidity/winddirection/windpower/reporttime
`extensions=all` 时返回 `forecasts[]` 未来数日预报。
## 用法
```bash
export JD_AGENTRS_TOKEN='...'
python3 scripts/weather.py --city 110100 # 北京实况
python3 scripts/weather.py --city 110100 --extensions all # 北京预报
python3 scripts/weather.py --address 上海市黄浦区 # 城市名自动解析
```
打印规整 JSON遇 1001/9999 在 stderr 给可读提示。
## 直接 curl需先有 adcode
```bash
curl -s https://agentrs.jd.com/api/saas/proxy-k/v1/weather_inquiry \
-X POST -H "Content-Type: application/json" \
-H "Authorization: Bearer $JD_AGENTRS_TOKEN" \
-d '{"city":"110100","extensions":"base"}'
```
> adcode/经纬度/POI/路径 等地图地理能力见姊妹 skill **agentrs-map**
> 联网搜索见 **agentrs-search**。三者共用同一个 JD_AGENTRS_TOKEN。

View File

@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
agentrs-weather 京东 agentrs 代理网关的高德天气客户端
封装高德 weather_inquiry实况(base) / 预报(all)
端点https://agentrs.jd.com/api/saas/proxy-k/v1/<tool>
高德天气入参是 adcode北京=110100不是经纬度也不是和风 LocationID
本脚本内置城市名adcode自动解析 geocoding使天气查询可独立使用
--city 110100 直接给 adcode
--address 北京 / 朝阳区 自动 geocoding 解析出 adcode 再查
鉴权环境变量 JD_AGENTRS_TOKEN不硬编码密钥
约定网关一律从 JSON body 取参query string 不生效HTTP 200业务看 body
用法
python3 weather.py --city 110100 # 北京实况
python3 weather.py --city 110100 --extensions all # 预报
python3 weather.py --address 上海市黄浦区 # 城市名自动解析
"""
import os, sys, json, argparse, urllib.request, urllib.error
BASE = "https://agentrs.jd.com/api/saas/proxy-k/v1"
def _token():
tok = os.environ.get("JD_AGENTRS_TOKEN")
if not tok:
sys.exit("错误:未设置 JD_AGENTRS_TOKEN。\n export JD_AGENTRS_TOKEN='你的token'")
return tok
def call(tool, body, timeout=30):
payload = {k: v for k, v in body.items() if v not in ("", None)}
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request(f"{BASE}/{tool}", data=data, method="POST",
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {_token()}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
raw = r.read().decode("utf-8")
except urllib.error.URLError as e:
return {"_error": f"网络错误: {e}"}
try:
return json.loads(raw)
except json.JSONDecodeError:
return {"_raw": raw}
def biz_error(resp):
if not isinstance(resp, dict):
return None
code, msg = resp.get("code"), resp.get("msg") or ""
if code in (1001, "1001"):
return f"[未授权 1001] 该 token 未开通此工具:{msg}"
if code in (9999, "9999") or "积分不足" in msg or "缺少必要输入参数" in msg:
return f"[业务失败 {code}] {msg}"
return None
def resolve_adcode(address):
"""城市名/地址 → adcode借 geocoding 第一条结果)。"""
resp = call("geocoding", dict(address=address))
geos = resp.get("geocodes") if isinstance(resp, dict) else None
if geos:
return geos[0].get("adcode")
return None
def weather(city, extensions="base"):
return call("weather_inquiry", dict(city=city, extensions=extensions))
def main():
p = argparse.ArgumentParser(description="京东 agentrs 高德天气(实况/预报)")
p.add_argument("--city", default=None, help="高德 adcode如北京 110100")
p.add_argument("--address", default=None, help="城市名/地址,自动解析 adcode")
p.add_argument("--extensions", default="base", help="base 实况 / all 预报")
a = p.parse_args()
city = a.city
if not city and a.address:
city = resolve_adcode(a.address)
if not city:
sys.exit(f"无法从“{a.address}”解析出 adcode")
print(f"[解析] {a.address} → adcode {city}", file=sys.stderr)
if not city:
sys.exit("需提供 --city <adcode> 或 --address <城市名>")
resp = weather(city, a.extensions)
err = biz_error(resp)
if err:
print(err, file=sys.stderr)
print(json.dumps(resp, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()

4
ai-pair/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
*.pyc
__pycache__/
.claude/

21
ai-pair/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Axton Liu
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

218
ai-pair/README.md Normal file
View File

@ -0,0 +1,218 @@
# AI-Pair: Heterogeneous AI Team Collaboration
# AI-Pair异构 AI 团队协作
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Status: Experimental](https://img.shields.io/badge/Status-Experimental-orange.svg)](#status)
[![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-blue)](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills)
Coordinate multiple AI models to work together as a team. One creates, two review — not for redundancy, but because different models naturally focus on different dimensions.
让不同 AI 模型组成团队协作。一个创作,两个审查 — 不是为了冗余,而是因为不同模型天然关注不同维度。
> **Next Step:** Want to turn Skills from demo to asset? Check out [Agent Skills Resource Library](https://www.axtonliu.ai/agent-skills) (includes slides, PDF, diagnostics)
## Status
> **Status: Experimental | 状态:实验性**
>
> - This is a public prototype that works for real workflows, but does not yet cover all edge cases. | 公开原型,可用于实际工作流,但未覆盖所有边界情况。
> - Requires Claude Code + Codex CLI + Gemini CLI
> - My primary focus is demonstrating how tools and systems work together, not maintaining this codebase. | 重点是展示工具和系统如何协作,而非维护这个代码库。
> - If you encounter issues, please submit a reproducible case (input + output + steps to reproduce). | 如遇问题,请提交可复现的案例。
## Why This Exists | 为什么做这个
Most people use multiple AI subscriptions by asking the same question to each and comparing answers. That's useful sometimes, but it only uses one dimension of what different models can do — you get multiple answers to the same question, instead of multiple perspectives on the same work.
大部分人用多个 AI 的方式是:同一个问题分别问一遍,然后对比答案。这有时候有用,但只用到了不同模型能力的一个维度 — 你得到的是同一个问题的多个回答,而不是同一份工作的多个视角。
AI-Pair turns model differences into a structured workflow: assign each model a role that matches its strength, and let them review the same work from different angles. It's a [Claude Code Skill](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills) — a reusable instruction set that extends Claude Code's capabilities.
AI-Pair 把模型差异变成结构化的工作流:给每个模型分配匹配其特长的角色,让它们从不同角度审查同一份工作。它是一个 [Claude Code Skill](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/skills) — 一组可复用的指令,扩展 Claude Code 的能力。
## How It Works | 工作原理
```
User (you) | 用户(你)
|
Team Lead (Claude Code session) | 团队领导Claude Code 会话)
|-- creator (Claude Code agent) — writes code or content | 创作者 — 写代码或内容
|-- codex-reviewer (agent → Codex CLI) — analytical review | 分析型审查
|-- gemini-reviewer (agent → Gemini CLI) — editorial review | 编辑型审查
```
The workflow is semi-automatic — you stay in control at every step:
工作流是半自动的 — 每一步你都保持控制权:
1. You assign a task → creator executes | 你下达任务 → 创作者执行
2. Creator reports back → you decide whether to send for review | 创作者回报 → 你决定是否送审
3. Both reviewers analyze in parallel → consolidated report | 两个审查者并行分析 → 汇总报告
4. You decide: revise or pass → loop or next task | 你决定:修改还是通过 → 循环或下一个任务
## Prerequisites | 前置条件
All three are **command-line tools** that run in your terminal (Terminal, iTerm2, etc.), not desktop apps.
三个都是**命令行工具**在终端中运行Terminal、iTerm2 等),不是桌面应用。
| Tool | Purpose | Install |
|------|---------|---------|
| [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) | Team Lead + agent runtime | `npm install -g @anthropic-ai/claude-code` |
| [Codex CLI](https://github.com/openai/codex) | GPT-powered reviewer | `npm install -g @openai/codex` |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | Gemini-powered reviewer | `npm install -g @google/gemini-cli` |
All three CLIs must have authentication configured before use.
三个 CLI 使用前都需要配置好认证。
> **Quick check | 快速检查:** Run `claude --version`, `codex --version`, and `gemini --version` to verify all three are installed.
## Installation | 安装
### Option A: Direct Install (Recommended) | 直接安装(推荐)
```bash
# Clone to your global Claude Code skills directory
# 克隆到 Claude Code 全局 skills 目录
git clone https://github.com/axtonliu/ai-pair.git ~/.claude/skills/ai-pair
```
For project-level installation, clone into `.claude/skills/ai-pair` within your project directory instead.
如需项目级安装,克隆到项目目录下的 `.claude/skills/ai-pair`
### Option B: Manual | 手动安装
1. Download `SKILL.md` from this repo | 下载本仓库的 `SKILL.md`
2. Place it in `~/.claude/skills/ai-pair/SKILL.md` | 放到 `~/.claude/skills/ai-pair/SKILL.md`
3. Restart Claude Code | 重启 Claude Code
## Usage | 使用
### Dev Team — for code, bugs, refactoring | 开发团队 — 写代码、修 bug、重构
```bash
/ai-pair dev-team MyProject
```
Team Lead creates | 团队领导创建:
- **developer** — writes code | 写代码
- **codex-reviewer** — checks bugs, security, performance, edge cases | 审查 bug、安全、性能、边界条件
- **gemini-reviewer** — checks architecture, design patterns, maintainability | 审查架构、设计模式、可维护性
### Content Team — for articles, scripts, newsletters | 内容团队 — 写文章、脚本、Newsletter
```bash
/ai-pair content-team AI-Newsletter
```
Team Lead creates | 团队领导创建:
- **author** — writes content | 写内容
- **codex-reviewer** — checks logic, accuracy, structure, fact-checking | 审查逻辑、准确性、结构、事实核查
- **gemini-reviewer** — checks readability, engagement, style, audience fit | 审查可读性、吸引力、风格、受众适配
### Stop Team | 关闭团队
```bash
/ai-pair team-stop
```
## Real-World Example | 真实案例
We used `content-team` to review a newsletter article. The three AIs found completely different issues:
我们用 `content-team` 审查了一篇 Newsletter 文章。三个 AI 发现的问题完全不同:
- **Claude** (Team Lead): spotted an overreach in interpreting a cited source | 发现对引用来源的过度解读
- **GPT** (Codex): dissected the argument chain and challenged a logical leap | 拆解论证链,质疑逻辑跳跃
- **Gemini**: suggested the opening was too academic for the target audience | 建议开头对目标读者来说太学术化
None of these overlapped. That's the point. See [`examples/`](examples/) for step-by-step walkthrough scenarios.
三者零重叠。这就是意义所在。查看 [`examples/`](examples/) 获取分步演示场景。
## File Structure | 文件结构
```
ai-pair/
├── SKILL.md # Claude Code skill definition | Skill 定义文件
├── README.md # This file | 本文件
├── LICENSE # MIT
└── examples/ # Usage examples | 使用示例
├── dev-team.md
└── content-team.md
```
## Troubleshooting | 常见问题
### Reviewers not actually calling Codex/Gemini CLI | 审查者没有真正调用 Codex/Gemini CLI
**Symptom:** Reviews complete but only Claude Code's usage decreases; Codex/Gemini CLI usage stays flat. The sub-agents are role-playing as Codex/Gemini instead of actually invoking them.
**症状:** 审查完成但只有 Claude Code 的用量在下降Codex/Gemini CLI 用量没有任何变化。Sub-agent 在角色扮演而非真正调用外部 CLI。
**How to verify | 如何验证:** Check the review output for the `**Source: Codex CLI**` / `**Source: Gemini CLI**` label and the `### CLI Raw Output` section. If these are missing, the CLI was not called.
**如何验证:** 检查审查输出中是否有 `**Source: Codex CLI**` / `**Source: Gemini CLI**` 标签和 `### CLI Raw Output` 部分。如果缺失,说明 CLI 没有被调用。
**Fix | 解决方案:** This was addressed in v1.1.0 with mandatory CLI invocation rules. If you're on an older version, update your SKILL.md. If the issue persists, ensure both CLIs are installed and authenticated (`codex --version`, `gemini --version`).
**解决方案:** 此问题已在 v1.1.0 中通过强制 CLI 调用规则修复。如果你使用旧版本,请更新 SKILL.md。如果问题仍然存在确认两个 CLI 都已安装并完成认证(`codex --version`、`gemini --version`)。
## What's Not Included | 未包含的功能
This open-source version includes the **Agent Teams mode** only. The full private version also has:
开源版仅包含 **Agent Teams 模式**。完整私有版还包括:
- **Manual mode** — two CLI instances communicating via shared file | 手动模式 — 两个 CLI 通过共享文件通信
- **iTerm2 orchestration** — automated Author/Reviewer relay with file watchers | iTerm2 编排 — 自动化的创作/审查中继
These require specific local setup and are maintained separately.
这些需要特定的本地配置,单独维护。
## Evolution | 演变
AI-Pair evolved from [AI Roundtable](https://github.com/axtonliu/ai-roundtable), a Chrome extension that lets multiple AI web interfaces discuss and cross-review in the same panel. AI-Pair moves this concept to the command line with structured role assignments, making it more practical for daily workflows.
AI-Pair 从 [AI Roundtable](https://github.com/axtonliu/ai-roundtable) 演变而来。AI Roundtable 是一个 Chrome 扩展,让多个 AI 的网页版在同一个面板里讨论和互评。AI-Pair 把这个概念搬到了命令行,加入了结构化的角色分工,更适合日常工作流。
## Contributing | 贡献
Contributions welcome (low-maintenance project):
欢迎贡献(低维护项目):
- Reproducible bug reports (input + output + steps + environment) | 可复现的 bug 报告
- Documentation improvements | 文档改进
- Small PRs (fixes/docs) | 小型 PR修复/文档)
> **Note:** Feature requests may not be acted on due to limited maintenance capacity. | 功能需求可能因维护精力有限而无法响应。
## License | 许可证
[MIT](LICENSE) - Axton Liu
---
## Author | 作者
**Axton Liu** — AI Educator & Creator
- Website: [axtonliu.ai](https://www.axtonliu.ai)
- YouTube: [@AxtonLiu](https://youtube.com/@AxtonLiu)
- Twitter/X: [@axtonliu](https://x.com/axtonliu)
### Learn More
- [MAPS™ AI Agent Course](https://www.axtonliu.ai/aiagent) - Systematic AI agent skills training
- [Claude Skills: A Systematic Guide](https://www.axtonliu.ai/newsletters/ai-2/posts/claude-agent-skills-maps-framework) - Complete methodology
- [AI Elite Weekly Newsletter](https://www.axtonliu.ai/newsletters/ai-2) - Weekly AI insights
- [Free AI Course](https://www.axtonliu.ai/axton-free-course) - Get started with AI
---
© AXTONLIU™ & AI 精英学院™ 版权所有

609
ai-pair/SKILL.md Normal file
View File

@ -0,0 +1,609 @@
---
name: ai-pair
description: |
AI Pair Collaboration Skill. Coordinate multiple AI models to work together:
one creates (Author/Developer), others review (configurable: Codex, Gemini, Kimi).
Works for code, articles, video scripts, and any creative task.
Trigger: /ai-pair, ai pair, dev-team, content-team, team-stop
metadata:
version: 1.4.0
---
# AI Pair Collaboration
Coordinate heterogeneous AI teams: one creates, others review from different angles.
Uses Claude Code's native Agent Teams capability with configurable reviewers (Codex, Gemini, Kimi).
## Why Multiple AI Reviewers?
Different AI models have fundamentally different review tendencies. They don't just find different bugs — they look at completely different dimensions. Using reviewers from different model families maximizes coverage.
- **Codex** (OpenAI) — analytical, detail-oriented: bugs, security, edge cases
- **Gemini** (Google) — architectural, big-picture: design patterns, maintainability
- **Kimi** (Moonshot AI) — quality-focused, Chinese-native: code quality, naming, readability, Chinese context
## Commands
```bash
/ai-pair dev-team [project] # Default reviewers: codex + gemini
/ai-pair dev-team [project] --reviewers codex,gemini # Same as default
/ai-pair dev-team [project] --reviewers codex,kimi # Codex + Kimi
/ai-pair dev-team [project] --reviewers gemini,kimi # Gemini + Kimi
/ai-pair dev-team [project] --reviewers codex,gemini,kimi # All three
/ai-pair dev-team [project] --reviewers kimi # Kimi only
/ai-pair content-team [topic] # Default reviewers: codex + gemini
/ai-pair content-team [topic] --reviewers codex,kimi # Codex + Kimi
/ai-pair team-stop # Shut down the team
```
### `--reviewers` Flag
- Comma-separated list of reviewer names: `codex`, `gemini`, `kimi`
- Default (when omitted): `codex,gemini` (backward compatible)
- At least one reviewer must be specified
- Team Lead parses the argument and only launches the selected reviewer agents
- If an invalid name is given, report error and list valid options
Examples:
```bash
/ai-pair dev-team HighlightCut # codex + gemini (default)
/ai-pair dev-team HighlightCut --reviewers kimi # kimi only
/ai-pair dev-team HighlightCut --reviewers codex,kimi # codex + kimi
/ai-pair content-team AI-Newsletter --reviewers gemini,kimi # gemini + kimi
/ai-pair team-stop # Shut down team
```
## Prerequisites
- **Claude Code** — Team Lead + agent runtime
- **Codex CLI** (`codex`) — for codex-reviewer (when selected)
- **Gemini CLI** (`gemini`) — for gemini-reviewer (when selected)
- **Kimi CLI** (`kimi`) — for kimi-reviewer (when selected), Moonshot AI's model
- Selected external CLIs must have authentication configured
## Team Architecture
### Dev Team (`/ai-pair dev-team [project]`)
```
User (Commander)
|
Team Lead (current Claude session)
|-- developer (Claude Code agent) — writes code, implements features
|-- codex-reviewer (when selected) — via codex CLI
| Focus: bugs, security, concurrency, performance, edge cases
|-- gemini-reviewer (when selected) — via gemini CLI
| Focus: architecture, design patterns, maintainability, alternatives
|-- kimi-reviewer (when selected) — via kimi CLI
Focus: code quality, naming conventions, readability, Chinese comments
```
### Content Team (`/ai-pair content-team [topic]`)
```
User (Commander)
|
Team Lead (current Claude session)
|-- author (Claude Code agent) — writes articles, scripts, newsletters
|-- codex-reviewer (when selected) — via codex CLI
| Focus: logic, accuracy, structure, fact-checking
|-- gemini-reviewer (when selected) — via gemini CLI
| Focus: readability, engagement, style consistency, audience fit
|-- kimi-reviewer (when selected) — via kimi CLI
Focus: narrative flow, emotional resonance, cultural sensitivity (Chinese audience)
```
## Workflow (Semi-Automatic)
Team Lead coordinates the following loop:
1. **User assigns task** → Team Lead sends to developer/author
2. **Developer/author completes** → Team Lead shows result to user
3. **User approves for review** → Team Lead sends to all selected reviewers in parallel
4. **Reviewers report back** → Team Lead consolidates and presents (only selected reviewers appear):
```
## {Reviewer-Name} Review
{reviewer feedback summary}
## {Reviewer-Name} Review
{reviewer feedback summary}
```
5. **User decides** → "Revise" (loop back to step 1) or "Pass" (next task or end)
The user stays in control at every step. No autonomous loops.
## Project Detection
The project/topic is determined by:
1. **Explicitly specified** → use as-is
2. **Current directory is inside a project** → extract project name from path
3. **Ambiguous** → ask user to choose
## Team Lead Execution Steps
### Step 1: Parse Arguments
Parse the command for:
- Team type: `dev-team` or `content-team`
- Project/topic name
- `--reviewers` flag (default: `codex,gemini`)
Validate reviewer names. Valid values: `codex`, `gemini`, `kimi`. If invalid name found, report error immediately.
### Step 2: Create Team
```
TeamCreate: team_name = "{project}-dev" or "{topic}-content"
```
### Step 3: Create Tasks
Use TaskCreate to set up initial task structure:
1. "Awaiting task assignment" — for developer/author, status: pending
For each selected reviewer:
- "Awaiting review" — for {reviewer}-reviewer, status: pending, blockedBy task 1
Example with `--reviewers codex,kimi`:
1. "Awaiting task assignment" — for developer, status: pending
2. "Awaiting review" — for codex-reviewer, status: pending, blockedBy task 1
3. "Awaiting review" — for kimi-reviewer, status: pending, blockedBy task 1
### Step 4: Pre-flight CLI Check
Before launching agents, verify ONLY the selected reviewers' CLIs:
```bash
# Only check CLIs that are selected via --reviewers
# For codex (if selected):
command -v codex && codex --version || echo "CODEX_MISSING"
# For gemini (if selected):
command -v gemini && gemini --version || echo "GEMINI_MISSING"
# For kimi (if selected):
command -v kimi && kimi --version || echo "KIMI_MISSING"
```
If any selected CLI is missing, warn the user immediately and ask whether to:
- Proceed with degraded mode (Claude-only review for that reviewer, clearly labeled)
- Remove that reviewer from the team
- Abort
### Step 5: Launch Agents
Launch 1 + N agents (1 developer/author + N selected reviewers) using the Agent tool with `subagent_type: "general-purpose"` and `mode: "bypassPermissions"` (required because reviewers need to execute external CLI commands and read project files).
See Agent Prompt Templates below for each agent's startup prompt. Only launch agents for selected reviewers.
### Step 6: Confirm to User
```
Team ready.
Team: {team_name}
Type: {Dev Team / Content Team}
Reviewers: {comma-separated selected reviewers} (via --reviewers)
Members:
- developer/author: ready
- {reviewer-1}-reviewer: ready
- {reviewer-2}-reviewer: ready
[... for each selected reviewer ...]
Awaiting your first task.
```
## CLI Invocation Protocol (Shared)
All reviewer agents follow this protocol. Team Lead includes it in each reviewer's prompt.
```
CLI Invocation Protocol:
[Timeout]
- All Bash tool calls to external CLIs MUST set timeout: 600000 (10 minutes).
- External CLIs (codex/gemini/kimi) need 10-15 seconds to load,
plus model reasoning time. The default 2-minute timeout is far too short.
[Degradation Retry by CLI]
Codex CLI:
- Defaults to xhigh reasoning level.
- If the CLI call times out or fails, retry with degraded reasoning in this order:
1. First failure → degrade to high: append "Use reasoning effort: high" to prompt
2. Second failure → degrade to medium: append "Use reasoning effort: medium"
3. Third failure → degrade to low: append "Use reasoning effort: low"
4. Fourth failure → Claude fallback analysis (last resort)
Gemini CLI:
- If timeout, append simplified instructions / reduce analysis dimensions.
1. First failure → simplify prompt (fewer analysis dimensions)
2. Second failure → minimal prompt (single-focus review)
3. Third failure → Claude fallback analysis (last resort)
Kimi CLI:
- Defaults to full thinking mode with --print.
- If the CLI call times out or fails, retry with degraded mode in this order:
1. First failure → switch to --quiet (--print --output-format text --final-message-only)
2. Second failure → add --no-thinking to disable extended thinking
3. Third failure → simplified prompt (reduce analysis dimensions)
4. Fourth failure → Claude fallback analysis (last resort)
- Report the current degradation level to team-lead on each retry.
[Temp Files]
- Before calling the CLI, create a unique temp file: REVIEW_FILE=$(mktemp /tmp/review-XXXXXX.txt)
Write content to $REVIEW_FILE. This prevents concurrent tasks from overwriting each other.
[Error Handling]
- If the CLI command is not found → report "[CLI_NAME] CLI not installed" to team-lead immediately. Do NOT substitute your own review.
- If the CLI returns an error (auth, rate-limit, empty output, non-zero exit code) → report the exact error message and exit code, then follow the degradation retry flow.
- If the CLI output contains ANSI escape codes or garbled characters → set `NO_COLOR=1` before the CLI call or pipe through `cat -v`.
- NEVER silently skip the CLI call.
- Only use Claude fallback after ALL degradation retries have failed, clearly labeled "[Claude Fallback — [CLI_NAME] all retries failed]".
[Cleanup]
- Clean up: rm -f $REVIEW_FILE after capturing output.
```
## Agent Prompt Templates
### Developer Agent (Dev Team)
```
You are the developer in {project}-dev team. You write code.
Project path: {project_path}
Project info: {CLAUDE.md summary if available}
Workflow:
1. Read relevant files to understand context
2. Implement the feature / fix the bug / refactor
3. Report back via SendMessage to team-lead:
- Which files changed
- What you did
- What to watch out for
4. When receiving reviewer feedback, address items and report again
5. Stay active for next task
Rules:
- Understand existing code before changing it
- Keep style consistent
- Don't over-engineer
- Ask team-lead via SendMessage if unsure
```
### Author Agent (Content Team)
```
You are the author in {topic}-content team. You write content.
Working directory: {working_directory}
Topic: {topic}
Workflow:
1. Understand the writing task and reference materials
2. If style-memory.md exists, read and follow it
3. Write content following the appropriate format
4. Report back via SendMessage to team-lead with full content or summary
5. When receiving reviewer feedback, revise and report again
6. Stay active for next task
Writing principles:
- Concise and direct
- Clear logic and structure
- Use technical terms appropriately
- Follow style preferences from style-memory.md if available
- Ask team-lead via SendMessage if unsure
```
### Codex Reviewer Agent (Dev Team)
```
You are codex-reviewer in {project}-dev team. Your job is to get CODE REVIEW from the real Codex CLI.
CRITICAL RULE: You MUST use the Bash tool to invoke the `codex` command. You are a dispatcher, NOT a reviewer.
DO NOT review the code yourself. DO NOT role-play as Codex. Your value is that you bring a DIFFERENT model's perspective.
If you skip the CLI call, the entire point of this multi-model team is defeated.
Project path: {project_path}
Review process:
1. Read relevant code changes using Read/Glob/Grep
2. Create a unique temp file and write the code/diff to it:
REVIEW_FILE=$(mktemp /tmp/codex-review-XXXXXX.txt)
3. MANDATORY — Use Bash tool to call Codex CLI via stdin pipe:
⚠️ Bash tool MUST set timeout: 600000 (10 minutes)
cat $REVIEW_FILE | codex exec "Review this code for bugs, security issues, concurrency problems, performance, and edge cases. Be specific about file paths and line numbers. Output in Chinese." 2>&1
4. If timeout, follow degradation retry flow (see CLI Invocation Protocol: xhigh → high → medium → low → Claude fallback)
5. Capture the FULL CLI output. Do not summarize or rewrite it.
6. Clean up: rm -f $REVIEW_FILE
7. Report to team-lead via SendMessage:
## Codex Code Review
**Source: Codex CLI [reasoning level]** (or "Source: Claude Fallback — four retries all failed" if all failed)
### CLI Raw Output
{paste the actual codex CLI output here}
### Consolidated Assessment
#### CRITICAL (blocking issues)
- {description + file:line + suggested fix}
#### WARNING (important issues)
- {description + suggestion}
#### SUGGESTION (improvements)
- {suggestion}
### Summary
{one-line quality assessment}
Focus: bugs, security vulnerabilities, concurrency/race conditions, performance, edge cases.
Follow the shared CLI Invocation Protocol (timeout + degradation retry). Stay active for next review task.
```
### Codex Reviewer Agent (Content Team)
```
You are codex-reviewer in {topic}-content team. Your job is to get CONTENT REVIEW from the real Codex CLI.
CRITICAL RULE: You MUST use the Bash tool to invoke the `codex` command. You are a dispatcher, NOT a reviewer.
DO NOT review the content yourself. DO NOT role-play as Codex. Your value is that you bring a DIFFERENT model's perspective.
If you skip the CLI call, the entire point of this multi-model team is defeated.
Review process:
1. Understand the content and context
2. Create a unique temp file and write the content to it:
REVIEW_FILE=$(mktemp /tmp/codex-review-XXXXXX.txt)
3. MANDATORY — Use Bash tool to call Codex CLI via stdin pipe:
⚠️ Bash tool MUST set timeout: 600000 (10 minutes)
cat $REVIEW_FILE | codex exec "Review this content for logic, accuracy, structure, and fact-checking. Be specific. Output in Chinese." 2>&1
4. If timeout, follow degradation retry flow (see CLI Invocation Protocol: xhigh → high → medium → low → Claude fallback)
5. Capture the FULL CLI output.
6. Clean up: rm -f $REVIEW_FILE
7. Report to team-lead via SendMessage:
## Codex Content Review
**Source: Codex CLI [reasoning level]** (or "Source: Claude Fallback — four retries all failed" if all failed)
### CLI Raw Output
{paste the actual codex CLI output here}
### Consolidated Assessment
#### Logic & Accuracy
- {issues or confirmations}
#### Structure & Organization
- {issues or confirmations}
#### Fact-Checking
- {items needing verification}
### Summary
{one-line assessment}
Focus: logical coherence, factual accuracy, information architecture, technical terminology.
Follow the shared CLI Invocation Protocol (timeout + degradation retry). Stay active for next review task.
```
### Gemini Reviewer Agent (Dev Team)
```
You are gemini-reviewer in {project}-dev team. Your job is to get CODE REVIEW from the real Gemini CLI.
CRITICAL RULE: You MUST use the Bash tool to invoke the `gemini` command. You are a dispatcher, NOT a reviewer.
DO NOT review the code yourself. DO NOT role-play as Gemini. Your value is that you bring a DIFFERENT model's perspective.
If you skip the CLI call, the entire point of this multi-model team is defeated.
Project path: {project_path}
Review process:
1. Read relevant code changes using Read/Glob/Grep
2. Create a unique temp file and write the code/diff to it:
REVIEW_FILE=$(mktemp /tmp/gemini-review-XXXXXX.txt)
3. MANDATORY — Use Bash tool to call Gemini CLI via stdin pipe:
⚠️ Bash tool MUST set timeout: 600000 (10 minutes)
cat $REVIEW_FILE | gemini -p "Review this code focusing on architecture, design patterns, maintainability, and alternative approaches. Be specific about file paths and line numbers. Output in Chinese." 2>&1
4. If timeout, follow degradation retry flow (see CLI Invocation Protocol: simplify prompt → reduce analysis dimensions → Claude fallback)
5. Capture the FULL CLI output. Do not summarize or rewrite it.
6. Clean up: rm -f $REVIEW_FILE
7. Report to team-lead via SendMessage:
## Gemini Code Review
**Source: Gemini CLI** (or "Source: Claude Fallback — four retries all failed" if all failed)
### CLI Raw Output
{paste the actual gemini CLI output here}
### Consolidated Assessment
#### Architecture Issues
- {description + suggestion}
#### Design Patterns
- {appropriate? + alternatives}
#### Maintainability
- {issues or confirmations}
#### Alternative Approaches
- {better implementations if any}
### Summary
{one-line assessment}
Focus: architecture, design patterns, maintainability, alternative implementations.
Follow the shared CLI Invocation Protocol (timeout + degradation retry). Stay active for next review task.
```
### Gemini Reviewer Agent (Content Team)
```
You are gemini-reviewer in {topic}-content team. Your job is to get CONTENT REVIEW from the real Gemini CLI.
CRITICAL RULE: You MUST use the Bash tool to invoke the `gemini` command. You are a dispatcher, NOT a reviewer.
DO NOT review the content yourself. DO NOT role-play as Gemini. Your value is that you bring a DIFFERENT model's perspective.
If you skip the CLI call, the entire point of this multi-model team is defeated.
Review process:
1. Understand the content and context
2. Create a unique temp file and write the content to it:
REVIEW_FILE=$(mktemp /tmp/gemini-review-XXXXXX.txt)
3. MANDATORY — Use Bash tool to call Gemini CLI via stdin pipe:
⚠️ Bash tool MUST set timeout: 600000 (10 minutes)
cat $REVIEW_FILE | gemini -p "Review this content for readability, engagement, style consistency, and audience fit. Be specific. Output in Chinese." 2>&1
4. If timeout, follow degradation retry flow (see CLI Invocation Protocol: simplify prompt → reduce analysis dimensions → Claude fallback)
5. Capture the FULL CLI output.
6. Clean up: rm -f $REVIEW_FILE
7. Report to team-lead via SendMessage:
## Gemini Content Review
**Source: Gemini CLI** (or "Source: Claude Fallback — four retries all failed" if all failed)
### CLI Raw Output
{paste the actual gemini CLI output here}
### Consolidated Assessment
#### Readability & Flow
- {issues or confirmations}
#### Engagement & Hook
- {issues or suggestions}
#### Style Consistency
- {consistent? + specific deviations}
#### Audience Fit
- {appropriate? + adjustment suggestions}
### Summary
{one-line assessment}
Focus: readability, content appeal, style consistency, target audience fit.
Follow the shared CLI Invocation Protocol (timeout + degradation retry). Stay active for next review task.
```
### Kimi Reviewer Agent (Dev Team)
```
You are kimi-reviewer in {project}-dev team. Your job is to get CODE REVIEW from the real Kimi CLI.
CRITICAL RULE: You MUST use the Bash tool to invoke the `kimi` command. You are a dispatcher, NOT a reviewer.
DO NOT review the code yourself. DO NOT role-play as Kimi. Your value is that you bring a DIFFERENT model's perspective (Moonshot AI / Kimi K2).
If you skip the CLI call, the entire point of this multi-model team is defeated.
Project path: {project_path}
Review process:
1. Read relevant code changes using Read/Glob/Grep
2. Create a unique temp file and write the code/diff to it:
REVIEW_FILE=$(mktemp /tmp/kimi-review-XXXXXX.txt)
3. MANDATORY — Use Bash tool to call Kimi CLI via stdin pipe:
⚠️ Bash tool MUST set timeout: 600000 (10 minutes)
cat $REVIEW_FILE | kimi --print -p "审查这段代码重点关注代码质量简洁性、DRY原则、单一职责、命名规范变量/函数/类名是否清晰准确、风格是否一致)、可读性(逻辑是否易懂、是否需要注释)、中文注释质量(如有中文注释,检查准确性和表达)。请指出具体文件路径和行号。用中文输出。" 2>&1
4. If timeout, follow degradation retry flow (see CLI Invocation Protocol: --print → --quiet → --no-thinking → simplified prompt → Claude fallback)
5. Capture the FULL CLI output. Do not summarize or rewrite it.
6. Clean up: rm -f $REVIEW_FILE
7. Report to team-lead via SendMessage:
## Kimi Code Review
**Source: Kimi CLI** (or "Source: Claude Fallback — all retries failed" if all failed)
### CLI Raw Output
{paste the actual kimi CLI output here}
### Consolidated Assessment
#### Code Quality
- {simplicity, DRY, single responsibility + file:line + suggested fix}
#### Naming Conventions
- {variable/function/class names: clear? consistent? following project conventions?}
#### Readability
- {code clarity, complexity, self-documenting quality}
#### Chinese Comments Quality
- {accuracy, completeness, grammar of Chinese comments if present; or "N/A" if no Chinese comments}
### Summary
{one-line quality assessment}
Focus: code quality, naming conventions, readability, Chinese code comments quality.
Follow the shared CLI Invocation Protocol (timeout + degradation retry). Stay active for next review task.
```
### Kimi Reviewer Agent (Content Team)
```
You are kimi-reviewer in {topic}-content team. Your job is to get CONTENT REVIEW from the real Kimi CLI.
CRITICAL RULE: You MUST use the Bash tool to invoke the `kimi` command. You are a dispatcher, NOT a reviewer.
DO NOT review the content yourself. DO NOT role-play as Kimi. Your value is that you bring a DIFFERENT model's perspective (Moonshot AI / Kimi K2).
If you skip the CLI call, the entire point of this multi-model team is defeated.
Review process:
1. Understand the content and context
2. Create a unique temp file and write the content to it:
REVIEW_FILE=$(mktemp /tmp/kimi-review-XXXXXX.txt)
3. MANDATORY — Use Bash tool to call Kimi CLI via stdin pipe:
⚠️ Bash tool MUST set timeout: 600000 (10 minutes)
cat $REVIEW_FILE | kimi --print -p "审查这篇内容,重点关注:叙事节奏(故事弧线、段落衔接、起承转合)、情感共鸣(情感冲击力、读者代入感、语调把控)、中文受众文化适配(文化引用是否恰当、表达是否符合中文读者习惯、本地化质量)。请具体指出问题位置。用中文输出。" 2>&1
4. If timeout, follow degradation retry flow (see CLI Invocation Protocol: --print → --quiet → --no-thinking → simplified prompt → Claude fallback)
5. Capture the FULL CLI output.
6. Clean up: rm -f $REVIEW_FILE
7. Report to team-lead via SendMessage:
## Kimi Content Review
**Source: Kimi CLI** (or "Source: Claude Fallback — all retries failed" if all failed)
### CLI Raw Output
{paste the actual kimi CLI output here}
### Consolidated Assessment
#### Narrative Flow
- {story arc, pacing, transitions between sections}
#### Emotional Resonance
- {emotional impact, reader connection, tone consistency}
#### Cultural Sensitivity (Chinese Audience)
- {cultural references, idioms, audience-appropriate expressions, localization quality}
### Summary
{one-line assessment}
Focus: narrative flow, emotional resonance, cultural sensitivity for Chinese audience.
Follow the shared CLI Invocation Protocol (timeout + degradation retry). Stay active for next review task.
```
## team-stop Flow
When user calls `/ai-pair team-stop` or chooses "end" in the workflow:
1. Send `shutdown_request` to all active agents
2. Wait for all agents to confirm shutdown
3. Call `TeamDelete` to clean up team resources
4. Output:
```
Team shut down.
Closed members: developer/author, {list of active reviewers}
Resources cleaned up.
```

View File

@ -0,0 +1,102 @@
# Content Team Example
## Scenario: Review a Newsletter Article
You've drafted a newsletter about AI memory systems and want multi-perspective feedback before publishing.
### Start the team
```bash
/ai-pair content-team AI-Newsletter
```
Output:
```
Team ready.
Team: AI-Newsletter-content
Type: Content Team
Members:
- author: ready
- codex-reviewer: ready
- gemini-reviewer: ready
Awaiting your first task.
```
### Assign a task
```
Review this draft newsletter article about why AI memory
(like MEMORY.md in Claude Code) isn't actually "memory" in the
way most people think. The article argues that it's closer to
a post-it note than a brain.
[paste your draft or provide file path]
```
The author agent reads the draft and prepares it for review. You approve sending to reviewers.
### Review results
```
## Codex Review
### Logic & Accuracy
- The claim "LLMs have no persistent state between sessions" is technically
correct but oversimplified. Some systems do maintain vector stores that
persist. Consider qualifying with "by default" or "natively".
- The analogy to post-it notes breaks down: post-it notes don't have
retrieval logic. MEMORY.md is more like a briefing document.
### Fact-Checking
- The reference to "context window limits of 200K tokens" should specify
this is Claude's limit. GPT-4 Turbo is 128K.
### Summary
Solid argument, but two factual claims need tightening.
## Gemini Review
### Readability & Flow
- The opening paragraph uses three technical terms before the reader
gets the hook. Consider leading with the relatable scenario first.
### Engagement & Hook
- The "post-it note" metaphor is strong but introduced too late (paragraph 4).
Move it to the opening for immediate impact.
### Audience Fit
- Tone shifts between casual (paragraphs 1-3) and academic (paragraphs 4-6).
The casual tone matches the target audience better — maintain it throughout.
### Summary
Good content, needs structural reorganization for maximum impact.
```
Codex caught **factual precision issues**. Gemini caught **readability and structure issues**. Zero overlap.
### Iterate
You tell Team Lead:
```
Fix the factual claims Codex flagged.
Move the post-it metaphor to the opening as Gemini suggested.
Keep the casual tone throughout.
Don't change the core argument.
```
The author revises. You can send for another round of review or pass.
### End the session
```bash
/ai-pair team-stop
```
## Tips for Content Team
1. **Provide context about your audience** — reviewers give better feedback when they know who's reading
2. **Don't fix everything** — you decide which feedback matters. Codex tends to over-index on precision; Gemini tends to over-index on accessibility
3. **Use iteratively** — first round for big issues, second round for polish
4. **Style memory** — if you have a `style-memory.md` file, the author agent will automatically follow your style preferences

View File

@ -0,0 +1,81 @@
# Dev Team Example
## Scenario: Review a Pull Request
You're working on a project and want multiple AI perspectives on your code changes.
### Start the team
```bash
/ai-pair dev-team my-web-app
```
Output:
```
Team ready.
Team: my-web-app-dev
Type: Dev Team
Members:
- developer: ready
- codex-reviewer: ready
- gemini-reviewer: ready
Awaiting your first task.
```
### Assign a task
```
Implement rate limiting for the /api/login endpoint.
Max 5 attempts per IP per 15 minutes.
```
The developer agent reads the codebase, implements the feature, and reports back.
### Review results
After you approve sending for review, both reviewers analyze in parallel:
```
## Codex Review
### WARNING
- Rate limit key uses only IP address. Behind a proxy, all users share one IP.
Consider combining IP + user-agent or using X-Forwarded-For.
- No cleanup mechanism for expired entries in the rate limit store.
### SUGGESTION
- Add a header (X-RateLimit-Remaining) so clients know their status.
### Summary
Functional implementation, but proxy scenario needs attention.
## Gemini Review
### Architecture Issues
- In-memory store won't work with multiple server instances.
Consider Redis or a shared store.
### Design Patterns
- Rate limiter is tightly coupled to the route handler.
Extract as middleware for reuse on other endpoints.
### Summary
Works for single-instance deployment. Needs abstraction for production scale.
```
Notice how Codex found a **security edge case** (proxy IP sharing) while Gemini found an **architecture limitation** (single-instance only). Different dimensions, both valuable.
### Decide
You can say:
- **"Revise"** — feedback gets sent to developer, loop continues
- **"Pass"** — move to next task or end team
- **"Revise, but only fix the proxy issue and Redis. Skip the middleware refactor for now."** — you control the scope
### End the session
```bash
/ai-pair team-stop
```

View File

@ -0,0 +1,112 @@
---
name: annuity-disclosure-crawler
description: 全量爬取中国企业年金基金投资管理人在其官网发布的《企业年金基金投资管理情况》季度管理报告。内置人社部许可的22家投资管理人名录+每家官网披露栏目入口、抓取策略与站点反爬坑位;驱动脚本按季全量下载并做真伪校验与清单记录。当需要采集/批量下载企业年金(或职业年金)季度管理报告/信息披露、定位某投资管理人官网披露页、更新报告库、或做年金管理人季报数据集时使用。触发词企业年金、投资管理人、季度报告、季报、信息披露、管理情况、年金爬取、全量爬取、年金披露、pension disclosure crawl。
---
# 企业年金投资管理人季报全量爬取
## 这个技能做什么
把人社部许可的 **22 家企业年金基金投资管理人**在各自官网公开发布的
《企业年金基金投资管理情况》**季度报告**,按季**全量**抓取到本地,做真伪校验并生成清单。
名录、每家披露栏目 URL、抓取策略、站点反爬坑位都已固化在 `references/registry.json`
两个脚本读它干活,无需每次重新摸索。
核心事实(先读懂再动手):
- **口径**:抓的是投资管理人**自披露**的《企业年金基金投资管理情况》季度信息披露表。它 ≠ 受托人向委托人/人社部报送的完整《企业年金基金管理情况季度报告》全文(后者按规定不公开)。检索时排在前面的“集合型企业年金计划管理情况信息披露表”常是**受托人口径**(如工商银行、各托管行),别混进投资管理人季报。
- **不是每家都公开**:约 19 家官网公开可下;博时、招商的季报仅登录客户门户可见;建信养老金官网只发“养老金产品”层报告且被 WAF+加密挡住。详见 registry 里各家 `access`
- **报告形态三种**PDF多数、**DOCX**(泰康资产,链接无扩展名)、**HTML 网页正文**(海富通/南方/长江/中信证券/人保,无 PDF 附件,存网页)。
- **数据坑**:人保养老(picc)官网自 2022Q1 起正文区为空,只登记标题+日期,近四年数据未公开。
## 两层抓取工作流
先用纯 HTTP快、无依赖搞不定的再上无头浏览器。
### 第 1 步:看名录与策略
```bash
python3 scripts/crawl.py --list
```
列出 22 家的 slug / 简称 / access(public|waf|login) / strategy / 披露入口。
### 第 2 步HTTP 全量抓取(默认路径)
```bash
# 全量抓所有可 HTTP 抓取的机构(跳过 waf/login会明确提示哪几家要换 headless
python3 scripts/crawl.py --out annuity_reports
# 只抓某几家;--latest N 只留最新 N 期(0=全量,默认全量)
python3 scripts/crawl.py --only m04,m09,m17 --latest 4 --out annuity_reports
```
`strategy` 含义(脚本自动按此处理,一般无需关心):
`pdf_index` 列表页直给 PDF 直链 · `detail_index` 列表→详情页→再取文件 ·
`html_index` 报告本身是 HTML 网页(存网页)· `spa_api` 列表走后端 JSON ·
`headless` 需浏览器(脚本会跳过并提示)· `login_blocked` 非公开(跳过并说明)。
脚本已内置的通用能力:列表翻页(首页 index.html + index_1/2… 自动补全)、
GBK/UTF-8 自适应解码、pdfjs `viewer.html?file=` 包装解包、相对链接补全、
期次识别(`2026Q1`/`2026年1季度`/中文数字季度、真伪校验PDF 看 `%PDF` 魔数、
DOCX 看 `PK`、HTML 看大小+关键字)、内容 MD5 去重、同期两类报告防覆盖。
### 第 3 步无头浏览器兜底WAF/SPA 机构)
当第 2 步对某家提示“0 命中:请改用 crawl_headless.py”或该家 `access=waf`/`strategy=headless`
```bash
pip install playwright --break-system-packages && python3 -m playwright install chromium
# 默认抓所有需浏览器的机构;也可 --only 指定;--headful 显示窗口调试
python3 scripts/crawl_headless.py --only m15,m21 --latest 4 --out annuity_reports
```
真实 Chromium 会自动过瑞数(botgate 412)、加速乐(jsl 521)等 JS 挑战,
渲染出 SPA 列表后用带 Cookie 的浏览器上下文下载(含跨域文件站)。复用 crawl.py 的
识别/校验/命名逻辑,落地到**同一** `--out` 目录,另出 `manifest_headless.*`
典型需 headless 的:富国/工银瑞信/嘉实/易方达(SPA 列表)、平安养老/华泰资产(动态列表)、
银华/太平养老/中金(反爬 WAF)。
## 输出结构与清单
```
annuity_reports/
├── m04/ m04_2026Q1.pdf m04_2025Q4.pdf ... _disclosure_page.html
├── m13/ m13_2026Q1.docx ...
├── m22/ m22_2026Q1.html ...
├── manifest.csv / manifest.json # crawl.py 清单机构·期次·状态·字节·本地路径·源URL
└── manifest_headless.csv / .json # crawl_headless.py 清单
```
`status` 取值:`ok`(校验通过)/ `not-pdf`/`not-docx`/`suspect`(存为 `*.suspect` 待查)/ `download-fail:*`
交付前用 manifest 核对每家是否抓全、有无 suspect。
## 全量 vs 增量
- **全量**:不加 `--latest`(默认 0脚本翻完列表所有分页抓每家全部历史季度。
首轮建议单跑几家验证:`--only m04,m17,m13`,确认无误再整体跑。
- **增量**:定期只补新季度用 `--latest 2`MD5 去重会跳过已存在的同内容文件(但会重新下载比对,
真正省流可结合已存在文件名跳过)。季度披露节奏:一般次季度中下旬发布(如 Q1 报告约 6 月中下旬)。
## 排障速查
- **HTTP 0 命中** → 列表 JS 渲染或被 WAF 挡:改 `crawl_headless.py`
- **headless 仍 0 命中** → 可能需登录、或报告藏在需点击的折叠/下一页里:加 `--headful` 肉眼看,
必要时在 registry 该家 `list_url` 填精确分页模板或改 `strategy`
- **拿到的 PDF 打不开/很小** → 多半是 WAF 降级的 HTML 错误页;脚本已标 `suspect`。换 headless。
- **404 一堆中文名 .pdf** → 详情页里的“显示用文件名”被误当链接;已在 `files_in_page` 收敛,若仍有属噪声可忽略(不影响真链)。
- **工银瑞信**:官网迁 icbccs→icbcubs 成 SPA直链多 404可靠源是 Wayback 快照
`https://web.archive.org/web/{ts}id_/{原PDF直链}`(快照止于 2024Q2
- **name 冲突** `rtk gain` 无关;与本技能无关的报错先 `--list` 自检 registry 是否可读。
## 维护 registry增删改机构或修坑
`references/registry.json` 是唯一事实源,字段语义见其 `meta.field_notes`。常见维护:
- 名单变动:以**人社部社保基金监管司**最新《企业年金基金管理机构名单》为准,增删 `managers` 条目。
- 某家官网改版:更新其 `disclosure_url` / `list_url`(分页模板,`{n}` 为页码) / `file_url_pattern`(报告文件或报告页 URL 正则) / `strategy`
- 加新机构:复制一条,填 `slug`(mNN)、`short`、`name`、`category`、`access`、`strategy`、`item_type`、入口与正则,先 `--only 新slug --latest 2` 验证。
registry 里每家的 `notes` 记录了该站的真实坑位(编码、反爬类型、命名规律、口径提醒),改站点前先读。

View File

@ -0,0 +1,421 @@
{
"meta": {
"title": "企业年金基金投资管理人 · 官网季度管理报告披露源登记",
"roster_size": 22,
"roster_source": "人社部 2021-10《关于企业年金基金管理机构资格延续的通告》中的投资管理人22家+ 基金业协会 amac.org.cn 查询栏目交叉核对。以人社部社保基金监管司最新《企业年金基金管理机构名单》为最终依据。",
"verified": "2026-07",
"report_title_canonical": "企业年金基金投资管理情况(季度)",
"field_notes": {
"access": "public=官网公开可匿名下载; waf=官网有披露栏目但被反爬WAF/JS挑战拦截需无头浏览器; login=季报仅登录客户门户可见,不公开",
"strategy": "驱动脚本 scripts/crawl.py 的抓取策略pdf_index=列表页直接给文件直链; detail_index=列表→详情页→再取文件链; html_index=报告本身是HTML网页(无PDF附件),存网页; spa_api=列表走后端JSON接口; headless=需 playwright 渲染(WAF/SPA); login_blocked=跳过",
"item_type": "报告落地文件类型pdf / docx / html",
"list_url": "披露列表页;{n} 为翻页占位符全量抓取时从1递增直到空页"
}
},
"managers": [
{
"slug": "m01",
"short": "博时",
"name": "博时基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.bosera.com",
"access": "login",
"strategy": "login_blocked",
"item_type": "html",
"disclosure_url": "https://www.bosera.com/column/index.do?classid=00020002000600090001",
"list_url": null,
"page_param": null,
"file_url_pattern": null,
"notes": "官网养老/年金板块的季度管理报告、动态月报、净值持仓在「年金客户专区」,需账号密码登录 trade.bosera.com不对公众公开。客服 95105568。"
},
{
"slug": "m02",
"short": "富国",
"name": "富国基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.fullgoal.com.cn",
"access": "public",
"strategy": "detail_index",
"item_type": "pdf",
"disclosure_url": "https://www.fullgoal.com.cn/main/InstiServices/Retirement/RetirementInfoDis/qynjtzgl/index.html",
"list_url": "https://www.fullgoal.com.cn/main/InstiServices/Retirement/RetirementInfoDis/qynjtzgl/index.html",
"page_param": null,
"file_url_pattern": "https?://www\\.fullgoal\\.com\\.cn/(upload/fck/userfiles/file|wbs-file/ws)/[^\"'\\s]+\\.pdf",
"example_links": [
"https://www.fullgoal.com.cn/wbs-file/ws/20250320/73059d0333984f7cb04df6ac6fe35fec.pdf"
],
"notes": "Vue SPA 但列表/文章页 SSRPDF 直链稳定。栏目→机构服务→养老金信息披露→企业年金投资管理。命名早期 fgjjglyxgsqynjjjtzglqk(YYYYnXjd).pdf后期哈希名。抓取时官网疑似停留在 2024Q4。SPA 渲染不全时用 headless 兜底。\n\n[2026-07-07 全量验证] crawl.py HTTP 全量抓取通过10期(2023Q4-2026Q1)。⚠该披露页无翻页控件就是官网展示的全部——早于2023Q4的报告在当前官网结构下不存在任何入口历史更早数据对公众不可得非爬虫限制是官网只保留近~2年展示。"
},
{
"slug": "m03",
"short": "工银瑞信",
"name": "工银瑞信基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.icbcubs.com.cn",
"access": "waf",
"strategy": "headless",
"item_type": "pdf",
"disclosure_url": "https://www.icbccs.com.cn/gyrx/yljj/qynj/qynjxxpl/pages/1.html",
"list_url": "https://www.icbccs.com.cn/gyrx/yljj/qynj/qynjxxpl/pages/{n}.html",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.icbc(cs|ubs)\\.com\\.cn/upload/[0-9]+/[0-9]+/[^\"'\\s]+\\.pdf",
"example_links": [
"https://www.icbccs.com.cn/upload/2427/2023/20240905qynjglqk.pdf"
],
"notes": "因瑞信被UBS收购域名 icbccs→icbcubs 迁移为 Vue SPA旧直链多404。可靠来源为 Waybackhttps://web.archive.org/web/{ts}id_/{原PDF直链}快照止于2024Q2。新站列表接口 /gyrx-article/app/article/searchPage 需未公开 POST 参数。注意别混入工商银行(受托人)口径的披露表。\n\n[2026-07-07 复核] 确认 icbccs.com.cn 旧路径headless渲染下仍404(\"此次请求暂不能处理\"),域名迁移导致的失效是稳定状态非偶发。当前可靠来源仍只有 Wayback 快照(止于2024Q2)2024Q3至今(含2026Q1)在新站(icbcubs.com.cn) SPA 后台是否已发布不明——新站文章接口需要的POST参数未逆向出来这段历史目前对我们不可得。"
},
{
"slug": "m04",
"short": "国泰",
"name": "国泰基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.gtfund.com",
"access": "public",
"strategy": "pdf_index",
"item_type": "pdf",
"disclosure_url": "https://www.gtfund.com/Etrade/Report/nianjinreport/",
"list_url": "https://www.gtfund.com/Etrade/Report/nianjinreport/",
"page_param": null,
"file_url_pattern": "https?://st\\.gtfund\\.com/report/[0-9]{4}/[0-9]{2}/[^\"'\\s]+\\.pdf",
"example_links": [
"https://st.gtfund.com/report/2026/06/企业年金基金信息披露表_企业年金基金投资管理情况2026Q1.pdf"
],
"notes": "最省事的正面样本单个列表页即列出从2022至今十余期PDF 托管 st.gtfund.com/report/YYYY/MM/,文件名含中文与 YYYYQn需 URL 编码。"
},
{
"slug": "m05",
"short": "海富通",
"name": "海富通基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.hftfund.com",
"access": "public",
"strategy": "html_index",
"item_type": "html",
"disclosure_url": "https://www.hftfund.com/annuity/info/index.html",
"list_url": "https://www.hftfund.com/annuity/info/index{n}.html",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.hftfund\\.com/contents/[0-9]{4}/[0-9]+/[^\"'\\s]+\\.html",
"example_links": [
"https://www.hftfund.com/contents/2025/6/13-399abf20de9f46a59c180b96a9eb12f3.html"
],
"notes": "报告为 HTML 正文页无PDF附件存网页存档即可。列表分页 index.html/index2.html/index3.html列表 AJAX 渲染,静态只拿到部分历史,全量需 headless 枚举分页。detail URL 用随机哈希。\n\n[2026-07-07 全量验证] HTTP 静态分页拿到 2012A-2023Q2 共46期历史深度全名录最佳。⚠但2023Q3至今(含2026Q1最新)在静态HTML里不存在——该区间列表改为AJAX渲染HTTP拿不到需 crawl_headless.py 专门补最近这一段;即“旧历史全,新数据缺”,两种手段需配合使用才能补全全部期次。"
},
{
"slug": "m06",
"short": "华夏",
"name": "华夏基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.chinaamc.com.cn",
"access": "public",
"strategy": "detail_index",
"item_type": "pdf",
"disclosure_url": "https://www.chinaamc.com.cn/yanglaojijin/nianjin/nianjinyeji/index.shtml",
"list_url": "https://www.chinaamc.com.cn/yanglaojijin/nianjin/nianjinyeji/index_{n}.shtml",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.chinaamc\\.com/(upload/resources/file/[^\"'\\s]+\\.pdf|front/ui/contentcore/resource/download\\?ID=[0-9]+)",
"example_links": [
"https://www.chinaamc.com/upload/resources/file/2026/06/18/453207.pdf",
"https://www.chinaamc.com/front/ui/contentcore/resource/download?ID=433263"
],
"notes": "养老金频道→年金→年金业绩。列表每条→详情页 /c/YYYY-MM-DD/xxxxxx.shtml内含PDF直链或 contentcore/resource/download?ID= 下载接口。覆盖2017至今最新2026Q1。首页 index.shtml翻页 index_1.shtml…"
},
{
"slug": "m07",
"short": "嘉实",
"name": "嘉实基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.jsfund.cn",
"access": "public",
"strategy": "spa_api",
"item_type": "pdf",
"disclosure_url": "https://www.jsfund.cn/main/pensions/AnnuityBusiness/index.shtml",
"list_url": "https://www.jsfund.cn/main/pensions/AnnuityBusiness/index.shtml",
"page_param": null,
"file_url_pattern": "https?://www\\.jsfund\\.cn/ueditor/jsp/upload/file/[0-9]{8}/[0-9]+\\.pdf",
"api": {
"endpoint": "https://www.jsfund.cn/servlet/json?random={rand}",
"method": "POST",
"params": {
"funcNo": "741002",
"type": "2",
"article_id": "{id}",
"rows": "1"
},
"pdf_field": "content"
},
"example_links": [
"https://www.jsfund.cn/ueditor/jsp/upload/file/20260617/1781691526655083303.pdf"
],
"notes": "Thinkive 框架 SPA。列表详情页 /main/a/{YYYYMMDD}/{articleId}.shtml 正文异步加载POST /servlet/json funcNo=741002 取 content(内嵌PDF直链),无需登录/签名。PDF 路径 /ueditor/jsp/upload/file/{YYYYMMDD}/{雪花id}.pdf。\n\n[2026-07-07 已知缺陷] crawl.py 只做静态HTML抓取未真正调用 registry 里记录的 servlet/json(funcNo=741002) API故全量运行只抓到列表首屏SSR的最新1期(2026Q1)2025Q4及更早的历史期次未被抓取。这是 crawl.py 的功能缺口(spa_api策略未落地为真实API调用),不是网站不可得——需在 crawl.py 里为 spa_api 策略实现 POST /servlet/json 二次取件才能补全历史。"
},
{
"slug": "m08",
"short": "南方",
"name": "南方基金管理股份有限公司",
"category": "基金公司",
"homepage": "https://www.nffund.com",
"access": "public",
"strategy": "html_index",
"item_type": "html",
"disclosure_url": "https://www.nffund.com/main/newCCZX/qynj/xxpl/index.shtml",
"list_url": "https://www.nffund.com/main/newCCZX/qynj/xxpl/index_{n}.shtml",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.nffund\\.com/main/newCCZX/qynj/xxpl/[0-9]+\\.shtml",
"example_links": [
"https://www.nffund.com/main/newCCZX/qynj/xxpl/133350.shtml"
],
"notes": "持仓资讯→企业年金→信息披露。报告为 HTML 正文GBK/gb18030 编码,抓后 iconv 转 UTF-8。无PDF。nffund.com 与 southernfund.com 同主体,直连 nffund.com 抓。\n\n[2026-07-07 诊断] HTTP 0命中(原始HTML里根本没有报告文本纯AJAX注入)。headless 渲染后报告标题确实可见(2024Q3-2026Q1一路可读),但这些条目不是 <a href> 而是无链接的文本行/onclick故现有 headless 脚本(只扫<a>标签)仍抓不到文件直链。需针对该站定制:抓可见文本+配对同页的隐藏 data-id/onclick 参数,或抓包找底层 AJAX/JSON 接口。"
},
{
"slug": "m09",
"short": "易方达",
"name": "易方达基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.efunds.com.cn",
"access": "public",
"strategy": "detail_index",
"item_type": "pdf",
"disclosure_url": "https://www.efunds.com.cn/Mobile/lm/yljxxpl/qynjtzglqk/",
"list_url": "https://www.efunds.com.cn/Mobile/lm/yljxxpl/qynjtzglqk/",
"page_param": null,
"file_url_pattern": "https?://cdn\\.efunds\\.com\\.cn/owch/data/bulletin/[0-9]{8}/[^\"'\\s]+\\.pdf",
"example_links": [
"https://cdn.efunds.com.cn/owch/data/bulletin/20260618/易方达基金管理有限公司企业年金基金投资管理情况-2026年1季度.pdf"
],
"notes": "机构服务→养老金信息披露→企业年金投资管理情况报告。PDF 在 CDN cdn.efunds.com.cn/owch/data/bulletin/{发布日}/,命名固定「易方达基金管理有限公司企业年金基金投资管理情况-YYYY年N季度.pdf」。列表动态全量枚举用 headless 或按季推日期。PC 入口 /lm/jgfw/xxpl/。"
},
{
"slug": "m10",
"short": "银华",
"name": "银华基金管理股份有限公司",
"category": "基金公司",
"homepage": "https://www.yhfund.com.cn",
"access": "waf",
"strategy": "headless",
"item_type": "pdf",
"disclosure_url": "http://www.yhfund.com.cn/main/corporatePension/report/index.shtml",
"list_url": "http://www.yhfund.com.cn/main/corporatePension/report/index.shtml",
"page_param": null,
"file_url_pattern": "https?://www\\.yhfund\\.com\\.cn/upload/[0-9]{8}/[^\"'\\s]+\\.pdf",
"example_links": [
"https://www.yhfund.com.cn/upload/20240528/202405281716881071862.pdf"
],
"notes": "报告栏目 /main/corporatePension/report/。所有动态 .shtml 页对数据中心IP被WAF拦(837B「可疑访问」页),列表需 headless/住宅IP。但 /upload/YYYYMMDD/ 下的PDF不受WAF限拿到直链即可curl。文件名含随机时间戳不可猜须从列表页取。\n\n[2026-07-07 复核] 确认 headless(无stealth) 仍被WAF拦返回 `可疑访问行为被拒绝`(200状态码但内容为拦截页)。说明该WAF会指纹识别headless/自动化浏览器不是简单等待JS渲染能绕过的需要更强的反检测(stealth插件/真实指纹)或人工过一次拿到cookie复用。已知可行的是若能从其它渠道(搜索引擎缓存等)拿到具体PDF直链/upload/路径本身不拦,可以直接下载——但发现新链接这一步现阶段做不到。"
},
{
"slug": "m11",
"short": "招商",
"name": "招商基金管理有限公司",
"category": "基金公司",
"homepage": "https://www.cmfchina.com",
"access": "login",
"strategy": "login_blocked",
"item_type": "pdf",
"disclosure_url": "https://www.cmfchina.com/web/elderlyCare/enterprisePension/index.html",
"list_url": null,
"page_param": null,
"file_url_pattern": null,
"notes": "企业年金页有「信息披露」入口,但跳转客户门户 qy.cmfchina.com/#/pensionInfo未登录一律 redirect /login需 fundAccount+计划号+验证码换 token 才能下载季报。公开的只有「养老金产品公告」(分红/变更等),非季度管理报告。"
},
{
"slug": "m12",
"short": "华泰资产",
"name": "华泰资产管理有限公司",
"category": "保险资管",
"homepage": "https://www.htam.com.cn",
"access": "public",
"strategy": "detail_index",
"item_type": "pdf",
"disclosure_url": "https://www.htam.com.cn/product/notice",
"list_url": "https://www.htam.com.cn/product/notice",
"page_param": null,
"file_url_pattern": "https?://www\\.htam\\.com\\.cn/(File|Uploads/Picture)/[^\"'\\s]+\\.pdf",
"api": {
"endpoint": "https://www.htam.com.cn/api/v1/article/{id}",
"method": "GET",
"pdf_field": "content"
},
"example_links": [
"https://www.htam.com.cn/File/2026/6/22/202606221619312026年1季度企业年金基金投资管理情况.pdf"
],
"notes": "产品→公告。PDF 在 /File/YYYY/M/D/ 或 /Uploads/Picture/;详情走 /api/v1/article/{id} 返回含PDF链。文件名含中文季度标识。\n\n[2026-07-07 诊断] 与首次人工爬取(2026-07-06)相比官网疑似已改版headless 渲染「产品公告→养老金产品」页面正常200且标题正确但列表区显示「暂无数据」此前记录的 /File/...pdf 与 /api/v1/article/{id} 路径当前均取不到任何条目。需重新踩点新的筛选/接口方式,此为站点近期变动导致的临时失效,非永久不可得。"
},
{
"slug": "m13",
"short": "泰康资产",
"name": "泰康资产管理有限责任公司",
"category": "保险资管",
"homepage": "https://www.taikangasset.cn",
"access": "public",
"strategy": "detail_index",
"item_type": "docx",
"disclosure_url": "https://www.taikangasset.cn/comproduct/ylbusiness/enterprisefunds/enterpriseinfo/list_294_1.html",
"list_url": "https://www.taikangasset.cn/comproduct/ylbusiness/enterprisefunds/enterpriseinfo/list_294_{n}.html",
"page_param": "{n}",
"list_pages_known": 6,
"file_url_pattern": "https?://files\\.taikangasset\\.cn/(service/business/website/download/[0-9A-F]{32}|customer/websiteDownload\\.do\\?id=[0-9]+)",
"example_links": [
"https://files.taikangasset.cn/service/business/website/download/76CBEEB8DCFA465FB436646D8624585B",
"https://files.taikangasset.cn/customer/websiteDownload.do?id=154963234050491764318476102"
],
"notes": "养老金业务→企业年金→信息披露。⚠️报告是 .docx(Word) 不是PDF下载链接无扩展名服务端返回Word按 .docx 命名。列表倒序6页。新报告走 /service/business/website/download/{32位HEX},旧报告 /customer/websiteDownload.do?id={数字}。"
},
{
"slug": "m14",
"short": "长江养老",
"name": "长江养老保险股份有限公司",
"category": "养老保险公司",
"homepage": "https://www.cj-pension.com.cn",
"access": "public",
"strategy": "html_index",
"item_type": "html",
"disclosure_url": "https://www.cj-pension.com.cn/cjyl/Channel/3505179_1/qynjcpxxpl/jdxxpl",
"list_url": "https://www.cj-pension.com.cn/cjyl/Channel/3505179_{n}/qynjcpxxpl/jdxxpl",
"page_param": "{n}",
"list_pages_known": 11,
"file_url_pattern": "https?://www\\.cj-pension\\.com\\.cn/c/[0-9]{4}-[0-9]{2}-[0-9]{2}/[0-9]+\\.shtml",
"example_links": [
"https://www.cj-pension.com.cn/c/2026-06-18/1884003.shtml"
],
"notes": "信息披露→企业年金产品信息披露→季度/年度信息披露。每季两类:①企业年金集合计划管理情况 ②企业年金基金投资管理情况。报告为 HTML(.shtml) 正文无PDF。分11页。别混入 health.cpic.com.cn 的公司年度PDF。"
},
{
"slug": "m15",
"short": "平安养老",
"name": "平安养老保险股份有限公司",
"category": "养老保险公司",
"homepage": "https://yl.pingan.com",
"access": "public",
"strategy": "headless",
"item_type": "pdf",
"disclosure_url": "https://yl.pingan.com/branding/products",
"list_url": "https://yl.pingan.com/branding/products",
"page_param": null,
"file_url_pattern": "https?://yl\\.pingan\\.com/app_upload/file/[0-9]{8}/[^\"'\\s]+\\.pdf",
"example_links": [
"https://yl.pingan.com/app_upload/file/20251128/企业年金基金投资管理情况2025Q3_1470.pdf"
],
"notes": "产品及方案→年金→企业年金投资管理业绩披露。PDF 在 /app_upload/file/{YYYYMMDD}/,文件名含 YYYYQn。列表页动态渲染(WebFetch只得标题),全量枚举用 headlessPDF直链本身可匿名curl。yl.pingan.com=pension.pingan.com。"
},
{
"slug": "m16",
"short": "太平养老",
"name": "太平养老保险股份有限公司",
"category": "养老保险公司",
"homepage": "http://tppension.cntaiping.com",
"access": "waf",
"strategy": "headless",
"item_type": "pdf",
"disclosure_url": "https://tppension.cntaiping.com/njjylbz-jhxqynjjhglqk/",
"list_url": "https://tppension.cntaiping.com/njjylbz-jhxqynjjhglqk/",
"page_param": null,
"file_url_pattern": null,
"notes": "公开信息披露→企业年金信息披露(/info-qynjxxpl/);报告列表在 /njjylbz-jhxqynjjhglqk/(集合型企业年金计划管理情况,按季)。⚠️全域瑞数(botgate)动态JS反爬首请求 HTTP 412需浏览器执行 $_ts JS 挑战算 Cookie。必须 headless(playwright/真浏览器)。个案账户走登录门户 service.tppension.cntaiping.com。\n\n[2026-07-07 复核] 确认 headless(无stealth) 仍卡在瑞数botgate挑战HTTP 412body仅39字节。这是主动的JS人机验证(不只是异步渲染延迟),需要专门的挑战破解逻辑(执行页面下发的混淆JS算出正确Cookie)通用playwright耐心等待解决不了。当前仍无法从官网直接爬到任何报告直链。"
},
{
"slug": "m17",
"short": "新华养老",
"name": "新华养老保险股份有限公司",
"category": "养老保险公司",
"homepage": "https://www.newchinapension.com",
"access": "public",
"strategy": "detail_index",
"item_type": "pdf",
"disclosure_url": "https://www.newchinapension.com/xhylbx/_300468/312175/index.html",
"list_url": "https://www.newchinapension.com/xhylbx/_300468/312175/index_{n}.html",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.newchinapension\\.com/xhylbx/articleFileDir/[0-9]{4}-[0-9]{2}/[0-9]+/[^\"'\\s]+\\.pdf",
"example_links": [
"https://www.newchinapension.com/xhylbx/articleFileDir/2026-06/25/ccedf2a37bcf433f9d21a9e20eedcf0a/新华养老保险股份有限公司_企业年金基金投资管理情况2026Q1.pdf"
],
"notes": "公开信息披露→企业年金投管业务信息披露共29条最新2026Q1。PDF直链含中文文件名(需URL编码)命名「新华养老保险股份有限公司_企业年金基金投资管理情况YYYYQn.pdf」。另有「年度信息披露报告」为不同栏目。"
},
{
"slug": "m18",
"short": "国寿养老",
"name": "中国人寿养老保险股份有限公司",
"category": "养老保险公司",
"homepage": "https://www.clpc.com.cn",
"access": "public",
"strategy": "detail_index",
"item_type": "pdf",
"disclosure_url": "https://www.clpc.com.cn/chinalifepension/jgkh/qynj_yljcpxxpl/index.html",
"list_url": "https://www.clpc.com.cn/chinalifepension/jgkh/qynj_yljcpxxpl/index_{n}.html",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.clpc\\.com\\.cn/chinalifepension/articleFileDir/[0-9]{4}-[0-9]{2}/[0-9]+/[^\"'\\s]+\\.pdf",
"example_links": [
"https://www.clpc.com.cn/chinalifepension/articleFileDir/2026-06/23/543e6a924dbd470f86913e5d570d903d.pdf"
],
"notes": "机构客户→企业年金/养老金产品信息披露。每季两份:①企业年金基金投资管理情况 ②集合型企业年金计划管理情况。PDF 在 /articleFileDir/YYYY-MM/DD/{hash}.pdf。公司层面「信息披露」(/gkxxpl/)是另一独立栏目。"
},
{
"slug": "m19",
"short": "人保养老",
"name": "中国人民养老保险有限责任公司",
"category": "养老保险公司",
"homepage": "https://www.picc-pension.com.cn",
"access": "public",
"strategy": "html_index",
"item_type": "html",
"disclosure_url": "https://www.picc-pension.com.cn/html/category/1812/200019-1.htm",
"list_url": "https://www.picc-pension.com.cn/html/category/1812/200019-{n}.htm",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.picc-pension\\.com\\.cn/html/report/[0-9]{4}/[0-9]+-1\\.htm",
"example_links": [
"https://www.picc-pension.com.cn/html/report/2606/0007-1.htm"
],
"notes": "信息披露→企业年金信息披露2018Q32026Q1 共62条每季两类(投资管理情况/集合计划受托)。报告为内联 HTML 表格(<span id=ReportIDtext>),无附件。⚠️注意:仅 2018Q32021Q4 正文含完整数据表2022Q1 起官网正文区为空(只有标题+日期),近四年数据未公开。\n\n[2026-07-07 诊断] 双重问题:①技术层——该页文章列表由站点自有的 `/asset/dyn/a_article/script.js` 挂件异步注入headless 渲染10秒后 body 仍完全为空(0字节),该挂件在无头环境下未能执行/未能取到数据,尚未查明原因(可能校验referrer/viewport/需真实交互)导致列表拿不到是当前爬虫的真实缺口②数据层——即使解决①、能进入具体报告页此前已确认自2022Q1起正文区`<span id=ReportIDtext>`本身为空(仅标题+日期),即近四年官网自己就没有公开数据,这部分是官网侧的历史数据缺失,与技术能力无关,无法通过改进爬虫解决。"
},
{
"slug": "m20",
"short": "建信养老金",
"name": "建信养老金管理有限责任公司",
"category": "养老金管理公司",
"homepage": "http://www.ccbpension.com",
"access": "waf",
"strategy": "login_blocked",
"item_type": "pdf",
"disclosure_url": "http://www.ccbpension.com/xxpl_yyjnbg.html",
"list_url": null,
"page_param": null,
"file_url_pattern": null,
"notes": "Vue SPA信息披露走 /api/modules/disclosure/product/pageQuery整站WAF(「访问拦截」页)+ API返回加密密文({jsonData:base64},需 /api/crypto/getKey 解密),无头浏览器也难。且官网披露的是「养老金产品」层报告,非计划层企业年金季报——计划层季报不在官网公开(报送受托人/人社部)。第三方(企查查)缓存可见零星产品年报,非官网、非季度。"
},
{
"slug": "m21",
"short": "中金",
"name": "中国国际金融股份有限公司",
"category": "证券公司",
"homepage": "http://www.cicc.com",
"access": "waf",
"strategy": "headless",
"item_type": "pdf",
"disclosure_url": "http://www.cicc.com/portal/investor/info/fnancial_cn.xhtml?columnId=718",
"list_url": "http://www.cicc.com/portal/investor/info/fnancial_cn.xhtml?columnId=718",
"page_param": null,
"file_url_pattern": "https?://www\\.cicc\\.com/upload/file/[^\"'\\s]+\\.pdf",
"example_links": [
"https://www.cicc.com/upload/file/2025/10/27/a9a6eb17-36b4-49ba-ae3f-8ec0f9c86eb5.pdf"
],
"notes": "投资者关系→信息披露(columnId=718)。⚠️加速乐(jsl)反爬首请求521+JS挑战需两级cookie(__jsluid_s + __jsl_clearance_s)第二级为混淆VM挑战需 node/浏览器执行算出。用 headless 最稳。披露为「养老金产品管理情况报告」(产品层,中金作投管人)。\n\n[2026-07-07 复核] 确认 headless(无stealth) 仍返回 HTTP 521(加速乐jsl挑战页)。此前人工爬取是用 node 手写脚本模拟执行两级混淆VM挑战才拿到 __jsl_clearance_s cookie 换到真实PDF——这是一次性手工破解未固化进 crawl_headless.py 的通用逻辑,故本次自动化复测未能复现,仍需人工/定制脚本介入。"
},
{
"slug": "m22",
"short": "中信证券",
"name": "中信证券股份有限公司",
"category": "证券公司",
"homepage": "http://www.cs.ecitic.com",
"access": "public",
"strategy": "html_index",
"item_type": "html",
"disclosure_url": "http://www.cs.ecitic.com/newsite/ywzx/zcgl/qynj/xxdt/",
"list_url": "http://www.cs.ecitic.com/newsite/ywzx/zcgl/qynj/xxdt/index_{n}.html",
"page_param": "{n}",
"file_url_pattern": "https?://www\\.cs\\.ecitic\\.com/newsite/ywzx/zcgl/qynj/xxdt/[0-9]{6}/t[0-9]{8}_[0-9]+\\.html",
"example_links": [
"http://www.cs.ecitic.com/newsite/ywzx/zcgl/qynj/xxdt/202606/t20260618_1213353.html"
],
"notes": "业务专区→资产管理→企业年金→信息动态。《中信证券企业年金基金投资管理情况》按季,报告为 HTML 正文(无PDF附件)列表可追溯至2023Q4。www.cs.ecitic.com=www.citics.com。首页 xxdt/,翻页 index_1.html…"
}
]
}

View File

@ -0,0 +1,372 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
企业年金投资管理人 · 官网季度管理报告 全量爬取驱动
=================================================
读取 references/registry.json按每家机构的 strategy 抓取官网披露栏目下
**全部历史季度**企业年金基金投资管理情况报告落地到 --out 目录
并做真伪校验(魔数/大小)与清单(manifest)记录
HTTP 可搞定的策略pdf_index / detail_index / html_index / spa_api(部分)
需浏览器的策略headless本脚本跳过并提示改用 crawl_headless.py
不公开login_blocked跳过并说明
用法:
python3 crawl.py --list # 只列出名录与每家策略/入口
python3 crawl.py # 全量抓取所有可 HTTP 抓取的机构
python3 crawl.py --only m04,m09,m17 # 只抓指定机构(逗号分隔 slug 或简称)
python3 crawl.py --latest 3 # 每家只保留最新 N 期(默认0=全量)
python3 crawl.py --out /path/to/out # 指定输出目录(默认 ./annuity_reports)
python3 crawl.py --max-pages 40 # 列表分页最多翻多少页(默认30)
python3 crawl.py --include-waf # 也尝试对 waf 机构做HTTP直取(多半失败,仅留痕)
依赖: requests (pip install requests --break-system-packages)
"""
import argparse, json, os, re, sys, time, hashlib
from urllib.parse import urljoin, urlparse, unquote
try:
import requests
except ImportError:
sys.exit("需要 requests: pip install requests --break-system-packages")
HERE = os.path.dirname(os.path.abspath(__file__))
REGISTRY = os.path.join(os.path.dirname(HERE), "references", "registry.json")
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"
# 期次识别: 2026Q1 / 2026年1季度 / 2026年第一季度 / 2026-1季度
CN_NUM = {"": "1", "": "2", "": "3", "": "4"}
PERIOD_RES = [
re.compile(r"(20\d{2})\s*[Qq]\s*([1-4])"),
re.compile(r"(20\d{2})\D{0,4}?第?\s*([1-4一二三四])\s*季度"),
]
# 报告标题判别词(区分季报条目 vs 栏目里的其它通知)
TITLE_KW = re.compile(r"管理情况|投资管理|年金.{0,6}报告")
def parse_period(*texts):
for t in texts:
if not t:
continue
t = unquote(t)
for rx in PERIOD_RES:
m = rx.search(t)
if m:
q = CN_NUM.get(m.group(2), m.group(2))
return f"{m.group(1)}Q{q}"
# 年度报告
m = re.search(r"(20\d{2})\s*年度", t)
if m:
return f"{m.group(1)}A"
return None
def fetch(url, referer=None, binary=False, timeout=60, session=None):
s = session or requests
h = {"User-Agent": UA, "Accept-Language": "zh-CN,zh;q=0.9"}
if referer:
h["Referer"] = referer
r = s.get(url, headers=h, timeout=timeout, allow_redirects=True, verify=False)
r.raise_for_status()
if binary:
return r.content, r.headers.get("Content-Type", "")
# 文本:尝试正确解码(GBK 站点很多)
raw = r.content
ct = r.headers.get("Content-Type", "").lower()
enc = None
m = re.search(r"charset=([\w-]+)", ct)
if m:
enc = m.group(1)
if not enc:
head = raw[:2000].decode("ascii", "ignore").lower()
m = re.search(r'charset=["\']?([\w-]+)', head)
if m:
enc = m.group(1)
for e in ([enc] if enc else []) + ["utf-8", "gb18030"]:
try:
return raw.decode(e), r.headers.get("Content-Type", "")
except Exception:
continue
return raw.decode("utf-8", "ignore"), r.headers.get("Content-Type", "")
def abs_links(html, base):
"""返回 [(href_abs, anchor_text)]"""
out = []
for m in re.finditer(r'<a\b[^>]*?href=["\']([^"\']+)["\'][^>]*>(.*?)</a>', html, re.I | re.S):
href = m.group(1).strip()
if href.startswith(("javascript:", "#", "mailto:")):
continue
text = re.sub(r"<[^>]+>", "", m.group(2)).strip()
out.append((urljoin(base, href), text))
return out
def file_from_href(href, base, rx):
"""判断一个链接是否指向报告文件,返回可下载的绝对 URL 或 None。
处理 pdfjs 包装(viewer.html?file=xxx.pdf)相对路径以及 .pdf/.docx 后缀兜底"""
cands = [href]
m = re.search(r"[?&]file=([^&]+)", href)
if m:
cands.append(unquote(m.group(1)))
for c in cands:
cabs = urljoin(base, c)
if rx and (rx.search(c) or rx.search(cabs)):
return cabs
if c.split("?")[0].lower().endswith((".pdf", ".docx", ".doc", ".xlsx")):
return cabs
return None
def files_in_page(html, base, rx):
"""从一个页面里挖出所有报告文件直链(详情页二跳用)。"""
hits = set()
for href, _ in abs_links(html, base):
f = file_from_href(href, base, rx)
if f:
hits.add(f)
if rx:
for m in rx.finditer(html):
hits.add(urljoin(base, m.group(0)))
# 裸露的 .pdf/.docx 直链(在 JS/iframe src 里)——必须是真路径(以 / 或 http 开头)
# 避免把详情页里的“显示用中文文件名”误当成同目录相对链接
for m in re.finditer(r'["\']((?:https?:)?/[^"\'\s]+\.(?:pdf|docx))["\']', html, re.I):
hits.add(urljoin(base, m.group(1)))
return list(hits)
def list_pages(mgr, max_pages):
"""生成要翻的列表页 URL去重、保序
很多站点第 1 页是 index.html无编号 2 页起才是 index_1/index2
因此始终把 disclosure_url 作为首页再拼编号页"""
lu = mgr.get("list_url")
disc = mgr.get("disclosure_url")
if not lu:
return [disc] if disc else []
if "{n}" not in lu:
return [lu]
known = mgr.get("list_pages_known")
top = known if known else max_pages
seq = [disc] + [lu.replace("{n}", str(i)) for i in range(1, top + 1)]
seen, out = set(), []
for u in seq:
if u and u not in seen:
seen.add(u); out.append(u)
return out
def verify(path, item_type):
try:
sz = os.path.getsize(path)
except OSError:
return False, 0, "missing"
if sz < 512:
return False, sz, "too-small"
with open(path, "rb") as f:
head = f.read(8)
if item_type == "pdf":
return (head[:4] == b"%PDF", sz, "ok" if head[:4] == b"%PDF" else "not-pdf")
if item_type == "docx":
return (head[:2] == b"PK", sz, "ok" if head[:2] == b"PK" else "not-docx")
# html
with open(path, "rb") as f:
body = f.read(60000)
txt = body.decode("utf-8", "ignore") + body.decode("gb18030", "ignore")
ok = ("年金" in txt or "<title" in txt.lower()) and b"<html" in body.lower() or sz > 3000
return (bool(ok), sz, "ok" if ok else "suspect")
def safe_name(period, url, item_type, slug):
ext = {"pdf": ".pdf", "docx": ".docx", "html": ".html"}[item_type]
base = period or ("item_" + hashlib.md5(url.encode()).hexdigest()[:8])
return f"{slug}_{base}{ext}"
def crawl_manager(mgr, outdir, latest, max_pages, include_waf):
slug = mgr["slug"]
strat = mgr["strategy"]
item_type = mgr["item_type"]
res = {"slug": slug, "name": mgr["name"], "strategy": strat, "access": mgr["access"],
"disclosure_url": mgr["disclosure_url"], "files": [], "skipped": None}
if strat == "login_blocked":
res["skipped"] = "login/非公开:季报仅登录客户门户可见或未在官网公开"
return res
if strat in ("headless",) and not include_waf:
res["skipped"] = "需无头浏览器(WAF/SPA):改用 crawl_headless.py 或加 --include-waf 试探"
return res
d = os.path.join(outdir, slug)
os.makedirs(d, exist_ok=True)
sess = requests.Session()
pat = mgr.get("file_url_pattern")
rx = re.compile(pat) if pat else None
# 1) 逐列表页收集候选(锚点为主,带标题文本→可解析期次并过滤非报告条目)
found = {} # file_url -> period (pdf_index/html_index 的最终文件)
detail_q = {} # detail_url -> period (detail_index 需二跳)
pages = list_pages(mgr, max_pages) or [mgr["disclosure_url"]]
empty_streak = 0
for i, pg in enumerate(pages):
try:
html, _ = fetch(pg, referer=mgr["disclosure_url"], session=sess)
except Exception:
empty_streak += 1
if empty_streak >= 2:
break
continue
if i == 0:
with open(os.path.join(d, "_disclosure_page.html"), "w", encoding="utf-8", errors="ignore") as f:
f.write(html)
new = 0
for href, text in abs_links(html, pg):
p = parse_period(text, href)
titled = bool(text and TITLE_KW.search(text))
if not (p or titled):
continue # 不像报告条目,跳过(滤掉栏目里的其它通知/导航)
direct = file_from_href(href, pg, rx)
if direct: # 锚点本身就是文件(或 pdfjs 包装) → 直接收
if direct not in found:
found[direct] = p; new += 1
elif rx and rx.search(href): # 锚点是 html 报告页(html_index) → 收
if href not in found:
found[href] = p; new += 1
elif strat in ("detail_index", "spa_api"): # 是详情页 → 二跳
if href not in detail_q:
detail_q[href] = (p, text); new += 1
# pdf_index 兜底:直链可能不在 <a> 里onclick 等),用正则再捞一遍(要求文件名自带期次)
if strat == "pdf_index" and rx:
for m in rx.finditer(html):
u = urljoin(pg, m.group(0))
pu = parse_period(u)
if pu and u not in found:
found[u] = pu; new += 1
empty_streak = 0 if new else empty_streak + 1
if empty_streak >= 3:
break
time.sleep(0.3)
# 2) detail_index进详情页取文件直链
for href, (p, text) in detail_q.items():
try:
html, _ = fetch(href, referer=mgr["disclosure_url"], session=sess)
except Exception:
continue
for u in files_in_page(html, href, rx):
if u not in found:
found[u] = parse_period(u) or p
time.sleep(0.2)
if not found:
res["skipped"] = "HTTP 抓取 0 命中:列表多为 JS 渲染或被拦,请改用 crawl_headless.py"
return res
# 3) 排序、按 latest 截断(保留最新 N 个不同期次的全部报告)、下载
items = sorted(found.items(), key=lambda kv: (kv[1] or ""), reverse=True)
if latest and latest > 0:
periods_kept, kept = [], []
for u, p in items:
if p is None:
continue # 无法定期次的条目在 latest 模式下略过
if p not in periods_kept:
if len(periods_kept) >= latest:
break
periods_kept.append(p)
kept.append((u, p))
items = kept
seen_md5 = set()
for u, period in items:
try:
content, ct = fetch(u, referer=mgr["disclosure_url"], binary=True, session=sess)
except Exception as e:
res["files"].append({"url": u, "period": period, "status": f"download-fail:{e}", "local": None, "bytes": 0})
continue
h = hashlib.md5(content).hexdigest()
if h in seen_md5:
continue # 同一报告被多个 URL 变体重复收录,按内容去重
seen_md5.add(h)
# 若声明 pdf 却拿到 html 错误页
it = item_type
if item_type == "pdf" and content[:4] != b"%PDF" and content[:2] == b"PK":
it = "docx"
name = safe_name(period, u, it, slug)
path = os.path.join(d, name)
if os.path.exists(path): # 同期两类报告(投资管理/集合计划)等→加短哈希防覆盖
stem, ext = os.path.splitext(name)
path = os.path.join(d, f"{stem}_{hashlib.md5(u.encode()).hexdigest()[:6]}{ext}")
with open(path, "wb") as f:
f.write(content)
ok, sz, why = verify(path, it)
if not ok:
os.rename(path, path + ".suspect")
path = path + ".suspect"
res["files"].append({"url": u, "period": period, "status": why,
"local": os.path.relpath(path), "bytes": sz})
time.sleep(0.2)
return res
def main():
import urllib3
urllib3.disable_warnings()
ap = argparse.ArgumentParser()
ap.add_argument("--registry", default=REGISTRY)
ap.add_argument("--out", default="annuity_reports")
ap.add_argument("--only", default="")
ap.add_argument("--latest", type=int, default=0, help="每家保留最新N期0=全量")
ap.add_argument("--max-pages", type=int, default=30)
ap.add_argument("--include-waf", action="store_true")
ap.add_argument("--list", action="store_true")
args = ap.parse_args()
reg = json.load(open(args.registry, encoding="utf-8"))
managers = reg["managers"]
if args.list:
print(f"{'slug':5} {'简称':10} {'access':7} {'strategy':13} 披露入口")
for m in managers:
print(f"{m['slug']:5} {m['short']:10} {m['access']:7} {m['strategy']:13} {m['disclosure_url']}")
return
only = [x.strip() for x in args.only.split(",") if x.strip()]
if only:
managers = [m for m in managers if m["slug"] in only or m["short"] in only]
os.makedirs(args.out, exist_ok=True)
all_res = []
for m in managers:
print(f"\n=== {m['slug']} {m['short']} [{m['strategy']}/{m['access']}] ===", flush=True)
r = crawl_manager(m, args.out, args.latest, args.max_pages, args.include_waf)
if r["skipped"]:
print("" + r["skipped"])
else:
okn = sum(1 for f in r["files"] if f["status"] == "ok")
print(f"{okn}/{len(r['files'])} 个文件有效,落地 {os.path.join(args.out, m['slug'])}/")
for f in r["files"][:6]:
print(f" [{f['period']}] {f['status']} {f['bytes']}B {f['local']}")
all_res.append(r)
# manifest
mani = os.path.join(args.out, "manifest.json")
json.dump({"generated": time.strftime("%Y-%m-%d %H:%M"), "results": all_res},
open(mani, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
import csv
with open(os.path.join(args.out, "manifest.csv"), "w", newline="", encoding="utf-8-sig") as fp:
w = csv.writer(fp)
w.writerow(["slug", "机构", "策略", "access", "期次", "状态", "字节", "本地路径", "源URL"])
for r in all_res:
if r["skipped"]:
w.writerow([r["slug"], r["name"], r["strategy"], r["access"], "", "SKIP:" + r["skipped"], "", "", ""])
for f in r["files"]:
w.writerow([r["slug"], r["name"], r["strategy"], r["access"], f["period"], f["status"], f["bytes"], f["local"], f["url"]])
tot = sum(len(r["files"]) for r in all_res)
okt = sum(1 for r in all_res for f in r["files"] if f["status"] == "ok")
print(f"\n完成:{okt}/{tot} 个有效文件。清单:{mani} 及 manifest.csv")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,218 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
企业年金季报爬取 · 无头浏览器兜底
================================
用于 crawl.py 搞不定的机构列表页是 SPA(JS 渲染) 或整站有反爬 WAF/JS 挑战
strategy=headless / spa_api access=waf用真实 Chromium 渲染页面
自动通过瑞数/加速乐等 JS 挑战取到报告链接后用带 Cookie 的浏览器上下文下载
依赖:
pip install playwright --break-system-packages
python3 -m playwright install chromium
用法:
python3 crawl_headless.py --only m15,m21 # 指定机构
python3 crawl_headless.py # 默认抓所有 headless/spa_api/waf 机构
python3 crawl_headless.py --latest 3 --out DIR # 同 crawl.py 语义
python3 crawl_headless.py --only m21 --headful # 显示浏览器窗口(调试/过人机)
crawl.py 复用同一 registry.json 与期次识别/校验/命名逻辑
"""
import argparse, json, os, sys, time, hashlib, re, csv
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import crawl # 复用 parse_period / file_from_href / files_in_page / verify / safe_name / TITLE_KW / list_pages
from playwright.sync_api import sync_playwright
UA = crawl.UA
REGISTRY = crawl.REGISTRY
def harvest_links(page):
"""返回页面上所有 <a> 的 (href_abs, text)。"""
return page.eval_on_selector_all(
"a",
"els => els.map(a => [a.href, (a.innerText||a.textContent||'').trim()])",
)
def crawl_manager(ctx, mgr, outdir, latest, max_pages):
slug = mgr["slug"]
strat = mgr["strategy"]
item_type = mgr["item_type"]
rx = re.compile(mgr["file_url_pattern"]) if mgr.get("file_url_pattern") else None
res = {"slug": slug, "name": mgr["name"], "strategy": strat, "access": mgr["access"],
"disclosure_url": mgr["disclosure_url"], "files": [], "skipped": None}
if strat == "login_blocked":
res["skipped"] = "登录墙/非公开,浏览器也无法匿名取得"
return res
d = os.path.join(outdir, slug)
os.makedirs(d, exist_ok=True)
page = ctx.new_page()
found = {} # file_url -> period
detail_q = {}
pages = crawl.list_pages(mgr, max_pages) or [mgr["disclosure_url"]]
empty = 0
for i, pg in enumerate(pages):
try:
page.goto(pg, wait_until="networkidle", timeout=45000)
except Exception:
try:
page.goto(pg, wait_until="domcontentloaded", timeout=45000)
except Exception:
empty += 1
if empty >= 2:
break
continue
time.sleep(2.5) # 等 JS 挑战/异步渲染
for _ in range(4): # 滚动触发懒加载
page.mouse.wheel(0, 20000)
time.sleep(0.6)
if i == 0:
try:
open(os.path.join(d, "_disclosure_rendered.html"), "w", encoding="utf-8").write(page.content())
except Exception:
pass
new = 0
for href, text in harvest_links(page):
if not href:
continue
p = crawl.parse_period(text, href)
titled = bool(text and crawl.TITLE_KW.search(text))
if not (p or titled):
continue
direct = crawl.file_from_href(href, pg, rx)
if direct:
if direct not in found:
found[direct] = p; new += 1
elif strat in ("detail_index", "spa_api", "headless"):
if href not in detail_q:
detail_q[href] = (p, text); new += 1
empty = 0 if new else empty + 1
if empty >= 3:
break
# 详情页二跳(渲染后取文件链)
for href, (p, text) in list(detail_q.items()):
try:
page.goto(href, wait_until="networkidle", timeout=40000)
time.sleep(1.5)
except Exception:
continue
html = page.content()
for u in crawl.files_in_page(html, href, rx):
if u not in found:
found[u] = crawl.parse_period(u) or p
if not found:
res["skipped"] = "渲染后仍 0 命中:可能需登录、或分页/下载入口需交互(试 --headful 观察)"
page.close()
return res
items = sorted(found.items(), key=lambda kv: (kv[1] or ""), reverse=True)
if latest and latest > 0:
periods_kept, kept = [], []
for u, pp in items:
if pp is None:
continue
if pp not in periods_kept:
if len(periods_kept) >= latest:
break
periods_kept.append(pp)
kept.append((u, pp))
items = kept
seen_md5 = set()
for u, period in items:
try:
r = ctx.request.get(u, timeout=60000) # 带浏览器 Cookie 下载,过 WAF
content = r.body()
except Exception as e:
res["files"].append({"url": u, "period": period, "status": f"download-fail:{e}", "local": None, "bytes": 0})
continue
h = hashlib.md5(content).hexdigest()
if h in seen_md5:
continue
seen_md5.add(h)
it = item_type
if item_type == "pdf" and content[:4] != b"%PDF" and content[:2] == b"PK":
it = "docx"
name = crawl.safe_name(period, u, it, slug)
path = os.path.join(d, name)
if os.path.exists(path):
stem, ext = os.path.splitext(name)
path = os.path.join(d, f"{stem}_{hashlib.md5(u.encode()).hexdigest()[:6]}{ext}")
open(path, "wb").write(content)
ok, sz, why = crawl.verify(path, it)
if not ok:
os.rename(path, path + ".suspect"); path += ".suspect"
res["files"].append({"url": u, "period": period, "status": why, "local": os.path.relpath(path), "bytes": sz})
time.sleep(0.2)
page.close()
return res
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--registry", default=REGISTRY)
ap.add_argument("--out", default="annuity_reports")
ap.add_argument("--only", default="")
ap.add_argument("--latest", type=int, default=0)
ap.add_argument("--max-pages", type=int, default=30)
ap.add_argument("--headful", action="store_true")
args = ap.parse_args()
reg = json.load(open(args.registry, encoding="utf-8"))
managers = reg["managers"]
only = [x.strip() for x in args.only.split(",") if x.strip()]
if only:
managers = [m for m in managers if m["slug"] in only or m["short"] in only]
else: # 默认:需浏览器的那批
managers = [m for m in managers if m["strategy"] in ("headless", "spa_api") or m["access"] == "waf"]
managers = [m for m in managers if m["strategy"] != "login_blocked"]
os.makedirs(args.out, exist_ok=True)
all_res = []
with sync_playwright() as pw:
browser = pw.chromium.launch(headless=not args.headful)
ctx = browser.new_context(user_agent=UA, locale="zh-CN", ignore_https_errors=True,
extra_http_headers={"Accept-Language": "zh-CN,zh;q=0.9"})
for m in managers:
print(f"\n=== {m['slug']} {m['short']} [{m['strategy']}/{m['access']}] ===", flush=True)
try:
r = crawl_manager(ctx, m, args.out, args.latest, args.max_pages)
except Exception as e:
r = {"slug": m["slug"], "name": m["name"], "strategy": m["strategy"], "access": m["access"],
"disclosure_url": m["disclosure_url"], "files": [], "skipped": f"异常:{e}"}
if r["skipped"]:
print("" + r["skipped"])
else:
okn = sum(1 for f in r["files"] if f["status"] == "ok")
print(f"{okn}/{len(r['files'])} 个有效,落地 {os.path.join(args.out, m['slug'])}/")
for f in r["files"][:6]:
print(f" [{f['period']}] {f['status']} {f['bytes']}B {f['local']}")
all_res.append(r)
browser.close()
mani = os.path.join(args.out, "manifest_headless.json")
json.dump({"generated": time.strftime("%Y-%m-%d %H:%M"), "results": all_res},
open(mani, "w", encoding="utf-8"), ensure_ascii=False, indent=2)
with open(os.path.join(args.out, "manifest_headless.csv"), "w", newline="", encoding="utf-8-sig") as fp:
w = csv.writer(fp)
w.writerow(["slug", "机构", "策略", "access", "期次", "状态", "字节", "本地路径", "源URL"])
for r in all_res:
if r["skipped"]:
w.writerow([r["slug"], r["name"], r["strategy"], r["access"], "", "SKIP:" + r["skipped"], "", "", ""])
for f in r["files"]:
w.writerow([r["slug"], r["name"], r["strategy"], r["access"], f["period"], f["status"], f["bytes"], f["local"], f["url"]])
okt = sum(1 for r in all_res for f in r["files"] if f["status"] == "ok")
print(f"\n完成:{okt} 个有效文件。清单:{mani}")
if __name__ == "__main__":
main()

192
asset-monitor/SKILL.md Normal file
View File

@ -0,0 +1,192 @@
---
name: asset-monitor
description: 资产异常波动日频监控与报告生成。当用户需要对股票、指数、商品等资产进行日频异常波动监控检测偏离历史均值超过2倍标准差的异常并生成Excel报告时使用此技能。
---
# 资产异常波动日频监控
## 触发条件
当用户需要:
- 监控多资产类别的日频异常波动
- 检测偏离历史均值超过2倍标准差的资产
- 生成异常波动报告Excel/Markdown
- 批量分析申万三级行业、A股指数、商品期货、全球指数等
## 依赖
本 skill 依赖 windpy-sdk 获取数据。使用时需要:
1. Wind 金融终端已启动
2. 参考 windpy-sdk skill 了解数据获取方法
## 监控脚本
使用 `scripts/monitor.py` 进行监控:
```bash
# 基础监控
python scripts/monitor.py
# 带报告输出
python scripts/monitor.py --notify
# 自定义参数
python scripts/monitor.py --threshold 2.5 --min-days 60
```
## 监控资产范围
| 资产类别 | 数量 | 说明 |
|---------|------|------|
| 申万三级行业 | 259个 | 全量三级行业指数 |
| A股主要指数 | 9个 | 沪深300、中证500等 |
| 中债指数 | 5个 | 中债总指数、国债指数等 |
| 主流ETF | 8个 | 沪深300ETF、创业板ETF等 |
| 商品期货 | 8个 | 黄金、白银、铜、原油等 |
| 全球指数 | 9个 | 标普500、纳指、道指等 |
**总计**: 298个资产
## 核心监控逻辑
### Z-Score 异常检测
```python
# 计算Z值
z_score = (今日涨跌幅 - 历史均值) / 历史标准差
# 异常判定
if abs(z_score) > 2.0:
标记为异常
```
### 筛选条件
- 历史数据 > 30个交易日
- 历史标准差 > 0
- 按 |Z| 绝对值降序排列
## 输出结果
### Excel 报告
| 字段 | 说明 |
|------|------|
| category | 资产类别 |
| code | 资产代码 |
| name | 资产名称 |
| today_return | 今日涨跌幅(%) |
| z_score | Z值 |
| direction | 大涨/大跌 |
### 监控输出示例
```
================================================================================
📊 资产异常波动监控
时间: 2026-02-09 06:22
Z值阈值: 2.0
================================================================================
[监控] 申万三级行业 (259个)
共 259 个资产
⚠️ 印染(申万): +5.30% (Z=+3.33)
⚠️ 纺织化学制品(申万): +5.35% (Z=+3.22)
发现 5 个异常
[监控] 商品期货 (8个)
⚠️ 沪银近月: -14.02% (Z=-4.39)
发现 1 个异常
[监控] 全球指数 (9个)
⚠️ 道琼斯: +2.47% (Z=+2.34)
⚠️ VIX波动率: -18.42% (Z=-2.26)
发现 2 个异常
================================================================================
✅ 监控完成,共发现 8 个异常
================================================================================
```
## 脚本使用方法
详见 `scripts/monitor.py` 代码注释。
### 命令行参数
```bash
python scripts/monitor.py --help
Options:
--threshold FLOAT Z-Score阈值默认2.0
--min-days INT 最小交易日默认30
--output DIR 输出目录默认output
--notify 打印文本报告
```
### Python API
```python
# 直接导入脚本中的函数使用
import sys
sys.path.insert(0, 'scripts')
from monitor import run_monitoring, generate_excel_report
# 运行监控需先连接Wind
from WindPy import w
w.start()
anomalies = run_monitoring(threshold_z=2.0)
w.stop()
# 生成报告
excel_path = generate_excel_report(anomalies)
```
## 定时任务设置
```bash
# crontab -e
# 每日15:30运行
30 15 * * * cd /path/to/skill && python scripts/monitor.py --notify
```
## 配置文件
配置示例见 `references/monitor-config-example.json`
```json
{
"monitor": {
"threshold_z": 2.0,
"min_trading_days": 30
},
"assets": {
"sw3_industry": {"enabled": true},
"ashare_index": {"enabled": true},
"commodity": {"enabled": true}
}
}
```
## 故障排查
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| Wind连接失败 | Wind终端未启动 | 启动Wind终端 |
| 无数据返回 | 无数据权限 | 联系Wind开通权限 |
| 报告为空 | 今日无异常 | 正常现象 |
## 与其他 Skill 的关系
```
asset-monitor (监控逻辑 + 报告生成)
↓ 使用 WindPy 获取数据
WindPy SDK (Wind 金融终端 API)
```
**注意**: asset-monitor 直接使用 WindPy但字段和板块代码可参考 windpy-sdk skill 的文档。
## 参考
- windpy-sdk skill - WindPy 函数参考和字段速查
- `references/monitor-config-example.json` - 配置示例

View File

@ -0,0 +1,37 @@
{
"monitor": {
"threshold_z": 2.0,
"min_trading_days": 30,
"lookback_period": "-252TD"
},
"assets": {
"sw3_industry": {
"enabled": true,
"description": "申万三级行业(259个全量)"
},
"ashare_index": {
"enabled": true,
"description": "A股主要指数(9个)"
},
"bond_index": {
"enabled": true,
"description": "中债指数(5个)"
},
"etf": {
"enabled": true,
"description": "主流ETF(8个)"
},
"commodity": {
"enabled": true,
"description": "商品期货(8个主要品种)"
},
"global_index": {
"enabled": true,
"description": "全球指数(9个主要指数)"
}
},
"notification": {
"excel": true,
"markdown": true
}
}

View File

@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
资产异常波动日频监控脚本
使用方法:
python scripts/monitor.py
python scripts/monitor.py --notify
"""
from WindPy import w
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import os
import sys
# 监控资产配置
ASSET_CONFIG = {
"sw3_industry": {
"name": "申万三级行业",
"type": "sector",
"sectorid": "a39901011i000000", # 259个
},
"ashare_index": {
"name": "A股主要指数",
"type": "direct",
"codes": [
"000300.SH", "000905.SH", "000016.SH", "000852.SH",
"000001.SH", "399001.SZ", "399006.SZ", "000688.SH", "883985.WI"
],
},
"bond_index": {
"name": "中债指数",
"type": "direct",
"codes": [
"CBA00101.CS", "CBA00301.CS", "CBA00401.CS",
"CBA00501.CS", "CBA00601.CS"
],
},
"etf": {
"name": "主流ETF",
"type": "direct",
"codes": [
"510300.SH", "510500.SH", "510050.SH", "159915.SZ",
"588000.SH", "512480.SH", "515030.SH", "512760.SH"
],
},
"commodity": {
"name": "商品期货",
"type": "direct",
"codes": [
"AU00.SHF", "AG00.SHF", "CU00.SHF", "AL00.SHF",
"ZN00.SHF", "RB00.SHF", "SC00.INE", "TA00.CZC"
],
},
"global_index": {
"name": "全球指数",
"type": "direct",
"codes": [
"SPX.GI", "IXIC.GI", "DJI.GI", "VIX.GI",
"HSI.HI", "N225.GI", "KS11.GI", "GDAXI.GI", "FTSE.GI"
],
},
}
def analyze_asset(code, name, category, threshold_z=2.0, min_days=30):
"""分析单个资产的波动"""
try:
one_year_ago = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
today_str = datetime.now().strftime('%Y%m%d')
hist = w.wsd(code, "pct_chg", one_year_ago, today_str, "", usedf=True)
if hist[0] == 0 and len(hist[1]) > min_days:
returns = hist[1]['PCT_CHG'].dropna()
if len(returns) > min_days:
mean_ret = returns.mean()
std_ret = returns.std()
today_ret = returns.iloc[-1] if len(returns) > 0 else None
if today_ret is not None and std_ret > 0:
z_score = (today_ret - mean_ret) / std_ret
if abs(z_score) > threshold_z:
return {
'category': category,
'code': code,
'name': name,
'today_return': float(today_ret),
'z_score': float(z_score),
'direction': '大涨' if z_score > 0 else '大跌'
}
except Exception:
pass
return None
def monitor_sector(config, threshold_z=2.0):
"""监控板块类资产"""
print(f"\n[监控] {config['name']}")
today_str = datetime.now().strftime('%Y%m%d')
result = w.wset("sectorconstituent", f"date={today_str};sectorid={config['sectorid']}")
if result.ErrorCode != 0 or len(result.Data) < 2:
print(f" ⚠️ 未获取到数据")
return []
codes = result.Data[1]
names = result.Data[2]
print(f"{len(codes)} 个资产")
anomalies = []
for code, name in zip(codes, names):
result = analyze_asset(code, name, config['name'], threshold_z)
if result:
anomalies.append(result)
print(f" ⚠️ {name}: {result['today_return']:+.2f}% (Z={result['z_score']:+.2f})")
print(f" 发现 {len(anomalies)} 个异常")
return anomalies
def monitor_direct(config, threshold_z=2.0):
"""监控直接代码类资产"""
print(f"\n[监控] {config['name']} ({len(config['codes'])}个)")
# 获取名称
try:
result = w.wss(','.join(config['codes']), "sec_name", "", usedf=True)
name_map = dict(zip(result[1].index, result[1]['SEC_NAME'])) if result[0] == 0 else {}
except:
name_map = {code: code for code in config['codes']}
anomalies = []
for code in config['codes']:
name = name_map.get(code, code)
result = analyze_asset(code, name, config['name'], threshold_z)
if result:
anomalies.append(result)
print(f" ⚠️ {name}: {result['today_return']:+.2f}% (Z={result['z_score']:+.2f})")
print(f" 发现 {len(anomalies)} 个异常")
return anomalies
def run_monitoring(threshold_z=2.0):
"""运行完整监控"""
today = datetime.now()
print("="*70)
print(f"📊 资产异常波动监控")
print(f"时间: {today.strftime('%Y-%m-%d %H:%M')}")
print(f"Z值阈值: {threshold_z}")
print("="*70)
all_anomalies = []
for key, config in ASSET_CONFIG.items():
try:
if config['type'] == 'sector':
anomalies = monitor_sector(config, threshold_z)
else:
anomalies = monitor_direct(config, threshold_z)
all_anomalies.extend(anomalies)
except Exception as e:
print(f"{config['name']} 监控失败: {e}")
print(f"\n{'='*70}")
print(f"✅ 监控完成,共发现 {len(all_anomalies)} 个异常")
print(f"{'='*70}\n")
return all_anomalies
def generate_excel_report(anomalies, output_dir="output"):
"""生成 Excel 报告"""
if not anomalies:
return None
os.makedirs(output_dir, exist_ok=True)
df = pd.DataFrame(anomalies)
df = df.sort_values('z_score', key=abs, ascending=False)
today_str = datetime.now().strftime('%Y%m%d')
excel_path = os.path.join(output_dir, f"asset_anomaly_report_{today_str}.xlsx")
df.to_excel(excel_path, index=False, sheet_name='异常波动资产')
print(f"✅ Excel 报告: {excel_path}")
return excel_path
def generate_text_report(anomalies):
"""生成文本报告"""
if not anomalies:
return "📊 资产异常监控\n\n✅ 今日无异常资产。"
lines = [
"📊 资产异常波动报告",
f"报告时间: {datetime.now().strftime('%Y-%m-%d')}",
"",
f"共发现 {len(anomalies)} 个异常资产:",
"",
]
for i, item in enumerate(anomalies, 1):
emoji = "🚀" if item['z_score'] > 0 else "📉"
lines.append(
f"{i}. {emoji} {item['name']} ({item['category']})\n"
f" 涨跌幅: {item['today_return']:+.2f}% | Z值: {item['z_score']:+.2f}"
)
return '\n'.join(lines)
def main():
import argparse
parser = argparse.ArgumentParser(description='资产异常波动监控')
parser.add_argument('--threshold', type=float, default=2.0, help='Z-Score阈值')
parser.add_argument('--output', type=str, default='output', help='输出目录')
parser.add_argument('--notify', action='store_true', help='打印报告')
args = parser.parse_args()
# 连接 Wind
print("正在连接 Wind...")
w.start()
print("✅ Wind 连接成功\n")
try:
# 运行监控
anomalies = run_monitoring(threshold_z=args.threshold)
# 生成报告
if anomalies:
excel_path = generate_excel_report(anomalies, args.output)
if args.notify:
print("\n" + generate_text_report(anomalies))
else:
print("✅ 今日无异常资产")
finally:
w.stop()
print("\nWind 连接已断开")
if __name__ == "__main__":
main()

489
docx-cn/SKILL.md Normal file
View File

@ -0,0 +1,489 @@
---
name: docx-cn
description: "基于 docx-js (Node.js) 创建和编辑符合 GB/T 9704-2012 中国公文格式标准的 Word 文档。当用户需要生成公文、研究报告、内部文件等中文 Word 文档时使用。触发词公文、Word、docx、报告排版、格式规范、GB/T 9704。"
---
# 中文公文 Word 文档生成docx-js
## 概述
基于官方 docx skill 微调,专为中文公文 / 金融研究报告场景优化。
使用 **docx-js**Node.js生成文档配合官方 skill 的 unpack/pack/validate 工具链编辑现有文档。
## 核心工具链
| 任务 | 方法 |
|------|------|
| **新建文档** | `docx-js` — 本文件中的模板 |
| **编辑现有文档** | unpack XML → 编辑 → repack见「编辑现有文档」节 |
| **验证** | `python .claude/skills/docx/scripts/office/validate.py output.docx` |
| **读取内容** | `pandoc document.docx -o output.md` |
---
## GB/T 9704-2012 公文格式常量
### 单位换算速查
```
1 inch = 1440 DXA = 72 pt = 25.4 mm
1 mm = 56.693 DXA
1 pt = 20 DXAspacing/行距用)
font size 单位 = half-point半磅如 14pt = 28
```
### 页面设置A4
```javascript
const PAGE = {
WIDTH: 11906, // 210mm
HEIGHT: 16838, // 297mm
MARGIN_TOP: 2098, // 37mm
MARGIN_BOTTOM: 1985, // 35mm
MARGIN_LEFT: 1588, // 28mm
MARGIN_RIGHT: 1474, // 26mm
CONTENT_WIDTH: 8844, // 210mm - 28mm - 26mm
};
```
### 字号对照表
| 中文字号 | pt | half-point (docx-js size) | 用途 |
|----------|-----|---------------------------|------|
| 二号 | 22 | 44 | 大标题 |
| 三号 | 16 | 32 | 一级标题 |
| 四号 | 14 | 28 | 正文 / 二级标题 |
| 小四 | 12 | 24 | 表格内容 |
| 五号 | 10.5 | 21 | 页脚、注释 |
### 字体规范
```javascript
// 中文字体必须同时设置 eastAsia否则 Word 渲染时可能回退到宋体
const FONT = {
HEITI: { ascii: "SimHei", eastAsia: "黑体", hAnsi: "SimHei" }, // 标题
FANGSONG: { ascii: "FangSong", eastAsia: "仿宋", hAnsi: "FangSong" }, // 正文
KAITI: { ascii: "KaiTi", eastAsia: "楷体", hAnsi: "KaiTi" }, // 二级标题
SONGTI: { ascii: "SimSun", eastAsia: "宋体", hAnsi: "SimSun" }, // 备用
};
```
### 行距与段落
```javascript
const SPACING = {
// 行距twips = pt × 20lineRule: "exact" → 固定值
BODY: { line: 560, lineRule: "exact" }, // 28pt 固定行距
HEADING1: { line: 640, lineRule: "exact" }, // 32pt
HEADING2: { line: 560, lineRule: "exact" }, // 28pt
TABLE: { line: 440, lineRule: "exact" }, // 22pt
// 段前段后twips
H1_BEFORE: 240, H1_AFTER: 120, // 12pt / 6pt
H2_BEFORE: 120, H2_AFTER: 60, // 6pt / 3pt
};
const INDENT = {
FIRST_LINE: 560, // 首行缩进 2 字符 ≈ 28pt = 560 DXA
};
```
---
## 创建新文档
### 安装
```bash
npm install docx # 项目内安装即可,无需全局
```
### 完整模板:公文报告(无封面)
```javascript
const fs = require("fs");
const {
Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell,
Header, Footer, AlignmentType, HeadingLevel, BorderStyle, WidthType,
ShadingType, PageNumber, PageBreak, LevelFormat, TabStopType, TabStopPosition,
} = require("docx");
// ─── GB/T 9704-2012 格式常量 ─────────────────────────────
const PAGE = {
WIDTH: 11906, HEIGHT: 16838,
MARGIN_TOP: 2098, MARGIN_BOTTOM: 1985,
MARGIN_LEFT: 1588, MARGIN_RIGHT: 1474,
CONTENT_WIDTH: 8844,
};
const FONT = {
HEITI: { ascii: "SimHei", eastAsia: "黑体", hAnsi: "SimHei" },
FANGSONG: { ascii: "FangSong", eastAsia: "仿宋", hAnsi: "FangSong" },
KAITI: { ascii: "KaiTi", eastAsia: "楷体", hAnsi: "KaiTi" },
};
// ─── 样式定义 ─────────────────────────────────────────────
const styles = {
default: {
document: {
run: { font: FONT.FANGSONG, size: 28 }, // 默认:仿宋四号
paragraph: {
spacing: { line: 560, lineRule: "exact" },
},
},
},
paragraphStyles: [
{
id: "Heading1", name: "Heading 1",
basedOn: "Normal", next: "Normal", quickFormat: true,
run: { font: FONT.HEITI, size: 32, bold: true }, // 黑体三号
paragraph: {
spacing: { line: 640, lineRule: "exact", before: 240, after: 120 },
outlineLevel: 0,
},
},
{
id: "Heading2", name: "Heading 2",
basedOn: "Normal", next: "Normal", quickFormat: true,
run: { font: FONT.KAITI, size: 28, bold: true }, // 楷体四号加粗
paragraph: {
spacing: { line: 560, lineRule: "exact", before: 120, after: 60 },
outlineLevel: 1,
},
},
],
};
// ─── 列表定义 ─────────────────────────────────────────────
const numbering = {
config: [
{
reference: "bullets",
levels: [{
level: 0, format: LevelFormat.BULLET, text: "\u2022",
alignment: AlignmentType.LEFT,
style: {
run: { font: FONT.FANGSONG, size: 28 },
paragraph: { indent: { left: 1120, hanging: 560 } }, // 缩进 2 字符 + 悬挂
},
}],
},
],
};
// ─── 辅助函数 ─────────────────────────────────────────────
/** 创建正文段落(仿宋四号,首行缩进 2 字符) */
function bodyParagraph(text, options = {}) {
const { bold = false, alignment = AlignmentType.JUSTIFIED } = options;
return new Paragraph({
alignment,
indent: { firstLine: 560 },
spacing: { line: 560, lineRule: "exact" },
children: [new TextRun({ text, font: FONT.FANGSONG, size: 28, bold })],
});
}
/** 创建一级标题(黑体三号) */
function heading1(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_1,
children: [new TextRun({ text, font: FONT.HEITI, size: 32, bold: true })],
});
}
/** 创建二级标题(楷体四号加粗) */
function heading2(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_2,
children: [new TextRun({ text, font: FONT.KAITI, size: 28, bold: true })],
});
}
/** 创建三线表 */
function threeLineTable(headers, rows) {
const noBorder = { style: BorderStyle.NONE, size: 0 };
const thickBorder = { style: BorderStyle.SINGLE, size: 12, color: "000000" }; // 1.5pt
const thinBorder = { style: BorderStyle.SINGLE, size: 6, color: "000000" }; // 0.75pt
// 表头行边框:顶粗 + 底细
const headerBorders = {
top: thickBorder, bottom: thinBorder,
left: noBorder, right: noBorder,
};
// 数据行边框:无顶底左右(靠 insideH 提供行间线)
const dataBorders = {
top: noBorder, bottom: noBorder,
left: noBorder, right: noBorder,
};
// 最后一行底部:粗线
const lastRowBorders = {
top: noBorder, bottom: thickBorder,
left: noBorder, right: noBorder,
};
const colWidths = headers.map(() => Math.floor(PAGE.CONTENT_WIDTH / headers.length));
function makeCell(text, isHeader, borders) {
return new TableCell({
borders,
width: { size: colWidths[0], type: WidthType.DXA },
shading: isHeader
? { fill: "F2F2F2", type: ShadingType.CLEAR }
: undefined,
margins: { top: 40, bottom: 40, left: 80, right: 80 },
children: [new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { line: 440, lineRule: "exact" },
children: [new TextRun({
text,
font: isHeader ? FONT.HEITI : FONT.FANGSONG,
size: 24, // 小四 12pt
bold: isHeader,
})],
})],
});
}
const tableRows = [
// 表头
new TableRow({
children: headers.map(h => makeCell(h, true, headerBorders)),
}),
// 数据行
...rows.map((row, rowIdx) =>
new TableRow({
children: row.map(cell =>
makeCell(cell, false, rowIdx === rows.length - 1 ? lastRowBorders : dataBorders)
),
})
),
];
return new Table({
width: { size: PAGE.CONTENT_WIDTH, type: WidthType.DXA },
columnWidths: colWidths,
rows: tableRows,
});
}
/** 报告标题区(居中,无封面) */
function titleBlock(title, subtitle, date, institution) {
return [
new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { line: 640, lineRule: "exact", after: 0 },
children: [new TextRun({ text: title, font: FONT.HEITI, size: 32, bold: true })],
}),
subtitle ? new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { line: 640, lineRule: "exact", after: 120 },
children: [new TextRun({ text: subtitle, font: FONT.HEITI, size: 32 })],
}) : null,
new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { line: 560, lineRule: "exact", after: 0 },
children: [new TextRun({ text: date, font: FONT.FANGSONG, size: 28 })],
}),
institution ? new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { line: 560, lineRule: "exact", after: 240 },
children: [new TextRun({ text: institution, font: FONT.FANGSONG, size: 28 })],
}) : null,
].filter(Boolean);
}
// ─── 使用示例:构建文档 ──────────────────────────────────
const doc = new Document({
styles,
numbering,
sections: [{
properties: {
page: {
size: { width: PAGE.WIDTH, height: PAGE.HEIGHT },
margin: {
top: PAGE.MARGIN_TOP, bottom: PAGE.MARGIN_BOTTOM,
left: PAGE.MARGIN_LEFT, right: PAGE.MARGIN_RIGHT,
},
},
},
headers: {
default: new Header({
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({
text: "内部资料 注意保密",
font: FONT.FANGSONG, size: 21, color: "808080",
})],
})],
}),
},
footers: {
default: new Footer({
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({ text: "— ", font: FONT.FANGSONG, size: 21 }),
new TextRun({ children: [PageNumber.CURRENT], font: FONT.FANGSONG, size: 21 }),
new TextRun({ text: " —", font: FONT.FANGSONG, size: 21 }),
],
})],
}),
},
children: [
// 标题区
...titleBlock(
"zn04鲁肆号年金组合",
"权益投资分析与策略展望报告",
"估值日2026年3月10日",
"工银瑞信基金管理有限公司"
),
// 正文
heading1("一、投资策略与研判框架"),
bodyParagraph("本组合采用"多维度全局配置"策略横跨A股和港股市场..."),
heading2("1.1 八大核心研判及验证"),
threeLineTable(
["#", "核心判断", "验证状态", "关键证据"],
[
["1", "恒生科技有超额收益", "1月验证后逆转", "1月+4.2%→3月-12.45%"],
["2", "AI国内参与性价比不高", "已验证", "A股AI概念高位回落"],
]
),
heading1("二、组合现状概览"),
bodyParagraph("截至估值日组合单位净值1.4430权益仓位合计占比约23%。"),
],
}],
});
// 导出
Packer.toBuffer(doc).then(buffer => {
fs.writeFileSync("output.docx", buffer);
console.log("✅ 已生成: output.docx");
});
```
### 运行
```bash
node generate_report.js
# 验证(可选,使用官方 docx skill 的验证工具)
python .claude/skills/docx/scripts/office/validate.py output.docx
```
---
## 辅助函数速查
| 函数 | 用途 | 格式 |
|------|------|------|
| `titleBlock(title, subtitle, date, institution)` | 报告标题区(无封面) | 黑体三号居中 |
| `heading1(text)` | 一级标题 | 黑体三号 16pt |
| `heading2(text)` | 二级标题 | 楷体四号加粗 |
| `bodyParagraph(text, {bold, alignment})` | 正文段落 | 仿宋四号首行缩进28pt行距 |
| `threeLineTable(headers, rows)` | 三线表 | 顶底1.5pt内部0.75pt,无竖线 |
---
## 编辑现有文档
当需要修改已有 .docx 文件时,使用官方 docx skill 的 unpack/pack 工作流:
```bash
# 1. 解包
python .claude/skills/docx/scripts/office/unpack.py document.docx unpacked/
# 2. 编辑 unpacked/word/document.xml直接用 Edit 工具修改 XML
# 3. 重新打包
python .claude/skills/docx/scripts/office/pack.py unpacked/ output.docx --original document.docx
```
### 公文格式关键 XML 片段
#### 页面设置(插入 `<w:sectPr>` 中)
```xml
<w:pgSz w:w="11906" w:h="16838"/>
<w:pgMar w:top="2098" w:right="1474" w:bottom="1985" w:left="1588"
w:header="851" w:footer="851" w:gutter="0"/>
```
#### 正文段落格式
```xml
<w:pPr>
<w:spacing w:line="560" w:lineRule="exact"/>
<w:ind w:firstLine="560"/>
<w:jc w:val="both"/>
</w:pPr>
<w:rPr>
<w:rFonts w:ascii="FangSong" w:eastAsia="仿宋" w:hAnsi="FangSong"/>
<w:sz w:val="28"/>
<w:szCs w:val="28"/>
</w:rPr>
```
#### 一级标题
```xml
<w:pPr>
<w:pStyle w:val="Heading1"/>
<w:spacing w:line="640" w:lineRule="exact" w:before="240" w:after="120"/>
</w:pPr>
<w:rPr>
<w:rFonts w:ascii="SimHei" w:eastAsia="黑体" w:hAnsi="SimHei"/>
<w:b/>
<w:sz w:val="32"/>
<w:szCs w:val="32"/>
</w:rPr>
```
#### 三线表边框(`<w:tblBorders>` 内)
```xml
<w:top w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:bottom w:val="single" w:sz="12" w:space="0" w:color="000000"/>
<w:insideH w:val="single" w:sz="6" w:space="0" w:color="000000"/>
<w:left w:val="none" w:sz="0" w:space="0" w:color="auto"/>
<w:right w:val="none" w:sz="0" w:space="0" w:color="auto"/>
<w:insideV w:val="none" w:sz="0" w:space="0" w:color="auto"/>
```
---
## 从现有文档提取内容并重建
当需要对已有文档「重排版」时,推荐提取内容后用 docx-js 重建:
```bash
# 提取为 Markdown
pandoc original.docx -o content.md
# 然后读取 content.md用上面的模板函数构建新文档
```
这比原地修改格式更可靠,避免了样式冲突和继承问题。
---
## 格式检查清单
- [ ] 纸张A411906 × 16838 DXA
- [ ] 页边距上37 / 下35 / 左28 / 右26 mm
- [ ] 一级标题黑体三号16pt固定行距 32pt
- [ ] 二级标题楷体四号加粗14pt固定行距 28pt
- [ ] 正文仿宋四号14pt固定行距 28pt首行缩进 2 字符
- [ ] 表格:三线表,表头黑体小四 + 浅灰底,内容仿宋小四
- [ ] 页脚:居中 "— 页码 —",仿宋五号
- [ ] 字体必须设置 `eastAsia` 属性
## 依赖
- **Node.js**: v18+
- **docx**: `npm install docx`docx-js 库)
- **pandoc**: 文本提取(可选)
- **官方 docx skill**: 提供 validate.py / unpack.py / pack.py

View File

@ -0,0 +1,287 @@
/**
* GB/T 9704-2012 公文格式常量
* 供所有公文文档生成脚本共享
*/
const { AlignmentType, BorderStyle, WidthType, ShadingType, LevelFormat,
HeadingLevel, Paragraph, TextRun, Table, TableRow, TableCell,
Header, Footer, PageNumber } = require("docx");
// ─── 页面 ─────────────────────────────────────────────────
const PAGE = {
WIDTH: 11906, // 210mm
HEIGHT: 16838, // 297mm
MARGIN_TOP: 2098, // 37mm
MARGIN_BOTTOM: 1985, // 35mm
MARGIN_LEFT: 1588, // 28mm
MARGIN_RIGHT: 1474, // 26mm
CONTENT_WIDTH: 8844, // 210 - 28 - 26 mm
};
// ─── 字体 ─────────────────────────────────────────────────
const FONT = {
HEITI: { ascii: "SimHei", eastAsia: "黑体", hAnsi: "SimHei" },
FANGSONG: { ascii: "FangSong", eastAsia: "仿宋", hAnsi: "FangSong" },
KAITI: { ascii: "KaiTi", eastAsia: "楷体", hAnsi: "KaiTi" },
SONGTI: { ascii: "SimSun", eastAsia: "宋体", hAnsi: "SimSun" },
};
// ─── 字号half-point ───────────────────────────────────
const SIZE = {
ER_HAO: 44, // 二号 22pt
SAN_HAO: 32, // 三号 16pt
SI_HAO: 28, // 四号 14pt
XIAO_SI: 24, // 小四 12pt
WU_HAO: 21, // 五号 10.5pt
};
// ─── 行距twips = pt × 20 ─────────────────────────────
const LINE = {
BODY: { line: 560, lineRule: "exact" }, // 28pt
HEADING1: { line: 640, lineRule: "exact" }, // 32pt
HEADING2: { line: 560, lineRule: "exact" }, // 28pt
TABLE: { line: 440, lineRule: "exact" }, // 22pt
TITLE: { line: 640, lineRule: "exact" }, // 32pt
COMPACT: { line: 360, lineRule: "exact" }, // 18pt
};
// ─── 缩进DXA ─────────────────────────────────────────
const INDENT = {
FIRST_LINE: 560, // 首行缩进 2 字符
BULLET_LEFT: 1120,
BULLET_HANG: 560,
};
// ─── 边框 ─────────────────────────────────────────────────
const BORDER = {
NONE: { style: BorderStyle.NONE, size: 0 },
THICK: { style: BorderStyle.SINGLE, size: 12, color: "000000" }, // 1.5pt
THIN: { style: BorderStyle.SINGLE, size: 6, color: "000000" }, // 0.75pt
};
// ─── 样式预设 ─────────────────────────────────────────────
function getStyles() {
return {
default: {
document: {
run: { font: FONT.FANGSONG, size: SIZE.SI_HAO },
paragraph: { spacing: LINE.BODY },
},
},
paragraphStyles: [
{
id: "Heading1", name: "Heading 1",
basedOn: "Normal", next: "Normal", quickFormat: true,
run: { font: FONT.HEITI, size: SIZE.SAN_HAO, bold: true },
paragraph: { spacing: { ...LINE.HEADING1, before: 240, after: 120 }, outlineLevel: 0 },
},
{
id: "Heading2", name: "Heading 2",
basedOn: "Normal", next: "Normal", quickFormat: true,
run: { font: FONT.KAITI, size: SIZE.SI_HAO, bold: true },
paragraph: { spacing: { ...LINE.HEADING2, before: 120, after: 60 }, outlineLevel: 1 },
},
],
};
}
function getNumbering() {
return {
config: [{
reference: "bullets",
levels: [{
level: 0, format: LevelFormat.BULLET, text: "\u2022",
alignment: AlignmentType.LEFT,
style: {
run: { font: FONT.FANGSONG, size: SIZE.SI_HAO },
paragraph: { indent: { left: INDENT.BULLET_LEFT, hanging: INDENT.BULLET_HANG } },
},
}],
}],
};
}
// ─── 辅助函数 ─────────────────────────────────────────────
/** 报告标题区(无封面,居中) */
function titleBlock(title, subtitle, date, institution) {
const items = [];
items.push(new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { ...LINE.TITLE, after: 0 },
children: [new TextRun({ text: title, font: FONT.HEITI, size: SIZE.SAN_HAO, bold: true })],
}));
if (subtitle) {
items.push(new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { ...LINE.TITLE, after: 120 },
children: [new TextRun({ text: subtitle, font: FONT.HEITI, size: SIZE.SAN_HAO })],
}));
}
items.push(new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { ...LINE.BODY, after: 0 },
children: [new TextRun({ text: date, font: FONT.FANGSONG, size: SIZE.SI_HAO })],
}));
if (institution) {
items.push(new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { ...LINE.BODY, after: 240 },
children: [new TextRun({ text: institution, font: FONT.FANGSONG, size: SIZE.SI_HAO })],
}));
}
return items;
}
/** 一级标题 */
function h1(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_1,
spacing: { ...LINE.HEADING1, before: 240, after: 120 },
children: [new TextRun({ text, font: FONT.HEITI, size: SIZE.SAN_HAO, bold: true })],
});
}
/** 二级标题 */
function h2(text) {
return new Paragraph({
heading: HeadingLevel.HEADING_2,
spacing: { ...LINE.HEADING2, before: 120, after: 60 },
children: [new TextRun({ text, font: FONT.KAITI, size: SIZE.SI_HAO, bold: true })],
});
}
/** 正文段落 */
function body(text, opts = {}) {
const { bold = false, align = AlignmentType.JUSTIFIED, indent = true } = opts;
return new Paragraph({
alignment: align,
indent: indent ? { firstLine: INDENT.FIRST_LINE } : undefined,
spacing: LINE.BODY,
children: [new TextRun({ text, font: FONT.FANGSONG, size: SIZE.SI_HAO, bold })],
});
}
/** 多 run 正文段落(支持混排加粗) */
function bodyRuns(runs, opts = {}) {
const { align = AlignmentType.JUSTIFIED, indent = true } = opts;
return new Paragraph({
alignment: align,
indent: indent ? { firstLine: INDENT.FIRST_LINE } : undefined,
spacing: LINE.BODY,
children: runs.map(r => {
if (typeof r === "string") {
return new TextRun({ text: r, font: FONT.FANGSONG, size: SIZE.SI_HAO });
}
return new TextRun({ font: FONT.FANGSONG, size: SIZE.SI_HAO, ...r });
}),
});
}
/** 列表项 */
function bullet(text) {
return new Paragraph({
numbering: { reference: "bullets", level: 0 },
spacing: LINE.BODY,
children: [new TextRun({ text, font: FONT.FANGSONG, size: SIZE.SI_HAO })],
});
}
/** 三线表 */
function threeLineTable(headers, rows, opts = {}) {
const { colWidths } = opts;
const widths = colWidths || headers.map(() => Math.floor(PAGE.CONTENT_WIDTH / headers.length));
function cell(text, isHeader, isLastRow, colIdx) {
const borders = {
top: isHeader ? BORDER.THICK : BORDER.NONE,
bottom: isHeader ? BORDER.THIN : (isLastRow ? BORDER.THICK : BORDER.NONE),
left: BORDER.NONE,
right: BORDER.NONE,
};
return new TableCell({
borders,
width: { size: widths[colIdx], type: WidthType.DXA },
shading: isHeader ? { fill: "1F3864", type: ShadingType.CLEAR } : undefined,
margins: { top: 40, bottom: 40, left: 80, right: 80 },
children: [new Paragraph({
alignment: AlignmentType.CENTER,
spacing: LINE.TABLE,
children: [new TextRun({
text: String(text),
font: isHeader ? FONT.HEITI : FONT.FANGSONG,
size: SIZE.XIAO_SI,
bold: isHeader,
color: isHeader ? "FFFFFF" : undefined,
})],
})],
});
}
// 表级边框全部 NONE由 cell 级别控制三线效果
const noBorder = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" };
return new Table({
width: { size: PAGE.CONTENT_WIDTH, type: WidthType.DXA },
columnWidths: widths,
borders: {
top: noBorder, bottom: noBorder,
left: noBorder, right: noBorder,
insideHorizontal: noBorder, insideVertical: noBorder,
},
rows: [
new TableRow({ children: headers.map((h, i) => cell(h, true, false, i)) }),
...rows.map((row, ri) =>
new TableRow({
children: row.map((c, ci) => cell(c, false, ri === rows.length - 1, ci)),
})
),
],
});
}
/** 标准页脚(— 页码 —) */
function pageFooter() {
return new Footer({
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({ text: "— ", font: FONT.FANGSONG, size: SIZE.WU_HAO }),
new TextRun({ children: [PageNumber.CURRENT], font: FONT.FANGSONG, size: SIZE.WU_HAO }),
new TextRun({ text: " —", font: FONT.FANGSONG, size: SIZE.WU_HAO }),
],
})],
});
}
/** 标准页眉 */
function pageHeader(text = "内部资料 注意保密") {
return new Header({
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({
text, font: FONT.FANGSONG, size: SIZE.WU_HAO, color: "808080",
})],
})],
});
}
/** section 属性(页面设置) */
function sectionProps(opts = {}) {
return {
page: {
size: { width: PAGE.WIDTH, height: PAGE.HEIGHT },
margin: {
top: PAGE.MARGIN_TOP, bottom: PAGE.MARGIN_BOTTOM,
left: PAGE.MARGIN_LEFT, right: PAGE.MARGIN_RIGHT,
},
},
...opts,
};
}
module.exports = {
PAGE, FONT, SIZE, LINE, INDENT, BORDER,
getStyles, getNumbering, sectionProps,
titleBlock, h1, h2, body, bodyRuns, bullet,
threeLineTable, pageFooter, pageHeader,
};

30
docx/LICENSE.txt Normal file
View File

@ -0,0 +1,30 @@
© 2025 Anthropic, PBC. All rights reserved.
LICENSE: Use of these materials (including all code, prompts, assets, files,
and other components of this Skill) is governed by your agreement with
Anthropic regarding use of Anthropic's services. If no separate agreement
exists, use is governed by Anthropic's Consumer Terms of Service or
Commercial Terms of Service, as applicable:
https://www.anthropic.com/legal/consumer-terms
https://www.anthropic.com/legal/commercial-terms
Your applicable agreement is referred to as the "Agreement." "Services" are
as defined in the Agreement.
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
contrary, users may not:
- Extract these materials from the Services or retain copies of these
materials outside the Services
- Reproduce or copy these materials, except for temporary copies created
automatically during authorized use of the Services
- Create derivative works based on these materials
- Distribute, sublicense, or transfer these materials to any third party
- Make, offer to sell, sell, or import any inventions embodied in these
materials
- Reverse engineer, decompile, or disassemble these materials
The receipt, viewing, or possession of these materials does not convey or
imply any license or right beyond those expressly granted above.
Anthropic retains all right, title, and interest in these materials,
including all copyrights, patents, and other intellectual property rights.

198
docx/SKILL.md Normal file
View File

@ -0,0 +1,198 @@
---
name: docx
description: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When Claude needs to work with professional documents (.docx files) for: (1) Creating new documents, (2) Modifying or editing content, (3) Working with tracked changes, (4) Adding comments, or any other document tasks"
license: Proprietary. LICENSE.txt has complete terms
---
# DOCX creation, editing, and analysis
## Overview
A user may ask you to create, edit, or analyze the contents of a .docx file. A .docx file is essentially a ZIP archive containing XML files and other resources that you can read or edit. You have different tools and workflows available for different tasks.
## Workflow Decision Tree
### Reading/Analyzing Content
Use "Text extraction" or "Raw XML access" sections below
### Creating New Document
Use "Creating a new Word document" workflow
### Editing Existing Document
- **Your own document + simple changes**
Use "Basic OOXML editing" workflow
- **Someone else's document**
Use **"Redlining workflow"** (recommended default)
- **Legal, academic, business, or government docs**
Use **"Redlining workflow"** (required)
## Reading and analyzing content
### Text extraction
If you just need to read the text contents of a document, you should convert the document to markdown using pandoc. Pandoc provides excellent support for preserving document structure and can show tracked changes:
```bash
# Convert document to markdown with tracked changes
pandoc --track-changes=all path-to-file.docx -o output.md
# Options: --track-changes=accept/reject/all
```
### Raw XML access
You need raw XML access for: comments, complex formatting, document structure, embedded media, and metadata. For any of these features, you'll need to unpack a document and read its raw XML contents.
#### Unpacking a file
`python ooxml/scripts/unpack.py <office_file> <output_directory>`
#### Key file structures
* `word/document.xml` - Main document contents
* `word/comments.xml` - Comments referenced in document.xml
* `word/media/` - Embedded images and media files
* Tracked changes use `<w:ins>` (insertions) and `<w:del>` (deletions) tags
## Creating a new Word document
When creating a new Word document from scratch, use **docx-js**, which allows you to create Word documents using JavaScript/TypeScript.
### Workflow
1. **MANDATORY - READ ENTIRE FILE**: Read [`docx-js.md`](docx-js.md) (~500 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for detailed syntax, critical formatting rules, and best practices before proceeding with document creation.
2. Create a JavaScript/TypeScript file using Document, Paragraph, TextRun components (You can assume all dependencies are installed, but if not, refer to the dependencies section below)
3. Export as .docx using Packer.toBuffer()
## Editing an existing Word document
When editing an existing Word document, use the **Document library** (a Python library for OOXML manipulation). The library automatically handles infrastructure setup and provides methods for document manipulation. For complex scenarios, you can access the underlying DOM directly through the library.
### Workflow
1. **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for the Document library API and XML patterns for directly editing document files.
2. Unpack the document: `python ooxml/scripts/unpack.py <office_file> <output_directory>`
3. Create and run a Python script using the Document library (see "Document Library" section in ooxml.md)
4. Pack the final document: `python ooxml/scripts/pack.py <input_directory> <office_file>`
The Document library provides both high-level methods for common operations and direct DOM access for complex scenarios.
## Redlining workflow for document review
This workflow allows you to plan comprehensive tracked changes using markdown before implementing them in OOXML. **CRITICAL**: For complete tracked changes, you must implement ALL changes systematically.
**Batching Strategy**: Group related changes into batches of 3-10 changes. This makes debugging manageable while maintaining efficiency. Test each batch before moving to the next.
**Principle: Minimal, Precise Edits**
When implementing tracked changes, only mark text that actually changes. Repeating unchanged text makes edits harder to review and appears unprofessional. Break replacements into: [unchanged text] + [deletion] + [insertion] + [unchanged text]. Preserve the original run's RSID for unchanged text by extracting the `<w:r>` element from the original and reusing it.
Example - Changing "30 days" to "60 days" in a sentence:
```python
# BAD - Replaces entire sentence
'<w:del><w:r><w:delText>The term is 30 days.</w:delText></w:r></w:del><w:ins><w:r><w:t>The term is 60 days.</w:t></w:r></w:ins>'
# GOOD - Only marks what changed, preserves original <w:r> for unchanged text
'<w:r w:rsidR="00AB12CD"><w:t>The term is </w:t></w:r><w:del><w:r><w:delText>30</w:delText></w:r></w:del><w:ins><w:r><w:t>60</w:t></w:r></w:ins><w:r w:rsidR="00AB12CD"><w:t> days.</w:t></w:r>'
```
### Tracked changes workflow
1. **Get markdown representation**: Convert document to markdown with tracked changes preserved:
```bash
pandoc --track-changes=all path-to-file.docx -o current.md
```
2. **Identify and group changes**: Review the document and identify ALL changes needed, organizing them into logical batches:
**Location methods** (for finding changes in XML):
- Section/heading numbers (e.g., "Section 3.2", "Article IV")
- Paragraph identifiers if numbered
- Grep patterns with unique surrounding text
- Document structure (e.g., "first paragraph", "signature block")
- **DO NOT use markdown line numbers** - they don't map to XML structure
**Batch organization** (group 3-10 related changes per batch):
- By section: "Batch 1: Section 2 amendments", "Batch 2: Section 5 updates"
- By type: "Batch 1: Date corrections", "Batch 2: Party name changes"
- By complexity: Start with simple text replacements, then tackle complex structural changes
- Sequential: "Batch 1: Pages 1-3", "Batch 2: Pages 4-6"
3. **Read documentation and unpack**:
- **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~600 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Pay special attention to the "Document Library" and "Tracked Change Patterns" sections.
- **Unpack the document**: `python ooxml/scripts/unpack.py <file.docx> <dir>`
- **Note the suggested RSID**: The unpack script will suggest an RSID to use for your tracked changes. Copy this RSID for use in step 4b.
4. **Implement changes in batches**: Group changes logically (by section, by type, or by proximity) and implement them together in a single script. This approach:
- Makes debugging easier (smaller batch = easier to isolate errors)
- Allows incremental progress
- Maintains efficiency (batch size of 3-10 changes works well)
**Suggested batch groupings:**
- By document section (e.g., "Section 3 changes", "Definitions", "Termination clause")
- By change type (e.g., "Date changes", "Party name updates", "Legal term replacements")
- By proximity (e.g., "Changes on pages 1-3", "Changes in first half of document")
For each batch of related changes:
**a. Map text to XML**: Grep for text in `word/document.xml` to verify how text is split across `<w:r>` elements.
**b. Create and run script**: Use `get_node` to find nodes, implement changes, then `doc.save()`. See **"Document Library"** section in ooxml.md for patterns.
**Note**: Always grep `word/document.xml` immediately before writing a script to get current line numbers and verify text content. Line numbers change after each script run.
5. **Pack the document**: After all batches are complete, convert the unpacked directory back to .docx:
```bash
python ooxml/scripts/pack.py unpacked reviewed-document.docx
```
6. **Final verification**: Do a comprehensive check of the complete document:
- Convert final document to markdown:
```bash
pandoc --track-changes=all reviewed-document.docx -o verification.md
```
- Verify ALL changes were applied correctly:
```bash
grep "original phrase" verification.md # Should NOT find it
grep "replacement phrase" verification.md # Should find it
```
- Check that no unintended changes were introduced
## Converting Documents to Images
To visually analyze Word documents, convert them to images using a two-step process:
1. **Convert DOCX to PDF**:
```bash
soffice --headless --convert-to pdf document.docx
```
2. **Convert PDF pages to JPEG images**:
```bash
pdftoppm -jpeg -r 150 document.pdf page
```
This creates files like `page-1.jpg`, `page-2.jpg`, etc.
Options:
- `-r 150`: Sets resolution to 150 DPI (adjust for quality/size balance)
- `-jpeg`: Output JPEG format (use `-png` for PNG if preferred)
- `-f N`: First page to convert (e.g., `-f 2` starts from page 2)
- `-l N`: Last page to convert (e.g., `-l 5` stops at page 5)
- `page`: Prefix for output files
Example for specific range:
```bash
pdftoppm -jpeg -r 150 -f 2 -l 5 document.pdf page # Converts only pages 2-5
```
## Code Style Guidelines
**IMPORTANT**: When generating code for DOCX operations:
- Write concise code
- Avoid verbose variable names and redundant operations
- Avoid unnecessary print statements
- **跨平台路径**: 始终用 `path.join()` (JS) 或 `os.path.join()` (Python) 拼接路径,不要硬编码 `/``\\`。详见 docx-js.md「Cross-Platform 路径处理」章节
## Dependencies
Required dependencies (install if not available):
- **pandoc**: `sudo apt-get install pandoc` (for text extraction)
- **docx**: `npm install -g docx` (for creating new documents)
- **LibreOffice**: `sudo apt-get install libreoffice` (for PDF conversion)
- **Poppler**: `sudo apt-get install poppler-utils` (for pdftoppm to convert PDF to images)
- **defusedxml**: `pip install defusedxml` (for secure XML parsing)

499
docx/docx-js.md Normal file
View File

@ -0,0 +1,499 @@
# DOCX Library Tutorial
Generate .docx files with JavaScript/TypeScript.
**Important: Read this entire document before starting.** Critical formatting rules and common pitfalls are covered throughout - skipping sections may result in corrupted files or rendering issues.
## Setup
Assumes docx is already installed globally
If not installed: `npm install -g docx`
```javascript
const { Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, ImageRun, Media,
Header, Footer, AlignmentType, PageOrientation, LevelFormat, ExternalHyperlink,
InternalHyperlink, TableOfContents, HeadingLevel, BorderStyle, WidthType, TabStopType,
TabStopPosition, UnderlineType, ShadingType, VerticalAlign, SymbolRun, PageNumber,
FootnoteReferenceRun, Footnote, PageBreak } = require('docx');
// Create & Save
const doc = new Document({ sections: [{ children: [/* content */] }] });
Packer.toBuffer(doc).then(buffer => fs.writeFileSync("doc.docx", buffer)); // Node.js
Packer.toBlob(doc).then(blob => { /* download logic */ }); // Browser
```
## Text & Formatting
```javascript
// IMPORTANT: Never use \n for line breaks - always use separate Paragraph elements
// ❌ WRONG: new TextRun("Line 1\nLine 2")
// ✅ CORRECT: new Paragraph({ children: [new TextRun("Line 1")] }), new Paragraph({ children: [new TextRun("Line 2")] })
// Basic text with all formatting options (公文配置:仿宋 14pt 默认)
new Paragraph({
alignment: AlignmentType.JUSTIFIED, // 公文两端对齐
spacing: { before: 200, after: 200 },
indent: { firstLine: 560 }, // 公文首行缩进 2em
children: [
new TextRun({ text: "加粗", bold: true }),
new TextRun({ text: "斜体", italics: true }),
new TextRun({ text: "下划线", underline: { type: UnderlineType.SINGLE, color: "000000" } }), // 公文用黑色
new TextRun({ text: "指定字号", size: 28, font: "STFangsong" }), // 仿宋 14pt
new TextRun({ text: "高亮", highlight: "yellow" }),
new TextRun({ text: "删除线", strike: true }),
new TextRun({ text: "x2", superScript: true }),
new TextRun({ text: "H2O", subScript: true }),
new SymbolRun({ char: "2022", font: "Symbol" }), // Bullet •
new SymbolRun({ char: "00A9", font: "STFangsong" }) // Copyright ©
]
})
```
## Styles — AbleMind 公文 UI 设计系统
### 字体体系
| 变量 | 字体栈 | 用途 |
|------|--------|------|
| `--gov-font-body` | STFangsong → FangSong → Fangsong SC → Noto Serif SC → serif | 正文(仿宋体) |
| `--gov-font-heading` | Heiti SC → PingFang SC → SimHei → Noto Sans SC → sans-serif | 标题(黑体) |
| `--gov-font-mono` | IBM Plex Mono → JetBrains Mono → monospace | UI 等宽 |
| `--gov-font-code` | Courier New → monospace | 代码块 |
在 docx-js 中使用时font 值按优先级取第一个系统可用字体即可macOS 优先 STFangsong / Heiti SC
### 公文排版规范
| 元素 | 字体 | 字号 | 其他 |
|------|------|------|------|
| 正文 | 仿宋 (STFangsong) | 14pt (size: 28) | 行距 1.5,首行缩进 2em两端对齐 |
| h1 | 黑体 (Heiti SC) | 16pt (size: 32) | 居中,加粗 |
| h2 | 黑体 (Heiti SC) | 15pt (size: 30) | 左对齐,加粗 |
| h3h6 | 黑体 (Heiti SC) | 14pt (size: 28) | 左对齐,加粗 |
| 表格 | 仿宋 (STFangsong) | 小四 12pt (size: 24) | 全线框,表头灰底 |
| 代码 | Courier New | 12pt (size: 24) | 灰底框线 |
| 链接 | 同正文 | 同正文 | 黑色下划线(公文不用彩色链接) |
### 标准公文样式模板
```javascript
// AbleMind 公文配置 — 默认样式
const GOV_FONT_BODY = "STFangsong"; // 仿宋体(正文)
const GOV_FONT_HEADING = "Heiti SC"; // 黑体(标题)
const GOV_FONT_CODE = "Courier New"; // 代码块
const doc = new Document({
styles: {
default: {
document: {
run: { font: GOV_FONT_BODY, size: 28 }, // 仿宋 14pt
paragraph: {
spacing: { line: 360 }, // 行距 1.5 (240 * 1.5)
alignment: AlignmentType.JUSTIFIED // 两端对齐
}
}
},
paragraphStyles: [
// 公文标题 — 黑体 16pt 居中
{ id: "Title", name: "Title", basedOn: "Normal",
run: { size: 32, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 240, after: 120 }, alignment: AlignmentType.CENTER } },
// h1 — 黑体 16pt 居中
{ id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 32, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 240, after: 240, line: 360 }, alignment: AlignmentType.CENTER, outlineLevel: 0 } },
// h2 — 黑体 15pt 左对齐
{ id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 30, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 180, after: 180, line: 360 }, outlineLevel: 1 } },
// h3h6 — 黑体 14pt 左对齐
{ id: "Heading3", name: "Heading 3", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 120, after: 120, line: 360 }, outlineLevel: 2 } },
{ id: "Heading4", name: "Heading 4", basedOn: "Normal", next: "Normal", quickFormat: true,
run: { size: 28, bold: true, color: "000000", font: GOV_FONT_HEADING },
paragraph: { spacing: { before: 120, after: 120, line: 360 }, outlineLevel: 3 } },
// 自定义样式仍可添加
{ id: "govNote", name: "Gov Note", basedOn: "Normal",
run: { size: 24, color: "333333", font: GOV_FONT_BODY },
paragraph: { spacing: { after: 60 } } }
],
characterStyles: [
// 公文链接:黑色下划线,不用彩色
{ id: "Hyperlink", name: "Hyperlink",
run: { color: "000000", underline: { type: UnderlineType.SINGLE, color: "000000" } } },
{ id: "govEmphasis", name: "Gov Emphasis",
run: { bold: true, font: GOV_FONT_HEADING } }
]
},
sections: [{
properties: {
page: {
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // A4 标准页边距 1 英寸
size: { width: 11906, height: 16838 } // A4 尺寸 (210mm × 297mm in DXA)
}
},
children: [
new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun("公文标题")] }),
new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("一级标题")] }),
new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun("二级标题")] }),
// 正文段落 — 首行缩进 2em仿宋14pt ≈ 560 DXA
new Paragraph({
indent: { firstLine: 560 },
children: [new TextRun("正文内容仿宋14pt行距1.5首行缩进2em两端对齐。")]
})
]
}]
});
```
### 公文首行缩进说明
- 首行缩进 2em = 2 × 字号对应的 DXA 值
- 仿宋 14pt → `firstLine: 560` (14pt × 20 DXA/pt × 2)
- 小四 12pt → `firstLine: 480` (12pt × 20 DXA/pt × 2)
- 通过 `indent: { firstLine: 560 }` 设置在每个正文 Paragraph 上
### 跨平台字体回退
- **macOS**: STFangsong / Heiti SC系统自带
- **Windows**: FangSong / SimHei系统自带
- **Linux/CI**: Noto Serif SC / Noto Sans SC需安装 Google Noto CJK
- docx-js 的 `font` 属性只写一个字体名Word 打开时自动使用系统可用字体
**Key Styling Principles:**
- **Override built-in styles**: Use exact IDs like "Heading1", "Heading2", "Heading3" to override Word's built-in heading styles
- **HeadingLevel constants**: `HeadingLevel.HEADING_1` uses "Heading1" style, `HeadingLevel.HEADING_2` uses "Heading2" style, etc.
- **Include outlineLevel**: Set `outlineLevel: 0` for H1, `outlineLevel: 1` for H2, etc. to ensure TOC works correctly
- **公文字体一致性**: 正文统一仿宋,标题统一黑体,不混用其他字体
- **公文不用彩色**: 链接、标题全部黑色,不使用蓝色超链接或灰色标题
- **A4 纸张**: 使用 `size: { width: 11906, height: 16838 }` 设置 A4 尺寸
- **行距 1.5**: 在 default paragraph spacing 中设置 `line: 360`
- **首行缩进**: 正文段落添加 `indent: { firstLine: 560 }`
## Lists (ALWAYS USE PROPER LISTS - NEVER USE UNICODE BULLETS)
```javascript
// Bullets - ALWAYS use the numbering config, NOT unicode symbols
// CRITICAL: Use LevelFormat.BULLET constant, NOT the string "bullet"
const doc = new Document({
numbering: {
config: [
{ reference: "bullet-list",
levels: [{ level: 0, format: LevelFormat.BULLET, text: "•", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "first-numbered-list",
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: "second-numbered-list", // Different reference = restarts at 1
levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1.", alignment: AlignmentType.LEFT,
style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] }
]
},
sections: [{
children: [
// Bullet list items
new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("First bullet point")] }),
new Paragraph({ numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun("Second bullet point")] }),
// Numbered list items
new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
children: [new TextRun("First numbered item")] }),
new Paragraph({ numbering: { reference: "first-numbered-list", level: 0 },
children: [new TextRun("Second numbered item")] }),
// ⚠️ CRITICAL: Different reference = INDEPENDENT list that restarts at 1
// Same reference = CONTINUES previous numbering
new Paragraph({ numbering: { reference: "second-numbered-list", level: 0 },
children: [new TextRun("Starts at 1 again (because different reference)")] })
]
}]
});
// ⚠️ CRITICAL NUMBERING RULE: Each reference creates an INDEPENDENT numbered list
// - Same reference = continues numbering (1, 2, 3... then 4, 5, 6...)
// - Different reference = restarts at 1 (1, 2, 3... then 1, 2, 3...)
// Use unique reference names for each separate numbered section!
// ⚠️ CRITICAL: NEVER use unicode bullets - they create fake lists that don't work properly
// new TextRun("• Item") // WRONG
// new SymbolRun({ char: "2022" }) // WRONG
// ✅ ALWAYS use numbering config with LevelFormat.BULLET for real Word lists
```
## Tables — 公文表格规范
```javascript
// 公文表格:小四 12pt 仿宋,全线框,表头灰底居中加粗
const GOV_FONT_BODY = "STFangsong";
const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "000000" }; // 公文用黑色全线框
const cellBorders = { top: tableBorder, bottom: tableBorder, left: tableBorder, right: tableBorder };
new Table({
columnWidths: [4680, 4680], // ⚠️ CRITICAL: Set column widths at table level - values in DXA (twentieths of a point)
margins: { top: 80, bottom: 80, left: 120, right: 120 }, // Set once for all cells
rows: [
// 表头行:灰底居中加粗
new TableRow({
tableHeader: true,
children: [
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
// ⚠️ CRITICAL: Always use ShadingType.CLEAR to prevent black backgrounds in Word.
shading: { fill: "D9D9D9", type: ShadingType.CLEAR }, // 浅灰底
verticalAlign: VerticalAlign.CENTER,
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "表头", bold: true, size: 24, font: GOV_FONT_BODY })] // 小四 12pt
})]
}),
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
shading: { fill: "D9D9D9", type: ShadingType.CLEAR },
verticalAlign: VerticalAlign.CENTER,
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun({ text: "列标题", bold: true, size: 24, font: GOV_FONT_BODY })]
})]
})
]
}),
// 数据行:小四仿宋,左对齐
new TableRow({
children: [
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
children: [new Paragraph({ children: [new TextRun({ text: "数据内容", size: 24, font: GOV_FONT_BODY })] })]
}),
new TableCell({
borders: cellBorders,
width: { size: 4680, type: WidthType.DXA },
children: [
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun({ text: "列表项一", size: 24, font: GOV_FONT_BODY })]
}),
new Paragraph({
numbering: { reference: "bullet-list", level: 0 },
children: [new TextRun({ text: "列表项二", size: 24, font: GOV_FONT_BODY })]
})
]
})
]
})
]
})
```
**IMPORTANT: Table Width & Borders**
- Use BOTH `columnWidths: [width1, width2, ...]` array AND `width: { size: X, type: WidthType.DXA }` on each cell
- Values in DXA (twentieths of a point): 1440 = 1 inch, Letter usable width = 9360 DXA (with 1" margins)
- Apply borders to individual `TableCell` elements, NOT the `Table` itself
**Precomputed Column Widths (Letter size with 1" margins = 9360 DXA total):**
- **2 columns:** `columnWidths: [4680, 4680]` (equal width)
- **3 columns:** `columnWidths: [3120, 3120, 3120]` (equal width)
## Links & Navigation
```javascript
// TOC (requires headings) - CRITICAL: Use HeadingLevel only, NOT custom styles
// ❌ WRONG: new Paragraph({ heading: HeadingLevel.HEADING_1, style: "customHeader", children: [new TextRun("Title")] })
// ✅ CORRECT: new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Title")] })
new TableOfContents("Table of Contents", { hyperlink: true, headingStyleRange: "1-3" }),
// External link
new Paragraph({
children: [new ExternalHyperlink({
children: [new TextRun({ text: "Google", style: "Hyperlink" })],
link: "https://www.google.com"
})]
}),
// Internal link & bookmark
new Paragraph({
children: [new InternalHyperlink({
children: [new TextRun({ text: "Go to Section", style: "Hyperlink" })],
anchor: "section1"
})]
}),
new Paragraph({
children: [new TextRun("Section Content")],
bookmark: { id: "section1", name: "section1" }
}),
```
## Images & Media
```javascript
// Basic image with sizing & positioning
// CRITICAL: Always specify 'type' parameter - it's REQUIRED for ImageRun
new Paragraph({
alignment: AlignmentType.CENTER,
children: [new ImageRun({
type: "png", // NEW REQUIREMENT: Must specify image type (png, jpg, jpeg, gif, bmp, svg)
data: fs.readFileSync("image.png"),
transformation: { width: 200, height: 150, rotation: 0 }, // rotation in degrees
altText: { title: "Logo", description: "Company logo", name: "Name" } // IMPORTANT: All three fields are required
})]
})
```
## Page Breaks
```javascript
// Manual page break
new Paragraph({ children: [new PageBreak()] }),
// Page break before paragraph
new Paragraph({
pageBreakBefore: true,
children: [new TextRun("This starts on a new page")]
})
// ⚠️ CRITICAL: NEVER use PageBreak standalone - it will create invalid XML that Word cannot open
// ❌ WRONG: new PageBreak()
// ✅ CORRECT: new Paragraph({ children: [new PageBreak()] })
```
## Headers/Footers & Page Setup
```javascript
const doc = new Document({
sections: [{
properties: {
page: {
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 }, // 1440 = 1 inch
size: { orientation: PageOrientation.LANDSCAPE },
pageNumbers: { start: 1, formatType: "decimal" } // "upperRoman", "lowerRoman", "upperLetter", "lowerLetter"
}
},
headers: {
default: new Header({ children: [new Paragraph({
alignment: AlignmentType.RIGHT,
children: [new TextRun("Header Text")]
})] })
},
footers: {
default: new Footer({ children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [new TextRun("Page "), new TextRun({ children: [PageNumber.CURRENT] }), new TextRun(" of "), new TextRun({ children: [PageNumber.TOTAL_PAGES] })]
})] })
},
children: [/* content */]
}]
});
```
## Tabs
```javascript
new Paragraph({
tabStops: [
{ type: TabStopType.LEFT, position: TabStopPosition.MAX / 4 },
{ type: TabStopType.CENTER, position: TabStopPosition.MAX / 2 },
{ type: TabStopType.RIGHT, position: TabStopPosition.MAX * 3 / 4 }
],
children: [new TextRun("Left\tCenter\tRight")]
})
```
## Constants & Quick Reference
- **Underlines:** `SINGLE`, `DOUBLE`, `WAVY`, `DASH`
- **Borders:** `SINGLE`, `DOUBLE`, `DASHED`, `DOTTED`
- **Numbering:** `DECIMAL` (1,2,3), `UPPER_ROMAN` (I,II,III), `LOWER_LETTER` (a,b,c)
- **Tabs:** `LEFT`, `CENTER`, `RIGHT`, `DECIMAL`
- **Symbols:** `"2022"` (•), `"00A9"` (©), `"00AE"` (®), `"2122"` (™), `"00B0"` (°), `"F070"` (✓), `"F0FC"` (✗)
## Cross-Platform 路径处理Windows / macOS / Linux
**根本原因**: Windows 用 `\` 作路径分隔符macOS/Linux 用 `/`。在 JS 字符串中 `\` 是转义符,直接写 `"C:\Users\file"` 会被解析为 `"C:Usersile"`
### 必须遵守的规则
```javascript
const path = require('path');
const fs = require('fs');
// ❌ 硬编码斜杠 — Windows 上可能失败
const img = fs.readFileSync("images/logo.png");
const out = "output/report.docx";
// ✅ 始终用 path.join() 拼接路径
const img = fs.readFileSync(path.join("images", "logo.png"));
const out = path.join("output", "report.docx");
// ❌ 模板字符串拼路径
const file = `${dir}/report.docx`;
// ✅ path.join 拼接
const file = path.join(dir, "report.docx");
// ❌ __dirname + 硬编码斜杠
const tpl = __dirname + "/templates/header.xml";
// ✅ path.join(__dirname, ...)
const tpl = path.join(__dirname, "templates", "header.xml");
```
### 输出文件名注意事项
```javascript
// ✅ 写文件前确保目录存在
const outDir = path.join("output");
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
Packer.toBuffer(doc).then(buf => fs.writeFileSync(path.join(outDir, "report.docx"), buf));
```
### Python 脚本同样适用
```python
import os
# ❌ 硬编码斜杠
doc_path = "word/document.xml"
# ✅ os.path.join
doc_path = os.path.join("word", "document.xml")
# ✅ pathlib (Python 3.4+) 更优雅
from pathlib import Path
doc_path = Path("word") / "document.xml"
```
### Shell 命令中的路径
```bash
# ✅ 正斜杠在所有平台的 shell 中都能工作(包括 Windows PowerShell/cmd
python ooxml/scripts/unpack.py input.docx output_dir
# ⚠️ 但如果路径来自变量且含空格,务必加引号
python "ooxml/scripts/unpack.py" "$INPUT_FILE" "$OUTPUT_DIR"
```
### 快速检查清单
| 检查项 | 说明 |
|--------|------|
| 不出现 `"/"` 拼路径 | 用 `path.join()` / `os.path.join()` |
| 不出现 `"\\"` 拼路径 | 同上 |
| 不出现 `` `${x}/y` `` 拼路径 | 用 `path.join(x, "y")` |
| `fs.mkdirSync``recursive` | 确保输出目录存在 |
| 文件名不含 `: * ? " < > \|` | Windows 保留字符,会导致写入失败 |
| 路径含空格时加引号 | shell 命令中 `"$PATH"` |
## Critical Issues & Common Mistakes
- **CRITICAL: PageBreak must ALWAYS be inside a Paragraph** - standalone PageBreak creates invalid XML that Word cannot open
- **ALWAYS use ShadingType.CLEAR for table cell shading** - Never use ShadingType.SOLID (causes black background).
- Measurements in DXA (1440 = 1 inch) | Each table cell needs ≥1 Paragraph | TOC requires HeadingLevel styles only
- **公文字体**: 正文用仿宋 (STFangsong),标题用黑体 (Heiti SC),表格用小四仿宋,代码用 Courier New
- **公文默认字号**: 正文 14pt (size: 28)h1 16pt (size: 32)h2 15pt (size: 30)h3+ 14pt (size: 28),表格 12pt (size: 24)
- **公文行距**: 在 default paragraph 中设置 `spacing: { line: 360 }` (1.5 倍行距)
- **公文首行缩进**: 正文段落添加 `indent: { firstLine: 560 }`14pt × 20 × 2
- **公文纸张**: A4 尺寸 `size: { width: 11906, height: 16838 }`
- **公文链接**: 黑色下划线,覆盖 Hyperlink 字符样式为 `color: "000000"`
- **公文表格**: 黑色全线框 `color: "000000"`,表头灰底 `fill: "D9D9D9"`
- **ALWAYS use columnWidths array for tables** + individual cell widths for compatibility
- **NEVER use unicode symbols for bullets** - always use proper numbering configuration with `LevelFormat.BULLET` constant (NOT the string "bullet")
- **NEVER use \n for line breaks anywhere** - always use separate Paragraph elements for each line
- **ALWAYS use TextRun objects within Paragraph children** - never use text property directly on Paragraph
- **CRITICAL for images**: ImageRun REQUIRES `type` parameter - always specify "png", "jpg", "jpeg", "gif", "bmp", or "svg"
- **CRITICAL for bullets**: Must use `LevelFormat.BULLET` constant, not string "bullet", and include `text: "•"` for the bullet character
- **CRITICAL for numbering**: Each numbering reference creates an INDEPENDENT list. Same reference = continues numbering (1,2,3 then 4,5,6). Different reference = restarts at 1 (1,2,3 then 1,2,3). Use unique reference names for each separate numbered section!
- **CRITICAL for TOC**: When using TableOfContents, headings must use HeadingLevel ONLY - do NOT add custom styles to heading paragraphs or TOC will break
- **Tables**: Set `columnWidths` array + individual cell widths, apply borders to cells not table
- **Set table margins at TABLE level** for consistent cell padding (avoids repetition per cell)
- **跨平台路径**: 始终用 `path.join()` 拼接路径,不硬编码 `/``\\`。写文件前用 `fs.mkdirSync(dir, { recursive: true })` 确保目录存在

610
docx/ooxml.md Normal file
View File

@ -0,0 +1,610 @@
# Office Open XML Technical Reference
**Important: Read this entire document before starting.** This document covers:
- [Technical Guidelines](#technical-guidelines) - Schema compliance rules and validation requirements
- [Document Content Patterns](#document-content-patterns) - XML patterns for headings, lists, tables, formatting, etc.
- [Document Library (Python)](#document-library-python) - Recommended approach for OOXML manipulation with automatic infrastructure setup
- [Tracked Changes (Redlining)](#tracked-changes-redlining) - XML patterns for implementing tracked changes
## Technical Guidelines
### Schema Compliance
- **Element ordering in `<w:pPr>`**: `<w:pStyle>`, `<w:numPr>`, `<w:spacing>`, `<w:ind>`, `<w:jc>`
- **Whitespace**: Add `xml:space='preserve'` to `<w:t>` elements with leading/trailing spaces
- **Unicode**: Escape characters in ASCII content: `"` becomes `&#8220;`
- **Character encoding reference**: Curly quotes `""` become `&#8220;&#8221;`, apostrophe `'` becomes `&#8217;`, em-dash `—` becomes `&#8212;`
- **Tracked changes**: Use `<w:del>` and `<w:ins>` tags with `w:author="Claude"` outside `<w:r>` elements
- **Critical**: `<w:ins>` closes with `</w:ins>`, `<w:del>` closes with `</w:del>` - never mix
- **RSIDs must be 8-digit hex**: Use values like `00AB1234` (only 0-9, A-F characters)
- **trackRevisions placement**: Add `<w:trackRevisions/>` after `<w:proofState>` in settings.xml
- **Images**: Add to `word/media/`, reference in `document.xml`, set dimensions to prevent overflow
## Document Content Patterns
### Basic Structure
```xml
<w:p>
<w:r><w:t>Text content</w:t></w:r>
</w:p>
```
### Headings and Styles
```xml
<w:p>
<w:pPr>
<w:pStyle w:val="Title"/>
<w:jc w:val="center"/>
</w:pPr>
<w:r><w:t>Document Title</w:t></w:r>
</w:p>
<w:p>
<w:pPr><w:pStyle w:val="Heading2"/></w:pPr>
<w:r><w:t>Section Heading</w:t></w:r>
</w:p>
```
### Text Formatting
```xml
<!-- Bold -->
<w:r><w:rPr><w:b/><w:bCs/></w:rPr><w:t>Bold</w:t></w:r>
<!-- Italic -->
<w:r><w:rPr><w:i/><w:iCs/></w:rPr><w:t>Italic</w:t></w:r>
<!-- Underline -->
<w:r><w:rPr><w:u w:val="single"/></w:rPr><w:t>Underlined</w:t></w:r>
<!-- Highlight -->
<w:r><w:rPr><w:highlight w:val="yellow"/></w:rPr><w:t>Highlighted</w:t></w:r>
```
### Lists
```xml
<!-- Numbered list -->
<w:p>
<w:pPr>
<w:pStyle w:val="ListParagraph"/>
<w:numPr><w:ilvl w:val="0"/><w:numId w:val="1"/></w:numPr>
<w:spacing w:before="240"/>
</w:pPr>
<w:r><w:t>First item</w:t></w:r>
</w:p>
<!-- Restart numbered list at 1 - use different numId -->
<w:p>
<w:pPr>
<w:pStyle w:val="ListParagraph"/>
<w:numPr><w:ilvl w:val="0"/><w:numId w:val="2"/></w:numPr>
<w:spacing w:before="240"/>
</w:pPr>
<w:r><w:t>New list item 1</w:t></w:r>
</w:p>
<!-- Bullet list (level 2) -->
<w:p>
<w:pPr>
<w:pStyle w:val="ListParagraph"/>
<w:numPr><w:ilvl w:val="1"/><w:numId w:val="1"/></w:numPr>
<w:spacing w:before="240"/>
<w:ind w:left="900"/>
</w:pPr>
<w:r><w:t>Bullet item</w:t></w:r>
</w:p>
```
### Tables
```xml
<w:tbl>
<w:tblPr>
<w:tblStyle w:val="TableGrid"/>
<w:tblW w:w="0" w:type="auto"/>
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="4675"/><w:gridCol w:w="4675"/>
</w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>Cell 1</w:t></w:r></w:p>
</w:tc>
<w:tc>
<w:tcPr><w:tcW w:w="4675" w:type="dxa"/></w:tcPr>
<w:p><w:r><w:t>Cell 2</w:t></w:r></w:p>
</w:tc>
</w:tr>
</w:tbl>
```
### Layout
```xml
<!-- Page break before new section (common pattern) -->
<w:p>
<w:r>
<w:br w:type="page"/>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Heading1"/>
</w:pPr>
<w:r>
<w:t>New Section Title</w:t>
</w:r>
</w:p>
<!-- Centered paragraph -->
<w:p>
<w:pPr>
<w:spacing w:before="240" w:after="0"/>
<w:jc w:val="center"/>
</w:pPr>
<w:r><w:t>Centered text</w:t></w:r>
</w:p>
<!-- Font change - paragraph level (applies to all runs) -->
<w:p>
<w:pPr>
<w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
</w:pPr>
<w:r><w:t>Monospace text</w:t></w:r>
</w:p>
<!-- Font change - run level (specific to this text) -->
<w:p>
<w:r>
<w:rPr><w:rFonts w:ascii="Courier New" w:hAnsi="Courier New"/></w:rPr>
<w:t>This text is Courier New</w:t>
</w:r>
<w:r><w:t> and this text uses default font</w:t></w:r>
</w:p>
```
## File Updates
When adding content, update these files:
**`word/_rels/document.xml.rels`:**
```xml
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" Target="numbering.xml"/>
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>
```
**`[Content_Types].xml`:**
```xml
<Default Extension="png" ContentType="image/png"/>
<Override PartName="/word/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>
```
### Images
**CRITICAL**: Calculate dimensions to prevent page overflow and maintain aspect ratio.
```xml
<!-- Minimal required structure -->
<w:p>
<w:r>
<w:drawing>
<wp:inline>
<wp:extent cx="2743200" cy="1828800"/>
<wp:docPr id="1" name="Picture 1"/>
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvPicPr>
<pic:cNvPr id="0" name="image1.png"/>
<pic:cNvPicPr/>
</pic:nvPicPr>
<pic:blipFill>
<a:blip r:embed="rId5"/>
<!-- Add for stretch fill with aspect ratio preservation -->
<a:stretch>
<a:fillRect/>
</a:stretch>
</pic:blipFill>
<pic:spPr>
<a:xfrm>
<a:ext cx="2743200" cy="1828800"/>
</a:xfrm>
<a:prstGeom prst="rect"/>
</pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>
```
### Links (Hyperlinks)
**IMPORTANT**: All hyperlinks (both internal and external) require the Hyperlink style to be defined in styles.xml. Without this style, links will look like regular text instead of blue underlined clickable links.
**External Links:**
```xml
<!-- In document.xml -->
<w:hyperlink r:id="rId5">
<w:r>
<w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
<w:t>Link Text</w:t>
</w:r>
</w:hyperlink>
<!-- In word/_rels/document.xml.rels -->
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
Target="https://www.example.com/" TargetMode="External"/>
```
**Internal Links:**
```xml
<!-- Link to bookmark -->
<w:hyperlink w:anchor="myBookmark">
<w:r>
<w:rPr><w:rStyle w:val="Hyperlink"/></w:rPr>
<w:t>Link Text</w:t>
</w:r>
</w:hyperlink>
<!-- Bookmark target -->
<w:bookmarkStart w:id="0" w:name="myBookmark"/>
<w:r><w:t>Target content</w:t></w:r>
<w:bookmarkEnd w:id="0"/>
```
**Hyperlink Style (required in styles.xml):**
```xml
<w:style w:type="character" w:styleId="Hyperlink">
<w:name w:val="Hyperlink"/>
<w:basedOn w:val="DefaultParagraphFont"/>
<w:uiPriority w:val="99"/>
<w:unhideWhenUsed/>
<w:rPr>
<w:color w:val="467886" w:themeColor="hyperlink"/>
<w:u w:val="single"/>
</w:rPr>
</w:style>
```
## Document Library (Python)
Use the Document class from `scripts/document.py` for all tracked changes and comments. It automatically handles infrastructure setup (people.xml, RSIDs, settings.xml, comment files, relationships, content types). Only use direct XML manipulation for complex scenarios not supported by the library.
**Working with Unicode and Entities:**
- **Searching**: Both entity notation and Unicode characters work - `contains="&#8220;Company"` and `contains="\u201cCompany"` find the same text
- **Replacing**: Use either entities (`&#8220;`) or Unicode (`\u201c`) - both work and will be converted appropriately based on the file's encoding (ascii → entities, utf-8 → Unicode)
### Initialization
**Find the docx skill root** (directory containing `scripts/` and `ooxml/`):
```bash
# Search for document.py to locate the skill root
# Note: /mnt/skills is used here as an example; check your context for the actual location
find /mnt/skills -name "document.py" -path "*/docx/scripts/*" 2>/dev/null | head -1
# Example output: /mnt/skills/docx/scripts/document.py
# Skill root is: /mnt/skills/docx
```
**Run your script with PYTHONPATH** set to the docx skill root:
```bash
PYTHONPATH=/mnt/skills/docx python your_script.py
```
**In your script**, import from the skill root:
```python
from scripts.document import Document, DocxXMLEditor
# Basic initialization (automatically creates temp copy and sets up infrastructure)
doc = Document('unpacked')
# Customize author and initials
doc = Document('unpacked', author="John Doe", initials="JD")
# Enable track revisions mode
doc = Document('unpacked', track_revisions=True)
# Specify custom RSID (auto-generated if not provided)
doc = Document('unpacked', rsid="07DC5ECB")
```
### Creating Tracked Changes
**CRITICAL**: Only mark text that actually changes. Keep ALL unchanged text outside `<w:del>`/`<w:ins>` tags. Marking unchanged text makes edits unprofessional and harder to review.
**Attribute Handling**: The Document class auto-injects attributes (w:id, w:date, w:rsidR, w:rsidDel, w16du:dateUtc, xml:space) into new elements. When preserving unchanged text from the original document, copy the original `<w:r>` element with its existing attributes to maintain document integrity.
**Method Selection Guide**:
- **Adding your own changes to regular text**: Use `replace_node()` with `<w:del>`/`<w:ins>` tags, or `suggest_deletion()` for removing entire `<w:r>` or `<w:p>` elements
- **Partially modifying another author's tracked change**: Use `replace_node()` to nest your changes inside their `<w:ins>`/`<w:del>`
- **Completely rejecting another author's insertion**: Use `revert_insertion()` on the `<w:ins>` element (NOT `suggest_deletion()`)
- **Completely rejecting another author's deletion**: Use `revert_deletion()` on the `<w:del>` element to restore deleted content using tracked changes
```python
# Minimal edit - change one word: "The report is monthly" → "The report is quarterly"
# Original: <w:r w:rsidR="00AB12CD"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>The report is monthly</w:t></w:r>
node = doc["word/document.xml"].get_node(tag="w:r", contains="The report is monthly")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:r w:rsidR="00AB12CD">{rpr}<w:t>The report is </w:t></w:r><w:del><w:r>{rpr}<w:delText>monthly</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>quarterly</w:t></w:r></w:ins>'
doc["word/document.xml"].replace_node(node, replacement)
# Minimal edit - change number: "within 30 days" → "within 45 days"
# Original: <w:r w:rsidR="00XYZ789"><w:rPr><w:rFonts w:ascii="Calibri"/></w:rPr><w:t>within 30 days</w:t></w:r>
node = doc["word/document.xml"].get_node(tag="w:r", contains="within 30 days")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:r w:rsidR="00XYZ789">{rpr}<w:t>within </w:t></w:r><w:del><w:r>{rpr}<w:delText>30</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>45</w:t></w:r></w:ins><w:r w:rsidR="00XYZ789">{rpr}<w:t> days</w:t></w:r>'
doc["word/document.xml"].replace_node(node, replacement)
# Complete replacement - preserve formatting even when replacing all text
node = doc["word/document.xml"].get_node(tag="w:r", contains="apple")
rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else ""
replacement = f'<w:del><w:r>{rpr}<w:delText>apple</w:delText></w:r></w:del><w:ins><w:r>{rpr}<w:t>banana orange</w:t></w:r></w:ins>'
doc["word/document.xml"].replace_node(node, replacement)
# Insert new content (no attributes needed - auto-injected)
node = doc["word/document.xml"].get_node(tag="w:r", contains="existing text")
doc["word/document.xml"].insert_after(node, '<w:ins><w:r><w:t>new text</w:t></w:r></w:ins>')
# Partially delete another author's insertion
# Original: <w:ins w:author="Jane Smith" w:date="..."><w:r><w:t>quarterly financial report</w:t></w:r></w:ins>
# Goal: Delete only "financial" to make it "quarterly report"
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
# IMPORTANT: Preserve w:author="Jane Smith" on the outer <w:ins> to maintain authorship
replacement = '''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
<w:r><w:t>quarterly </w:t></w:r>
<w:del><w:r><w:delText>financial </w:delText></w:r></w:del>
<w:r><w:t>report</w:t></w:r>
</w:ins>'''
doc["word/document.xml"].replace_node(node, replacement)
# Change part of another author's insertion
# Original: <w:ins w:author="Jane Smith"><w:r><w:t>in silence, safe and sound</w:t></w:r></w:ins>
# Goal: Change "safe and sound" to "soft and unbound"
node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "8"})
replacement = f'''<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
<w:r><w:t>in silence, </w:t></w:r>
</w:ins>
<w:ins>
<w:r><w:t>soft and unbound</w:t></w:r>
</w:ins>
<w:ins w:author="Jane Smith" w:date="2025-01-15T10:00:00Z">
<w:del><w:r><w:delText>safe and sound</w:delText></w:r></w:del>
</w:ins>'''
doc["word/document.xml"].replace_node(node, replacement)
# Delete entire run (use only when deleting all content; use replace_node for partial deletions)
node = doc["word/document.xml"].get_node(tag="w:r", contains="text to delete")
doc["word/document.xml"].suggest_deletion(node)
# Delete entire paragraph (in-place, handles both regular and numbered list paragraphs)
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph to delete")
doc["word/document.xml"].suggest_deletion(para)
# Add new numbered list item
target_para = doc["word/document.xml"].get_node(tag="w:p", contains="existing list item")
pPr = tags[0].toxml() if (tags := target_para.getElementsByTagName("w:pPr")) else ""
new_item = f'<w:p>{pPr}<w:r><w:t>New item</w:t></w:r></w:p>'
tracked_para = DocxXMLEditor.suggest_paragraph(new_item)
doc["word/document.xml"].insert_after(target_para, tracked_para)
# Optional: add spacing paragraph before content for better visual separation
# spacing = DocxXMLEditor.suggest_paragraph('<w:p><w:pPr><w:pStyle w:val="ListParagraph"/></w:pPr></w:p>')
# doc["word/document.xml"].insert_after(target_para, spacing + tracked_para)
```
### Adding Comments
```python
# Add comment spanning two existing tracked changes
# Note: w:id is auto-generated. Only search by w:id if you know it from XML inspection
start_node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})
end_node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "2"})
doc.add_comment(start=start_node, end=end_node, text="Explanation of this change")
# Add comment on a paragraph
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
doc.add_comment(start=para, end=para, text="Comment on this paragraph")
# Add comment on newly created tracked change
# First create the tracked change
node = doc["word/document.xml"].get_node(tag="w:r", contains="old")
new_nodes = doc["word/document.xml"].replace_node(
node,
'<w:del><w:r><w:delText>old</w:delText></w:r></w:del><w:ins><w:r><w:t>new</w:t></w:r></w:ins>'
)
# Then add comment on the newly created elements
# new_nodes[0] is the <w:del>, new_nodes[1] is the <w:ins>
doc.add_comment(start=new_nodes[0], end=new_nodes[1], text="Changed old to new per requirements")
# Reply to existing comment
doc.reply_to_comment(parent_comment_id=0, text="I agree with this change")
```
### Rejecting Tracked Changes
**IMPORTANT**: Use `revert_insertion()` to reject insertions and `revert_deletion()` to restore deletions using tracked changes. Use `suggest_deletion()` only for regular unmarked content.
```python
# Reject insertion (wraps it in deletion)
# Use this when another author inserted text that you want to delete
ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"})
nodes = doc["word/document.xml"].revert_insertion(ins) # Returns [ins]
# Reject deletion (creates insertion to restore deleted content)
# Use this when another author deleted text that you want to restore
del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"})
nodes = doc["word/document.xml"].revert_deletion(del_elem) # Returns [del_elem, new_ins]
# Reject all insertions in a paragraph
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
nodes = doc["word/document.xml"].revert_insertion(para) # Returns [para]
# Reject all deletions in a paragraph
para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph text")
nodes = doc["word/document.xml"].revert_deletion(para) # Returns [para]
```
### Inserting Images
**CRITICAL**: The Document class works with a temporary copy at `doc.unpacked_path`. Always copy images to this temp directory, not the original unpacked folder.
```python
from PIL import Image
import shutil, os
# Initialize document first
doc = Document('unpacked')
# Copy image and calculate full-width dimensions with aspect ratio
media_dir = os.path.join(doc.unpacked_path, 'word/media')
os.makedirs(media_dir, exist_ok=True)
shutil.copy('image.png', os.path.join(media_dir, 'image1.png'))
img = Image.open(os.path.join(media_dir, 'image1.png'))
width_emus = int(6.5 * 914400) # 6.5" usable width, 914400 EMUs/inch
height_emus = int(width_emus * img.size[1] / img.size[0])
# Add relationship and content type
rels_editor = doc['word/_rels/document.xml.rels']
next_rid = rels_editor.get_next_rid()
rels_editor.append_to(rels_editor.dom.documentElement,
f'<Relationship Id="{next_rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/image1.png"/>')
doc['[Content_Types].xml'].append_to(doc['[Content_Types].xml'].dom.documentElement,
'<Default Extension="png" ContentType="image/png"/>')
# Insert image
node = doc["word/document.xml"].get_node(tag="w:p", line_number=100)
doc["word/document.xml"].insert_after(node, f'''<w:p>
<w:r>
<w:drawing>
<wp:inline distT="0" distB="0" distL="0" distR="0">
<wp:extent cx="{width_emus}" cy="{height_emus}"/>
<wp:docPr id="1" name="Picture 1"/>
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
<pic:nvPicPr><pic:cNvPr id="1" name="image1.png"/><pic:cNvPicPr/></pic:nvPicPr>
<pic:blipFill><a:blip r:embed="{next_rid}"/><a:stretch><a:fillRect/></a:stretch></pic:blipFill>
<pic:spPr><a:xfrm><a:ext cx="{width_emus}" cy="{height_emus}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></pic:spPr>
</pic:pic>
</a:graphicData>
</a:graphic>
</wp:inline>
</w:drawing>
</w:r>
</w:p>''')
```
### Getting Nodes
```python
# By text content
node = doc["word/document.xml"].get_node(tag="w:p", contains="specific text")
# By line range
para = doc["word/document.xml"].get_node(tag="w:p", line_number=range(100, 150))
# By attributes
node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"})
# By exact line number (must be line number where tag opens)
para = doc["word/document.xml"].get_node(tag="w:p", line_number=42)
# Combine filters
node = doc["word/document.xml"].get_node(tag="w:r", line_number=range(40, 60), contains="text")
# Disambiguate when text appears multiple times - add line_number range
node = doc["word/document.xml"].get_node(tag="w:r", contains="Section", line_number=range(2400, 2500))
```
### Saving
```python
# Save with automatic validation (copies back to original directory)
doc.save() # Validates by default, raises error if validation fails
# Save to different location
doc.save('modified-unpacked')
# Skip validation (debugging only - needing this in production indicates XML issues)
doc.save(validate=False)
```
### Direct DOM Manipulation
For complex scenarios not covered by the library:
```python
# Access any XML file
editor = doc["word/document.xml"]
editor = doc["word/comments.xml"]
# Direct DOM access (defusedxml.minidom.Document)
node = doc["word/document.xml"].get_node(tag="w:p", line_number=5)
parent = node.parentNode
parent.removeChild(node)
parent.appendChild(node) # Move to end
# General document manipulation (without tracked changes)
old_node = doc["word/document.xml"].get_node(tag="w:p", contains="original text")
doc["word/document.xml"].replace_node(old_node, "<w:p><w:r><w:t>replacement text</w:t></w:r></w:p>")
# Multiple insertions - use return value to maintain order
node = doc["word/document.xml"].get_node(tag="w:r", line_number=100)
nodes = doc["word/document.xml"].insert_after(node, "<w:r><w:t>A</w:t></w:r>")
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>B</w:t></w:r>")
nodes = doc["word/document.xml"].insert_after(nodes[-1], "<w:r><w:t>C</w:t></w:r>")
# Results in: original_node, A, B, C
```
## Tracked Changes (Redlining)
**Use the Document class above for all tracked changes.** The patterns below are for reference when constructing replacement XML strings.
### Validation Rules
The validator checks that the document text matches the original after reverting Claude's changes. This means:
- **NEVER modify text inside another author's `<w:ins>` or `<w:del>` tags**
- **ALWAYS use nested deletions** to remove another author's insertions
- **Every edit must be properly tracked** with `<w:ins>` or `<w:del>` tags
### Tracked Change Patterns
**CRITICAL RULES**:
1. Never modify the content inside another author's tracked changes. Always use nested deletions.
2. **XML Structure**: Always place `<w:del>` and `<w:ins>` at paragraph level containing complete `<w:r>` elements. Never nest inside `<w:r>` elements - this creates invalid XML that breaks document processing.
**Text Insertion:**
```xml
<w:ins w:id="1" w:author="Claude" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
<w:r w:rsidR="00792858">
<w:t>inserted text</w:t>
</w:r>
</w:ins>
```
**Text Deletion:**
```xml
<w:del w:id="2" w:author="Claude" w:date="2025-07-30T23:05:00Z" w16du:dateUtc="2025-07-31T06:05:00Z">
<w:r w:rsidDel="00792858">
<w:delText>deleted text</w:delText>
</w:r>
</w:del>
```
**Deleting Another Author's Insertion (MUST use nested structure):**
```xml
<!-- Nest deletion inside the original insertion -->
<w:ins w:author="Jane Smith" w:id="16">
<w:del w:author="Claude" w:id="40">
<w:r><w:delText>monthly</w:delText></w:r>
</w:del>
</w:ins>
<w:ins w:author="Claude" w:id="41">
<w:r><w:t>weekly</w:t></w:r>
</w:ins>
```
**Restoring Another Author's Deletion:**
```xml
<!-- Leave their deletion unchanged, add new insertion after it -->
<w:del w:author="Jane Smith" w:id="50">
<w:r><w:delText>within 30 days</w:delText></w:r>
</w:del>
<w:ins w:author="Claude" w:id="51">
<w:r><w:t>within 30 days</w:t></w:r>
</w:ins>
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:complexType name="CT_ShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Shape">
<xsd:sequence>
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_ConnectorNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Connector">
<xsd:sequence>
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_PictureNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Picture">
<xsd:sequence>
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GraphicFrameNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GraphicFrame">
<xsd:sequence>
<xsd:element name="nvGraphicFramePr" type="CT_GraphicFrameNonVisual" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GroupShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupShape">
<xsd:sequence>
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_ObjectChoices">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:choice>
</xsd:sequence>
</xsd:group>
<xsd:simpleType name="ST_MarkerCoordinate">
<xsd:restriction base="xsd:double">
<xsd:minInclusive value="0.0"/>
<xsd:maxInclusive value="1.0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Marker">
<xsd:sequence>
<xsd:element name="x" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
<xsd:element name="y" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_RelSizeAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="to" type="CT_Marker"/>
<xsd:group ref="EG_ObjectChoices"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AbsSizeAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
<xsd:group ref="EG_ObjectChoices"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_Anchor">
<xsd:choice>
<xsd:element name="relSizeAnchor" type="CT_RelSizeAnchor"/>
<xsd:element name="absSizeAnchor" type="CT_AbsSizeAnchor"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_Drawing">
<xsd:sequence>
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
elementFormDefault="qualified"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:element name="lockedCanvas" type="a:CT_GvmlGroupShape"/>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/picture"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" elementFormDefault="qualified"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/picture">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:complexType name="CT_PictureNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Picture">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:schema>

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:import schemaLocation="shared-relationshipReference.xsd"
namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="to" type="CT_Marker"/>
<xsd:complexType name="CT_AnchorClientData">
<xsd:attribute name="fLocksWithSheet" type="xsd:boolean" use="optional" default="true"/>
<xsd:attribute name="fPrintsWithSheet" type="xsd:boolean" use="optional" default="true"/>
</xsd:complexType>
<xsd:complexType name="CT_ShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Shape">
<xsd:sequence>
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_ConnectorNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Connector">
<xsd:sequence>
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_PictureNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Picture">
<xsd:sequence>
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GraphicalObjectFrameNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GraphicalObjectFrame">
<xsd:sequence>
<xsd:element name="nvGraphicFramePr" type="CT_GraphicalObjectFrameNonVisual" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GroupShapeNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupShape">
<xsd:sequence>
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicalObjectFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_ObjectChoices">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="sp" type="CT_Shape"/>
<xsd:element name="grpSp" type="CT_GroupShape"/>
<xsd:element name="graphicFrame" type="CT_GraphicalObjectFrame"/>
<xsd:element name="cxnSp" type="CT_Connector"/>
<xsd:element name="pic" type="CT_Picture"/>
<xsd:element name="contentPart" type="CT_Rel"/>
</xsd:choice>
</xsd:sequence>
</xsd:group>
<xsd:complexType name="CT_Rel">
<xsd:attribute ref="r:id" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_ColID">
<xsd:restriction base="xsd:int">
<xsd:minInclusive value="0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_RowID">
<xsd:restriction base="xsd:int">
<xsd:minInclusive value="0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Marker">
<xsd:sequence>
<xsd:element name="col" type="ST_ColID"/>
<xsd:element name="colOff" type="a:ST_Coordinate"/>
<xsd:element name="row" type="ST_RowID"/>
<xsd:element name="rowOff" type="a:ST_Coordinate"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="ST_EditAs">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="twoCell"/>
<xsd:enumeration value="oneCell"/>
<xsd:enumeration value="absolute"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_TwoCellAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="to" type="CT_Marker"/>
<xsd:group ref="EG_ObjectChoices"/>
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="editAs" type="ST_EditAs" use="optional" default="twoCell"/>
</xsd:complexType>
<xsd:complexType name="CT_OneCellAnchor">
<xsd:sequence>
<xsd:element name="from" type="CT_Marker"/>
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
<xsd:group ref="EG_ObjectChoices"/>
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AbsoluteAnchor">
<xsd:sequence>
<xsd:element name="pos" type="a:CT_Point2D"/>
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
<xsd:group ref="EG_ObjectChoices"/>
<xsd:element name="clientData" type="CT_AnchorClientData" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_Anchor">
<xsd:choice>
<xsd:element name="twoCellAnchor" type="CT_TwoCellAnchor"/>
<xsd:element name="oneCellAnchor" type="CT_OneCellAnchor"/>
<xsd:element name="absoluteAnchor" type="CT_AbsoluteAnchor"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_Drawing">
<xsd:sequence>
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="wsDr" type="CT_Drawing"/>
</xsd:schema>

View File

@ -0,0 +1,287 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:dpct="http://schemas.openxmlformats.org/drawingml/2006/picture"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
schemaLocation="dml-main.xsd"/>
<xsd:import schemaLocation="wml.xsd"
namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/picture"
schemaLocation="dml-picture.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
schemaLocation="shared-relationshipReference.xsd"/>
<xsd:complexType name="CT_EffectExtent">
<xsd:attribute name="l" type="a:ST_Coordinate" use="required"/>
<xsd:attribute name="t" type="a:ST_Coordinate" use="required"/>
<xsd:attribute name="r" type="a:ST_Coordinate" use="required"/>
<xsd:attribute name="b" type="a:ST_Coordinate" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_WrapDistance">
<xsd:restriction base="xsd:unsignedInt"/>
</xsd:simpleType>
<xsd:complexType name="CT_Inline">
<xsd:sequence>
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="0" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_WrapText">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="bothSides"/>
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="largest"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_WrapPath">
<xsd:sequence>
<xsd:element name="start" type="a:CT_Point2D" minOccurs="1" maxOccurs="1"/>
<xsd:element name="lineTo" type="a:CT_Point2D" minOccurs="2" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="edited" type="xsd:boolean" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapNone"/>
<xsd:complexType name="CT_WrapSquare">
<xsd:sequence>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapTight">
<xsd:sequence>
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapThrough">
<xsd:sequence>
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_WrapTopBottom">
<xsd:sequence>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
</xsd:complexType>
<xsd:group name="EG_WrapType">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="wrapNone" type="CT_WrapNone" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapSquare" type="CT_WrapSquare" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapTight" type="CT_WrapTight" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapThrough" type="CT_WrapThrough" minOccurs="1" maxOccurs="1"/>
<xsd:element name="wrapTopAndBottom" type="CT_WrapTopBottom" minOccurs="1" maxOccurs="1"/>
</xsd:choice>
</xsd:sequence>
</xsd:group>
<xsd:simpleType name="ST_PositionOffset">
<xsd:restriction base="xsd:int"/>
</xsd:simpleType>
<xsd:simpleType name="ST_AlignH">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_RelFromH">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="column"/>
<xsd:enumeration value="character"/>
<xsd:enumeration value="leftMargin"/>
<xsd:enumeration value="rightMargin"/>
<xsd:enumeration value="insideMargin"/>
<xsd:enumeration value="outsideMargin"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_PosH">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="align" type="ST_AlignH" minOccurs="1" maxOccurs="1"/>
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="relativeFrom" type="ST_RelFromH" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_AlignV">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="top"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_RelFromV">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="paragraph"/>
<xsd:enumeration value="line"/>
<xsd:enumeration value="topMargin"/>
<xsd:enumeration value="bottomMargin"/>
<xsd:enumeration value="insideMargin"/>
<xsd:enumeration value="outsideMargin"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_PosV">
<xsd:sequence>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="align" type="ST_AlignV" minOccurs="1" maxOccurs="1"/>
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
</xsd:choice>
</xsd:sequence>
<xsd:attribute name="relativeFrom" type="ST_RelFromV" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_Anchor">
<xsd:sequence>
<xsd:element name="simplePos" type="a:CT_Point2D"/>
<xsd:element name="positionH" type="CT_PosH"/>
<xsd:element name="positionV" type="CT_PosV"/>
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
<xsd:group ref="EG_WrapType"/>
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
minOccurs="0" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
<xsd:attribute name="simplePos" type="xsd:boolean"/>
<xsd:attribute name="relativeHeight" type="xsd:unsignedInt" use="required"/>
<xsd:attribute name="behindDoc" type="xsd:boolean" use="required"/>
<xsd:attribute name="locked" type="xsd:boolean" use="required"/>
<xsd:attribute name="layoutInCell" type="xsd:boolean" use="required"/>
<xsd:attribute name="hidden" type="xsd:boolean" use="optional"/>
<xsd:attribute name="allowOverlap" type="xsd:boolean" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_TxbxContent">
<xsd:group ref="w:EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/>
</xsd:complexType>
<xsd:complexType name="CT_TextboxInfo">
<xsd:sequence>
<xsd:element name="txbxContent" type="CT_TxbxContent" minOccurs="1" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:unsignedShort" use="optional" default="0"/>
</xsd:complexType>
<xsd:complexType name="CT_LinkedTextboxInformation">
<xsd:sequence>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:unsignedShort" use="required"/>
<xsd:attribute name="seq" type="xsd:unsignedShort" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingShape">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="cNvCnPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
maxOccurs="1"/>
</xsd:choice>
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element name="txbx" type="CT_TextboxInfo" minOccurs="1" maxOccurs="1"/>
<xsd:element name="linkedTxbx" type="CT_LinkedTextboxInformation" minOccurs="1"
maxOccurs="1"/>
</xsd:choice>
<xsd:element name="bodyPr" type="a:CT_TextBodyProperties" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="normalEastAsianFlow" type="xsd:boolean" use="optional" default="false"/>
</xsd:complexType>
<xsd:complexType name="CT_GraphicFrame">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
<xsd:element name="cNvFrPr" type="a:CT_NonVisualGraphicFrameProperties" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingContentPartNonVisual">
<xsd:sequence>
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
<xsd:element name="cNvContentPartPr" type="a:CT_NonVisualContentPartProperties" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingContentPart">
<xsd:sequence>
<xsd:element name="nvContentPartPr" type="CT_WordprocessingContentPartNonVisual" minOccurs="0" maxOccurs="1"/>
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="0" maxOccurs="1"/>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="bwMode" type="a:ST_BlackWhiteMode" use="optional"/>
<xsd:attribute ref="r:id" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingGroup">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
maxOccurs="1"/>
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="wsp"/>
<xsd:element name="grpSp" type="CT_WordprocessingGroup"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
<xsd:element ref="dpct:pic"/>
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
</xsd:choice>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_WordprocessingCanvas">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element name="bg" type="a:CT_BackgroundFormatting" minOccurs="0" maxOccurs="1"/>
<xsd:element name="whole" type="a:CT_WholeE2oFormatting" minOccurs="0" maxOccurs="1"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element ref="wsp"/>
<xsd:element ref="dpct:pic"/>
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
<xsd:element ref="wgp"/>
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
</xsd:choice>
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="wpc" type="CT_WordprocessingCanvas"/>
<xsd:element name="wgp" type="CT_WordprocessingGroup"/>
<xsd:element name="wsp" type="CT_WordprocessingShape"/>
<xsd:element name="inline" type="CT_Inline"/>
<xsd:element name="anchor" type="CT_Anchor"/>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/characteristics"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/characteristics"
elementFormDefault="qualified">
<xsd:complexType name="CT_AdditionalCharacteristics">
<xsd:sequence>
<xsd:element name="characteristic" type="CT_Characteristic" minOccurs="0"
maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Characteristic">
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="relation" type="ST_Relation" use="required"/>
<xsd:attribute name="val" type="xsd:string" use="required"/>
<xsd:attribute name="vocabulary" type="xsd:anyURI" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_Relation">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="ge"/>
<xsd:enumeration value="le"/>
<xsd:enumeration value="gt"/>
<xsd:enumeration value="lt"/>
<xsd:enumeration value="eq"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="additionalCharacteristics" type="CT_AdditionalCharacteristics"/>
</xsd:schema>

View File

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/bibliography"
elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:simpleType name="ST_SourceType">
<xsd:restriction base="s:ST_String">
<xsd:enumeration value="ArticleInAPeriodical"/>
<xsd:enumeration value="Book"/>
<xsd:enumeration value="BookSection"/>
<xsd:enumeration value="JournalArticle"/>
<xsd:enumeration value="ConferenceProceedings"/>
<xsd:enumeration value="Report"/>
<xsd:enumeration value="SoundRecording"/>
<xsd:enumeration value="Performance"/>
<xsd:enumeration value="Art"/>
<xsd:enumeration value="DocumentFromInternetSite"/>
<xsd:enumeration value="InternetSite"/>
<xsd:enumeration value="Film"/>
<xsd:enumeration value="Interview"/>
<xsd:enumeration value="Patent"/>
<xsd:enumeration value="ElectronicSource"/>
<xsd:enumeration value="Case"/>
<xsd:enumeration value="Misc"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_NameListType">
<xsd:sequence>
<xsd:element name="Person" type="CT_PersonType" minOccurs="1" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_PersonType">
<xsd:sequence>
<xsd:element name="Last" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="First" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="Middle" type="s:ST_String" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_NameType">
<xsd:sequence>
<xsd:element name="NameList" type="CT_NameListType" minOccurs="1" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_NameOrCorporateType">
<xsd:sequence>
<xsd:choice minOccurs="0" maxOccurs="1">
<xsd:element name="NameList" type="CT_NameListType" minOccurs="1" maxOccurs="1"/>
<xsd:element name="Corporate" minOccurs="1" maxOccurs="1" type="s:ST_String"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AuthorType">
<xsd:sequence>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="Artist" type="CT_NameType"/>
<xsd:element name="Author" type="CT_NameOrCorporateType"/>
<xsd:element name="BookAuthor" type="CT_NameType"/>
<xsd:element name="Compiler" type="CT_NameType"/>
<xsd:element name="Composer" type="CT_NameType"/>
<xsd:element name="Conductor" type="CT_NameType"/>
<xsd:element name="Counsel" type="CT_NameType"/>
<xsd:element name="Director" type="CT_NameType"/>
<xsd:element name="Editor" type="CT_NameType"/>
<xsd:element name="Interviewee" type="CT_NameType"/>
<xsd:element name="Interviewer" type="CT_NameType"/>
<xsd:element name="Inventor" type="CT_NameType"/>
<xsd:element name="Performer" type="CT_NameOrCorporateType"/>
<xsd:element name="ProducerName" type="CT_NameType"/>
<xsd:element name="Translator" type="CT_NameType"/>
<xsd:element name="Writer" type="CT_NameType"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SourceType">
<xsd:sequence>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="AbbreviatedCaseNumber" type="s:ST_String"/>
<xsd:element name="AlbumTitle" type="s:ST_String"/>
<xsd:element name="Author" type="CT_AuthorType"/>
<xsd:element name="BookTitle" type="s:ST_String"/>
<xsd:element name="Broadcaster" type="s:ST_String"/>
<xsd:element name="BroadcastTitle" type="s:ST_String"/>
<xsd:element name="CaseNumber" type="s:ST_String"/>
<xsd:element name="ChapterNumber" type="s:ST_String"/>
<xsd:element name="City" type="s:ST_String"/>
<xsd:element name="Comments" type="s:ST_String"/>
<xsd:element name="ConferenceName" type="s:ST_String"/>
<xsd:element name="CountryRegion" type="s:ST_String"/>
<xsd:element name="Court" type="s:ST_String"/>
<xsd:element name="Day" type="s:ST_String"/>
<xsd:element name="DayAccessed" type="s:ST_String"/>
<xsd:element name="Department" type="s:ST_String"/>
<xsd:element name="Distributor" type="s:ST_String"/>
<xsd:element name="Edition" type="s:ST_String"/>
<xsd:element name="Guid" type="s:ST_String"/>
<xsd:element name="Institution" type="s:ST_String"/>
<xsd:element name="InternetSiteTitle" type="s:ST_String"/>
<xsd:element name="Issue" type="s:ST_String"/>
<xsd:element name="JournalName" type="s:ST_String"/>
<xsd:element name="LCID" type="s:ST_Lang"/>
<xsd:element name="Medium" type="s:ST_String"/>
<xsd:element name="Month" type="s:ST_String"/>
<xsd:element name="MonthAccessed" type="s:ST_String"/>
<xsd:element name="NumberVolumes" type="s:ST_String"/>
<xsd:element name="Pages" type="s:ST_String"/>
<xsd:element name="PatentNumber" type="s:ST_String"/>
<xsd:element name="PeriodicalTitle" type="s:ST_String"/>
<xsd:element name="ProductionCompany" type="s:ST_String"/>
<xsd:element name="PublicationTitle" type="s:ST_String"/>
<xsd:element name="Publisher" type="s:ST_String"/>
<xsd:element name="RecordingNumber" type="s:ST_String"/>
<xsd:element name="RefOrder" type="s:ST_String"/>
<xsd:element name="Reporter" type="s:ST_String"/>
<xsd:element name="SourceType" type="ST_SourceType"/>
<xsd:element name="ShortTitle" type="s:ST_String"/>
<xsd:element name="StandardNumber" type="s:ST_String"/>
<xsd:element name="StateProvince" type="s:ST_String"/>
<xsd:element name="Station" type="s:ST_String"/>
<xsd:element name="Tag" type="s:ST_String"/>
<xsd:element name="Theater" type="s:ST_String"/>
<xsd:element name="ThesisType" type="s:ST_String"/>
<xsd:element name="Title" type="s:ST_String"/>
<xsd:element name="Type" type="s:ST_String"/>
<xsd:element name="URL" type="s:ST_String"/>
<xsd:element name="Version" type="s:ST_String"/>
<xsd:element name="Volume" type="s:ST_String"/>
<xsd:element name="Year" type="s:ST_String"/>
<xsd:element name="YearAccessed" type="s:ST_String"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="Sources" type="CT_Sources"/>
<xsd:complexType name="CT_Sources">
<xsd:sequence>
<xsd:element name="Source" type="CT_SourceType" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="SelectedStyle" type="s:ST_String"/>
<xsd:attribute name="StyleName" type="s:ST_String"/>
<xsd:attribute name="URI" type="s:ST_String"/>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,174 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
elementFormDefault="qualified">
<xsd:simpleType name="ST_Lang">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_HexColorRGB">
<xsd:restriction base="xsd:hexBinary">
<xsd:length value="3" fixed="true"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Panose">
<xsd:restriction base="xsd:hexBinary">
<xsd:length value="10"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_CalendarType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="gregorian"/>
<xsd:enumeration value="gregorianUs"/>
<xsd:enumeration value="gregorianMeFrench"/>
<xsd:enumeration value="gregorianArabic"/>
<xsd:enumeration value="hijri"/>
<xsd:enumeration value="hebrew"/>
<xsd:enumeration value="taiwan"/>
<xsd:enumeration value="japan"/>
<xsd:enumeration value="thai"/>
<xsd:enumeration value="korea"/>
<xsd:enumeration value="saka"/>
<xsd:enumeration value="gregorianXlitEnglish"/>
<xsd:enumeration value="gregorianXlitFrench"/>
<xsd:enumeration value="none"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_AlgClass">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="hash"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_CryptProv">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="rsaAES"/>
<xsd:enumeration value="rsaFull"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_AlgType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="typeAny"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ColorType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_Guid">
<xsd:restriction base="xsd:token">
<xsd:pattern value="\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OnOff">
<xsd:union memberTypes="xsd:boolean ST_OnOff1"/>
</xsd:simpleType>
<xsd:simpleType name="ST_OnOff1">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="on"/>
<xsd:enumeration value="off"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_String">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_XmlName">
<xsd:restriction base="xsd:NCName">
<xsd:minLength value="1"/>
<xsd:maxLength value="255"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_TrueFalse">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="t"/>
<xsd:enumeration value="f"/>
<xsd:enumeration value="true"/>
<xsd:enumeration value="false"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_TrueFalseBlank">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="t"/>
<xsd:enumeration value="f"/>
<xsd:enumeration value="true"/>
<xsd:enumeration value="false"/>
<xsd:enumeration value=""/>
<xsd:enumeration value="True"/>
<xsd:enumeration value="False"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_UnsignedDecimalNumber">
<xsd:restriction base="xsd:decimal">
<xsd:minInclusive value="0"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_TwipsMeasure">
<xsd:union memberTypes="ST_UnsignedDecimalNumber ST_PositiveUniversalMeasure"/>
</xsd:simpleType>
<xsd:simpleType name="ST_VerticalAlignRun">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="baseline"/>
<xsd:enumeration value="superscript"/>
<xsd:enumeration value="subscript"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Xstring">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_XAlign">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="left"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_YAlign">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="inline"/>
<xsd:enumeration value="top"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="inside"/>
<xsd:enumeration value="outside"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ConformanceClass">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="strict"/>
<xsd:enumeration value="transitional"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_UniversalMeasure">
<xsd:restriction base="xsd:string">
<xsd:pattern value="-?[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PositiveUniversalMeasure">
<xsd:restriction base="ST_UniversalMeasure">
<xsd:pattern value="[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Percentage">
<xsd:restriction base="xsd:string">
<xsd:pattern value="-?[0-9]+(\.[0-9]+)?%"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FixedPercentage">
<xsd:restriction base="ST_Percentage">
<xsd:pattern value="-?((100)|([0-9][0-9]?))(\.[0-9][0-9]?)?%"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PositivePercentage">
<xsd:restriction base="ST_Percentage">
<xsd:pattern value="[0-9]+(\.[0-9]+)?%"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PositiveFixedPercentage">
<xsd:restriction base="ST_Percentage">
<xsd:pattern value="((100)|([0-9][0-9]?))(\.[0-9][0-9]?)?%"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/customXml"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/customXml"
elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:complexType name="CT_DatastoreSchemaRef">
<xsd:attribute name="uri" type="xsd:string" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_DatastoreSchemaRefs">
<xsd:sequence>
<xsd:element name="schemaRef" type="CT_DatastoreSchemaRef" minOccurs="0" maxOccurs="unbounded"
/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_DatastoreItem">
<xsd:sequence>
<xsd:element name="schemaRefs" type="CT_DatastoreSchemaRefs" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="itemID" type="s:ST_Guid" use="required"/>
</xsd:complexType>
<xsd:element name="datastoreItem" type="CT_DatastoreItem"/>
</xsd:schema>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
targetNamespace="http://schemas.openxmlformats.org/schemaLibrary/2006/main"
attributeFormDefault="qualified" elementFormDefault="qualified">
<xsd:complexType name="CT_Schema">
<xsd:attribute name="uri" type="xsd:string" default=""/>
<xsd:attribute name="manifestLocation" type="xsd:string"/>
<xsd:attribute name="schemaLocation" type="xsd:string"/>
<xsd:attribute name="schemaLanguage" type="xsd:token"/>
</xsd:complexType>
<xsd:complexType name="CT_SchemaLibrary">
<xsd:sequence>
<xsd:element name="schema" type="CT_Schema" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="schemaLibrary" type="CT_SchemaLibrary"/>
</xsd:schema>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties"
blockDefault="#all" elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
schemaLocation="shared-documentPropertiesVariantTypes.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:element name="Properties" type="CT_Properties"/>
<xsd:complexType name="CT_Properties">
<xsd:sequence>
<xsd:element name="property" minOccurs="0" maxOccurs="unbounded" type="CT_Property"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Property">
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:vector"/>
<xsd:element ref="vt:array"/>
<xsd:element ref="vt:blob"/>
<xsd:element ref="vt:oblob"/>
<xsd:element ref="vt:empty"/>
<xsd:element ref="vt:null"/>
<xsd:element ref="vt:i1"/>
<xsd:element ref="vt:i2"/>
<xsd:element ref="vt:i4"/>
<xsd:element ref="vt:i8"/>
<xsd:element ref="vt:int"/>
<xsd:element ref="vt:ui1"/>
<xsd:element ref="vt:ui2"/>
<xsd:element ref="vt:ui4"/>
<xsd:element ref="vt:ui8"/>
<xsd:element ref="vt:uint"/>
<xsd:element ref="vt:r4"/>
<xsd:element ref="vt:r8"/>
<xsd:element ref="vt:decimal"/>
<xsd:element ref="vt:lpstr"/>
<xsd:element ref="vt:lpwstr"/>
<xsd:element ref="vt:bstr"/>
<xsd:element ref="vt:date"/>
<xsd:element ref="vt:filetime"/>
<xsd:element ref="vt:bool"/>
<xsd:element ref="vt:cy"/>
<xsd:element ref="vt:error"/>
<xsd:element ref="vt:stream"/>
<xsd:element ref="vt:ostream"/>
<xsd:element ref="vt:storage"/>
<xsd:element ref="vt:ostorage"/>
<xsd:element ref="vt:vstream"/>
<xsd:element ref="vt:clsid"/>
</xsd:choice>
<xsd:attribute name="fmtid" use="required" type="s:ST_Guid"/>
<xsd:attribute name="pid" use="required" type="xsd:int"/>
<xsd:attribute name="name" use="optional" type="xsd:string"/>
<xsd:attribute name="linkTarget" use="optional" type="xsd:string"/>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties"
elementFormDefault="qualified" blockDefault="#all">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
schemaLocation="shared-documentPropertiesVariantTypes.xsd"/>
<xsd:element name="Properties" type="CT_Properties"/>
<xsd:complexType name="CT_Properties">
<xsd:all>
<xsd:element name="Template" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Manager" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Company" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Pages" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Words" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Characters" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="PresentationFormat" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="Lines" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Paragraphs" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Slides" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="Notes" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="TotalTime" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="HiddenSlides" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="MMClips" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="ScaleCrop" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="HeadingPairs" minOccurs="0" maxOccurs="1" type="CT_VectorVariant"/>
<xsd:element name="TitlesOfParts" minOccurs="0" maxOccurs="1" type="CT_VectorLpstr"/>
<xsd:element name="LinksUpToDate" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="CharactersWithSpaces" minOccurs="0" maxOccurs="1" type="xsd:int"/>
<xsd:element name="SharedDoc" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="HyperlinkBase" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="HLinks" minOccurs="0" maxOccurs="1" type="CT_VectorVariant"/>
<xsd:element name="HyperlinksChanged" minOccurs="0" maxOccurs="1" type="xsd:boolean"/>
<xsd:element name="DigSig" minOccurs="0" maxOccurs="1" type="CT_DigSigBlob"/>
<xsd:element name="Application" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="AppVersion" minOccurs="0" maxOccurs="1" type="xsd:string"/>
<xsd:element name="DocSecurity" minOccurs="0" maxOccurs="1" type="xsd:int"/>
</xsd:all>
</xsd:complexType>
<xsd:complexType name="CT_VectorVariant">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:vector"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_VectorLpstr">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:vector"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_DigSigBlob">
<xsd:sequence minOccurs="1" maxOccurs="1">
<xsd:element ref="vt:blob"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,195 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
blockDefault="#all" elementFormDefault="qualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:simpleType name="ST_VectorBaseType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="variant"/>
<xsd:enumeration value="i1"/>
<xsd:enumeration value="i2"/>
<xsd:enumeration value="i4"/>
<xsd:enumeration value="i8"/>
<xsd:enumeration value="ui1"/>
<xsd:enumeration value="ui2"/>
<xsd:enumeration value="ui4"/>
<xsd:enumeration value="ui8"/>
<xsd:enumeration value="r4"/>
<xsd:enumeration value="r8"/>
<xsd:enumeration value="lpstr"/>
<xsd:enumeration value="lpwstr"/>
<xsd:enumeration value="bstr"/>
<xsd:enumeration value="date"/>
<xsd:enumeration value="filetime"/>
<xsd:enumeration value="bool"/>
<xsd:enumeration value="cy"/>
<xsd:enumeration value="error"/>
<xsd:enumeration value="clsid"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ArrayBaseType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="variant"/>
<xsd:enumeration value="i1"/>
<xsd:enumeration value="i2"/>
<xsd:enumeration value="i4"/>
<xsd:enumeration value="int"/>
<xsd:enumeration value="ui1"/>
<xsd:enumeration value="ui2"/>
<xsd:enumeration value="ui4"/>
<xsd:enumeration value="uint"/>
<xsd:enumeration value="r4"/>
<xsd:enumeration value="r8"/>
<xsd:enumeration value="decimal"/>
<xsd:enumeration value="bstr"/>
<xsd:enumeration value="date"/>
<xsd:enumeration value="bool"/>
<xsd:enumeration value="cy"/>
<xsd:enumeration value="error"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Cy">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\s*[0-9]*\.[0-9]{4}\s*"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Error">
<xsd:restriction base="xsd:string">
<xsd:pattern value="\s*0x[0-9A-Za-z]{8}\s*"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Empty"/>
<xsd:complexType name="CT_Null"/>
<xsd:complexType name="CT_Vector">
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="variant"/>
<xsd:element ref="i1"/>
<xsd:element ref="i2"/>
<xsd:element ref="i4"/>
<xsd:element ref="i8"/>
<xsd:element ref="ui1"/>
<xsd:element ref="ui2"/>
<xsd:element ref="ui4"/>
<xsd:element ref="ui8"/>
<xsd:element ref="r4"/>
<xsd:element ref="r8"/>
<xsd:element ref="lpstr"/>
<xsd:element ref="lpwstr"/>
<xsd:element ref="bstr"/>
<xsd:element ref="date"/>
<xsd:element ref="filetime"/>
<xsd:element ref="bool"/>
<xsd:element ref="cy"/>
<xsd:element ref="error"/>
<xsd:element ref="clsid"/>
</xsd:choice>
<xsd:attribute name="baseType" type="ST_VectorBaseType" use="required"/>
<xsd:attribute name="size" type="xsd:unsignedInt" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_Array">
<xsd:choice minOccurs="1" maxOccurs="unbounded">
<xsd:element ref="variant"/>
<xsd:element ref="i1"/>
<xsd:element ref="i2"/>
<xsd:element ref="i4"/>
<xsd:element ref="int"/>
<xsd:element ref="ui1"/>
<xsd:element ref="ui2"/>
<xsd:element ref="ui4"/>
<xsd:element ref="uint"/>
<xsd:element ref="r4"/>
<xsd:element ref="r8"/>
<xsd:element ref="decimal"/>
<xsd:element ref="bstr"/>
<xsd:element ref="date"/>
<xsd:element ref="bool"/>
<xsd:element ref="error"/>
<xsd:element ref="cy"/>
</xsd:choice>
<xsd:attribute name="lBounds" type="xsd:int" use="required"/>
<xsd:attribute name="uBounds" type="xsd:int" use="required"/>
<xsd:attribute name="baseType" type="ST_ArrayBaseType" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_Variant">
<xsd:choice minOccurs="1" maxOccurs="1">
<xsd:element ref="variant"/>
<xsd:element ref="vector"/>
<xsd:element ref="array"/>
<xsd:element ref="blob"/>
<xsd:element ref="oblob"/>
<xsd:element ref="empty"/>
<xsd:element ref="null"/>
<xsd:element ref="i1"/>
<xsd:element ref="i2"/>
<xsd:element ref="i4"/>
<xsd:element ref="i8"/>
<xsd:element ref="int"/>
<xsd:element ref="ui1"/>
<xsd:element ref="ui2"/>
<xsd:element ref="ui4"/>
<xsd:element ref="ui8"/>
<xsd:element ref="uint"/>
<xsd:element ref="r4"/>
<xsd:element ref="r8"/>
<xsd:element ref="decimal"/>
<xsd:element ref="lpstr"/>
<xsd:element ref="lpwstr"/>
<xsd:element ref="bstr"/>
<xsd:element ref="date"/>
<xsd:element ref="filetime"/>
<xsd:element ref="bool"/>
<xsd:element ref="cy"/>
<xsd:element ref="error"/>
<xsd:element ref="stream"/>
<xsd:element ref="ostream"/>
<xsd:element ref="storage"/>
<xsd:element ref="ostorage"/>
<xsd:element ref="vstream"/>
<xsd:element ref="clsid"/>
</xsd:choice>
</xsd:complexType>
<xsd:complexType name="CT_Vstream">
<xsd:simpleContent>
<xsd:extension base="xsd:base64Binary">
<xsd:attribute name="version" type="s:ST_Guid"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:element name="variant" type="CT_Variant"/>
<xsd:element name="vector" type="CT_Vector"/>
<xsd:element name="array" type="CT_Array"/>
<xsd:element name="blob" type="xsd:base64Binary"/>
<xsd:element name="oblob" type="xsd:base64Binary"/>
<xsd:element name="empty" type="CT_Empty"/>
<xsd:element name="null" type="CT_Null"/>
<xsd:element name="i1" type="xsd:byte"/>
<xsd:element name="i2" type="xsd:short"/>
<xsd:element name="i4" type="xsd:int"/>
<xsd:element name="i8" type="xsd:long"/>
<xsd:element name="int" type="xsd:int"/>
<xsd:element name="ui1" type="xsd:unsignedByte"/>
<xsd:element name="ui2" type="xsd:unsignedShort"/>
<xsd:element name="ui4" type="xsd:unsignedInt"/>
<xsd:element name="ui8" type="xsd:unsignedLong"/>
<xsd:element name="uint" type="xsd:unsignedInt"/>
<xsd:element name="r4" type="xsd:float"/>
<xsd:element name="r8" type="xsd:double"/>
<xsd:element name="decimal" type="xsd:decimal"/>
<xsd:element name="lpstr" type="xsd:string"/>
<xsd:element name="lpwstr" type="xsd:string"/>
<xsd:element name="bstr" type="xsd:string"/>
<xsd:element name="date" type="xsd:dateTime"/>
<xsd:element name="filetime" type="xsd:dateTime"/>
<xsd:element name="bool" type="xsd:boolean"/>
<xsd:element name="cy" type="ST_Cy"/>
<xsd:element name="error" type="ST_Error"/>
<xsd:element name="stream" type="xsd:base64Binary"/>
<xsd:element name="ostream" type="xsd:base64Binary"/>
<xsd:element name="storage" type="xsd:base64Binary"/>
<xsd:element name="ostorage" type="xsd:base64Binary"/>
<xsd:element name="vstream" type="CT_Vstream"/>
<xsd:element name="clsid" type="s:ST_Guid"/>
</xsd:schema>

View File

@ -0,0 +1,582 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/math">
<xsd:import namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
schemaLocation="wml.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" schemaLocation="xml.xsd"/>
<xsd:simpleType name="ST_Integer255">
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="1"/>
<xsd:maxInclusive value="255"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Integer255">
<xsd:attribute name="val" type="ST_Integer255" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Integer2">
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="-2"/>
<xsd:maxInclusive value="2"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Integer2">
<xsd:attribute name="val" type="ST_Integer2" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_SpacingRule">
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="4"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_SpacingRule">
<xsd:attribute name="val" type="ST_SpacingRule" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_UnSignedInteger">
<xsd:restriction base="xsd:unsignedInt"/>
</xsd:simpleType>
<xsd:complexType name="CT_UnSignedInteger">
<xsd:attribute name="val" type="ST_UnSignedInteger" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Char">
<xsd:restriction base="xsd:string">
<xsd:maxLength value="1"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Char">
<xsd:attribute name="val" type="ST_Char" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_OnOff">
<xsd:attribute name="val" type="s:ST_OnOff"/>
</xsd:complexType>
<xsd:complexType name="CT_String">
<xsd:attribute name="val" type="s:ST_String"/>
</xsd:complexType>
<xsd:complexType name="CT_XAlign">
<xsd:attribute name="val" type="s:ST_XAlign" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_YAlign">
<xsd:attribute name="val" type="s:ST_YAlign" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Shp">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="centered"/>
<xsd:enumeration value="match"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Shp">
<xsd:attribute name="val" type="ST_Shp" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_FType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="bar"/>
<xsd:enumeration value="skw"/>
<xsd:enumeration value="lin"/>
<xsd:enumeration value="noBar"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_FType">
<xsd:attribute name="val" type="ST_FType" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_LimLoc">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="undOvr"/>
<xsd:enumeration value="subSup"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_LimLoc">
<xsd:attribute name="val" type="ST_LimLoc" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_TopBot">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="top"/>
<xsd:enumeration value="bot"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_TopBot">
<xsd:attribute name="val" type="ST_TopBot" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_Script">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="roman"/>
<xsd:enumeration value="script"/>
<xsd:enumeration value="fraktur"/>
<xsd:enumeration value="double-struck"/>
<xsd:enumeration value="sans-serif"/>
<xsd:enumeration value="monospace"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Script">
<xsd:attribute name="val" type="ST_Script"/>
</xsd:complexType>
<xsd:simpleType name="ST_Style">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="p"/>
<xsd:enumeration value="b"/>
<xsd:enumeration value="i"/>
<xsd:enumeration value="bi"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Style">
<xsd:attribute name="val" type="ST_Style"/>
</xsd:complexType>
<xsd:complexType name="CT_ManualBreak">
<xsd:attribute name="alnAt" type="ST_Integer255"/>
</xsd:complexType>
<xsd:group name="EG_ScriptStyle">
<xsd:sequence>
<xsd:element name="scr" minOccurs="0" type="CT_Script"/>
<xsd:element name="sty" minOccurs="0" type="CT_Style"/>
</xsd:sequence>
</xsd:group>
<xsd:complexType name="CT_RPR">
<xsd:sequence>
<xsd:element name="lit" minOccurs="0" type="CT_OnOff"/>
<xsd:choice>
<xsd:element name="nor" minOccurs="0" type="CT_OnOff"/>
<xsd:sequence>
<xsd:group ref="EG_ScriptStyle"/>
</xsd:sequence>
</xsd:choice>
<xsd:element name="brk" minOccurs="0" type="CT_ManualBreak"/>
<xsd:element name="aln" minOccurs="0" type="CT_OnOff"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Text">
<xsd:simpleContent>
<xsd:extension base="s:ST_String">
<xsd:attribute ref="xml:space" use="optional"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="CT_R">
<xsd:sequence>
<xsd:element name="rPr" type="CT_RPR" minOccurs="0"/>
<xsd:group ref="w:EG_RPr" minOccurs="0"/>
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:group ref="w:EG_RunInnerContent"/>
<xsd:element name="t" type="CT_Text" minOccurs="0"/>
</xsd:choice>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_CtrlPr">
<xsd:sequence>
<xsd:group ref="w:EG_RPrMath" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_AccPr">
<xsd:sequence>
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Acc">
<xsd:sequence>
<xsd:element name="accPr" type="CT_AccPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BarPr">
<xsd:sequence>
<xsd:element name="pos" type="CT_TopBot" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Bar">
<xsd:sequence>
<xsd:element name="barPr" type="CT_BarPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BoxPr">
<xsd:sequence>
<xsd:element name="opEmu" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="noBreak" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="diff" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="brk" type="CT_ManualBreak" minOccurs="0"/>
<xsd:element name="aln" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Box">
<xsd:sequence>
<xsd:element name="boxPr" type="CT_BoxPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BorderBoxPr">
<xsd:sequence>
<xsd:element name="hideTop" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="hideBot" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="hideLeft" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="hideRight" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeH" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeV" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeBLTR" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="strikeTLBR" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_BorderBox">
<xsd:sequence>
<xsd:element name="borderBoxPr" type="CT_BorderBoxPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_DPr">
<xsd:sequence>
<xsd:element name="begChr" type="CT_Char" minOccurs="0"/>
<xsd:element name="sepChr" type="CT_Char" minOccurs="0"/>
<xsd:element name="endChr" type="CT_Char" minOccurs="0"/>
<xsd:element name="grow" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="shp" type="CT_Shp" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_D">
<xsd:sequence>
<xsd:element name="dPr" type="CT_DPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_EqArrPr">
<xsd:sequence>
<xsd:element name="baseJc" type="CT_YAlign" minOccurs="0"/>
<xsd:element name="maxDist" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="objDist" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="rSpRule" type="CT_SpacingRule" minOccurs="0"/>
<xsd:element name="rSp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_EqArr">
<xsd:sequence>
<xsd:element name="eqArrPr" type="CT_EqArrPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_FPr">
<xsd:sequence>
<xsd:element name="type" type="CT_FType" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_F">
<xsd:sequence>
<xsd:element name="fPr" type="CT_FPr" minOccurs="0"/>
<xsd:element name="num" type="CT_OMathArg"/>
<xsd:element name="den" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_FuncPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Func">
<xsd:sequence>
<xsd:element name="funcPr" type="CT_FuncPr" minOccurs="0"/>
<xsd:element name="fName" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupChrPr">
<xsd:sequence>
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
<xsd:element name="pos" type="CT_TopBot" minOccurs="0"/>
<xsd:element name="vertJc" type="CT_TopBot" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GroupChr">
<xsd:sequence>
<xsd:element name="groupChrPr" type="CT_GroupChrPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimLowPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimLow">
<xsd:sequence>
<xsd:element name="limLowPr" type="CT_LimLowPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="lim" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimUppPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LimUpp">
<xsd:sequence>
<xsd:element name="limUppPr" type="CT_LimUppPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="lim" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MCPr">
<xsd:sequence>
<xsd:element name="count" type="CT_Integer255" minOccurs="0"/>
<xsd:element name="mcJc" type="CT_XAlign" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MC">
<xsd:sequence>
<xsd:element name="mcPr" type="CT_MCPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MCS">
<xsd:sequence>
<xsd:element name="mc" type="CT_MC" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MPr">
<xsd:sequence>
<xsd:element name="baseJc" type="CT_YAlign" minOccurs="0"/>
<xsd:element name="plcHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="rSpRule" type="CT_SpacingRule" minOccurs="0"/>
<xsd:element name="cGpRule" type="CT_SpacingRule" minOccurs="0"/>
<xsd:element name="rSp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="cSp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="cGp" type="CT_UnSignedInteger" minOccurs="0"/>
<xsd:element name="mcs" type="CT_MCS" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_MR">
<xsd:sequence>
<xsd:element name="e" type="CT_OMathArg" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_M">
<xsd:sequence>
<xsd:element name="mPr" type="CT_MPr" minOccurs="0"/>
<xsd:element name="mr" type="CT_MR" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_NaryPr">
<xsd:sequence>
<xsd:element name="chr" type="CT_Char" minOccurs="0"/>
<xsd:element name="limLoc" type="CT_LimLoc" minOccurs="0"/>
<xsd:element name="grow" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="subHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="supHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Nary">
<xsd:sequence>
<xsd:element name="naryPr" type="CT_NaryPr" minOccurs="0"/>
<xsd:element name="sub" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_PhantPr">
<xsd:sequence>
<xsd:element name="show" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="zeroWid" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="zeroAsc" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="zeroDesc" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="transp" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Phant">
<xsd:sequence>
<xsd:element name="phantPr" type="CT_PhantPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_RadPr">
<xsd:sequence>
<xsd:element name="degHide" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Rad">
<xsd:sequence>
<xsd:element name="radPr" type="CT_RadPr" minOccurs="0"/>
<xsd:element name="deg" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SPrePr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SPre">
<xsd:sequence>
<xsd:element name="sPrePr" type="CT_SPrePr" minOccurs="0"/>
<xsd:element name="sub" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
<xsd:element name="e" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSubPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSub">
<xsd:sequence>
<xsd:element name="sSubPr" type="CT_SSubPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="sub" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSubSupPr">
<xsd:sequence>
<xsd:element name="alnScr" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSubSup">
<xsd:sequence>
<xsd:element name="sSubSupPr" type="CT_SSubSupPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="sub" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSupPr">
<xsd:sequence>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_SSup">
<xsd:sequence>
<xsd:element name="sSupPr" type="CT_SSupPr" minOccurs="0"/>
<xsd:element name="e" type="CT_OMathArg"/>
<xsd:element name="sup" type="CT_OMathArg"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_OMathMathElements">
<xsd:choice>
<xsd:element name="acc" type="CT_Acc"/>
<xsd:element name="bar" type="CT_Bar"/>
<xsd:element name="box" type="CT_Box"/>
<xsd:element name="borderBox" type="CT_BorderBox"/>
<xsd:element name="d" type="CT_D"/>
<xsd:element name="eqArr" type="CT_EqArr"/>
<xsd:element name="f" type="CT_F"/>
<xsd:element name="func" type="CT_Func"/>
<xsd:element name="groupChr" type="CT_GroupChr"/>
<xsd:element name="limLow" type="CT_LimLow"/>
<xsd:element name="limUpp" type="CT_LimUpp"/>
<xsd:element name="m" type="CT_M"/>
<xsd:element name="nary" type="CT_Nary"/>
<xsd:element name="phant" type="CT_Phant"/>
<xsd:element name="rad" type="CT_Rad"/>
<xsd:element name="sPre" type="CT_SPre"/>
<xsd:element name="sSub" type="CT_SSub"/>
<xsd:element name="sSubSup" type="CT_SSubSup"/>
<xsd:element name="sSup" type="CT_SSup"/>
<xsd:element name="r" type="CT_R"/>
</xsd:choice>
</xsd:group>
<xsd:group name="EG_OMathElements">
<xsd:choice>
<xsd:group ref="EG_OMathMathElements"/>
<xsd:group ref="w:EG_PContentMath"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_OMathArgPr">
<xsd:sequence>
<xsd:element name="argSz" type="CT_Integer2" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_OMathArg">
<xsd:sequence>
<xsd:element name="argPr" type="CT_OMathArgPr" minOccurs="0"/>
<xsd:group ref="EG_OMathElements" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="ctrlPr" type="CT_CtrlPr" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="ST_Jc">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="centerGroup"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_OMathJc">
<xsd:attribute name="val" type="ST_Jc"/>
</xsd:complexType>
<xsd:complexType name="CT_OMathParaPr">
<xsd:sequence>
<xsd:element name="jc" type="CT_OMathJc" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_TwipsMeasure">
<xsd:attribute name="val" type="s:ST_TwipsMeasure" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_BreakBin">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="before"/>
<xsd:enumeration value="after"/>
<xsd:enumeration value="repeat"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_BreakBin">
<xsd:attribute name="val" type="ST_BreakBin"/>
</xsd:complexType>
<xsd:simpleType name="ST_BreakBinSub">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="--"/>
<xsd:enumeration value="-+"/>
<xsd:enumeration value="+-"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_BreakBinSub">
<xsd:attribute name="val" type="ST_BreakBinSub"/>
</xsd:complexType>
<xsd:complexType name="CT_MathPr">
<xsd:sequence>
<xsd:element name="mathFont" type="CT_String" minOccurs="0"/>
<xsd:element name="brkBin" type="CT_BreakBin" minOccurs="0"/>
<xsd:element name="brkBinSub" type="CT_BreakBinSub" minOccurs="0"/>
<xsd:element name="smallFrac" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="dispDef" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="lMargin" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="rMargin" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="defJc" type="CT_OMathJc" minOccurs="0"/>
<xsd:element name="preSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="postSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="interSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:element name="intraSp" type="CT_TwipsMeasure" minOccurs="0"/>
<xsd:choice minOccurs="0">
<xsd:element name="wrapIndent" type="CT_TwipsMeasure"/>
<xsd:element name="wrapRight" type="CT_OnOff"/>
</xsd:choice>
<xsd:element name="intLim" type="CT_LimLoc" minOccurs="0"/>
<xsd:element name="naryLim" type="CT_LimLoc" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="mathPr" type="CT_MathPr"/>
<xsd:complexType name="CT_OMathPara">
<xsd:sequence>
<xsd:element name="oMathParaPr" type="CT_OMathParaPr" minOccurs="0"/>
<xsd:element name="oMath" type="CT_OMath" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_OMath">
<xsd:sequence>
<xsd:group ref="EG_OMathElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="oMathPara" type="CT_OMathPara"/>
<xsd:element name="oMath" type="CT_OMath"/>
</xsd:schema>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
elementFormDefault="qualified"
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
blockDefault="#all">
<xsd:simpleType name="ST_RelationshipId">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:attribute name="id" type="ST_RelationshipId"/>
<xsd:attribute name="embed" type="ST_RelationshipId"/>
<xsd:attribute name="link" type="ST_RelationshipId"/>
<xsd:attribute name="dm" type="ST_RelationshipId" default=""/>
<xsd:attribute name="lo" type="ST_RelationshipId" default=""/>
<xsd:attribute name="qs" type="ST_RelationshipId" default=""/>
<xsd:attribute name="cs" type="ST_RelationshipId" default=""/>
<xsd:attribute name="blip" type="ST_RelationshipId" default=""/>
<xsd:attribute name="pict" type="ST_RelationshipId"/>
<xsd:attribute name="href" type="ST_RelationshipId"/>
<xsd:attribute name="topLeft" type="ST_RelationshipId"/>
<xsd:attribute name="topRight" type="ST_RelationshipId"/>
<xsd:attribute name="bottomLeft" type="ST_RelationshipId"/>
<xsd:attribute name="bottomRight" type="ST_RelationshipId"/>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,570 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:schemas-microsoft-com:vml"
xmlns:pvml="urn:schemas-microsoft-com:office:powerpoint"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
xmlns:w10="urn:schemas-microsoft-com:office:word"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="urn:schemas-microsoft-com:vml" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="urn:schemas-microsoft-com:office:office"
schemaLocation="vml-officeDrawing.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
schemaLocation="wml.xsd"/>
<xsd:import namespace="urn:schemas-microsoft-com:office:word"
schemaLocation="vml-wordprocessingDrawing.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
schemaLocation="shared-relationshipReference.xsd"/>
<xsd:import namespace="urn:schemas-microsoft-com:office:excel"
schemaLocation="vml-spreadsheetDrawing.xsd"/>
<xsd:import namespace="urn:schemas-microsoft-com:office:powerpoint"
schemaLocation="vml-presentationDrawing.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:attributeGroup name="AG_Id">
<xsd:attribute name="id" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Style">
<xsd:attribute name="style" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Type">
<xsd:attribute name="type" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Adj">
<xsd:attribute name="adj" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Path">
<xsd:attribute name="path" type="xsd:string" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Fill">
<xsd:attribute name="filled" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fillcolor" type="s:ST_ColorType" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Chromakey">
<xsd:attribute name="chromakey" type="s:ST_ColorType" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_Ext">
<xsd:attribute name="ext" form="qualified" type="ST_Ext"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_CoreAttributes">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Style"/>
<xsd:attribute name="href" type="xsd:string" use="optional"/>
<xsd:attribute name="target" type="xsd:string" use="optional"/>
<xsd:attribute name="class" type="xsd:string" use="optional"/>
<xsd:attribute name="title" type="xsd:string" use="optional"/>
<xsd:attribute name="alt" type="xsd:string" use="optional"/>
<xsd:attribute name="coordsize" type="xsd:string" use="optional"/>
<xsd:attribute name="coordorigin" type="xsd:string" use="optional"/>
<xsd:attribute name="wrapcoords" type="xsd:string" use="optional"/>
<xsd:attribute name="print" type="s:ST_TrueFalse" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_ShapeAttributes">
<xsd:attributeGroup ref="AG_Chromakey"/>
<xsd:attributeGroup ref="AG_Fill"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="stroked" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="strokecolor" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="strokeweight" type="xsd:string" use="optional"/>
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_OfficeCoreAttributes">
<xsd:attribute ref="o:spid"/>
<xsd:attribute ref="o:oned"/>
<xsd:attribute ref="o:regroupid"/>
<xsd:attribute ref="o:doubleclicknotify"/>
<xsd:attribute ref="o:button"/>
<xsd:attribute ref="o:userhidden"/>
<xsd:attribute ref="o:bullet"/>
<xsd:attribute ref="o:hr"/>
<xsd:attribute ref="o:hrstd"/>
<xsd:attribute ref="o:hrnoshade"/>
<xsd:attribute ref="o:hrpct"/>
<xsd:attribute ref="o:hralign"/>
<xsd:attribute ref="o:allowincell"/>
<xsd:attribute ref="o:allowoverlap"/>
<xsd:attribute ref="o:userdrawn"/>
<xsd:attribute ref="o:bordertopcolor"/>
<xsd:attribute ref="o:borderleftcolor"/>
<xsd:attribute ref="o:borderbottomcolor"/>
<xsd:attribute ref="o:borderrightcolor"/>
<xsd:attribute ref="o:dgmlayout"/>
<xsd:attribute ref="o:dgmnodekind"/>
<xsd:attribute ref="o:dgmlayoutmru"/>
<xsd:attribute ref="o:insetmode"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_OfficeShapeAttributes">
<xsd:attribute ref="o:spt"/>
<xsd:attribute ref="o:connectortype"/>
<xsd:attribute ref="o:bwmode"/>
<xsd:attribute ref="o:bwpure"/>
<xsd:attribute ref="o:bwnormal"/>
<xsd:attribute ref="o:forcedash"/>
<xsd:attribute ref="o:oleicon"/>
<xsd:attribute ref="o:ole"/>
<xsd:attribute ref="o:preferrelative"/>
<xsd:attribute ref="o:cliptowrap"/>
<xsd:attribute ref="o:clip"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_AllCoreAttributes">
<xsd:attributeGroup ref="AG_CoreAttributes"/>
<xsd:attributeGroup ref="AG_OfficeCoreAttributes"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_AllShapeAttributes">
<xsd:attributeGroup ref="AG_ShapeAttributes"/>
<xsd:attributeGroup ref="AG_OfficeShapeAttributes"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_ImageAttributes">
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute name="cropleft" type="xsd:string" use="optional"/>
<xsd:attribute name="croptop" type="xsd:string" use="optional"/>
<xsd:attribute name="cropright" type="xsd:string" use="optional"/>
<xsd:attribute name="cropbottom" type="xsd:string" use="optional"/>
<xsd:attribute name="gain" type="xsd:string" use="optional"/>
<xsd:attribute name="blacklevel" type="xsd:string" use="optional"/>
<xsd:attribute name="gamma" type="xsd:string" use="optional"/>
<xsd:attribute name="grayscale" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="bilevel" type="s:ST_TrueFalse" use="optional"/>
</xsd:attributeGroup>
<xsd:attributeGroup name="AG_StrokeAttributes">
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="weight" type="xsd:string" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="linestyle" type="ST_StrokeLineStyle" use="optional"/>
<xsd:attribute name="miterlimit" type="xsd:decimal" use="optional"/>
<xsd:attribute name="joinstyle" type="ST_StrokeJoinStyle" use="optional"/>
<xsd:attribute name="endcap" type="ST_StrokeEndCap" use="optional"/>
<xsd:attribute name="dashstyle" type="xsd:string" use="optional"/>
<xsd:attribute name="filltype" type="ST_FillType" use="optional"/>
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute name="imageaspect" type="ST_ImageAspect" use="optional"/>
<xsd:attribute name="imagesize" type="xsd:string" use="optional"/>
<xsd:attribute name="imagealignshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="startarrow" type="ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="startarrowwidth" type="ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="startarrowlength" type="ST_StrokeArrowLength" use="optional"/>
<xsd:attribute name="endarrow" type="ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="endarrowwidth" type="ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="endarrowlength" type="ST_StrokeArrowLength" use="optional"/>
<xsd:attribute ref="o:href"/>
<xsd:attribute ref="o:althref"/>
<xsd:attribute ref="o:title"/>
<xsd:attribute ref="o:forcedash"/>
<xsd:attribute ref="r:id" use="optional"/>
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute ref="o:relid"/>
</xsd:attributeGroup>
<xsd:group name="EG_ShapeElements">
<xsd:choice>
<xsd:element ref="path"/>
<xsd:element ref="formulas"/>
<xsd:element ref="handles"/>
<xsd:element ref="fill"/>
<xsd:element ref="stroke"/>
<xsd:element ref="shadow"/>
<xsd:element ref="textbox"/>
<xsd:element ref="textpath"/>
<xsd:element ref="imagedata"/>
<xsd:element ref="o:skew"/>
<xsd:element ref="o:extrusion"/>
<xsd:element ref="o:callout"/>
<xsd:element ref="o:lock"/>
<xsd:element ref="o:clippath"/>
<xsd:element ref="o:signatureline"/>
<xsd:element ref="w10:wrap"/>
<xsd:element ref="w10:anchorlock"/>
<xsd:element ref="w10:bordertop"/>
<xsd:element ref="w10:borderbottom"/>
<xsd:element ref="w10:borderleft"/>
<xsd:element ref="w10:borderright"/>
<xsd:element ref="x:ClientData" minOccurs="0"/>
<xsd:element ref="pvml:textdata" minOccurs="0"/>
</xsd:choice>
</xsd:group>
<xsd:element name="shape" type="CT_Shape"/>
<xsd:element name="shapetype" type="CT_Shapetype"/>
<xsd:element name="group" type="CT_Group"/>
<xsd:element name="background" type="CT_Background"/>
<xsd:complexType name="CT_Shape">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements"/>
<xsd:element ref="o:ink"/>
<xsd:element ref="pvml:iscomment"/>
<xsd:element ref="o:equationxml"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attributeGroup ref="AG_Type"/>
<xsd:attributeGroup ref="AG_Adj"/>
<xsd:attributeGroup ref="AG_Path"/>
<xsd:attribute ref="o:gfxdata"/>
<xsd:attribute name="equationxml" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Shapetype">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element ref="o:complex" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attributeGroup ref="AG_Adj"/>
<xsd:attributeGroup ref="AG_Path"/>
<xsd:attribute ref="o:master"/>
</xsd:complexType>
<xsd:complexType name="CT_Group">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements"/>
<xsd:element ref="group"/>
<xsd:element ref="shape"/>
<xsd:element ref="shapetype"/>
<xsd:element ref="arc"/>
<xsd:element ref="curve"/>
<xsd:element ref="image"/>
<xsd:element ref="line"/>
<xsd:element ref="oval"/>
<xsd:element ref="polyline"/>
<xsd:element ref="rect"/>
<xsd:element ref="roundrect"/>
<xsd:element ref="o:diagram"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_Fill"/>
<xsd:attribute name="editas" type="ST_EditAs" use="optional"/>
<xsd:attribute ref="o:tableproperties"/>
<xsd:attribute ref="o:tablelimits"/>
</xsd:complexType>
<xsd:complexType name="CT_Background">
<xsd:sequence>
<xsd:element ref="fill" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Fill"/>
<xsd:attribute ref="o:bwmode"/>
<xsd:attribute ref="o:bwpure"/>
<xsd:attribute ref="o:bwnormal"/>
<xsd:attribute ref="o:targetscreensize"/>
</xsd:complexType>
<xsd:element name="fill" type="CT_Fill"/>
<xsd:element name="formulas" type="CT_Formulas"/>
<xsd:element name="handles" type="CT_Handles"/>
<xsd:element name="imagedata" type="CT_ImageData"/>
<xsd:element name="path" type="CT_Path"/>
<xsd:element name="textbox" type="CT_Textbox"/>
<xsd:element name="shadow" type="CT_Shadow"/>
<xsd:element name="stroke" type="CT_Stroke"/>
<xsd:element name="textpath" type="CT_TextPath"/>
<xsd:complexType name="CT_Fill">
<xsd:sequence>
<xsd:element ref="o:fill" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attribute name="type" type="ST_FillType" use="optional"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute ref="o:href"/>
<xsd:attribute ref="o:althref"/>
<xsd:attribute name="size" type="xsd:string" use="optional"/>
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
<xsd:attribute name="position" type="xsd:string" use="optional"/>
<xsd:attribute name="aspect" type="ST_ImageAspect" use="optional"/>
<xsd:attribute name="colors" type="xsd:string" use="optional"/>
<xsd:attribute name="angle" type="xsd:decimal" use="optional"/>
<xsd:attribute name="alignshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="focus" type="xsd:string" use="optional"/>
<xsd:attribute name="focussize" type="xsd:string" use="optional"/>
<xsd:attribute name="focusposition" type="xsd:string" use="optional"/>
<xsd:attribute name="method" type="ST_FillMethod" use="optional"/>
<xsd:attribute ref="o:detectmouseclick"/>
<xsd:attribute ref="o:title"/>
<xsd:attribute ref="o:opacity2"/>
<xsd:attribute name="recolor" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="rotate" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute ref="r:id" use="optional"/>
<xsd:attribute ref="o:relid" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Formulas">
<xsd:sequence>
<xsd:element name="f" type="CT_F" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_F">
<xsd:attribute name="eqn" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_Handles">
<xsd:sequence>
<xsd:element name="h" type="CT_H" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_H">
<xsd:attribute name="position" type="xsd:string"/>
<xsd:attribute name="polar" type="xsd:string"/>
<xsd:attribute name="map" type="xsd:string"/>
<xsd:attribute name="invx" type="s:ST_TrueFalse"/>
<xsd:attribute name="invy" type="s:ST_TrueFalse"/>
<xsd:attribute name="switch" type="s:ST_TrueFalseBlank"/>
<xsd:attribute name="xrange" type="xsd:string"/>
<xsd:attribute name="yrange" type="xsd:string"/>
<xsd:attribute name="radiusrange" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_ImageData">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_ImageAttributes"/>
<xsd:attributeGroup ref="AG_Chromakey"/>
<xsd:attribute name="embosscolor" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="recolortarget" type="s:ST_ColorType"/>
<xsd:attribute ref="o:href"/>
<xsd:attribute ref="o:althref"/>
<xsd:attribute ref="o:title"/>
<xsd:attribute ref="o:oleid"/>
<xsd:attribute ref="o:detectmouseclick"/>
<xsd:attribute ref="o:movie"/>
<xsd:attribute ref="o:relid"/>
<xsd:attribute ref="r:id"/>
<xsd:attribute ref="r:pict"/>
<xsd:attribute ref="r:href"/>
</xsd:complexType>
<xsd:complexType name="CT_Path">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attribute name="v" type="xsd:string" use="optional"/>
<xsd:attribute name="limo" type="xsd:string" use="optional"/>
<xsd:attribute name="textboxrect" type="xsd:string" use="optional"/>
<xsd:attribute name="fillok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="strokeok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="shadowok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="arrowok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="gradientshapeok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="textpathok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="insetpenok" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute ref="o:connecttype"/>
<xsd:attribute ref="o:connectlocs"/>
<xsd:attribute ref="o:connectangles"/>
<xsd:attribute ref="o:extrusionok"/>
</xsd:complexType>
<xsd:complexType name="CT_Shadow">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="type" type="ST_ShadowType" use="optional"/>
<xsd:attribute name="obscured" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="offset" type="xsd:string" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="offset2" type="xsd:string" use="optional"/>
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
<xsd:attribute name="matrix" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Stroke">
<xsd:sequence>
<xsd:element ref="o:left" minOccurs="0"/>
<xsd:element ref="o:top" minOccurs="0"/>
<xsd:element ref="o:right" minOccurs="0"/>
<xsd:element ref="o:bottom" minOccurs="0"/>
<xsd:element ref="o:column" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_StrokeAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_Textbox">
<xsd:choice>
<xsd:element ref="w:txbxContent" minOccurs="0"/>
<xsd:any namespace="##local" processContents="skip"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Style"/>
<xsd:attribute name="inset" type="xsd:string" use="optional"/>
<xsd:attribute ref="o:singleclick"/>
<xsd:attribute ref="o:insetmode"/>
</xsd:complexType>
<xsd:complexType name="CT_TextPath">
<xsd:attributeGroup ref="AG_Id"/>
<xsd:attributeGroup ref="AG_Style"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fitshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fitpath" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="trim" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="xscale" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="string" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:element name="arc" type="CT_Arc"/>
<xsd:element name="curve" type="CT_Curve"/>
<xsd:element name="image" type="CT_Image"/>
<xsd:element name="line" type="CT_Line"/>
<xsd:element name="oval" type="CT_Oval"/>
<xsd:element name="polyline" type="CT_PolyLine"/>
<xsd:element name="rect" type="CT_Rect"/>
<xsd:element name="roundrect" type="CT_RoundRect"/>
<xsd:complexType name="CT_Arc">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="startAngle" type="xsd:decimal" use="optional"/>
<xsd:attribute name="endAngle" type="xsd:decimal" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Curve">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="from" type="xsd:string" use="optional"/>
<xsd:attribute name="control1" type="xsd:string" use="optional"/>
<xsd:attribute name="control2" type="xsd:string" use="optional"/>
<xsd:attribute name="to" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Image">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attributeGroup ref="AG_ImageAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_Line">
<xsd:sequence>
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="from" type="xsd:string" use="optional"/>
<xsd:attribute name="to" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Oval">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_PolyLine">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements"/>
<xsd:element ref="o:ink"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="points" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Rect">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
</xsd:complexType>
<xsd:complexType name="CT_RoundRect">
<xsd:choice maxOccurs="unbounded">
<xsd:group ref="EG_ShapeElements" minOccurs="0" maxOccurs="unbounded"/>
</xsd:choice>
<xsd:attributeGroup ref="AG_AllCoreAttributes"/>
<xsd:attributeGroup ref="AG_AllShapeAttributes"/>
<xsd:attribute name="arcsize" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_Ext">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="view"/>
<xsd:enumeration value="edit"/>
<xsd:enumeration value="backwardCompatible"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FillType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="solid"/>
<xsd:enumeration value="gradient"/>
<xsd:enumeration value="gradientRadial"/>
<xsd:enumeration value="tile"/>
<xsd:enumeration value="pattern"/>
<xsd:enumeration value="frame"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FillMethod">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="linear"/>
<xsd:enumeration value="sigma"/>
<xsd:enumeration value="any"/>
<xsd:enumeration value="linear sigma"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ShadowType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="single"/>
<xsd:enumeration value="double"/>
<xsd:enumeration value="emboss"/>
<xsd:enumeration value="perspective"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeLineStyle">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="single"/>
<xsd:enumeration value="thinThin"/>
<xsd:enumeration value="thinThick"/>
<xsd:enumeration value="thickThin"/>
<xsd:enumeration value="thickBetweenThin"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeJoinStyle">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="round"/>
<xsd:enumeration value="bevel"/>
<xsd:enumeration value="miter"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeEndCap">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="flat"/>
<xsd:enumeration value="square"/>
<xsd:enumeration value="round"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeArrowLength">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="short"/>
<xsd:enumeration value="medium"/>
<xsd:enumeration value="long"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeArrowWidth">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="narrow"/>
<xsd:enumeration value="medium"/>
<xsd:enumeration value="wide"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_StrokeArrowType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="block"/>
<xsd:enumeration value="classic"/>
<xsd:enumeration value="oval"/>
<xsd:enumeration value="diamond"/>
<xsd:enumeration value="open"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ImageAspect">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="ignore"/>
<xsd:enumeration value="atMost"/>
<xsd:enumeration value="atLeast"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_EditAs">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="canvas"/>
<xsd:enumeration value="orgchart"/>
<xsd:enumeration value="radial"/>
<xsd:enumeration value="cycle"/>
<xsd:enumeration value="stacked"/>
<xsd:enumeration value="venn"/>
<xsd:enumeration value="bullseye"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,509 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="urn:schemas-microsoft-com:office:office" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="urn:schemas-microsoft-com:vml" schemaLocation="vml-main.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
schemaLocation="shared-relationshipReference.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:attribute name="bwmode" type="ST_BWMode"/>
<xsd:attribute name="bwpure" type="ST_BWMode"/>
<xsd:attribute name="bwnormal" type="ST_BWMode"/>
<xsd:attribute name="targetscreensize" type="ST_ScreenSize"/>
<xsd:attribute name="insetmode" type="ST_InsetMode" default="custom"/>
<xsd:attribute name="spt" type="xsd:float"/>
<xsd:attribute name="wrapcoords" type="xsd:string"/>
<xsd:attribute name="oned" type="s:ST_TrueFalse"/>
<xsd:attribute name="regroupid" type="xsd:integer"/>
<xsd:attribute name="doubleclicknotify" type="s:ST_TrueFalse"/>
<xsd:attribute name="connectortype" type="ST_ConnectorType" default="straight"/>
<xsd:attribute name="button" type="s:ST_TrueFalse"/>
<xsd:attribute name="userhidden" type="s:ST_TrueFalse"/>
<xsd:attribute name="forcedash" type="s:ST_TrueFalse"/>
<xsd:attribute name="oleicon" type="s:ST_TrueFalse"/>
<xsd:attribute name="ole" type="s:ST_TrueFalseBlank"/>
<xsd:attribute name="preferrelative" type="s:ST_TrueFalse"/>
<xsd:attribute name="cliptowrap" type="s:ST_TrueFalse"/>
<xsd:attribute name="clip" type="s:ST_TrueFalse"/>
<xsd:attribute name="bullet" type="s:ST_TrueFalse"/>
<xsd:attribute name="hr" type="s:ST_TrueFalse"/>
<xsd:attribute name="hrstd" type="s:ST_TrueFalse"/>
<xsd:attribute name="hrnoshade" type="s:ST_TrueFalse"/>
<xsd:attribute name="hrpct" type="xsd:float"/>
<xsd:attribute name="hralign" type="ST_HrAlign" default="left"/>
<xsd:attribute name="allowincell" type="s:ST_TrueFalse"/>
<xsd:attribute name="allowoverlap" type="s:ST_TrueFalse"/>
<xsd:attribute name="userdrawn" type="s:ST_TrueFalse"/>
<xsd:attribute name="bordertopcolor" type="xsd:string"/>
<xsd:attribute name="borderleftcolor" type="xsd:string"/>
<xsd:attribute name="borderbottomcolor" type="xsd:string"/>
<xsd:attribute name="borderrightcolor" type="xsd:string"/>
<xsd:attribute name="connecttype" type="ST_ConnectType"/>
<xsd:attribute name="connectlocs" type="xsd:string"/>
<xsd:attribute name="connectangles" type="xsd:string"/>
<xsd:attribute name="master" type="xsd:string"/>
<xsd:attribute name="extrusionok" type="s:ST_TrueFalse"/>
<xsd:attribute name="href" type="xsd:string"/>
<xsd:attribute name="althref" type="xsd:string"/>
<xsd:attribute name="title" type="xsd:string"/>
<xsd:attribute name="singleclick" type="s:ST_TrueFalse"/>
<xsd:attribute name="oleid" type="xsd:float"/>
<xsd:attribute name="detectmouseclick" type="s:ST_TrueFalse"/>
<xsd:attribute name="movie" type="xsd:float"/>
<xsd:attribute name="spid" type="xsd:string"/>
<xsd:attribute name="opacity2" type="xsd:string"/>
<xsd:attribute name="relid" type="r:ST_RelationshipId"/>
<xsd:attribute name="dgmlayout" type="ST_DiagramLayout"/>
<xsd:attribute name="dgmnodekind" type="xsd:integer"/>
<xsd:attribute name="dgmlayoutmru" type="ST_DiagramLayout"/>
<xsd:attribute name="gfxdata" type="xsd:base64Binary"/>
<xsd:attribute name="tableproperties" type="xsd:string"/>
<xsd:attribute name="tablelimits" type="xsd:string"/>
<xsd:element name="shapedefaults" type="CT_ShapeDefaults"/>
<xsd:element name="shapelayout" type="CT_ShapeLayout"/>
<xsd:element name="signatureline" type="CT_SignatureLine"/>
<xsd:element name="ink" type="CT_Ink"/>
<xsd:element name="diagram" type="CT_Diagram"/>
<xsd:element name="equationxml" type="CT_EquationXml"/>
<xsd:complexType name="CT_ShapeDefaults">
<xsd:all minOccurs="0">
<xsd:element ref="v:fill" minOccurs="0"/>
<xsd:element ref="v:stroke" minOccurs="0"/>
<xsd:element ref="v:textbox" minOccurs="0"/>
<xsd:element ref="v:shadow" minOccurs="0"/>
<xsd:element ref="skew" minOccurs="0"/>
<xsd:element ref="extrusion" minOccurs="0"/>
<xsd:element ref="callout" minOccurs="0"/>
<xsd:element ref="lock" minOccurs="0"/>
<xsd:element name="colormru" minOccurs="0" type="CT_ColorMru"/>
<xsd:element name="colormenu" minOccurs="0" type="CT_ColorMenu"/>
</xsd:all>
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="spidmax" type="xsd:integer" use="optional"/>
<xsd:attribute name="style" type="xsd:string" use="optional"/>
<xsd:attribute name="fill" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="fillcolor" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="stroke" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="strokecolor" type="s:ST_ColorType"/>
<xsd:attribute name="allowincell" form="qualified" type="s:ST_TrueFalse"/>
</xsd:complexType>
<xsd:complexType name="CT_Ink">
<xsd:sequence/>
<xsd:attribute name="i" type="xsd:string"/>
<xsd:attribute name="annotation" type="s:ST_TrueFalse"/>
<xsd:attribute name="contentType" type="ST_ContentType" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_SignatureLine">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="issignatureline" type="s:ST_TrueFalse"/>
<xsd:attribute name="id" type="s:ST_Guid"/>
<xsd:attribute name="provid" type="s:ST_Guid"/>
<xsd:attribute name="signinginstructionsset" type="s:ST_TrueFalse"/>
<xsd:attribute name="allowcomments" type="s:ST_TrueFalse"/>
<xsd:attribute name="showsigndate" type="s:ST_TrueFalse"/>
<xsd:attribute name="suggestedsigner" type="xsd:string" form="qualified"/>
<xsd:attribute name="suggestedsigner2" type="xsd:string" form="qualified"/>
<xsd:attribute name="suggestedsigneremail" type="xsd:string" form="qualified"/>
<xsd:attribute name="signinginstructions" type="xsd:string"/>
<xsd:attribute name="addlxml" type="xsd:string"/>
<xsd:attribute name="sigprovurl" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_ShapeLayout">
<xsd:all>
<xsd:element name="idmap" type="CT_IdMap" minOccurs="0"/>
<xsd:element name="regrouptable" type="CT_RegroupTable" minOccurs="0"/>
<xsd:element name="rules" type="CT_Rules" minOccurs="0"/>
</xsd:all>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_IdMap">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="data" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_RegroupTable">
<xsd:sequence>
<xsd:element name="entry" type="CT_Entry" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_Entry">
<xsd:attribute name="new" type="xsd:int" use="optional"/>
<xsd:attribute name="old" type="xsd:int" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Rules">
<xsd:sequence>
<xsd:element name="r" type="CT_R" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_R">
<xsd:sequence>
<xsd:element name="proxy" type="CT_Proxy" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="id" type="xsd:string" use="required"/>
<xsd:attribute name="type" type="ST_RType" use="optional"/>
<xsd:attribute name="how" type="ST_How" use="optional"/>
<xsd:attribute name="idref" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Proxy">
<xsd:attribute name="start" type="s:ST_TrueFalseBlank" use="optional" default="false"/>
<xsd:attribute name="end" type="s:ST_TrueFalseBlank" use="optional" default="false"/>
<xsd:attribute name="idref" type="xsd:string" use="optional"/>
<xsd:attribute name="connectloc" type="xsd:int" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Diagram">
<xsd:sequence>
<xsd:element name="relationtable" type="CT_RelationTable" minOccurs="0"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="dgmstyle" type="xsd:integer" use="optional"/>
<xsd:attribute name="autoformat" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="reverse" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="autolayout" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="dgmscalex" type="xsd:integer" use="optional"/>
<xsd:attribute name="dgmscaley" type="xsd:integer" use="optional"/>
<xsd:attribute name="dgmfontsize" type="xsd:integer" use="optional"/>
<xsd:attribute name="constrainbounds" type="xsd:string" use="optional"/>
<xsd:attribute name="dgmbasetextscale" type="xsd:integer" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_EquationXml">
<xsd:sequence>
<xsd:any namespace="##any"/>
</xsd:sequence>
<xsd:attribute name="contentType" type="ST_AlternateMathContentType" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_AlternateMathContentType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:complexType name="CT_RelationTable">
<xsd:sequence>
<xsd:element name="rel" type="CT_Relation" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_Relation">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="idsrc" type="xsd:string" use="optional"/>
<xsd:attribute name="iddest" type="xsd:string" use="optional"/>
<xsd:attribute name="idcntr" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_ColorMru">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="colors" type="xsd:string"/>
</xsd:complexType>
<xsd:complexType name="CT_ColorMenu">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="strokecolor" type="s:ST_ColorType"/>
<xsd:attribute name="fillcolor" type="s:ST_ColorType"/>
<xsd:attribute name="shadowcolor" type="s:ST_ColorType"/>
<xsd:attribute name="extrusioncolor" type="s:ST_ColorType"/>
</xsd:complexType>
<xsd:element name="skew" type="CT_Skew"/>
<xsd:element name="extrusion" type="CT_Extrusion"/>
<xsd:element name="callout" type="CT_Callout"/>
<xsd:element name="lock" type="CT_Lock"/>
<xsd:element name="OLEObject" type="CT_OLEObject"/>
<xsd:element name="complex" type="CT_Complex"/>
<xsd:element name="left" type="CT_StrokeChild"/>
<xsd:element name="top" type="CT_StrokeChild"/>
<xsd:element name="right" type="CT_StrokeChild"/>
<xsd:element name="bottom" type="CT_StrokeChild"/>
<xsd:element name="column" type="CT_StrokeChild"/>
<xsd:element name="clippath" type="CT_ClipPath"/>
<xsd:element name="fill" type="CT_Fill"/>
<xsd:complexType name="CT_Skew">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="id" type="xsd:string" use="optional"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="offset" type="xsd:string" use="optional"/>
<xsd:attribute name="origin" type="xsd:string" use="optional"/>
<xsd:attribute name="matrix" type="xsd:string" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Extrusion">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="type" type="ST_ExtrusionType" default="parallel" use="optional"/>
<xsd:attribute name="render" type="ST_ExtrusionRender" default="solid" use="optional"/>
<xsd:attribute name="viewpointorigin" type="xsd:string" use="optional"/>
<xsd:attribute name="viewpoint" type="xsd:string" use="optional"/>
<xsd:attribute name="plane" type="ST_ExtrusionPlane" default="XY" use="optional"/>
<xsd:attribute name="skewangle" type="xsd:float" use="optional"/>
<xsd:attribute name="skewamt" type="xsd:string" use="optional"/>
<xsd:attribute name="foredepth" type="xsd:string" use="optional"/>
<xsd:attribute name="backdepth" type="xsd:string" use="optional"/>
<xsd:attribute name="orientation" type="xsd:string" use="optional"/>
<xsd:attribute name="orientationangle" type="xsd:float" use="optional"/>
<xsd:attribute name="lockrotationcenter" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="autorotationcenter" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="rotationcenter" type="xsd:string" use="optional"/>
<xsd:attribute name="rotationangle" type="xsd:string" use="optional"/>
<xsd:attribute name="colormode" type="ST_ColorMode" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="shininess" type="xsd:float" use="optional"/>
<xsd:attribute name="specularity" type="xsd:string" use="optional"/>
<xsd:attribute name="diffusity" type="xsd:string" use="optional"/>
<xsd:attribute name="metal" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="edge" type="xsd:string" use="optional"/>
<xsd:attribute name="facet" type="xsd:string" use="optional"/>
<xsd:attribute name="lightface" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="brightness" type="xsd:string" use="optional"/>
<xsd:attribute name="lightposition" type="xsd:string" use="optional"/>
<xsd:attribute name="lightlevel" type="xsd:string" use="optional"/>
<xsd:attribute name="lightharsh" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="lightposition2" type="xsd:string" use="optional"/>
<xsd:attribute name="lightlevel2" type="xsd:string" use="optional"/>
<xsd:attribute name="lightharsh2" type="s:ST_TrueFalse" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Callout">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="type" type="xsd:string" use="optional"/>
<xsd:attribute name="gap" type="xsd:string" use="optional"/>
<xsd:attribute name="angle" type="ST_Angle" use="optional"/>
<xsd:attribute name="dropauto" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="drop" type="ST_CalloutDrop" use="optional"/>
<xsd:attribute name="distance" type="xsd:string" use="optional"/>
<xsd:attribute name="lengthspecified" type="s:ST_TrueFalse" default="f" use="optional"/>
<xsd:attribute name="length" type="xsd:string" use="optional"/>
<xsd:attribute name="accentbar" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="textborder" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="minusx" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="minusy" type="s:ST_TrueFalse" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Lock">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="position" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="selection" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="grouping" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="ungrouping" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="rotation" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="cropping" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="verticies" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="adjusthandles" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="text" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="aspectratio" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="shapetype" type="s:ST_TrueFalse" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_OLEObject">
<xsd:sequence>
<xsd:element name="LinkType" type="ST_OLELinkType" minOccurs="0"/>
<xsd:element name="LockedField" type="s:ST_TrueFalseBlank" minOccurs="0"/>
<xsd:element name="FieldCodes" type="xsd:string" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="Type" type="ST_OLEType" use="optional"/>
<xsd:attribute name="ProgID" type="xsd:string" use="optional"/>
<xsd:attribute name="ShapeID" type="xsd:string" use="optional"/>
<xsd:attribute name="DrawAspect" type="ST_OLEDrawAspect" use="optional"/>
<xsd:attribute name="ObjectID" type="xsd:string" use="optional"/>
<xsd:attribute ref="r:id" use="optional"/>
<xsd:attribute name="UpdateMode" type="ST_OLEUpdateMode" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_Complex">
<xsd:attributeGroup ref="v:AG_Ext"/>
</xsd:complexType>
<xsd:complexType name="CT_StrokeChild">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="on" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="weight" type="xsd:string" use="optional"/>
<xsd:attribute name="color" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="color2" type="s:ST_ColorType" use="optional"/>
<xsd:attribute name="opacity" type="xsd:string" use="optional"/>
<xsd:attribute name="linestyle" type="v:ST_StrokeLineStyle" use="optional"/>
<xsd:attribute name="miterlimit" type="xsd:decimal" use="optional"/>
<xsd:attribute name="joinstyle" type="v:ST_StrokeJoinStyle" use="optional"/>
<xsd:attribute name="endcap" type="v:ST_StrokeEndCap" use="optional"/>
<xsd:attribute name="dashstyle" type="xsd:string" use="optional"/>
<xsd:attribute name="insetpen" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="filltype" type="v:ST_FillType" use="optional"/>
<xsd:attribute name="src" type="xsd:string" use="optional"/>
<xsd:attribute name="imageaspect" type="v:ST_ImageAspect" use="optional"/>
<xsd:attribute name="imagesize" type="xsd:string" use="optional"/>
<xsd:attribute name="imagealignshape" type="s:ST_TrueFalse" use="optional"/>
<xsd:attribute name="startarrow" type="v:ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="startarrowwidth" type="v:ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="startarrowlength" type="v:ST_StrokeArrowLength" use="optional"/>
<xsd:attribute name="endarrow" type="v:ST_StrokeArrowType" use="optional"/>
<xsd:attribute name="endarrowwidth" type="v:ST_StrokeArrowWidth" use="optional"/>
<xsd:attribute name="endarrowlength" type="v:ST_StrokeArrowLength" use="optional"/>
<xsd:attribute ref="href"/>
<xsd:attribute ref="althref"/>
<xsd:attribute ref="title"/>
<xsd:attribute ref="forcedash"/>
</xsd:complexType>
<xsd:complexType name="CT_ClipPath">
<xsd:attribute name="v" type="xsd:string" use="required" form="qualified"/>
</xsd:complexType>
<xsd:complexType name="CT_Fill">
<xsd:attributeGroup ref="v:AG_Ext"/>
<xsd:attribute name="type" type="ST_FillType"/>
</xsd:complexType>
<xsd:simpleType name="ST_RType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="arc"/>
<xsd:enumeration value="callout"/>
<xsd:enumeration value="connector"/>
<xsd:enumeration value="align"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_How">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="top"/>
<xsd:enumeration value="middle"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="left"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="right"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_BWMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="color"/>
<xsd:enumeration value="auto"/>
<xsd:enumeration value="grayScale"/>
<xsd:enumeration value="lightGrayscale"/>
<xsd:enumeration value="inverseGray"/>
<xsd:enumeration value="grayOutline"/>
<xsd:enumeration value="highContrast"/>
<xsd:enumeration value="black"/>
<xsd:enumeration value="white"/>
<xsd:enumeration value="hide"/>
<xsd:enumeration value="undrawn"/>
<xsd:enumeration value="blackTextAndLines"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ScreenSize">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="544,376"/>
<xsd:enumeration value="640,480"/>
<xsd:enumeration value="720,512"/>
<xsd:enumeration value="800,600"/>
<xsd:enumeration value="1024,768"/>
<xsd:enumeration value="1152,862"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_InsetMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="auto"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ColorMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="auto"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ContentType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_DiagramLayout">
<xsd:restriction base="xsd:integer">
<xsd:enumeration value="0"/>
<xsd:enumeration value="1"/>
<xsd:enumeration value="2"/>
<xsd:enumeration value="3"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ExtrusionType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="perspective"/>
<xsd:enumeration value="parallel"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ExtrusionRender">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="solid"/>
<xsd:enumeration value="wireFrame"/>
<xsd:enumeration value="boundingCube"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ExtrusionPlane">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="XY"/>
<xsd:enumeration value="ZX"/>
<xsd:enumeration value="YZ"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Angle">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="any"/>
<xsd:enumeration value="30"/>
<xsd:enumeration value="45"/>
<xsd:enumeration value="60"/>
<xsd:enumeration value="90"/>
<xsd:enumeration value="auto"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_CalloutDrop">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_CalloutPlacement">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="top"/>
<xsd:enumeration value="center"/>
<xsd:enumeration value="bottom"/>
<xsd:enumeration value="user"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ConnectorType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="straight"/>
<xsd:enumeration value="elbow"/>
<xsd:enumeration value="curved"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_HrAlign">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="center"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_ConnectType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="rect"/>
<xsd:enumeration value="segments"/>
<xsd:enumeration value="custom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OLELinkType">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_OLEType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Embed"/>
<xsd:enumeration value="Link"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OLEDrawAspect">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Content"/>
<xsd:enumeration value="Icon"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_OLEUpdateMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Always"/>
<xsd:enumeration value="OnCall"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_FillType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="gradientCenter"/>
<xsd:enumeration value="solid"/>
<xsd:enumeration value="pattern"/>
<xsd:enumeration value="tile"/>
<xsd:enumeration value="frame"/>
<xsd:enumeration value="gradientUnscaled"/>
<xsd:enumeration value="gradientRadial"/>
<xsd:enumeration value="gradient"/>
<xsd:enumeration value="background"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:powerpoint"
targetNamespace="urn:schemas-microsoft-com:office:powerpoint" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="iscomment" type="CT_Empty"/>
<xsd:element name="textdata" type="CT_Rel"/>
<xsd:complexType name="CT_Empty"/>
<xsd:complexType name="CT_Rel">
<xsd:attribute name="id" type="xsd:string"/>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:excel"
xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
targetNamespace="urn:schemas-microsoft-com:office:excel" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
schemaLocation="shared-commonSimpleTypes.xsd"/>
<xsd:element name="ClientData" type="CT_ClientData"/>
<xsd:complexType name="CT_ClientData">
<xsd:choice minOccurs="0" maxOccurs="unbounded">
<xsd:element name="MoveWithCells" type="s:ST_TrueFalseBlank"/>
<xsd:element name="SizeWithCells" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Anchor" type="xsd:string"/>
<xsd:element name="Locked" type="s:ST_TrueFalseBlank"/>
<xsd:element name="DefaultSize" type="s:ST_TrueFalseBlank"/>
<xsd:element name="PrintObject" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Disabled" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoFill" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoLine" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoPict" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FmlaMacro" type="xsd:string"/>
<xsd:element name="TextHAlign" type="xsd:string"/>
<xsd:element name="TextVAlign" type="xsd:string"/>
<xsd:element name="LockText" type="s:ST_TrueFalseBlank"/>
<xsd:element name="JustLastX" type="s:ST_TrueFalseBlank"/>
<xsd:element name="SecretEdit" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Default" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Help" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Cancel" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Dismiss" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Accel" type="xsd:integer"/>
<xsd:element name="Accel2" type="xsd:integer"/>
<xsd:element name="Row" type="xsd:integer"/>
<xsd:element name="Column" type="xsd:integer"/>
<xsd:element name="Visible" type="s:ST_TrueFalseBlank"/>
<xsd:element name="RowHidden" type="s:ST_TrueFalseBlank"/>
<xsd:element name="ColHidden" type="s:ST_TrueFalseBlank"/>
<xsd:element name="VTEdit" type="xsd:integer"/>
<xsd:element name="MultiLine" type="s:ST_TrueFalseBlank"/>
<xsd:element name="VScroll" type="s:ST_TrueFalseBlank"/>
<xsd:element name="ValidIds" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FmlaRange" type="xsd:string"/>
<xsd:element name="WidthMin" type="xsd:integer"/>
<xsd:element name="Sel" type="xsd:integer"/>
<xsd:element name="NoThreeD2" type="s:ST_TrueFalseBlank"/>
<xsd:element name="SelType" type="xsd:string"/>
<xsd:element name="MultiSel" type="xsd:string"/>
<xsd:element name="LCT" type="xsd:string"/>
<xsd:element name="ListItem" type="xsd:string"/>
<xsd:element name="DropStyle" type="xsd:string"/>
<xsd:element name="Colored" type="s:ST_TrueFalseBlank"/>
<xsd:element name="DropLines" type="xsd:integer"/>
<xsd:element name="Checked" type="xsd:integer"/>
<xsd:element name="FmlaLink" type="xsd:string"/>
<xsd:element name="FmlaPict" type="xsd:string"/>
<xsd:element name="NoThreeD" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FirstButton" type="s:ST_TrueFalseBlank"/>
<xsd:element name="FmlaGroup" type="xsd:string"/>
<xsd:element name="Val" type="xsd:integer"/>
<xsd:element name="Min" type="xsd:integer"/>
<xsd:element name="Max" type="xsd:integer"/>
<xsd:element name="Inc" type="xsd:integer"/>
<xsd:element name="Page" type="xsd:integer"/>
<xsd:element name="Horiz" type="s:ST_TrueFalseBlank"/>
<xsd:element name="Dx" type="xsd:integer"/>
<xsd:element name="MapOCX" type="s:ST_TrueFalseBlank"/>
<xsd:element name="CF" type="ST_CF"/>
<xsd:element name="Camera" type="s:ST_TrueFalseBlank"/>
<xsd:element name="RecalcAlways" type="s:ST_TrueFalseBlank"/>
<xsd:element name="AutoScale" type="s:ST_TrueFalseBlank"/>
<xsd:element name="DDE" type="s:ST_TrueFalseBlank"/>
<xsd:element name="UIObj" type="s:ST_TrueFalseBlank"/>
<xsd:element name="ScriptText" type="xsd:string"/>
<xsd:element name="ScriptExtended" type="xsd:string"/>
<xsd:element name="ScriptLanguage" type="xsd:nonNegativeInteger"/>
<xsd:element name="ScriptLocation" type="xsd:nonNegativeInteger"/>
<xsd:element name="FmlaTxbx" type="xsd:string"/>
</xsd:choice>
<xsd:attribute name="ObjectType" type="ST_ObjectType" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_CF">
<xsd:restriction base="xsd:string"/>
</xsd:simpleType>
<xsd:simpleType name="ST_ObjectType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="Button"/>
<xsd:enumeration value="Checkbox"/>
<xsd:enumeration value="Dialog"/>
<xsd:enumeration value="Drop"/>
<xsd:enumeration value="Edit"/>
<xsd:enumeration value="GBox"/>
<xsd:enumeration value="Label"/>
<xsd:enumeration value="LineA"/>
<xsd:enumeration value="List"/>
<xsd:enumeration value="Movie"/>
<xsd:enumeration value="Note"/>
<xsd:enumeration value="Pict"/>
<xsd:enumeration value="Radio"/>
<xsd:enumeration value="RectA"/>
<xsd:enumeration value="Scroll"/>
<xsd:enumeration value="Spin"/>
<xsd:enumeration value="Shape"/>
<xsd:enumeration value="Group"/>
<xsd:enumeration value="Rect"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns="urn:schemas-microsoft-com:office:word"
targetNamespace="urn:schemas-microsoft-com:office:word" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<xsd:element name="bordertop" type="CT_Border"/>
<xsd:element name="borderleft" type="CT_Border"/>
<xsd:element name="borderright" type="CT_Border"/>
<xsd:element name="borderbottom" type="CT_Border"/>
<xsd:complexType name="CT_Border">
<xsd:attribute name="type" type="ST_BorderType" use="optional"/>
<xsd:attribute name="width" type="xsd:positiveInteger" use="optional"/>
<xsd:attribute name="shadow" type="ST_BorderShadow" use="optional"/>
</xsd:complexType>
<xsd:element name="wrap" type="CT_Wrap"/>
<xsd:complexType name="CT_Wrap">
<xsd:attribute name="type" type="ST_WrapType" use="optional"/>
<xsd:attribute name="side" type="ST_WrapSide" use="optional"/>
<xsd:attribute name="anchorx" type="ST_HorizontalAnchor" use="optional"/>
<xsd:attribute name="anchory" type="ST_VerticalAnchor" use="optional"/>
</xsd:complexType>
<xsd:element name="anchorlock" type="CT_AnchorLock"/>
<xsd:complexType name="CT_AnchorLock"/>
<xsd:simpleType name="ST_BorderType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="single"/>
<xsd:enumeration value="thick"/>
<xsd:enumeration value="double"/>
<xsd:enumeration value="hairline"/>
<xsd:enumeration value="dot"/>
<xsd:enumeration value="dash"/>
<xsd:enumeration value="dotDash"/>
<xsd:enumeration value="dashDotDot"/>
<xsd:enumeration value="triple"/>
<xsd:enumeration value="thinThickSmall"/>
<xsd:enumeration value="thickThinSmall"/>
<xsd:enumeration value="thickBetweenThinSmall"/>
<xsd:enumeration value="thinThick"/>
<xsd:enumeration value="thickThin"/>
<xsd:enumeration value="thickBetweenThin"/>
<xsd:enumeration value="thinThickLarge"/>
<xsd:enumeration value="thickThinLarge"/>
<xsd:enumeration value="thickBetweenThinLarge"/>
<xsd:enumeration value="wave"/>
<xsd:enumeration value="doubleWave"/>
<xsd:enumeration value="dashedSmall"/>
<xsd:enumeration value="dashDotStroked"/>
<xsd:enumeration value="threeDEmboss"/>
<xsd:enumeration value="threeDEngrave"/>
<xsd:enumeration value="HTMLOutset"/>
<xsd:enumeration value="HTMLInset"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_BorderShadow">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="t"/>
<xsd:enumeration value="true"/>
<xsd:enumeration value="f"/>
<xsd:enumeration value="false"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_WrapType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="topAndBottom"/>
<xsd:enumeration value="square"/>
<xsd:enumeration value="none"/>
<xsd:enumeration value="tight"/>
<xsd:enumeration value="through"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_WrapSide">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="both"/>
<xsd:enumeration value="left"/>
<xsd:enumeration value="right"/>
<xsd:enumeration value="largest"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_HorizontalAnchor">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="text"/>
<xsd:enumeration value="char"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_VerticalAnchor">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="margin"/>
<xsd:enumeration value="page"/>
<xsd:enumeration value="text"/>
<xsd:enumeration value="line"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,116 @@
<?xml version='1.0'?>
<xs:schema targetNamespace="http://www.w3.org/XML/1998/namespace" xmlns:xs="http://www.w3.org/2001/XMLSchema" xml:lang="en">
<xs:annotation>
<xs:documentation>
See http://www.w3.org/XML/1998/namespace.html and
http://www.w3.org/TR/REC-xml for information about this namespace.
This schema document describes the XML namespace, in a form
suitable for import by other schema documents.
Note that local names in this namespace are intended to be defined
only by the World Wide Web Consortium or its subgroups. The
following names are currently defined in this namespace and should
not be used with conflicting semantics by any Working Group,
specification, or document instance:
base (as an attribute name): denotes an attribute whose value
provides a URI to be used as the base for interpreting any
relative URIs in the scope of the element on which it
appears; its value is inherited. This name is reserved
by virtue of its definition in the XML Base specification.
lang (as an attribute name): denotes an attribute whose value
is a language code for the natural language of the content of
any element; its value is inherited. This name is reserved
by virtue of its definition in the XML specification.
space (as an attribute name): denotes an attribute whose
value is a keyword indicating what whitespace processing
discipline is intended for the content of the element; its
value is inherited. This name is reserved by virtue of its
definition in the XML specification.
Father (in any context at all): denotes Jon Bosak, the chair of
the original XML Working Group. This name is reserved by
the following decision of the W3C XML Plenary and
XML Coordination groups:
In appreciation for his vision, leadership and dedication
the W3C XML Plenary on this 10th day of February, 2000
reserves for Jon Bosak in perpetuity the XML name
xml:Father
</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>This schema defines attributes and an attribute group
suitable for use by
schemas wishing to allow xml:base, xml:lang or xml:space attributes
on elements they define.
To enable this, such a schema must import this schema
for the XML namespace, e.g. as follows:
&lt;schema . . .>
. . .
&lt;import namespace="http://www.w3.org/XML/1998/namespace"
schemaLocation="http://www.w3.org/2001/03/xml.xsd"/>
Subsequently, qualified reference to any of the attributes
or the group defined below will have the desired effect, e.g.
&lt;type . . .>
. . .
&lt;attributeGroup ref="xml:specialAttrs"/>
will define a type which will schema-validate an instance
element with any of those attributes</xs:documentation>
</xs:annotation>
<xs:annotation>
<xs:documentation>In keeping with the XML Schema WG's standard versioning
policy, this schema document will persist at
http://www.w3.org/2001/03/xml.xsd.
At the date of issue it can also be found at
http://www.w3.org/2001/xml.xsd.
The schema document at that URI may however change in the future,
in order to remain compatible with the latest version of XML Schema
itself. In other words, if the XML Schema namespace changes, the version
of this document at
http://www.w3.org/2001/xml.xsd will change
accordingly; the version at
http://www.w3.org/2001/03/xml.xsd will not change.
</xs:documentation>
</xs:annotation>
<xs:attribute name="lang" type="xs:language">
<xs:annotation>
<xs:documentation>In due course, we should install the relevant ISO 2- and 3-letter
codes as the enumerated possible values . . .</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="space" default="preserve">
<xs:simpleType>
<xs:restriction base="xs:NCName">
<xs:enumeration value="default"/>
<xs:enumeration value="preserve"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="base" type="xs:anyURI">
<xs:annotation>
<xs:documentation>See http://www.w3.org/TR/xmlbase/ for
information about this attribute.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attributeGroup name="specialAttrs">
<xs:attribute ref="xml:base"/>
<xs:attribute ref="xml:lang"/>
<xs:attribute ref="xml:space"/>
</xs:attributeGroup>
</xs:schema>

View File

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xs:schema xmlns="http://schemas.openxmlformats.org/package/2006/content-types"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://schemas.openxmlformats.org/package/2006/content-types"
elementFormDefault="qualified" attributeFormDefault="unqualified" blockDefault="#all">
<xs:element name="Types" type="CT_Types"/>
<xs:element name="Default" type="CT_Default"/>
<xs:element name="Override" type="CT_Override"/>
<xs:complexType name="CT_Types">
<xs:choice minOccurs="0" maxOccurs="unbounded">
<xs:element ref="Default"/>
<xs:element ref="Override"/>
</xs:choice>
</xs:complexType>
<xs:complexType name="CT_Default">
<xs:attribute name="Extension" type="ST_Extension" use="required"/>
<xs:attribute name="ContentType" type="ST_ContentType" use="required"/>
</xs:complexType>
<xs:complexType name="CT_Override">
<xs:attribute name="ContentType" type="ST_ContentType" use="required"/>
<xs:attribute name="PartName" type="xs:anyURI" use="required"/>
</xs:complexType>
<xs:simpleType name="ST_ContentType">
<xs:restriction base="xs:string">
<xs:pattern
value="(((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+))/((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+))((\s+)*;(\s+)*(((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+))=((([\p{IsBasicLatin}-[\p{Cc}&#127;\(\)&lt;&gt;@,;:\\&quot;/\[\]\?=\{\}\s\t]])+)|(&quot;(([\p{IsLatin-1Supplement}\p{IsBasicLatin}-[\p{Cc}&#127;&quot;\n\r]]|(\s+))|(\\[\p{IsBasicLatin}]))*&quot;))))*)"
/>
</xs:restriction>
</xs:simpleType>
<xs:simpleType name="ST_Extension">
<xs:restriction base="xs:string">
<xs:pattern
value="([!$&amp;'\(\)\*\+,:=]|(%[0-9a-fA-F][0-9a-fA-F])|[:@]|[a-zA-Z0-9\-_~])+"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema targetNamespace="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns="http://schemas.openxmlformats.org/package/2006/metadata/core-properties"
xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:dcterms="http://purl.org/dc/terms/" elementFormDefault="qualified" blockDefault="#all">
<xs:import namespace="http://purl.org/dc/elements/1.1/"
schemaLocation="http://dublincore.org/schemas/xmls/qdc/2003/04/02/dc.xsd"/>
<xs:import namespace="http://purl.org/dc/terms/"
schemaLocation="http://dublincore.org/schemas/xmls/qdc/2003/04/02/dcterms.xsd"/>
<xs:import id="xml" namespace="http://www.w3.org/XML/1998/namespace"/>
<xs:element name="coreProperties" type="CT_CoreProperties"/>
<xs:complexType name="CT_CoreProperties">
<xs:all>
<xs:element name="category" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="contentStatus" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element ref="dcterms:created" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:creator" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:description" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:identifier" minOccurs="0" maxOccurs="1"/>
<xs:element name="keywords" minOccurs="0" maxOccurs="1" type="CT_Keywords"/>
<xs:element ref="dc:language" minOccurs="0" maxOccurs="1"/>
<xs:element name="lastModifiedBy" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element name="lastPrinted" minOccurs="0" maxOccurs="1" type="xs:dateTime"/>
<xs:element ref="dcterms:modified" minOccurs="0" maxOccurs="1"/>
<xs:element name="revision" minOccurs="0" maxOccurs="1" type="xs:string"/>
<xs:element ref="dc:subject" minOccurs="0" maxOccurs="1"/>
<xs:element ref="dc:title" minOccurs="0" maxOccurs="1"/>
<xs:element name="version" minOccurs="0" maxOccurs="1" type="xs:string"/>
</xs:all>
</xs:complexType>
<xs:complexType name="CT_Keywords" mixed="true">
<xs:sequence>
<xs:element name="value" minOccurs="0" maxOccurs="unbounded" type="CT_Keyword"/>
</xs:sequence>
<xs:attribute ref="xml:lang" use="optional"/>
</xs:complexType>
<xs:complexType name="CT_Keyword">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute ref="xml:lang" use="optional"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:schema>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://schemas.openxmlformats.org/package/2006/digital-signature"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://schemas.openxmlformats.org/package/2006/digital-signature"
elementFormDefault="qualified" attributeFormDefault="unqualified" blockDefault="#all">
<xsd:element name="SignatureTime" type="CT_SignatureTime"/>
<xsd:element name="RelationshipReference" type="CT_RelationshipReference"/>
<xsd:element name="RelationshipsGroupReference" type="CT_RelationshipsGroupReference"/>
<xsd:complexType name="CT_SignatureTime">
<xsd:sequence>
<xsd:element name="Format" type="ST_Format"/>
<xsd:element name="Value" type="ST_Value"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_RelationshipReference">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="SourceId" type="xsd:string" use="required"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:complexType name="CT_RelationshipsGroupReference">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="SourceType" type="xsd:anyURI" use="required"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:simpleType name="ST_Format">
<xsd:restriction base="xsd:string">
<xsd:pattern
value="(YYYY)|(YYYY-MM)|(YYYY-MM-DD)|(YYYY-MM-DDThh:mmTZD)|(YYYY-MM-DDThh:mm:ssTZD)|(YYYY-MM-DDThh:mm:ss.sTZD)"
/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_Value">
<xsd:restriction base="xsd:string">
<xsd:pattern
value="(([0-9][0-9][0-9][0-9]))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2))))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1))))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1)))T((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9]))(((\+|-)((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])))|Z))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1)))T((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9]))(((\+|-)((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])))|Z))|(([0-9][0-9][0-9][0-9])-((0[1-9])|(1(0|1|2)))-((0[1-9])|(1[0-9])|(2[0-9])|(3(0|1)))T((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])):(((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9]))\.[0-9])(((\+|-)((0[0-9])|(1[0-9])|(2(0|1|2|3))):((0[0-9])|(1[0-9])|(2[0-9])|(3[0-9])|(4[0-9])|(5[0-9])))|Z))"
/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsd:schema xmlns="http://schemas.openxmlformats.org/package/2006/relationships"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://schemas.openxmlformats.org/package/2006/relationships"
elementFormDefault="qualified" attributeFormDefault="unqualified" blockDefault="#all">
<xsd:element name="Relationships" type="CT_Relationships"/>
<xsd:element name="Relationship" type="CT_Relationship"/>
<xsd:complexType name="CT_Relationships">
<xsd:sequence>
<xsd:element ref="Relationship" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Relationship">
<xsd:simpleContent>
<xsd:extension base="xsd:string">
<xsd:attribute name="TargetMode" type="ST_TargetMode" use="optional"/>
<xsd:attribute name="Target" type="xsd:anyURI" use="required"/>
<xsd:attribute name="Type" type="xsd:anyURI" use="required"/>
<xsd:attribute name="Id" type="xsd:ID" use="required"/>
</xsd:extension>
</xsd:simpleContent>
</xsd:complexType>
<xsd:simpleType name="ST_TargetMode">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="External"/>
<xsd:enumeration value="Internal"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@ -0,0 +1,75 @@
<?xml version="1.0" encoding="utf-8"?>
<xsd:schema xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
attributeFormDefault="unqualified" elementFormDefault="qualified"
targetNamespace="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<!--
This XSD is a modified version of the one found at:
https://github.com/plutext/docx4j/blob/master/xsd/mce/markup-compatibility-2006-MINIMAL.xsd
This XSD has 2 objectives:
1. round tripping @mc:Ignorable
<w:document
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
mc:Ignorable="w14 w15 wp14">
2. enabling AlternateContent to be manipulated in certain elements
(in the unusual case where the content model is xsd:any, it doesn't have to be explicitly added)
See further ECMA-376, 4th Edition, Office Open XML File Formats
Part 3 : Markup Compatibility and Extensibility
-->
<!-- Objective 1 -->
<xsd:attribute name="Ignorable" type="xsd:string" />
<!-- Objective 2 -->
<xsd:attribute name="MustUnderstand" type="xsd:string" />
<xsd:attribute name="ProcessContent" type="xsd:string" />
<!-- An AlternateContent element shall contain one or more Choice child elements, optionally followed by a
Fallback child element. If present, there shall be only one Fallback element, and it shall follow all Choice
elements. -->
<xsd:element name="AlternateContent">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="Choice" minOccurs="0" maxOccurs="unbounded">
<xsd:complexType>
<xsd:sequence>
<xsd:any minOccurs="0" maxOccurs="unbounded"
processContents="strict">
</xsd:any>
</xsd:sequence>
<xsd:attribute name="Requires" type="xsd:string" use="required" />
<xsd:attribute ref="mc:Ignorable" use="optional" />
<xsd:attribute ref="mc:MustUnderstand" use="optional" />
<xsd:attribute ref="mc:ProcessContent" use="optional" />
</xsd:complexType>
</xsd:element>
<xsd:element name="Fallback" minOccurs="0" maxOccurs="1">
<xsd:complexType>
<xsd:sequence>
<xsd:any minOccurs="0" maxOccurs="unbounded"
processContents="strict">
</xsd:any>
</xsd:sequence>
<xsd:attribute ref="mc:Ignorable" use="optional" />
<xsd:attribute ref="mc:MustUnderstand" use="optional" />
<xsd:attribute ref="mc:ProcessContent" use="optional" />
</xsd:complexType>
</xsd:element>
</xsd:sequence>
<!-- AlternateContent elements might include the attributes Ignorable,
MustUnderstand and ProcessContent described in this Part of ECMA-376. These
attributes qualified names shall be prefixed when associated with an AlternateContent
element. -->
<xsd:attribute ref="mc:Ignorable" use="optional" />
<xsd:attribute ref="mc:MustUnderstand" use="optional" />
<xsd:attribute ref="mc:ProcessContent" use="optional" />
</xsd:complexType>
</xsd:element>
</xsd:schema>

View File

@ -0,0 +1,560 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:w12="http://schemas.openxmlformats.org/wordprocessingml/2006/main" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns="http://schemas.microsoft.com/office/word/2010/wordml" targetNamespace="http://schemas.microsoft.com/office/word/2010/wordml">
<!-- <xsd:import id="rel" namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships" schemaLocation="orel.xsd"/> -->
<xsd:import id="w" namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main" schemaLocation="../ISO-IEC29500-4_2016/wml.xsd"/>
<!-- <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main" schemaLocation="oartbasetypes.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main" schemaLocation="oartsplineproperties.xsd"/> -->
<xsd:complexType name="CT_LongHexNumber">
<xsd:attribute name="val" type="w:ST_LongHexNumber" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_OnOff">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="true"/>
<xsd:enumeration value="false"/>
<xsd:enumeration value="0"/>
<xsd:enumeration value="1"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_OnOff">
<xsd:attribute name="val" type="ST_OnOff"/>
</xsd:complexType>
<xsd:element name="docId" type="CT_LongHexNumber"/>
<xsd:element name="conflictMode" type="CT_OnOff"/>
<xsd:attributeGroup name="AG_Parids">
<xsd:attribute name="paraId" type="w:ST_LongHexNumber"/>
<xsd:attribute name="textId" type="w:ST_LongHexNumber"/>
</xsd:attributeGroup>
<xsd:attribute name="anchorId" type="w:ST_LongHexNumber"/>
<xsd:attribute name="noSpellErr" type="ST_OnOff"/>
<xsd:element name="customXmlConflictInsRangeStart" type="w:CT_TrackChange"/>
<xsd:element name="customXmlConflictInsRangeEnd" type="w:CT_Markup"/>
<xsd:element name="customXmlConflictDelRangeStart" type="w:CT_TrackChange"/>
<xsd:element name="customXmlConflictDelRangeEnd" type="w:CT_Markup"/>
<xsd:group name="EG_RunLevelConflicts">
<xsd:sequence>
<xsd:element name="conflictIns" type="w:CT_RunTrackChange" minOccurs="0"/>
<xsd:element name="conflictDel" type="w:CT_RunTrackChange" minOccurs="0"/>
</xsd:sequence>
</xsd:group>
<xsd:group name="EG_Conflicts">
<xsd:choice>
<xsd:element name="conflictIns" type="w:CT_TrackChange" minOccurs="0"/>
<xsd:element name="conflictDel" type="w:CT_TrackChange" minOccurs="0"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_Percentage">
<xsd:attribute name="val" type="a:ST_Percentage" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_PositiveFixedPercentage">
<xsd:attribute name="val" type="a:ST_PositiveFixedPercentage" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_PositivePercentage">
<xsd:attribute name="val" type="a:ST_PositivePercentage" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_SchemeColorVal">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="bg1"/>
<xsd:enumeration value="tx1"/>
<xsd:enumeration value="bg2"/>
<xsd:enumeration value="tx2"/>
<xsd:enumeration value="accent1"/>
<xsd:enumeration value="accent2"/>
<xsd:enumeration value="accent3"/>
<xsd:enumeration value="accent4"/>
<xsd:enumeration value="accent5"/>
<xsd:enumeration value="accent6"/>
<xsd:enumeration value="hlink"/>
<xsd:enumeration value="folHlink"/>
<xsd:enumeration value="dk1"/>
<xsd:enumeration value="lt1"/>
<xsd:enumeration value="dk2"/>
<xsd:enumeration value="lt2"/>
<xsd:enumeration value="phClr"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_RectAlignment">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="tl"/>
<xsd:enumeration value="t"/>
<xsd:enumeration value="tr"/>
<xsd:enumeration value="l"/>
<xsd:enumeration value="ctr"/>
<xsd:enumeration value="r"/>
<xsd:enumeration value="bl"/>
<xsd:enumeration value="b"/>
<xsd:enumeration value="br"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PathShadeType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="shape"/>
<xsd:enumeration value="circle"/>
<xsd:enumeration value="rect"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_LineCap">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="rnd"/>
<xsd:enumeration value="sq"/>
<xsd:enumeration value="flat"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PresetLineDashVal">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="solid"/>
<xsd:enumeration value="dot"/>
<xsd:enumeration value="sysDot"/>
<xsd:enumeration value="dash"/>
<xsd:enumeration value="sysDash"/>
<xsd:enumeration value="lgDash"/>
<xsd:enumeration value="dashDot"/>
<xsd:enumeration value="sysDashDot"/>
<xsd:enumeration value="lgDashDot"/>
<xsd:enumeration value="lgDashDotDot"/>
<xsd:enumeration value="sysDashDotDot"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_PenAlignment">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="ctr"/>
<xsd:enumeration value="in"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_CompoundLine">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="sng"/>
<xsd:enumeration value="dbl"/>
<xsd:enumeration value="thickThin"/>
<xsd:enumeration value="thinThick"/>
<xsd:enumeration value="tri"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_RelativeRect">
<xsd:attribute name="l" use="optional" type="a:ST_Percentage"/>
<xsd:attribute name="t" use="optional" type="a:ST_Percentage"/>
<xsd:attribute name="r" use="optional" type="a:ST_Percentage"/>
<xsd:attribute name="b" use="optional" type="a:ST_Percentage"/>
</xsd:complexType>
<xsd:group name="EG_ColorTransform">
<xsd:choice>
<xsd:element name="tint" type="CT_PositiveFixedPercentage"/>
<xsd:element name="shade" type="CT_PositiveFixedPercentage"/>
<xsd:element name="alpha" type="CT_PositiveFixedPercentage"/>
<xsd:element name="hueMod" type="CT_PositivePercentage"/>
<xsd:element name="sat" type="CT_Percentage"/>
<xsd:element name="satOff" type="CT_Percentage"/>
<xsd:element name="satMod" type="CT_Percentage"/>
<xsd:element name="lum" type="CT_Percentage"/>
<xsd:element name="lumOff" type="CT_Percentage"/>
<xsd:element name="lumMod" type="CT_Percentage"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_SRgbColor">
<xsd:sequence>
<xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="val" type="s:ST_HexColorRGB" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_SchemeColor">
<xsd:sequence>
<xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
<xsd:attribute name="val" type="ST_SchemeColorVal" use="required"/>
</xsd:complexType>
<xsd:group name="EG_ColorChoice">
<xsd:choice>
<xsd:element name="srgbClr" type="CT_SRgbColor"/>
<xsd:element name="schemeClr" type="CT_SchemeColor"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_Color">
<xsd:sequence>
<xsd:group ref="EG_ColorChoice"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GradientStop">
<xsd:sequence>
<xsd:group ref="EG_ColorChoice"/>
</xsd:sequence>
<xsd:attribute name="pos" type="a:ST_PositiveFixedPercentage" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_GradientStopList">
<xsd:sequence>
<xsd:element name="gs" type="CT_GradientStop" minOccurs="2" maxOccurs="10"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_LinearShadeProperties">
<xsd:attribute name="ang" type="a:ST_PositiveFixedAngle" use="optional"/>
<xsd:attribute name="scaled" type="ST_OnOff" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_PathShadeProperties">
<xsd:sequence>
<xsd:element name="fillToRect" type="CT_RelativeRect" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="path" type="ST_PathShadeType" use="optional"/>
</xsd:complexType>
<xsd:group name="EG_ShadeProperties">
<xsd:choice>
<xsd:element name="lin" type="CT_LinearShadeProperties"/>
<xsd:element name="path" type="CT_PathShadeProperties"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_SolidColorFillProperties">
<xsd:sequence>
<xsd:group ref="EG_ColorChoice" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_GradientFillProperties">
<xsd:sequence>
<xsd:element name="gsLst" type="CT_GradientStopList" minOccurs="0"/>
<xsd:group ref="EG_ShadeProperties" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_FillProperties">
<xsd:choice>
<xsd:element name="noFill" type="w:CT_Empty"/>
<xsd:element name="solidFill" type="CT_SolidColorFillProperties"/>
<xsd:element name="gradFill" type="CT_GradientFillProperties"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_PresetLineDashProperties">
<xsd:attribute name="val" type="ST_PresetLineDashVal" use="optional"/>
</xsd:complexType>
<xsd:group name="EG_LineDashProperties">
<xsd:choice>
<xsd:element name="prstDash" type="CT_PresetLineDashProperties"/>
</xsd:choice>
</xsd:group>
<xsd:complexType name="CT_LineJoinMiterProperties">
<xsd:attribute name="lim" type="a:ST_PositivePercentage" use="optional"/>
</xsd:complexType>
<xsd:group name="EG_LineJoinProperties">
<xsd:choice>
<xsd:element name="round" type="w:CT_Empty"/>
<xsd:element name="bevel" type="w:CT_Empty"/>
<xsd:element name="miter" type="CT_LineJoinMiterProperties"/>
</xsd:choice>
</xsd:group>
<xsd:simpleType name="ST_PresetCameraType">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="legacyObliqueTopLeft"/>
<xsd:enumeration value="legacyObliqueTop"/>
<xsd:enumeration value="legacyObliqueTopRight"/>
<xsd:enumeration value="legacyObliqueLeft"/>
<xsd:enumeration value="legacyObliqueFront"/>
<xsd:enumeration value="legacyObliqueRight"/>
<xsd:enumeration value="legacyObliqueBottomLeft"/>
<xsd:enumeration value="legacyObliqueBottom"/>
<xsd:enumeration value="legacyObliqueBottomRight"/>
<xsd:enumeration value="legacyPerspectiveTopLeft"/>
<xsd:enumeration value="legacyPerspectiveTop"/>
<xsd:enumeration value="legacyPerspectiveTopRight"/>
<xsd:enumeration value="legacyPerspectiveLeft"/>
<xsd:enumeration value="legacyPerspectiveFront"/>
<xsd:enumeration value="legacyPerspectiveRight"/>
<xsd:enumeration value="legacyPerspectiveBottomLeft"/>
<xsd:enumeration value="legacyPerspectiveBottom"/>
<xsd:enumeration value="legacyPerspectiveBottomRight"/>
<xsd:enumeration value="orthographicFront"/>
<xsd:enumeration value="isometricTopUp"/>
<xsd:enumeration value="isometricTopDown"/>
<xsd:enumeration value="isometricBottomUp"/>
<xsd:enumeration value="isometricBottomDown"/>
<xsd:enumeration value="isometricLeftUp"/>
<xsd:enumeration value="isometricLeftDown"/>
<xsd:enumeration value="isometricRightUp"/>
<xsd:enumeration value="isometricRightDown"/>
<xsd:enumeration value="isometricOffAxis1Left"/>
<xsd:enumeration value="isometricOffAxis1Right"/>
<xsd:enumeration value="isometricOffAxis1Top"/>
<xsd:enumeration value="isometricOffAxis2Left"/>
<xsd:enumeration value="isometricOffAxis2Right"/>
<xsd:enumeration value="isometricOffAxis2Top"/>
<xsd:enumeration value="isometricOffAxis3Left"/>
<xsd:enumeration value="isometricOffAxis3Right"/>
<xsd:enumeration value="isometricOffAxis3Bottom"/>
<xsd:enumeration value="isometricOffAxis4Left"/>
<xsd:enumeration value="isometricOffAxis4Right"/>
<xsd:enumeration value="isometricOffAxis4Bottom"/>
<xsd:enumeration value="obliqueTopLeft"/>
<xsd:enumeration value="obliqueTop"/>
<xsd:enumeration value="obliqueTopRight"/>
<xsd:enumeration value="obliqueLeft"/>
<xsd:enumeration value="obliqueRight"/>
<xsd:enumeration value="obliqueBottomLeft"/>
<xsd:enumeration value="obliqueBottom"/>
<xsd:enumeration value="obliqueBottomRight"/>
<xsd:enumeration value="perspectiveFront"/>
<xsd:enumeration value="perspectiveLeft"/>
<xsd:enumeration value="perspectiveRight"/>
<xsd:enumeration value="perspectiveAbove"/>
<xsd:enumeration value="perspectiveBelow"/>
<xsd:enumeration value="perspectiveAboveLeftFacing"/>
<xsd:enumeration value="perspectiveAboveRightFacing"/>
<xsd:enumeration value="perspectiveContrastingLeftFacing"/>
<xsd:enumeration value="perspectiveContrastingRightFacing"/>
<xsd:enumeration value="perspectiveHeroicLeftFacing"/>
<xsd:enumeration value="perspectiveHeroicRightFacing"/>
<xsd:enumeration value="perspectiveHeroicExtremeLeftFacing"/>
<xsd:enumeration value="perspectiveHeroicExtremeRightFacing"/>
<xsd:enumeration value="perspectiveRelaxed"/>
<xsd:enumeration value="perspectiveRelaxedModerately"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Camera">
<xsd:attribute name="prst" use="required" type="ST_PresetCameraType"/>
</xsd:complexType>
<xsd:complexType name="CT_SphereCoords">
<xsd:attribute name="lat" type="a:ST_PositiveFixedAngle" use="required"/>
<xsd:attribute name="lon" type="a:ST_PositiveFixedAngle" use="required"/>
<xsd:attribute name="rev" type="a:ST_PositiveFixedAngle" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_LightRigType">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="legacyFlat1"/>
<xsd:enumeration value="legacyFlat2"/>
<xsd:enumeration value="legacyFlat3"/>
<xsd:enumeration value="legacyFlat4"/>
<xsd:enumeration value="legacyNormal1"/>
<xsd:enumeration value="legacyNormal2"/>
<xsd:enumeration value="legacyNormal3"/>
<xsd:enumeration value="legacyNormal4"/>
<xsd:enumeration value="legacyHarsh1"/>
<xsd:enumeration value="legacyHarsh2"/>
<xsd:enumeration value="legacyHarsh3"/>
<xsd:enumeration value="legacyHarsh4"/>
<xsd:enumeration value="threePt"/>
<xsd:enumeration value="balanced"/>
<xsd:enumeration value="soft"/>
<xsd:enumeration value="harsh"/>
<xsd:enumeration value="flood"/>
<xsd:enumeration value="contrasting"/>
<xsd:enumeration value="morning"/>
<xsd:enumeration value="sunrise"/>
<xsd:enumeration value="sunset"/>
<xsd:enumeration value="chilly"/>
<xsd:enumeration value="freezing"/>
<xsd:enumeration value="flat"/>
<xsd:enumeration value="twoPt"/>
<xsd:enumeration value="glow"/>
<xsd:enumeration value="brightRoom"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ST_LightRigDirection">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="tl"/>
<xsd:enumeration value="t"/>
<xsd:enumeration value="tr"/>
<xsd:enumeration value="l"/>
<xsd:enumeration value="r"/>
<xsd:enumeration value="bl"/>
<xsd:enumeration value="b"/>
<xsd:enumeration value="br"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_LightRig">
<xsd:sequence>
<xsd:element name="rot" type="CT_SphereCoords" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="rig" type="ST_LightRigType" use="required"/>
<xsd:attribute name="dir" type="ST_LightRigDirection" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_BevelPresetType">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="relaxedInset"/>
<xsd:enumeration value="circle"/>
<xsd:enumeration value="slope"/>
<xsd:enumeration value="cross"/>
<xsd:enumeration value="angle"/>
<xsd:enumeration value="softRound"/>
<xsd:enumeration value="convex"/>
<xsd:enumeration value="coolSlant"/>
<xsd:enumeration value="divot"/>
<xsd:enumeration value="riblet"/>
<xsd:enumeration value="hardEdge"/>
<xsd:enumeration value="artDeco"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Bevel">
<xsd:attribute name="w" type="a:ST_PositiveCoordinate" use="optional"/>
<xsd:attribute name="h" type="a:ST_PositiveCoordinate" use="optional"/>
<xsd:attribute name="prst" type="ST_BevelPresetType" use="optional"/>
</xsd:complexType>
<xsd:simpleType name="ST_PresetMaterialType">
<xsd:restriction base="xsd:token">
<xsd:enumeration value="legacyMatte"/>
<xsd:enumeration value="legacyPlastic"/>
<xsd:enumeration value="legacyMetal"/>
<xsd:enumeration value="legacyWireframe"/>
<xsd:enumeration value="matte"/>
<xsd:enumeration value="plastic"/>
<xsd:enumeration value="metal"/>
<xsd:enumeration value="warmMatte"/>
<xsd:enumeration value="translucentPowder"/>
<xsd:enumeration value="powder"/>
<xsd:enumeration value="dkEdge"/>
<xsd:enumeration value="softEdge"/>
<xsd:enumeration value="clear"/>
<xsd:enumeration value="flat"/>
<xsd:enumeration value="softmetal"/>
<xsd:enumeration value="none"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Glow">
<xsd:sequence>
<xsd:group ref="EG_ColorChoice"/>
</xsd:sequence>
<xsd:attribute name="rad" use="optional" type="a:ST_PositiveCoordinate"/>
</xsd:complexType>
<xsd:complexType name="CT_Shadow">
<xsd:sequence>
<xsd:group ref="EG_ColorChoice"/>
</xsd:sequence>
<xsd:attribute name="blurRad" use="optional" type="a:ST_PositiveCoordinate"/>
<xsd:attribute name="dist" use="optional" type="a:ST_PositiveCoordinate"/>
<xsd:attribute name="dir" use="optional" type="a:ST_PositiveFixedAngle"/>
<xsd:attribute name="sx" use="optional" type="a:ST_Percentage"/>
<xsd:attribute name="sy" use="optional" type="a:ST_Percentage"/>
<xsd:attribute name="kx" use="optional" type="a:ST_FixedAngle"/>
<xsd:attribute name="ky" use="optional" type="a:ST_FixedAngle"/>
<xsd:attribute name="algn" use="optional" type="ST_RectAlignment"/>
</xsd:complexType>
<xsd:complexType name="CT_Reflection">
<xsd:attribute name="blurRad" use="optional" type="a:ST_PositiveCoordinate"/>
<xsd:attribute name="stA" use="optional" type="a:ST_PositiveFixedPercentage"/>
<xsd:attribute name="stPos" use="optional" type="a:ST_PositiveFixedPercentage"/>
<xsd:attribute name="endA" use="optional" type="a:ST_PositiveFixedPercentage"/>
<xsd:attribute name="endPos" use="optional" type="a:ST_PositiveFixedPercentage"/>
<xsd:attribute name="dist" use="optional" type="a:ST_PositiveCoordinate"/>
<xsd:attribute name="dir" use="optional" type="a:ST_PositiveFixedAngle"/>
<xsd:attribute name="fadeDir" use="optional" type="a:ST_PositiveFixedAngle"/>
<xsd:attribute name="sx" use="optional" type="a:ST_Percentage"/>
<xsd:attribute name="sy" use="optional" type="a:ST_Percentage"/>
<xsd:attribute name="kx" use="optional" type="a:ST_FixedAngle"/>
<xsd:attribute name="ky" use="optional" type="a:ST_FixedAngle"/>
<xsd:attribute name="algn" use="optional" type="ST_RectAlignment"/>
</xsd:complexType>
<xsd:complexType name="CT_FillTextEffect">
<xsd:sequence>
<xsd:group ref="EG_FillProperties" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_TextOutlineEffect">
<xsd:sequence>
<xsd:group ref="EG_FillProperties" minOccurs="0"/>
<xsd:group ref="EG_LineDashProperties" minOccurs="0"/>
<xsd:group ref="EG_LineJoinProperties" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="w" use="optional" type="a:ST_LineWidth"/>
<xsd:attribute name="cap" use="optional" type="ST_LineCap"/>
<xsd:attribute name="cmpd" use="optional" type="ST_CompoundLine"/>
<xsd:attribute name="algn" use="optional" type="ST_PenAlignment"/>
</xsd:complexType>
<xsd:complexType name="CT_Scene3D">
<xsd:sequence>
<xsd:element name="camera" type="CT_Camera"/>
<xsd:element name="lightRig" type="CT_LightRig"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_Props3D">
<xsd:sequence>
<xsd:element name="bevelT" type="CT_Bevel" minOccurs="0"/>
<xsd:element name="bevelB" type="CT_Bevel" minOccurs="0"/>
<xsd:element name="extrusionClr" type="CT_Color" minOccurs="0"/>
<xsd:element name="contourClr" type="CT_Color" minOccurs="0"/>
</xsd:sequence>
<xsd:attribute name="extrusionH" type="a:ST_PositiveCoordinate" use="optional"/>
<xsd:attribute name="contourW" type="a:ST_PositiveCoordinate" use="optional"/>
<xsd:attribute name="prstMaterial" type="ST_PresetMaterialType" use="optional"/>
</xsd:complexType>
<xsd:group name="EG_RPrTextEffects">
<xsd:sequence>
<xsd:element name="glow" minOccurs="0" type="CT_Glow"/>
<xsd:element name="shadow" minOccurs="0" type="CT_Shadow"/>
<xsd:element name="reflection" minOccurs="0" type="CT_Reflection"/>
<xsd:element name="textOutline" minOccurs="0" type="CT_TextOutlineEffect"/>
<xsd:element name="textFill" minOccurs="0" type="CT_FillTextEffect"/>
<xsd:element name="scene3d" minOccurs="0" type="CT_Scene3D"/>
<xsd:element name="props3d" minOccurs="0" type="CT_Props3D"/>
</xsd:sequence>
</xsd:group>
<xsd:simpleType name="ST_Ligatures">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="none"/>
<xsd:enumeration value="standard"/>
<xsd:enumeration value="contextual"/>
<xsd:enumeration value="historical"/>
<xsd:enumeration value="discretional"/>
<xsd:enumeration value="standardContextual"/>
<xsd:enumeration value="standardHistorical"/>
<xsd:enumeration value="contextualHistorical"/>
<xsd:enumeration value="standardDiscretional"/>
<xsd:enumeration value="contextualDiscretional"/>
<xsd:enumeration value="historicalDiscretional"/>
<xsd:enumeration value="standardContextualHistorical"/>
<xsd:enumeration value="standardContextualDiscretional"/>
<xsd:enumeration value="standardHistoricalDiscretional"/>
<xsd:enumeration value="contextualHistoricalDiscretional"/>
<xsd:enumeration value="all"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Ligatures">
<xsd:attribute name="val" type="ST_Ligatures" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_NumForm">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="default"/>
<xsd:enumeration value="lining"/>
<xsd:enumeration value="oldStyle"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_NumForm">
<xsd:attribute name="val" type="ST_NumForm" use="required"/>
</xsd:complexType>
<xsd:simpleType name="ST_NumSpacing">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="default"/>
<xsd:enumeration value="proportional"/>
<xsd:enumeration value="tabular"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_NumSpacing">
<xsd:attribute name="val" type="ST_NumSpacing" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_StyleSet">
<xsd:attribute name="id" type="s:ST_UnsignedDecimalNumber" use="required"/>
<xsd:attribute name="val" type="ST_OnOff" use="optional"/>
</xsd:complexType>
<xsd:complexType name="CT_StylisticSets">
<xsd:sequence minOccurs="0">
<xsd:element name="styleSet" minOccurs="0" maxOccurs="unbounded" type="CT_StyleSet"/>
</xsd:sequence>
</xsd:complexType>
<xsd:group name="EG_RPrOpenType">
<xsd:sequence>
<xsd:element name="ligatures" minOccurs="0" type="CT_Ligatures"/>
<xsd:element name="numForm" minOccurs="0" type="CT_NumForm"/>
<xsd:element name="numSpacing" minOccurs="0" type="CT_NumSpacing"/>
<xsd:element name="stylisticSets" minOccurs="0" type="CT_StylisticSets"/>
<xsd:element name="cntxtAlts" minOccurs="0" type="CT_OnOff"/>
</xsd:sequence>
</xsd:group>
<xsd:element name="discardImageEditingData" type="CT_OnOff"/>
<xsd:element name="defaultImageDpi" type="CT_DefaultImageDpi"/>
<xsd:complexType name="CT_DefaultImageDpi">
<xsd:attribute name="val" type="w:ST_DecimalNumber" use="required"/>
</xsd:complexType>
<xsd:element name="entityPicker" type="w:CT_Empty"/>
<xsd:complexType name="CT_SdtCheckboxSymbol">
<xsd:attribute name="font" type="s:ST_String"/>
<xsd:attribute name="val" type="w:ST_ShortHexNumber"/>
</xsd:complexType>
<xsd:complexType name="CT_SdtCheckbox">
<xsd:sequence>
<xsd:element name="checked" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="checkedState" type="CT_SdtCheckboxSymbol" minOccurs="0"/>
<xsd:element name="uncheckedState" type="CT_SdtCheckboxSymbol" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:element name="checkbox" type="CT_SdtCheckbox"/>
</xsd:schema>

View File

@ -0,0 +1,67 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:w12="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" xmlns="http://schemas.microsoft.com/office/word/2012/wordml" targetNamespace="http://schemas.microsoft.com/office/word/2012/wordml">
<xsd:import id="w12" namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main" schemaLocation="../ISO-IEC29500-4_2016/wml.xsd"/>
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" schemaLocation="../ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd"/>
<xsd:element name="color" type="w12:CT_Color"/>
<xsd:simpleType name="ST_SdtAppearance">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="boundingBox"/>
<xsd:enumeration value="tags"/>
<xsd:enumeration value="hidden"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:element name="dataBinding" type="w12:CT_DataBinding"/>
<xsd:complexType name="CT_SdtAppearance">
<xsd:attribute name="val" type="ST_SdtAppearance"/>
</xsd:complexType>
<xsd:element name="appearance" type="CT_SdtAppearance"/>
<xsd:complexType name="CT_CommentsEx">
<xsd:sequence>
<xsd:element name="commentEx" type="CT_CommentEx" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_CommentEx">
<xsd:attribute name="paraId" type="w12:ST_LongHexNumber" use="required"/>
<xsd:attribute name="paraIdParent" type="w12:ST_LongHexNumber" use="optional"/>
<xsd:attribute name="done" type="s:ST_OnOff" use="optional"/>
</xsd:complexType>
<xsd:element name="commentsEx" type="CT_CommentsEx"/>
<xsd:complexType name="CT_People">
<xsd:sequence>
<xsd:element name="person" type="CT_Person" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_PresenceInfo">
<xsd:attribute name="providerId" type="xsd:string" use="required"/>
<xsd:attribute name="userId" type="xsd:string" use="required"/>
</xsd:complexType>
<xsd:complexType name="CT_Person">
<xsd:sequence>
<xsd:element name="presenceInfo" type="CT_PresenceInfo" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="author" type="s:ST_String" use="required"/>
</xsd:complexType>
<xsd:element name="people" type="CT_People"/>
<xsd:complexType name="CT_SdtRepeatedSection">
<xsd:sequence>
<xsd:element name="sectionTitle" type="w12:CT_String" minOccurs="0"/>
<xsd:element name="doNotAllowInsertDeleteSection" type="w12:CT_OnOff" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="ST_Guid">
<xsd:restriction base="xsd:token">
<xsd:pattern value="\{[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}\}"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:complexType name="CT_Guid">
<xsd:attribute name="val" type="ST_Guid"/>
</xsd:complexType>
<xsd:element name="repeatingSection" type="CT_SdtRepeatedSection"/>
<xsd:element name="repeatingSectionItem" type="w12:CT_Empty"/>
<xsd:element name="chartTrackingRefBased" type="w12:CT_OnOff"/>
<xsd:element name="collapsed" type="w12:CT_OnOff"/>
<xsd:element name="docId" type="CT_Guid"/>
<xsd:element name="footnoteColumns" type="w12:CT_DecimalNumber"/>
<xsd:element name="webExtensionLinked" type="w12:CT_OnOff"/>
<xsd:element name="webExtensionCreated" type="w12:CT_OnOff"/>
<xsd:attribute name="restartNumberingAfterBreak" type="s:ST_OnOff"/>
</xsd:schema>

View File

@ -0,0 +1,14 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:w12="http://schemas.openxmlformats.org/wordprocessingml/2006/main" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" xmlns="http://schemas.microsoft.com/office/word/2018/wordml" targetNamespace="http://schemas.microsoft.com/office/word/2018/wordml">
<xsd:import id="w12" namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main" schemaLocation="../ISO-IEC29500-4_2016/wml.xsd"/>
<xsd:complexType name="CT_Extension">
<xsd:sequence>
<xsd:any processContents="lax"/>
</xsd:sequence>
<xsd:attribute name="uri" type="xsd:token"/>
</xsd:complexType>
<xsd:complexType name="CT_ExtensionList">
<xsd:sequence>
<xsd:element name="ext" type="CT_Extension" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:schema>

View File

@ -0,0 +1,20 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" xmlns="http://schemas.microsoft.com/office/word/2018/wordml/cex" targetNamespace="http://schemas.microsoft.com/office/word/2018/wordml/cex">
<xsd:import id="w16" namespace="http://schemas.microsoft.com/office/word/2018/wordml" schemaLocation="wml-2018.xsd"/>
<xsd:import id="w" namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main" schemaLocation="../ISO-IEC29500-4_2016/wml.xsd"/>
<xsd:import id="s" namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" schemaLocation="../ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd"/>
<xsd:complexType name="CT_CommentsExtensible">
<xsd:sequence>
<xsd:element name="commentExtensible" type="CT_CommentExtensible" minOccurs="0" maxOccurs="unbounded"/>
<xsd:element name="extLst" type="w16:CT_ExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_CommentExtensible">
<xsd:sequence>
<xsd:element name="extLst" type="w16:CT_ExtensionList" minOccurs="0" maxOccurs="1"/>
</xsd:sequence>
<xsd:attribute name="durableId" type="w:ST_LongHexNumber" use="required"/>
<xsd:attribute name="dateUtc" type="w:ST_DateTime" use="optional"/>
<xsd:attribute name="intelligentPlaceholder" type="s:ST_OnOff" use="optional"/>
</xsd:complexType>
<xsd:element name="commentsExtensible" type="CT_CommentsExtensible"/>
</xsd:schema>

View File

@ -0,0 +1,13 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:w12="http://schemas.openxmlformats.org/wordprocessingml/2006/main" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" xmlns="http://schemas.microsoft.com/office/word/2016/wordml/cid" targetNamespace="http://schemas.microsoft.com/office/word/2016/wordml/cid">
<xsd:import id="w12" namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main" schemaLocation="../ISO-IEC29500-4_2016/wml.xsd"/>
<xsd:complexType name="CT_CommentsIds">
<xsd:sequence>
<xsd:element name="commentId" type="CT_CommentId" minOccurs="0" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="CT_CommentId">
<xsd:attribute name="paraId" type="w12:ST_LongHexNumber" use="required"/>
<xsd:attribute name="durableId" type="w12:ST_LongHexNumber" use="required"/>
</xsd:complexType>
<xsd:element name="commentsIds" type="CT_CommentsIds"/>
</xsd:schema>

View File

@ -0,0 +1,4 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:w12="http://schemas.openxmlformats.org/wordprocessingml/2006/main" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" xmlns="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" targetNamespace="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash">
<xsd:import id="w12" namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main" schemaLocation="../ISO-IEC29500-4_2016/wml.xsd"/>
<xsd:attribute name="storeItemChecksum" type="w12:ST_String"/>
</xsd:schema>

View File

@ -0,0 +1,8 @@
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:w12="http://schemas.openxmlformats.org/wordprocessingml/2006/main" elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" xmlns="http://schemas.microsoft.com/office/word/2015/wordml/symex" targetNamespace="http://schemas.microsoft.com/office/word/2015/wordml/symex">
<xsd:import id="w12" namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main" schemaLocation="../ISO-IEC29500-4_2016/wml.xsd"/>
<xsd:complexType name="CT_SymEx">
<xsd:attribute name="font" type="w12:ST_String"/>
<xsd:attribute name="char" type="w12:ST_LongHexNumber"/>
</xsd:complexType>
<xsd:element name="symEx" type="CT_SymEx"/>
</xsd:schema>

159
docx/ooxml/scripts/pack.py Executable file
View File

@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""
Tool to pack a directory into a .docx, .pptx, or .xlsx file with XML formatting undone.
Example usage:
python pack.py <input_directory> <office_file> [--force]
"""
import argparse
import shutil
import subprocess
import sys
import tempfile
import defusedxml.minidom
import zipfile
from pathlib import Path
def main():
parser = argparse.ArgumentParser(description="Pack a directory into an Office file")
parser.add_argument("input_directory", help="Unpacked Office document directory")
parser.add_argument("output_file", help="Output Office file (.docx/.pptx/.xlsx)")
parser.add_argument("--force", action="store_true", help="Skip validation")
args = parser.parse_args()
try:
success = pack_document(
args.input_directory, args.output_file, validate=not args.force
)
# Show warning if validation was skipped
if args.force:
print("Warning: Skipped validation, file may be corrupt", file=sys.stderr)
# Exit with error if validation failed
elif not success:
print("Contents would produce a corrupt file.", file=sys.stderr)
print("Please validate XML before repacking.", file=sys.stderr)
print("Use --force to skip validation and pack anyway.", file=sys.stderr)
sys.exit(1)
except ValueError as e:
sys.exit(f"Error: {e}")
def pack_document(input_dir, output_file, validate=False):
"""Pack a directory into an Office file (.docx/.pptx/.xlsx).
Args:
input_dir: Path to unpacked Office document directory
output_file: Path to output Office file
validate: If True, validates with soffice (default: False)
Returns:
bool: True if successful, False if validation failed
"""
input_dir = Path(input_dir)
output_file = Path(output_file)
if not input_dir.is_dir():
raise ValueError(f"{input_dir} is not a directory")
if output_file.suffix.lower() not in {".docx", ".pptx", ".xlsx"}:
raise ValueError(f"{output_file} must be a .docx, .pptx, or .xlsx file")
# Work in temporary directory to avoid modifying original
with tempfile.TemporaryDirectory() as temp_dir:
temp_content_dir = Path(temp_dir) / "content"
shutil.copytree(input_dir, temp_content_dir)
# Process XML files to remove pretty-printing whitespace
for pattern in ["*.xml", "*.rels"]:
for xml_file in temp_content_dir.rglob(pattern):
condense_xml(xml_file)
# Create final Office file as zip archive
output_file.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf:
for f in temp_content_dir.rglob("*"):
if f.is_file():
zf.write(f, f.relative_to(temp_content_dir))
# Validate if requested
if validate:
if not validate_document(output_file):
output_file.unlink() # Delete the corrupt file
return False
return True
def validate_document(doc_path):
"""Validate document by converting to HTML with soffice."""
# Determine the correct filter based on file extension
match doc_path.suffix.lower():
case ".docx":
filter_name = "html:HTML"
case ".pptx":
filter_name = "html:impress_html_Export"
case ".xlsx":
filter_name = "html:HTML (StarCalc)"
with tempfile.TemporaryDirectory() as temp_dir:
try:
result = subprocess.run(
[
"soffice",
"--headless",
"--convert-to",
filter_name,
"--outdir",
temp_dir,
str(doc_path),
],
capture_output=True,
timeout=10,
text=True,
)
if not (Path(temp_dir) / f"{doc_path.stem}.html").exists():
error_msg = result.stderr.strip() or "Document validation failed"
print(f"Validation error: {error_msg}", file=sys.stderr)
return False
return True
except FileNotFoundError:
print("Warning: soffice not found. Skipping validation.", file=sys.stderr)
return True
except subprocess.TimeoutExpired:
print("Validation error: Timeout during conversion", file=sys.stderr)
return False
except Exception as e:
print(f"Validation error: {e}", file=sys.stderr)
return False
def condense_xml(xml_file):
"""Strip unnecessary whitespace and remove comments."""
with open(xml_file, "r", encoding="utf-8") as f:
dom = defusedxml.minidom.parse(f)
# Process each element to remove whitespace and comments
for element in dom.getElementsByTagName("*"):
# Skip w:t elements and their processing
if element.tagName.endswith(":t"):
continue
# Remove whitespace-only text nodes and comment nodes
for child in list(element.childNodes):
if (
child.nodeType == child.TEXT_NODE
and child.nodeValue
and child.nodeValue.strip() == ""
) or child.nodeType == child.COMMENT_NODE:
element.removeChild(child)
# Write back the condensed XML
with open(xml_file, "wb") as f:
f.write(dom.toxml(encoding="UTF-8"))
if __name__ == "__main__":
main()

29
docx/ooxml/scripts/unpack.py Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python3
"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)"""
import random
import sys
import defusedxml.minidom
import zipfile
from pathlib import Path
# Get command line arguments
assert len(sys.argv) == 3, "Usage: python unpack.py <office_file> <output_dir>"
input_file, output_dir = sys.argv[1], sys.argv[2]
# Extract and format
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
zipfile.ZipFile(input_file).extractall(output_path)
# Pretty print all XML files
xml_files = list(output_path.rglob("*.xml")) + list(output_path.rglob("*.rels"))
for xml_file in xml_files:
content = xml_file.read_text(encoding="utf-8")
dom = defusedxml.minidom.parseString(content)
xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii"))
# For .docx files, suggest an RSID for tracked changes
if input_file.endswith(".docx"):
suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8))
print(f"Suggested RSID for edit session: {suggested_rsid}")

69
docx/ooxml/scripts/validate.py Executable file
View File

@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""
Command line tool to validate Office document XML files against XSD schemas and tracked changes.
Usage:
python validate.py <dir> --original <original_file>
"""
import argparse
import sys
from pathlib import Path
from validation import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
def main():
parser = argparse.ArgumentParser(description="Validate Office document XML files")
parser.add_argument(
"unpacked_dir",
help="Path to unpacked Office document directory",
)
parser.add_argument(
"--original",
required=True,
help="Path to original file (.docx/.pptx/.xlsx)",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
)
args = parser.parse_args()
# Validate paths
unpacked_dir = Path(args.unpacked_dir)
original_file = Path(args.original)
file_extension = original_file.suffix.lower()
assert unpacked_dir.is_dir(), f"Error: {unpacked_dir} is not a directory"
assert original_file.is_file(), f"Error: {original_file} is not a file"
assert file_extension in [".docx", ".pptx", ".xlsx"], (
f"Error: {original_file} must be a .docx, .pptx, or .xlsx file"
)
# Run validations
match file_extension:
case ".docx":
validators = [DOCXSchemaValidator, RedliningValidator]
case ".pptx":
validators = [PPTXSchemaValidator]
case _:
print(f"Error: Validation not supported for file type {file_extension}")
sys.exit(1)
# Run validators
success = True
for V in validators:
validator = V(unpacked_dir, original_file, verbose=args.verbose)
if not validator.validate():
success = False
if success:
print("All validations PASSED!")
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,15 @@
"""
Validation modules for Word document processing.
"""
from .base import BaseSchemaValidator
from .docx import DOCXSchemaValidator
from .pptx import PPTXSchemaValidator
from .redlining import RedliningValidator
__all__ = [
"BaseSchemaValidator",
"DOCXSchemaValidator",
"PPTXSchemaValidator",
"RedliningValidator",
]

View File

@ -0,0 +1,951 @@
"""
Base validator with common validation logic for document files.
"""
import re
from pathlib import Path
import lxml.etree
class BaseSchemaValidator:
"""Base validator with common validation logic for document files."""
# Elements whose 'id' attributes must be unique within their file
# Format: element_name -> (attribute_name, scope)
# scope can be 'file' (unique within file) or 'global' (unique across all files)
UNIQUE_ID_REQUIREMENTS = {
# Word elements
"comment": ("id", "file"), # Comment IDs in comments.xml
"commentrangestart": ("id", "file"), # Must match comment IDs
"commentrangeend": ("id", "file"), # Must match comment IDs
"bookmarkstart": ("id", "file"), # Bookmark start IDs
"bookmarkend": ("id", "file"), # Bookmark end IDs
# Note: ins and del (track changes) can share IDs when part of same revision
# PowerPoint elements
"sldid": ("id", "file"), # Slide IDs in presentation.xml
"sldmasterid": ("id", "global"), # Slide master IDs must be globally unique
"sldlayoutid": ("id", "global"), # Slide layout IDs must be globally unique
"cm": ("authorid", "file"), # Comment author IDs
# Excel elements
"sheet": ("sheetid", "file"), # Sheet IDs in workbook.xml
"definedname": ("id", "file"), # Named range IDs
# Drawing/Shape elements (all formats)
"cxnsp": ("id", "file"), # Connection shape IDs
"sp": ("id", "file"), # Shape IDs
"pic": ("id", "file"), # Picture IDs
"grpsp": ("id", "file"), # Group shape IDs
}
# Mapping of element names to expected relationship types
# Subclasses should override this with format-specific mappings
ELEMENT_RELATIONSHIP_TYPES = {}
# Unified schema mappings for all Office document types
SCHEMA_MAPPINGS = {
# Document type specific schemas
"word": "ISO-IEC29500-4_2016/wml.xsd", # Word documents
"ppt": "ISO-IEC29500-4_2016/pml.xsd", # PowerPoint presentations
"xl": "ISO-IEC29500-4_2016/sml.xsd", # Excel spreadsheets
# Common file types
"[Content_Types].xml": "ecma/fouth-edition/opc-contentTypes.xsd",
"app.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd",
"core.xml": "ecma/fouth-edition/opc-coreProperties.xsd",
"custom.xml": "ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd",
".rels": "ecma/fouth-edition/opc-relationships.xsd",
# Word-specific files
"people.xml": "microsoft/wml-2012.xsd",
"commentsIds.xml": "microsoft/wml-cid-2016.xsd",
"commentsExtensible.xml": "microsoft/wml-cex-2018.xsd",
"commentsExtended.xml": "microsoft/wml-2012.xsd",
# Chart files (common across document types)
"chart": "ISO-IEC29500-4_2016/dml-chart.xsd",
# Theme files (common across document types)
"theme": "ISO-IEC29500-4_2016/dml-main.xsd",
# Drawing and media files
"drawing": "ISO-IEC29500-4_2016/dml-main.xsd",
}
# Unified namespace constants
MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006"
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
# Common OOXML namespaces used across validators
PACKAGE_RELATIONSHIPS_NAMESPACE = (
"http://schemas.openxmlformats.org/package/2006/relationships"
)
OFFICE_RELATIONSHIPS_NAMESPACE = (
"http://schemas.openxmlformats.org/officeDocument/2006/relationships"
)
CONTENT_TYPES_NAMESPACE = (
"http://schemas.openxmlformats.org/package/2006/content-types"
)
# Folders where we should clean ignorable namespaces
MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"}
# All allowed OOXML namespaces (superset of all document types)
OOXML_NAMESPACES = {
"http://schemas.openxmlformats.org/officeDocument/2006/math",
"http://schemas.openxmlformats.org/officeDocument/2006/relationships",
"http://schemas.openxmlformats.org/schemaLibrary/2006/main",
"http://schemas.openxmlformats.org/drawingml/2006/main",
"http://schemas.openxmlformats.org/drawingml/2006/chart",
"http://schemas.openxmlformats.org/drawingml/2006/chartDrawing",
"http://schemas.openxmlformats.org/drawingml/2006/diagram",
"http://schemas.openxmlformats.org/drawingml/2006/picture",
"http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing",
"http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing",
"http://schemas.openxmlformats.org/wordprocessingml/2006/main",
"http://schemas.openxmlformats.org/presentationml/2006/main",
"http://schemas.openxmlformats.org/spreadsheetml/2006/main",
"http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes",
"http://www.w3.org/XML/1998/namespace",
}
def __init__(self, unpacked_dir, original_file, verbose=False):
self.unpacked_dir = Path(unpacked_dir).resolve()
self.original_file = Path(original_file)
self.verbose = verbose
# Set schemas directory
self.schemas_dir = Path(__file__).parent.parent.parent / "schemas"
# Get all XML and .rels files
patterns = ["*.xml", "*.rels"]
self.xml_files = [
f for pattern in patterns for f in self.unpacked_dir.rglob(pattern)
]
if not self.xml_files:
print(f"Warning: No XML files found in {self.unpacked_dir}")
def validate(self):
"""Run all validation checks and return True if all pass."""
raise NotImplementedError("Subclasses must implement the validate method")
def validate_xml(self):
"""Validate that all XML files are well-formed."""
errors = []
for xml_file in self.xml_files:
try:
# Try to parse the XML file
lxml.etree.parse(str(xml_file))
except lxml.etree.XMLSyntaxError as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {e.lineno}: {e.msg}"
)
except Exception as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Unexpected error: {str(e)}"
)
if errors:
print(f"FAILED - Found {len(errors)} XML violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All XML files are well-formed")
return True
def validate_namespaces(self):
"""Validate that namespace prefixes in Ignorable attributes are declared."""
errors = []
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
declared = set(root.nsmap.keys()) - {None} # Exclude default namespace
for attr_val in [
v for k, v in root.attrib.items() if k.endswith("Ignorable")
]:
undeclared = set(attr_val.split()) - declared
errors.extend(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Namespace '{ns}' in Ignorable but not declared"
for ns in undeclared
)
except lxml.etree.XMLSyntaxError:
continue
if errors:
print(f"FAILED - {len(errors)} namespace issues:")
for error in errors:
print(error)
return False
if self.verbose:
print("PASSED - All namespace prefixes properly declared")
return True
def validate_unique_ids(self):
"""Validate that specific IDs are unique according to OOXML requirements."""
errors = []
global_ids = {} # Track globally unique IDs across all files
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
file_ids = {} # Track IDs that must be unique within this file
# Remove all mc:AlternateContent elements from the tree
mc_elements = root.xpath(
".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE}
)
for elem in mc_elements:
elem.getparent().remove(elem)
# Now check IDs in the cleaned tree
for elem in root.iter():
# Get the element name without namespace
tag = (
elem.tag.split("}")[-1].lower()
if "}" in elem.tag
else elem.tag.lower()
)
# Check if this element type has ID uniqueness requirements
if tag in self.UNIQUE_ID_REQUIREMENTS:
attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag]
# Look for the specified attribute
id_value = None
for attr, value in elem.attrib.items():
attr_local = (
attr.split("}")[-1].lower()
if "}" in attr
else attr.lower()
)
if attr_local == attr_name:
id_value = value
break
if id_value is not None:
if scope == "global":
# Check global uniqueness
if id_value in global_ids:
prev_file, prev_line, prev_tag = global_ids[
id_value
]
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: Global ID '{id_value}' in <{tag}> "
f"already used in {prev_file} at line {prev_line} in <{prev_tag}>"
)
else:
global_ids[id_value] = (
xml_file.relative_to(self.unpacked_dir),
elem.sourceline,
tag,
)
elif scope == "file":
# Check file-level uniqueness
key = (tag, attr_name)
if key not in file_ids:
file_ids[key] = {}
if id_value in file_ids[key]:
prev_line = file_ids[key][id_value]
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: Duplicate {attr_name}='{id_value}' in <{tag}> "
f"(first occurrence at line {prev_line})"
)
else:
file_ids[key][id_value] = elem.sourceline
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} ID uniqueness violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All required IDs are unique")
return True
def validate_file_references(self):
"""
Validate that all .rels files properly reference files and that all files are referenced.
"""
errors = []
# Find all .rels files
rels_files = list(self.unpacked_dir.rglob("*.rels"))
if not rels_files:
if self.verbose:
print("PASSED - No .rels files found")
return True
# Get all files in the unpacked directory (excluding reference files)
all_files = []
for file_path in self.unpacked_dir.rglob("*"):
if (
file_path.is_file()
and file_path.name != "[Content_Types].xml"
and not file_path.name.endswith(".rels")
): # This file is not referenced by .rels
all_files.append(file_path.resolve())
# Track all files that are referenced by any .rels file
all_referenced_files = set()
if self.verbose:
print(
f"Found {len(rels_files)} .rels files and {len(all_files)} target files"
)
# Check each .rels file
for rels_file in rels_files:
try:
# Parse relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
# Get the directory where this .rels file is located
rels_dir = rels_file.parent
# Find all relationships and their targets
referenced_files = set()
broken_refs = []
for rel in rels_root.findall(
".//ns:Relationship",
namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE},
):
target = rel.get("Target")
if target and not target.startswith(
("http", "mailto:")
): # Skip external URLs
# Resolve the target path relative to the .rels file location
if rels_file.name == ".rels":
# Root .rels file - targets are relative to unpacked_dir
target_path = self.unpacked_dir / target
else:
# Other .rels files - targets are relative to their parent's parent
# e.g., word/_rels/document.xml.rels -> targets relative to word/
base_dir = rels_dir.parent
target_path = base_dir / target
# Normalize the path and check if it exists
try:
target_path = target_path.resolve()
if target_path.exists() and target_path.is_file():
referenced_files.add(target_path)
all_referenced_files.add(target_path)
else:
broken_refs.append((target, rel.sourceline))
except (OSError, ValueError):
broken_refs.append((target, rel.sourceline))
# Report broken references
if broken_refs:
rel_path = rels_file.relative_to(self.unpacked_dir)
for broken_ref, line_num in broken_refs:
errors.append(
f" {rel_path}: Line {line_num}: Broken reference to {broken_ref}"
)
except Exception as e:
rel_path = rels_file.relative_to(self.unpacked_dir)
errors.append(f" Error parsing {rel_path}: {e}")
# Check for unreferenced files (files that exist but are not referenced anywhere)
unreferenced_files = set(all_files) - all_referenced_files
if unreferenced_files:
for unref_file in sorted(unreferenced_files):
unref_rel_path = unref_file.relative_to(self.unpacked_dir)
errors.append(f" Unreferenced file: {unref_rel_path}")
if errors:
print(f"FAILED - Found {len(errors)} relationship validation errors:")
for error in errors:
print(error)
print(
"CRITICAL: These errors will cause the document to appear corrupt. "
+ "Broken references MUST be fixed, "
+ "and unreferenced files MUST be referenced or removed."
)
return False
else:
if self.verbose:
print(
"PASSED - All references are valid and all files are properly referenced"
)
return True
def validate_all_relationship_ids(self):
"""
Validate that all r:id attributes in XML files reference existing IDs
in their corresponding .rels files, and optionally validate relationship types.
"""
import lxml.etree
errors = []
# Process each XML file that might contain r:id references
for xml_file in self.xml_files:
# Skip .rels files themselves
if xml_file.suffix == ".rels":
continue
# Determine the corresponding .rels file
# For dir/file.xml, it's dir/_rels/file.xml.rels
rels_dir = xml_file.parent / "_rels"
rels_file = rels_dir / f"{xml_file.name}.rels"
# Skip if there's no corresponding .rels file (that's okay)
if not rels_file.exists():
continue
try:
# Parse the .rels file to get valid relationship IDs and their types
rels_root = lxml.etree.parse(str(rels_file)).getroot()
rid_to_type = {}
for rel in rels_root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
):
rid = rel.get("Id")
rel_type = rel.get("Type", "")
if rid:
# Check for duplicate rIds
if rid in rid_to_type:
rels_rel_path = rels_file.relative_to(self.unpacked_dir)
errors.append(
f" {rels_rel_path}: Line {rel.sourceline}: "
f"Duplicate relationship ID '{rid}' (IDs must be unique)"
)
# Extract just the type name from the full URL
type_name = (
rel_type.split("/")[-1] if "/" in rel_type else rel_type
)
rid_to_type[rid] = type_name
# Parse the XML file to find all r:id references
xml_root = lxml.etree.parse(str(xml_file)).getroot()
# Find all elements with r:id attributes
for elem in xml_root.iter():
# Check for r:id attribute (relationship ID)
rid_attr = elem.get(f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id")
if rid_attr:
xml_rel_path = xml_file.relative_to(self.unpacked_dir)
elem_name = (
elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
)
# Check if the ID exists
if rid_attr not in rid_to_type:
errors.append(
f" {xml_rel_path}: Line {elem.sourceline}: "
f"<{elem_name}> references non-existent relationship '{rid_attr}' "
f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})"
)
# Check if we have type expectations for this element
elif self.ELEMENT_RELATIONSHIP_TYPES:
expected_type = self._get_expected_relationship_type(
elem_name
)
if expected_type:
actual_type = rid_to_type[rid_attr]
# Check if the actual type matches or contains the expected type
if expected_type not in actual_type.lower():
errors.append(
f" {xml_rel_path}: Line {elem.sourceline}: "
f"<{elem_name}> references '{rid_attr}' which points to '{actual_type}' "
f"but should point to a '{expected_type}' relationship"
)
except Exception as e:
xml_rel_path = xml_file.relative_to(self.unpacked_dir)
errors.append(f" Error processing {xml_rel_path}: {e}")
if errors:
print(f"FAILED - Found {len(errors)} relationship ID reference errors:")
for error in errors:
print(error)
print("\nThese ID mismatches will cause the document to appear corrupt!")
return False
else:
if self.verbose:
print("PASSED - All relationship ID references are valid")
return True
def _get_expected_relationship_type(self, element_name):
"""
Get the expected relationship type for an element.
First checks the explicit mapping, then tries pattern detection.
"""
# Normalize element name to lowercase
elem_lower = element_name.lower()
# Check explicit mapping first
if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES:
return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower]
# Try pattern detection for common patterns
# Pattern 1: Elements ending in "Id" often expect a relationship of the prefix type
if elem_lower.endswith("id") and len(elem_lower) > 2:
# e.g., "sldId" -> "sld", "sldMasterId" -> "sldMaster"
prefix = elem_lower[:-2] # Remove "id"
# Check if this might be a compound like "sldMasterId"
if prefix.endswith("master"):
return prefix.lower()
elif prefix.endswith("layout"):
return prefix.lower()
else:
# Simple case like "sldId" -> "slide"
# Common transformations
if prefix == "sld":
return "slide"
return prefix.lower()
# Pattern 2: Elements ending in "Reference" expect a relationship of the prefix type
if elem_lower.endswith("reference") and len(elem_lower) > 9:
prefix = elem_lower[:-9] # Remove "reference"
return prefix.lower()
return None
def validate_content_types(self):
"""Validate that all content files are properly declared in [Content_Types].xml."""
errors = []
# Find [Content_Types].xml file
content_types_file = self.unpacked_dir / "[Content_Types].xml"
if not content_types_file.exists():
print("FAILED - [Content_Types].xml file not found")
return False
try:
# Parse and get all declared parts and extensions
root = lxml.etree.parse(str(content_types_file)).getroot()
declared_parts = set()
declared_extensions = set()
# Get Override declarations (specific files)
for override in root.findall(
f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Override"
):
part_name = override.get("PartName")
if part_name is not None:
declared_parts.add(part_name.lstrip("/"))
# Get Default declarations (by extension)
for default in root.findall(
f".//{{{self.CONTENT_TYPES_NAMESPACE}}}Default"
):
extension = default.get("Extension")
if extension is not None:
declared_extensions.add(extension.lower())
# Root elements that require content type declaration
declarable_roots = {
"sld",
"sldLayout",
"sldMaster",
"presentation", # PowerPoint
"document", # Word
"workbook",
"worksheet", # Excel
"theme", # Common
}
# Common media file extensions that should be declared
media_extensions = {
"png": "image/png",
"jpg": "image/jpeg",
"jpeg": "image/jpeg",
"gif": "image/gif",
"bmp": "image/bmp",
"tiff": "image/tiff",
"wmf": "image/x-wmf",
"emf": "image/x-emf",
}
# Get all files in the unpacked directory
all_files = list(self.unpacked_dir.rglob("*"))
all_files = [f for f in all_files if f.is_file()]
# Check all XML files for Override declarations
for xml_file in self.xml_files:
path_str = str(xml_file.relative_to(self.unpacked_dir)).replace(
"\\", "/"
)
# Skip non-content files
if any(
skip in path_str
for skip in [".rels", "[Content_Types]", "docProps/", "_rels/"]
):
continue
try:
root_tag = lxml.etree.parse(str(xml_file)).getroot().tag
root_name = root_tag.split("}")[-1] if "}" in root_tag else root_tag
if root_name in declarable_roots and path_str not in declared_parts:
errors.append(
f" {path_str}: File with <{root_name}> root not declared in [Content_Types].xml"
)
except Exception:
continue # Skip unparseable files
# Check all non-XML files for Default extension declarations
for file_path in all_files:
# Skip XML files and metadata files (already checked above)
if file_path.suffix.lower() in {".xml", ".rels"}:
continue
if file_path.name == "[Content_Types].xml":
continue
if "_rels" in file_path.parts or "docProps" in file_path.parts:
continue
extension = file_path.suffix.lstrip(".").lower()
if extension and extension not in declared_extensions:
# Check if it's a known media extension that should be declared
if extension in media_extensions:
relative_path = file_path.relative_to(self.unpacked_dir)
errors.append(
f' {relative_path}: File with extension \'{extension}\' not declared in [Content_Types].xml - should add: <Default Extension="{extension}" ContentType="{media_extensions[extension]}"/>'
)
except Exception as e:
errors.append(f" Error parsing [Content_Types].xml: {e}")
if errors:
print(f"FAILED - Found {len(errors)} content type declaration errors:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print(
"PASSED - All content files are properly declared in [Content_Types].xml"
)
return True
def validate_file_against_xsd(self, xml_file, verbose=False):
"""Validate a single XML file against XSD schema, comparing with original.
Args:
xml_file: Path to XML file to validate
verbose: Enable verbose output
Returns:
tuple: (is_valid, new_errors_set) where is_valid is True/False/None (skipped)
"""
# Resolve both paths to handle symlinks
xml_file = Path(xml_file).resolve()
unpacked_dir = self.unpacked_dir.resolve()
# Validate current file
is_valid, current_errors = self._validate_single_file_xsd(
xml_file, unpacked_dir
)
if is_valid is None:
return None, set() # Skipped
elif is_valid:
return True, set() # Valid, no errors
# Get errors from original file for this specific file
original_errors = self._get_original_file_errors(xml_file)
# Compare with original (both are guaranteed to be sets here)
assert current_errors is not None
new_errors = current_errors - original_errors
if new_errors:
if verbose:
relative_path = xml_file.relative_to(unpacked_dir)
print(f"FAILED - {relative_path}: {len(new_errors)} new error(s)")
for error in list(new_errors)[:3]:
truncated = error[:250] + "..." if len(error) > 250 else error
print(f" - {truncated}")
return False, new_errors
else:
# All errors existed in original
if verbose:
print(
f"PASSED - No new errors (original had {len(current_errors)} errors)"
)
return True, set()
def validate_against_xsd(self):
"""Validate XML files against XSD schemas, showing only new errors compared to original."""
new_errors = []
original_error_count = 0
valid_count = 0
skipped_count = 0
for xml_file in self.xml_files:
relative_path = str(xml_file.relative_to(self.unpacked_dir))
is_valid, new_file_errors = self.validate_file_against_xsd(
xml_file, verbose=False
)
if is_valid is None:
skipped_count += 1
continue
elif is_valid and not new_file_errors:
valid_count += 1
continue
elif is_valid:
# Had errors but all existed in original
original_error_count += 1
valid_count += 1
continue
# Has new errors
new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)")
for error in list(new_file_errors)[:3]: # Show first 3 errors
new_errors.append(
f" - {error[:250]}..." if len(error) > 250 else f" - {error}"
)
# Print summary
if self.verbose:
print(f"Validated {len(self.xml_files)} files:")
print(f" - Valid: {valid_count}")
print(f" - Skipped (no schema): {skipped_count}")
if original_error_count:
print(f" - With original errors (ignored): {original_error_count}")
print(
f" - With NEW errors: {len(new_errors) > 0 and len([e for e in new_errors if not e.startswith(' ')]) or 0}"
)
if new_errors:
print("\nFAILED - Found NEW validation errors:")
for error in new_errors:
print(error)
return False
else:
if self.verbose:
print("\nPASSED - No new XSD validation errors introduced")
return True
def _get_schema_path(self, xml_file):
"""Determine the appropriate schema path for an XML file."""
# Check exact filename match
if xml_file.name in self.SCHEMA_MAPPINGS:
return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name]
# Check .rels files
if xml_file.suffix == ".rels":
return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"]
# Check chart files
if "charts/" in str(xml_file) and xml_file.name.startswith("chart"):
return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"]
# Check theme files
if "theme/" in str(xml_file) and xml_file.name.startswith("theme"):
return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"]
# Check if file is in a main content folder and use appropriate schema
if xml_file.parent.name in self.MAIN_CONTENT_FOLDERS:
return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.parent.name]
return None
def _clean_ignorable_namespaces(self, xml_doc):
"""Remove attributes and elements not in allowed namespaces."""
# Create a clean copy
xml_string = lxml.etree.tostring(xml_doc, encoding="unicode")
xml_copy = lxml.etree.fromstring(xml_string)
# Remove attributes not in allowed namespaces
for elem in xml_copy.iter():
attrs_to_remove = []
for attr in elem.attrib:
# Check if attribute is from a namespace other than allowed ones
if "{" in attr:
ns = attr.split("}")[0][1:]
if ns not in self.OOXML_NAMESPACES:
attrs_to_remove.append(attr)
# Remove collected attributes
for attr in attrs_to_remove:
del elem.attrib[attr]
# Remove elements not in allowed namespaces
self._remove_ignorable_elements(xml_copy)
return lxml.etree.ElementTree(xml_copy)
def _remove_ignorable_elements(self, root):
"""Recursively remove all elements not in allowed namespaces."""
elements_to_remove = []
# Find elements to remove
for elem in list(root):
# Skip non-element nodes (comments, processing instructions, etc.)
if not hasattr(elem, "tag") or callable(elem.tag):
continue
tag_str = str(elem.tag)
if tag_str.startswith("{"):
ns = tag_str.split("}")[0][1:]
if ns not in self.OOXML_NAMESPACES:
elements_to_remove.append(elem)
continue
# Recursively clean child elements
self._remove_ignorable_elements(elem)
# Remove collected elements
for elem in elements_to_remove:
root.remove(elem)
def _preprocess_for_mc_ignorable(self, xml_doc):
"""Preprocess XML to handle mc:Ignorable attribute properly."""
# Remove mc:Ignorable attributes before validation
root = xml_doc.getroot()
# Remove mc:Ignorable attribute from root
if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib:
del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"]
return xml_doc
def _validate_single_file_xsd(self, xml_file, base_path):
"""Validate a single XML file against XSD schema. Returns (is_valid, errors_set)."""
schema_path = self._get_schema_path(xml_file)
if not schema_path:
return None, None # Skip file
try:
# Load schema
with open(schema_path, "rb") as xsd_file:
parser = lxml.etree.XMLParser()
xsd_doc = lxml.etree.parse(
xsd_file, parser=parser, base_url=str(schema_path)
)
schema = lxml.etree.XMLSchema(xsd_doc)
# Load and preprocess XML
with open(xml_file, "r") as f:
xml_doc = lxml.etree.parse(f)
xml_doc, _ = self._remove_template_tags_from_text_nodes(xml_doc)
xml_doc = self._preprocess_for_mc_ignorable(xml_doc)
# Clean ignorable namespaces if needed
relative_path = xml_file.relative_to(base_path)
if (
relative_path.parts
and relative_path.parts[0] in self.MAIN_CONTENT_FOLDERS
):
xml_doc = self._clean_ignorable_namespaces(xml_doc)
# Validate
if schema.validate(xml_doc):
return True, set()
else:
errors = set()
for error in schema.error_log:
# Store normalized error message (without line numbers for comparison)
errors.add(error.message)
return False, errors
except Exception as e:
return False, {str(e)}
def _get_original_file_errors(self, xml_file):
"""Get XSD validation errors from a single file in the original document.
Args:
xml_file: Path to the XML file in unpacked_dir to check
Returns:
set: Set of error messages from the original file
"""
import tempfile
import zipfile
# Resolve both paths to handle symlinks (e.g., /var vs /private/var on macOS)
xml_file = Path(xml_file).resolve()
unpacked_dir = self.unpacked_dir.resolve()
relative_path = xml_file.relative_to(unpacked_dir)
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Extract original file
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_path)
# Find corresponding file in original
original_xml_file = temp_path / relative_path
if not original_xml_file.exists():
# File didn't exist in original, so no original errors
return set()
# Validate the specific file in original
is_valid, errors = self._validate_single_file_xsd(
original_xml_file, temp_path
)
return errors if errors else set()
def _remove_template_tags_from_text_nodes(self, xml_doc):
"""Remove template tags from XML text nodes and collect warnings.
Template tags follow the pattern {{ ... }} and are used as placeholders
for content replacement. They should be removed from text content before
XSD validation while preserving XML structure.
Returns:
tuple: (cleaned_xml_doc, warnings_list)
"""
warnings = []
template_pattern = re.compile(r"\{\{[^}]*\}\}")
# Create a copy of the document to avoid modifying the original
xml_string = lxml.etree.tostring(xml_doc, encoding="unicode")
xml_copy = lxml.etree.fromstring(xml_string)
def process_text_content(text, content_type):
if not text:
return text
matches = list(template_pattern.finditer(text))
if matches:
for match in matches:
warnings.append(
f"Found template tag in {content_type}: {match.group()}"
)
return template_pattern.sub("", text)
return text
# Process all text nodes in the document
for elem in xml_copy.iter():
# Skip processing if this is a w:t element
if not hasattr(elem, "tag") or callable(elem.tag):
continue
tag_str = str(elem.tag)
if tag_str.endswith("}t") or tag_str == "t":
continue
elem.text = process_text_content(elem.text, "text content")
elem.tail = process_text_content(elem.tail, "tail content")
return lxml.etree.ElementTree(xml_copy), warnings
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

View File

@ -0,0 +1,274 @@
"""
Validator for Word document XML files against XSD schemas.
"""
import re
import tempfile
import zipfile
import lxml.etree
from .base import BaseSchemaValidator
class DOCXSchemaValidator(BaseSchemaValidator):
"""Validator for Word document XML files against XSD schemas."""
# Word-specific namespace
WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
# Word-specific element to relationship type mappings
# Start with empty mapping - add specific cases as we discover them
ELEMENT_RELATIONSHIP_TYPES = {}
def validate(self):
"""Run all validation checks and return True if all pass."""
# Test 0: XML well-formedness
if not self.validate_xml():
return False
# Test 1: Namespace declarations
all_valid = True
if not self.validate_namespaces():
all_valid = False
# Test 2: Unique IDs
if not self.validate_unique_ids():
all_valid = False
# Test 3: Relationship and file reference validation
if not self.validate_file_references():
all_valid = False
# Test 4: Content type declarations
if not self.validate_content_types():
all_valid = False
# Test 5: XSD schema validation
if not self.validate_against_xsd():
all_valid = False
# Test 6: Whitespace preservation
if not self.validate_whitespace_preservation():
all_valid = False
# Test 7: Deletion validation
if not self.validate_deletions():
all_valid = False
# Test 8: Insertion validation
if not self.validate_insertions():
all_valid = False
# Test 9: Relationship ID reference validation
if not self.validate_all_relationship_ids():
all_valid = False
# Count and compare paragraphs
self.compare_paragraph_counts()
return all_valid
def validate_whitespace_preservation(self):
"""
Validate that w:t elements with whitespace have xml:space='preserve'.
"""
errors = []
for xml_file in self.xml_files:
# Only check document.xml files
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Find all w:t elements
for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"):
if elem.text:
text = elem.text
# Check if text starts or ends with whitespace
if re.match(r"^\s.*", text) or re.match(r".*\s$", text):
# Check if xml:space="preserve" attribute exists
xml_space_attr = f"{{{self.XML_NAMESPACE}}}space"
if (
xml_space_attr not in elem.attrib
or elem.attrib[xml_space_attr] != "preserve"
):
# Show a preview of the text
text_preview = (
repr(text)[:50] + "..."
if len(repr(text)) > 50
else repr(text)
)
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: w:t element with whitespace missing xml:space='preserve': {text_preview}"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} whitespace preservation violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All whitespace is properly preserved")
return True
def validate_deletions(self):
"""
Validate that w:t elements are not within w:del elements.
For some reason, XSD validation does not catch this, so we do it manually.
"""
errors = []
for xml_file in self.xml_files:
# Only check document.xml files
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Find all w:t elements that are descendants of w:del elements
namespaces = {"w": self.WORD_2006_NAMESPACE}
xpath_expression = ".//w:del//w:t"
problematic_t_elements = root.xpath(
xpath_expression, namespaces=namespaces
)
for t_elem in problematic_t_elements:
if t_elem.text:
# Show a preview of the text
text_preview = (
repr(t_elem.text)[:50] + "..."
if len(repr(t_elem.text)) > 50
else repr(t_elem.text)
)
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {t_elem.sourceline}: <w:t> found within <w:del>: {text_preview}"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} deletion validation violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - No w:t elements found within w:del elements")
return True
def count_paragraphs_in_unpacked(self):
"""Count the number of paragraphs in the unpacked document."""
count = 0
for xml_file in self.xml_files:
# Only check document.xml files
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
count = len(paragraphs)
except Exception as e:
print(f"Error counting paragraphs in unpacked document: {e}")
return count
def count_paragraphs_in_original(self):
"""Count the number of paragraphs in the original docx file."""
count = 0
try:
# Create temporary directory to unpack original
with tempfile.TemporaryDirectory() as temp_dir:
# Unpack original docx
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
zip_ref.extractall(temp_dir)
# Parse document.xml
doc_xml_path = temp_dir + "/word/document.xml"
root = lxml.etree.parse(doc_xml_path).getroot()
# Count all w:p elements
paragraphs = root.findall(f".//{{{self.WORD_2006_NAMESPACE}}}p")
count = len(paragraphs)
except Exception as e:
print(f"Error counting paragraphs in original document: {e}")
return count
def validate_insertions(self):
"""
Validate that w:delText elements are not within w:ins elements.
w:delText is only allowed in w:ins if nested within a w:del.
"""
errors = []
for xml_file in self.xml_files:
if xml_file.name != "document.xml":
continue
try:
root = lxml.etree.parse(str(xml_file)).getroot()
namespaces = {"w": self.WORD_2006_NAMESPACE}
# Find w:delText in w:ins that are NOT within w:del
invalid_elements = root.xpath(
".//w:ins//w:delText[not(ancestor::w:del)]",
namespaces=namespaces
)
for elem in invalid_elements:
text_preview = (
repr(elem.text or "")[:50] + "..."
if len(repr(elem.text or "")) > 50
else repr(elem.text or "")
)
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: <w:delText> within <w:ins>: {text_preview}"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} insertion validation violations:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - No w:delText elements within w:ins elements")
return True
def compare_paragraph_counts(self):
"""Compare paragraph counts between original and new document."""
original_count = self.count_paragraphs_in_original()
new_count = self.count_paragraphs_in_unpacked()
diff = new_count - original_count
diff_str = f"+{diff}" if diff > 0 else str(diff)
print(f"\nParagraphs: {original_count}{new_count} ({diff_str})")
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

View File

@ -0,0 +1,315 @@
"""
Validator for PowerPoint presentation XML files against XSD schemas.
"""
import re
from .base import BaseSchemaValidator
class PPTXSchemaValidator(BaseSchemaValidator):
"""Validator for PowerPoint presentation XML files against XSD schemas."""
# PowerPoint presentation namespace
PRESENTATIONML_NAMESPACE = (
"http://schemas.openxmlformats.org/presentationml/2006/main"
)
# PowerPoint-specific element to relationship type mappings
ELEMENT_RELATIONSHIP_TYPES = {
"sldid": "slide",
"sldmasterid": "slidemaster",
"notesmasterid": "notesmaster",
"sldlayoutid": "slidelayout",
"themeid": "theme",
"tablestyleid": "tablestyles",
}
def validate(self):
"""Run all validation checks and return True if all pass."""
# Test 0: XML well-formedness
if not self.validate_xml():
return False
# Test 1: Namespace declarations
all_valid = True
if not self.validate_namespaces():
all_valid = False
# Test 2: Unique IDs
if not self.validate_unique_ids():
all_valid = False
# Test 3: UUID ID validation
if not self.validate_uuid_ids():
all_valid = False
# Test 4: Relationship and file reference validation
if not self.validate_file_references():
all_valid = False
# Test 5: Slide layout ID validation
if not self.validate_slide_layout_ids():
all_valid = False
# Test 6: Content type declarations
if not self.validate_content_types():
all_valid = False
# Test 7: XSD schema validation
if not self.validate_against_xsd():
all_valid = False
# Test 8: Notes slide reference validation
if not self.validate_notes_slide_references():
all_valid = False
# Test 9: Relationship ID reference validation
if not self.validate_all_relationship_ids():
all_valid = False
# Test 10: Duplicate slide layout references validation
if not self.validate_no_duplicate_slide_layouts():
all_valid = False
return all_valid
def validate_uuid_ids(self):
"""Validate that ID attributes that look like UUIDs contain only hex values."""
import lxml.etree
errors = []
# UUID pattern: 8-4-4-4-12 hex digits with optional braces/hyphens
uuid_pattern = re.compile(
r"^[\{\(]?[0-9A-Fa-f]{8}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{4}-?[0-9A-Fa-f]{12}[\}\)]?$"
)
for xml_file in self.xml_files:
try:
root = lxml.etree.parse(str(xml_file)).getroot()
# Check all elements for ID attributes
for elem in root.iter():
for attr, value in elem.attrib.items():
# Check if this is an ID attribute
attr_name = attr.split("}")[-1].lower()
if attr_name == "id" or attr_name.endswith("id"):
# Check if value looks like a UUID (has the right length and pattern structure)
if self._looks_like_uuid(value):
# Validate that it contains only hex characters in the right positions
if not uuid_pattern.match(value):
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: "
f"Line {elem.sourceline}: ID '{value}' appears to be a UUID but contains invalid hex characters"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {xml_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} UUID ID validation errors:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All UUID-like IDs contain valid hex values")
return True
def _looks_like_uuid(self, value):
"""Check if a value has the general structure of a UUID."""
# Remove common UUID delimiters
clean_value = value.strip("{}()").replace("-", "")
# Check if it's 32 hex-like characters (could include invalid hex chars)
return len(clean_value) == 32 and all(c.isalnum() for c in clean_value)
def validate_slide_layout_ids(self):
"""Validate that sldLayoutId elements in slide masters reference valid slide layouts."""
import lxml.etree
errors = []
# Find all slide master files
slide_masters = list(self.unpacked_dir.glob("ppt/slideMasters/*.xml"))
if not slide_masters:
if self.verbose:
print("PASSED - No slide masters found")
return True
for slide_master in slide_masters:
try:
# Parse the slide master file
root = lxml.etree.parse(str(slide_master)).getroot()
# Find the corresponding _rels file for this slide master
rels_file = slide_master.parent / "_rels" / f"{slide_master.name}.rels"
if not rels_file.exists():
errors.append(
f" {slide_master.relative_to(self.unpacked_dir)}: "
f"Missing relationships file: {rels_file.relative_to(self.unpacked_dir)}"
)
continue
# Parse the relationships file
rels_root = lxml.etree.parse(str(rels_file)).getroot()
# Build a set of valid relationship IDs that point to slide layouts
valid_layout_rids = set()
for rel in rels_root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
):
rel_type = rel.get("Type", "")
if "slideLayout" in rel_type:
valid_layout_rids.add(rel.get("Id"))
# Find all sldLayoutId elements in the slide master
for sld_layout_id in root.findall(
f".//{{{self.PRESENTATIONML_NAMESPACE}}}sldLayoutId"
):
r_id = sld_layout_id.get(
f"{{{self.OFFICE_RELATIONSHIPS_NAMESPACE}}}id"
)
layout_id = sld_layout_id.get("id")
if r_id and r_id not in valid_layout_rids:
errors.append(
f" {slide_master.relative_to(self.unpacked_dir)}: "
f"Line {sld_layout_id.sourceline}: sldLayoutId with id='{layout_id}' "
f"references r:id='{r_id}' which is not found in slide layout relationships"
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {slide_master.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print(f"FAILED - Found {len(errors)} slide layout ID validation errors:")
for error in errors:
print(error)
print(
"Remove invalid references or add missing slide layouts to the relationships file."
)
return False
else:
if self.verbose:
print("PASSED - All slide layout IDs reference valid slide layouts")
return True
def validate_no_duplicate_slide_layouts(self):
"""Validate that each slide has exactly one slideLayout reference."""
import lxml.etree
errors = []
slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
for rels_file in slide_rels_files:
try:
root = lxml.etree.parse(str(rels_file)).getroot()
# Find all slideLayout relationships
layout_rels = [
rel
for rel in root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
)
if "slideLayout" in rel.get("Type", "")
]
if len(layout_rels) > 1:
errors.append(
f" {rels_file.relative_to(self.unpacked_dir)}: has {len(layout_rels)} slideLayout references"
)
except Exception as e:
errors.append(
f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
if errors:
print("FAILED - Found slides with duplicate slideLayout references:")
for error in errors:
print(error)
return False
else:
if self.verbose:
print("PASSED - All slides have exactly one slideLayout reference")
return True
def validate_notes_slide_references(self):
"""Validate that each notesSlide file is referenced by only one slide."""
import lxml.etree
errors = []
notes_slide_references = {} # Track which slides reference each notesSlide
# Find all slide relationship files
slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
if not slide_rels_files:
if self.verbose:
print("PASSED - No slide relationship files found")
return True
for rels_file in slide_rels_files:
try:
# Parse the relationships file
root = lxml.etree.parse(str(rels_file)).getroot()
# Find all notesSlide relationships
for rel in root.findall(
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
):
rel_type = rel.get("Type", "")
if "notesSlide" in rel_type:
target = rel.get("Target", "")
if target:
# Normalize the target path to handle relative paths
normalized_target = target.replace("../", "")
# Track which slide references this notesSlide
slide_name = rels_file.stem.replace(
".xml", ""
) # e.g., "slide1"
if normalized_target not in notes_slide_references:
notes_slide_references[normalized_target] = []
notes_slide_references[normalized_target].append(
(slide_name, rels_file)
)
except (lxml.etree.XMLSyntaxError, Exception) as e:
errors.append(
f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
)
# Check for duplicate references
for target, references in notes_slide_references.items():
if len(references) > 1:
slide_names = [ref[0] for ref in references]
errors.append(
f" Notes slide '{target}' is referenced by multiple slides: {', '.join(slide_names)}"
)
for slide_name, rels_file in references:
errors.append(f" - {rels_file.relative_to(self.unpacked_dir)}")
if errors:
print(
f"FAILED - Found {len([e for e in errors if not e.startswith(' ')])} notes slide reference validation errors:"
)
for error in errors:
print(error)
print("Each slide may optionally have its own slide file.")
return False
else:
if self.verbose:
print("PASSED - All notes slide references are unique")
return True
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

View File

@ -0,0 +1,279 @@
"""
Validator for tracked changes in Word documents.
"""
import subprocess
import tempfile
import zipfile
from pathlib import Path
class RedliningValidator:
"""Validator for tracked changes in Word documents."""
def __init__(self, unpacked_dir, original_docx, verbose=False):
self.unpacked_dir = Path(unpacked_dir)
self.original_docx = Path(original_docx)
self.verbose = verbose
self.namespaces = {
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
}
def validate(self):
"""Main validation method that returns True if valid, False otherwise."""
# Verify unpacked directory exists and has correct structure
modified_file = self.unpacked_dir / "word" / "document.xml"
if not modified_file.exists():
print(f"FAILED - Modified document.xml not found at {modified_file}")
return False
# First, check if there are any tracked changes by Claude to validate
try:
import xml.etree.ElementTree as ET
tree = ET.parse(modified_file)
root = tree.getroot()
# Check for w:del or w:ins tags authored by Claude
del_elements = root.findall(".//w:del", self.namespaces)
ins_elements = root.findall(".//w:ins", self.namespaces)
# Filter to only include changes by Claude
claude_del_elements = [
elem
for elem in del_elements
if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude"
]
claude_ins_elements = [
elem
for elem in ins_elements
if elem.get(f"{{{self.namespaces['w']}}}author") == "Claude"
]
# Redlining validation is only needed if tracked changes by Claude have been used.
if not claude_del_elements and not claude_ins_elements:
if self.verbose:
print("PASSED - No tracked changes by Claude found.")
return True
except Exception:
# If we can't parse the XML, continue with full validation
pass
# Create temporary directory for unpacking original docx
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Unpack original docx
try:
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
zip_ref.extractall(temp_path)
except Exception as e:
print(f"FAILED - Error unpacking original docx: {e}")
return False
original_file = temp_path / "word" / "document.xml"
if not original_file.exists():
print(
f"FAILED - Original document.xml not found in {self.original_docx}"
)
return False
# Parse both XML files using xml.etree.ElementTree for redlining validation
try:
import xml.etree.ElementTree as ET
modified_tree = ET.parse(modified_file)
modified_root = modified_tree.getroot()
original_tree = ET.parse(original_file)
original_root = original_tree.getroot()
except ET.ParseError as e:
print(f"FAILED - Error parsing XML files: {e}")
return False
# Remove Claude's tracked changes from both documents
self._remove_claude_tracked_changes(original_root)
self._remove_claude_tracked_changes(modified_root)
# Extract and compare text content
modified_text = self._extract_text_content(modified_root)
original_text = self._extract_text_content(original_root)
if modified_text != original_text:
# Show detailed character-level differences for each paragraph
error_message = self._generate_detailed_diff(
original_text, modified_text
)
print(error_message)
return False
if self.verbose:
print("PASSED - All changes by Claude are properly tracked")
return True
def _generate_detailed_diff(self, original_text, modified_text):
"""Generate detailed word-level differences using git word diff."""
error_parts = [
"FAILED - Document text doesn't match after removing Claude's tracked changes",
"",
"Likely causes:",
" 1. Modified text inside another author's <w:ins> or <w:del> tags",
" 2. Made edits without proper tracked changes",
" 3. Didn't nest <w:del> inside <w:ins> when deleting another's insertion",
"",
"For pre-redlined documents, use correct patterns:",
" - To reject another's INSERTION: Nest <w:del> inside their <w:ins>",
" - To restore another's DELETION: Add new <w:ins> AFTER their <w:del>",
"",
]
# Show git word diff
git_diff = self._get_git_word_diff(original_text, modified_text)
if git_diff:
error_parts.extend(["Differences:", "============", git_diff])
else:
error_parts.append("Unable to generate word diff (git not available)")
return "\n".join(error_parts)
def _get_git_word_diff(self, original_text, modified_text):
"""Generate word diff using git with character-level precision."""
try:
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
# Create two files
original_file = temp_path / "original.txt"
modified_file = temp_path / "modified.txt"
original_file.write_text(original_text, encoding="utf-8")
modified_file.write_text(modified_text, encoding="utf-8")
# Try character-level diff first for precise differences
result = subprocess.run(
[
"git",
"diff",
"--word-diff=plain",
"--word-diff-regex=.", # Character-by-character diff
"-U0", # Zero lines of context - show only changed lines
"--no-index",
str(original_file),
str(modified_file),
],
capture_output=True,
text=True,
)
if result.stdout.strip():
# Clean up the output - remove git diff header lines
lines = result.stdout.split("\n")
# Skip the header lines (diff --git, index, +++, ---, @@)
content_lines = []
in_content = False
for line in lines:
if line.startswith("@@"):
in_content = True
continue
if in_content and line.strip():
content_lines.append(line)
if content_lines:
return "\n".join(content_lines)
# Fallback to word-level diff if character-level is too verbose
result = subprocess.run(
[
"git",
"diff",
"--word-diff=plain",
"-U0", # Zero lines of context
"--no-index",
str(original_file),
str(modified_file),
],
capture_output=True,
text=True,
)
if result.stdout.strip():
lines = result.stdout.split("\n")
content_lines = []
in_content = False
for line in lines:
if line.startswith("@@"):
in_content = True
continue
if in_content and line.strip():
content_lines.append(line)
return "\n".join(content_lines)
except (subprocess.CalledProcessError, FileNotFoundError, Exception):
# Git not available or other error, return None to use fallback
pass
return None
def _remove_claude_tracked_changes(self, root):
"""Remove tracked changes authored by Claude from the XML root."""
ins_tag = f"{{{self.namespaces['w']}}}ins"
del_tag = f"{{{self.namespaces['w']}}}del"
author_attr = f"{{{self.namespaces['w']}}}author"
# Remove w:ins elements
for parent in root.iter():
to_remove = []
for child in parent:
if child.tag == ins_tag and child.get(author_attr) == "Claude":
to_remove.append(child)
for elem in to_remove:
parent.remove(elem)
# Unwrap content in w:del elements where author is "Claude"
deltext_tag = f"{{{self.namespaces['w']}}}delText"
t_tag = f"{{{self.namespaces['w']}}}t"
for parent in root.iter():
to_process = []
for child in parent:
if child.tag == del_tag and child.get(author_attr) == "Claude":
to_process.append((child, list(parent).index(child)))
# Process in reverse order to maintain indices
for del_elem, del_index in reversed(to_process):
# Convert w:delText to w:t before moving
for elem in del_elem.iter():
if elem.tag == deltext_tag:
elem.tag = t_tag
# Move all children of w:del to its parent before removing w:del
for child in reversed(list(del_elem)):
parent.insert(del_index, child)
parent.remove(del_elem)
def _extract_text_content(self, root):
"""Extract text content from Word XML, preserving paragraph structure.
Empty paragraphs are skipped to avoid false positives when tracked
insertions add only structural elements without text content.
"""
p_tag = f"{{{self.namespaces['w']}}}p"
t_tag = f"{{{self.namespaces['w']}}}t"
paragraphs = []
for p_elem in root.findall(f".//{p_tag}"):
# Get all text elements within this paragraph
text_parts = []
for t_elem in p_elem.findall(f".//{t_tag}"):
if t_elem.text:
text_parts.append(t_elem.text)
paragraph_text = "".join(text_parts)
# Skip empty paragraphs - they don't affect content validation
if paragraph_text:
paragraphs.append(paragraph_text)
return "\n".join(paragraphs)
if __name__ == "__main__":
raise RuntimeError("This module should not be run directly.")

1
docx/scripts/__init__.py Executable file
View File

@ -0,0 +1 @@
# Make scripts directory a package for relative imports in tests

1276
docx/scripts/document.py Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:comments xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14">
</w:comments>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w15:commentsEx xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14">
</w15:commentsEx>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w16cex:commentsExtensible xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:cr="http://schemas.microsoft.com/office/comments/2020/reactions" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl cr w16du wp14">
</w16cex:commentsExtensible>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w16cid:commentsIds xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14">
</w16cid:commentsIds>

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w15:people xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">
</w15:people>

374
docx/scripts/utilities.py Executable file
View File

@ -0,0 +1,374 @@
#!/usr/bin/env python3
"""
Utilities for editing OOXML documents.
This module provides XMLEditor, a tool for manipulating XML files with support for
line-number-based node finding and DOM manipulation. Each element is automatically
annotated with its original line and column position during parsing.
Example usage:
editor = XMLEditor("document.xml")
# Find node by line number or range
elem = editor.get_node(tag="w:r", line_number=519)
elem = editor.get_node(tag="w:p", line_number=range(100, 200))
# Find node by text content
elem = editor.get_node(tag="w:p", contains="specific text")
# Find node by attributes
elem = editor.get_node(tag="w:r", attrs={"w:id": "target"})
# Combine filters
elem = editor.get_node(tag="w:p", line_number=range(1, 50), contains="text")
# Replace, insert, or manipulate
new_elem = editor.replace_node(elem, "<w:r><w:t>new text</w:t></w:r>")
editor.insert_after(new_elem, "<w:r><w:t>more</w:t></w:r>")
# Save changes
editor.save()
"""
import html
from pathlib import Path
from typing import Optional, Union
import defusedxml.minidom
import defusedxml.sax
class XMLEditor:
"""
Editor for manipulating OOXML XML files with line-number-based node finding.
This class parses XML files and tracks the original line and column position
of each element. This enables finding nodes by their line number in the original
file, which is useful when working with Read tool output.
Attributes:
xml_path: Path to the XML file being edited
encoding: Detected encoding of the XML file ('ascii' or 'utf-8')
dom: Parsed DOM tree with parse_position attributes on elements
"""
def __init__(self, xml_path):
"""
Initialize with path to XML file and parse with line number tracking.
Args:
xml_path: Path to XML file to edit (str or Path)
Raises:
ValueError: If the XML file does not exist
"""
self.xml_path = Path(xml_path)
if not self.xml_path.exists():
raise ValueError(f"XML file not found: {xml_path}")
with open(self.xml_path, "rb") as f:
header = f.read(200).decode("utf-8", errors="ignore")
self.encoding = "ascii" if 'encoding="ascii"' in header else "utf-8"
parser = _create_line_tracking_parser()
self.dom = defusedxml.minidom.parse(str(self.xml_path), parser)
def get_node(
self,
tag: str,
attrs: Optional[dict[str, str]] = None,
line_number: Optional[Union[int, range]] = None,
contains: Optional[str] = None,
):
"""
Get a DOM element by tag and identifier.
Finds an element by either its line number in the original file or by
matching attribute values. Exactly one match must be found.
Args:
tag: The XML tag name (e.g., "w:del", "w:ins", "w:r")
attrs: Dictionary of attribute name-value pairs to match (e.g., {"w:id": "1"})
line_number: Line number (int) or line range (range) in original XML file (1-indexed)
contains: Text string that must appear in any text node within the element.
Supports both entity notation (&#8220;) and Unicode characters (\u201c).
Returns:
defusedxml.minidom.Element: The matching DOM element
Raises:
ValueError: If node not found or multiple matches found
Example:
elem = editor.get_node(tag="w:r", line_number=519)
elem = editor.get_node(tag="w:r", line_number=range(100, 200))
elem = editor.get_node(tag="w:del", attrs={"w:id": "1"})
elem = editor.get_node(tag="w:p", attrs={"w14:paraId": "12345678"})
elem = editor.get_node(tag="w:commentRangeStart", attrs={"w:id": "0"})
elem = editor.get_node(tag="w:p", contains="specific text")
elem = editor.get_node(tag="w:t", contains="&#8220;Agreement") # Entity notation
elem = editor.get_node(tag="w:t", contains="\u201cAgreement") # Unicode character
"""
matches = []
for elem in self.dom.getElementsByTagName(tag):
# Check line_number filter
if line_number is not None:
parse_pos = getattr(elem, "parse_position", (None,))
elem_line = parse_pos[0]
# Handle both single line number and range
if isinstance(line_number, range):
if elem_line not in line_number:
continue
else:
if elem_line != line_number:
continue
# Check attrs filter
if attrs is not None:
if not all(
elem.getAttribute(attr_name) == attr_value
for attr_name, attr_value in attrs.items()
):
continue
# Check contains filter
if contains is not None:
elem_text = self._get_element_text(elem)
# Normalize the search string: convert HTML entities to Unicode characters
# This allows searching for both "&#8220;Rowan" and ""Rowan"
normalized_contains = html.unescape(contains)
if normalized_contains not in elem_text:
continue
# If all applicable filters passed, this is a match
matches.append(elem)
if not matches:
# Build descriptive error message
filters = []
if line_number is not None:
line_str = (
f"lines {line_number.start}-{line_number.stop - 1}"
if isinstance(line_number, range)
else f"line {line_number}"
)
filters.append(f"at {line_str}")
if attrs is not None:
filters.append(f"with attributes {attrs}")
if contains is not None:
filters.append(f"containing '{contains}'")
filter_desc = " ".join(filters) if filters else ""
base_msg = f"Node not found: <{tag}> {filter_desc}".strip()
# Add helpful hint based on filters used
if contains:
hint = "Text may be split across elements or use different wording."
elif line_number:
hint = "Line numbers may have changed if document was modified."
elif attrs:
hint = "Verify attribute values are correct."
else:
hint = "Try adding filters (attrs, line_number, or contains)."
raise ValueError(f"{base_msg}. {hint}")
if len(matches) > 1:
raise ValueError(
f"Multiple nodes found: <{tag}>. "
f"Add more filters (attrs, line_number, or contains) to narrow the search."
)
return matches[0]
def _get_element_text(self, elem):
"""
Recursively extract all text content from an element.
Skips text nodes that contain only whitespace (spaces, tabs, newlines),
which typically represent XML formatting rather than document content.
Args:
elem: defusedxml.minidom.Element to extract text from
Returns:
str: Concatenated text from all non-whitespace text nodes within the element
"""
text_parts = []
for node in elem.childNodes:
if node.nodeType == node.TEXT_NODE:
# Skip whitespace-only text nodes (XML formatting)
if node.data.strip():
text_parts.append(node.data)
elif node.nodeType == node.ELEMENT_NODE:
text_parts.append(self._get_element_text(node))
return "".join(text_parts)
def replace_node(self, elem, new_content):
"""
Replace a DOM element with new XML content.
Args:
elem: defusedxml.minidom.Element to replace
new_content: String containing XML to replace the node with
Returns:
List[defusedxml.minidom.Node]: All inserted nodes
Example:
new_nodes = editor.replace_node(old_elem, "<w:r><w:t>text</w:t></w:r>")
"""
parent = elem.parentNode
nodes = self._parse_fragment(new_content)
for node in nodes:
parent.insertBefore(node, elem)
parent.removeChild(elem)
return nodes
def insert_after(self, elem, xml_content):
"""
Insert XML content after a DOM element.
Args:
elem: defusedxml.minidom.Element to insert after
xml_content: String containing XML to insert
Returns:
List[defusedxml.minidom.Node]: All inserted nodes
Example:
new_nodes = editor.insert_after(elem, "<w:r><w:t>text</w:t></w:r>")
"""
parent = elem.parentNode
next_sibling = elem.nextSibling
nodes = self._parse_fragment(xml_content)
for node in nodes:
if next_sibling:
parent.insertBefore(node, next_sibling)
else:
parent.appendChild(node)
return nodes
def insert_before(self, elem, xml_content):
"""
Insert XML content before a DOM element.
Args:
elem: defusedxml.minidom.Element to insert before
xml_content: String containing XML to insert
Returns:
List[defusedxml.minidom.Node]: All inserted nodes
Example:
new_nodes = editor.insert_before(elem, "<w:r><w:t>text</w:t></w:r>")
"""
parent = elem.parentNode
nodes = self._parse_fragment(xml_content)
for node in nodes:
parent.insertBefore(node, elem)
return nodes
def append_to(self, elem, xml_content):
"""
Append XML content as a child of a DOM element.
Args:
elem: defusedxml.minidom.Element to append to
xml_content: String containing XML to append
Returns:
List[defusedxml.minidom.Node]: All inserted nodes
Example:
new_nodes = editor.append_to(elem, "<w:r><w:t>text</w:t></w:r>")
"""
nodes = self._parse_fragment(xml_content)
for node in nodes:
elem.appendChild(node)
return nodes
def get_next_rid(self):
"""Get the next available rId for relationships files."""
max_id = 0
for rel_elem in self.dom.getElementsByTagName("Relationship"):
rel_id = rel_elem.getAttribute("Id")
if rel_id.startswith("rId"):
try:
max_id = max(max_id, int(rel_id[3:]))
except ValueError:
pass
return f"rId{max_id + 1}"
def save(self):
"""
Save the edited XML back to the file.
Serializes the DOM tree and writes it back to the original file path,
preserving the original encoding (ascii or utf-8).
"""
content = self.dom.toxml(encoding=self.encoding)
self.xml_path.write_bytes(content)
def _parse_fragment(self, xml_content):
"""
Parse XML fragment and return list of imported nodes.
Args:
xml_content: String containing XML fragment
Returns:
List of defusedxml.minidom.Node objects imported into this document
Raises:
AssertionError: If fragment contains no element nodes
"""
# Extract namespace declarations from the root document element
root_elem = self.dom.documentElement
namespaces = []
if root_elem and root_elem.attributes:
for i in range(root_elem.attributes.length):
attr = root_elem.attributes.item(i)
if attr.name.startswith("xmlns"): # type: ignore
namespaces.append(f'{attr.name}="{attr.value}"') # type: ignore
ns_decl = " ".join(namespaces)
wrapper = f"<root {ns_decl}>{xml_content}</root>"
fragment_doc = defusedxml.minidom.parseString(wrapper)
nodes = [
self.dom.importNode(child, deep=True)
for child in fragment_doc.documentElement.childNodes # type: ignore
]
elements = [n for n in nodes if n.nodeType == n.ELEMENT_NODE]
assert elements, "Fragment must contain at least one element"
return nodes
def _create_line_tracking_parser():
"""
Create a SAX parser that tracks line and column numbers for each element.
Monkey patches the SAX content handler to store the current line and column
position from the underlying expat parser onto each element as a parse_position
attribute (line, column) tuple.
Returns:
defusedxml.sax.xmlreader.XMLReader: Configured SAX parser
"""
def set_content_handler(dom_handler):
def startElementNS(name, tagName, attrs):
orig_start_cb(name, tagName, attrs)
cur_elem = dom_handler.elementStack[-1]
cur_elem.parse_position = (
parser._parser.CurrentLineNumber, # type: ignore
parser._parser.CurrentColumnNumber, # type: ignore
)
orig_start_cb = dom_handler.startElementNS
dom_handler.startElementNS = startElementNS
orig_set_content_handler(dom_handler)
parser = defusedxml.sax.make_parser()
orig_set_content_handler = parser.setContentHandler
parser.setContentHandler = set_content_handler # type: ignore
return parser

View File

@ -0,0 +1,685 @@
# 金融研究报告撰写技能
## 概述
本技能用于协助用户撰写专业的A股上市公司投资研究报告涵盖数据获取、分析和文档格式化等全流程。
## 适用场景
- 撰写上市公司周报、月报或专项研究报告
- 需要使用Wind金融终端获取实时或历史数据
- 生成符合中国国家标准的Word格式文档
- 技术分析与基本面分析相结合的投资报告
## 核心能力
### 1. 数据获取WindPy SDK
#### 连接Wind终端
```python
from WindPy import w
w.start()
data = w.wsd("300866.SZ", "close", "2025-11-01", "2026-02-06")
w.stop()
```
#### 常用数据函数
- `w.wsd()` - 日线序列数据
- `w.wsi()` - 分钟线数据
- `w.wss()` - 截面数据(最新值)
- `wsd` 常用字段:
- `close` - 收盘价
- `open`/`high`/`low` - 开盘/最高/最低价
- `volume`/`amt` - 成交量/成交额
- `pct_chg` - 涨跌幅
- `ma5`, `ma10`, `ma20`, `ma60` - 均线
- `roe` - 净资产收益率
- `pe_ttm`, `pb_lf` - 市盈率/市净率
#### 技术指标计算
- 移动平均线MA5, MA10, MA20, MA60
- 波动率:使用标准差计算年化波动率
- 支撑位/阻力位:基于高低点分析
### 2. Word文档格式化python-docx
#### 国家标准 GB/T 9704-2012
**页面设置**
```python
from docx.shared import Mm
section.top_margin = Mm(37)
section.bottom_margin = Mm(35)
section.left_margin = Mm(28)
section.right_margin = Mm(26)
```
**字体规范**
```python
def set_font(run, font_name, size=None, bold=None):
run.font.name = font_name
run.font.size = Pt(size) if size else None
if bold is not None:
run.font.bold = bold
# 设置中文字体
run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
# 三号标题16pt
# 四号正文14pt
# 小四表格12pt
```
**字号对照表**
- 三号16pt
- 四号14pt
- 小四12pt
- 五号10.5pt
**行距设置**
```python
paragraph.paragraph_format.line_spacing = Pt(28) # 正文
paragraph.paragraph_format.line_spacing = Pt(25) # 小标题
paragraph.paragraph_format.line_spacing_rule = 2 # 固定值
```
**段落格式**
```python
# 首行缩进2字符28pt
paragraph.paragraph_format.first_line_indent = Pt(28)
# 两端对齐
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
# 段前段后距
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.space_after = Pt(0)
```
#### 三线表格式
```python
def set_table_borders(table):
tblPr = table._tbl.tblPr
tblBorders = OxmlElement('w:tblBorders')
# 顶底边框 1.5pt (sz=18)
for pos in ['top', 'bottom']:
border = OxmlElement(f'w:{pos}')
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), '18')
border.set(qn('w:color'), 'auto')
tblBorders.append(border)
# 内部横线 0.75pt (sz=9)
for pos in ['insideH']:
border = OxmlElement(f'w:{pos}')
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), '9')
border.set(qn('w:color'), 'auto')
tblBorders.append(border)
# 无左右和竖线
for pos in ['left', 'right', 'insideV']:
border = OxmlElement(f'w:{pos}')
border.set(qn('w:val'), 'none')
tblBorders.append(border)
tblPr.append(tblBorders)
```
#### 页脚页码
```python
from docx.enum.text import WD_ALIGN_PARAGRAPH
footer = section.footer
p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
r1 = p.add_run('—')
set_font(r1, '仿宋', 12)
r2 = p.add_run() # 页码占位
set_font(r2, '仿宋', 12)
r3 = p.add_run(' ')
set_font(r3, '仿宋', 12)
r4 = p.add_run() # 页码占位
set_font(r4, '仿宋', 12)
r5 = p.add_run('—')
set_font(r5, '仿宋', 12)
```
### 3. 报告结构标准
#### 完整报告结构(可选封面版)
1. **封面页**
- 标题黑体三号16pt居中
- 报告机构和日期黑体三号16pt居中
2. **免责声明**(可选)
- 标题黑体三号16pt居中
- 内容仿宋四号14pt两端对齐
3. **核心提要**
- 标题黑体三号16pt居中
- 内容3-4个要点仿宋四号14pt首行缩进2字符
4. **一、投资评级**
- 标题黑体三号16pt
- 评级表格小四12pt三线表
- 内容:投资建议、目标价格、有效期、风险等级
5. **二、核心观点**
- 标题黑体三号16pt
- 内容:基本面+技术面分析+投资建议
6. **三、市场表现回顾**
- 标题黑体三号16pt
- 价格走势分析黑体三号16pt
- K线数据表标题黑体小四12pt
#### 简化报告结构(无封面)
- 核心提要 → 一、投资评级 → 二、核心观点 → 三、市场表现回顾
- 不使用分页符,保持内容连贯
### 4. 内容撰写要点
#### 核心提要
- 突出关键数据ROE、涨跌幅等
- 简明扼要3-4个要点
- 包含明确的投资建议
#### 核心观点
- 基本面盈利能力ROE、估值水平PE/PB
- 技术面:价格趋势、均线、支撑阻力位
- 投资建议:具体操作策略
#### 市场表现回顾
- 时间周期近13周周线数据
- 价格区间:最高价、最低价、累计涨跌幅
- 数据表格:日期、开盘、最高、最低、收盘、涨跌幅、成交量
### 5. 数据处理技巧
#### 时间序列对齐
```python
import pandas as pd
df = pd.DataFrame(data, columns=['date', 'open', 'high', 'low', 'close'])
df['date'] = pd.to_datetime(df['date'])
df.sort_values('date', ascending=True)
```
#### 周线数据汇总
```python
df_weekly = df.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
```
#### 波动率计算
```python
import numpy as np
returns = df['close'].pct_change().dropna()
volatility = returns.std() * np.sqrt(52) * 100 # 年化百分比
```
### 6. 常见问题解决
#### 字体显示问题
- 必须同时设置中文字体:`run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)`
- 不要只设置 `run.font.name`
#### 表格单元格格式
- Cell对象没有 `paragraph_format` 属性
- 需要通过 `cell.paragraphs[0].paragraph_format` 访问
#### Run对象访问
- Run对象没有 `runs` 属性
- 段落的runs通过 `paragraph.runs` 访问
#### 行距单位
- 使用 `Pt()` 设置固定行距(点)
- `line_spacing_rule = 2` 表示固定值
## 使用示例
### 基本流程
1. 使用WindPy SDK获取股票数据
2. 计算技术指标MA、波动率等
3. 分析基本面指标ROE、PE等
4. 整理分析结论
5. 使用python-docx生成Word文档
6. 应用GB/T 9704-2012格式规范
7. 保存文档
### 快速检查清单Word文档
- [ ] 页面边距37/35/28/26mm
- [ ] 字体:标题黑体三号、正文仿宋四号、表格小四
- [ ] 行距正文28pt、小标题25pt
- [ ] 首行缩进正文2字符28pt
- [ ] 三线表顶底1.5pt、内部0.75pt、无边框
- [ ] 数据来源注脚右对齐小四12pt
### Excel图表检查清单
详见「图表设置检查清单」表格第7节末尾
## 注意事项
1. **字体重要性**:中文文本必须设置 `w:eastAsia` 属性,否则可能显示为默认字体
2. **表格边框**:使用三线表格式,无左右和竖线
3. **段落缩进**:正文段落需要首行缩进,标题不需要
4. **分页控制**:根据需要决定是否分页,简化版通常不分页
5. **数据时效**:注意数据更新日期,避免使用过期数据
6. **投资建议**:提供明确的操作策略,但需提醒风险
### 7. Excel 图表绘制openpyxl
本技能支持生成包含原生 Excel 图表的 .xlsx 文件,图表可在 Excel 中编辑和交互。
#### 基础图表创建
```python
from openpyxl import Workbook
from openpyxl.chart import LineChart, BarChart, Reference
from openpyxl.chart.axis import DateAxis
# 创建工作簿
wb = Workbook()
ws = wb.active
ws.title = "股价走势"
# 写入数据
ws.append(["日期", "收盘价", "成交量"])
for date, close, vol in data:
ws.append([date, close, vol])
# 创建折线图(股价走势)
chart = LineChart()
chart.title = "股价走势"
chart.y_axis.title = "价格(元)"
chart.x_axis.title = "日期"
# 设置数据区域
data_ref = Reference(ws, min_col=2, min_row=1, max_row=len(data)+1)
cats_ref = Reference(ws, min_col=1, min_row=2, max_row=len(data)+1)
chart.add_data(data_ref, titles_from_data=True)
chart.set_categories(cats_ref)
# 设置样式
chart.style = 10
chart.height = 10 # 高度(厘米)
chart.width = 20 # 宽度(厘米)
# 添加到工作表
ws.add_chart(chart, "D2")
wb.save("股价走势.xlsx")
```
#### 常用图表类型
**折线图LineChart**
- 适用:股价走势、均线趋势
- 特点:展示时间序列变化
```python
from openpyxl.chart import LineChart
chart = LineChart()
chart.add_data(data_ref)
chart.set_categories(cats_ref)
```
**柱状图BarChart**
- 适用:成交量对比、财务数据对比
- 特点:横向展示数据大小
```python
from openpyxl.chart import BarChart
chart = BarChart()
chart.type = "col" # 垂直柱状图
chart.add_data(data_ref)
```
**组合图表(股价+成交量)**
- 适用K线+成交量组合展示
- 特点双Y轴价格+量能
```python
from openpyxl.chart import LineChart, BarChart
from openpyxl.chart.series import DataPoint
# 主图:股价线
price_chart = LineChart()
price_chart.add_data(price_ref, titles_from_data=True)
price_chart.y_axis.title = "价格(元)"
# 副图:成交量柱状
vol_chart = BarChart()
vol_chart.add_data(vol_ref, titles_from_data=True)
vol_chart.y_axis.axId = 200 # 设置次要Y轴
vol_chart.y_axis.title = "成交量(手)"
# 组合图表
price_chart += vol_chart
price_chart.y_axis.crosses = "max" # 成交量轴在右侧
```
#### 图表样式设置
**线条与颜色规范**
```python
# 线条样式
from openpyxl.chart.series import DataPoint
from openpyxl.drawing.fill import SolidColorFillProperties
from openpyxl.drawing.line import LineProperties
series = chart.series[0]
series.graphicalProperties.line.solidFill = "4472C4" # 线条颜色
series.graphicalProperties.line.width = 25000 # 线条粗细EMUs
# 标记点样式
series.marker.symbol = "circle"
series.marker.size = 5
```
**A股颜色规范涨跌自动着色**
```python
# 柱状图根据涨跌自动设置颜色(红涨绿跌)
from openpyxl.drawing.fill import PatternFillProperties, ColorChoice
from openpyxl.chart.series import DataPoint
# 为每个数据点设置颜色
for i, val in enumerate(values):
pt = DataPoint(idx=i)
# A股规范上涨红色下跌绿色
fill_color = "FF0000" if val >= 0 else "00B050"
pt.graphicalProperties = GraphicalProperties(
solidFill=fill_color
)
series.data_points.append(pt)
```
**网格线与图例**
```python
# 网格线设置(研报风格:仅水平主网格线)
chart.x_axis.majorGridlines = None # 隐藏X轴网格
chart.y_axis.majorGridlines = None # 或设置为浅灰色虚线
# 图例位置
chart.legend.position = "b" # 底部b, 右侧r, 顶部t, 左侧l
```
#### 双轴图表(价格+涨跌幅)
```python
from openpyxl.chart import LineChart
from openpyxl.chart.axis import DateAxis
# 主图:股价
price_chart = LineChart()
price_chart.add_data(price_ref, titles_from_data=True)
price_chart.y_axis.title = "价格(元)"
# 副图:涨跌幅(柱状)
pct_chart = LineChart()
pct_chart.add_data(pct_ref, titles_from_data=True)
pct_chart.y_axis.axId = 200
pct_chart.y_axis.title = "涨跌幅(%"
# 组合
price_chart += pct_chart
price_chart.y_axis.crosses = "max"
```
#### 坐标轴精修(研报级设置)
**数值轴Y轴刻度设置**
```python
# 设置Y轴最大最小值避免自动刻度导致的空白过多
chart.y_axis.scaling.min = 10 # 根据实际数据调整
chart.y_axis.scaling.max = 50
# 设置主要刻度单位
chart.y_axis.majorUnit = 5
# 对于大数值(如成交量),建议数据预处理转换为"亿"或"万"
# 并在轴标题中注明单位
chart.y_axis.title = "成交量(亿元)"
```
**日期轴X轴标签优化**
```python
from openpyxl.chart.axis import DateAxis
from openpyxl.drawing.text import Paragraph, ParagraphProperties, CharacterProperties
# 设置X轴为日期轴
chart.x_axis = DateAxis()
chart.x_axis.title = "日期"
# 标签间隔(避免数据点过多导致重叠)
chart.x_axis.tickLblSkip = 5 # 每5个数据点显示一个标签
# 标签旋转(防止重叠)
from copy import deepcopy
from openpyxl.drawing.text import RichText
rt = RichText(p=[Paragraph(
pPr=ParagraphProperties(defRPr=CharacterProperties(sz=900)),
endParaRPr=CharacterProperties(sz=900)
)])
chart.x_axis.txPr = rt
```
**坐标轴数字格式**
```python
# Y轴数字格式保留2位小数
chart.y_axis.numFmt = '0.00'
# 百分比格式
chart.y_axis.numFmt = '0.00%'
```
#### 数据标签与参考线(视觉增强)
**数据标签设置**
```python
from openpyxl.chart.label import DataLabelList
# 显示所有数据点的值
chart.dataLabels = DataLabelList()
chart.dataLabels.showVal = True
# 仅显示最后一个点(标注最新价)- 需要手动设置
from openpyxl.chart.series import DataPoint
last_pt = DataPoint(idx=len(values)-1)
last_pt.graphicalProperties = GraphicalProperties(solidFill="FF0000")
series.data_points = [last_pt]
```
**参考线(均值/目标价)**
```python
from openpyxl.chart.series import SeriesLabel
# 添加均值参考线(作为单独的数据系列)
mean_value = sum(values) / len(values)
ws.append([""] * (len(headers)-1) + ["均值"])
for i in range(len(values)):
ws.append([""] * (len(headers)-1) + [mean_value])
mean_ref = Reference(ws, min_col=len(headers), min_row=len(values)+2, max_row=len(values)*2+1)
mean_series = Series(mean_ref, title="均值线")
chart.series.append(mean_series)
```
#### 财务数据可视化
**营收利润趋势图**
```python
# 数据准备
ws.append(["报告期", "营业收入", "净利润"])
for period, revenue, profit in financial_data:
ws.append([period, revenue/1e8, profit/1e8]) # 转换为亿元
# 组合图表
chart = BarChart()
chart.type = "col"
chart.grouping = "clustered"
chart.title = "营业收入与净利润"
chart.y_axis.title = "金额(亿元)"
data = Reference(ws, min_col=2, max_col=3, min_row=1, max_row=len(financial_data)+1)
cats = Reference(ws, min_col=1, min_row=2, max_row=len(financial_data)+1)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
# 添加数据标签
chart.dataLabels = DataLabelList()
chart.dataLabels.showVal = True
```
**ROE 柱状图**
```python
chart = BarChart()
chart.type = "col"
data = Reference(ws, min_col=2, min_row=1, max_row=5)
cats = Reference(ws, min_col=1, min_row=2, max_row=5)
chart.add_data(data, titles_from_data=True)
chart.set_categories(cats)
chart.title = "年度 ROE 对比"
```
#### Excel 文件与 Word 报告配合
**方案AExcel 作为附件**
```python
# 生成带图表的 Excel 文件
wb.save(f"{stock_name}_数据分析.xlsx")
# 在 Word 中引用
paragraph = doc.add_paragraph()
run = paragraph.add_run(f"详细数据及图表请参见附件《{stock_name}_数据分析.xlsx》")
set_font(run, "仿宋", 12)
```
**方案B图表图片直接插入 Word推荐**
```python
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from docx.shared import Inches
# 使用 Matplotlib 生成符合 GB/T 9704 字体要求的图表
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.plot(dates, prices, color='#4472C4', linewidth=1.5)
ax.set_title('股价走势', fontsize=12, fontweight='bold')
ax.set_xlabel('日期', fontsize=10)
ax.set_ylabel('价格(元)', fontsize=10)
ax.grid(True, alpha=0.3)
# 保存为图片
fig.savefig('chart.png', dpi=150, bbox_inches='tight')
plt.close()
# 插入 Word
doc.add_picture('chart.png', width=Inches(6))
```
**方案CWord 原生图表(功能有限)**
```python
from docx.chart import Chart
from docx.enum.chart import XL_CHART_TYPE
# 注意docx.chart 功能较弱,仅支持基础图表类型
# 如需复杂图表建议使用方案A或B
```
#### 图表最佳实践
1. **数据准备**:使用 pandas 预处理数据,确保格式正确
2. **图表选择**
- 时间序列 → 折线图
- 对比分析 → 柱状图
- 价格+成交量 → 组合图
3. **颜色规范**A股标准
- 上涨红色FF0000
- 下跌绿色00B050推荐00AA00
- 中性蓝色4472C4
4. **数据单位**:图表标题或轴标题中注明单位(元、%、亿元等)
5. **数据来源**:图表下方注明数据来源,如"数据来源Wind"
#### 图表设置检查清单
| 检查项 | 专业要求 | 常见错误 |
|--------|----------|----------|
| **坐标轴标题** | 必须包含单位,如"价格(元)"、"比例(%" | 漏写单位或单位不统一 |
| **色彩方案** | 涨红跌绿A股规范多序列使用高对比度商务色 | 使用 Excel 默认全家桶配色 |
| **网格线** | 仅保留水平主网格线,且设为浅灰色/虚线,或直接隐藏 | 粗黑实线网格,遮盖趋势线 |
| **字体统一** | 图表内字体需与 Word 正文匹配建议微软雅黑或等线9pt | 默认小号宋体,缩放后模糊 |
| **数据源标注** | 图表下方须注明数据来源WindXX证券研究所 | 来源不明 |
| **标签间隔** | X轴数据点密集时需设置 `tickLblSkip` 防止重叠 | 标签重叠难以辨认 |
| **数值精度** | Y轴根据数据类型设置合适的小数位价格2位百分比1-2位 | 小数位过多或过少 |
#### 高度封装工具函数
工具函数已封装在 `scripts/chart_utils.py` 中,使用时直接导入:
```python
import sys
sys.path.insert(0, ".claude/skills/financial-report-writing/scripts")
from chart_utils import create_research_chart, create_price_volume_chart
import pandas as pd
# 准备数据
df = pd.DataFrame({
"date": ["2024-01", "2024-02", "2024-03"],
"close": [10.5, 11.2, 10.8]
})
# 生成股价走势图
wb = create_research_chart(
df,
chart_type="line",
title="股价走势",
x_col="date",
y_cols="close",
y_axis_title="价格(元)",
show_last_label_only=True,
add_mean_line=True,
output_path="股价走势.xlsx"
)
```
**可用函数**
| 函数 | 用途 |
|------|------|
| `create_research_chart()` | 通用图表(折线/柱状/多序列) |
| `create_price_volume_chart()` | 股价+成交量双Y轴组合图 |
**`create_research_chart()` 主要参数**
| 参数 | 说明 | 默认值 |
|------|------|--------|
| `df` | DataFrame数据源 | 必填 |
| `chart_type` | 图表类型:"line"/"bar" | "line" |
| `x_col` | X轴列名 | 必填 |
| `y_cols` | Y轴列名支持列表 | 必填 |
| `y_axis_title` | Y轴标题建议带单位 | "" |
| `y_format` | 数字格式 | "0.00" |
| `show_last_label_only` | 仅显示最后一个标签 | False |
| `add_mean_line` | 添加均值参考线 | False |
| `color_up/color_down` | 涨跌颜色 | FF0000/00B050 |
| `tick_skip` | X轴标签间隔 | 5 |
完整函数定义和源码见:`scripts/chart_utils.py`
## 相关工具
- **WindPy**Wind金融终端Python API
- **python-docx**Word文档操作库
- **openpyxl**Excel文件操作支持原生图表
- **pandas**:数据处理和分析
- **numpy**:数值计算
- **GB/T 9704-2012**:党政机关公文格式标准
## 参考资源
- WindPy SDK官方文档
- python-docx API文档
- GB/T 9704-2012国家公文格式标准
- 券商研究报告格式规范

View File

@ -0,0 +1,337 @@
# 金融研究报告最佳实践
## 目录
1. [数据获取最佳实践](#数据获取最佳实践)
2. [分析框架](#分析框架)
3. [报告撰写规范](#报告撰写规范)
4. [常见陷阱](#常见陷阱)
5. [质量控制检查清单](#质量控制检查清单)
6. [案例分析](#案例分析)
---
## 数据获取最佳实践
### WindPy 连接管理
```python
# 推荐做法:使用上下文管理器
with WindDataFetcher() as fetcher:
df = fetcher.get_daily_data(stock_code, start_date, end_date, fields)
# 自动处理连接关闭
# 不推荐:手动管理连接
w.start()
# ... 代码 ...
w.stop() # 容易遗漏
```
### 日期处理
- 始终使用标准格式 `YYYY-MM-DD`
- 获取周线数据时,使用 `resample('W')` 而非手动计算
- 处理交易日历,避免非交易日数据的干扰
### 数据校验
```python
def validate_data(df, required_columns):
"""数据质量检查"""
# 检查空值
if df.isnull().any().any():
print(f"警告:数据包含空值")
df = df.dropna()
# 检查异常值
for col in ['close', 'high', 'low']:
if col in df.columns:
if (df[col] <= 0).any():
raise ValueError(f"{col} 包含非正值")
# 检查必需字段
missing_cols = set(required_columns) - set(df.columns)
if missing_cols:
raise ValueError(f"缺少必需字段: {missing_cols}")
return df
```
---
## 分析框架
### 基本面分析维度
1. **盈利能力**
- ROE净资产收益率\>15% 优秀10-15% 良好
- ROA总资产收益率\>8% 优秀
- 毛利率:行业对比
- 净利率:趋势分析
2. **成长能力**
- 营收增长率
- 净利润增长率
- 复合年均增长率CAGR
3. **运营效率**
- 存货周转率
- 应收账款周转率
- 总资产周转率
4. **偿债能力**
- 资产负债率:<70% 安全
- 流动比率:>1.5 较好
- 速动比率:>1 较好
5. **现金流**
- 经营现金流净额
- 自由现金流
- 现金流/净利润 比率
### 技术面分析维度
1. **趋势判断**
- 均线系统MA5/10/20/60
- 高低点连线
- 趋势线斜率
2. **支撑阻力**
- 历史高低点
- 密集成交区
- 均线支撑
3. **波动特征**
- 历史波动率
- 振幅统计
- 布林带宽度
### 估值分析框架
1. **相对估值**
- PE市盈率与历史/行业对比
- PB市净率适用于重资产行业
- PS市销率适用于成长型企业
2. **绝对估值**
- DCF模型
- DDM模型
---
## 报告撰写规范
### 标题层级
```
一级标题黑体三号16pt如"一、投资评级"
二级标题黑体三号16pt如"(一)基本面分析"
三级标题黑体小四12pt如"1. 盈利能力"
```
### 正文写作原则
1. **数据先行**:先陈述事实数据,后给出分析结论
2. **逻辑清晰**:使用"首先...其次...最后..."等逻辑连接词
3. **客观中立**:避免使用"必然"、"一定"等绝对化表述
4. **风险提示**:每个投资建议都应附带风险提示
### 表格规范
- **表头**黑体小四12pt居中
- **数据**仿宋小四12pt居中
- **对齐**:日期左对齐,数值右对齐
- **单位**:在表头或表注中明确标注
### 数字格式
```python
# 价格保留2位小数
price_str = f"{price:.2f}元"
# 百分比保留2位小数带%号
pct_str = f"{pct:.2f}%"
# 大数字:使用万/亿单位
amount_wan = f"{amount / 10000:.0f}万元"
amount_yi = f"{amount / 100000000:.2f}亿元"
```
---
## 常见陷阱
### 数据陷阱
1. **前复权与后复权混淆**
- 计算收益率时必须使用前复权数据
- 查看历史价格时注意复权方式
2. **停牌数据处理**
- 停牌期间数据可能为空
- 成交量为0需要特殊处理
3. **财报季节效应**
- Q4数据通常包含全年调整
- 同比计算时注意去年同期特殊情况
### 分析陷阱
1. **幸存者偏差**
- 只分析现有公司,忽视已退市公司
- 行业对比时使用可比较样本
2. **过度拟合**
- 避免基于历史数据过度优化参数
- 使用滚动回测验证策略
3. **相关性不等于因果性**
- 相关系数高不代表因果关系
- 考虑第三变量的影响
### 报告陷阱
1. **目标价计算过于乐观**
- 考虑多种情景(乐观/中性/悲观)
- 明确目标价有效期
2. **忽视流动性风险**
- 小市值股票注意流动性
- 考虑交易成本影响
3. **风险提示流于形式**
- 针对具体股票的风险要具体化
- 量化风险等级
---
## 质量控制检查清单
### 数据检查
- [ ] 数据来源明确标注
- [ ] 数据时间范围正确
- [ ] 无缺失值或已说明处理方式
- [ ] 价格数据使用正确复权方式
- [ ] 财务数据使用最新财报
### 分析检查
- [ ] ROE、PE等关键指标计算正确
- [ ] 涨跌幅计算准确
- [ ] 均线计算无未来函数
- [ ] 支撑阻力位基于实际高低点
### 格式检查
- [ ] 页面边距37/35/28/26mm
- [ ] 标题使用黑体三号
- [ ] 正文使用仿宋四号
- [ ] 表格使用小四
- [ ] 三线表格式正确
- [ ] 页脚页码格式正确
### 内容检查
- [ ] 核心提要包含4个要点
- [ ] 投资评级明确
- [ ] 目标价格合理且有依据
- [ ] 风险提示具体充分
- [ ] 无明显错别字
- [ ] 语句通顺无歧义
---
## 案例分析
### 案例1成长股分析报告
**股票特征**高ROE、高PE、快速增长
**分析重点**
- 关注营收和利润增长持续性
- 分析行业空间和竞争格局
- 评估估值合理性PEG指标
**报告结构**
```
核心提要
├── 高速增长:营收/利润CAGR > 30%
├── 行业龙头:市场份额持续提升
├── 估值偏高PE高于行业平均
└── 给予"增持"评级
一、投资评级
├── 增持
├── 目标价
└── 风险提示:增速放缓风险
二、核心观点
├── (一)基本面分析
│ ├── 1. 成长性营收增长35%净利润增长42%
│ ├── 2. 盈利能力ROE 22%毛利率58%
│ └── 3. 估值PE 45倍PEG 1.1
├── (二)技术面分析
│ ├── 1. 趋势:长期上升通道
│ ├── 2. 支撑MA60支撑有效
│ └── 3. 成交量:放量突破
└── (三)投资建议
└── 关注回调至支撑位买入机会
```
### 案例2价值股分析报告
**股票特征**低PE、高股息、稳定增长
**分析重点**
- 关注股息率和分红稳定性
- 分析护城河和竞争优势
- 评估安全边际
**核心指标对比**
| 指标 | 该公司 | 行业平均 | 评价 |
|------|--------|----------|------|
| PE TTM | 8.5 | 15.2 | 低估 |
| PB LF | 1.2 | 2.1 | 低估 |
| 股息率 | 5.2% | 2.8% | 优秀 |
| ROE | 14.5% | 12.3% | 良好 |
### 案例3周期股分析报告
**股票特征**:业绩波动大、与宏观经济高度相关
**分析重点**
- 关注行业景气度周期
- 分析产能利用率和库存
- 判断周期位置(顶部/底部/上升/下降)
**风险提示模板**
```
风险提示:
1. 宏观经济下行风险
2. 行业产能过剩风险
3. 原材料价格波动风险
4. 下游需求不及预期风险
5. 政策监管变化风险
```
---
## 附录:常用 Wind 字段速查
### 价格字段
| 字段 | 说明 | 用途 |
|------|------|------|
| close | 收盘价 | 技术分析基础 |
| open | 开盘价 | 日K线 |
| high | 最高价 | 支撑阻力分析 |
| low | 最低价 | 支撑阻力分析 |
| volume | 成交量 | 量价分析 |
| amt | 成交额 | 流动性分析 |
| pct_chg | 涨跌幅 | 收益率计算 |
### 技术指标字段
| 字段 | 说明 | 用途 |
|------|------|------|
| ma5/ma10/ma20/ma60 | 均线 | 趋势判断 |
| pe_ttm | 市盈率TTM | 估值分析 |
| pb_lf | 市净率LF | 估值分析 |
| ps_ttm | 市销率TTM | 估值分析 |
| turnover | 换手率 | 活跃度分析 |
### 财务字段
| 字段 | 说明 | 用途 |
|------|------|------|
| roe_wgt | 加权ROE | 盈利能力 |
| roa2 | 总资产报酬率 | 盈利能力 |
| grossprofitmargin | 毛利率 | 盈利能力 |
| netprofitmargin | 净利率 | 盈利能力 |
| debt_to_assets | 资产负债率 | 偿债能力 |
| current_ratio | 流动比率 | 偿债能力 |
---
## 版本记录
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| 1.0 | 2026-02-06 | 初始版本,包含基础最佳实践 |

View File

@ -0,0 +1,469 @@
"""
投资分析引擎
提供基本面分析技术面分析和估值分析功能
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
class InvestmentRating(Enum):
"""投资评级"""
BUY = "买入"
ACCUMULATE = "增持"
HOLD = "持有"
REDUCE = "减持"
SELL = "卖出"
class RiskLevel(Enum):
"""风险等级"""
LOW = "低风险"
LOW_MEDIUM = "中低风险"
MEDIUM = "中等风险"
MEDIUM_HIGH = "中高风险"
HIGH = "高风险"
@dataclass
class TechnicalAnalysis:
"""技术分析结果"""
trend: str # 趋势判断
support_level: float # 支撑位
resistance_level: float # 阻力位
volatility: float # 波动率
ma_status: str # 均线状态
technical_summary: str # 技术总结
@dataclass
class FundamentalAnalysis:
"""基本面分析结果"""
roe: float # 净资产收益率
roe_comment: str # ROE评价
pe_ttm: float # 市盈率TTM
pb_lf: float # 市净率LF
valuation_comment: str # 估值评价
fundamental_summary: str # 基本面总结
@dataclass
class InvestmentRecommendation:
"""投资建议"""
rating: InvestmentRating
target_price: float
risk_level: RiskLevel
recommendation_text: str
key_factors: List[str]
class AnalysisEngine:
"""
投资分析引擎
"""
# ROE评价标准
ROE_THRESHOLDS = {
'excellent': 20,
'good': 15,
'average': 10,
'poor': 5
}
# PE评价标准参考值实际应根据行业调整
PE_THRESHOLDS = {
'undervalued': 15,
'fair': 25,
'overvalued': 35
}
# PB评价标准
PB_THRESHOLDS = {
'undervalued': 1.5,
'fair': 3.0,
'overvalued': 5.0
}
def __init__(self, price_df: pd.DataFrame, financial_data: Dict):
"""
初始化分析引擎
Args:
price_df: 价格数据DataFrame
financial_data: 财务数据字典
"""
self.price_df = price_df.copy()
self.financial_data = financial_data
# 计算涨跌幅和收益率
if 'pct_chg' not in self.price_df.columns and 'close' in self.price_df.columns:
self.price_df['pct_chg'] = self.price_df['close'].pct_change() * 100
def analyze_technical(self) -> TechnicalAnalysis:
"""
技术分析
Returns:
TechnicalAnalysis对象
"""
df = self.price_df
# 计算均线
if 'ma5' not in df.columns:
df['ma5'] = df['close'].rolling(window=5).mean()
if 'ma10' not in df.columns:
df['ma10'] = df['close'].rolling(window=10).mean()
if 'ma20' not in df.columns:
df['ma20'] = df['close'].rolling(window=20).mean()
latest = df.iloc[-1]
# 趋势判断
if latest['close'] > latest['ma5'] > latest['ma10'] > latest['ma20']:
trend = "强势上涨"
elif latest['close'] > latest['ma5'] > latest['ma10']:
trend = "震荡上行"
elif latest['close'] < latest['ma5'] < latest['ma10'] < latest['ma20']:
trend = "强势下跌"
elif latest['close'] < latest['ma5'] < latest['ma10']:
trend = "震荡下行"
else:
trend = "震荡整理"
# 计算支撑位和阻力位(基于近期高低点)
recent_data = df.tail(20)
support_level = recent_data['low'].min()
resistance_level = recent_data['high'].max()
# 计算波动率
returns = df['close'].pct_change().dropna()
volatility = returns.std() * np.sqrt(52) * 100 # 年化波动率
# 均线状态
if latest['ma5'] > latest['ma20']:
ma_status = "多头排列"
elif latest['ma5'] < latest['ma20']:
ma_status = "空头排列"
else:
ma_status = "均线粘合"
# 技术总结
if trend in ["强势上涨", "震荡上行"] and ma_status == "多头排列":
technical_summary = "技术面呈上升趋势,建议关注回调买入机会。"
elif trend in ["强势下跌", "震荡下行"]:
technical_summary = "技术面呈下降趋势,建议观望或减仓。"
else:
technical_summary = "技术面呈震荡格局,建议高抛低吸。"
return TechnicalAnalysis(
trend=trend,
support_level=round(support_level, 2),
resistance_level=round(resistance_level, 2),
volatility=round(volatility, 2),
ma_status=ma_status,
technical_summary=technical_summary
)
def analyze_fundamental(self) -> FundamentalAnalysis:
"""
基本面分析
Returns:
FundamentalAnalysis对象
"""
# 获取ROE
roe = self.financial_data.get('roe_wgt', 0) or self.financial_data.get('roe', 0)
# ROE评价
if roe >= self.ROE_THRESHOLDS['excellent']:
roe_comment = "ROE表现优异公司盈利能力强劲"
elif roe >= self.ROE_THRESHOLDS['good']:
roe_comment = "ROE表现良好盈利能力较强"
elif roe >= self.ROE_THRESHOLDS['average']:
roe_comment = "ROE处于行业平均水平"
else:
roe_comment = "ROE偏低需关注盈利能力改善"
# 获取PE和PB
pe_ttm = self.financial_data.get('pe_ttm', 0) or self.price_df['pe_ttm'].iloc[-1] if 'pe_ttm' in self.price_df.columns else 0
pb_lf = self.financial_data.get('pb_lf', 0) or self.price_df['pb_lf'].iloc[-1] if 'pb_lf' in self.price_df.columns else 0
# 估值评价
if pe_ttm <= self.PE_THRESHOLDS['undervalued'] and pb_lf <= self.PB_THRESHOLDS['undervalued']:
valuation_comment = "估值处于历史低位,具备较高安全边际"
elif pe_ttm <= self.PE_THRESHOLDS['fair']:
valuation_comment = "估值处于合理区间"
elif pe_ttm <= self.PE_THRESHOLDS['overvalued']:
valuation_comment = "估值略高,需关注业绩成长性"
else:
valuation_comment = "估值偏高,注意追高风险"
# 基本面总结
if roe >= self.ROE_THRESHOLDS['good'] and pe_ttm <= self.PE_THRESHOLDS['fair']:
fundamental_summary = "公司基本面健康,盈利能力强,估值合理,具备投资价值。"
elif roe >= self.ROE_THRESHOLDS['good']:
fundamental_summary = "公司盈利能力较强,但估值偏高,建议等待回调机会。"
else:
fundamental_summary = "公司基本面一般,建议谨慎观望。"
return FundamentalAnalysis(
roe=round(roe, 2) if roe else 0,
roe_comment=roe_comment,
pe_ttm=round(pe_ttm, 2) if pe_ttm else 0,
pb_lf=round(pb_lf, 2) if pb_lf else 0,
valuation_comment=valuation_comment,
fundamental_summary=fundamental_summary
)
def generate_recommendation(self,
current_price: float,
fundamental: FundamentalAnalysis = None,
technical: TechnicalAnalysis = None) -> InvestmentRecommendation:
"""
生成投资建议
Args:
current_price: 当前价格
fundamental: 基本面分析结果
technical: 技术面分析结果
Returns:
InvestmentRecommendation对象
"""
if fundamental is None:
fundamental = self.analyze_fundamental()
if technical is None:
technical = self.analyze_technical()
# 综合评分
score = 0
# 基本面评分
if fundamental.roe >= self.ROE_THRESHOLDS['excellent']:
score += 3
elif fundamental.roe >= self.ROE_THRESHOLDS['good']:
score += 2
elif fundamental.roe >= self.ROE_THRESHOLDS['average']:
score += 1
# 估值评分
if fundamental.pe_ttm <= self.PE_THRESHOLDS['undervalued']:
score += 3
elif fundamental.pe_ttm <= self.PE_THRESHOLDS['fair']:
score += 2
elif fundamental.pe_ttm <= self.PE_THRESHOLDS['overvalued']:
score += 1
# 技术面评分
if technical.trend in ["强势上涨", "震荡上行"]:
score += 2
elif technical.trend == "震荡整理":
score += 1
# 风险等级
if technical.volatility > 50:
risk_level = RiskLevel.HIGH
elif technical.volatility > 35:
risk_level = RiskLevel.MEDIUM_HIGH
elif technical.volatility > 25:
risk_level = RiskLevel.MEDIUM
elif technical.volatility > 15:
risk_level = RiskLevel.LOW_MEDIUM
else:
risk_level = RiskLevel.LOW
# 目标价格计算基于PE和支撑阻力
target_price_pe = current_price * (self.PE_THRESHOLDS['fair'] / max(fundamental.pe_ttm, 1))
target_price_resistance = technical.resistance_level * 1.1
target_price = round((target_price_pe + target_price_resistance) / 2, 2)
# 投资建议
key_factors = []
if score >= 6:
rating = InvestmentRating.BUY
recommendation_text = f"综合基本面和技术面分析,该股投资价值较高。目标价{target_price}元,建议逢低买入。"
key_factors.append("盈利能力强劲ROE表现优异")
elif score >= 4:
rating = InvestmentRating.ACCUMULATE
recommendation_text = f"公司基本面良好,建议关注回调机会逐步建仓。目标价{target_price}元。"
key_factors.append("基本面稳健,估值合理")
elif score >= 2:
rating = InvestmentRating.HOLD
recommendation_text = "公司基本面一般,当前估值合理,建议持有观望。"
key_factors.append("基本面中性,估值合理")
else:
rating = InvestmentRating.REDUCE
recommendation_text = "公司基本面较弱或估值偏高,建议谨慎或减仓。"
key_factors.append("基本面一般或估值偏高")
if technical.trend in ["强势上涨", "震荡上行"]:
key_factors.append("技术面呈上升趋势")
elif technical.trend in ["强势下跌", "震荡下行"]:
key_factors.append("技术面呈下降趋势")
if fundamental.pe_ttm <= self.PE_THRESHOLDS['undervalued']:
key_factors.append("估值处于历史低位")
return InvestmentRecommendation(
rating=rating,
target_price=target_price,
risk_level=risk_level,
recommendation_text=recommendation_text,
key_factors=key_factors
)
def generate_core_summary(self,
fundamental: FundamentalAnalysis = None,
technical: TechnicalAnalysis = None,
recommendation: InvestmentRecommendation = None) -> List[str]:
"""
生成核心提要要点
Returns:
要点列表
"""
if fundamental is None:
fundamental = self.analyze_fundamental()
if technical is None:
technical = self.analyze_technical()
if recommendation is None:
# 获取最新价格
latest_price = self.price_df['close'].iloc[-1] if 'close' in self.price_df.columns else 0
recommendation = self.generate_recommendation(latest_price, fundamental, technical)
summary_points = []
# ROE要点
if fundamental.roe > 0:
summary_points.append(
f"该公司最新净资产收益率ROE{fundamental.roe}%{fundamental.roe_comment}"
)
# 估值要点
if fundamental.pe_ttm > 0:
summary_points.append(
f"当前市盈率TTM{fundamental.pe_ttm}倍,{fundamental.valuation_comment}"
)
# 技术面要点
summary_points.append(
f"技术面显示股价呈现{technical.trend}趋势,{technical.technical_summary}"
)
# 投资建议要点
summary_points.append(
f"给予\"{recommendation.rating.value}\"评级,目标价格{recommendation.target_price}元,风险等级为{recommendation.risk_level.value}"
)
return summary_points
def get_price_statistics(self) -> Dict:
"""
获取价格统计数据
Returns:
统计数据字典
"""
df = self.price_df
if len(df) == 0:
return {}
return {
'latest_price': round(df['close'].iloc[-1], 2),
'highest_price': round(df['high'].max(), 2),
'lowest_price': round(df['low'].min(), 2),
'avg_price': round(df['close'].mean(), 2),
'total_change': round(((df['close'].iloc[-1] / df['close'].iloc[0]) - 1) * 100, 2),
'max_daily_gain': round(df['pct_chg'].max(), 2) if 'pct_chg' in df.columns else 0,
'max_daily_loss': round(df['pct_chg'].min(), 2) if 'pct_chg' in df.columns else 0,
'volatility': round(df['close'].pct_change().std() * np.sqrt(52) * 100, 2)
}
def perform_comprehensive_analysis(price_df: pd.DataFrame,
financial_data: Dict,
current_price: float = None) -> Dict:
"""
执行综合分析并返回所有结果
Args:
price_df: 价格数据
financial_data: 财务数据
current_price: 当前价格可选
Returns:
包含所有分析结果的字典
"""
engine = AnalysisEngine(price_df, financial_data)
if current_price is None:
current_price = price_df['close'].iloc[-1]
technical = engine.analyze_technical()
fundamental = engine.analyze_fundamental()
recommendation = engine.generate_recommendation(current_price, fundamental, technical)
summary_points = engine.generate_core_summary(fundamental, technical, recommendation)
price_stats = engine.get_price_statistics()
return {
'technical': technical,
'fundamental': fundamental,
'recommendation': recommendation,
'summary_points': summary_points,
'price_stats': price_stats
}
if __name__ == '__main__':
# 测试代码
import numpy as np
# 创建模拟数据
dates = pd.date_range(start='2025-01-01', periods=60, freq='W')
np.random.seed(42)
prices = 30 + np.cumsum(np.random.randn(60) * 0.5)
price_df = pd.DataFrame({
'date': dates,
'open': prices * 0.98,
'high': prices * 1.03,
'low': prices * 0.97,
'close': prices,
'volume': np.random.randint(1000000, 5000000, 60),
'pe_ttm': 25 + np.random.randn(60) * 2,
'pb_lf': 3 + np.random.randn(60) * 0.2
})
financial_data = {
'roe_wgt': 16.5,
'pe_ttm': 25.3,
'pb_lf': 3.1,
'sec_name': '测试公司'
}
# 执行分析
results = perform_comprehensive_analysis(price_df, financial_data)
print("=== 技术分析 ===")
print(f"趋势: {results['technical'].trend}")
print(f"支撑位: {results['technical'].support_level}")
print(f"阻力位: {results['technical'].resistance_level}")
print("\n=== 基本面分析 ===")
print(f"ROE: {results['fundamental'].roe}%")
print(f"PE TTM: {results['fundamental'].pe_ttm}")
print(f"PB LF: {results['fundamental'].pb_lf}")
print("\n=== 投资建议 ===")
print(f"评级: {results['recommendation'].rating.value}")
print(f"目标价: {results['recommendation'].target_price}")
print(f"风险等级: {results['recommendation'].risk_level.value}")
print("\n=== 核心提要 ===")
for i, point in enumerate(results['summary_points'], 1):
print(f"{i}. {point}")

View File

@ -0,0 +1,311 @@
"""
金融研究报告图表工具函数
============================
提供符合研报规范的高度封装图表生成函数
Usage:
from chart_utils import create_research_chart, create_price_volume_chart
wb = create_research_chart(
df,
chart_type="line",
title="股价走势",
x_col="date",
y_cols="close",
output_path="report.xlsx"
)
"""
from openpyxl import Workbook
from openpyxl.chart import LineChart, BarChart, Reference
from openpyxl.chart.axis import DateAxis
from openpyxl.chart.label import DataLabelList
from openpyxl.chart.series import DataPoint, Series
from openpyxl.chart.shapes import GraphicalProperties
import pandas as pd
from typing import Optional, List, Union
def create_research_chart(
df: pd.DataFrame,
chart_type: str = "line",
title: str = "",
x_col: str = None,
y_cols: Union[str, List[str]] = None,
y_axis_title: str = "",
x_axis_title: str = "",
y_format: str = "0.00",
show_data_labels: bool = False,
show_last_label_only: bool = False,
add_mean_line: bool = False,
color_up: str = "FF0000",
color_down: str = "00B050",
tick_skip: int = 5,
height: int = 10,
width: int = 20,
output_path: str = None
) -> Workbook:
"""
生成符合研报规范的 Excel 图表
Parameters:
-----------
df : pd.DataFrame
数据源必须包含 x_col y_cols 指定的列
chart_type : str
图表类型"line"(折线图), "bar"(柱状图)
title : str
图表标题
x_col : str
X轴数据列名日期/类别
y_cols : str or List[str]
Y轴数据列名支持多序列
y_axis_title : str
Y轴标题建议带单位"价格(元)"
x_axis_title : str
X轴标题
y_format : str
Y轴数字格式默认"0.00"百分比用"0.00%"
show_data_labels : bool
是否显示所有数据标签
show_last_label_only : bool
是否仅显示最后一个数据点的标签标注最新值
add_mean_line : bool
是否添加均值参考线
color_up : str
上涨/正值颜色默认红色 FF0000
color_down : str
下跌/负值颜色默认绿色 00B050
tick_skip : int
X轴标签间隔防止重叠
height : int
图表高度厘米
width : int
图表宽度厘米
output_path : str
输出文件路径None则返回Workbook对象
Returns:
--------
Workbook : openpyxl Workbook对象
Examples:
---------
>>> # 股价走势图
>>> df = pd.DataFrame({
... 'date': ['2024-01', '2024-02', '2024-03'],
... 'close': [10.5, 11.2, 10.8]
... })
>>> wb = create_research_chart(
... df, chart_type="line",
... title="股价走势",
... x_col="date", y_cols="close",
... y_axis_title="价格(元)",
... show_last_label_only=True,
... output_path="股价走势.xlsx"
... )
>>> # 营收利润对比图
>>> wb = create_research_chart(
... df, chart_type="bar",
... title="营业收入与净利润",
... x_col="period", y_cols=["revenue", "profit"],
... y_axis_title="金额(亿元)",
... y_format="0.0"
... )
"""
# 标准化 y_cols
if isinstance(y_cols, str):
y_cols = [y_cols]
# 创建工作簿
wb = Workbook()
ws = wb.active
ws.title = "数据"
# 写入表头
headers = [x_col] + y_cols
ws.append(headers)
# 写入数据
for _, row in df.iterrows():
ws.append([row[col] for col in headers])
# 创建图表
if chart_type == "line":
chart = LineChart()
elif chart_type == "bar":
chart = BarChart()
chart.type = "col"
chart.grouping = "clustered"
else:
chart = LineChart()
# 设置标题和轴
chart.title = title
chart.y_axis.title = y_axis_title
chart.x_axis.title = x_axis_title
# 设置图表尺寸
chart.height = height
chart.width = width
# 设置数据区域
data_start_row = 1
data_end_row = len(df) + 1
for i, y_col in enumerate(y_cols):
col_idx = headers.index(y_col) + 1
data_ref = Reference(ws, min_col=col_idx, min_row=data_start_row,
max_row=data_end_row)
cats_ref = Reference(ws, min_col=1, min_row=2, max_row=data_end_row)
chart.add_data(data_ref, titles_from_data=True)
if i == 0:
chart.set_categories(cats_ref)
# 设置Y轴格式
chart.y_axis.numFmt = y_format
# 设置X轴标签间隔
chart.x_axis.tickLblSkip = tick_skip
# 网格线设置(仅保留水平主网格线)
chart.x_axis.majorGridlines = None
# 数据标签设置
if show_data_labels:
chart.dataLabels = DataLabelList()
chart.dataLabels.showVal = True
# 仅显示最后一个标签
if show_last_label_only and chart.series:
series = chart.series[0]
last_idx = len(df) - 1
pt = DataPoint(idx=last_idx)
pt.graphicalProperties = GraphicalProperties(solidFill=color_up)
series.data_points = [pt]
# 柱状图涨跌着色
if chart_type == "bar" and len(y_cols) == 1:
series = chart.series[0]
values = df[y_cols[0]].tolist()
for i, val in enumerate(values):
pt = DataPoint(idx=i)
fill_color = color_up if val >= 0 else color_down
pt.graphicalProperties = GraphicalProperties(solidFill=fill_color)
series.data_points.append(pt)
# 添加均值参考线
if add_mean_line and chart.series:
series = chart.series[0]
values = df[y_cols[0]].tolist()
mean_val = sum(values) / len(values)
# 在工作表添加均值列
mean_col = len(headers) + 1
ws.cell(row=1, column=mean_col, value="均值")
for i in range(2, data_end_row + 1):
ws.cell(row=i, column=mean_col, value=mean_val)
mean_ref = Reference(ws, min_col=mean_col, min_row=1, max_row=data_end_row)
mean_series = Series(mean_ref, title="均值")
chart.series.append(mean_series)
# 图例位置(多序列时显示在底部)
if len(y_cols) > 1 or add_mean_line:
chart.legend.position = "b"
else:
chart.legend = None
# 添加图表到工作表
ws.add_chart(chart, "E2")
# 添加数据来源注脚
ws["A" + str(data_end_row + 2)] = "数据来源Wind"
# 保存或返回
if output_path:
wb.save(output_path)
return wb
def create_price_volume_chart(
df: pd.DataFrame,
title: str = "股价与成交量",
date_col: str = "date",
price_col: str = "close",
volume_col: str = "volume",
output_path: str = None
) -> Workbook:
"""
生成股价+成交量组合图表双Y轴
Parameters:
-----------
df : pd.DataFrame
包含日期收盘价成交量的数据
title : str
图表标题
date_col, price_col, volume_col : str
各数据列名
output_path : str
输出路径
Returns:
--------
Workbook
"""
wb = Workbook()
ws = wb.active
ws.title = "数据"
# 写入数据
ws.append([date_col, price_col, volume_col])
for _, row in df.iterrows():
ws.append([row[date_col], row[price_col], row[volume_col]])
# 创建价格折线图主Y轴
price_chart = LineChart()
price_chart.title = title
price_chart.y_axis.title = "价格(元)"
price_chart.x_axis.title = date_col
price_ref = Reference(ws, min_col=2, min_row=1, max_row=len(df)+1)
cats_ref = Reference(ws, min_col=1, min_row=2, max_row=len(df)+1)
price_chart.add_data(price_ref, titles_from_data=True)
price_chart.set_categories(cats_ref)
price_chart.y_axis.numFmt = "0.00"
price_chart.x_axis.tickLblSkip = max(1, len(df) // 10)
# 创建成交量柱状图次Y轴
vol_chart = BarChart()
vol_chart.type = "col"
vol_chart.grouping = "clustered"
vol_chart.y_axis.axId = 200
vol_chart.y_axis.title = "成交量(手)"
vol_ref = Reference(ws, min_col=3, min_row=1, max_row=len(df)+1)
vol_chart.add_data(vol_ref, titles_from_data=True)
# 组合图表
price_chart += vol_chart
price_chart.y_axis.crosses = "max"
# 尺寸设置
price_chart.height = 10
price_chart.width = 20
# 隐藏网格线
price_chart.x_axis.majorGridlines = None
ws.add_chart(price_chart, "E2")
ws["A" + str(len(df) + 3)] = "数据来源Wind"
if output_path:
wb.save(output_path)
return wb

View File

@ -0,0 +1,265 @@
"""
Wind 数据获取模块
提供统一的接口获取股票历史数据财务数据和技术指标
"""
from WindPy import w
import pandas as pd
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Tuple
class WindDataFetcher:
"""Wind数据获取器"""
def __init__(self):
"""初始化Wind连接"""
self._start()
def _start(self):
"""启动Wind连接"""
result = w.start()
if result.ErrorCode != 0:
raise ConnectionError(f"Wind连接失败: {result.Data}")
print("Wind连接成功")
def _stop(self):
"""关闭Wind连接"""
w.stop()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self._stop()
@staticmethod
def _handle_wsd_result(result, field_names):
"""处理wsd返回结果"""
if result.ErrorCode != 0:
raise ValueError(f"数据获取失败: {result.Data}")
# 转换为DataFrame
data_dict = dict(zip(field_names, result.Data))
df = pd.DataFrame(data_dict, columns=field_names)
df['date'] = result.Times
return df
def get_daily_data(self,
stock_code: str,
start_date: str,
end_date: str,
fields: List[str]) -> pd.DataFrame:
"""
获取日线数据
Args:
stock_code: 股票代码 '300866.SZ'
start_date: 开始日期格式 'YYYY-MM-DD'
end_date: 结束日期格式 'YYYY-MM-DD'
fields: 数据字段列表
Returns:
DataFrame包含日期和指定字段的数据
"""
result = w.wsd(stock_code, fields, start_date, end_date)
return self._handle_wsd_result(result, fields)
def get_stock_basic(self, stock_code: str) -> Dict:
"""
获取股票基本信息
Args:
stock_code: 股票代码
Returns:
包含股票名称行业上市日期等信息的字典
"""
fields = ['sec_name', 'industry', 'ipo_date', 'list_date', 'trade_status']
result = w.wss(stock_code, fields)
if result.ErrorCode != 0:
raise ValueError(f"基本信息获取失败: {result.Data}")
return dict(zip(fields, result.Data[0]))
def get_technical_indicators(self,
stock_code: str,
start_date: str,
end_date: str) -> pd.DataFrame:
"""
获取技术指标数据
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
Returns:
DataFrame包含MA成交量等技术指标
"""
fields = [
'close', 'open', 'high', 'low', 'volume', 'amt', 'pct_chg',
'ma5', 'ma10', 'ma20', 'ma60',
'pe_ttm', 'pb_lf', 'ps_ttm', 'pcf_ncf_ttm'
]
return self.get_daily_data(stock_code, start_date, end_date, fields)
def get_financial_data(self,
stock_code: str,
report_date: str = '') -> Dict:
"""
获取财务数据
Args:
stock_code: 股票代码
report_date: 报告期格式 'YYYYMMDD'默认最新
Returns:
包含ROE营收净利润等财务指标的字典
"""
fields = [
'roe_wgt', 'roa2', 'net_profit_to_profit', 'total_revenue_ps',
'profit_to_gr', 'ebit_ps', 'assets_to_eqt', 'debt_to_assets',
'current_ratio', 'quick_ratio', 'op_income_to_revenue'
]
result = w.wss(stock_code, fields, 'rptDate={}'.format(report_date))
if result.ErrorCode != 0:
raise ValueError(f"财务数据获取失败: {result.Data}")
return dict(zip(fields, result.Data[0]))
def get_weekly_data(self,
stock_code: str,
start_date: str,
end_date: str,
fields: List[str] = None) -> pd.DataFrame:
"""
获取并汇总周线数据
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
fields: 数据字段默认为基本OHLCV
Returns:
DataFrame包含周线数据
"""
if fields is None:
fields = ['close', 'open', 'high', 'low', 'volume', 'amt', 'pct_chg']
# 获取日线数据
df_daily = self.get_daily_data(stock_code, start_date, end_date, fields)
# 转换为周线数据
df_daily.set_index('date', inplace=True)
df_weekly = df_daily.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum',
'amt': 'sum',
'pct_chg': 'sum' # 周涨跌幅近似为日涨跌幅之和
}).dropna()
df_weekly.reset_index(inplace=True)
return df_weekly
def get_stock_list_by_sector(self, sector_name: str, limit: int = 10) -> List[str]:
"""
获取某行业的龙头股票列表
Args:
sector_name: 行业名称
limit: 返回数量
Returns:
股票代码列表
"""
# 使用Wind的行业板块数据
fields = ['sec_name', 'ipo_date']
# 这里简化处理实际需要根据Wind板块接口调整
print(f"注意: 行业股票列表获取需要根据实际Wind板块接口实现")
return []
def calculate_volatility(self, df: pd.DataFrame, annualized: bool = True) -> float:
"""
计算波动率
Args:
df: 包含收盘价的DataFrame
annualized: 是否年化
Returns:
波动率百分比
"""
returns = df['close'].pct_change().dropna()
volatility = returns.std()
if annualized:
# 日数据年化系数sqrt(252)周数据年化系数sqrt(52)
n = len(df)
if n > 250: # 日线数据
volatility *= 252 ** 0.5
else: # 周线数据
volatility *= 52 ** 0.5
return volatility * 100 # 转换为百分比
# 便捷函数
def fetch_stock_report_data(stock_code: str,
days_back: int = 90,
weekly: bool = True) -> Tuple[pd.DataFrame, Dict]:
"""
获取股票报告所需的数据
Args:
stock_code: 股票代码
days_back: 获取多少天的数据
weekly: 是否为周线数据
Returns:
(price_df, financial_dict) 价格数据和财务数据
"""
end_date = datetime.now().strftime('%Y-%m-%d')
start_date = (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d')
with WindDataFetcher() as fetcher:
# 获取价格数据
if weekly:
price_df = fetcher.get_weekly_data(stock_code, start_date, end_date)
else:
price_df = fetcher.get_technical_indicators(stock_code, start_date, end_date)
# 获取财务数据
financial_data = fetcher.get_financial_data(stock_code)
# 获取基本信息
basic_info = fetcher.get_stock_basic(stock_code)
# 合并财务信息
financial_data.update(basic_info)
return price_df, financial_data
if __name__ == '__main__':
# 测试代码
with WindDataFetcher() as fetcher:
# 示例:获取某股票的数据
code = '300866.SZ'
start = '2025-01-01'
end = '2026-02-06'
print(f"获取 {code} 的日线数据...")
df = fetcher.get_technical_indicators(code, start, end)
print(df.tail())
print(f"\n获取 {code} 的基本信息...")
info = fetcher.get_stock_basic(code)
print(info)

View File

@ -0,0 +1,377 @@
"""
Word文档格式化模块
按照GB/T 9704-2012标准格式化金融研究报告
"""
from docx import Document
from docx.shared import Pt, Mm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_LINE_SPACING
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
class GBT9704Formatter:
"""
GB/T 9704-2012党政机关公文格式化器
"""
# 字体映射(号数 -> 磅值)
FONT_SIZES = {
'三号': 16,
'四号': 14,
'小四': 12,
'五号': 10.5
}
def __init__(self, doc: Document):
"""
初始化格式化器
Args:
doc: Document对象
"""
self.doc = doc
self._setup_page()
def _setup_page(self):
"""设置页面格式"""
for section in self.doc.sections:
# 页面边距(单位:毫米)
section.top_margin = Mm(37)
section.bottom_margin = Mm(35)
section.left_margin = Mm(28)
section.right_margin = Mm(26)
def set_font(self, run, font_name: str, size: int = None, bold: bool = None, color: RGBColor = None):
"""
设置字体
Args:
run: Run对象
font_name: 字体名称中文字体
size: 字号磅值 16, 14, 12
bold: 是否加粗
color: 字体颜色
"""
run.font.name = font_name
run._element.rPr.rFonts.set(qn('w:eastAsia'), font_name)
if size is not None:
run.font.size = Pt(size)
if bold is not None:
run.font.bold = bold
if color is not None:
run.font.color.rgb = color
def add_heading(self, text: str, level: int = 1, font_name: str = '黑体', size: int = 16):
"""
添加标题
Args:
text: 标题文本
level: 标题级别1-3
font_name: 字体名称
size: 字号
Returns:
Paragraph对象
"""
if level == 1:
heading = self.doc.add_heading(text, level=1)
run = heading.runs[0]
self.set_font(run, font_name, size, bold=True)
heading.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
heading.paragraph_format.space_before = Pt(0)
heading.paragraph_format.space_after = Pt(0)
return heading
else:
# 二级、三级标题使用"一、"、"(一)"等格式
prefix_map = {1: '一、', 2: '(一)', 3: '1.'}
prefix = prefix_map.get(level, '')
paragraph = self.doc.add_paragraph(f"{prefix}{text}")
run = paragraph.runs[0]
self.set_font(run, font_name, size, bold=True)
# 段落格式
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.space_after = Pt(0)
paragraph.paragraph_format.line_spacing = Pt(25)
paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
return paragraph
def add_paragraph(self, text: str,
font_name: str = '仿宋',
size: int = 14,
first_line_indent: bool = True,
alignment: str = 'justify'):
"""
添加正文段落
Args:
text: 段落文本
font_name: 字体名称
size: 字号磅值
first_line_indent: 是否首行缩进
alignment: 对齐方式'left', 'center', 'right', 'justify'
Returns:
Paragraph对象
"""
paragraph = self.doc.add_paragraph(text)
run = paragraph.runs[0]
self.set_font(run, font_name, size)
# 段落格式
alignment_map = {
'left': WD_ALIGN_PARAGRAPH.LEFT,
'center': WD_ALIGN_PARAGRAPH.CENTER,
'right': WD_ALIGN_PARAGRAPH.RIGHT,
'justify': WD_ALIGN_PARAGRAPH.JUSTIFY
}
paragraph.paragraph_format.alignment = alignment_map.get(alignment, WD_ALIGN_PARAGRAPH.JUSTIFY)
if first_line_indent:
# 首行缩进2字符28pt
paragraph.paragraph_format.first_line_indent = Pt(28)
# 行距固定值28pt
paragraph.paragraph_format.line_spacing = Pt(28)
paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
# 段前段后距为0
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.space_after = Pt(0)
return paragraph
def add_bullet_list(self, items: List[str], font_name: str = '仿宋', size: int = 14):
"""
添加项目符号列表
Args:
items: 列表项
font_name: 字体名称
size: 字号
"""
for item in items:
paragraph = self.doc.add_paragraph(item, style='List Bullet')
run = paragraph.runs[0]
self.set_font(run, font_name, size)
paragraph.paragraph_format.line_spacing = Pt(28)
paragraph.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.space_after = Pt(0)
def add_three_line_table(self, data: List[List[str]],
headers: List[str],
font_name: str = '仿宋',
size: int = 12):
"""
添加三线表
Args:
data: 表格数据二维列表
headers: 表头
font_name: 字体名称
size: 字号
Returns:
Table对象
"""
# 创建表格
table = self.doc.add_table(rows=len(data) + 1, cols=len(headers))
self._set_three_line_table_borders(table)
# 设置表头
header_cells = table.rows[0].cells
for i, header in enumerate(headers):
cell = header_cells[i]
self._set_cell_font(cell, header, font_name, size, bold=True)
cell.paragraphs[0].paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 设置数据行
for row_idx, row_data in enumerate(data, start=1):
cells = table.rows[row_idx].cells
for i, cell_data in enumerate(row_data):
cell = cells[i]
self._set_cell_font(cell, str(cell_data), font_name, size)
cell.paragraphs[0].paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
return table
def _set_three_line_table_borders(self, table):
"""设置三线表边框"""
tblPr = table._tbl.tblPr
tblBorders = OxmlElement('w:tblBorders')
# 顶底边框 1.5pt (sz=18)
for pos in ['top', 'bottom']:
border = OxmlElement(f'w:{pos}')
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), '18')
border.set(qn('w:color'), 'auto')
tblBorders.append(border)
# 内部横线 0.75pt (sz=9)
for pos in ['insideH']:
border = OxmlElement(f'w:{pos}')
border.set(qn('w:val'), 'single')
border.set(qn('w:sz'), '9')
border.set(qn('w:color'), 'auto')
tblBorders.append(border)
# 无左右和竖线
for pos in ['left', 'right', 'insideV']:
border = OxmlElement(f'w:{pos}')
border.set(qn('w:val'), 'none')
tblBorders.append(border)
tblPr.append(tblBorders)
def _set_cell_font(self, cell, text: str, font_name: str, size: int, bold: bool = False):
"""设置单元格字体"""
cell.text = ''
run = cell.paragraphs[0].add_run(text)
self.set_font(run, font_name, size, bold=bold)
def add_page_number_footer(self):
"""添加页码页脚"""
for section in self.doc.sections:
footer = section.footer
# 添加页码段落
p = footer.paragraphs[0] if footer.paragraphs else footer.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 页码格式:— 1 —
self.add_footer_run(p, '', font_name='仿宋', size=12)
# 添加页码字段
self.add_page_number_field(p)
self.add_footer_run(p, ' ', font_name='仿宋', size=12)
self.add_footer_run(p, '', font_name='仿宋', size=12)
def add_footer_run(self, paragraph, text: str, font_name: str, size: int):
"""添加页脚run"""
run = paragraph.add_run(text)
self.set_font(run, font_name, size)
def add_page_number_field(self, paragraph):
"""添加页码字段"""
fldChar1 = OxmlElement('w:fldChar')
fldChar1.set(qn('w:fldCharType'), 'begin')
instrText = OxmlElement('w:instrText')
instrText.set(qn('xml:space'), 'preserve')
instrText.text = "PAGE"
fldChar2 = OxmlElement('w:fldChar')
fldChar2.set(qn('w:fldCharType'), 'end')
run = paragraph.add_run()
run._r.append(fldChar1)
run._r.append(instrText)
run._r.append(fldChar2)
self.set_font(run, '仿宋', 12)
def add_cover_page(self, title: str, organization: str, date_str: str):
"""
添加封面页
Args:
title: 报告标题
organization: 机构名称
date_str: 日期
"""
# 标题
paragraph = self.doc.add_paragraph()
run = paragraph.add_run(title)
self.set_font(run, '黑体', 22, bold=True)
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
paragraph.paragraph_format.space_before = Pt(100)
paragraph.paragraph_format.space_after = Pt(50)
# 机构名称
paragraph = self.doc.add_paragraph()
run = paragraph.add_run(organization)
self.set_font(run, '黑体', 16)
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
paragraph.paragraph_format.space_after = Pt(20)
# 日期
paragraph = self.doc.add_paragraph()
run = paragraph.add_run(date_str)
self.set_font(run, '黑体', 16)
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.CENTER
# 添加分页符
self.doc.add_page_break()
def add_data_source_footnote(self, text: str = "数据来源Wind资讯"):
"""
添加数据来源注脚
Args:
text: 注脚文本
"""
paragraph = self.doc.add_paragraph()
run = paragraph.add_run(text)
self.set_font(run, '仿宋', 12)
paragraph.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.RIGHT
# 便捷函数
def create_formatted_document() -> Tuple[Document, GBT9704Formatter]:
"""
创建格式化的Word文档
Returns:
(Document, GBT9704Formatter) 文档对象和格式化器
"""
doc = Document()
formatter = GBT9704Formatter(doc)
return doc, formatter
if __name__ == '__main__':
# 测试代码
doc, formatter = create_formatted_document()
# 添加封面
formatter.add_cover_page(
'某某股票投资研究报告',
'某某证券研究所',
'2026年2月6日'
)
# 添加核心提要
formatter.add_heading('核心提要', level=1)
formatter.add_bullet_list([
'该公司2025年三季度净资产收益率ROE为15.2%,盈利能力较强',
'当前市盈率TTM为25.3倍,略高于行业平均水平',
'技术面显示股价呈现震荡上行趋势,建议关注回调机会',
'给予"买入"评级目标价格35元'
])
# 添加投资评级表格
formatter.add_heading('一、投资评级', level=1)
data = [
['买入', '35.00元', '2026年12月31日', '中风险']
]
formatter.add_three_line_table(
data,
['投资建议', '目标价格', '有效期', '风险等级']
)
# 添加数据来源
formatter.add_data_source_footnote()
# 保存文档
output_path = 'test_report.docx'
doc.save(output_path)
print(f"测试报告已保存到: {output_path}")

View File

@ -0,0 +1,313 @@
"""
金融研究报告生成器
整合数据获取分析和文档生成功能一键生成完整报告
"""
import sys
import argparse
from datetime import datetime, timedelta
from pathlib import Path
from typing import Optional
# 添加脚本目录到路径
sys.path.insert(0, str(Path(__file__).parent))
from docx import Document
from docx.shared import Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from data_fetcher import WindDataFetcher, fetch_stock_report_data
from document_formatter import create_formatted_document, GBT9704Formatter
from analysis_engine import perform_comprehensive_analysis
class ReportGenerator:
"""
金融研究报告生成器
"""
def __init__(self):
self.doc = None
self.formatter = None
self.analysis_results = None
def generate_report(self,
stock_code: str,
stock_name: str = None,
organization: str = "某某证券研究所",
author: str = "",
include_cover: bool = True,
output_path: str = None) -> str:
"""
生成完整的投资研究报告
Args:
stock_code: 股票代码 '300866.SZ'
stock_name: 股票名称如不提供则自动获取
organization: 研究机构名称
author: 分析师姓名
include_cover: 是否包含封面
output_path: 输出文件路径
Returns:
生成的文件路径
"""
print(f"开始生成 {stock_code} 的投资研究报告...")
# 1. 获取数据
print("正在获取股票数据...")
try:
price_df, financial_data = fetch_stock_report_data(stock_code, days_back=90, weekly=True)
except Exception as e:
print(f"数据获取失败: {e}")
raise
# 如果未提供股票名称,从财务数据获取
if stock_name is None:
stock_name = financial_data.get('sec_name', stock_code)
print(f"获取到 {len(price_df)} 条周线数据")
# 2. 执行分析
print("正在执行投资分析...")
current_price = price_df['close'].iloc[-1]
self.analysis_results = perform_comprehensive_analysis(
price_df, financial_data, current_price
)
# 3. 创建文档
print("正在生成Word文档...")
self.doc, self.formatter = create_formatted_document()
# 4. 添加封面(可选)
if include_cover:
self._add_cover_page(stock_name, organization)
# 5. 添加免责声明
self._add_disclaimer()
# 6. 添加核心提要
self._add_core_summary(stock_name)
# 7. 添加投资评级
self._add_investment_rating(stock_name, current_price)
# 8. 添加核心观点
self._add_core_views()
# 9. 添加市场表现回顾
self._add_market_review(price_df)
# 10. 添加页脚
self.formatter.add_page_number_footer()
# 11. 保存文档
if output_path is None:
date_str = datetime.now().strftime('%Y%m%d')
output_path = f"{stock_name}_{stock_code.replace('.', '_')}_研究报告_{date_str}.docx"
self.doc.save(output_path)
print(f"报告已保存: {output_path}")
return output_path
def _add_cover_page(self, stock_name: str, organization: str):
"""添加封面页"""
date_str = datetime.now().strftime('%Y年%m月%d')
self.formatter.add_cover_page(
title=f"{stock_name}投资研究报告",
organization=organization,
date_str=date_str
)
def _add_disclaimer(self):
"""添加免责声明"""
self.formatter.add_heading('免责声明', level=1)
self.formatter.add_paragraph(
"本报告所载资料的来源及观点皆为公开信息,但并不能保证其准确性和完整性。本报告仅供参考,"
"不构成任何投资建议或承诺,投资者应审慎决策,独立判断,自行承担投资风险。"
)
self.formatter.add_paragraph(
"本报告版权归本公司所有,未经书面许可,任何机构和个人不得以任何形式翻版、复制、刊登、"
"发表或引用。如征得本公司同意进行引用、刊发的,需在允许的范围内使用,并注明出处为"
"'某某证券研究所',且不得对本报告进行任何有悖原意的引用、删节和修改。"
)
def _add_core_summary(self, stock_name: str):
"""添加核心提要"""
self.formatter.add_heading('核心提要', level=1)
summary_points = self.analysis_results['summary_points']
for i, point in enumerate(summary_points, 1):
# 添加项目符号
paragraph = self.doc.add_paragraph()
run = paragraph.add_run(f"{i}. {point}")
self.formatter.set_font(run, '仿宋', 14)
paragraph.paragraph_format.line_spacing = Pt(28)
paragraph.paragraph_format.line_spacing_rule = 2
paragraph.paragraph_format.space_before = Pt(0)
paragraph.paragraph_format.space_after = Pt(0)
def _add_investment_rating(self, stock_name: str, current_price: float):
"""添加投资评级"""
self.formatter.add_heading('一、投资评级', level=1)
# 表格数据
recommendation = self.analysis_results['recommendation']
data = [[
recommendation.rating.value,
f"{recommendation.target_price:.2f}",
(datetime.now() + timedelta(days=365)).strftime('%Y年%m月%d'),
recommendation.risk_level.value
]]
self.formatter.add_three_line_table(
data,
['投资建议', '目标价格', '有效期', '风险等级'],
font_name='仿宋',
size=12
)
# 添加说明文字
self.formatter.add_paragraph(
f"当前股价:{current_price:.2f}元。"
f"目标价较当前价格{'上涨' if recommendation.target_price > current_price else '下跌'}"
f"{abs((recommendation.target_price / current_price - 1) * 100):.2f}%。"
)
def _add_core_views(self):
"""添加核心观点"""
self.formatter.add_heading('二、核心观点', level=1)
# 基本面分析
fundamental = self.analysis_results['fundamental']
self.formatter.add_heading('(一)基本面分析', level=2)
self.formatter.add_paragraph(
f"1. 盈利能力公司最新净资产收益率ROE{fundamental.roe}%"
f"{fundamental.roe_comment}"
)
self.formatter.add_paragraph(
f"2. 估值水平当前市盈率TTM{fundamental.pe_ttm}倍,"
f"市净率LF{fundamental.pb_lf}倍。{fundamental.valuation_comment}"
)
self.formatter.add_paragraph(f"3. 结论:{fundamental.fundamental_summary}")
# 技术面分析
technical = self.analysis_results['technical']
self.formatter.add_heading('(二)技术面分析', level=2)
self.formatter.add_paragraph(
f"1. 价格趋势:近期股价呈现{technical.trend}态势,"
f"{technical.ma_status}"
)
self.formatter.add_paragraph(
f"2. 支撑与阻力:近期支撑位{technical.support_level}元,"
f"阻力位{technical.resistance_level}元。"
)
self.formatter.add_paragraph(
f"3. 波动率:近期年化波动率为{technical.volatility}%"
f"{'属于高波动品种' if technical.volatility > 40 else '波动适中' if technical.volatility > 25 else '波动较低'}"
)
# 投资建议
recommendation = self.analysis_results['recommendation']
self.formatter.add_heading('(三)投资建议', level=2)
self.formatter.add_paragraph(recommendation.recommendation_text)
self.formatter.add_paragraph("主要投资逻辑:")
for factor in recommendation.key_factors:
paragraph = self.doc.add_paragraph()
run = paragraph.add_run(f"{factor}")
self.formatter.set_font(run, '仿宋', 14)
paragraph.paragraph_format.line_spacing = Pt(28)
paragraph.paragraph_format.line_spacing_rule = 2
paragraph.paragraph_format.first_line_indent = Pt(28)
def _add_market_review(self, price_df):
"""添加市场表现回顾"""
self.formatter.add_heading('三、市场表现回顾', level=1)
# 价格走势分析
self.formatter.add_heading('(一)价格走势分析', level=2)
price_stats = self.analysis_results['price_stats']
self.formatter.add_paragraph(
f"近期股价最高达到{price_stats['highest_price']}元,"
f"最低下探至{price_stats['lowest_price']}元,"
f"区间累计涨跌幅为{price_stats['total_change']:.2f}%。"
f"最新收盘价为{price_stats['latest_price']}元,"
f"区间平均价格为{price_stats['avg_price']}元。"
)
# K线数据表
self.formatter.add_heading('K线数据', level=2)
# 准备表格数据最近13周
recent_data = price_df.tail(13).copy()
table_data = []
for _, row in recent_data.iterrows():
date_str = row['date'].strftime('%Y-%m-%d') if hasattr(row['date'], 'strftime') else str(row['date'])[:10]
table_data.append([
date_str,
f"{row['open']:.2f}",
f"{row['high']:.2f}",
f"{row['low']:.2f}",
f"{row['close']:.2f}",
f"{row.get('pct_chg', 0):.2f}%",
f"{row.get('volume', 0) / 10000:.0f}"
])
self.formatter.add_three_line_table(
table_data,
['日期', '开盘价', '最高价', '最低价', '收盘价', '涨跌幅', '成交量(万股)'],
font_name='仿宋',
size=12
)
# 数据来源
self.formatter.add_data_source_footnote()
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='生成金融投资研究报告')
parser.add_argument('stock_code', help='股票代码,如 300866.SZ 或 000001')
parser.add_argument('--name', '-n', help='股票名称')
parser.add_argument('--org', '-o', default='某某证券研究所', help='研究机构名称')
parser.add_argument('--author', '-a', default='', help='分析师姓名')
parser.add_argument('--no-cover', action='store_true', help='不包含封面')
parser.add_argument('--output', '-p', help='输出文件路径')
args = parser.parse_args()
# 标准化股票代码
stock_code = args.stock_code
if '.' not in stock_code:
if stock_code.startswith('6'):
stock_code = f"{stock_code}.SH"
else:
stock_code = f"{stock_code}.SZ"
generator = ReportGenerator()
try:
output_path = generator.generate_report(
stock_code=stock_code,
stock_name=args.name,
organization=args.org,
author=args.author,
include_cover=not args.no_cover,
output_path=args.output
)
print(f"\n报告生成成功: {output_path}")
except Exception as e:
print(f"\n报告生成失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()

133
find-skills/SKILL.md Normal file
View File

@ -0,0 +1,133 @@
---
name: find-skills
description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
---
# Find Skills
This skill helps you discover and install skills from the open agent skills ecosystem.
## When to Use This Skill
Use this skill when the user:
- Asks "how do I do X" where X might be a common task with an existing skill
- Says "find a skill for X" or "is there a skill for X"
- Asks "can you do X" where X is a specialized capability
- Expresses interest in extending agent capabilities
- Wants to search for tools, templates, or workflows
- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
## What is the Skills CLI?
The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
**Key commands:**
- `npx skills find [query]` - Search for skills interactively or by keyword
- `npx skills add <package>` - Install a skill from GitHub or other sources
- `npx skills check` - Check for skill updates
- `npx skills update` - Update all installed skills
**Browse skills at:** https://skills.sh/
## How to Help Users Find Skills
### Step 1: Understand What They Need
When a user asks for help with something, identify:
1. The domain (e.g., React, testing, design, deployment)
2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
3. Whether this is a common enough task that a skill likely exists
### Step 2: Search for Skills
Run the find command with a relevant query:
```bash
npx skills find [query]
```
For example:
- User asks "how do I make my React app faster?" → `npx skills find react performance`
- User asks "can you help me with PR reviews?" → `npx skills find pr review`
- User asks "I need to create a changelog" → `npx skills find changelog`
The command will return results like:
```
Install with npx skills add <owner/repo@skill>
vercel-labs/agent-skills@vercel-react-best-practices
└ https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
```
### Step 3: Present Options to the User
When you find relevant skills, present them to the user with:
1. The skill name and what it does
2. The install command they can run
3. A link to learn more at skills.sh
Example response:
```
I found a skill that might help! The "vercel-react-best-practices" skill provides
React and Next.js performance optimization guidelines from Vercel Engineering.
To install it:
npx skills add vercel-labs/agent-skills@vercel-react-best-practices
Learn more: https://skills.sh/vercel-labs/agent-skills/vercel-react-best-practices
```
### Step 4: Offer to Install
If the user wants to proceed, you can install the skill for them:
```bash
npx skills add <owner/repo@skill> -g -y
```
The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
## Common Skill Categories
When searching, consider these common categories:
| Category | Example Queries |
| --------------- | ---------------------------------------- |
| Web Development | react, nextjs, typescript, css, tailwind |
| Testing | testing, jest, playwright, e2e |
| DevOps | deploy, docker, kubernetes, ci-cd |
| Documentation | docs, readme, changelog, api-docs |
| Code Quality | review, lint, refactor, best-practices |
| Design | ui, ux, design-system, accessibility |
| Productivity | workflow, automation, git |
## Tips for Effective Searches
1. **Use specific keywords**: "react testing" is better than just "testing"
2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
## When No Skills Are Found
If no relevant skills exist:
1. Acknowledge that no existing skill was found
2. Offer to help with the task directly using your general capabilities
3. Suggest the user could create their own skill with `npx skills init`
Example:
```
I searched for skills related to "xyz" but didn't find any matches.
I can still help you with this task directly! Would you like me to proceed?
If this is something you do often, you could create your own skill:
npx skills init my-xyz-skill
```

22
fund-slides/LICENSE Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 Zara Zhang (original author)
Copyright (c) 2026 hanlinlibham (fork with financial enhancements)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

120
fund-slides/README.md Normal file
View File

@ -0,0 +1,120 @@
# Fund Slides
> Forked from [frontend-slides](https://github.com/zarazhangrui/frontend-slides) by [@zarazhangrui](https://github.com/zarazhangrui) (Zara Zhang).
> Enhanced by [@hanlinlibham](https://github.com/hanlinlibham) with financial chart integration and offline optimizations.
A Claude Code skill for creating animation-rich HTML presentations from any content — with first-class support for financial data visualization.
## What This Does
**Fund Slides** helps non-designers create beautiful web presentations without knowing CSS or JavaScript. It uses a "show, don't tell" approach: instead of asking you to describe your aesthetic preferences in words, it generates visual previews and lets you pick what you like.
Here is a deck about the original skill, made through the skill:
https://github.com/user-attachments/assets/ef57333e-f879-432a-afb9-180388982478
### Key Features
- **Zero Dependencies** — Single HTML files with inline CSS/JS. No npm, no build tools, no frameworks.
- **Offline-Ready** — ECharts library (~1MB) is inlined directly into the HTML. No CDN, no network required.
- **Visual Style Discovery** — Can't articulate design preferences? No problem. Pick from generated visual previews.
- **PPT Conversion** — Convert existing PowerPoint files to web, preserving all images and content.
- **Anti-AI-Slop** — Curated distinctive styles that avoid generic AI aesthetics.
- **Production Quality** — Accessible, responsive, well-commented code you can customize.
### What's New in This Fork
Compared to the original `frontend-slides`:
- **ECharts Inline Integration** — ECharts v5 library pre-bundled in `references/echarts.min.js`, inlined at generation time for fully offline HTML output. The original used CDN `<script src="...">` which requires network access.
- **Financial Chart Library** — Added specialized chart references for financial use cases:
- `charts-trend.md` — Line/area/bar charts for time series
- `charts-composition.md` — Pie/rose/treemap/sunburst for composition analysis
- `charts-matrix.md` — Heatmap/radar/scatter for multi-dimensional comparison
- `charts-flow.md` — Waterfall/candlestick/funnel/sankey for flow analysis
- **A-Share Color Convention** — Red-up/green-down (China stock market standard) built into the base template
- **Financial Layouts** — KPI cards, data tables, timelines, comparison cards optimized for investment presentations
- **Content Mapping System** — Intelligent content-to-visual mapping that automatically selects optimal chart types based on data characteristics
- **Safe Base Template** — Defensive HTML template with built-in viewport fitting, CJK typography, responsive breakpoints, and Chinese number formatting (`formatCN`)
- **15 Visual Presets** — Expanded from 12 to 15 curated styles including `Research Formal`, `Data Dashboard`, and `Roadshow Elegance`
- **Self-Check Checklist** — Phase 3.5 automated quality gate ensuring zero external dependencies, proper `clamp()` usage, and A-share color compliance
## Installation
```bash
git clone https://github.com/hanlinlibham/skills.git ~/.claude/skills
```
Then use it by typing `/fund-slides` in Claude Code.
## Usage
### Create a New Presentation
```
/fund-slides
> "Create a fund analysis presentation for XX Fund"
```
The skill will:
1. Ask about your content, length, chart needs, and editing preference
2. Analyze content and map each block to optimal visual form
3. Generate 3 visual style previews for you to compare
4. Create the full presentation with charts inlined
5. Open it in your browser — works offline
### Convert a PowerPoint
```
/fund-slides
> "Convert my presentation.pptx to a web slideshow"
```
## Included Styles
### Dark Themes
- **Bold Signal** — Confident, high-impact, vibrant card on dark
- **Electric Studio** — Clean, professional, split-panel
- **Creative Voltage** — Energetic, retro-modern, electric blue + neon
- **Dark Botanical** — Elegant, sophisticated, warm accents
### Light Themes
- **Notebook Tabs** — Editorial, organized, paper with colorful tabs
- **Pastel Geometry** — Friendly, approachable, vertical pills
- **Split Pastel** — Playful, modern, two-color vertical split
- **Vintage Editorial** — Witty, personality-driven, geometric shapes
### Financial / Specialty
- **Research Formal** — Institutional, trustworthy, data-heavy layouts
- **Data Dashboard** — Clean metrics, KPI cards, chart-forward
- **Roadshow Elegance** — Premium, investor-facing, bold confidence
- **Swiss Modern** — Minimal, Bauhaus-inspired, geometric
- **Paper & Ink** — Literary, drop caps, pull quotes
- **Neon Cyber** — Futuristic, particle backgrounds, neon glow
- **Terminal Green** — Developer-focused, hacker aesthetic
## Architecture
| File | Purpose | Loaded When |
|------|---------|-------------|
| `SKILL.md` | Core workflow and rules | Always (skill invocation) |
| `content-mapping.md` | Content type to visual form mapping | Phase 1 (content analysis) |
| `STYLE_PRESETS.md` | 15 curated visual presets | Phase 2 (style selection) |
| `references/safe-base-template.html` | Defensive base HTML template | Phase 3 (generation) |
| `references/echarts.min.js` | ECharts v5 library (~1MB) | Phase 3 (inline into HTML) |
| `references/charts-*.md` | Chart type configurations | Phase 3 (per chart type) |
| `references/financial-layouts.md` | KPI/table/timeline layouts | Phase 3 (financial slides) |
| `references/animation-patterns.md` | CSS/JS animation reference | Phase 3 (generation) |
| `references/html-template.md` | HTML structure and JS features | Phase 3 (generation) |
| `scripts/extract-pptx.py` | PPT content extraction | Phase 4 (conversion) |
## Credits
- **Original author**: [Zara Zhang](https://github.com/zarazhangrui) — created [frontend-slides](https://github.com/zarazhangrui/frontend-slides) with Claude Code
- **Fork maintainer**: [hanlinlibham](https://github.com/hanlinlibham) — financial chart integration, offline ECharts inlining, A-share conventions, and expanded presets
## License
MIT — See [LICENSE](LICENSE) for details.

307
fund-slides/SKILL.md Normal file
View File

@ -0,0 +1,307 @@
---
name: fund-slides
description: Create animation-rich HTML presentations from any content. Provides intelligent content-to-visual mapping -- given information of any type, selects the optimal slide layout, chart, and animation. Zero-dependency single HTML files with Chinese font support and ECharts inline integration. Use when the user wants to build a presentation, convert a PPT/PPTX to web, or create slides.
---
# Frontend Slides
将任意内容转化为零依赖、动画丰富的 HTML 演示文稿。
本技能不关心内容从哪来、用什么分析框架 -- 它只做一件事:**给定一块信息,选择最佳的视觉呈现方式。**
## Core Principles
1. **Content-Agnostic** -- 技能不决定"展示什么",只决定"怎么展示"。研究逻辑由用户或上游技能负责。
2. **Zero Dependencies** -- 单 HTML 文件CSS/JS 全部内联。需要图表时ECharts 库直接内嵌在 HTML 的 `<script>` 标签中(通过 WebFetch 下载 echarts.min.js 后内联),确保离线可用。
3. **Show, Don't Tell** -- 生成可视化预览让用户选择风格,而非要求用户用语言描述偏好。
4. **Viewport Fitting (NON-NEGOTIABLE)** -- 每张 slide 精确适配 100vh。禁止滚动。内容超限时拆分。
5. **No Emoji** -- 禁止任何 emoji。用 CSS 形状、SVG 图标或文字替代。
6. **Chinese Font Required** -- 每份演示文稿必须包含中文字体回退链。
## Design Aesthetics
杜绝"AI 味"。每份演示文稿都应有定制感。
- 字体:选择有辨识度的字体,避免 Arial/Inter/Roboto。所有字号用 `clamp()`
- 配色:主色 + 锐利强调色避免均匀分布。CSS 变量保持一致。
- 动画:聚焦高影响力时刻 -- 入场交错显现优于零散微交互。
- 背景:渐变/图案/纹理营造层次,而非纯色。
## Viewport Fitting Rules
适用于每张 slide 的不变量:
- `.slide`: `height: 100vh; height: 100dvh; overflow: hidden;`
- 所有字号: `clamp(min, preferred, max)` -- 禁止固定 px/rem
- 所有间距: `clamp()` 或视口单位
- 图片: `max-height: min(50vh, 400px)`
- 断点: 700px, 600px, 500px (height), 600px (width)
- `prefers-reduced-motion` 支持
- CSS 函数取反用 `calc(-1 * clamp(...))` -- 禁止 `-clamp()`
---
## Phase 0: Detect Mode
- **Mode A: 新建** -- 用户提供内容(文本/数据/图片),从零创建。进入 Phase 1。
- **Mode B: PPT 转换** -- 转换 .pptx 文件。进入 Phase 4。
- **Mode C: 增强** -- 改进现有 HTML 演示文稿。遵循 Mode C 规则。
### Mode C: 修改规则
1. 添加内容前统计现有元素,对照密度限制
2. 任何修改后验证: `overflow: hidden`、`clamp()` 字号、图片 max-height
3. 将要溢出时主动拆分并告知用户
---
## Phase 1: Content Discovery
**目标:理解用户有什么内容,而非告诉用户应该有什么内容。**
通过一次 AskUserQuestion 收集信息:
**问题 1 -- 内容来源** (header: "内容"):
你的内容是什么形式?选项:
- "内容已就绪" -- 有完整的文字/数据/图表素材
- "有粗略笔记" -- 有要点但未整理
- "仅有主题" -- 只有主题,需要协助组织结构
**问题 2 -- 长度** (header: "长度"):
大约多少页?选项:简短 5-10 / 中等 10-20 / 较长 20+
**问题 3 -- 图表需求** (header: "图表"):
是否需要数据图表?选项:
- "ECharts (推荐)" -- 折线/柱状/饼图/K线/瀑布图,内嵌到 HTML 中,离线可用
- "简单 SVG" -- 更轻量,适合简单图表
- "不需要"
**问题 4 -- 在线编辑** (header: "编辑"):
是否需要浏览器内编辑文字?选项:是(推荐) / 否
然后请用户提供内容。
### Step 1.2: Content Analysis
**用户提供内容后,读取 [content-mapping.md](content-mapping.md) 进行内容分析:**
1. **识别** -- 将用户内容拆解为独立信息块
2. **分类** -- 对每块内容标注信息类型(数值指标 / 时序趋势 / 结构构成 / 对比关系 / 叙事论点 / 时间序列 / 风险评估)
3. **映射** -- 为每块内容匹配最佳视觉形式KPI卡片 / 图表 / 表格 / 分栏 / 列表 / 时间线 / 对比卡片)
4. **排序** -- 组织 slide 顺序(概览 -> 核心内容 -> 结论)
5. **呈现给用户确认** -- 展示映射结果表格
示例输出:
```
Slide 大纲:
| # | 内容 | 信息类型 | 视觉形式 |
|---|------|---------|---------|
| 1 | 标题与主题 | -- | 标题页 |
| 2 | 6 个核心财务指标 | 少量关键指标 | KPI 卡片网格 |
| 3 | 2019-2024 营收趋势 | 时间序列 | 柱线混合图 |
| 4 | 收入构成 + 解读 | 构成 + 论点 | 图文分栏 (饼图+要点) |
| 5 | 三条核心优势 | 论点+支撑 | 标题+要点列表 |
| ... | ... | ... | ... |
```
通过 AskUserQuestion 确认 (header: "大纲"): "这个 slide 大纲是否合适?" 选项:合适 / 调整
### Step 1.4: 状态暂存(防止跨 Phase 遗忘)
确认大纲后,**必须**将分析结果写入 `.slide-plan.md`,格式如下:
```markdown
# Slide Plan
- 预设: [所选预设名称]
- 图表方案: [ECharts / SVG / 无]
- 编辑模式: [是 / 否]
- 需读取的 references: [列出 Phase 3 需要的文件]
| # | 内容摘要 | 类型 | 视觉形式 | 需要的 CSS class |
|---|---------|------|---------|----------------|
| 1 | ... | ... | ... | ... |
```
Phase 3 生成前**必须先读取此文件**,确保不会遗忘 Phase 1 的分析结果。
### Step 1.3: Image Evaluation (如有)
如果用户提供了图片:扫描 -> 查看 -> 评估可用性 -> 融入大纲。
---
## Phase 2: Style Discovery
**"看效果选风格"环节。**
### Step 2.0: 风格路径
询问 (header: "风格"):
- "给我看几个选项"(推荐)-- 基于感受生成 3 个预览
- "我知道我想要什么" -- 直接从预设列表选择
### Step 2.1: 感受选择
询问 (header: "感受", multiSelect: true, max 2):
- 专业/可信 -- 机构感、值得信赖
- 自信/有力 -- 创新、大胆
- 沉稳/专注 -- 清晰、深思熟虑
- 高端/精致 -- 优雅、令人难忘
### Step 2.2: 生成 3 个风格预览
读取 [STYLE_PRESETS.md](STYLE_PRESETS.md) 获取 15 个可用预设。根据感受从中选择 3 个差异化预设生成单页预览。
| 感受 | 建议预设(按适配度排序) |
|------|------------------------|
| 专业/可信 | Research Formal, Swiss Modern, Notebook Tabs, Data Dashboard |
| 自信/有力 | Roadshow Elegance, Bold Signal, Electric Studio |
| 沉稳/专注 | Paper & Ink, Data Dashboard, Swiss Modern |
| 高端/精致 | Roadshow Elegance, Dark Botanical, Vintage Editorial |
保存预览到 `.claude-design/slide-previews/`,自动打开。
### Step 2.3: 用户选择
询问 (header: "风格选择"): 哪个风格选项A / B / C / 混合
---
## Phase 3: Generate Presentation
### Step 3.0: 读取状态 + 防护模板
1. **读取 `.slide-plan.md`** -- 恢复 Phase 1 的分析结果(类型、视觉形式、需要的文件)
2. **复制 [references/safe-base-template.html](references/safe-base-template.html) 作为起点** -- 此模板已内置 viewport-base.css、中文字体、A 股配色、formatCN、SlidePresentation 控制器。在此基础上添加内容,**不要删除模板中的任何 CSS 变量或 JS 函数**。
### Step 3.1: 按需读取参考文件
根据 `.slide-plan.md` 中列出的 references 读取:
1. **[STYLE_PRESETS.md](STYLE_PRESETS.md)** -- 获取所选风格的配色、字体、签名元素,覆盖模板中的 SLOT: THEME 变量
2. **(按需)** [references/charts-base.md](references/charts-base.md) + 对应的 charts-trend/composition/matrix/flow -- 按 `.slide-plan.md` 列出的图表类型读取
3. **(按需)** [references/financial-layouts.md](references/financial-layouts.md) -- 如有 KPI/表格/时间线/对比等布局
4. **(按需)** [references/animation-patterns.md](references/animation-patterns.md) -- 高级动画效果
5. **(按需)** [references/html-template.md](references/html-template.md) -- 编辑功能等 JS 参考
### Step 3.1.5: ECharts 内联(如需图表)
如果 `.slide-plan.md` 中图表方案为 ECharts**必须**将 ECharts 库内嵌到 HTML 中:
1. 读取本地文件 [references/echarts.min.js](references/echarts.min.js)~1MB已预存在 skill 目录中)
2. 将完整内容包裹在 `<script>/* ECharts v5 */...内容...</script>`
3. 放入模板的 `<!-- SLOT: ECHARTS INLINE -->` 位置
4. **禁止使用 `<script src="...">` 外部引用** -- 最终 HTML 必须离线可用
### Step 3.2: 生成 slide 内容
在 safe-base-template.html 的 `<!-- SLOT: SLIDES -->` 位置添加 slide。每张 slide 的视觉形式严格按 `.slide-plan.md` 执行。
**slide 结构由 Phase 1 的 content-mapping 结果驱动,而非固定模板。每份演示文稿的页面组合都应该不同。**
---
## Phase 3.5: 代码完整性自检(生成后必须执行)
输出最终 HTML 前,逐项检查以下清单。任何一项失败,修复后再继续。
- [ ] `<html lang="zh-CN">` 已设置
- [ ] safe-base-template 的核心 CSS 完整保留(搜索 `scroll-snap-type` 确认存在)
- [ ] 所有 `font-size:` 使用 `clamp()`(搜索 `font-size:`,确认无固定 px/rem 值)
- [ ] `formatCN` 函数已内嵌(如有数字数据)
- [ ] 无 emoji 字符(涨跌用 `.trend-up` / `.trend-down` CSS 类)
- [ ] 每个 `.slide` 继承了 `overflow: hidden`(来自 safe-base-template
- [ ] 中文字体已加载Google Fonts link 包含 `Noto+Sans+SC``Noto+Serif+SC`
- [ ] `--color-positive: #dc2626``--color-negative: #16a34a` 存在A 股默认)
- [ ] **零外部依赖**:无 `<script src="...">` 外部引用(如有 ECharts确认是内联 `<script>` 而非 CDN
**如有任何项失败,修复后再继续。不要跳过自检。**
---
## Phase 4: PPT Conversion
1. 运行 `python scripts/extract-pptx.py <input.pptx> <output_dir>`
2. 展示提取结果,与用户确认
3. 进入 Phase 2 选择风格
4. 生成 HTML保留原始内容和图片
---
## Phase 5: Delivery
1. 删除 `.claude-design/slide-previews/`
2. `open [filename].html`
3. 告知:文件位置、风格、页数、导航方式、自定义方法
4. **如需通过飞书发送给用户**,使用 message 工具:
```javascript
message({
action: "send",
channel: "feishu",
path: "/home/core/.openclaw/workspace/output/presentation.html",
filename: "presentation.html",
caption: "基金分析演示文稿"
})
```
---
## Fallback: 降级模式
当内容超过 15 页、或包含 3 种以上图表类型、或模型多次生成失败时,切换到简化路径:
**布局限制为 3 种:**
- KPI 卡片网格(指标页)
- 图文分栏(解读页)
- 标题 + 要点列表(论述页)
**风格固定为 Data Dashboard** 最简洁的预设,最低出错概率。
**功能限制:**
- 不使用 inline editing
- 动画只用 fade-in`.reveal` 类)
- 图表只用柱状图和折线图(不用旭日图/桑基图等复杂类型)
- 不使用自定义导航sidebar/nav-dots
**分步生成:** 先生成前 3 页预览,确认正确后再生成剩余页面。避免一次生成全部后发现基础层有错。
---
## File Map
```
fund-slides/
SKILL.md ← 入口(始终加载)
content-mapping.md ← 信息类型→视觉形式映射 + 强制检查清单
STYLE_PRESETS.md ← 15 个视觉预设
references/
safe-base-template.html ← 防护性基础模板Phase 3 起点,内置全部安全层)
echarts.min.js ← ECharts v5 库(~1MB生成时内联到 HTML
viewport-base.css ← 响应式 CSS + CJK + 全局 A 股配色 + 趋势箭头
animation-patterns.md ← 动画模式
charts-base.md ← ECharts 基础设施 + 速查表 + SVG
charts-trend.md ← 折线/面积/柱状图
charts-composition.md ← 饼图/玫瑰/Treemap/旭日图
charts-matrix.md ← 热力图/雷达/散点图
charts-flow.md ← 瀑布/K线/漏斗/桑基图
financial-layouts.md ← KPI/表格/时间线/对比布局
html-template.md ← HTML 结构 + JS 功能
scripts/
extract-pptx.py ← PPT 内容提取Phase 4
```
| File | When to Read |
|------|-------------|
| content-mapping.md | **MANDATORY** Phase 1.2(强制检查清单) |
| STYLE_PRESETS.md | **MANDATORY** Phase 2 + Phase 3.1 |
| references/safe-base-template.html | **MANDATORY** Phase 3.0(复制为起点) |
| references/charts-base.md | Phase 3.1 -- 含图表时(基础设施 + 速查表) |
| references/charts-trend.md | Phase 3.1 -- 含折线/面积/柱状图时 |
| references/charts-composition.md | Phase 3.1 -- 含饼图/玫瑰/Treemap/旭日图时 |
| references/charts-matrix.md | Phase 3.1 -- 含热力图/雷达/散点图时 |
| references/charts-flow.md | Phase 3.1 -- 含瀑布/K线/漏斗/桑基图时 |
| references/financial-layouts.md | Phase 3.1 -- 含 KPI/表格/时间线/对比时 |
| references/animation-patterns.md | Phase 3.1 -- 高级动画时 |
| references/html-template.md | Phase 3.1 -- 编辑功能等 JS 参考 |

View File

@ -0,0 +1,559 @@
# Style Presets Reference
Curated visual styles for Frontend Slides. Each preset is inspired by real design references — no generic "AI slop" aesthetics. **Abstract shapes only — no illustrations.**
**Viewport CSS:** For mandatory base styles, see [references/viewport-base.css](references/viewport-base.css). Include in every presentation.
---
## Chinese Font Strategy
Every preset must include Chinese font fallback. Load via Google Fonts alongside Latin fonts.
**Standard Chinese Font Stack:**
| Category | Font | Weight | Use Case |
|----------|------|--------|----------|
| Sans | Noto Sans SC | 300-700 | Body text, UI, data labels |
| Serif | Noto Serif SC | 400-700 | Display headings, editorial |
| Literary | LXGW WenKai | 400-700 | Drop caps, quotes, literary feel |
**Loading template:**
```html
<!-- Add Chinese font families to the Google Fonts link -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Noto+Serif+SC:wght@400;600;700&family=LXGW+WenKai:wght@400;700&display=swap" rel="stylesheet">
```
**Font-family pairing rules:**
- Sans-serif preset display/body: append `, 'Noto Sans SC', sans-serif`
- Serif preset display: append `, 'Noto Serif SC', serif`
- Serif preset body: append `, 'Noto Sans SC', sans-serif`
- Mono preset: append `, 'Noto Sans SC', sans-serif` as final fallback
- Literary/editorial preset: can use `'LXGW WenKai'` for display, `'Noto Sans SC'` for body
Example:
```css
:root {
--font-display: 'Archivo Black', 'Noto Sans SC', sans-serif;
--font-body: 'Space Grotesk', 'Noto Sans SC', sans-serif;
}
```
---
## Dark Themes
### 1. Bold Signal
**Vibe:** Confident, bold, modern, high-impact
**Layout:** Colored card on dark gradient. Number top-left, navigation top-right, title bottom-left.
**Typography:**
- Display: `Archivo Black`, `Noto Sans SC` (900)
- Body: `Space Grotesk`, `Noto Sans SC` (400/500)
**Colors:**
```css
:root {
--bg-primary: #1a1a1a;
--bg-gradient: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 50%, #1a1a1a 100%);
--card-bg: #FF5722;
--text-primary: #ffffff;
--text-on-card: #1a1a1a;
--font-display: 'Archivo Black', 'Noto Sans SC', sans-serif;
--font-body: 'Space Grotesk', 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Bold colored card as focal point (orange, coral, or vibrant accent)
- Large section numbers (01, 02, etc.)
- Navigation breadcrumbs with active/inactive opacity states
- Grid-based layout for precise alignment
---
### 2. Electric Studio
**Vibe:** Bold, clean, professional, high contrast
**Layout:** Split panel—white top, blue bottom. Brand marks in corners.
**Typography:**
- Display: `Manrope`, `Noto Sans SC` (800)
- Body: `Manrope`, `Noto Sans SC` (400/500)
**Colors:**
```css
:root {
--bg-dark: #0a0a0a;
--bg-white: #ffffff;
--accent-blue: #4361ee;
--text-dark: #0a0a0a;
--text-light: #ffffff;
--font-display: 'Manrope', 'Noto Sans SC', sans-serif;
--font-body: 'Manrope', 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Two-panel vertical split
- Accent bar on panel edge
- Quote typography as hero element
- Minimal, confident spacing
---
### 3. Creative Voltage
**Vibe:** Bold, creative, energetic, retro-modern
**Layout:** Split panels—electric blue left, dark right. Script accents.
**Typography:**
- Display: `Syne`, `Noto Sans SC` (700/800)
- Mono: `Space Mono`, `Noto Sans SC` (400/700)
**Colors:**
```css
:root {
--bg-primary: #0066ff;
--bg-dark: #1a1a2e;
--accent-neon: #d4ff00;
--text-light: #ffffff;
--font-display: 'Syne', 'Noto Sans SC', sans-serif;
--font-body: 'Space Mono', 'Noto Sans SC', monospace;
}
```
**Signature Elements:**
- Electric blue + neon yellow contrast
- Halftone texture patterns
- Neon badges/callouts
- Script typography for creative flair
---
### 4. Dark Botanical
**Vibe:** Elegant, sophisticated, artistic, premium
**Layout:** Centered content on dark. Abstract soft shapes in corner.
**Typography:**
- Display: `Cormorant`, `Noto Serif SC` (400/600) — elegant serif
- Body: `IBM Plex Sans`, `Noto Sans SC` (300/400)
**Colors:**
```css
:root {
--bg-primary: #0f0f0f;
--text-primary: #e8e4df;
--text-secondary: #9a9590;
--accent-warm: #d4a574;
--accent-pink: #e8b4b8;
--accent-gold: #c9b896;
--font-display: 'Cormorant', 'Noto Serif SC', serif;
--font-body: 'IBM Plex Sans', 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Abstract soft gradient circles (blurred, overlapping)
- Warm color accents (pink, gold, terracotta)
- Thin vertical accent lines
- Italic signature typography
- **No illustrations—only abstract CSS shapes**
---
## Light Themes
### 5. Notebook Tabs
**Vibe:** Editorial, organized, elegant, tactile
**Layout:** Cream paper card on dark background. Colorful tabs on right edge.
**Typography:**
- Display: `Bodoni Moda`, `Noto Serif SC` (400/700) — classic editorial
- Body: `DM Sans`, `Noto Sans SC` (400/500)
**Colors:**
```css
:root {
--bg-outer: #2d2d2d;
--bg-page: #f8f6f1;
--text-primary: #1a1a1a;
--tab-1: #98d4bb; /* Mint */
--tab-2: #c7b8ea; /* Lavender */
--tab-3: #f4b8c5; /* Pink */
--tab-4: #a8d8ea; /* Sky */
--tab-5: #ffe6a7; /* Cream */
--font-display: 'Bodoni Moda', 'Noto Serif SC', serif;
--font-body: 'DM Sans', 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Paper container with subtle shadow
- Colorful section tabs on right edge (vertical text)
- Binder hole decorations on left
- Tab text must scale with viewport: `font-size: clamp(0.5rem, 1vh, 0.7rem)`
---
### 6. Pastel Geometry
**Vibe:** Friendly, organized, modern, approachable
**Layout:** White card on pastel background. Vertical pills on right edge.
**Typography:**
- Display: `Plus Jakarta Sans`, `Noto Sans SC` (700/800)
- Body: `Plus Jakarta Sans`, `Noto Sans SC` (400/500)
**Colors:**
```css
:root {
--bg-primary: #c8d9e6;
--card-bg: #faf9f7;
--pill-pink: #f0b4d4;
--pill-mint: #a8d4c4;
--pill-sage: #5a7c6a;
--pill-lavender: #9b8dc4;
--pill-violet: #7c6aad;
--font-display: 'Plus Jakarta Sans', 'Noto Sans SC', sans-serif;
--font-body: 'Plus Jakarta Sans', 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Rounded card with soft shadow
- **Vertical pills on right edge** with varying heights (like tabs)
- Consistent pill width, heights: short → medium → tall → medium → short
- Download/action icon in corner
---
### 7. Split Pastel
**Vibe:** Playful, modern, friendly, creative
**Layout:** Two-color vertical split (peach left, lavender right).
**Typography:**
- Display: `Outfit`, `Noto Sans SC` (700/800)
- Body: `Outfit`, `Noto Sans SC` (400/500)
**Colors:**
```css
:root {
--bg-peach: #f5e6dc;
--bg-lavender: #e4dff0;
--text-dark: #1a1a1a;
--badge-mint: #c8f0d8;
--badge-yellow: #f0f0c8;
--badge-pink: #f0d4e0;
--font-display: 'Outfit', 'Noto Sans SC', sans-serif;
--font-body: 'Outfit', 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Split background colors
- Playful badge pills with icons
- Grid pattern overlay on right panel
- Rounded CTA buttons
---
### 8. Vintage Editorial
**Vibe:** Witty, confident, editorial, personality-driven
**Layout:** Centered content on cream. Abstract geometric shapes as accent.
**Typography:**
- Display: `Fraunces`, `Noto Serif SC` (700/900) — distinctive serif
- Body: `Work Sans`, `Noto Sans SC` (400/500)
**Colors:**
```css
:root {
--bg-cream: #f5f3ee;
--text-primary: #1a1a1a;
--text-secondary: #555;
--accent-warm: #e8d4c0;
--font-display: 'Fraunces', 'Noto Serif SC', serif;
--font-body: 'Work Sans', 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Abstract geometric shapes (circle outline + line + dot)
- Bold bordered CTA boxes
- Witty, conversational copy style
- **No illustrations—only geometric CSS shapes**
---
## Specialty Themes
### 9. Neon Cyber
**Vibe:** Futuristic, techy, confident
**Typography:** `Clash Display` + `Satoshi` (Fontshare), CN fallback: `Noto Sans SC`
**Colors:** Deep navy (#0a0f1c), cyan accent (#00ffcc), magenta (#ff00aa)
**Font vars:**
```css
:root {
--font-display: 'Clash Display', 'Noto Sans SC', sans-serif;
--font-body: 'Satoshi', 'Noto Sans SC', sans-serif;
}
```
**Signature:** Particle backgrounds, neon glow, grid patterns
---
### 10. Terminal Green
**Vibe:** Developer-focused, hacker aesthetic
**Typography:** `JetBrains Mono` (monospace only), CN fallback: `Noto Sans SC`
**Colors:** GitHub dark (#0d1117), terminal green (#39d353)
**Font vars:**
```css
:root {
--font-display: 'JetBrains Mono', 'Noto Sans SC', monospace;
--font-body: 'JetBrains Mono', 'Noto Sans SC', monospace;
}
```
**Signature:** Scan lines, blinking cursor, code syntax styling
---
### 11. Swiss Modern
**Vibe:** Clean, precise, Bauhaus-inspired
**Typography:** `Archivo` (800) + `Nunito` (400), CN fallback: `Noto Sans SC`
**Colors:** Pure white, pure black, red accent (#ff3300)
**Font vars:**
```css
:root {
--font-display: 'Archivo', 'Noto Sans SC', sans-serif;
--font-body: 'Nunito', 'Noto Sans SC', sans-serif;
}
```
**Signature:** Visible grid, asymmetric layouts, geometric shapes
---
### 12. Paper & Ink
**Vibe:** Editorial, literary, thoughtful
**Typography:** `Cormorant Garamond` + `Source Serif 4`, CN: `LXGW WenKai` (display) + `Noto Serif SC` (body)
**Colors:** Warm cream (#faf9f7), charcoal (#1a1a1a), crimson accent (#c41e3a)
**Font vars:**
```css
:root {
--font-display: 'Cormorant Garamond', 'LXGW WenKai', serif;
--font-body: 'Source Serif 4', 'Noto Serif SC', serif;
}
```
**Signature:** Drop caps, pull quotes, elegant horizontal rules
---
## Font Pairing Quick Reference
| Preset | Display Font | Body Font | CN Fallback | Source |
|--------|--------------|-----------|-------------|--------|
| Bold Signal | Archivo Black | Space Grotesk | Noto Sans SC | Google |
| Electric Studio | Manrope | Manrope | Noto Sans SC | Google |
| Creative Voltage | Syne | Space Mono | Noto Sans SC | Google |
| Dark Botanical | Cormorant | IBM Plex Sans | Noto Serif SC / Noto Sans SC | Google |
| Notebook Tabs | Bodoni Moda | DM Sans | Noto Serif SC / Noto Sans SC | Google |
| Pastel Geometry | Plus Jakarta Sans | Plus Jakarta Sans | Noto Sans SC | Google |
| Split Pastel | Outfit | Outfit | Noto Sans SC | Google |
| Vintage Editorial | Fraunces | Work Sans | Noto Serif SC / Noto Sans SC | Google |
| Neon Cyber | Clash Display | Satoshi | Noto Sans SC | Fontshare |
| Terminal Green | JetBrains Mono | JetBrains Mono | Noto Sans SC | JetBrains |
| Swiss Modern | Archivo | Nunito | Noto Sans SC | Google |
| Paper & Ink | Cormorant Garamond | Source Serif 4 | LXGW WenKai / Noto Serif SC | Google |
| **Research Formal** | Noto Serif SC | Noto Sans SC | (native) | Google |
| **Roadshow Elegance** | Playfair Display | Noto Sans SC | Noto Serif SC | Google |
| **Data Dashboard** | Noto Sans SC | Noto Sans SC | (native) | Google |
---
## Financial Themes (金融专用)
### 13. Research Formal (研报正式)
**Vibe:** Institutional, authoritative, data-driven. Suitable for research reports, analyst presentations, internal review meetings.
**Layout:** Clean white background with navy sidebar navigation. Emphasis on tables and structured data. Section numbers in sidebar.
**Typography:**
- Display: `Noto Serif SC` (600/700) — authoritative serif for Chinese headings
- Body: `Noto Sans SC` (400/500) — clean sans for data and body text
**Colors:**
```css
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #1a1a2e;
--text-secondary: #5a5a7a;
--accent: #1a365d;
--accent-secondary: #2563eb;
--accent-gold: #c5963a;
--border-color: rgba(26, 54, 93, 0.12);
--color-positive: #dc2626;
--color-negative: #16a34a;
--font-display: 'Noto Serif SC', serif;
--font-body: 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Navy blue sidebar with section navigation and page numbers
- Horizontal rule separators between sections (2px solid navy)
- Tables with navy header row and alternating gray rows
- Source citations in small text at slide bottom
- Company/team logo placement: top-right corner
- Section numbering: large navy numerals (01, 02, 03)
**Financial Layouts:** Optimized for KPI Dashboard, Data Table, Chart + Text Split. See [references/financial-layouts.md](references/financial-layouts.md).
---
### 14. Roadshow Elegance (路演风范)
**Vibe:** Premium, confident, investor-grade. For IPO roadshows, investor meetings, board presentations.
**Layout:** Dark gradient background with centered content. Gold accent highlights key metrics. Generous whitespace for gravitas.
**Typography:**
- Display: `Playfair Display`, `Noto Serif SC` (700/800) — premium serif
- Body: `Noto Sans SC` (300/400) — light weight for elegance
**Colors:**
```css
:root {
--bg-primary: #0c1929;
--bg-gradient: linear-gradient(145deg, #0c1929 0%, #162d50 40%, #0c1929 100%);
--text-primary: #f0ece4;
--text-secondary: #8a9bb5;
--accent: #d4a853;
--accent-secondary: #e8c882;
--border-color: rgba(212, 168, 83, 0.2);
--color-positive: #ef4444;
--color-negative: #22c55e;
--card-bg: rgba(255, 255, 255, 0.04);
--font-display: 'Playfair Display', 'Noto Serif SC', serif;
--font-body: 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Subtle radial gradient glow behind key content
- Gold accent lines (1px) as decorative separators
- Large KPI values in gold with light glow effect
- Minimal text, maximum impact
- Abstract geometric shapes: thin gold circles, lines
- Progress indicators and slide numbers in muted gold
**Best for:** Title slides with company valuation, KPI highlights with trend arrows (CSS `::after` triangles, not emoji), full-width chart slides with dark chart themes.
---
### 15. Data Dashboard (数据简报)
**Vibe:** Efficient, analytical, information-dense. For daily/weekly market updates, portfolio reviews, data-driven briefings.
**Layout:** Grid-based with tight spacing. Header bar with date and context. Content area maximizes data display. 2-3 column grids for KPI cards.
**Typography:**
- Display: `Noto Sans SC` (700) — clean, functional
- Body: `Noto Sans SC` (400) — consistent sans throughout
**Colors:**
```css
:root {
--bg-primary: #f5f5f5;
--bg-card: #ffffff;
--text-primary: #1f2937;
--text-secondary: #6b7280;
--accent: #2563eb;
--accent-secondary: #60a5fa;
--border-color: #e5e7eb;
--color-positive: #dc2626;
--color-negative: #16a34a;
--font-display: 'Noto Sans SC', sans-serif;
--font-body: 'Noto Sans SC', sans-serif;
}
```
**Signature Elements:**
- Top status bar: date + report title + page count
- Card-based layout with thin borders (1px solid #e5e7eb)
- Compact KPI cards with mini inline sparklines (SVG)
- Data tables with fixed header and zebra striping
- Trend indicators: CSS triangles + percentage (never emoji)
- Minimal decoration, maximum data real estate
- Color coding: blue = neutral/info, red = up (A-share), green = down
**Best for:** Multi-metric overview pages, dense comparison tables, chart grids (2x2), status/alert slides.
---
## DO NOT USE (Generic AI Patterns)
**Fonts:** Inter, Roboto, Arial, system fonts as display
**Colors:** `#6366f1` (generic indigo), purple gradients on white
**Layouts:** Everything centered, generic hero sections, identical card grids
**Decorations:** Realistic illustrations, gratuitous glassmorphism, drop shadows without purpose
---
## CSS Gotchas
### Negating CSS Functions
**WRONG — silently ignored by browsers (no console error):**
```css
right: -clamp(28px, 3.5vw, 44px); /* Browser ignores this */
margin-left: -min(10vw, 100px); /* Browser ignores this */
```
**CORRECT — wrap in `calc()`:**
```css
right: calc(-1 * clamp(28px, 3.5vw, 44px)); /* Works */
margin-left: calc(-1 * min(10vw, 100px)); /* Works */
```
CSS does not allow a leading `-` before function names. The browser silently discards the entire declaration — no error, the element just appears in the wrong position. **Always use `calc(-1 * ...)` to negate CSS function values.**

View File

@ -0,0 +1,211 @@
# Content Mapping Reference
信息类型到视觉形式的映射。这是本技能的核心知识 -- 给定任意内容,选择最佳的 slide 形态。
生成演示文稿时,按以下流程使用本表:
1. 识别用户提供的每块内容属于哪种信息类型
2. 查表确定对应的视觉形式
3. 按内容密度限制拆分 slide
4. 选择匹配的 CSS 布局和组件
---
## 核心映射表
### 数值与指标类
| 信息类型 | 典型内容 | 推荐视觉形式 | 最大密度 | 何时拆分 |
|----------|---------|-------------|---------|---------|
| 少量关键指标 (3-6 个) | 营收、利润率、用户数、ROE | **KPI 卡片网格** -- 大数字 + 标签 + 趋势标记 | 6 张卡片 | >6 个指标时分页 |
| 单个核心数字 + 解释 | "市占率 35%" + 为什么重要 | **Hero 数字** -- 超大数字居中,下方 2-3 行解释 | 1 个数字 + 3 行文字 | 不拆分 |
| 指标 + 同比/环比变化 | 本季收入 vs 上季 | **KPI 卡片 + CSS 三角趋势箭头** | 同 KPI 网格 | 同上 |
| 评分/打分 (多维度) | 五维能力评分、护城河评级 | **评分条 (progress bar)** -- 名称 + 分数 + 填充条 | 6 个维度 | >6 时分页 |
### 时序与趋势类
| 信息类型 | 典型内容 | 推荐视觉形式 | 备注 |
|----------|---------|-------------|------|
| 单指标时间序列 (3-8 期) | 年度营收 2019-2024 | **柱状图** (ECharts/SVG) | 直观看绝对值和趋势 |
| 多指标时间序列 (2-3 条线) | 收入 + 利润 + 毛利率 | **柱线混合图** -- 柱状(绝对值) + 折线(比率) | 双 Y 轴,左绝对值右百分比 |
| 增长率/变化率序列 | YoY 增速变化 | **折线图****面积图** | 强调趋势方向 |
| 长期趋势 (>8 期) | 10 年股价、月度数据 | **面积折线图** | 数据点密集时隐藏标签 |
### 结构与构成类
| 信息类型 | 典型内容 | 推荐视觉形式 | 备注 |
|----------|---------|-------------|------|
| 整体拆分为部分 (3-6 项) | 收入结构、成本构成 | **环形饼图** (donut) | >6 项时合并长尾为"其他" |
| 部分之间有层级 | 业务线 > 子业务 > 产品 | **矩形树图** (treemap) | 层级不超过 2 层 |
| 加减关系 (从 A 到 B 的拆解) | 营收 - 成本 - 费用 = 利润 | **瀑布图** (waterfall) | 正负值用不同颜色 |
| 占比对比 (多主体同一维度) | 各公司市占率 | **堆叠柱状图****分组柱状图** | 主体 <=5 个 |
### 对比与关系类
| 信息类型 | 典型内容 | 推荐视觉形式 | 最大密度 |
|----------|---------|-------------|---------|
| 多主体同维度对比 (3-5 项) | 可比公司估值、产品对比 | **对比卡片网格** -- 每卡片 4-5 行指标 | 4 张卡片 x 5 行 |
| 两个主体深度对比 | A 方案 vs B 方案 | **双列对照** -- 左右分栏,逐行对比 | 5-6 行对比项 |
| 优劣势分析 | 优势/劣势/机会/威胁 | **2x2 网格卡片** -- 每格 3-4 条 | 4 格 x 4 条 |
| 多维度矩阵 (>5 行 x 4 列) | 财务报表、详细数据 | **数据表格** -- 表头 + 交替行 + 高亮列 | 8 行 x 6 列 |
### 叙事与论点类
| 信息类型 | 典型内容 | 推荐视觉形式 | 最大密度 |
|----------|---------|-------------|---------|
| 核心论点 + 支撑要点 | "我们认为 X因为 A/B/C" | **标题 + 要点列表** -- 每条 1-2 行 | 1 标题 + 5 条 |
| 论点 + 数据佐证 | "增速领先" + 图表 | **图文分栏** -- 左文字右图表(或反过来) | 3-4 条文字 + 1 图表 |
| 因果链/推导过程 | A 导致 BB 导致 C | **步骤流程** -- 水平箭头连接的卡片 | 3-5 步 |
| 单条核心引用/结论 | "护城河评级: 宽" | **引用页** -- 大字居中 + 出处 | 1 句 (3 行内) |
| 多条并列结论 | 投资建议的 3-5 个关注点 | **结论列表** -- 左边框线 + 加粗主句 + 说明 | 4-5 条 |
### 时间与过程类
| 信息类型 | 典型内容 | 推荐视觉形式 | 最大密度 |
|----------|---------|-------------|---------|
| 历史事件序列 | 公司发展史、政策演变 | **纵向时间线** -- 年份 + 节点 + 描述 | 4-5 个节点 |
| 未来规划/路线图 | Q1 做 A, Q2 做 B | **横向时间线** -- 阶段 + 里程碑 | 4-6 个阶段 |
| 流程步骤 | 投资决策流程、审批流程 | **编号步骤** -- 序号圆 + 标题 + 描述 | 4-5 步 |
### 风险与评估类
| 信息类型 | 典型内容 | 推荐视觉形式 | 备注 |
|----------|---------|-------------|------|
| 风险清单 (含严重性) | 5 个风险 + 影响 + 概率 | **风险表格** -- 3-4 列,含严重性标签 | 5-6 行 |
| 评级/评分总结 | 总分 + 分项 | **评分仪表盘** -- 总分居中 + 分项条 | 总分 + 5-6 分项 |
| 正面/负面因素对比 | 利好 vs 利空 | **双列对照** -- 绿色左列/红色右列 | 每列 3-4 条 |
---
## 视觉形式速查
按使用频率排序。每种形式对应的完整 HTML/CSS 实现在 [references/financial-layouts.md](references/financial-layouts.md) 和 [references/html-template.md](references/html-template.md) 中。
### CSS 布局类
| 视觉形式 | CSS class / 组件 | 适用场景 | 参考文件 |
|----------|-----------------|---------|---------|
| KPI 卡片网格 | `.kpi-grid > .kpi-card` | 指标概览 | financial-layouts.md #1 |
| 标题 + 要点列表 | `h2 + .insight-list` | 论点阐述 | html-template.md |
| 数据表格 | `.data-table` | 多维数据 | financial-layouts.md #2 |
| 图文分栏 | `.split-layout` | 数据 + 解读 | financial-layouts.md #5 |
| 对比卡片 | `.comparison-grid > .comp-card` | 多主体对比 | financial-layouts.md #4 |
| 时间线 | `.timeline > .timeline-item` | 事件序列 | financial-layouts.md #3 |
| 评分条 | `.moat-item > .moat-bar` | 多维评分 | (inline) |
| Hero 数字 | 自定义大字 + 说明 | 单核心指标强调 | (inline) |
| 引用页 | `blockquote` 居中 | 核心结论/引用 | html-template.md |
| 双列对照 | 2-col grid | A vs B | (inline) |
| 步骤流程 | numbered steps | 过程/流程 | (inline) |
### ECharts 图表类
| 图表类型 | 适用信息 | 备注 |
|----------|---------|------|
| 面积图 | 单指标趋势,强调规模感 | 渐变填充增加视觉重量 |
| 堆叠面积图 | 多组分构成随时间的变化 | 同时看总量和各部分 |
| 柱线混合 + 双Y轴 | 绝对值 + 增长率/比率 | 左轴绝对值,右轴百分比 |
| 柱状图 | 离散值对比 | 分组/堆叠/横向三种变体 |
| 堆叠柱状图 | 构成 + 趋势 | 看总量变化和结构变化 |
| 分组柱状图 | 多主体同维度对比 | 2-4 个主体 |
| 横向条形图 | 排名 / Top N | 长标签更易阅读 |
| 折线图 | 连续趋势 | 多条线 <=3 条 |
| 环形饼图 | 3-6 项占比 | padAngle + borderRadius 更精致 |
| 玫瑰图 | 差异大的占比 | 面积缩放强调差距 |
| 嵌套环形图 | 两级分类构成 | 内环大类,外环细分 |
| 半环仪表盘 | 单一完成度/占比 | 直观展示达标率 |
| Treemap | 面积=数值的层级构成 | 支持 2 层,面积直觉强 |
| 旭日图 | 3 层以上层级 | 从内到外逐层细化 |
| 热力图 | NxM 矩阵数值 | 颜色深浅表示强度 |
| 相关性热力图 | 相关系数方阵 | 对称矩阵,-1 到 +1 色阶 |
| 雷达图 | 多维综合评分 | 2 个主体对比最佳 |
| 散点图 | 两变量相关性 | 气泡大小可编码第三维 |
| 瀑布图 | 加减拆解 | 正/负值不同颜色 |
| K 线图 | 股价走势 | A 股红涨绿跌 |
| 漏斗图 | 逐步递减/转化 | 筛选、转化流程 |
| 桑基图 | 流向/流转关系 | 来源到去向的价值流 |
| Inline SVG | 简单趋势 (零依赖) | 无需 ECharts CDN |
所有 ECharts 图表的完整配置样例见 [references/charts-base.md](references/charts-base.md)。
---
## 组合策略
一页 slide 可以组合多种视觉元素,但必须遵守密度限制:
**允许的组合:**
- KPI 网格 (3-4 卡片) + 1 行小字注释
- 图表 + 标题 + 数据来源注释
- 图文分栏: 左侧 3-4 条要点 + 右侧 1 个图表
- 表格 + 标题 + 1 行脚注
- 标题 + 2 个并排小图表 (高度减半)
**禁止的组合:**
- 图表 + 表格 (两者都需要大量空间)
- KPI 网格 (6 卡片) + 要点列表 (空间不够)
- 两个全尺寸图表并排
- 表格 + 要点列表
**遇到复杂内容时:** 拆分为多张 slide用一致的标题前缀保持连贯性如"收入分析 (1/2)"、"收入分析 (2/2)")。
---
## Slide 生成检查清单(逐项强制执行)
对每块用户内容,按顺序完成以下 4 步。每步必须有明确结论,不得跳过。
### Step 1: 分类(单选,必须命中一个)
| IF 内容是... | THEN 类型 = |
|-------------|------------|
| 3-6 个数值指标(营收、利润率、用户数等) | `数值指标` |
| 带年份/季度的数值序列 | `时序趋势` |
| 整体拆分为部分(收入结构、成本构成) | `结构构成` |
| 多个主体的同维度对比 | `对比关系` |
| 观点/论点/结论 + 文字支撑 | `叙事论点` |
| 按时间排列的事件/里程碑 | `时间过程` |
| 风险清单/评分/评级 | `风险评估` |
### Step 2: 密度检查(必须回答 Yes/No
对照上方映射表的"最大密度"列:
- IF 内容量 <= 密度限制 THEN 继续
- IF 内容量 > 密度限制 THEN **必须拆分为多张 slide**,每张只保留一个聚焦点
**禁止行为:** 不得通过缩小字号、减少间距来容纳超限内容。
### Step 3: 选择视觉形式(直接查表)
| IF 类型 = | THEN 视觉形式 = |
|-----------|----------------|
| `数值指标`<=6 个 | `.kpi-grid` KPI 卡片网格 |
| `数值指标` 且 =1 个核心数字 | Hero 数字(超大居中) |
| `时序趋势` 且单指标 | 面积图 或 柱状图 |
| `时序趋势` 且多指标 | 柱线混合图(双 Y 轴) |
| `结构构成` 且 3-6 项 | 环形饼图 |
| `结构构成` 且有层级 | Treemap 或 旭日图 |
| `结构构成` 且加减拆解 | 瀑布图 |
| `对比关系` 且 3-4 个主体 | `.comparison-grid` 对比卡片 |
| `对比关系` 且 >5 行数据 | `.data-table` 数据表格 |
| `叙事论点` 且纯文字 | 标题 + `.insight-list` 要点列表 |
| `叙事论点` 且有数据佐证 | `.split-layout` 图文分栏 |
| `时间过程` | `.timeline` 时间线 |
| `风险评估` 且清单型 | `.data-table` 风险表格 |
| `风险评估` 且多维打分 | 评分条 (progress bar) |
### Step 4: 位置排序
- 开头 1-2 页: 标题页 + 概览/KPI
- 中间: 核心论证(图表、表格、分栏)
- 结尾 1-2 页: 结论/建议
---
## 禁止行为清单(看到即纠正)
- 超过 6 个 KPI 卡片不分页
- 图表 + 表格放在同一页
- 两个全尺寸图表并排
- 使用固定 px/rem 字号(必须 clamp()
- 使用 emoji 代替 CSS 趋势箭头
- 内容溢出时缩小字号而非拆分 slide
- 遗漏中文字体回退链

View File

@ -0,0 +1,204 @@
# Animation Patterns Reference
Use this reference when generating presentations. Match animations to the intended feeling.
## Effect-to-Feeling Guide
| Feeling | Animations | Visual Cues |
|---------|-----------|-------------|
| **Dramatic / Cinematic** | Slow fade-ins (1-1.5s), large scale transitions (0.9 to 1), parallax scrolling | Dark backgrounds, spotlight effects, full-bleed images |
| **Techy / Futuristic** | Neon glow (box-shadow), glitch/scramble text, grid reveals | Particle systems (canvas), grid patterns, monospace accents, cyan/magenta/electric blue |
| **Playful / Friendly** | Bouncy easing (spring physics), floating/bobbing | Rounded corners, pastel/bright colors, hand-drawn elements |
| **Professional / Corporate** | Subtle fast animations (200-300ms), clean slides | Navy/slate/charcoal, precise spacing, data visualization focus |
| **Calm / Minimal** | Very slow subtle motion, gentle fades | High whitespace, muted palette, serif typography, generous padding |
| **Editorial / Magazine** | Staggered text reveals, image-text interplay | Strong type hierarchy, pull quotes, grid-breaking layouts, serif headlines + sans body |
## Entrance Animations
```css
/* Fade + Slide Up (most versatile) */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s var(--ease-out-expo),
transform 0.6s var(--ease-out-expo);
}
.visible .reveal {
opacity: 1;
transform: translateY(0);
}
/* Scale In */
.reveal-scale {
opacity: 0;
transform: scale(0.9);
transition: opacity 0.6s, transform 0.6s var(--ease-out-expo);
}
/* Slide from Left */
.reveal-left {
opacity: 0;
transform: translateX(-50px);
transition: opacity 0.6s, transform 0.6s var(--ease-out-expo);
}
/* Blur In */
.reveal-blur {
opacity: 0;
filter: blur(10px);
transition: opacity 0.8s, filter 0.8s var(--ease-out-expo);
}
```
## Background Effects
```css
/* Gradient Mesh — layered radial gradients for depth */
.gradient-bg {
background:
radial-gradient(ellipse at 20% 80%, rgba(120, 0, 255, 0.3) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(0, 255, 200, 0.2) 0%, transparent 50%),
var(--bg-primary);
}
/* Noise Texture — inline SVG for grain */
.noise-bg {
background-image: url("data:image/svg+xml,..."); /* Inline SVG noise */
}
/* Grid Pattern — subtle structural lines */
.grid-bg {
background-image:
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 50px 50px;
}
```
## Interactive Effects
```javascript
/* 3D Tilt on Hover — adds depth to cards/panels */
class TiltEffect {
constructor(element) {
this.element = element;
this.element.style.transformStyle = 'preserve-3d';
this.element.style.perspective = '1000px';
this.element.addEventListener('mousemove', (e) => {
const rect = this.element.getBoundingClientRect();
const x = (e.clientX - rect.left) / rect.width - 0.5;
const y = (e.clientY - rect.top) / rect.height - 0.5;
this.element.style.transform = `rotateY(${x * 10}deg) rotateX(${-y * 10}deg)`;
});
this.element.addEventListener('mouseleave', () => {
this.element.style.transform = 'rotateY(0) rotateX(0)';
});
}
}
```
## Financial Data Animations
金融演示专用动画效果。数据展示优先清晰和准确,动画起辅助作用。
### KPI 数字滚动
```javascript
/* 数字从 0 滚动到目标值,配合 formatCN 格式化 */
class CountUp {
constructor(el, target, duration = 1200) {
this.el = el;
this.target = target;
this.duration = duration;
}
start() {
const t0 = performance.now();
const tick = (now) => {
const p = Math.min((now - t0) / this.duration, 1);
const eased = 1 - Math.pow(1 - p, 3);
this.el.textContent = typeof formatCN === 'function'
? formatCN(this.target * eased)
: Math.round(this.target * eased).toLocaleString();
if (p < 1) requestAnimationFrame(tick);
};
requestAnimationFrame(tick);
}
}
```
### 表格逐行显现
```css
.data-table tbody tr {
opacity: 0;
transform: translateY(10px);
transition: opacity 0.4s var(--ease-out-expo),
transform 0.4s var(--ease-out-expo);
}
.slide.visible .data-table tbody tr { opacity: 1; transform: none; }
.slide.visible .data-table tbody tr:nth-child(1) { transition-delay: 0.1s; }
.slide.visible .data-table tbody tr:nth-child(2) { transition-delay: 0.15s; }
.slide.visible .data-table tbody tr:nth-child(3) { transition-delay: 0.2s; }
.slide.visible .data-table tbody tr:nth-child(4) { transition-delay: 0.25s; }
.slide.visible .data-table tbody tr:nth-child(5) { transition-delay: 0.3s; }
.slide.visible .data-table tbody tr:nth-child(6) { transition-delay: 0.35s; }
```
### KPI 卡片依次进入
```css
.kpi-grid .kpi-card {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s var(--ease-out-expo),
transform 0.5s var(--ease-out-expo);
}
.slide.visible .kpi-grid .kpi-card { opacity: 1; transform: none; }
.slide.visible .kpi-grid .kpi-card:nth-child(1) { transition-delay: 0.1s; }
.slide.visible .kpi-grid .kpi-card:nth-child(2) { transition-delay: 0.2s; }
.slide.visible .kpi-grid .kpi-card:nth-child(3) { transition-delay: 0.3s; }
.slide.visible .kpi-grid .kpi-card:nth-child(4) { transition-delay: 0.4s; }
```
### 图表容器淡入
```css
.chart-container {
opacity: 0;
transition: opacity 0.8s ease;
}
.slide.visible .chart-container {
opacity: 1;
}
```
### 趋势指标闪烁强调
```css
@keyframes highlight-pulse {
0%, 100% { background-color: transparent; }
50% { background-color: rgba(var(--accent-rgb, 37, 99, 235), 0.1); }
}
.kpi-trend.positive, .kpi-trend.negative {
animation: highlight-pulse 2s ease 1;
animation-delay: 1.5s;
border-radius: 2px;
padding: 0 0.2em;
}
```
---
## Troubleshooting
| Problem | Fix |
|---------|-----|
| Fonts not loading | Check Fontshare/Google Fonts URL; ensure font names match in CSS |
| Chinese fonts slow | Noto Sans/Serif SC are 2-4MB; use `display=swap` and subset `&subset=chinese-simplified` |
| Animations not triggering | Verify Intersection Observer is running; check `.visible` class is being added |
| Scroll snap not working | Ensure `scroll-snap-type: y mandatory` on html; each slide needs `scroll-snap-align: start` |
| ECharts not rendering | Charts must init after slide is visible (container needs non-zero dimensions) |
| Mobile issues | Disable heavy effects at 768px breakpoint; test touch events; reduce particle count |
| Performance issues | Use `will-change` sparingly; prefer `transform`/`opacity` animations; throttle scroll handlers |

View File

@ -0,0 +1,226 @@
# Charts Base -- 基础设施与工具
ECharts 集成基础:内联引入、容器规范、初始化模式、格式化工具、配色适配。
按需加载具体图表配置:
- [charts-trend.md](charts-trend.md) -- 折线/面积/柱状等趋势图
- [charts-composition.md](charts-composition.md) -- 饼图/玫瑰/Treemap/旭日图等构成图
- [charts-matrix.md](charts-matrix.md) -- 热力图/雷达/散点等矩阵图
- [charts-flow.md](charts-flow.md) -- 瀑布/K线/漏斗/桑基等流向图
---
## ECharts 内联引入(零外部依赖)
**禁止使用 CDN `<script src="...">`。** ECharts 必须内嵌到 HTML 中确保离线可用。
生成流程:
1. 读取本地文件 [echarts.min.js](echarts.min.js)~1MB已预存在 references 目录)
2. 将完整内容包裹在 `<script>/* ECharts v5 */...内容...</script>`
3. 放置位置:模板的 `<!-- SLOT: ECHARTS INLINE -->` 处(在 SLOT: SLIDES 之后、base JS 之前)
```html
<!-- 正确:内联(读取 references/echarts.min.js 内容) -->
<script>/* ECharts v5 - inline */
...references/echarts.min.js 的完整内容)...
</script>
<!-- 错误CDN 引用(禁止) -->
<!-- <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> -->
```
## 容器规范
```html
<div class="chart-container" id="chart1"></div>
```
```css
.chart-container {
width: min(90vw, 800px);
height: min(50vh, 400px);
margin: 0 auto;
}
@media (max-height: 700px) { .chart-container { height: min(40vh, 300px); } }
@media (max-height: 500px) { .chart-container { height: min(35vh, 250px); } }
```
## 初始化模式
图表必须在 slide 可见时初始化,否则容器尺寸为 0
```javascript
const chartObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const charts = entry.target.querySelectorAll('.chart-container[data-chart]');
charts.forEach(el => {
if (!el._echartInstance) {
const chart = echarts.init(el);
chart.setOption(JSON.parse(el.dataset.chart));
el._echartInstance = chart;
}
});
}
});
}, { threshold: 0.3 });
```
## 窗口 resize
```javascript
window.addEventListener('resize', () => {
document.querySelectorAll('.chart-container').forEach(el => {
if (el._echartInstance) el._echartInstance.resize();
});
});
```
---
## 中文数字格式化工具
每个包含数据的演示必须内嵌:
```javascript
function formatCN(num, decimals = 1) {
if (num === null || num === undefined) return '--';
const abs = Math.abs(num);
const sign = num < 0 ? '-' : '';
if (abs >= 1e8) return sign + (abs / 1e8).toFixed(decimals).replace(/\.0$/, '') + '\u4ebf';
if (abs >= 1e4) return sign + (abs / 1e4).toFixed(decimals).replace(/\.0$/, '') + '\u4e07';
return sign + abs.toLocaleString('zh-CN');
}
function formatPct(num, decimals = 2) {
if (num === null || num === undefined) return '--';
return (num * 100).toFixed(decimals) + '%';
}
function formatDateCN(dateStr) {
const d = new Date(dateStr);
return d.getFullYear() + '\u5e74' + (d.getMonth() + 1) + '\u6708' + d.getDate() + '\u65e5';
}
```
---
## 配色适配
```javascript
function getThemeColors() {
const s = getComputedStyle(document.documentElement);
return {
primary: s.getPropertyValue('--accent').trim() || '#2563eb',
secondary: s.getPropertyValue('--accent-secondary').trim() || '#60a5fa',
negative: s.getPropertyValue('--color-negative').trim() || '#dc2626',
positive: s.getPropertyValue('--color-positive').trim() || '#16a34a',
text: s.getPropertyValue('--text-primary').trim() || '#333',
textSec: s.getPropertyValue('--text-secondary').trim() || '#666',
bg: s.getPropertyValue('--bg-primary').trim() || '#fff'
};
}
function initThemedChart(el, option) {
const colors = getThemeColors();
const chart = echarts.init(el, null, { renderer: 'canvas' });
option.textStyle = option.textStyle || {};
option.textStyle.fontFamily = getComputedStyle(document.documentElement)
.getPropertyValue('--font-body').trim() || 'Noto Sans SC, sans-serif';
option.textStyle.color = colors.text;
chart.setOption(option);
return chart;
}
```
---
## 图表类型速查
| 要表达什么 | 推荐图表 | 次选 | 配置文件 |
|-----------|---------|------|---------|
| 单指标趋势 | 面积图 | 折线图 | charts-trend |
| 多指标趋势 | 柱线混合图 | 堆叠面积图 | charts-trend |
| 增长率趋势 | 双Y轴折线 | 面积图 | charts-trend |
| 构成占比 (3-6项) | 环形饼图 | 玫瑰图 | charts-composition |
| 差异大的占比 | 玫瑰图 | Treemap | charts-composition |
| 两级分类构成 | 嵌套环形图 | Treemap | charts-composition |
| 三级以上层级 | 旭日图 | Treemap | charts-composition |
| 面积=数值的层级 | Treemap | 旭日图 | charts-composition |
| 单一达标率 | 半环仪表盘 | KPI卡片 | charts-composition |
| 排名 / Top N | 横向条形图 | 柱状图 | charts-flow |
| 多主体多指标对比 | 分组柱状图 | 雷达图 | charts-trend |
| 构成+时间趋势 | 堆叠柱状图 | 堆叠面积图 | charts-trend |
| 多维综合评分 | 雷达图 | CSS评分条 | charts-matrix |
| 两变量相关性 | 散点图 | 热力图 | charts-matrix |
| NxM 矩阵数据 | 热力图 | 表格 | charts-matrix |
| 相关系数矩阵 | 相关性热力图 | 表格 | charts-matrix |
| 逐步递减/转化 | 漏斗图 | 瀑布图 | charts-flow |
| 加减拆解 | 瀑布图 | 堆叠柱 | charts-flow |
| 流向/流转 | 桑基图 | 嵌套环形 | charts-flow |
| 股价走势 | K线图 | 面积折线 | charts-flow |
---
## Inline SVG (零依赖)
不引入 ECharts 时,用 SVG 实现简单图表。
### 简单柱状图
```html
<svg class="chart-svg" viewBox="0 0 400 200" preserveAspectRatio="xMidYMid meet">
<style>
.bar { transition: height 0.6s var(--ease-out-expo), y 0.6s var(--ease-out-expo); }
.bar-label { font-family: var(--font-body); font-size: 11px; fill: var(--text-secondary); text-anchor: middle; }
.bar-value { font-family: var(--font-body); font-size: 10px; fill: var(--text-primary); text-anchor: middle; }
</style>
<line x1="50" y1="10" x2="50" y2="170" stroke="var(--text-secondary)" stroke-width="0.5"/>
<rect class="bar" x="70" y="50" width="40" height="120" fill="var(--accent)" rx="2"/>
<rect class="bar" x="130" y="80" width="40" height="90" fill="var(--accent)" rx="2" opacity="0.7"/>
<rect class="bar" x="190" y="30" width="40" height="140" fill="var(--accent)" rx="2"/>
<rect class="bar" x="250" y="60" width="40" height="110" fill="var(--accent)" rx="2" opacity="0.7"/>
<text class="bar-label" x="90" y="190">Q1</text>
<text class="bar-label" x="150" y="190">Q2</text>
<text class="bar-label" x="210" y="190">Q3</text>
<text class="bar-label" x="270" y="190">Q4</text>
<text class="bar-value" x="90" y="45">1.2亿</text>
<text class="bar-value" x="150" y="75">0.9亿</text>
<text class="bar-value" x="210" y="25">1.4亿</text>
<text class="bar-value" x="270" y="55">1.1亿</text>
</svg>
```
### 简单折线图
```html
<svg class="chart-svg" viewBox="0 0 400 200" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="lineGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--accent)" stop-opacity="0.3"/>
<stop offset="100%" stop-color="var(--accent)" stop-opacity="0"/>
</linearGradient>
</defs>
<path d="M50,140 L120,100 L190,110 L260,60 L330,40 L330,170 L50,170 Z" fill="url(#lineGrad)"/>
<polyline points="50,140 120,100 190,110 260,60 330,40"
fill="none" stroke="var(--accent)" stroke-width="2"
stroke-dasharray="500" stroke-dashoffset="500">
<animate attributeName="stroke-dashoffset" to="0" dur="1.5s" fill="freeze"
begin="0.3s" calcMode="spline" keySplines="0.16 1 0.3 1"/>
</polyline>
<circle cx="50" cy="140" r="3" fill="var(--accent)"/>
<circle cx="120" cy="100" r="3" fill="var(--accent)"/>
<circle cx="190" cy="110" r="3" fill="var(--accent)"/>
<circle cx="260" cy="60" r="3" fill="var(--accent)"/>
<circle cx="330" cy="40" r="3" fill="var(--accent)"/>
</svg>
```
```css
.chart-svg {
width: min(90vw, 600px);
height: min(40vh, 300px);
display: block;
margin: 0 auto;
}
```

View File

@ -0,0 +1,236 @@
# Charts -- Composition (构成类)
饼图、环形图、Treemap、旭日图等。基础设施见 [charts-base.md](charts-base.md)。
---
## 环形饼图3-6 项占比)
```javascript
const pieOption = {
color: ['#1a365d', '#2563eb', '#60a5fa', '#93c5fd', '#c5963a'],
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { orient: 'vertical', right: '5%', top: 'center' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['40%', '50%'],
padAngle: 2,
itemStyle: { borderRadius: 4 },
label: { formatter: '{b}\n{d}%', fontSize: 11 },
emphasis: {
label: { fontSize: 14, fontWeight: 'bold' },
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.2)' }
},
data: [
{ value: 4500, name: '业务A' },
{ value: 3200, name: '业务B' },
{ value: 2100, name: '业务C' },
{ value: 1500, name: '其他' }
]
}]
};
```
---
## 玫瑰图 / 南丁格尔图
各项数值差异大时,用半径缩放强调差异。
```javascript
const roseOption = {
color: ['#1a365d', '#2563eb', '#3b82f6', '#60a5fa', '#93c5fd', '#c5963a'],
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: { bottom: '3%', left: 'center' },
series: [{
type: 'pie',
radius: ['15%', '70%'],
center: ['50%', '45%'],
roseType: 'area', /* 'radius' = 半径缩放, 'area' = 面积缩放 */
itemStyle: { borderRadius: 5 },
label: { formatter: '{b}\n{c}亿', fontSize: 11 },
data: [
{ value: 1843, name: '游戏' },
{ value: 1200, name: '社交网络' },
{ value: 1240, name: '网络广告' },
{ value: 2100, name: '金融科技' },
{ value: 69, name: '其他' }
]
}]
};
```
---
## 嵌套环形图(两级分类)
内环大类,外环细分。
```javascript
const nestedPieOption = {
tooltip: { trigger: 'item', formatter: '{a}<br/>{b}: {c} ({d}%)' },
series: [
{
name: '大类',
type: 'pie',
radius: ['0%', '35%'],
label: { position: 'inner', fontSize: 10, color: '#fff' },
data: [
{ value: 3043, name: '增值服务' },
{ value: 2100, name: '金融科技' },
{ value: 1240, name: '广告' }
]
},
{
name: '细分',
type: 'pie',
radius: ['45%', '70%'],
label: { formatter: '{b}: {d}%', fontSize: 10 },
data: [
{ value: 1843, name: '游戏' },
{ value: 1200, name: '社交网络' },
{ value: 1100, name: '支付' },
{ value: 1000, name: '企业服务' },
{ value: 740, name: '媒体广告' },
{ value: 500, name: '社交广告' }
]
}
]
};
```
---
## 半环仪表盘(单指标达标率)
```javascript
const gaugeOption = {
series: [{
type: 'gauge',
startAngle: 180,
endAngle: 0,
radius: '90%',
center: ['50%', '65%'],
min: 0, max: 100,
pointer: { show: false },
progress: { show: true, overlap: false, roundCap: true, width: 18, itemStyle: { color: '#2563eb' } },
axisLine: { lineStyle: { width: 18, color: [[1, '#e5e7eb']] } },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
detail: {
fontSize: 28, fontFamily: 'Noto Sans SC', fontWeight: 700,
formatter: '{value}%', offsetCenter: [0, '-15%'], color: '#1a1a2e'
},
title: { fontSize: 13, offsetCenter: [0, '15%'], color: '#5a5a7a' },
data: [{ value: 73.5, name: '目标达成率' }]
}]
};
```
---
## Treemap 矩形树图
面积 = 数值大小。适合两层层级构成。
```javascript
const treemapOption = {
tooltip: {
formatter: p => {
const path = p.treePathInfo.map(n => n.name).filter(n => n).join(' > ');
return path + '<br/>金额: ' + formatCN(p.value);
}
},
series: [{
type: 'treemap',
roam: false,
width: '92%', height: '85%', top: '8%',
breadcrumb: { show: true, bottom: 0 },
label: { show: true, formatter: '{b}\n{c}亿', fontSize: 12, fontFamily: 'Noto Sans SC' },
upperLabel: { show: true, height: 24, fontSize: 12, fontWeight: 600, color: '#fff' },
levels: [
{ itemStyle: { borderColor: '#fff', borderWidth: 3, gapWidth: 3 }, upperLabel: { show: true } },
{ itemStyle: { borderColor: 'rgba(255,255,255,0.5)', borderWidth: 1, gapWidth: 1 }, colorSaturation: [0.3, 0.6] }
],
data: [
{
name: '增值服务', value: 3043,
children: [
{ name: '本土游戏', value: 1200, itemStyle: { color: '#1a365d' } },
{ name: '海外游戏', value: 643, itemStyle: { color: '#2563eb' } },
{ name: '社交网络', value: 1200, itemStyle: { color: '#3b82f6' } }
]
},
{
name: '金融科技', value: 2100,
children: [
{ name: '支付', value: 1100, itemStyle: { color: '#c5963a' } },
{ name: '云服务', value: 600, itemStyle: { color: '#d4a853' } },
{ name: '其他', value: 400, itemStyle: { color: '#e8c882' } }
]
},
{
name: '网络广告', value: 1240,
children: [
{ name: '社交广告', value: 740, itemStyle: { color: '#16a34a' } },
{ name: '媒体广告', value: 500, itemStyle: { color: '#22c55e' } }
]
}
]
}]
};
```
---
## 旭日图 Sunburst多层级环形
从内到外逐层细化。适合 3 层以上层级。
```javascript
const sunburstOption = {
tooltip: {
formatter: p => {
const path = p.treePathInfo.map(n => n.name).filter(n => n).join(' > ');
return path + '<br/>金额: ' + formatCN(p.value);
}
},
series: [{
type: 'sunburst',
radius: ['12%', '85%'],
sort: 'desc',
emphasis: { focus: 'ancestor' },
label: { fontSize: 10, fontFamily: 'Noto Sans SC', rotate: 'radial', minAngle: 10 },
levels: [
{},
{ r0: '12%', r: '40%', label: { fontSize: 12, fontWeight: 600 }, itemStyle: { borderWidth: 2, borderColor: '#fff' } },
{ r0: '40%', r: '65%', label: { fontSize: 10 }, itemStyle: { borderWidth: 1.5, borderColor: '#fff' } },
{ r0: '65%', r: '85%', label: { fontSize: 9, align: 'right' }, itemStyle: { borderWidth: 1, borderColor: 'rgba(255,255,255,0.6)' } }
],
data: [
{
name: '增值服务', itemStyle: { color: '#1a365d' },
children: [
{ name: '游戏', value: 1843, children: [
{ name: '王者荣耀', value: 500 }, { name: '和平精英', value: 350 },
{ name: 'LoL/Valorant', value: 400 }, { name: '其他', value: 593 }
]},
{ name: '社交网络', value: 1200, children: [
{ name: '会员订阅', value: 650 }, { name: '虚拟道具', value: 550 }
]}
]
},
{
name: '金融科技', itemStyle: { color: '#c5963a' },
children: [ { name: '支付', value: 1100 }, { name: '云服务', value: 600 }, { name: '其他', value: 400 } ]
},
{
name: '广告', itemStyle: { color: '#16a34a' },
children: [ { name: '社交广告', value: 740 }, { name: '媒体广告', value: 500 } ]
}
]
}]
};
```

View File

@ -0,0 +1,233 @@
# Charts -- Flow (流向与特殊类型)
瀑布图、K 线、横向条形、漏斗图、桑基图。基础设施见 [charts-base.md](charts-base.md)。
---
## 瀑布图(加减拆解)
从总量逐步拆解到净值。正/负值不同颜色。
```javascript
const waterfallOption = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '10%', right: '5%', top: '10%', bottom: '12%' },
xAxis: { type: 'category', data: ['营业收入', '营业成本', '毛利', '销售费用', '管理费用', '财务费用', '营业利润'] },
yAxis: { type: 'value', axisLabel: { formatter: v => formatCN(v) } },
series: [
{
name: '辅助',
type: 'bar', stack: 'total',
itemStyle: { borderColor: 'transparent', color: 'transparent' },
data: [0, 0, 0, 0, 0, 0, 0] /* 计算后的偏移量 */
},
{
name: '数值',
type: 'bar', stack: 'total', barWidth: '40%',
label: { show: true, position: 'top', formatter: p => formatCN(p.value) },
data: [
{ value: 10000, itemStyle: { color: '#2563eb' } },
{ value: -6000, itemStyle: { color: '#dc2626' } },
{ value: 4000, itemStyle: { color: '#2563eb' } },
{ value: -800, itemStyle: { color: '#dc2626' } },
{ value: -600, itemStyle: { color: '#dc2626' } },
{ value: -200, itemStyle: { color: '#dc2626' } },
{ value: 2400, itemStyle: { color: '#16a34a' } }
]
}
]
};
```
**瀑布图辅助层计算逻辑:**
```javascript
// 自动计算 transparent 辅助柱的高度
function buildWaterfall(items) {
// items: [{ label, value, type: 'start'|'add'|'sub'|'total' }]
let running = 0;
const categories = [], assist = [], values = [];
items.forEach(item => {
categories.push(item.label);
if (item.type === 'start' || item.type === 'total') {
assist.push(0);
values.push({ value: item.value, itemStyle: { color: item.type === 'total' ? '#16a34a' : '#2563eb' } });
running = item.value;
} else {
const v = item.type === 'sub' ? -Math.abs(item.value) : Math.abs(item.value);
assist.push(Math.max(0, running + Math.min(0, v)));
values.push({ value: v, itemStyle: { color: v >= 0 ? '#2563eb' : '#dc2626' } });
running += v;
}
});
return { categories, assist, values };
}
```
---
## K 线图(股价走势)
A 股惯例:红涨绿跌。
```javascript
const candlestickOption = {
tooltip: { trigger: 'axis' },
grid: { left: '10%', right: '5%', top: '10%', bottom: '15%' },
xAxis: { type: 'category', data: ['03-01', '03-04', '03-05', '03-06', '03-07', '03-08', '03-11', '03-12'] },
yAxis: { type: 'value', scale: true },
series: [{
type: 'candlestick',
itemStyle: {
color: '#dc2626', /* 阳线填充 */
color0: '#16a34a', /* 阴线填充 */
borderColor: '#dc2626',
borderColor0: '#16a34a'
},
data: [
/* [open, close, low, high] */
[540, 548, 536, 552],
[548, 545, 540, 550],
[545, 555, 543, 558],
[555, 550, 546, 556],
[550, 560, 548, 562],
[560, 558, 554, 563],
[558, 565, 555, 568],
[565, 550, 548, 566]
]
}]
};
```
K 线 + 成交量组合:
```javascript
const klineWithVolOption = {
tooltip: { trigger: 'axis' },
grid: [
{ left: '10%', right: '5%', top: '8%', height: '55%' }, /* K 线区域 */
{ left: '10%', right: '5%', top: '72%', height: '18%' } /* 成交量区域 */
],
xAxis: [
{ type: 'category', data: /* dates */, gridIndex: 0 },
{ type: 'category', data: /* dates */, gridIndex: 1 }
],
yAxis: [
{ type: 'value', scale: true, gridIndex: 0 },
{ type: 'value', gridIndex: 1, axisLabel: { formatter: v => formatCN(v) } }
],
series: [
{
type: 'candlestick',
xAxisIndex: 0, yAxisIndex: 0,
itemStyle: { color: '#dc2626', color0: '#16a34a', borderColor: '#dc2626', borderColor0: '#16a34a' },
data: /* [[open,close,low,high], ...] */
},
{
type: 'bar',
xAxisIndex: 1, yAxisIndex: 1,
barWidth: '60%',
data: /* volume array, with itemStyle per bar based on up/down */
}
]
};
```
---
## 横向条形图(排名 / Top N
横向更易阅读长标签。inverse 从上到下递减。
```javascript
const horizontalBarOption = {
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
grid: { left: '22%', right: '10%', top: '5%', bottom: '5%' },
xAxis: { type: 'value', axisLabel: { formatter: v => formatCN(v) } },
yAxis: {
type: 'category',
inverse: true,
data: ['腾讯', 'Sony', '微软', '任天堂', 'EA', '动视暴雪'],
axisLabel: { fontSize: 12 }
},
series: [{
type: 'bar',
barWidth: '55%',
data: [
{ value: 1843, itemStyle: { color: '#1a365d' } },
{ value: 1250, itemStyle: { color: '#93c5fd' } },
{ value: 1180, itemStyle: { color: '#93c5fd' } },
{ value: 920, itemStyle: { color: '#93c5fd' } },
{ value: 580, itemStyle: { color: '#93c5fd' } },
{ value: 520, itemStyle: { color: '#93c5fd' } }
],
label: { show: true, position: 'right', formatter: p => formatCN(p.value), fontSize: 11 },
itemStyle: { borderRadius: [0, 3, 3, 0] }
}]
};
```
---
## 漏斗图(转化/筛选流程)
逐步递减的转化过程。
```javascript
const funnelOption = {
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
series: [{
type: 'funnel',
left: '15%', right: '15%', top: '8%', bottom: '8%',
width: '70%',
minSize: '15%', maxSize: '100%',
sort: 'descending',
gap: 4,
label: { show: true, position: 'inside', formatter: '{b}\n{c}', fontSize: 12, fontFamily: 'Noto Sans SC' },
itemStyle: { borderWidth: 0 },
data: [
{ value: 5000, name: '初筛股票池', itemStyle: { color: '#93c5fd' } },
{ value: 2800, name: '基本面过滤', itemStyle: { color: '#60a5fa' } },
{ value: 1200, name: '估值合理', itemStyle: { color: '#3b82f6' } },
{ value: 450, name: '技术面确认', itemStyle: { color: '#2563eb' } },
{ value: 120, name: '最终标的', itemStyle: { color: '#1a365d' } }
]
}]
};
```
---
## 桑基图 Sankey价值流向
从来源到去向的流转关系。
```javascript
const sankeyOption = {
tooltip: { trigger: 'item', formatter: '{b}: {c}亿' },
series: [{
type: 'sankey',
layout: 'none',
top: '5%', bottom: '5%', left: '5%', right: '15%',
nodeWidth: 20,
nodeGap: 12,
label: { fontSize: 11, fontFamily: 'Noto Sans SC' },
lineStyle: { color: 'gradient', curveness: 0.5, opacity: 0.4 },
emphasis: { lineStyle: { opacity: 0.7 } },
data: [
{ name: '游戏' }, { name: '社交' }, { name: '广告' }, { name: '金融科技' },
{ name: '营业收入' }, { name: '营业成本' }, { name: '毛利' }, { name: '费用' }, { name: '净利润' }
],
links: [
{ source: '游戏', target: '营业收入', value: 1843 },
{ source: '社交', target: '营业收入', value: 1200 },
{ source: '广告', target: '营业收入', value: 1240 },
{ source: '金融科技', target: '营业收入', value: 2100 },
{ source: '营业收入', target: '营业成本', value: 3073 },
{ source: '营业收入', target: '毛利', value: 3536 },
{ source: '毛利', target: '费用', value: 1515 },
{ source: '毛利', target: '净利润', value: 2021 }
]
}]
};
```

View File

@ -0,0 +1,146 @@
# Charts -- Matrix (矩阵与分布类)
热力图、相关性矩阵、雷达图、散点图。基础设施见 [charts-base.md](charts-base.md)。
---
## 热力图NxM 矩阵数据)
两个维度交叉的数值强度,颜色深浅表示大小。
```javascript
const heatmapOption = {
tooltip: { position: 'top', formatter: p => p.data[1] + ' x ' + p.data[0] + ': ' + p.data[2] },
grid: { left: '12%', right: '8%', top: '8%', bottom: '15%' },
xAxis: {
type: 'category',
data: ['Q1', 'Q2', 'Q3', 'Q4'],
splitArea: { show: true },
axisLabel: { fontSize: 11 }
},
yAxis: {
type: 'category',
data: ['游戏', '社交', '广告', '金融科技', '云服务'],
splitArea: { show: true },
axisLabel: { fontSize: 11 }
},
visualMap: {
min: 0, max: 600,
calculable: true,
orient: 'horizontal',
left: 'center', bottom: '0%',
inRange: { color: ['#f0f4ff', '#93c5fd', '#2563eb', '#1a365d'] },
textStyle: { fontSize: 10 }
},
series: [{
type: 'heatmap',
data: [
['Q1', '游戏', 480], ['Q2', '游戏', 520], ['Q3', '游戏', 460], ['Q4', '游戏', 580],
['Q1', '社交', 290], ['Q2', '社交', 310], ['Q3', '社交', 300], ['Q4', '社交', 320],
['Q1', '广告', 250], ['Q2', '广告', 340], ['Q3', '广告', 310], ['Q4', '广告', 380],
['Q1', '金融科技', 490], ['Q2', '金融科技', 530], ['Q3', '金融科技', 510], ['Q4', '金融科技', 560],
['Q1', '云服务', 130], ['Q2', '云服务', 150], ['Q3', '云服务', 155], ['Q4', '云服务', 170]
],
label: { show: true, fontSize: 10, color: '#333', formatter: p => p.data[2] },
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } }
}]
};
```
---
## 相关性热力图(方阵)
对称矩阵,-1 到 +1 色阶。适用于多资产/多因子相关系数。
```javascript
const labels = ['贵州茅台', '五粮液', '泸州老窖', '山西汾酒', '洋河股份'];
const corrData = [
[0,0,1.0],[1,0,0.85],[2,0,0.82],[3,0,0.71],[4,0,0.68],
[0,1,0.85],[1,1,1.0],[2,1,0.91],[3,1,0.78],[4,1,0.74],
[0,2,0.82],[1,2,0.91],[2,2,1.0],[3,2,0.80],[4,2,0.72],
[0,3,0.71],[1,3,0.78],[2,3,0.80],[3,3,1.0],[4,3,0.65],
[0,4,0.68],[1,4,0.74],[2,4,0.72],[3,4,0.65],[4,4,1.0]
];
const corrMatrixOption = {
tooltip: { formatter: p => labels[p.data[0]] + ' vs ' + labels[p.data[1]] + ': ' + p.data[2].toFixed(2) },
grid: { left: '15%', right: '12%', top: '15%', bottom: '5%' },
xAxis: { type: 'category', data: labels, axisLabel: { rotate: 30, fontSize: 10 } },
yAxis: { type: 'category', data: labels, axisLabel: { fontSize: 10 } },
visualMap: {
min: -1, max: 1,
inRange: { color: ['#dc2626', '#fef2f2', '#ffffff', '#eff6ff', '#2563eb'] },
orient: 'horizontal', left: 'center', bottom: '0%',
text: ['+1.0', '-1.0']
},
series: [{
type: 'heatmap',
data: corrData,
label: { show: true, fontSize: 10, formatter: p => p.data[2].toFixed(2) }
}]
};
```
---
## 雷达图(多维评分对比)
多个维度的综合评分。2 个主体对比最佳。
```javascript
const radarOption = {
tooltip: {},
legend: { top: '2%' },
radar: {
indicator: [
{ name: '网络效应', max: 10 },
{ name: '转换成本', max: 10 },
{ name: '品牌价值', max: 10 },
{ name: '规模经济', max: 10 },
{ name: '无形资产', max: 10 },
{ name: '成本优势', max: 10 }
],
radius: '65%',
axisName: { fontSize: 11, fontFamily: 'Noto Sans SC' },
splitArea: { areaStyle: { color: ['rgba(37,99,235,0.02)', 'rgba(37,99,235,0.05)'] } }
},
series: [{
type: 'radar',
data: [
{ name: '腾讯', value: [9.5, 9.0, 7.0, 8.0, 8.5, 5.0], areaStyle: { opacity: 0.25 }, lineStyle: { width: 2 } },
{ name: '阿里巴巴', value: [7.0, 7.5, 6.5, 8.5, 6.0, 6.0], areaStyle: { opacity: 0.15 }, lineStyle: { width: 2 } }
]
}]
};
```
---
## 散点图 / 气泡图
两变量相关性,气泡大小编码第三维度。
```javascript
const scatterOption = {
tooltip: {
formatter: p => p.data[3] + '<br/>PE: ' + p.data[0] + 'x<br/>ROE: ' + p.data[1] + '%<br/>市值: ' + formatCN(p.data[2])
},
grid: { left: '10%', right: '8%', top: '8%', bottom: '12%' },
xAxis: { name: 'PE (TTM)', nameLocation: 'center', nameGap: 30, scale: true },
yAxis: { name: 'ROE (%)', nameLocation: 'center', nameGap: 35, scale: true },
series: [{
type: 'scatter',
symbolSize: p => Math.sqrt(p[2]) / 800,
data: [
[22.8, 22.5, 53000e8, '腾讯'],
[16.2, 12.8, 22000e8, '阿里巴巴'],
[28.5, 35.2, 16000e8, 'Meta'],
[35.0, 42.0, 33000e8, '微软'],
[25.0, 30.0, 25000e8, '字节(估)']
],
label: { show: true, formatter: p => p.data[3], position: 'top', fontSize: 10 },
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' } }
}]
};
```

Some files were not shown because too many files have changed in this diff Show More