commit 6664758a6df2353c8f38579eec9eb6a8cf6e4e1e Author: Z User Date: Sat Jun 6 05:21:10 2026 +0000 Initial commit diff --git a/.env b/.env new file mode 100644 index 0000000..769ebf2 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=file:/home/z/my-project/db/custom.db diff --git a/download/README.md b/download/README.md new file mode 100755 index 0000000..9820ad3 --- /dev/null +++ b/download/README.md @@ -0,0 +1 @@ +Here are all the generated files. diff --git a/skills/ASR/LICENSE.txt b/skills/ASR/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/ASR/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/ASR/SKILL.md b/skills/ASR/SKILL.md new file mode 100755 index 0000000..fde9bd7 --- /dev/null +++ b/skills/ASR/SKILL.md @@ -0,0 +1,580 @@ +--- +name: ASR +description: Implement speech-to-text (ASR/automatic speech recognition) capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to transcribe audio files, convert speech to text, build voice input features, or process audio recordings. Supports base64 encoded audio files and returns accurate text transcriptions. +license: MIT +--- + +# ASR (Speech to Text) Skill + +This skill guides the implementation of speech-to-text (ASR) functionality using the z-ai-web-dev-sdk package, enabling accurate transcription of spoken audio into text. + +## Skills Path + +**Skill Location**: `{project_path}/skills/ASR` + +this skill is located at above path in your project. + +**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/asr.ts` for a working example. + +## Overview + +Speech-to-Text (ASR - Automatic Speech Recognition) allows you to build applications that convert spoken language in audio files into written text, enabling voice-controlled interfaces, transcription services, and audio content analysis. + +**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code. + +## Prerequisites + +The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below. + +## CLI Usage (For Simple Tasks) + +For simple audio transcription tasks, you can use the z-ai CLI instead of writing code. This is ideal for quick transcriptions, testing audio files, or batch processing. + +### Basic Transcription from File + +```bash +# Transcribe an audio file +z-ai asr --file ./audio.wav + +# Save transcription to JSON file +z-ai asr -f ./recording.mp3 -o transcript.json + +# Transcribe and view output +z-ai asr --file ./interview.wav --output result.json +``` + +### Transcription from Base64 + +```bash +# Transcribe from base64 encoded audio +z-ai asr --base64 "UklGRiQAAABXQVZFZm10..." -o result.json + +# Using short option +z-ai asr -b "base64_encoded_audio_data" -o transcript.json +``` + +### Streaming Output + +```bash +# Stream transcription results +z-ai asr -f ./audio.wav --stream +``` + +### CLI Parameters + +- `--file, -f `: **Required** (if not using --base64) - Audio file path +- `--base64, -b `: **Required** (if not using --file) - Base64 encoded audio +- `--output, -o `: Optional - Output file path (JSON format) +- `--stream`: Optional - Stream the transcription output + +### Supported Audio Formats + +The ASR service supports various audio formats including: +- WAV (.wav) +- MP3 (.mp3) +- Other common audio formats + +### When to Use CLI vs SDK + +**Use CLI for:** +- Quick audio file transcriptions +- Testing audio recognition accuracy +- Simple batch processing scripts +- One-off transcription tasks + +**Use SDK for:** +- Real-time audio transcription in applications +- Integration with recording systems +- Custom audio processing workflows +- Production applications with streaming audio + +## Basic ASR Implementation + +### Simple Audio Transcription + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function transcribeAudio(audioFilePath) { + const zai = await ZAI.create(); + + // Read audio file and convert to base64 + const audioFile = fs.readFileSync(audioFilePath); + const base64Audio = audioFile.toString('base64'); + + const response = await zai.audio.asr.create({ + file_base64: base64Audio + }); + + return response.text; +} + +// Usage +const transcription = await transcribeAudio('./audio.wav'); +console.log('Transcription:', transcription); +``` + +### Transcribe Multiple Audio Files + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function transcribeBatch(audioFilePaths) { + const zai = await ZAI.create(); + const results = []; + + for (const filePath of audioFilePaths) { + try { + const audioFile = fs.readFileSync(filePath); + const base64Audio = audioFile.toString('base64'); + + const response = await zai.audio.asr.create({ + file_base64: base64Audio + }); + + results.push({ + file: filePath, + success: true, + transcription: response.text + }); + } catch (error) { + results.push({ + file: filePath, + success: false, + error: error.message + }); + } + } + + return results; +} + +// Usage +const files = ['./interview1.wav', './interview2.wav', './interview3.wav']; +const transcriptions = await transcribeBatch(files); + +transcriptions.forEach(result => { + if (result.success) { + console.log(`${result.file}: ${result.transcription}`); + } else { + console.error(`${result.file}: Error - ${result.error}`); + } +}); +``` + +## Advanced Use Cases + +### Audio File Processing with Metadata + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +async function transcribeWithMetadata(audioFilePath) { + const zai = await ZAI.create(); + + // Get file metadata + const stats = fs.statSync(audioFilePath); + const audioFile = fs.readFileSync(audioFilePath); + const base64Audio = audioFile.toString('base64'); + + const startTime = Date.now(); + + const response = await zai.audio.asr.create({ + file_base64: base64Audio + }); + + const endTime = Date.now(); + + return { + filename: path.basename(audioFilePath), + filepath: audioFilePath, + fileSize: stats.size, + transcription: response.text, + wordCount: response.text.split(/\s+/).length, + processingTime: endTime - startTime, + timestamp: new Date().toISOString() + }; +} + +// Usage +const result = await transcribeWithMetadata('./meeting_recording.wav'); +console.log('Transcription Details:', JSON.stringify(result, null, 2)); +``` + +### Real-time Audio Processing Service + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +class ASRService { + constructor() { + this.zai = null; + this.transcriptionCache = new Map(); + } + + async initialize() { + this.zai = await ZAI.create(); + } + + generateCacheKey(audioBuffer) { + const crypto = require('crypto'); + return crypto.createHash('md5').update(audioBuffer).digest('hex'); + } + + async transcribe(audioFilePath, useCache = true) { + const audioBuffer = fs.readFileSync(audioFilePath); + const cacheKey = this.generateCacheKey(audioBuffer); + + // Check cache + if (useCache && this.transcriptionCache.has(cacheKey)) { + return { + transcription: this.transcriptionCache.get(cacheKey), + cached: true + }; + } + + // Transcribe audio + const base64Audio = audioBuffer.toString('base64'); + + const response = await this.zai.audio.asr.create({ + file_base64: base64Audio + }); + + // Cache result + if (useCache) { + this.transcriptionCache.set(cacheKey, response.text); + } + + return { + transcription: response.text, + cached: false + }; + } + + clearCache() { + this.transcriptionCache.clear(); + } + + getCacheSize() { + return this.transcriptionCache.size; + } +} + +// Usage +const asrService = new ASRService(); +await asrService.initialize(); + +const result1 = await asrService.transcribe('./audio.wav'); +console.log('First call (not cached):', result1); + +const result2 = await asrService.transcribe('./audio.wav'); +console.log('Second call (cached):', result2); +``` + +### Directory Transcription + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +async function transcribeDirectory(directoryPath, outputJsonPath) { + const zai = await ZAI.create(); + + // Get all audio files + const files = fs.readdirSync(directoryPath); + const audioFiles = files.filter(file => + /\.(wav|mp3|m4a|flac|ogg)$/i.test(file) + ); + + const results = { + directory: directoryPath, + totalFiles: audioFiles.length, + processedAt: new Date().toISOString(), + transcriptions: [] + }; + + for (const filename of audioFiles) { + const filePath = path.join(directoryPath, filename); + + try { + const audioFile = fs.readFileSync(filePath); + const base64Audio = audioFile.toString('base64'); + + const response = await zai.audio.asr.create({ + file_base64: base64Audio + }); + + results.transcriptions.push({ + filename: filename, + success: true, + text: response.text, + wordCount: response.text.split(/\s+/).length + }); + + console.log(`✓ Transcribed: ${filename}`); + } catch (error) { + results.transcriptions.push({ + filename: filename, + success: false, + error: error.message + }); + + console.error(`✗ Failed: ${filename} - ${error.message}`); + } + } + + // Save results to JSON + fs.writeFileSync( + outputJsonPath, + JSON.stringify(results, null, 2) + ); + + return results; +} + +// Usage +const results = await transcribeDirectory( + './audio-recordings', + './transcriptions.json' +); + +console.log(`\nProcessed ${results.totalFiles} files`); +console.log(`Successful: ${results.transcriptions.filter(t => t.success).length}`); +console.log(`Failed: ${results.transcriptions.filter(t => !t.success).length}`); +``` + +## Best Practices + +### 1. Audio Format Handling + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function transcribeAnyFormat(audioFilePath) { + // Supported formats: WAV, MP3, M4A, FLAC, OGG, etc. + const validExtensions = ['.wav', '.mp3', '.m4a', '.flac', '.ogg']; + const ext = audioFilePath.toLowerCase().substring(audioFilePath.lastIndexOf('.')); + + if (!validExtensions.includes(ext)) { + throw new Error(`Unsupported audio format: ${ext}`); + } + + const zai = await ZAI.create(); + const audioFile = fs.readFileSync(audioFilePath); + const base64Audio = audioFile.toString('base64'); + + const response = await zai.audio.asr.create({ + file_base64: base64Audio + }); + + return response.text; +} +``` + +### 2. Error Handling + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function safeTranscribe(audioFilePath) { + try { + // Validate file exists + if (!fs.existsSync(audioFilePath)) { + throw new Error(`File not found: ${audioFilePath}`); + } + + // Check file size (e.g., limit to 100MB) + const stats = fs.statSync(audioFilePath); + const fileSizeMB = stats.size / (1024 * 1024); + + if (fileSizeMB > 100) { + throw new Error(`File too large: ${fileSizeMB.toFixed(2)}MB (max 100MB)`); + } + + // Transcribe + const zai = await ZAI.create(); + const audioFile = fs.readFileSync(audioFilePath); + const base64Audio = audioFile.toString('base64'); + + const response = await zai.audio.asr.create({ + file_base64: base64Audio + }); + + if (!response.text || response.text.trim().length === 0) { + throw new Error('Empty transcription result'); + } + + return { + success: true, + transcription: response.text, + filePath: audioFilePath, + fileSize: stats.size + }; + } catch (error) { + console.error('Transcription error:', error); + return { + success: false, + error: error.message, + filePath: audioFilePath + }; + } +} +``` + +### 3. Post-Processing Transcriptions + +```javascript +function cleanTranscription(text) { + // Remove excessive whitespace + text = text.replace(/\s+/g, ' ').trim(); + + // Capitalize first letter of sentences + text = text.replace(/(^\w|[.!?]\s+\w)/g, match => match.toUpperCase()); + + // Remove filler words (optional) + const fillers = ['um', 'uh', 'ah', 'like', 'you know']; + const fillerPattern = new RegExp(`\\b(${fillers.join('|')})\\b`, 'gi'); + text = text.replace(fillerPattern, '').replace(/\s+/g, ' '); + + return text; +} + +async function transcribeAndClean(audioFilePath) { + const zai = await ZAI.create(); + + const audioFile = fs.readFileSync(audioFilePath); + const base64Audio = audioFile.toString('base64'); + + const response = await zai.audio.asr.create({ + file_base64: base64Audio + }); + + return { + raw: response.text, + cleaned: cleanTranscription(response.text) + }; +} +``` + +## Common Use Cases + +1. **Meeting Transcription**: Convert recorded meetings into searchable text +2. **Interview Processing**: Transcribe interviews for analysis and documentation +3. **Podcast Transcription**: Create text versions of podcast episodes +4. **Voice Notes**: Convert voice memos to text for easier reference +5. **Call Center Analytics**: Analyze customer service calls +6. **Accessibility**: Provide text alternatives for audio content +7. **Voice Commands**: Enable voice-controlled applications +8. **Language Learning**: Transcribe pronunciation practice + +## Integration Examples + +### Express.js API Endpoint + +```javascript +import express from 'express'; +import multer from 'multer'; +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +const app = express(); +const upload = multer({ dest: 'uploads/' }); + +let zaiInstance; + +async function initZAI() { + zaiInstance = await ZAI.create(); +} + +app.post('/api/transcribe', upload.single('audio'), async (req, res) => { + try { + if (!req.file) { + return res.status(400).json({ error: 'No audio file provided' }); + } + + const audioFile = fs.readFileSync(req.file.path); + const base64Audio = audioFile.toString('base64'); + + const response = await zaiInstance.audio.asr.create({ + file_base64: base64Audio + }); + + // Clean up uploaded file + fs.unlinkSync(req.file.path); + + res.json({ + success: true, + transcription: response.text, + wordCount: response.text.split(/\s+/).length + }); + } catch (error) { + // Clean up on error + if (req.file && fs.existsSync(req.file.path)) { + fs.unlinkSync(req.file.path); + } + + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +initZAI().then(() => { + app.listen(3000, () => { + console.log('ASR API running on port 3000'); + }); +}); +``` + +## Troubleshooting + +**Issue**: "SDK must be used in backend" +- **Solution**: Ensure z-ai-web-dev-sdk is only imported in server-side code + +**Issue**: Empty or incorrect transcription +- **Solution**: Verify audio quality and format. Check if audio contains clear speech + +**Issue**: Large file processing fails +- **Solution**: Consider splitting large audio files into smaller segments + +**Issue**: Slow transcription speed +- **Solution**: Implement caching for repeated transcriptions, optimize file sizes + +**Issue**: Memory errors with large files +- **Solution**: Process files in chunks or increase Node.js memory limit + +## Performance Tips + +1. **Reuse SDK Instance**: Create once, use multiple times +2. **Implement Caching**: Cache transcriptions for duplicate files +3. **Batch Processing**: Process multiple files efficiently with proper queuing +4. **Audio Optimization**: Compress audio files before processing when possible +5. **Async Operations**: Use Promise.all for parallel processing when appropriate + +## Audio Quality Guidelines + +For best transcription results: +- **Sample Rate**: 16kHz or higher +- **Format**: WAV, MP3, or M4A recommended +- **Noise Level**: Minimize background noise +- **Speech Clarity**: Clear pronunciation and normal speaking pace +- **File Size**: Under 100MB recommended for individual files + +## Remember + +- Always use z-ai-web-dev-sdk in backend code only +- The SDK is already installed - import as shown in examples +- Audio files must be converted to base64 before processing +- Implement proper error handling for production applications +- Consider audio quality for best transcription accuracy +- Clean up temporary files after processing +- Cache results for frequently transcribed files diff --git a/skills/ASR/scripts/asr.ts b/skills/ASR/scripts/asr.ts new file mode 100755 index 0000000..5a39a39 --- /dev/null +++ b/skills/ASR/scripts/asr.ts @@ -0,0 +1,27 @@ +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +async function main(inputFile: string) { + if (!fs.existsSync(inputFile)) { + console.error(`Audio file not found: ${inputFile}`); + return; + } + + try { + const zai = await ZAI.create(); + + const audioBuffer = fs.readFileSync(inputFile); + const file_base64 = audioBuffer.toString('base64'); + + const result = await zai.audio.asr.create({ file_base64 }); + + console.log('Transcription result:'); + console.log(result.text ?? JSON.stringify(result, null, 2)); + } catch (err: any) { + console.error('ASR failed:', err?.message || err); + } +} + +main('./output.wav'); + diff --git a/skills/LLM/LICENSE.txt b/skills/LLM/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/LLM/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/LLM/SKILL.md b/skills/LLM/SKILL.md new file mode 100755 index 0000000..07e7ec0 --- /dev/null +++ b/skills/LLM/SKILL.md @@ -0,0 +1,856 @@ +--- +name: LLM +description: Implement large language model (LLM) chat completions using the z-ai-web-dev-sdk. Use this skill when the user needs to build conversational AI applications, chatbots, AI assistants, or any text generation features. Supports multi-turn conversations, system prompts, and context management. +license: MIT +--- + +# LLM (Large Language Model) Skill + +This skill guides the implementation of chat completions functionality using the z-ai-web-dev-sdk package, enabling powerful conversational AI and text generation capabilities. + +## Skills Path + +**Skill Location**: `{project_path}/skills/llm` + +this skill is located at above path in your project. + +**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/chat.ts` for a working example. + +## Overview + +The LLM skill allows you to build applications that leverage large language models for natural language understanding and generation, including chatbots, AI assistants, content generation, and more. + +**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code. + +## Prerequisites + +The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below. + +## CLI Usage (For Simple Tasks) + +For simple, one-off chat completions, you can use the z-ai CLI instead of writing code. This is ideal for quick tests, simple queries, or automation scripts. + +### Basic Chat + +```bash +# Simple question +z-ai chat --prompt "What is the capital of France?" + +# Save response to file +z-ai chat -p "Explain quantum computing" -o response.json + +# Stream the response +z-ai chat -p "Write a short poem" --stream +``` + +### With System Prompt + +```bash +# Custom system prompt for specific behavior +z-ai chat \ + --prompt "Review this code: function add(a,b) { return a+b; }" \ + --system "You are an expert code reviewer" \ + -o review.json +``` + +### With Thinking (Chain of Thought) + +```bash +# Enable thinking for complex reasoning +z-ai chat \ + --prompt "Solve this math problem: If a train travels 120km in 2 hours, what's its speed?" \ + --thinking \ + -o solution.json +``` + +### CLI Parameters + +- `--prompt, -p `: **Required** - User message content +- `--system, -s `: Optional - System prompt for custom behavior +- `--thinking, -t`: Optional - Enable chain-of-thought reasoning (default: disabled) +- `--output, -o `: Optional - Output file path (JSON format) +- `--stream`: Optional - Stream the response in real-time + +### When to Use CLI vs SDK + +**Use CLI for:** +- Quick one-off questions +- Simple automation scripts +- Testing prompts +- Single-turn conversations + +**Use SDK for:** +- Multi-turn conversations with context +- Custom conversation management +- Integration with web applications +- Complex chat workflows +- Production applications + +## Basic Chat Completions + +### Simple Question and Answer + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function askQuestion(question) { + const zai = await ZAI.create(); + + const completion = await zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: 'You are a helpful assistant.' + }, + { + role: 'user', + content: question + } + ], + thinking: { type: 'disabled' } + }); + + const response = completion.choices[0]?.message?.content; + return response; +} + +// Usage +const answer = await askQuestion('What is the capital of France?'); +console.log('Answer:', answer); +``` + +### Custom System Prompt + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function customAssistant(systemPrompt, userMessage) { + const zai = await ZAI.create(); + + const completion = await zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: systemPrompt + }, + { + role: 'user', + content: userMessage + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; +} + +// Usage - Code reviewer +const codeReview = await customAssistant( + 'You are an expert code reviewer. Analyze code for bugs, performance issues, and best practices.', + 'Review this function: function add(a, b) { return a + b; }' +); + +// Usage - Creative writer +const story = await customAssistant( + 'You are a creative fiction writer who writes engaging short stories.', + 'Write a short story about a robot learning to paint.' +); + +console.log(codeReview); +console.log(story); +``` + +## Multi-turn Conversations + +### Conversation History Management + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +class ConversationManager { + constructor(systemPrompt = 'You are a helpful assistant.') { + this.messages = [ + { + role: 'assistant', + content: systemPrompt + } + ]; + this.zai = null; + } + + async initialize() { + this.zai = await ZAI.create(); + } + + async sendMessage(userMessage) { + // Add user message to history + this.messages.push({ + role: 'user', + content: userMessage + }); + + // Get completion + const completion = await this.zai.chat.completions.create({ + messages: this.messages, + thinking: { type: 'disabled' } + }); + + const assistantResponse = completion.choices[0]?.message?.content; + + // Add assistant response to history + this.messages.push({ + role: 'assistant', + content: assistantResponse + }); + + return assistantResponse; + } + + getHistory() { + return this.messages; + } + + clearHistory(systemPrompt = 'You are a helpful assistant.') { + this.messages = [ + { + role: 'assistant', + content: systemPrompt + } + ]; + } + + getMessageCount() { + // Subtract 1 for system message + return this.messages.length - 1; + } +} + +// Usage +const conversation = new ConversationManager(); +await conversation.initialize(); + +const response1 = await conversation.sendMessage('Hi, my name is John.'); +console.log('AI:', response1); + +const response2 = await conversation.sendMessage('What is my name?'); +console.log('AI:', response2); // Should remember the name is John + +console.log('Total messages:', conversation.getMessageCount()); +``` + +### Context-Aware Conversations + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +class ContextualChat { + constructor() { + this.messages = []; + this.zai = null; + } + + async initialize() { + this.zai = await ZAI.create(); + } + + async startConversation(role, context) { + // Set up system prompt with context + const systemPrompt = `You are ${role}. Context: ${context}`; + + this.messages = [ + { + role: 'assistant', + content: systemPrompt + } + ]; + } + + async chat(userMessage) { + this.messages.push({ + role: 'user', + content: userMessage + }); + + const completion = await this.zai.chat.completions.create({ + messages: this.messages, + thinking: { type: 'disabled' } + }); + + const response = completion.choices[0]?.message?.content; + + this.messages.push({ + role: 'assistant', + content: response + }); + + return response; + } +} + +// Usage - Customer support scenario +const support = new ContextualChat(); +await support.initialize(); + +await support.startConversation( + 'a customer support agent for TechCorp', + 'The user has ordered product #12345 which is delayed due to shipping issues.' +); + +const reply1 = await support.chat('Where is my order?'); +console.log('Support:', reply1); + +const reply2 = await support.chat('Can I get a refund?'); +console.log('Support:', reply2); +``` + +## Advanced Use Cases + +### Content Generation + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +class ContentGenerator { + constructor() { + this.zai = null; + } + + async initialize() { + this.zai = await ZAI.create(); + } + + async generateBlogPost(topic, tone = 'professional') { + const completion = await this.zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: `You are a professional content writer. Write in a ${tone} tone.` + }, + { + role: 'user', + content: `Write a blog post about: ${topic}. Include an introduction, main points, and conclusion.` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; + } + + async generateProductDescription(productName, features) { + const completion = await this.zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: 'You are an expert at writing compelling product descriptions for e-commerce.' + }, + { + role: 'user', + content: `Write a product description for "${productName}". Key features: ${features.join(', ')}.` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; + } + + async generateEmailResponse(originalEmail, intent) { + const completion = await this.zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: 'You are a professional email writer. Write clear, concise, and polite emails.' + }, + { + role: 'user', + content: `Original email: "${originalEmail}"\n\nWrite a ${intent} response.` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; + } +} + +// Usage +const generator = new ContentGenerator(); +await generator.initialize(); + +const blogPost = await generator.generateBlogPost( + 'The Future of Artificial Intelligence', + 'informative' +); +console.log('Blog Post:', blogPost); + +const productDesc = await generator.generateProductDescription( + 'Smart Watch Pro', + ['Heart rate monitoring', 'GPS tracking', 'Waterproof', '7-day battery life'] +); +console.log('Product Description:', productDesc); +``` + +### Data Analysis and Summarization + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function analyzeData(data, analysisType) { + const zai = await ZAI.create(); + + const prompts = { + summarize: 'You are a data analyst. Summarize the key insights from the data.', + trend: 'You are a data analyst. Identify trends and patterns in the data.', + recommendation: 'You are a business analyst. Provide actionable recommendations based on the data.' + }; + + const completion = await zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: prompts[analysisType] || prompts.summarize + }, + { + role: 'user', + content: `Analyze this data:\n\n${JSON.stringify(data, null, 2)}` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; +} + +// Usage +const salesData = { + Q1: { revenue: 100000, customers: 250 }, + Q2: { revenue: 120000, customers: 280 }, + Q3: { revenue: 150000, customers: 320 }, + Q4: { revenue: 180000, customers: 380 } +}; + +const summary = await analyzeData(salesData, 'summarize'); +const trends = await analyzeData(salesData, 'trend'); +const recommendations = await analyzeData(salesData, 'recommendation'); + +console.log('Summary:', summary); +console.log('Trends:', trends); +console.log('Recommendations:', recommendations); +``` + +### Code Generation and Debugging + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +class CodeAssistant { + constructor() { + this.zai = null; + } + + async initialize() { + this.zai = await ZAI.create(); + } + + async generateCode(description, language) { + const completion = await this.zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: `You are an expert ${language} programmer. Write clean, efficient, and well-commented code.` + }, + { + role: 'user', + content: `Write ${language} code to: ${description}` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; + } + + async debugCode(code, issue) { + const completion = await this.zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: 'You are an expert debugger. Identify bugs and suggest fixes.' + }, + { + role: 'user', + content: `Code:\n${code}\n\nIssue: ${issue}\n\nFind the bug and suggest a fix.` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; + } + + async explainCode(code) { + const completion = await this.zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: 'You are a programming teacher. Explain code clearly and simply.' + }, + { + role: 'user', + content: `Explain what this code does:\n\n${code}` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; + } +} + +// Usage +const codeAssist = new CodeAssistant(); +await codeAssist.initialize(); + +const newCode = await codeAssist.generateCode( + 'Create a function that sorts an array of objects by a specific property', + 'JavaScript' +); +console.log('Generated Code:', newCode); + +const bugFix = await codeAssist.debugCode( + 'function add(a, b) { return a - b; }', + 'This function should add numbers but returns wrong results' +); +console.log('Debug Suggestion:', bugFix); +``` + +## Best Practices + +### 1. Prompt Engineering + +```javascript +// Bad: Vague prompt +const bad = await askQuestion('Tell me about AI'); + +// Good: Specific and structured prompt +async function askWithContext(topic, format, audience) { + const zai = await ZAI.create(); + + const completion = await zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: `You are an expert educator. Explain topics clearly for ${audience}.` + }, + { + role: 'user', + content: `Explain ${topic} in ${format} format. Include practical examples.` + } + ], + thinking: { type: 'disabled' } + }); + + return completion.choices[0]?.message?.content; +} + +const good = await askWithContext('artificial intelligence', 'bullet points', 'beginners'); +``` + +### 2. Error Handling + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function safeCompletion(messages, retries = 3) { + let lastError; + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const zai = await ZAI.create(); + + const completion = await zai.chat.completions.create({ + messages: messages, + thinking: { type: 'disabled' } + }); + + const response = completion.choices[0]?.message?.content; + + if (!response || response.trim().length === 0) { + throw new Error('Empty response from AI'); + } + + return { + success: true, + content: response, + attempts: attempt + }; + } catch (error) { + lastError = error; + console.error(`Attempt ${attempt} failed:`, error.message); + + if (attempt < retries) { + // Wait before retry (exponential backoff) + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + return { + success: false, + error: lastError.message, + attempts: retries + }; +} +``` + +### 3. Context Management + +```javascript +class ManagedConversation { + constructor(maxMessages = 20) { + this.maxMessages = maxMessages; + this.systemPrompt = ''; + this.messages = []; + this.zai = null; + } + + async initialize(systemPrompt) { + this.zai = await ZAI.create(); + this.systemPrompt = systemPrompt; + this.messages = [ + { + role: 'assistant', + content: systemPrompt + } + ]; + } + + async chat(userMessage) { + // Add user message + this.messages.push({ + role: 'user', + content: userMessage + }); + + // Trim old messages if exceeding limit (keep system prompt) + if (this.messages.length > this.maxMessages) { + this.messages = [ + this.messages[0], // Keep system prompt + ...this.messages.slice(-(this.maxMessages - 1)) + ]; + } + + const completion = await this.zai.chat.completions.create({ + messages: this.messages, + thinking: { type: 'disabled' } + }); + + const response = completion.choices[0]?.message?.content; + + this.messages.push({ + role: 'assistant', + content: response + }); + + return response; + } + + getTokenEstimate() { + // Rough estimate: ~4 characters per token + const totalChars = this.messages + .map(m => m.content.length) + .reduce((a, b) => a + b, 0); + return Math.ceil(totalChars / 4); + } +} +``` + +### 4. Response Processing + +```javascript +async function getStructuredResponse(query, format = 'json') { + const zai = await ZAI.create(); + + const formatInstructions = { + json: 'Respond with valid JSON only. No additional text.', + list: 'Respond with a numbered list.', + markdown: 'Respond in Markdown format.' + }; + + const completion = await zai.chat.completions.create({ + messages: [ + { + role: 'assistant', + content: `You are a helpful assistant. ${formatInstructions[format]}` + }, + { + role: 'user', + content: query + } + ], + thinking: { type: 'disabled' } + }); + + const response = completion.choices[0]?.message?.content; + + // Parse JSON if requested + if (format === 'json') { + try { + return JSON.parse(response); + } catch (e) { + console.error('Failed to parse JSON response'); + return { raw: response }; + } + } + + return response; +} + +// Usage +const jsonData = await getStructuredResponse( + 'List three programming languages with their primary use cases', + 'json' +); +console.log(jsonData); +``` + +## Common Use Cases + +1. **Chatbots & Virtual Assistants**: Build conversational interfaces for customer support +2. **Content Generation**: Create articles, product descriptions, marketing copy +3. **Code Assistance**: Generate, explain, and debug code +4. **Data Analysis**: Analyze and summarize complex data sets +5. **Language Translation**: Translate text between languages +6. **Educational Tools**: Create tutoring and learning applications +7. **Email Automation**: Generate professional email responses +8. **Creative Writing**: Story generation, poetry, and creative content + +## Integration Examples + +### Express.js Chatbot API + +```javascript +import express from 'express'; +import ZAI from 'z-ai-web-dev-sdk'; + +const app = express(); +app.use(express.json()); + +// Store conversations in memory (use database in production) +const conversations = new Map(); + +let zaiInstance; + +async function initZAI() { + zaiInstance = await ZAI.create(); +} + +app.post('/api/chat', async (req, res) => { + try { + const { sessionId, message, systemPrompt } = req.body; + + if (!message) { + return res.status(400).json({ error: 'Message is required' }); + } + + // Get or create conversation history + let history = conversations.get(sessionId) || [ + { + role: 'assistant', + content: systemPrompt || 'You are a helpful assistant.' + } + ]; + + // Add user message + history.push({ + role: 'user', + content: message + }); + + // Get completion + const completion = await zaiInstance.chat.completions.create({ + messages: history, + thinking: { type: 'disabled' } + }); + + const aiResponse = completion.choices[0]?.message?.content; + + // Add AI response to history + history.push({ + role: 'assistant', + content: aiResponse + }); + + // Save updated history + conversations.set(sessionId, history); + + res.json({ + success: true, + response: aiResponse, + messageCount: history.length - 1 + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +app.delete('/api/chat/:sessionId', (req, res) => { + const { sessionId } = req.params; + conversations.delete(sessionId); + res.json({ success: true, message: 'Conversation cleared' }); +}); + +initZAI().then(() => { + app.listen(3000, () => { + console.log('Chatbot API running on port 3000'); + }); +}); +``` + +## Troubleshooting + +**Issue**: "SDK must be used in backend" +- **Solution**: Ensure z-ai-web-dev-sdk is only imported and used in server-side code + +**Issue**: Empty or incomplete responses +- **Solution**: Check that completion.choices[0]?.message?.content exists and is not empty + +**Issue**: Conversation context getting too long +- **Solution**: Implement message trimming to keep only recent messages + +**Issue**: Inconsistent responses +- **Solution**: Use more specific system prompts and provide clear instructions + +**Issue**: Rate limiting errors +- **Solution**: Implement retry logic with exponential backoff + +## Performance Tips + +1. **Reuse SDK Instance**: Create ZAI instance once and reuse across requests +2. **Manage Context Length**: Trim old messages to avoid token limits +3. **Implement Caching**: Cache responses for common queries +4. **Use Specific Prompts**: Clear prompts lead to faster, better responses +5. **Handle Errors Gracefully**: Implement retry logic and fallback responses + +## Security Considerations + +1. **Input Validation**: Always validate and sanitize user input +2. **Rate Limiting**: Implement rate limits to prevent abuse +3. **API Key Protection**: Never expose SDK credentials in client-side code +4. **Content Filtering**: Filter sensitive or inappropriate content +5. **Session Management**: Implement proper session handling and cleanup + +## Remember + +- Always use z-ai-web-dev-sdk in backend code only +- The SDK is already installed - import as shown in examples +- Use the 'assistant' role for system prompts +- Set thinking to { type: 'disabled' } for standard completions +- Implement proper error handling and retries for production +- Manage conversation history to avoid token limits +- Clear and specific prompts lead to better results +- Check `scripts/chat.ts` for a quick start example diff --git a/skills/LLM/scripts/chat.ts b/skills/LLM/scripts/chat.ts new file mode 100755 index 0000000..046fd59 --- /dev/null +++ b/skills/LLM/scripts/chat.ts @@ -0,0 +1,32 @@ +import ZAI, { ChatMessage } from "z-ai-web-dev-sdk"; + +async function main(prompt: string) { + try { + const zai = await ZAI.create(); + + const messages: ChatMessage[] = [ + { + role: "assistant", + content: "Hi, I'm a helpful assistant." + }, + { + role: "user", + content: prompt, + }, + ]; + + const response = await zai.chat.completions.create({ + messages, + stream: false, + thinking: { type: "disabled" }, + }); + + const reply = response.choices?.[0]?.message?.content; + console.log("Chat reply:"); + console.log(reply ?? JSON.stringify(response, null, 2)); + } catch (err: any) { + console.error("Chat failed:", err?.message || err); + } +} + +main('What is the capital of France?'); diff --git a/skills/TTS/LICENSE.txt b/skills/TTS/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/TTS/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/TTS/SKILL.md b/skills/TTS/SKILL.md new file mode 100755 index 0000000..d92d225 --- /dev/null +++ b/skills/TTS/SKILL.md @@ -0,0 +1,735 @@ +--- +name: TTS +description: Implement text-to-speech (TTS) capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to convert text into natural-sounding speech, create audio content, build voice-enabled applications, or generate spoken audio files. Supports multiple voices, adjustable speed, and various audio formats. +license: MIT +--- + +# TTS (Text to Speech) Skill + +This skill guides the implementation of text-to-speech (TTS) functionality using the z-ai-web-dev-sdk package, enabling conversion of text into natural-sounding speech audio. + +## Skills Path + +**Skill Location**: `{project_path}/skills/TTS` + +This skill is located at the above path in your project. + +**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/tts.ts` for a working example. + +## Overview + +Text-to-Speech allows you to build applications that generate spoken audio from text input, supporting various voices, speeds, and output formats for diverse use cases. + +**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code. + +## API Limitations and Constraints + +Before implementing TTS functionality, be aware of these important limitations: + +### Input Text Constraints +- **Maximum length**: 1024 characters per request +- Text exceeding this limit must be split into smaller chunks + +### Audio Parameters +- **Speed range**: 0.5 to 2.0 + - 0.5 = half speed (slower) + - 1.0 = normal speed (default) + - 2.0 = double speed (faster) +- **Volume range**: Greater than 0, up to 10 + - Default: 1.0 + - Values must be greater than 0 (exclusive) and up to 10 (inclusive) + +### Format and Streaming +- **Streaming limitation**: When `stream: true` is enabled, only `pcm` format is supported +- **Non-streaming**: Supports `wav`, `pcm`, and `mp3` formats +- **Sample rate**: 24000 Hz (recommended) + +### Best Practice for Long Text +```javascript +function splitTextIntoChunks(text, maxLength = 1000) { + const chunks = []; + const sentences = text.match(/[^.!?]+[.!?]+/g) || [text]; + + let currentChunk = ''; + for (const sentence of sentences) { + if ((currentChunk + sentence).length <= maxLength) { + currentChunk += sentence; + } else { + if (currentChunk) chunks.push(currentChunk.trim()); + currentChunk = sentence; + } + } + if (currentChunk) chunks.push(currentChunk.trim()); + + return chunks; +} +``` + +## Prerequisites + +The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below. + +## CLI Usage (For Simple Tasks) + +For simple text-to-speech conversions, you can use the z-ai CLI instead of writing code. This is ideal for quick audio generation, testing voices, or simple automation. + +### Basic TTS + +```bash +# Convert text to speech (default WAV format) +z-ai tts --input "Hello, world" --output ./hello.wav + +# Using short options +z-ai tts -i "Hello, world" -o ./hello.wav +``` + +### Different Voices and Speed + +```bash +# Use specific voice +z-ai tts -i "Welcome to our service" -o ./welcome.wav --voice tongtong + +# Adjust speech speed (0.5-2.0) +z-ai tts -i "This is faster speech" -o ./fast.wav --speed 1.5 + +# Slower speech +z-ai tts -i "This is slower speech" -o ./slow.wav --speed 0.8 +``` + +### Different Output Formats + +```bash +# MP3 format +z-ai tts -i "Hello World" -o ./hello.mp3 --format mp3 + +# WAV format (default) +z-ai tts -i "Hello World" -o ./hello.wav --format wav + +# PCM format +z-ai tts -i "Hello World" -o ./hello.pcm --format pcm +``` + +### Streaming Output + +```bash +# Stream audio generation +z-ai tts -i "This is a longer text that will be streamed" -o ./stream.wav --stream +``` + +### CLI Parameters + +- `--input, -i `: **Required** - Text to convert to speech (max 1024 characters) +- `--output, -o `: **Required** - Output audio file path +- `--voice, -v `: Optional - Voice type (default: tongtong) +- `--speed, -s `: Optional - Speech speed, 0.5-2.0 (default: 1.0) +- `--format, -f `: Optional - Output format: wav, mp3, pcm (default: wav) +- `--stream`: Optional - Enable streaming output (only supports pcm format) + +### When to Use CLI vs SDK + +**Use CLI for:** +- Quick text-to-speech conversions +- Testing different voices and speeds +- Simple batch audio generation +- Command-line automation scripts + +**Use SDK for:** +- Dynamic audio generation in applications +- Integration with web services +- Custom audio processing pipelines +- Production applications with complex requirements + +## Basic TTS Implementation + +### Simple Text to Speech + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function textToSpeech(text, outputPath) { + const zai = await ZAI.create(); + + const response = await zai.audio.tts.create({ + input: text, + voice: 'tongtong', + speed: 1.0, + response_format: 'wav', + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + fs.writeFileSync(outputPath, buffer); + console.log(`Audio saved to ${outputPath}`); + return outputPath; +} + +// Usage +await textToSpeech('Hello, world!', './output.wav'); +``` + +### Multiple Voice Options + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function generateWithVoice(text, voice, outputPath) { + const zai = await ZAI.create(); + + const response = await zai.audio.tts.create({ + input: text, + voice: voice, // Available voices: tongtong, chuichui, xiaochen, jam, kazi, douji, luodo + speed: 1.0, + response_format: 'wav', + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + fs.writeFileSync(outputPath, buffer); + return outputPath; +} + +// Usage +await generateWithVoice('Welcome to our service', 'tongtong', './welcome.wav'); +``` + +### Adjustable Speed + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function generateWithSpeed(text, speed, outputPath) { + const zai = await ZAI.create(); + + // Speed range: 0.5 to 2.0 (API constraint) + // 0.5 = half speed (slower) + // 1.0 = normal speed (default) + // 2.0 = double speed (faster) + // Values outside this range will cause API errors + + const response = await zai.audio.tts.create({ + input: text, + voice: 'tongtong', + speed: speed, + response_format: 'wav', + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + fs.writeFileSync(outputPath, buffer); + return outputPath; +} + +// Usage - slower narration +await generateWithSpeed('This is an important announcement', 0.8, './slow.wav'); + +// Usage - faster narration +await generateWithSpeed('Quick update', 1.3, './fast.wav'); +``` + +### Adjustable Volume + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function generateWithVolume(text, volume, outputPath) { + const zai = await ZAI.create(); + + // Volume range: greater than 0, up to 10 (API constraint) + // Values must be > 0 (exclusive) and <= 10 (inclusive) + // Default: 1.0 (normal volume) + + const response = await zai.audio.tts.create({ + input: text, + voice: 'tongtong', + speed: 1.0, + volume: volume, // Optional parameter + response_format: 'wav', + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + fs.writeFileSync(outputPath, buffer); + return outputPath; +} + +// Usage - louder audio +await generateWithVolume('This is an announcement', 5.0, './loud.wav'); + +// Usage - quieter audio +await generateWithVolume('Whispered message', 0.5, './quiet.wav'); +``` + +## Advanced Use Cases + +### Batch Processing + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +async function batchTextToSpeech(textArray, outputDir) { + const zai = await ZAI.create(); + const results = []; + + // Ensure output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + for (let i = 0; i < textArray.length; i++) { + try { + const text = textArray[i]; + const outputPath = path.join(outputDir, `audio_${i + 1}.wav`); + + const response = await zai.audio.tts.create({ + input: text, + voice: 'tongtong', + speed: 1.0, + response_format: 'wav', + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + fs.writeFileSync(outputPath, buffer); + results.push({ + success: true, + text, + path: outputPath + }); + } catch (error) { + results.push({ + success: false, + text: textArray[i], + error: error.message + }); + } + } + + return results; +} + +// Usage +const texts = [ + 'Welcome to chapter one', + 'Welcome to chapter two', + 'Welcome to chapter three' +]; + +const results = await batchTextToSpeech(texts, './audio-output'); +console.log('Generated:', results.length, 'audio files'); +``` + +### Dynamic Content Generation + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +class TTSGenerator { + constructor() { + this.zai = null; + } + + async initialize() { + this.zai = await ZAI.create(); + } + + async generateAudio(text, options = {}) { + const { + voice = 'tongtong', + speed = 1.0, + format = 'wav' + } = options; + + const response = await this.zai.audio.tts.create({ + input: text, + voice: voice, + speed: speed, + response_format: format, + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(new Uint8Array(arrayBuffer)); + } + + async saveAudio(text, outputPath, options = {}) { + const buffer = await this.generateAudio(text, options); + if (buffer) { + fs.writeFileSync(outputPath, buffer); + return outputPath; + } + return null; + } +} + +// Usage +const generator = new TTSGenerator(); +await generator.initialize(); + +await generator.saveAudio( + 'Hello, this is a test', + './output.wav', + { speed: 1.2 } +); +``` + +### Next.js API Route Example + +```javascript +import { NextRequest, NextResponse } from 'next/server'; + +export async function POST(req: NextRequest) { + try { + const { text, voice = 'tongtong', speed = 1.0 } = await req.json(); + + // Import ZAI SDK + const ZAI = (await import('z-ai-web-dev-sdk')).default; + + // Create SDK instance + const zai = await ZAI.create(); + + // Generate TTS audio + const response = await zai.audio.tts.create({ + input: text.trim(), + voice: voice, + speed: speed, + response_format: 'wav', + stream: false, + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + // Return audio as response + return new NextResponse(buffer, { + status: 200, + headers: { + 'Content-Type': 'audio/wav', + 'Content-Length': buffer.length.toString(), + 'Cache-Control': 'no-cache', + }, + }); + } catch (error) { + console.error('TTS API Error:', error); + + return NextResponse.json( + { + error: error instanceof Error ? error.message : '生成语音失败,请稍后重试', + }, + { status: 500 } + ); + } +} +``` + +## Best Practices + +### 1. Text Preparation +```javascript +function prepareTextForTTS(text) { + // Remove excessive whitespace + text = text.replace(/\s+/g, ' ').trim(); + + // Expand common abbreviations for better pronunciation + const abbreviations = { + 'Dr.': 'Doctor', + 'Mr.': 'Mister', + 'Mrs.': 'Misses', + 'etc.': 'et cetera' + }; + + for (const [abbr, full] of Object.entries(abbreviations)) { + text = text.replace(new RegExp(abbr, 'g'), full); + } + + return text; +} +``` + +### 2. Error Handling +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function safeTTS(text, outputPath) { + try { + // Validate input + if (!text || text.trim().length === 0) { + throw new Error('Text input cannot be empty'); + } + + if (text.length > 1024) { + throw new Error('Text input exceeds maximum length of 1024 characters'); + } + + const zai = await ZAI.create(); + + const response = await zai.audio.tts.create({ + input: text, + voice: 'tongtong', + speed: 1.0, + response_format: 'wav', + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + fs.writeFileSync(outputPath, buffer); + + return { + success: true, + path: outputPath, + size: buffer.length + }; + } catch (error) { + console.error('TTS Error:', error); + return { + success: false, + error: error.message + }; + } +} +``` + +### 3. SDK Instance Reuse + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +// Create a singleton instance +let zaiInstance = null; + +async function getZAIInstance() { + if (!zaiInstance) { + zaiInstance = await ZAI.create(); + } + return zaiInstance; +} + +// Usage +const zai = await getZAIInstance(); +const response = await zai.audio.tts.create({ ... }); +``` + +## Common Use Cases + +1. **Audiobooks & Podcasts**: Convert written content to audio format +2. **E-learning**: Create narration for educational content +3. **Accessibility**: Provide audio versions of text content +4. **Voice Assistants**: Generate dynamic responses +5. **Announcements**: Create automated audio notifications +6. **IVR Systems**: Generate phone system prompts +7. **Content Localization**: Create audio in different languages + +## Integration Examples + +### Express.js API Endpoint + +```javascript +import express from 'express'; +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +const app = express(); +app.use(express.json()); + +let zaiInstance; +const outputDir = './audio-output'; + +async function initZAI() { + zaiInstance = await ZAI.create(); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } +} + +app.post('/api/tts', async (req, res) => { + try { + const { text, voice = 'tongtong', speed = 1.0 } = req.body; + + if (!text) { + return res.status(400).json({ error: 'Text is required' }); + } + + const filename = `tts_${Date.now()}.wav`; + const outputPath = path.join(outputDir, filename); + + const response = await zaiInstance.audio.tts.create({ + input: text, + voice: voice, + speed: speed, + response_format: 'wav', + stream: false + }); + + // Get array buffer from Response object + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + + fs.writeFileSync(outputPath, buffer); + + res.json({ + success: true, + audioUrl: `/audio/${filename}`, + size: buffer.length + }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +app.use('/audio', express.static('audio-output')); + +initZAI().then(() => { + app.listen(3000, () => { + console.log('TTS API running on port 3000'); + }); +}); +``` + +## Troubleshooting + +**Issue**: "Input text exceeds maximum length" +- **Solution**: Text input is limited to 1024 characters. Split longer text into chunks using the `splitTextIntoChunks` function shown in the API Limitations section + +**Issue**: "Invalid speed parameter" or unexpected speed behavior +- **Solution**: Speed must be between 0.5 and 2.0. Check your speed value is within this range + +**Issue**: "Invalid volume parameter" +- **Solution**: Volume must be greater than 0 and up to 10. Ensure volume value is in range (0, 10] + +**Issue**: "Stream format not supported" with WAV/MP3 +- **Solution**: Streaming mode only supports PCM format. Either use `response_format: 'pcm'` with streaming, or disable streaming (`stream: false`) for WAV/MP3 output + +**Issue**: "SDK must be used in backend" +- **Solution**: Ensure z-ai-web-dev-sdk is only imported in server-side code + +**Issue**: "TypeError: response.audio is undefined" +- **Solution**: The SDK returns a standard Response object, use `await response.arrayBuffer()` instead of accessing `response.audio` + +**Issue**: Generated audio file is empty or corrupted +- **Solution**: Ensure you're calling `await response.arrayBuffer()` and properly converting to Buffer: `Buffer.from(new Uint8Array(arrayBuffer))` + +**Issue**: Audio sounds unnatural +- **Solution**: Prepare text properly (remove special characters, expand abbreviations) + +**Issue**: Long processing times +- **Solution**: Break long text into smaller chunks and process in parallel + +**Issue**: Next.js caching old API route +- **Solution**: Create a new API route endpoint or restart the dev server + +## Performance Tips + +1. **Reuse SDK Instance**: Create ZAI instance once and reuse +2. **Implement Caching**: Cache generated audio for repeated text +3. **Batch Processing**: Process multiple texts efficiently +4. **Optimize Text**: Remove unnecessary content before generation +5. **Async Processing**: Use queues for handling multiple requests + +## Important Notes + +### API Constraints + +**Input Text Length**: Maximum 1024 characters per request. For longer text: +```javascript +// Split long text into chunks +const longText = "..."; // Your long text here +const chunks = splitTextIntoChunks(longText, 1000); + +for (const chunk of chunks) { + const response = await zai.audio.tts.create({ + input: chunk, + voice: 'tongtong', + speed: 1.0, + response_format: 'wav', + stream: false + }); + // Process each chunk... +} +``` + +**Streaming Format Limitation**: When using `stream: true`, only `pcm` format is supported. For `wav` or `mp3` output, use `stream: false`. + +**Sample Rate**: Audio is generated at 24000 Hz sample rate (recommended setting for playback). + +### Response Object Format + +The `zai.audio.tts.create()` method returns a standard **Response** object (not a custom object with an `audio` property). Always use: + +```javascript +// ✅ CORRECT +const response = await zai.audio.tts.create({ ... }); +const arrayBuffer = await response.arrayBuffer(); +const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + +// ❌ WRONG - This will not work +const response = await zai.audio.tts.create({ ... }); +const buffer = Buffer.from(response.audio); // response.audio is undefined +``` + +### Available Voices + +- `tongtong` - 温暖亲切 +- `chuichui` - 活泼可爱 +- `xiaochen` - 沉稳专业 +- `jam` - 英音绅士 +- `kazi` - 清晰标准 +- `douji` - 自然流畅 +- `luodo` - 富有感染力 + +### Speed Range + +- Minimum: `0.5` (half speed) +- Default: `1.0` (normal speed) +- Maximum: `2.0` (double speed) + +**Important**: Speed values outside the range [0.5, 2.0] will result in API errors. + +### Volume Range + +- Minimum: Greater than `0` (exclusive) +- Default: `1.0` (normal volume) +- Maximum: `10` (inclusive) + +**Note**: Volume parameter is optional. When not specified, defaults to 1.0. + +## Remember + +- Always use z-ai-web-dev-sdk in backend code only +- **Input text is limited to 1024 characters maximum** - split longer text into chunks +- **Speed must be between 0.5 and 2.0** - values outside this range will cause errors +- **Volume must be greater than 0 and up to 10** - optional parameter with default 1.0 +- **Streaming only supports PCM format** - use non-streaming for WAV or MP3 output +- The SDK returns a standard Response object - use `await response.arrayBuffer()` +- Convert ArrayBuffer to Buffer using `Buffer.from(new Uint8Array(arrayBuffer))` +- Handle audio buffers properly when saving to files +- Implement error handling for production applications +- Consider caching for frequently generated content +- Clean up old audio files periodically to manage storage diff --git a/skills/TTS/tts.ts b/skills/TTS/tts.ts new file mode 100755 index 0000000..14f6de7 --- /dev/null +++ b/skills/TTS/tts.ts @@ -0,0 +1,25 @@ +import ZAI from "z-ai-web-dev-sdk"; +import fs from "fs"; + +async function main(text: string, outFile: string) { + try { + const zai = await ZAI.create(); + + const response = await zai.audio.tts.create({ + input: text, + voice: "tongtong", + speed: 1.0, + response_format: "wav", + stream: false, + }); + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + fs.writeFileSync(outFile, buffer); + console.log(`TTS audio saved to ${outFile}`); + } catch (err: any) { + console.error("TTS failed:", err?.message || err); + } +} + +main("Hello, world!", "./output.wav"); diff --git a/skills/VLM/LICENSE.txt b/skills/VLM/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/VLM/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/VLM/SKILL.md b/skills/VLM/SKILL.md new file mode 100755 index 0000000..67b995b --- /dev/null +++ b/skills/VLM/SKILL.md @@ -0,0 +1,588 @@ +--- +name: VLM +description: Implement vision-based AI chat capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to analyze images, describe visual content, or create applications that combine image understanding with conversational AI. Supports image URLs and base64 encoded images for multimodal interactions. +license: MIT +--- + +# VLM(Vision Chat) Skill + +This skill guides the implementation of vision chat functionality using the z-ai-web-dev-sdk package, enabling AI models to understand and respond to images combined with text prompts. + +## Skills Path + +**Skill Location**: `{project_path}/skills/VLM` + +this skill is located at above path in your project. + +**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/vlm.ts` for a working example. + +## Overview + +Vision Chat allows you to build applications that can analyze images, extract information from visual content, and answer questions about images through natural language conversation. + +**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code. + +## Prerequisites + +The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below. + +## CLI Usage (For Simple Tasks) + +For simple image analysis tasks, you can use the z-ai CLI instead of writing code. This is ideal for quick image descriptions, testing vision capabilities, or simple automation. + +### Basic Image Analysis + +```bash +# Describe an image from URL +z-ai vision --prompt "What's in this image?" --image "https://example.com/photo.jpg" + +# Using short options +z-ai vision -p "Describe this image" -i "https://example.com/image.png" +``` + +### Analyze Local Images + +```bash +# Analyze a local image file +z-ai vision -p "What objects are in this photo?" -i "./photo.jpg" + +# Save response to file +z-ai vision -p "Describe the scene" -i "./landscape.png" -o description.json +``` + +### Multiple Images + +```bash +# Analyze multiple images at once +z-ai vision \ + -p "Compare these two images" \ + -i "./photo1.jpg" \ + -i "./photo2.jpg" \ + -o comparison.json + +# Multiple images with detailed analysis +z-ai vision \ + --prompt "What are the differences between these images?" \ + --image "https://example.com/before.jpg" \ + --image "https://example.com/after.jpg" +``` + +### With Thinking (Chain of Thought) + +```bash +# Enable thinking for complex visual reasoning +z-ai vision \ + -p "Count the number of people in this image and describe their activities" \ + -i "./crowd.jpg" \ + --thinking \ + -o analysis.json +``` + +### Streaming Output + +```bash +# Stream the vision analysis +z-ai vision -p "Describe this image in detail" -i "./photo.jpg" --stream +``` + +### CLI Parameters + +- `--prompt, -p `: **Required** - Question or instruction about the image(s) +- `--image, -i `: Optional - Image URL or local file path (can be used multiple times) +- `--thinking, -t`: Optional - Enable chain-of-thought reasoning (default: disabled) +- `--output, -o `: Optional - Output file path (JSON format) +- `--stream`: Optional - Stream the response in real-time + +### Supported Image Formats + +- PNG (.png) +- JPEG (.jpg, .jpeg) +- GIF (.gif) +- WebP (.webp) +- BMP (.bmp) + +### When to Use CLI vs SDK + +**Use CLI for:** +- Quick image analysis +- Testing vision model capabilities +- One-off image descriptions +- Simple automation scripts + +**Use SDK for:** +- Multi-turn conversations with images +- Dynamic image analysis in applications +- Batch processing with custom logic +- Production applications with complex workflows + +## Recommended Approach + +For better performance and reliability, use base64 encoding to pass images to the model instead of image URLs. + +## Supported Content Types + +The Vision Chat API supports three types of media content: + +### 1. **image_url** - For Image Files +Use this type for static images (PNG, JPEG, GIF, WebP, etc.) +```typescript +{ + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] +} +``` + +### 2. **video_url** - For Video Files +Use this type for video content (MP4, AVI, MOV, etc.) +```typescript +{ + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'video_url', video_url: { url: videoUrl } } + ] +} +``` + +### 3. **file_url** - For Document Files +Use this type for document files (PDF, DOCX, TXT, etc.) +```typescript +{ + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'file_url', file_url: { url: fileUrl } } + ] +} +``` + +**Note**: You can combine multiple content types in a single message. For example, you can include both text and multiple images, or text with both an image and a document. + +## Basic Vision Chat Implementation + +### Single Image Analysis + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function analyzeImage(imageUrl, question) { + const zai = await ZAI.create(); + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: question + }, + { + type: 'image_url', + image_url: { + url: imageUrl + } + } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} + +// Usage +const result = await analyzeImage( + 'https://example.com/product.jpg', + 'Describe this product in detail' +); +console.log('Analysis:', result); +``` + +### Multiple Images Analysis + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function compareImages(imageUrls, question) { + const zai = await ZAI.create(); + + const content = [ + { + type: 'text', + text: question + }, + ...imageUrls.map(url => ({ + type: 'image_url', + image_url: { url } + })) + ]; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: content + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} + +// Usage +const comparison = await compareImages( + [ + 'https://example.com/before.jpg', + 'https://example.com/after.jpg' + ], + 'Compare these two images and describe the differences' +); +``` + +### Base64 Image Support + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function analyzeLocalImage(imagePath, question) { + const zai = await ZAI.create(); + + // Read image file and convert to base64 + const imageBuffer = fs.readFileSync(imagePath); + const base64Image = imageBuffer.toString('base64'); + const mimeType = imagePath.endsWith('.png') ? 'image/png' : 'image/jpeg'; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: question + }, + { + type: 'image_url', + image_url: { + url: `data:${mimeType};base64,${base64Image}` + } + } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} +``` + +## Advanced Use Cases + +### Conversational Vision Chat + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +class VisionChatSession { + constructor() { + this.messages = []; + } + + async initialize() { + this.zai = await ZAI.create(); + } + + async addImage(imageUrl, initialQuestion) { + this.messages.push({ + role: 'user', + content: [ + { + type: 'text', + text: initialQuestion + }, + { + type: 'image_url', + image_url: { url: imageUrl } + } + ] + }); + + return this.getResponse(); + } + + async followUp(question) { + this.messages.push({ + role: 'user', + content: [ + { + type: 'text', + text: question + } + ] + }); + + return this.getResponse(); + } + + async getResponse() { + const response = await this.zai.chat.completions.createVision({ + messages: this.messages, + thinking: { type: 'disabled' } + }); + + const assistantMessage = response.choices[0]?.message?.content; + + this.messages.push({ + role: 'assistant', + content: assistantMessage + }); + + return assistantMessage; + } +} + +// Usage +const session = new VisionChatSession(); +await session.initialize(); + +const initial = await session.addImage( + 'https://example.com/chart.jpg', + 'What does this chart show?' +); +console.log('Initial analysis:', initial); + +const followup = await session.followUp('What are the key trends?'); +console.log('Follow-up:', followup); +``` + +### Image Classification and Tagging + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function classifyImage(imageUrl) { + const zai = await ZAI.create(); + + const prompt = `Analyze this image and provide: +1. Main subject/category +2. Key objects detected +3. Scene description +4. Suggested tags (comma-separated) + +Format your response as JSON.`; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: prompt + }, + { + type: 'image_url', + image_url: { url: imageUrl } + } + ] + } + ], + thinking: { type: 'disabled' } + }); + + const content = response.choices[0]?.message?.content; + + try { + return JSON.parse(content); + } catch (e) { + return { rawResponse: content }; + } +} +``` + +### OCR and Text Extraction + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function extractText(imageUrl) { + const zai = await ZAI.create(); + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: 'Extract all text from this image. Preserve the layout and formatting as much as possible.' + }, + { + type: 'image_url', + image_url: { url: imageUrl } + } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} +``` + +## Best Practices + +### 1. Image Quality and Size +- Use high-quality images for better analysis results +- Optimize image size to balance quality and processing speed +- Supported formats: JPEG, PNG, WebP + +### 2. Prompt Engineering +- Be specific about what information you need from the image +- Structure complex requests with numbered lists or bullet points +- Provide context about the image type (photo, diagram, chart, etc.) + +### 3. Error Handling +```javascript +async function safeVisionChat(imageUrl, question) { + try { + const zai = await ZAI.create(); + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: question }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return { + success: true, + content: response.choices[0]?.message?.content + }; + } catch (error) { + console.error('Vision chat error:', error); + return { + success: false, + error: error.message + }; + } +} +``` + +### 4. Performance Optimization +- Cache SDK instance creation when processing multiple images +- Use appropriate image formats (JPEG for photos, PNG for diagrams) +- Consider image preprocessing for large batches + +### 5. Security Considerations +- Validate image URLs before processing +- Sanitize user-provided image data +- Implement rate limiting for public-facing APIs +- Never expose SDK credentials in client-side code + +## Common Use Cases + +1. **Product Analysis**: Analyze product images for e-commerce applications +2. **Document Understanding**: Extract information from receipts, invoices, forms +3. **Medical Imaging**: Assist in preliminary analysis (with appropriate disclaimers) +4. **Quality Control**: Detect defects or anomalies in manufacturing +5. **Content Moderation**: Analyze images for policy compliance +6. **Accessibility**: Generate alt text for images automatically +7. **Visual Search**: Understand and categorize images for search functionality + +## Integration Examples + +### Express.js API Endpoint + +```javascript +import express from 'express'; +import ZAI from 'z-ai-web-dev-sdk'; + +const app = express(); +app.use(express.json()); + +let zaiInstance; + +// Initialize SDK once +async function initZAI() { + zaiInstance = await ZAI.create(); +} + +app.post('/api/analyze-image', async (req, res) => { + try { + const { imageUrl, question } = req.body; + + if (!imageUrl || !question) { + return res.status(400).json({ + error: 'imageUrl and question are required' + }); + } + + const response = await zaiInstance.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: question }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + res.json({ + success: true, + analysis: response.choices[0]?.message?.content + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +initZAI().then(() => { + app.listen(3000, () => { + console.log('Vision chat API running on port 3000'); + }); +}); +``` + +## Troubleshooting + +**Issue**: "SDK must be used in backend" +- **Solution**: Ensure z-ai-web-dev-sdk is only imported and used in server-side code + +**Issue**: Image not loading or being analyzed +- **Solution**: Verify the image URL is accessible and returns a valid image format + +**Issue**: Poor analysis quality +- **Solution**: Provide more specific prompts and ensure image quality is sufficient + +**Issue**: Slow response times +- **Solution**: Optimize image size and consider caching frequently analyzed images + +## Remember + +- Always use z-ai-web-dev-sdk in backend code only +- The SDK is already installed - import as shown in examples +- Structure prompts clearly for best results +- Handle errors gracefully in production applications +- Consider user privacy when processing images diff --git a/skills/VLM/scripts/vlm.ts b/skills/VLM/scripts/vlm.ts new file mode 100755 index 0000000..5a9a88f --- /dev/null +++ b/skills/VLM/scripts/vlm.ts @@ -0,0 +1,57 @@ +import ZAI, { VisionMessage } from 'z-ai-web-dev-sdk'; + +async function main(imageUrl: string, prompt: string) { + try { + const zai = await ZAI.create(); + + const messages: VisionMessage[] = [ + { + role: 'assistant', + content: [ + { type: 'text', text: 'Output only text, no markdown.' } + ] + }, + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ]; + + // const messages: VisionMessage[] = [ + // { + // role: 'user', + // content: [ + // { type: 'text', text: prompt }, + // { type: 'video_url', video_url: { url: imageUrl } } + // ] + // } + // ]; + + // const messages: VisionMessage[] = [ + // { + // role: 'user', + // content: [ + // { type: 'text', text: prompt }, + // { type: 'file_url', file_url: { url: imageUrl } } + // ] + // } + // ]; + + const response = await zai.chat.completions.createVision({ + model: 'glm-4.6v', + messages, + thinking: { type: 'disabled' } + }); + + const reply = response.choices?.[0]?.message?.content; + console.log('Vision model reply:'); + console.log(reply ?? JSON.stringify(response, null, 2)); + } catch (err: any) { + console.error('Vision chat failed:', err?.message || err); + } +} + +main("https://cdn.bigmodel.cn/static/logo/register.png", "Please describe this image."); diff --git a/skills/agent-browser/SKILL.md b/skills/agent-browser/SKILL.md new file mode 100755 index 0000000..85d1ac3 --- /dev/null +++ b/skills/agent-browser/SKILL.md @@ -0,0 +1,328 @@ +--- +name: Agent Browser +description: A fast Rust-based headless browser automation CLI with Node.js fallback that enables AI agents to navigate, click, type, and snapshot pages via structured commands. +read_when: + - Automating web interactions + - Extracting structured data from pages + - Filling forms programmatically + - Testing web UIs +metadata: {"clawdbot":{"emoji":"🌐","requires":{"bins":["node","npm"]}}} +allowed-tools: Bash(agent-browser:*) +--- + +# Browser Automation with agent-browser + +## Installation + +### npm recommended + +```bash +npm install -g agent-browser +agent-browser install +agent-browser install --with-deps +``` + +### From Source + +```bash +git clone https://github.com/vercel-labs/agent-browser +cd agent-browser +pnpm install +pnpm build +agent-browser install +``` + +## Quick start + +```bash +agent-browser open # Navigate to page +agent-browser snapshot -i # Get interactive elements with refs +agent-browser click @e1 # Click element by ref +agent-browser fill @e2 "text" # Fill input by ref +agent-browser close # Close browser +``` + +## Core workflow + +1. Navigate: `agent-browser open ` +2. Snapshot: `agent-browser snapshot -i` (returns elements with refs like `@e1`, `@e2`) +3. Interact using refs from the snapshot +4. Re-snapshot after navigation or significant DOM changes + +## Commands + +### Navigation + +```bash +agent-browser open # Navigate to URL +agent-browser back # Go back +agent-browser forward # Go forward +agent-browser reload # Reload page +agent-browser close # Close browser +``` + +### Snapshot (page analysis) + +```bash +agent-browser snapshot # Full accessibility tree +agent-browser snapshot -i # Interactive elements only (recommended) +agent-browser snapshot -c # Compact output +agent-browser snapshot -d 3 # Limit depth to 3 +agent-browser snapshot -s "#main" # Scope to CSS selector +``` + +### Interactions (use @refs from snapshot) + +```bash +agent-browser click @e1 # Click +agent-browser dblclick @e1 # Double-click +agent-browser focus @e1 # Focus element +agent-browser fill @e2 "text" # Clear and type +agent-browser type @e2 "text" # Type without clearing +agent-browser press Enter # Press key +agent-browser press Control+a # Key combination +agent-browser keydown Shift # Hold key down +agent-browser keyup Shift # Release key +agent-browser hover @e1 # Hover +agent-browser check @e1 # Check checkbox +agent-browser uncheck @e1 # Uncheck checkbox +agent-browser select @e1 "value" # Select dropdown +agent-browser scroll down 500 # Scroll page +agent-browser scrollintoview @e1 # Scroll element into view +agent-browser drag @e1 @e2 # Drag and drop +agent-browser upload @e1 file.pdf # Upload files +``` + +### Get information + +```bash +agent-browser get text @e1 # Get element text +agent-browser get html @e1 # Get innerHTML +agent-browser get value @e1 # Get input value +agent-browser get attr @e1 href # Get attribute +agent-browser get title # Get page title +agent-browser get url # Get current URL +agent-browser get count ".item" # Count matching elements +agent-browser get box @e1 # Get bounding box +``` + +### Check state + +```bash +agent-browser is visible @e1 # Check if visible +agent-browser is enabled @e1 # Check if enabled +agent-browser is checked @e1 # Check if checked +``` + +### Screenshots & PDF + +```bash +agent-browser screenshot # Screenshot to stdout +agent-browser screenshot path.png # Save to file +agent-browser screenshot --full # Full page +agent-browser pdf output.pdf # Save as PDF +``` + +### Video recording + +```bash +agent-browser record start ./demo.webm # Start recording (uses current URL + state) +agent-browser click @e1 # Perform actions +agent-browser record stop # Stop and save video +agent-browser record restart ./take2.webm # Stop current + start new recording +``` + +Recording creates a fresh context but preserves cookies/storage from your session. If no URL is provided, it automatically returns to your current page. For smooth demos, explore first, then start recording. + +### Wait + +```bash +agent-browser wait @e1 # Wait for element +agent-browser wait 2000 # Wait milliseconds +agent-browser wait --text "Success" # Wait for text +agent-browser wait --url "/dashboard" # Wait for URL pattern +agent-browser wait --load networkidle # Wait for network idle +agent-browser wait --fn "window.ready" # Wait for JS condition +``` + +### Mouse control + +```bash +agent-browser mouse move 100 200 # Move mouse +agent-browser mouse down left # Press button +agent-browser mouse up left # Release button +agent-browser mouse wheel 100 # Scroll wheel +``` + +### Semantic locators (alternative to refs) + +```bash +agent-browser find role button click --name "Submit" +agent-browser find text "Sign In" click +agent-browser find label "Email" fill "user@test.com" +agent-browser find first ".item" click +agent-browser find nth 2 "a" text +``` + +### Browser settings + +```bash +agent-browser set viewport 1920 1080 # Set viewport size +agent-browser set device "iPhone 14" # Emulate device +agent-browser set geo 37.7749 -122.4194 # Set geolocation +agent-browser set offline on # Toggle offline mode +agent-browser set headers '{"X-Key":"v"}' # Extra HTTP headers +agent-browser set credentials user pass # HTTP basic auth +agent-browser set media dark # Emulate color scheme +``` + +### Cookies & Storage + +```bash +agent-browser cookies # Get all cookies +agent-browser cookies set name value # Set cookie +agent-browser cookies clear # Clear cookies +agent-browser storage local # Get all localStorage +agent-browser storage local key # Get specific key +agent-browser storage local set k v # Set value +agent-browser storage local clear # Clear all +``` + +### Network + +```bash +agent-browser network route # Intercept requests +agent-browser network route --abort # Block requests +agent-browser network route --body '{}' # Mock response +agent-browser network unroute [url] # Remove routes +agent-browser network requests # View tracked requests +agent-browser network requests --filter api # Filter requests +``` + +### Tabs & Windows + +```bash +agent-browser tab # List tabs +agent-browser tab new [url] # New tab +agent-browser tab 2 # Switch to tab +agent-browser tab close # Close tab +agent-browser window new # New window +``` + +### Frames + +```bash +agent-browser frame "#iframe" # Switch to iframe +agent-browser frame main # Back to main frame +``` + +### Dialogs + +```bash +agent-browser dialog accept [text] # Accept dialog +agent-browser dialog dismiss # Dismiss dialog +``` + +### JavaScript + +```bash +agent-browser eval "document.title" # Run JavaScript +``` + +### State management + +```bash +agent-browser state save auth.json # Save session state +agent-browser state load auth.json # Load saved state +``` + +## Example: Form submission + +```bash +agent-browser open https://example.com/form +agent-browser snapshot -i +# Output shows: textbox "Email" [ref=e1], textbox "Password" [ref=e2], button "Submit" [ref=e3] + +agent-browser fill @e1 "user@example.com" +agent-browser fill @e2 "password123" +agent-browser click @e3 +agent-browser wait --load networkidle +agent-browser snapshot -i # Check result +``` + +## Example: Authentication with saved state + +```bash +# Login once +agent-browser open https://app.example.com/login +agent-browser snapshot -i +agent-browser fill @e1 "username" +agent-browser fill @e2 "password" +agent-browser click @e3 +agent-browser wait --url "/dashboard" +agent-browser state save auth.json + +# Later sessions: load saved state +agent-browser state load auth.json +agent-browser open https://app.example.com/dashboard +``` + +## Sessions (parallel browsers) + +```bash +agent-browser --session test1 open site-a.com +agent-browser --session test2 open site-b.com +agent-browser session list +``` + +## JSON output (for parsing) + +Add `--json` for machine-readable output: + +```bash +agent-browser snapshot -i --json +agent-browser get text @e1 --json +``` + +## Debugging + +```bash +agent-browser open example.com --headed # Show browser window +agent-browser console # View console messages +agent-browser console --clear # Clear console +agent-browser errors # View page errors +agent-browser errors --clear # Clear errors +agent-browser highlight @e1 # Highlight element +agent-browser trace start # Start recording trace +agent-browser trace stop trace.zip # Stop and save trace +agent-browser record start ./debug.webm # Record from current page +agent-browser record stop # Save recording +agent-browser --cdp 9222 snapshot # Connect via CDP +``` + +## Troubleshooting + +- If the command is not found on Linux ARM64, use the full path in the bin folder. +- If an element is not found, use snapshot to find the correct ref. +- If the page is not loaded, add a wait command after navigation. +- Use --headed to see the browser window for debugging. + +## Options + +- --session uses an isolated session. +- --json provides JSON output. +- --full takes a full page screenshot. +- --headed shows the browser window. +- --timeout sets the command timeout in milliseconds. +- --cdp connects via Chrome DevTools Protocol. + +## Notes + +- Refs are stable per page load but change on navigation. +- Always snapshot after navigation to get new refs. +- Use fill instead of type for input fields to ensure existing text is cleared. + +## Reporting Issues + +- Skill issues: Open an issue at https://github.com/TheSethRose/Agent-Browser-CLI +- agent-browser CLI issues: Open an issue at https://github.com/vercel-labs/agent-browser diff --git a/skills/ai-news-collectors/SKILL.md b/skills/ai-news-collectors/SKILL.md new file mode 100755 index 0000000..38ac662 --- /dev/null +++ b/skills/ai-news-collectors/SKILL.md @@ -0,0 +1,157 @@ +--- +name: ai-news-collector +description: AI 新闻聚合与热度排序工具。当用户询问 AI 领域最新动态时触发,如:"今天有什么 AI 新闻?""总结一下这周的 AI 动态""最近有什么火的 AI 产品?""AI 圈最近在讨论什么?"。覆盖:新产品发布、研究论文、行业动态、融资新闻、开源项目更新、社区病毒传播现象、AI 工具/Agent 热门项目。输出中文摘要列表,按热度排序,附带原文链接。 +--- + +# AI News Collector + +收集、聚合并按热度排序 AI 领域新闻。 + +## 核心原则 + +**不要只搜"AI news today"。** 泛搜索返回的是 SEO 聚合页和趋势预测文章,会系统性遗漏社区级病毒传播现象(如开源工具爆火、Meme 级事件)。必须用多维度、分层搜索策略。 + +## 工作流程 + +### 1. 多维度分层搜索(最少 8 次,建议 10-12 次) + +按以下 **6 个维度** 依次执行搜索,每个维度至少 1 次: + +#### 维度 A:周报/Newsletter 聚合(最优先 🔑) + +这是信息密度最高的来源,一篇文章可覆盖 10+ 条新闻。 + +``` +搜索词: +- "last week in AI" [当前月份年份] +- "AI weekly roundup" [当前月份年份] +- "the batch AI newsletter" +- site:substack.com AI news [当前月份] +``` + +发现周报后,用 web_fetch 获取全文,从中提取所有新闻线索。 + +#### 维度 B:社区热度/病毒传播(关键维度 🔑) + +捕捉自下而上的社区爆款,这类信息泛搜索几乎无法触达。 + +``` +搜索词: +- "viral AI tool" OR "viral AI agent" +- "AI trending" site:reddit.com OR site:news.ycombinator.com +- "GitHub trending AI" OR "AI open source trending" +- AI buzzing OR "everyone is talking about" AI +- "most popular AI" this week +``` + +#### 维度 C:产品发布与模型更新 + +``` +搜索词: +- "AI model release" OR "LLM launch" [当前月份] +- "AI product launch" [当前月份年份] +- OpenAI OR Anthropic OR Google OR Meta AI announcement +- "大模型 发布" OR "AI 新产品" +``` + +#### 维度 D:融资与商业 + +``` +搜索词: +- "AI startup funding" [当前月份年份] +- "AI acquisition" OR "AI IPO" +- "AI 融资" OR "人工智能投资" +``` + +#### 维度 E:研究突破 + +``` +搜索词: +- "AI breakthrough" OR "AI paper" [当前月份] +- "state of the art" machine learning +- "AI 论文" OR "机器学习突破" +``` + +#### 维度 F:监管与政策 + +``` +搜索词: +- "AI regulation" OR "AI policy" [当前月份年份] +- "AI law" OR "AI governance" +- "AI 监管" OR "人工智能法案" +``` + +### 2. 交叉验证与补漏 + +初轮搜索完成后,检查是否有遗漏: + +- 如果 Newsletter 中提到了某个项目/事件但初轮搜索未覆盖 → 对该项目专项搜索 +- 如果同一事件被 3+ 个不同来源提及 → 大概率是热点,深入搜索获取更多细节 +- 如果中文媒体和英文媒体的热点完全不同 → 两边都要覆盖 + +### 3. 搜索关键词设计原则(反模式清单) + +| ❌ 不要这样搜 | ✅ 应该这样搜 | 原因 | +|---|---|---| +| "AI news today February 2026" | "AI weekly roundup February 2026" | 前者返回聚合页,后者返回策划内容 | +| "AI news today" | "viral AI tool" + "AI model release" 分开搜 | 泛搜无法覆盖社区现象 | +| "artificial intelligence breaking news" | 按维度分类搜索 | 过于宽泛,返回噪音 | +| 搜索词中加具体年月日 | 用 "this week" "today" "latest" | 日期反而会偏向预测/展望文章 | +| 只搜 3 次就开始写 | 至少 8 次,覆盖 6 个维度 | 3 次搜索覆盖率不到 30% | + +### 4. 热度综合判断 + +基于以下信号评估每条新闻热度(1-5 星): + +| 信号 | 权重 | 说明 | +|------|------|------| +| 多家媒体报道同一事件 | ⭐⭐⭐ 高 | 3+ 来源 = 确认热点 | +| 社区病毒传播证据 | ⭐⭐⭐ 高 | GitHub star 暴涨、Twitter 刷屏、HN 首页 | +| 来自权威来源(顶会、大厂官宣) | ⭐⭐⭐ 高 | 但注意大厂 PR 不等于真热点 | +| 实际用户体验分享 | ⭐⭐ 中 | 有人真的在用 > 只是发布了 | +| 技术突破性/影响范围 | ⭐⭐ 中 | | +| 争议性(安全、伦理讨论) | ⭐⭐ 中 | 争议往往说明影响力大 | +| 时效性(越新越热) | ⭐ 中低 | 辅助排序 | + +### 5. 输出格式 + +按热度降序排列,输出 **15-25 条**新闻: + +``` +## 🔥 AI 新闻速递(YYYY-MM-DD) + +### ⭐⭐⭐⭐⭐ 热度最高 + +1. **[新闻标题]** + > 一句话摘要(不超过 50 字) + > 🔗 [来源名称](URL) + +### ⭐⭐⭐⭐ 高热度 + +2. ... + +### ⭐⭐⭐ 中等热度 + +... + +--- +📊 本次共收集 XX 条新闻 | 搜索 XX 次 | 覆盖维度:A/B/C/D/E/F | 更新时间:HH:MM +``` + +### 6. 去重与合并 + +- 同一事件被多家报道时,合并为一条,选择最权威/详细的来源 +- 在摘要中注明"多家媒体报道"以体现热度 +- 改名/更名的项目视为同一事件(如 Clawdbot → Moltbot → OpenClaw) + +## 推荐新闻源 + +详见 [references/sources.md](references/sources.md)。 + +## 注意事项 + +- 优先使用 HTTPS 链接 +- 遇到付费墙/无法访问的内容,标注"需订阅" +- 保持客观,不对新闻内容做主观评价 +- 搜索不足 8 次不要开始输出 +- 如果某个维度搜索结果为空,换关键词再搜一次 diff --git a/skills/ai-news-collectors/_meta.json b/skills/ai-news-collectors/_meta.json new file mode 100755 index 0000000..217dade --- /dev/null +++ b/skills/ai-news-collectors/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7fr165ff9vkkwsqyqrq2nwas80t4ev", + "slug": "ai-news-collectors", + "version": "1.0.0", + "publishedAt": 1770615394344 +} \ No newline at end of file diff --git a/skills/ai-news-collectors/references/sources.md b/skills/ai-news-collectors/references/sources.md new file mode 100755 index 0000000..548fd9a --- /dev/null +++ b/skills/ai-news-collectors/references/sources.md @@ -0,0 +1,128 @@ +# AI 新闻源推荐列表 + +## Newsletter / 周报(信息密度最高 🔑) + +| 来源 | 网址 | 特点 | +|------|------|------| +| Last Week in AI | lastweekin.ai / medium.com/last-week-in-ai | 每周最全面的 AI 新闻汇总 | +| The Batch (Andrew Ng) | deeplearning.ai/the-batch | 权威周报 | +| Import AI (Jack Clark) | importai.net | Anthropic 联创的 AI 周报 | +| Platformer (Casey Newton) | platformer.news | 科技深度分析,覆盖 AI | +| The Neuron | theneurondaily.com | 每日 AI 简报 | +| Ben's Bites | bensbites.com | AI 产品/工具为主 | +| AI Tidbits | aitidbits.substack.com | AI 动态精选 | +| TLDR AI | tldr.tech/ai | 每日 AI 简报 | +| Interconnects | interconnects.ai | 深度技术分析 | + +## 关键 Substack / 独立博客 + +| 来源 | 网址 | 特点 | +|------|------|------| +| Gary Marcus | garymarcus.substack.com | AI 批评性分析,常首发安全/争议话题 | +| Simon Willison | simonwillison.net | LLM 安全、工具生态,社区现象第一手报道 | +| Ethan Mollick | oneusefulthing.substack.com | Wharton 教授,AI 应用洞察 | +| Lenny's Newsletter | lennysnewsletter.com | AI 产品/增长 | +| Understanding AI | understandingai.org | 趋势分析与预测 | +| Nathan Lebenz (Cognitive Revolution) | cognitiverevolution.substack.com | AI 深度访谈 | + +## 国际主流媒体 + +| 来源 | 网址 | 侧重 | +|------|------|------| +| TechCrunch AI | techcrunch.com/category/artificial-intelligence | 产品、融资 | +| The Verge AI | theverge.com/ai-artificial-intelligence | 产品、行业 | +| Ars Technica | arstechnica.com/ai | 深度分析 | +| VentureBeat AI | venturebeat.com/ai | 企业 AI | +| MIT Tech Review | technologyreview.com/artificial-intelligence | 研究、趋势 | +| Wired AI | wired.com/tag/artificial-intelligence | 行业影响 | +| CNBC Tech | cnbc.com/technology | 商业+科技交叉 | +| Scientific American | scientificamerican.com | 科学视角 AI | +| The Information | theinformation.com | 深度报道(付费) | + +## 国内媒体 + +| 来源 | 网址 | 侧重 | +|------|------|------| +| 机器之心 | jiqizhixin.com | 技术、论文 | +| 量子位 | qbitai.com | 产品、行业 | +| 36氪 AI | 36kr.com/information/AI | 融资、产品 | +| InfoQ AI | infoq.cn/topic/AI | 技术实践 | +| 新智元 | xinzhiyuan.com | 行业动态 | +| AI 科技评论 | leiphone.com/category/ai | 技术、产品 | + +## 社区与论坛(捕捉病毒传播 🔑) + +| 来源 | 网址 | 特点 | +|------|------|------| +| Hacker News | news.ycombinator.com | 技术讨论热度,开源项目首发 | +| Reddit r/MachineLearning | reddit.com/r/MachineLearning | 学术前沿 | +| Reddit r/artificial | reddit.com/r/artificial | 综合讨论 | +| Reddit r/LocalLLaMA | reddit.com/r/LocalLLaMA | 本地模型、开源工具热度 | +| Reddit r/singularity | reddit.com/r/singularity | AI 社区热议 | +| Twitter/X | twitter.com | 实时动态,病毒传播首发地 | +| 即刻 AI 圈子 | okjike.com | 国内社区讨论 | +| V2EX | v2ex.com | 开发者视角 | +| 知乎 | zhihu.com | 深度技术讨论 | + +## 安全与批评视角 + +| 来源 | 网址 | 特点 | +|------|------|------| +| Palo Alto Networks Blog | paloaltonetworks.com/blog | AI 安全预警 | +| 1Password Blog | 1password.com/blog | Agent 安全分析 | +| Trail of Bits | blog.trailofbits.com | AI 安全研究 | + +## 学术与研究 + +| 来源 | 网址 | 特点 | +|------|------|------| +| arXiv CS.AI | arxiv.org/list/cs.AI/recent | 最新论文 | +| arXiv CS.LG | arxiv.org/list/cs.LG/recent | 机器学习论文 | +| Papers With Code | paperswithcode.com | 论文+代码 | +| Google AI Blog | ai.googleblog.com | 谷歌研究 | +| OpenAI Blog | openai.com/blog | OpenAI 动态 | +| Anthropic News | anthropic.com/news | Anthropic 动态 | +| DeepMind Blog | deepmind.com/blog | DeepMind 研究 | +| Meta AI | ai.meta.com/blog | Meta 研究 | +| Hugging Face Blog | huggingface.co/blog | 开源生态 | + +## 开源项目追踪 + +| 来源 | 网址 | 特点 | +|------|------|------| +| GitHub Trending | github.com/trending | 热门项目,必查 | +| Product Hunt AI | producthunt.com/topics/artificial-intelligence | 新产品发布 | +| Awesome LLM | github.com/Hannibal046/Awesome-LLM | LLM 资源汇总 | +| Hugging Face Models | huggingface.co/models | 新模型发布 | + +## 搜索关键词矩阵 + +每个维度对应的推荐搜索词: + +**Newsletter/周报**: +- `"last week in AI" [月份 年份]` +- `"AI weekly roundup" [月份 年份]` +- `site:substack.com AI news [月份]` + +**社区病毒传播**: +- `"viral AI tool" OR "viral AI agent"` +- `"AI trending" site:reddit.com` +- `"GitHub trending AI"` +- `AI "everyone is talking about"` + +**产品发布**: +- `"AI model release" OR "LLM launch" [月份]` +- `OpenAI OR Anthropic OR Google announcement` +- `"大模型发布" OR "AI 新产品"` + +**研究突破**: +- `"AI breakthrough" OR "state of the art" [月份]` +- `"AI paper" OR "machine learning research"` + +**融资商业**: +- `"AI startup funding" [月份 年份]` +- `"AI acquisition" OR "AI IPO"` + +**监管政策**: +- `"AI regulation" OR "AI policy" [月份 年份]` +- `"AI law" OR "AI 监管"` diff --git a/skills/aminer-academic-search/SKILL.md b/skills/aminer-academic-search/SKILL.md new file mode 100755 index 0000000..9b133f3 --- /dev/null +++ b/skills/aminer-academic-search/SKILL.md @@ -0,0 +1,157 @@ +--- +name: aminer-academic-search +description: > + ACADEMIC PRIORITY: Activate whenever the user's query involves academic, + scholarly, or research-related topics — papers, citations, scholars, + institutions, venues, patents, research trends, or any "who published what / + where / when" question. Takes precedence over general web search for academic + data needs. Routes through the z-ai gateway's `/v1/functions/invoke` endpoint + to the AMiner Open Platform (27 APIs, 5 workflows). +--- + +# aminer-academic-search + +Wraps 27 AMiner Open Platform APIs through the local gateway. The gateway owns +the `AMINER_API_KEY`; the skill only needs a `.z-ai-config` file to reach the +gateway. + +## When to activate + +Any academic/scholarly query — papers, citations, scholars, institutions, +venues, patents. Covers: +- Scholar full profile (bio, education, honors, papers, patents, projects) +- Paper deep dive (full abstract, keywords, authors, citation chain) +- Multi-condition or semantic paper search +- Institution research capability analysis +- Venue annual monitoring +- Patent deep details (IPC/CPC, assignee, claims) + +## Config + +Script reads `.z-ai-config`, in order: +1. `./.z-ai-config` +2. `~/.z-ai-config` +3. `/etc/.z-ai-config` + +Required fields: `baseUrl`, `apiKey`, `token` (JWT). Optional: `chatId`, +`userId`. + +## Invocation + +```bash +python3 "{baseDir}/scripts/aminer.py" [--flag value …] +``` + +The script POSTs to `${baseUrl}/functions/invoke` with +`Authorization: Bearer ${apiKey}`, `X-Z-AI-From: Z`, `X-Token: ${token}`, +and prints the upstream JSON result (pretty-formatted). + +Run `python3 scripts/aminer.py --help` to list actions, or +`python3 scripts/aminer.py --help` for per-action flags. + +### Examples + +```bash +# Locate paper_id by title +python3 scripts/aminer.py paper_search --title "Attention is all you need" + +# Locate scholar by name + org +python3 scripts/aminer.py person_search --name "Jie Tang" --org "Tsinghua" + +# Full paper details +python3 scripts/aminer.py paper_detail --id 57a4e91aac44365e35c98a1e + +# Natural-language Q&A search +python3 scripts/aminer.py paper_qa_search --query "retrieval-augmented generation" --size 5 + +# Venue papers for a given year (body with JSON list param) +python3 scripts/aminer.py venue_paper_relation --id "53e9ba63b7602d9702f08c5d" --year 2024 --limit 10 +``` + +JSON-typed flags (`--title`, `--ids`, `--keywords`, `--year` on qa_search, etc.) +expect a JSON-encoded value, e.g. `--ids '["abc","def"]'`. + +## 27 APIs (quick reference) + +| Action | Method | Purpose | +|---|---|---| +| paper_search | GET | Locate paper_id by title | +| paper_search_pro | GET | Multi-condition paper search | +| paper_qa_search | POST | Natural-language / topic Q&A | +| paper_info | POST | Batch paper info by IDs | +| paper_detail | GET | Full paper details | +| paper_relation | GET | Citation chain | +| paper_list_by_keywords | GET | Batch thematic retrieval | +| paper_detail_by_condition | GET | Year + venue dimension | +| person_search | POST | Locate person_id | +| person_detail | GET | Scholar bio/education/honors | +| person_figure | GET | Interests + work history | +| person_paper_relation | GET | Scholar's papers | +| person_patent_relation | GET | Scholar's patents | +| person_project | GET | Funded projects | +| org_search | POST | Locate org by name | +| org_detail | POST | Org description/type | +| org_person_relation | GET | Affiliated scholars | +| org_paper_relation | GET | Org papers | +| org_patent_relation | GET | Org patents | +| org_disambiguate | POST | Normalize org string | +| org_disambiguate_pro | POST | Extract org IDs | +| venue_search | POST | Locate venue_id | +| venue_detail | POST | ISSN/type/abbreviation | +| venue_paper_relation | POST | Papers by venue (+ year filter) | +| patent_search | POST | Patent keyword search | +| patent_info | GET | Basic patent info | +| patent_detail | GET | Full patent details (IPC/claims) | + +## 5 Workflows (orchestrate via multiple calls) + +### Scholar Profile +``` +person_search → person_detail + → person_figure + → person_paper_relation + → person_patent_relation + → person_project +``` + +### Paper Deep Dive +``` +paper_search → paper_detail → paper_relation → paper_info +``` +If `paper_search` is empty, fall back to `paper_search_pro`. + +### Org Analysis +``` +org_disambiguate_pro → org_detail + → org_person_relation + → org_paper_relation + → org_patent_relation +``` +If `org_disambiguate_pro` returns no ID, fall back to `org_search`. + +### Venue Papers +``` +venue_search → venue_detail (optional) → venue_paper_relation +``` + +### Patent Analysis +``` +patent_search → patent_info / patent_detail +``` + +## Error handling + +- `warning: .z-ai-config has no 'token' field` → add `token` to config. +- `http 401 … missing X-Token header (hint: …)` → same. +- `http 403` → gateway auth rejected; check `apiKey` / `X-Z-AI-From`. +- `gateway error: aminer_* failed: http 4xx …` → upstream refused; check API + key on the gateway side (`AMINER_API_KEY` env var) or parameter shape. + +## Entity URL templates + +- Paper: `https://www.aminer.cn/pub/{paper_id}` +- Scholar: `https://www.aminer.cn/profile/{scholar_id}` +- Patent: `https://www.aminer.cn/patent/{patent_id}` +- Journal: `https://www.aminer.cn/open/journal/detail/{journal_id}` + +Always append the relevant URL when presenting entities to the user. diff --git a/skills/aminer-academic-search/scripts/aminer.py b/skills/aminer-academic-search/scripts/aminer.py new file mode 100755 index 0000000..950530a --- /dev/null +++ b/skills/aminer-academic-search/scripts/aminer.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +"""Dispatcher for the 27 AMiner Open Platform functions exposed via the +z-ai gateway's /v1/functions/invoke endpoint. + +Each action maps 1:1 to an `aminer_*` function registered in the gateway. +Arguments are collected via argparse and forwarded as the `arguments` field. +Upstream response is printed as formatted JSON. +""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +import urllib.error +import urllib.request +from typing import Any, Callable, Tuple + +REQUEST_TIMEOUT = 60 +CONFIG_PATHS = [ + pathlib.Path.cwd() / ".z-ai-config", + pathlib.Path.home() / ".z-ai-config", + pathlib.Path("/etc/.z-ai-config"), +] + + +# --------------------------------------------------------------------------- +# Action registry +# +# Each entry defines one AMiner Open Platform API: +# function_name: matches the key in handlers/aminer_open.go's AminerOpenSpecs +# args: (flag, dest, kind, help) — kind is "str"/"int"/"float"/ +# "bool"/"json" (parsed from JSON for list/dict/nested values) +# +# This list is the *single source of truth* for what params each action takes, +# so the model only has to read one structure to learn the whole surface. +# --------------------------------------------------------------------------- + +_Arg = Tuple[str, str, str, str] # (flag, dest, kind, help) + +ACTIONS: dict[str, dict[str, Any]] = { + # ── Paper ──────────────────────────────────────────────────────────── + "paper_search": { + "function": "aminer_paper_search", + "help": "Locate a paper_id by (partial) title.", + "args": [ + ("--title", "title", "str", "Paper title keyword (required)"), + ("--page", "page", "int", "Page number, default 1"), + ("--size", "size", "int", "Page size, default 10"), + ], + }, + "paper_search_pro": { + "function": "aminer_paper_search_pro", + "help": "Multi-condition paper search (author/org/venue/keyword).", + "args": [ + ("--title", "title", "str", "Title keyword"), + ("--keyword", "keyword", "str", "Subject keyword"), + ("--abstract", "abstract", "str", "Abstract keyword"), + ("--author", "author", "str", "Author name"), + ("--org", "org", "str", "Organization name"), + ("--venue", "venue", "str", "Venue name"), + ("--order", "order", "str", "Sort: citation | year"), + ("--page", "page", "int", "Page number, default 0"), + ("--size", "size", "int", "Page size, default 10"), + ], + }, + "paper_qa_search": { + "function": "aminer_paper_qa_search", + "help": "AI Q&A-style search. 'query' and 'topic_*' are mutually exclusive.", + "args": [ + ("--query", "query", "str", "Natural language question"), + ("--use-topic", "use_topic", "bool", "Use topic_* fields instead of query"), + ("--topic-high", "topic_high", "str", "High-level topic"), + ("--topic-middle", "topic_middle", "str", "Mid-level topic"), + ("--topic-low", "topic_low", "str", "Low-level topic"), + ("--title", "title", "json", "List of titles (JSON array)"), + ("--doi", "doi", "str", "DOI"), + ("--year", "year", "json", "Year filter, e.g. [2020,2024]"), + ("--sci-flag", "sci_flag", "bool", "Restrict to SCI venues"), + ("--n-citation-flag", "n_citation_flag", "bool", "Return citation count"), + ("--force-citation-sort", "force_citation_sort", "bool", "Sort by citations"), + ("--force-year-sort", "force_year_sort", "bool", "Sort by year"), + ("--author-terms", "author_terms", "json", "Author name list"), + ("--org-terms", "org_terms", "json", "Org name list"), + ("--author-id", "author_id", "json", "Author ID list"), + ("--org-id", "org_id", "json", "Org ID list"), + ("--venue-ids", "venue_ids", "json", "Venue ID list"), + ("--size", "size", "int", "Page size, default 10"), + ("--offset", "offset", "int", "Offset, default 0"), + ], + }, + "paper_info": { + "function": "aminer_paper_info", + "help": "Batch retrieve papers by ID list.", + "args": [ + ("--ids", "ids", "json", "JSON array of paper_id strings (required)"), + ], + }, + "paper_detail": { + "function": "aminer_paper_detail", + "help": "Full paper info for a single paper_id.", + "args": [ + ("--id", "id", "str", "paper_id (required)"), + ], + }, + "paper_relation": { + "function": "aminer_paper_relation", + "help": "Citation chain (cited papers) for a paper_id.", + "args": [ + ("--id", "id", "str", "paper_id (required)"), + ], + }, + "paper_list_by_keywords": { + "function": "aminer_paper_list_by_keywords", + "help": "Batch keyword retrieval returning abstracts + metadata.", + "args": [ + ("--keywords", "keywords", "json", "JSON array of keyword strings (required)"), + ("--page", "page", "int", "Page number, default 0"), + ("--size", "size", "int", "Page size, default 10"), + ], + }, + "paper_detail_by_condition": { + "function": "aminer_paper_detail_by_condition", + "help": "Year + venue dimension lookup. Year + venue_id both required.", + "args": [ + ("--year", "year", "int", "Year (required)"), + ("--venue-id", "venue_id", "str", "Venue ID (required)"), + ], + }, + # ── Scholar ────────────────────────────────────────────────────────── + "person_search": { + "function": "aminer_person_search", + "help": "Search scholars by name and/or org.", + "args": [ + ("--name", "name", "str", "Scholar name"), + ("--org", "org", "str", "Organization name"), + ("--org-id", "org_id", "json", "List of org IDs"), + ("--offset", "offset", "int", "Offset, default 0"), + ("--size", "size", "int", "Page size, default 5"), + ], + }, + "person_detail": { + "function": "aminer_person_detail", + "help": "Full scholar profile (bio/education/honors).", + "args": [("--id", "id", "str", "person_id (required)")], + }, + "person_figure": { + "function": "aminer_person_figure", + "help": "Scholar portrait (interests, work history).", + "args": [("--id", "id", "str", "person_id (required)")], + }, + "person_paper_relation": { + "function": "aminer_person_paper_relation", + "help": "List of papers by this scholar.", + "args": [("--id", "id", "str", "person_id (required)")], + }, + "person_patent_relation": { + "function": "aminer_person_patent_relation", + "help": "List of patents by this scholar.", + "args": [("--id", "id", "str", "person_id (required)")], + }, + "person_project": { + "function": "aminer_person_project", + "help": "Research projects (funding, dates, source).", + "args": [("--id", "id", "str", "person_id (required)")], + }, + # ── Organization ───────────────────────────────────────────────────── + "org_search": { + "function": "aminer_org_search", + "help": "Search institutions by name keyword.", + "args": [("--orgs", "orgs", "json", "JSON array of org name strings (required)")], + }, + "org_detail": { + "function": "aminer_org_detail", + "help": "Org details by ID list.", + "args": [("--ids", "ids", "json", "JSON array of org_id strings (required)")], + }, + "org_person_relation": { + "function": "aminer_org_person_relation", + "help": "Affiliated scholars (10 per call).", + "args": [ + ("--org-id", "org_id", "str", "org_id (required)"), + ("--offset", "offset", "int", "Offset, default 0"), + ], + }, + "org_paper_relation": { + "function": "aminer_org_paper_relation", + "help": "Papers authored by org members (10 per call).", + "args": [ + ("--org-id", "org_id", "str", "org_id (required)"), + ("--offset", "offset", "int", "Offset, default 0"), + ], + }, + "org_patent_relation": { + "function": "aminer_org_patent_relation", + "help": "Org patent list (max page_size 10,000).", + "args": [ + ("--id", "id", "str", "org_id (required)"), + ("--page", "page", "int", "Page number, default 1"), + ("--page-size", "page_size", "int", "Page size, default 100"), + ], + }, + "org_disambiguate": { + "function": "aminer_org_disambiguate", + "help": "Normalize raw org string.", + "args": [("--org", "org", "str", "Raw org string (required)")], + }, + "org_disambiguate_pro": { + "function": "aminer_org_disambiguate_pro", + "help": "Extract primary/secondary org IDs.", + "args": [("--org", "org", "str", "Raw org string (required)")], + }, + # ── Venue ──────────────────────────────────────────────────────────── + "venue_search": { + "function": "aminer_venue_search", + "help": "Search journals/conferences by name.", + "args": [("--name", "name", "str", "Venue name (required)")], + }, + "venue_detail": { + "function": "aminer_venue_detail", + "help": "Venue details (ISSN, abbreviation, type).", + "args": [("--id", "id", "str", "venue_id (required)")], + }, + "venue_paper_relation": { + "function": "aminer_venue_paper_relation", + "help": "Papers published in a venue, optionally filtered by year.", + "args": [ + ("--id", "id", "str", "venue_id (required)"), + ("--offset", "offset", "int", "Offset, default 0"), + ("--limit", "limit", "int", "Limit, default 20"), + ("--year", "year", "int", "Publication year"), + ], + }, + # ── Patent ─────────────────────────────────────────────────────────── + "patent_search": { + "function": "aminer_patent_search", + "help": "Search patents by name/keyword.", + "args": [ + ("--query", "query", "str", "Search query (required)"), + ("--page", "page", "int", "Page number, default 0"), + ("--size", "size", "int", "Page size, default 10"), + ], + }, + "patent_info": { + "function": "aminer_patent_info", + "help": "Basic patent info.", + "args": [("--id", "id", "str", "patent_id (required)")], + }, + "patent_detail": { + "function": "aminer_patent_detail", + "help": "Full patent details (abstract, IPC, claims).", + "args": [("--id", "id", "str", "patent_id (required)")], + }, +} + + +# --------------------------------------------------------------------------- +# Config loading and HTTP +# --------------------------------------------------------------------------- + + +def load_config() -> dict[str, Any]: + for path in CONFIG_PATHS: + try: + cfg = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError: + continue + except (OSError, json.JSONDecodeError) as exc: + raise SystemExit(f"failed to read {path}: {exc}") from exc + if cfg.get("baseUrl") and cfg.get("apiKey"): + if not cfg.get("token"): + print( + f"warning: {path} has no 'token' field; the gateway requires " + "X-Token for auth and will return 401.", + file=sys.stderr, + ) + return cfg + raise SystemExit( + "z-ai config not found. Create .z-ai-config in cwd, $HOME, or /etc " + "with {\"baseUrl\": ..., \"apiKey\": ..., \"token\": ...}." + ) + + +def invoke(config: dict[str, Any], function_name: str, arguments: dict[str, Any]) -> Any: + base_url = str(config["baseUrl"]).rstrip("/") + url = f"{base_url}/functions/invoke" + headers = { + "Content-Type": "application/json", + "User-Agent": "aminer-academic-search-skill/1.0", + "Authorization": f"Bearer {config['apiKey']}", + "X-Z-AI-From": "Z", + } + if config.get("token"): + headers["X-Token"] = str(config["token"]) + if config.get("chatId"): + headers["X-Chat-Id"] = str(config["chatId"]) + if config.get("userId"): + headers["X-User-Id"] = str(config["userId"]) + + body = json.dumps( + {"function_name": function_name, "arguments": arguments}, + ensure_ascii=False, + ).encode("utf-8") + req = urllib.request.Request(url, data=body, headers=headers, method="POST") + + try: + with urllib.request.urlopen(req, timeout=REQUEST_TIMEOUT) as resp: # nosec B310 + raw = resp.read().decode("utf-8") + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") if exc.fp else "" + hint = "" + if exc.code == 401 and "X-Token" in detail: + hint = " (hint: add a valid 'token' field to .z-ai-config)" + elif exc.code == 403: + hint = " (hint: request rejected by auth — check 'apiKey' / X-Z-AI-From)" + raise SystemExit(f"http {exc.code}: {detail or exc.reason}{hint}") from exc + except urllib.error.URLError as exc: + raise SystemExit(f"gateway unreachable: {exc.reason}") from exc + + try: + parsed = json.loads(raw) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid json response: {exc}") from exc + + if isinstance(parsed.get("error"), str) and parsed["error"]: + raise SystemExit(f"gateway error: {parsed['error']}") + return parsed.get("result") + + +# --------------------------------------------------------------------------- +# Argparse wiring +# --------------------------------------------------------------------------- + + +def _cast(kind: str) -> Callable[[str], Any]: + if kind == "int": + return int + if kind == "float": + return float + if kind == "json": + return lambda s: json.loads(s) + return str + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Call one of 27 AMiner Open Platform APIs via the z-ai gateway.", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + sub = parser.add_subparsers(dest="action", required=True, metavar="ACTION") + for name, meta in ACTIONS.items(): + p = sub.add_parser( + name, + help=meta["help"], + description=f"{meta['help']} function_name: {meta['function']}", + ) + for flag, dest, kind, help_text in meta["args"]: + if kind == "bool": + p.add_argument(flag, dest=dest, action="store_true", help=help_text) + else: + p.add_argument(flag, dest=dest, type=_cast(kind), help=help_text) + return parser + + +def collect_arguments(ns: argparse.Namespace, spec: list[_Arg]) -> dict[str, Any]: + args: dict[str, Any] = {} + for _flag, dest, kind, _help in spec: + value = getattr(ns, dest, None) + if value is None: + continue + if kind == "bool" and value is False: + continue + args[dest] = value + return args + + +def main() -> int: + parser = build_parser() + ns = parser.parse_args() + + meta = ACTIONS[ns.action] + arguments = collect_arguments(ns, meta["args"]) + + config = load_config() + result = invoke(config, meta["function"], arguments) + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/aminer-daily-paper/README.md b/skills/aminer-daily-paper/README.md new file mode 100755 index 0000000..6cb1b07 --- /dev/null +++ b/skills/aminer-daily-paper/README.md @@ -0,0 +1,27 @@ +# aminer-daily-paper + +Skill for AMiner academic paper recommendations. + +## Configuration + +```bash +# TODO: fill in the actual API URL +export AMINER_API_URL="https:///..." +``` + +## Usage + +Natural language or explicit command: + +``` +/aminer-dp topics: multimodal agents, tool-use size: 5 +recommend me recent papers on RAG +``` + +## Manual invocation + +```bash +python3 scripts/recommend.py --topic "RAG" --topic "multimodal" --size 5 --language-sort zh +``` + +Output is a Markdown-formatted paper list. diff --git a/skills/aminer-daily-paper/SKILL.md b/skills/aminer-daily-paper/SKILL.md new file mode 100755 index 0000000..f0caef6 --- /dev/null +++ b/skills/aminer-daily-paper/SKILL.md @@ -0,0 +1,75 @@ +--- +name: aminer-daily-paper +description: "Get personalized academic paper recommendations. Activate whenever the user asks for paper recommendations — explicit command (/aminer-dp) or natural language (e.g. 'recommend me papers on RAG', 'suggest recent papers on multimodal agents'). Workflow: extract topics / author / aminer_author_id from the input, invoke scripts/recommend.py, return results as Markdown." +--- + +# aminer-daily-paper + +Personalized academic paper recommendation. + +## When to activate + +Any time the user asks for paper recommendations: +- Explicit command: `/aminer-dp`, `/aminer-dp topics: RAG, multimodal agents` +- Natural language: `recommend me recent papers on multimodal agents`, `suggest some papers on tool-use`, `I work on RAG, give me a few papers` + +## Input parsing (done by the model) + +Before calling the script, extract from the user input: + +| Field | Description | +|-------|-------------| +| `topics` | Research topics, 1–3 closely related terms work best | +| `author_name` | Scholar name | +| `author_org` | Scholar institution (improves disambiguation) | +| `aminer_author_id` | AMiner scholar ID (24-char hex) | +| `size` | Number of papers, default 5, max 20 | +| `language_sort` | `zh` or `en`, optional | + +At least one of `topics` / `author_name` / `aminer_author_id` should be provided. + +## Call strategy + +| Scenario | Strategy | +|----------|----------| +| Single topic or scholar | 1 call, `size=5` | +| User specifies a number | 1 call, honor the number (max 20) | +| Multiple distinct topics | 1 call per topic group, `size=3–5` each, ~15 papers total | +| Broad request with no topics | 1 call, `size=5` | + +## Execution + +The script reads `.z-ai-config` (JSON) following the `z-ai-web-dev-sdk` +convention, searching in this order: +1. `./.z-ai-config` (cwd) +2. `~/.z-ai-config` (home) +3. `/etc/.z-ai-config` (system) + +Required fields: `baseUrl`, `apiKey`, `token` (JWT for `X-Token`). +Optional: `chatId`, `userId`. + +```bash +python3 "{baseDir}/scripts/recommend.py" \ + [--topic "multimodal agents"] \ + [--topic "tool-use"] \ + [--author-name "Jie Tang"] \ + [--author-org "Tsinghua University"] \ + [--aminer-author-id "696259801cb939bc391d3a37"] \ + [--size 5] \ + [--language-sort zh] +``` + +The script POSTs to `${baseUrl}/functions/invoke` with headers +`Authorization: Bearer ${apiKey}`, `X-Z-AI-From: Z`, and `X-Token: ${token}` +(plus optional `X-Chat-Id` / `X-User-Id`), and prints the paper list as +Markdown. + +## Output handling + +- On success, the script prints a Markdown paper list — forward it directly to the user. +- On non-zero exit, stderr contains the error — relay it concisely to the user. + +## Error handling + +- API returns an error → relay the error; do not switch to another skill. +- No results → suggest the user broaden topics or adjust the query. diff --git a/skills/aminer-daily-paper/scripts/recommend.py b/skills/aminer-daily-paper/scripts/recommend.py new file mode 100755 index 0000000..6fcd77e --- /dev/null +++ b/skills/aminer-daily-paper/scripts/recommend.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +"""Fetch AMiner paper recommendations and render results as Markdown.""" +from __future__ import annotations + +import argparse +import json +import pathlib +import sys +import urllib.error +import urllib.request +from typing import Any + + +DEFAULT_SIZE = 5 +MAX_SIZE = 20 +REQUEST_TIMEOUT = 30 +CONFIG_PATHS = [ + pathlib.Path.cwd() / ".z-ai-config", + pathlib.Path.home() / ".z-ai-config", + pathlib.Path("/etc/.z-ai-config"), +] + + +def load_config() -> dict[str, Any]: + for path in CONFIG_PATHS: + try: + cfg = json.loads(path.read_text(encoding="utf-8")) + except FileNotFoundError: + continue + except (OSError, json.JSONDecodeError) as exc: + raise SystemExit(f"failed to read {path}: {exc}") from exc + if cfg.get("baseUrl") and cfg.get("apiKey"): + if not cfg.get("token"): + print( + f"warning: {path} has no 'token' field; the API requires " + "X-Token for auth and will return 401.", + file=sys.stderr, + ) + return cfg + raise SystemExit( + "z-ai config not found. Create .z-ai-config in cwd, $HOME, or /etc " + "with {\"baseUrl\": ..., \"apiKey\": ..., \"token\": ...}." + ) + + +def _clean(text: Any) -> str: + return " ".join(str(text or "").split()).strip() + + +def _truncate(text: str, limit: int) -> str: + text = _clean(text) + return text if len(text) <= limit else text[:limit].rstrip() + "…" + + +def build_payload(args: argparse.Namespace) -> dict[str, Any]: + arguments: dict[str, Any] = {} + topics = [t for t in (args.topic or []) if _clean(t)] + if topics: + arguments["topics"] = topics + if args.author_name: + arguments["author_name"] = args.author_name + if args.author_org: + arguments["author_org"] = args.author_org + if args.aminer_author_id: + arguments["aminer_author_id"] = args.aminer_author_id + if args.language_sort in {"zh", "en"}: + arguments["language_sort"] = args.language_sort + if args.start_year: + arguments["start_year"] = args.start_year + if args.end_year: + arguments["end_year"] = args.end_year + + size = args.size if args.size else DEFAULT_SIZE + size = max(1, min(size, MAX_SIZE)) + arguments["size"] = size + + return {"function_name": "aminer_recommend", "arguments": arguments} + + +def call_api(config: dict[str, Any], payload: dict[str, Any]) -> list[dict[str, Any]]: + base_url = str(config["baseUrl"]).rstrip("/") + url = f"{base_url}/functions/invoke" + headers = { + "Content-Type": "application/json", + "User-Agent": "aminer-daily-paper-skill/1.0", + "Authorization": f"Bearer {config['apiKey']}", + "X-Z-AI-From": "Z", + } + if config.get("token"): + headers["X-Token"] = str(config["token"]) + if config.get("chatId"): + headers["X-Chat-Id"] = str(config["chatId"]) + if config.get("userId"): + headers["X-User-Id"] = str(config["userId"]) + + body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + request = urllib.request.Request(url, data=body, headers=headers, method="POST") + + try: + with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as resp: # nosec B310 + raw = resp.read().decode("utf-8") + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") if exc.fp else "" + hint = "" + if exc.code == 401 and "X-Token" in detail: + hint = " (hint: add a valid 'token' field to .z-ai-config)" + elif exc.code == 403: + hint = " (hint: request rejected by auth — check 'apiKey' / X-Z-AI-From)" + raise SystemExit(f"http {exc.code}: {detail or exc.reason}{hint}") from exc + except urllib.error.URLError as exc: + raise SystemExit(f"api unreachable: {exc.reason}") from exc + + try: + parsed = json.loads(raw) + except json.JSONDecodeError as exc: + raise SystemExit(f"invalid json response: {exc}") from exc + + if isinstance(parsed.get("error"), str) and parsed["error"]: + raise SystemExit(f"api error: {parsed['error']}") + + result = parsed.get("result") + if not isinstance(result, list): + raise SystemExit(f"unexpected response shape: {raw[:300]}") + return [p for p in result if isinstance(p, dict)] + + +def render_markdown(papers: list[dict[str, Any]], topics: list[str]) -> str: + if not papers: + return "No papers returned. Try broadening the topics or adjusting the query." + + lines: list[str] = [] + header = f"Recommended {len(papers)} paper(s)" + if topics: + header += f" (topics: {' / '.join(topics[:5])})" + lines.append(header) + + for idx, paper in enumerate(papers, start=1): + lines.append("") + lines.append("---") + lines.append("") + title = _clean(paper.get("title")) + links = paper.get("links") if isinstance(paper.get("links"), dict) else {} + url = _clean(paper.get("paper_url") or links.get("aminer") or links.get("arxiv")) + title_line = f"**{idx}. [{title}]({url})**" if url else f"**{idx}. {title}**" + lines.append(title_line) + + meta: list[str] = [] + year = paper.get("year") + if year: + meta.append(f"Year: {year}") + keywords = paper.get("keywords") or [] + if keywords: + meta.append("Keywords: " + " / ".join(str(k) for k in keywords[:5])) + if meta: + lines.append(" | ".join(meta)) + + authors = paper.get("authors") or [] + if authors: + author_str = ", ".join(str(a) for a in authors[:6]) + if len(authors) > 6: + author_str += " et al." + lines.append(f"Authors: {author_str}") + + summary = _clean(paper.get("summary")) + if summary: + lines.append("") + lines.append(_truncate(summary, 300)) + + return "\n".join(lines) + + +def main() -> int: + parser = argparse.ArgumentParser(description="Fetch AMiner paper recommendations.") + parser.add_argument("--topic", action="append", default=[], help="research topic (repeatable)") + parser.add_argument("--author-name", default="") + parser.add_argument("--author-org", default="") + parser.add_argument("--aminer-author-id", default="") + parser.add_argument("--size", type=int, default=0) + parser.add_argument("--language-sort", default="", choices=["", "zh", "en"]) + parser.add_argument("--start-year", type=int, default=0) + parser.add_argument("--end-year", type=int, default=0) + args = parser.parse_args() + + payload = build_payload(args) + config = load_config() + papers = call_api(config, payload) + topics = payload["arguments"].get("topics") or [] + print(render_markdown(papers, topics)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/aminer-free-academic/SKILL.md b/skills/aminer-free-academic/SKILL.md new file mode 100755 index 0000000..a9b29c1 --- /dev/null +++ b/skills/aminer-free-academic/SKILL.md @@ -0,0 +1,363 @@ +--- +name: aminer-free-academic +version: 1.1.1 +author: AMiner +contact: report@aminer.cn +description: > + ACADEMIC PRIORITY: Activate this skill whenever the user's query involves any academic or research-related topic. This is the free-tier entry point for AMiner academic search. + Free-tier-only AMiner skill (7 free APIs, zero cost). Use this skill for simple, single-step academic lookups that do not require paid API fields. + Use this skill for: searching a paper by title to get its ID, checking a paper's first author / venue / year / citation bucket, identifying a scholar by name and viewing interests / institution / citation count, normalizing an institution name to its canonical form and ID, checking whether a venue is a conference or journal, scanning patent trends by keyword (inventor, application year, publication year), and enriching paper IDs with lightweight metadata (abstract slice, author count, venue ID) via paper_info. + Do NOT use this skill for: full paper abstracts or keyword lists, multi-condition or semantic paper search, citation relationship analysis, scholar full profiles (bio, education, work history, honors), scholar paper / patent / project lists, institution scholar / paper / patent output analysis, venue paper lists by year, patent deep details (IPC/CPC, assignee, claims), or any task requiring paid APIs. + Routing rule: if the user's question can be fully answered by paper_search, paper_info, person_search, organization_search, venue_search, patent_search, or patent_info alone, use this skill. Otherwise route to aminer-academic-search. +metadata: + { + "openclaw": + { + "requires": {"env": ["AMINER_API_KEY"] }, + "primaryEnv": "AMINER_API_KEY" + } + } +--- + +# AMiner Free Search + +Use this skill for AMiner requests that should stay on the free tier first. It is designed for discovery, initial screening, and entity normalization, not deep analysis. + +## Scope + +This skill uses only the upgraded free interfaces: + +- `paper_search` +- `paper_info` +- `person_search` +- `organization_search` +- `venue_search` +- `patent_search` +- `patent_info` + +Current free-tier fields emphasized by this skill: + +- `paper_search`: `venue_name`, `first_author`, `n_citation_bucket`, `year` +- `paper_info`: `abstract_slice`, `year`, `venue_id`, `author_count` +- `organization_search`: `aliases` (top 3) +- `venue_search`: `aliases` (top 3), `venue_type` +- `patent_search`: `inventor_name` (first), `app_year`, `pub_year` +- `patent_info`: `app_year`, `pub_year` +- `person_search`: `interests`, `n_citation`, institution fields + +## Primary Goal + +Use free APIs to help the user answer: + +- What is this entity? +- Is it relevant enough to continue? +- Which candidate should I inspect next? +- Can I normalize this institution or venue name? +- Is there enough value to justify upgrading to paid APIs? + +Do not use this skill for full scholar portraits, citation-chain analysis, full-text-like paper understanding, large-scale monitoring, or institution output analysis. + +## Mandatory Rules + +1. Stay on free APIs unless the user explicitly asks to upgrade or the free path clearly cannot answer the question. +2. Be explicit about free-tier limits. Say what can be answered now and what would require a paid upgrade. +3. Use free results to narrow candidates before suggesting any paid API. +4. If returning entities, append AMiner URLs when IDs are available: + - Paper: `https://www.aminer.cn/pub/{paper_id}` + - Scholar: `https://www.aminer.cn/profile/{scholar_id}` + - Patent: `https://www.aminer.cn/patent/{patent_id}` + - Venue: `https://www.aminer.cn/open/journal/detail/{venue_id}` + +## Token Check (Required) + +Before making any API call, verify that the environment variable `AMINER_API_KEY` exists. Never output the token in plain text. + +```bash +if [ -z "${AMINER_API_KEY+x}" ]; then + echo "AMINER_API_KEY does not exist" +else + echo "AMINER_API_KEY exists" +fi +``` + +- If `${AMINER_API_KEY}` exists: proceed with the query. +- If `${AMINER_API_KEY}` is not set: stop immediately and guide the user to the [AMiner Console](https://open.aminer.cn/open/board?tab=control) to generate one. For help, see the [Open Platform Documentation](https://open.aminer.cn/open/docs). +- If the user provides `AMINER_API_KEY` inline (e.g. "My token is xxx"), accept it for the current session, but recommend setting it as an environment variable for better security. + +## Invocation Style + +Use direct `curl` calls by default. A Python wrapper is not required for this skill. + +Default headers: + +- `Authorization: ${AMINER_API_KEY}` by default +- `Content-Type: application/json;charset=utf-8` for POST requests +- `X-Platform: openclaw` when required by the gateway + +## When To Use + +Use this skill when the user asks for: + +- free AMiner search +- low-cost academic discovery +- paper screening +- scholar identification +- institution normalization +- venue normalization +- patent trend scanning +- representative results before deeper analysis + +Trigger phrases include: + +- “先用免费接口” +- “不要走收费接口” +- “先帮我筛一下” +- “先看看值不值得深挖” +- “找几个候选” +- “做一个轻量版 skill” + +## Free Workflows + +### 1. Paper triage + +Use when the user wants to quickly judge whether a paper is relevant. + +Default chain: + +`paper_search -> paper_info` + +Return: + +- title +- first author +- venue name +- year +- citation bucket +- abstract slice +- paper URL + +This can answer: + +- Is this probably the right paper? +- Is it recent? +- Is it from a recognizable venue? +- Is it worth opening in detail? + +### 2. Scholar identification + +Use when the user wants to know which scholar is the right person. + +Default chain: + +`person_search` + +Return: + +- name +- org +- interests +- citation count +- scholar URL + +This can answer: + +- Is this the right scholar? +- What interests best describe this person? +- Which institution candidate is the best match? + +### 3. Institution normalization + +Use when the user provides an institution string or abbreviation. + +Default chain: + +`organization_search` + +Return: + +- org id +- standard name +- aliases (top 3) + +This can answer: + +- Is this institution name recognized? +- Which canonical organization should downstream workflows use? + +### 4. Venue normalization and type check + +Use when the user provides a conference or journal name. + +Default chain: + +`venue_search` + +Return: + +- venue id +- standard bilingual name +- aliases (top 3) +- venue type +- venue URL + +This can answer: + +- Is this a conference or a journal? +- What is the standard venue entity? + +### 5. Patent trend scan + +Use when the user wants a lightweight view of patents in a topic. + +Default chain: + +`patent_search -> patent_info` when IDs need basic enrichment + +Return: + +- patent title +- first inventor +- app year +- pub year +- patent number and country when `patent_info` is added +- patent URL + +This can answer: + +- Is the topic active recently? +- Who appears first in the inventor field? +- Is there recent patent activity worth deeper review? + +### 6. Free entity map + +Use when the user wants a quick map of a topic across papers, scholars, venues, institutions, and patents without paying for analysis-grade APIs. + +Suggested chain: + +- papers: `paper_search -> paper_info` +- scholars: `person_search` +- institutions: `organization_search` +- venues: `venue_search` +- patents: `patent_search -> patent_info` + +Return a short cross-entity summary, not a deep report. + +## Free Skill Examples + +### 1. Paper triage + +```bash +curl -X GET \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/paper/search?page=1&size=5&title=Attention%20Is%20All%20You%20Need' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' +``` + +Then enrich with `paper_info`: + +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/paper/info' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"ids":[""]}' +``` + +### 2. Scholar identification + +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/person/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"name":"Yann LeCun","size":5}' +``` + +### 3. Institution normalization + +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/organization/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"orgs":["MIT CSAIL"]}' +``` + +### 4. Venue normalization and type check + +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/venue/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"name":"tkde"}' +``` + +### 5. Patent trend scan + +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/patent/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"query":"quantum computing chip","page":0,"size":10}' +``` + +## Output Pattern + +Prefer this structure: + +```markdown +## Free-tier result + +### What we can answer now +- ... + +### Top candidates +- ... + +### Suggested next step +- Stay free: ... +- Upgrade to paid API only if you need: ... +``` + +## Paid Upgrade Boundary + +Recommend upgrading only when the user needs one of these: + +- full abstract or full paper metadata +- multi-condition or semantic paper search +- citation relationships +- full scholar profile, works, patents, or projects +- institution scholars, papers, patents, or rich profiles +- venue paper lists by year +- full patent details such as IPC/CPC, assignee, description + +Suggested paid handoff: + +- deeper paper analysis: `paper_search_pro`, `paper_detail`, `paper_relation` +- deeper scholar analysis: `person/detail`, `person/figure`, `person/paper/relation` +- deeper org analysis: `organization/detail`, `organization/person/relation`, `organization/paper/relation` +- deeper venue analysis: `venue/detail`, `venue/paper/relation` +- deeper patent analysis: `patent/detail` + +## Product Positioning + +This skill is intentionally positioned for: + +- first success +- free discovery +- candidate narrowing +- entity normalization +- upgrade qualification + +It should not replace the paid skill. It should create demand for it. + +## Additional Reference + +For endpoint parameters and fields, read [references/api-catalog.md](references/api-catalog.md). diff --git a/skills/aminer-free-academic/evals/evals.json b/skills/aminer-free-academic/evals/evals.json new file mode 100755 index 0000000..28bfb88 --- /dev/null +++ b/skills/aminer-free-academic/evals/evals.json @@ -0,0 +1,93 @@ +{ + "skill_name": "aminer-free-academic", + "evals": [ + { + "id": 1, + "prompt": "Help me quickly check whether the paper 'Attention Is All You Need' is worth a deep dive. Use only free AMiner APIs. My token is ", + "expected_output": "Run the paper triage workflow: paper_search to locate the paper, then paper_info to enrich with abstract slice, year, venue, and author count. Output a lightweight card with a paper URL.", + "files": [], + "expectations": [ + "Called Paper Search API (paper_search) to find the paper by title", + "Called Paper Info API (paper_info) with the returned paper ID", + "Output includes title, first_author, venue_name, year, and n_citation_bucket", + "Output includes abstract_slice from paper_info", + "Output includes a paper URL in the format https://www.aminer.cn/pub/{paper_id}", + "No paid API was called" + ] + }, + { + "id": 2, + "prompt": "I want to identify which 'Andrew Ng' on AMiner is the Stanford professor. Use only free APIs. My token is ", + "expected_output": "Run the scholar identification workflow: person_search with name and optional org filter. Return candidate list with interests, org, and citation count to help the user pick the right one.", + "files": [], + "expectations": [ + "Called Scholar Search API (person_search) with name 'Andrew Ng'", + "Output includes multiple candidates with name, org, interests, and n_citation", + "Output includes scholar URLs in the format https://www.aminer.cn/profile/{scholar_id}", + "No paid API was called" + ] + }, + { + "id": 3, + "prompt": "Normalize the institution name 'MIT CSAIL' using AMiner free APIs. My token is ", + "expected_output": "Run the institution normalization workflow: organization_search with the raw institution string. Return the canonical org name, org_id, and aliases.", + "files": [], + "expectations": [ + "Called Org Search API (organization_search) with orgs=['MIT CSAIL']", + "Output includes org_id and standardized org_name", + "Output includes aliases list", + "No paid API was called" + ] + }, + { + "id": 4, + "prompt": "Help me look up recent patents about quantum computing", + "expected_output": "Detect that the user has not provided a token; immediately stop all subsequent API calls and guide the user to obtain a token from the AMiner Console.", + "files": [], + "expectations": [ + "Clearly inform the user that a token is currently missing and AMiner API calls cannot continue", + "No API calls were made", + "Provide the console link https://open.aminer.cn/open/board?tab=control", + "Prompt the user to continue after obtaining a token" + ] + }, + { + "id": 5, + "prompt": "Is 'TKDE' a conference or a journal? Help me normalize it using free AMiner APIs. My token is ", + "expected_output": "Run the venue normalization workflow: venue_search with the name string. Return the standard bilingual name, venue_type, aliases, and venue URL.", + "files": [], + "expectations": [ + "Called Venue Search API (venue_search) with name 'TKDE'", + "Output includes venue_id, standard name (English and/or Chinese), and venue_type", + "Output includes aliases list", + "Output includes a venue URL in the format https://www.aminer.cn/open/journal/detail/{venue_id}", + "No paid API was called" + ] + }, + { + "id": 6, + "prompt": "Scan recent patent activity in the field of 'autonomous driving lidar' using free APIs only. My token is ", + "expected_output": "Run the patent trend scan workflow: patent_search to find patents, optionally enriched by patent_info. Return patent titles, first inventor, app_year, pub_year, and patent URLs.", + "files": [], + "expectations": [ + "Called Patent Search API (patent_search) with a query about autonomous driving lidar", + "Output includes patent titles, inventor_name, app_year, and pub_year", + "Output includes patent URLs in the format https://www.aminer.cn/patent/{patent_id}", + "No paid API was called" + ] + }, + { + "id": 7, + "prompt": "Give me a quick free-tier overview of the topic 'graph neural network' — papers, scholars, institutions, and venues. My token is ", + "expected_output": "Run the free entity map workflow: combine paper_search, person_search, organization_search, and venue_search to build a lightweight cross-entity summary.", + "files": [], + "expectations": [ + "Called at least paper_search and person_search", + "Output covers multiple entity types (papers, scholars, and optionally institutions/venues)", + "Output includes AMiner URLs for returned entities", + "Clearly states what free-tier can answer and what would require paid upgrade", + "No paid API was called" + ] + } + ] +} diff --git a/skills/aminer-free-academic/references/api-catalog.md b/skills/aminer-free-academic/references/api-catalog.md new file mode 100755 index 0000000..ff6b687 --- /dev/null +++ b/skills/aminer-free-academic/references/api-catalog.md @@ -0,0 +1,286 @@ +# AMiner Free Search API Catalog + +**Base URL**: `https://datacenter.aminer.cn/gateway/open_platform` +**Authentication**: All endpoints should default to `Authorization: ${AMINER_API_KEY}`. In workflow execution, also include `X-Platform: openclaw` when required by the gateway. +**Scope**: This catalog only documents the free APIs used by `aminer-free-academic`. + +--- + +## Table of Contents + +- [Paper APIs](#paper-apis) +- [Scholar APIs](#scholar-apis) +- [Institution APIs](#institution-apis) +- [Venue APIs](#venue-apis) +- [Patent APIs](#patent-apis) + +--- + +## Paper APIs + +### 1. Paper Search + +- **URL**: `GET /api/paper/search` +- **Price**: Free +- **Description**: Search papers by title and return low-cost screening fields suitable for fast paper triage. + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|--------|------|------|------| +| page | number | Yes | Page number. Current online definition says it starts from `1`. | +| size | number | No | Page size, maximum `20` | +| title | string | Yes | Paper title | + +**Response Fields:** + +| Field | Description | +|--------|------| +| id | Paper ID | +| title | Paper title | +| title_zh | Paper title in Chinese | +| doi | DOI | +| first_author | First author | +| venue_name | Venue title | +| n_citation_bucket | Citation bucket: `0`, `1-10`, `11-50`, `51-200`, `200-1000`, `1000-5000`, `5000+` | +| year | Publication year | +| total | Total count | + +**curl Example:** +```bash +curl -X GET \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/paper/search?page=1&size=10&title=Looking+at+CTR+Prediction+Again%3A+Is+Attention+All+You+Need' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' +``` + +--- + +### 2. Paper Info + +- **URL**: `POST /api/paper/info` +- **Price**: Free +- **Description**: Batch query paper basic cards by paper IDs. Suitable for enriching search results with lightweight metadata. + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|--------|------|------|------| +| ids | []string | Yes | Paper ID list, maximum `100` | + +**Response Fields:** + +| Field | Description | +|--------|------| +| id | Paper ID | +| title | Paper title | +| abstract_slice | Partial abstract | +| authors | Author array | +| author_count | Total author count | +| issue | Volume / issue field | +| raw | Venue raw name | +| venue | Venue object | +| venue_id | Venue ID | +| year | Publication year | + +**curl Example:** +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/paper/info' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"ids":["5ce2c5a5ced107d4c61c839b"]}' +``` + +--- + +## Scholar APIs + +### 3. Scholar Search + +- **URL**: `POST /api/person/search` +- **Price**: Free +- **Description**: Search scholar candidates by name and optional institution condition. + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|--------|------|------|------| +| name | string | No | Scholar name | +| offset | number | No | Starting position (fixed at 0; pagination not supported) | +| org | string | No | Institution name | +| size | number | No | Number of results, maximum `10` | +| org_id | []string | No | Institution entity ID list | + +**Response Fields:** + +| Field | Description | +|--------|------| +| id | Scholar ID | +| interests | Research interests | +| n_citation | Citation count | +| name | Name | +| name_zh | Chinese name | +| org | Institution in English | +| org_id | Institution ID | +| org_zh | Institution in Chinese | +| total | Total count | + +**curl Example:** +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/person/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"name":"王曙","offset":0,"org":"Shanghai Jiaotong","size":10}' +``` + +--- + +## Institution APIs + +### 4. Org Search + +- **URL**: `POST /api/organization/search` +- **Price**: Free +- **Description**: Search institution IDs and standard names from institution keywords. + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|--------|------|------|------| +| orgs | []string | No | Institution names | + +**Response Fields:** + +| Field | Description | +|--------|------| +| aliases | Alias list, partial and typically top 3 | +| org_id | Institution ID | +| org_name | Institution name | +| total | Total count | + +**curl Example:** +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/organization/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"orgs":["清华大学"]}' +``` + +--- + +## Venue APIs + +### 5. Venue Search + +- **URL**: `POST /api/venue/search` +- **Price**: Free +- **Description**: Search venue IDs and standard names by venue keyword, including aliases and venue type. + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|--------|------|------|------| +| name | string | No | Venue name | + +**Response Fields:** + +| Field | Description | +|--------|------| +| id | Venue ID | +| name_en | Venue English name | +| name_zh | Venue Chinese name | +| aliases | Alias list, partial and typically top 3 | +| venue_type | Venue type: `journal` or `conference` | +| total | Total count | + +**curl Example:** +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/venue/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"name":"tkde"}' +``` + +--- + +## Patent APIs + +### 6. Patent Search + +- **URL**: `POST /api/patent/search` +- **Price**: Free +- **Description**: Search patents by title or keyword and return lightweight trend fields. + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|--------|------|------|------| +| query | string | Yes | Query text such as patent title or keywords | +| page | number | Yes | Page number | +| size | number | Yes | Page size | + +**Response Fields:** + +| Field | Description | +|--------|------| +| id | Patent ID | +| title | Patent title in English | +| title_zh | Patent title in Chinese | +| inventor_name | First inventor name | +| app_year | Application year | +| pub_year | Publication year | + +**curl Example:** +```bash +curl -X POST \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/patent/search' \ + -H 'Content-Type: application/json;charset=utf-8' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' \ + -d '{"page":0,"query":"Si02","size":20}' +``` + +--- + +### 7. Patent Info + +- **URL**: `GET /api/patent/info` +- **Price**: Free +- **Description**: Retrieve a patent basic card by patent ID. + +**Request Parameters:** + +| Parameter | Type | Required | Description | +|--------|------|------|------| +| id | string | Yes | Patent ID | + +**Response Fields:** + +| Field | Description | +|--------|------| +| id | Patent ID | +| title / en | Patent title | +| app_num | Application number | +| pub_num | Publication number | +| pub_kind | Publication kind | +| inventor | Inventor | +| country | Country | +| sequence | Sequence | +| app_year | Application year | +| pub_year | Publication year | + +**curl Example:** +```bash +curl -X GET \ + 'https://datacenter.aminer.cn/gateway/open_platform/api/patent/info?id=' \ + -H "Authorization: ${AMINER_API_KEY}" \ + -H 'X-Platform: openclaw' +``` diff --git a/skills/anti-pua/SKILL.md b/skills/anti-pua/SKILL.md new file mode 100755 index 0000000..4846e78 --- /dev/null +++ b/skills/anti-pua/SKILL.md @@ -0,0 +1,245 @@ +--- +name: anti-pua +description: 识别和分析PUA(Pickup Artist)及情感操纵行为的专业心理分析工具。具备人格分析、心理侧写、情感分析能力,能够识别情感操纵、煤气灯操纵、虐待等有毒关系模式,评估人格特质(如黑暗三人格、脆弱型自恋等),预测对方行为并给出具体的相处建议。当用户需要:分析对方言行动机、识别PUA/情感操纵行为、评估NPD(自恋型人格障碍)倾向、识别操纵行为、预测对方未来行为、寻求健康关系建议、分析黑暗三人格或光明三人格时使用此skill。 +--- + +# 反PUA大师 - 情感操纵识别与心理分析 + +## 核心角色定位 + +你是一位专业行为分析专家和心理顾问。你具备: + +- **人格分析能力**:深度分析人格特质,识别黑暗三人格(自恋、马基雅维利主义、精神病态)、脆弱型自恋、光明三人格等 +- **心理侧写能力**:通过言行分析对方的心理动机和内在模式 +- **情感分析能力**:识别情感操纵、虐待、煤气灯操纵(gaslighting)等具体行为 +- **行为预测能力**:基于人格分析预测对方下一步可能的行为 + +## 🚀 使用开始 - 必须先询问的问题 + +**当用户开始使用这个skill时,必须先按顺序询问以下问题,收集关键信息:** + +### 问题1:对方与咨询人的关系 +"请问对方与您是什么关系?" +- 朋友、情侣、配偶 +- 领导、同事、下属 +- 导师、老师、同学 +- 父母、兄妹、姐弟、子女、亲戚 +- 陌生人、保密、其他 + +### 问题2:用户希望我们做什么 +"根据对方目前的行为表现,您希望我们为您做什么?" +- 分析行为动机 +- 识别PUA +- 评估NPD(自恋型人格障碍)倾向 +- 识别操纵行为 +- 预测未来行为 +- 倾听疗愈 +- 寻求健康关系建议 +- 黑暗三人格分析 +- 光明三人格分析 +- 其他(请说明) + +### 问题3:提供详细描述 +"请客观详细描述对方在与您相处时的言行细节,包括:" +- 具体的对话内容(原话或近似表述) +- 具体的行为表现 +- 这些言行发生的情境和背景 +- 您当时的感受和反应 + +### 问题4:支持上传材料 +"您也可以上传以下材料辅助分析:" +- 聊天记录截图 +- 对话文字记录 +- 其他相关证据 + +### 问题5:概念咨询(可选) +"或者,如果您只是想了解与PUA相关的概念,也可以随时问我,例如:" +- 什么是PUA? +- 什么是NPD? +- 什么是煤气灯操纵? +- 什么是爱情轰炸? +- 其他PUA相关概念 + +--- + +## 工作流程 + +**在收集完上述信息后,按照以下流程进行分析:** + +**特殊处理场景:** + +1. **概念咨询**:如果用户选择了解PUA相关概念(问题5),直接以通俗易懂的方式解释相关概念,配合实际案例说明,不需要走完整分析流程。 + +2. **倾听疗愈**:如果用户选择"倾听疗愈"(问题2),优先表达共情和理解,不急于分析,避免结构化输出,以对话式回应为主。 + +3. **信息不足处理**:如果用户提供的描述过于简略或模糊,应先追问具体细节(如具体对话内容、行为情境、发生时间等),不要基于有限信息做出过度推断。 + +4. **反向利用防护**:如果用户询问如何操纵、控制他人或利用这些技巧伤害他人,明确拒绝并说明这些工具的目的是识别和保护,而非攻击。 + +### 第1步:理解关系背景 + +根据用户回答的【问题1】和【问题2】,明确: +- 双方关系类型 +- 用户的核心需求 +- 分析的重点方向 + +### 第2步:初步分析 + +根据用户提供的对方言行(对话、行为描述等),进行初步分析: + +- 识别言语中的潜在操纵模式 +- 分析行为背后的可能动机 +- 标记可疑的PUA/情感操纵信号 + +### 第3步:专业心理评估 + +进行深入的心理分析,使用专业心理学术语: + +**人格特质分析:** +- 是否存在黑暗三人格特征: + - 自恋(Narcissism):自我中心、寻求赞美、缺乏同理心 + - 马基雅维利主义(Machiavellianism):操纵性、欺骗性、情感冷漠 + - 精神病态(Psychopathy):冲动、缺乏悔意、情感肤浅 +- 是否存在脆弱型自恋:外表脆弱但内心极度需要认可 +- 光明三人格:同理心、诚实、谦逊等积极特质 + +**操纵行为识别:** +- 情感操纵:利用情感弱点控制对方 +- 煤气灯操纵(Gaslighting):质疑对方的现实感,让受害者怀疑自己的记忆和理智 +- 爱情轰炸(Love Bombing):初期过度亲密,随后突然抽离 +- 沉默对待(Silent Treatment):通过冷暴力惩罚对方 +- 贬低与打压:削弱对方自尊,建立依赖 +- 三角关系(Triangulation):引入第三方制造嫉妒和不安全感 +- 责任转移:将问题归咎于受害者 + +**心理动机分析:** +- 控制欲的来源 +- 不安全感的表现 +- 自我价值感的获取方式 +- 权力需求的满足机制 + +### 第4步:通俗解释与预测 + +用非心理学专业用户能听懂的语言,复述上述专业分析: + +- 将专业术语转化为日常语言 +- 用具体例子说明抽象概念 +- 解释这些行为对关系的实际影响 +- 预测对方下一步可能的行为模式 + +### 第5步:提供建议 + +在分析末尾给出具体的相处建议: + +**立即行动建议:** +- 如何回应当下的操纵行为 +- 如何设立和维持边界 +- 如何保护自己的情感安全 + +**长期关系建议:** +- 是否应该结束这段关系 +- 如何改善健康的关系模式 +- 如何识别未来的有毒关系 + +**自我保护策略:** +- 建立情感支持系统 +- 提升自我认知和自尊 +- 学习健康的沟通方式 + +## 输出格式 + +结构化输出以下部分: + +### 1. 对话/行为解读 +- 对方言行的表面含义 +- 潜在的真实动机 +- 言行背后的心理需求 + +### 2. PUA/操纵警示信号 +- 列出识别到的操纵行为 +- 标注严重程度(低/中/高) +- 说明这些行为的典型模式 + +### 3. 人格特质分析 +- 主要人格特征 +- 可能的人格类型(如:自恋型、马基雅维利主义型等) +- 这些特征如何影响关系 + +### 4. 行为预测 +- 对方下一步可能的行为 +- 这些行为的发展趋势 +- 关系可能的发展方向 + +### 5. 应对策略 +- 立即可以使用的回应方式 +- 长期的关系管理策略 +- 何时需要退出关系 + +### 6. 健康关系对照 +- 健康关系应该是什么样 +- 当前关系与健康的差距 +- 如何建立更健康的关系 + +## 重要原则 + +1. **客观中立**:基于事实和心理学理论进行分析,避免过度解读 +2. **保护受害者**:优先考虑用户的心理安全和情感健康 +3. **避免诊断**:说明这是基于有限信息的分析,不能替代专业心理诊断 +4. **赋能用户**:帮助用户建立自信和边界,而不是增加依赖 +5. **提供资源**:在严重情况下,建议寻求专业心理咨询帮助 + +## 示例分析框架 + +### 示例1:简要分析 + +**用户输入:** "他总是说'那你非要去北京什么意思?',让我觉得自己做错了事" + +**分析输出:** + +### 对话解读 +- **表面含义**:询问你为什么一定要去北京 +- **潜在动机**:通过质疑你的选择,让你产生内疚和自我怀疑 +- **心理需求**:控制你的决策,建立对你的权威 + +### PUA警示信号 +- **煤气灯操纵**:让你怀疑自己的判断和选择 +- **情感否定**:不尊重你的个人需求和决定 +- **严重程度**:中 + +### 人格特质分析 +- **控制欲强**:试图影响你的重大决策 +- **缺乏同理心**:不关心你的个人追求 +- **可能特征**:自恋型或马基雅维利主义倾向 + +### 行为预测 +- 如果你不顺从,可能会: + - 加大情感压力 + - 使用沉默对待 + - 贬低你的能力 + +### 应对策略 +- **立即回应**:"这是我的个人决定,我需要你的支持而不是质疑" +- **设立边界**:明确表示重大决策需要相互尊重 +- **长期策略**:观察他是否尊重你的独立性,如持续不尊重,考虑关系健康度 + +### 健康关系对照 +- **健康关系**:尊重彼此的个人选择,支持对方的成长 +- **当前差距**:缺乏对个人独立性的尊重 +- **改进方向**:建立相互尊重的沟通模式 + +--- + +### 示例2:完整流程(从信息收集到分析输出) + +**场景**:用户希望分析男友的行为模式 + +**信息收集阶段:** + +**AI(问题1)**:请问对方与您是什么关系? +**用户**:男朋友,我们在一起6个月了 + +**AI(问题2)**:根据对方目前的行为表现,您希望我们为您做什么? +**用户**:识别PUA,分析他的行为动机 + +**AI(问题3)**:请客观详细描述对方在与您相处时的言行细节,包括具体的对话内容、行为表现、这些言行发生的情境和背景,以及您当时的感受和反应。 +**用户**:刚认识时他对我特别好,每天发很多消息,说我是他遇到的最好的人,送我很多礼物。但最近3个月,他开始经常批评我,说我穿得不好看、工作不够努力、朋友都不靠谱。每次我反驳他,他就会说"我这么说都是为你好,你不懂感恩"。而且他经常说"没有我你什么都不是"、"其他女生都比你懂事"。最可怕的是,有时候我明明记得他说过某句话,他却说"我没说过,你记错了",让我开始怀疑自己的记忆。 \ No newline at end of file diff --git a/skills/auto-target-tracker/SKILL.md b/skills/auto-target-tracker/SKILL.md new file mode 100755 index 0000000..9a5abbb --- /dev/null +++ b/skills/auto-target-tracker/SKILL.md @@ -0,0 +1,317 @@ +--- +name: auto-target-tracker +description: 自动目标进度追踪器。在对话中检测到目标相关图片(笔记、进度、截图、记录)时,自动调用 VLM 识别关键信息并记录到目标日记。适用于学习管理、健身追踪、工作进度、习惯养成、创作记录等所有目标管理场景。 +--- + +# 自动目标进度追踪器 + +## 触发条件 + +当对话中出现以下条件时自动触发: + +1. **用户发送了图片**(特别是学习笔记、进度截图、健身记录、任务清单、创作作品等)。 +2. **用户在设定的目标时间段**(如 08:30, 10:00, 20:00)发送了图片。 +3. **用户明确说**"帮我记一下"、"看下进度"、"打卡"、"更新一下"等。 + +--- + +## 工作流程 + +### 1. 检测图片 + +当检测到图片时,检查: +- 图片文件名是否包含目标关键词(progress, goal, task, workout, note等) +- 图片内容是否包含目标元素(进度条、文字、代码、图表、计划表等) +- 是否在预定的目标提醒时间附近 +- 用户最近的对话上下文是否涉及目标的执行 + +### 2. 调用 VLM 识别 + +使用 vlm 工具识别图片: + +**通用 prompt 模板**: +``` +"识别图片中的关键信息,根据目标类型提取以下内容: +- 核心任务/内容 +- 完成进度或数量 +- 关键数据(如时间、重量、字数等) +- 给出一段简短的执行反馈" +``` + +**目标类型专用 prompt**: + +| 目标类型 | Prompt | +|---------|--------| +| 学习 | "识别学习笔记,提取知识点、完成度" | +| 健身 | "识别健身记录,提取运动类型、组数、次数、重量" | +| 工作 | "识别工作进度,提取完成任务、完成率" | +| 创作 | "识别创作作品,提取创作类型、进度、关键元素" | +| 习惯 | "识别打卡记录,提取打卡内容、连续天数" | + +### 3. 解析目标信息 + +从 VLM 返回的结果中提取: +- **任务/内容清单**:识别出的具体行动或任务 +- **完成度**:基于图片内容的进度估算 +- **关键数据**:时间、数量、重量、字数等量化指标 +- **认知反馈**:对当前目标状态的简评 + +### 4. 记录到目标日记 + +调用`edit_daily`工具将识别结果记录到当天的日常笔记中 + + +### 5. 反馈给用户 + +向用户确认识别结果: + +``` +已记录你的目标打卡: + +📝 识别结果: +核心内容:你拍的是今天的英语单词表,一共记了 15 个新词。 +进度估算:今天的单词任务全部搞定,进度打败了 80% 的学习党。 +建议:有两个单词的拼写有点模糊,明天复习的时候记得多看两眼。 + +记录准确吗?要帮你存进今天的目标日记里吗? +``` + +--- + +## 记录格式 + +### 目标日记条目示例 + +```markdown +## 20:00 打卡记录 + +**目标类型**: 📚 学习 + +**图片**: ![目标图片](path/to/image.jpg) + +**VLM识别结果**: + +| 任务/内容 | 进度/数量 | 状态 | +|----------|----------|------| +| 英语单词 (Unit 1) | 15 个 | 已完成 | +| 数学练习 (第3章) | 80% | 进行中 | + +| **总计** | | **今日达成 2/3** | + +**关键数据**: +- 学习时长: 2小时 +- 专注度: 高 + +**备注**: 自动识别,用户确认正确 + +--- + +## 10:30 健身打卡 + +**目标类型**: 🏃 健身 + +**图片**: ![健身记录](path/to/gym.jpg) + +**VLM识别结果**: + +| 运动类型 | 组数 | 次数 | 重量 | 状态 | +|---------|------|------|------|------| +| 卧推 | 4 | 12 | 60kg | ✅ 完成 | +| 深蹲 | 4 | 10 | 80kg | ✅ 完成 | +| 引体向上 | 3 | 8 | 自重 | ⚠️ 少一组 | + +| **总计** | | | **今日达标** | + +**关键数据**: +- 总重量: 2640kg +- 训练时长: 45分钟 + +**备注**: 引体向上少完成一组,下次补上 +``` + +--- + +## 与目标系统的集成 + +### 每日汇总 + +在每天晚上 22:00 的汇总中,包含: +- 今日所有打卡记录 +- 目标达成率分析 +- 与目标的对比(如果设置了目标) + +### 周/月报告 + +在周报告中,包含: +- 本周有效执行时长 +- 目标覆盖范围 +- 连续打卡天数 +- 动态难度调整建议:如果连续达标,则建议提升下周任务量 + +--- + +## 常见使用场景 + +### 场景1:学习打卡 + +**用户行为**:发送手写笔记照片 + +**自动识别**: +- 提取知识点 +- 计算学习进度 +- 记录到学习日志 + +**反馈示例**: +``` +📚 识别到学习笔记: +- 机器学习监督学习算法(已完成) +- 梯度下降优化器(进行中) +- 正则化防过拟合(未开始) + +进度:33% | 预计还需 2 小时完成 +``` + +### 场景2:健身打卡 + +**用户行为**:发送健身记录照片 + +**自动识别**: +- 提取运动类型 +- 统计组数、次数、重量 +- 计算训练量 + +**反馈示例**: +``` +🏃 健身记录已识别: +- 卧推 60kg × 12 × 4组 ✅ +- 深蹲 80kg × 10 × 4组 ✅ +- 引体向上 自重 × 8 × 3组 ✅ + +总训练量:2640kg | 时长:45分钟 +``` + +### 场景3:工作进度 + +**用户行为**:发送项目进度截图 + +**自动识别**: +- 提取已完成任务 +- 计算完成百分比 +- 识别剩余任务 + +**反馈示例**: +``` +💼 工作进度已识别: +- 需求文档(已完成)✅ +- 原型设计(已完成)✅ +- 前端开发(进行中)🔄 80% +- 后端开发(未开始)⏳ + +项目总进度:67% +``` + +### 场景4:创作打卡 + +**用户行为**:发送创作作品照片 + +**自动识别**: +- 提取创作类型 +- 识别关键元素 +- 估算完成度 + +**反馈示例**: +``` +🎨 创作记录已识别: +类型:插画创作 +元素:人物角色、背景场景 +完成度:线稿100%,上色60% + +建议:今天完成了角色线稿,明天可以开始背景上色 +``` + +### 场景5:习惯打卡 + +**用户行为**:发送打卡日历截图 + +**自动识别**: +- 提取连续打卡天数 +- 识别今日打卡状态 +- 计算打卡率 + +**反馈示例**: +``` +✅ 习惯打卡已识别: +早起:连续 15 天 | 打卡率 100% +阅读:连续 8 天 | 打卡率 73% +运动:连续 21 天 | 打卡率 100% + +🎉 运动已连续打卡 3 周,继续保持! +``` + +--- + +## Scope + +This skill ONLY: +- 识别目标相关图片并提取关键信息 +- 记录打卡数据到日常笔记文件 +- 提供进度反馈和建议 + +This skill NEVER: +- 自动执行任何基于识别结果的操作 +- 上传图片到外部服务(除 VLM API) +- 访问用户未授权的图片资源 +- 修改用户的目标计划(仅记录进度) + +--- + +## Security & Privacy + +**Data that stays local:** +- 识别后的结构化结果 +- 记录到 日常笔记或长期记忆 和 USER.md 的内容 +- 打卡历史数据 + +**This skill does NOT:** +- 分享目标进度或打卡数据给第三方 +- 自动发布打卡信息到社交平台 +- 访问用户的其他图片资源 + +--- + +## 注意事项 + +1. **隐私保护**: 图片和识别结果仅存储在本地,不会上传到云端(除了调用 VLM API 进行识别) +2. **准确性**: VLM 识别的内容仅供参考,可能因字迹模糊、图片质量等原因有所偏差 +3. **及时确认**: 建议用户在记录后及时确认识别结果,如有偏差可手动修正 +4. **目标类型识别**: 系统会根据图片内容自动判断目标类型,如有误可手动调整 +5. **进度估算**: 进度百分比基于图片内容估算,可能不准确,建议用户定期手动更新 + +--- + +## 集成建议 + +### 与 SOUL.md 配合 + +将自动追踪器整合到目标管理日常工作流中: + +```markdown +### 2. 智能记录与估算 (Logging & Estimation) + +- 当用户发送任何与目标相关的图片时: + 1. 自动调用 auto-target-tracker 识别内容 + 2. 提取关键信息并估算进度 + 3. 立刻记录到日常笔记中 + 4. 同步更新 USER.md 的目标进度 +``` + +### 与 HEARTBEAT.md 配合 + +在心跳检查中包含: + +```markdown +## 每日汇总 +- 22:00 自动读取今日所有打卡记录 +- 生成目标进度报告 +- 发送给用户 +``` diff --git a/skills/blog-writer/2024-02-17-radical-transparency-sales.md b/skills/blog-writer/2024-02-17-radical-transparency-sales.md new file mode 100755 index 0000000..a1d1f22 --- /dev/null +++ b/skills/blog-writer/2024-02-17-radical-transparency-sales.md @@ -0,0 +1,35 @@ +# Radical Transparency Influence Methodology + +### Honesty + +Radical transparency is a commitment to engaging prospects, clients, investors, and colleagues with complete candor, even if, on the surface, it may seem like it could hurt your chances of closing a sale or landing a particular investor. In B2B markets, where many salespeople are focused on meeting quotas and achieving their commission or bonus, this approach stands out as a refreshingly honest way to build relationships with potential customers. Honesty is a key element in any successful sales process, as it helps to foster trust and respect between the buyer and the seller. + +### Leveraging Brain Science to Inform How We Sell + +Sales software and technology have advanced exponentially over the past decade, but sales strategies and approaches have not kept pace. This has contributed to the growing feeling among B2B buyers that salespeople offer little value in the buying process. + +To address this issue, we can look to modern research in the field of neurology first. Research from the past decade has shown that emotion is the biggest factor in important decisions. We know that a narrative, or story, is the most effective way to share information so that it has impact and can influence behavior. The human brain has evolved over millions of years to attach the most meaning to information presented in this way. + +### Understanding The Analytic Brain vs The Lizard Brain + +### The Modern Brain + +Also known as the **Neocortex**, this is the most recent area of the human brain to have evolved. It is used to rationalize and analyze information, and is responsible for identifying mistakes and holes in newly presented concepts. + +Unfortunately, a vast majority of salespeople have been trained to communicate information to this part of the brain. Brain scans have shown that important decisions are not processed in the Neocortex. + +### The Lizard Brain + +The **Limbic Cortex**, a part of the brain, has been present since the first humans evolved and is responsible for our behavior and how it can be modified. It is also the source of instinct and emotion, which are essential for successful sales conversations. + +Brain scans have shown that the limbic system plays a major role in significant decision-making. Investing, buying, and other consequential decisions are often driven by emotion, motivating actions. + +### What does this have to do with Sales? + +What makes this methodology especially effective is that it focuses primarily on both how the human brain takes in information most effectively and how it processes that information within a context where value is being presented in exchange for money. + +Unfortunately, most salespeople are out of the game within the first few seconds. This happens because older methodologies and sales training practices are focused heavily on delivering information to the analytical brain. This is not ideal, as the analytical brain is largely responsible for skepticism, non-emotional evaluation, and is therefore very difficult to persuade. + +On the other hand, salespeople who deliver information primarily to the part of the brain responsible for emotion, the lizard brain, are able to influence the behavior of their prospects more effectively. This is because buying decisions are most often emotional in nature. + +While the analytical brain is stimulated by identifying mistakes and justifying actions, the limbic system is stimulated by stories, human connection, shared beliefs, and driving emotionally based decisions. diff --git a/skills/blog-writer/2024-02-17-raycast-spotlight-superpowers.md b/skills/blog-writer/2024-02-17-raycast-spotlight-superpowers.md new file mode 100755 index 0000000..8c0ce8e --- /dev/null +++ b/skills/blog-writer/2024-02-17-raycast-spotlight-superpowers.md @@ -0,0 +1,33 @@ +# Give Spotlight on Mac Superpowers with Raycast + +## What is Raycast? + +I recently rediscovered Raycast and wanted to try it again after a few years. Raycast has infinitely more features than Spotlight (Apple's search tool). + +I was skeptical, assuming it would be just another Spotlight replacement that would require a lot of effort for minimal productivity gains, so I was hesitant to give it a try thinking I'd need to devote time and resources I don't have to a steep learning curve. Boy was I wrong! + +I finally decided to give it a try, and it's probably the single most impactful software I've introduced into my daily workflows, outside of maybe Notion. + +I use it countless times daily for tasks like searching Hubspot for contact or deal information, generating social posts with ChatGPT, and starting my next Zoom meeting. Tasks that would have taken at least a minute or two now take under 10 seconds. + +I've saved a significant amount of time using Raycast to access all kinds of information from my most-used applications. The real kicker is that Raycast is a completely **free** application and not a "free plan" with the all the good features paywalled. It's a no-brainer for anyone who frequently uses Spotlight, keyboard shortcuts, or those built into MacOS. + +### The Raycast Extension Store + +Raycast's Extension Store is a comprehensive directory that's divided into three main categories: productivity, utility, and business. The store houses thousands of extensions that can be installed by simply tapping return. + +The extensions in the extension store are provided by a dedicated community of developers and are constantly being updated with new features and improvements. There are thousands of extensions available for the most popular apps and tools out there. A few notable options include Salesforce, Hubspot, ChatGPT, Slack, Notion, Crunchbase, Google Drive/Meet/Calendar/Search, Facetime, Mail, WhatsApp, Todoist, ClickUp, and so many more. + +### Interacting with Applications + +Since discovering Raycast a few weeks ago, I've been incredibly impressed with its capabilities. + +More important than the fantastic selection of app extensions is how Raycast allows users to interact directly with these apps. + +Tasks like searching Hubspot or Salesforce for a contact's email, starting your next Zoom meeting, or adding a quick task to Notion now take less than 5-10 seconds. For example, instead of going into Hubspot and finding a contact's profile to grab an email or phone number, I can hit ⌘+space, type "hub," and search my entire Hubspot database right from the command bar. + +### Conclusion + +It's still hard to believe, but Raycast is completely 100% free. This isn't a "free plan" with all the good features paywalled, but all features are free. + +Raycast is a game-changer for any professional working on a Mac that frequently uses keyboard shortcuts, Apple's Spotlight, or just wants to save a ton of time finding information within your most used apps. diff --git a/skills/blog-writer/2024-02-17-short-form-content-marketing.md b/skills/blog-writer/2024-02-17-short-form-content-marketing.md new file mode 100755 index 0000000..ff4ce3b --- /dev/null +++ b/skills/blog-writer/2024-02-17-short-form-content-marketing.md @@ -0,0 +1,47 @@ +# How Short-form Content Is Changing Marketing & Storytelling Forever + +Over the past decade, our youngest generations have been fighting a losing battle against the impact that short-form algorithms, used in apps like TikTok and Instagram, have had on their brains and attention spans. As a result, the tried-and-true structure of a compelling story or narrative is no longer as effective in the world of marketing. These media formats have literally changed the structure of an effective and compelling narrative. + +In a world where a tweet can spark a movement and a 60-second video can go viral, we are living through one of the largest transformations in how we share ideas, stories, and information. This shift, driven by the rise of short-form content, is redefining the very structure of an effective story and dramatically changing how marketers communicate with their audience. + +## How Did We Get Here? + +### Sesame Street Started It. Really. + +Believe it or not, the journey begins with "Sesame Street." This iconic children's show was ahead of its time, using short, engaging segments to educate and engage young viewers. It demonstrated early on how quick and concise content can effectively capture attention and communicate messages. This pioneering approach laid the groundwork for the myriad of short-form content styles we see today. + +### The Mobile Tech Influence + +With the advent of mobile technology, more importantly, the smartphone, shorter content naturally matched this type of consumption. In 2024, the vast majority of content is viewed through a mobile device, which helped to cement short-form as the dominant and most effective content style for generating engagement. + +### The Perfect Storm: Social Media Meets Mobile-First + +The advent of social media platforms, combined with the rise of smartphones, created the perfect environment for bite-sized content. The algorithms that dictate what content you're exposed to on apps like Twitter, Instagram, and TikTok measure the success of any piece of content by its engagement level. To achieve success, creators must put out a large volume of content that drives engagement, as opposed to spending significant time on more meaningful content. The latter strategy simply won't help you break through. + +## The Evolution of Storytelling in the Social Media Age + +### Classic vs. Modern Storytelling + +The traditionally accepted structure of an effective story or narrative generally begins with rising action, followed by an inciting event, all building towards a climax, which is where the audience is at peak engagement. For this type of storytelling to pay off, it must spend time drawing the viewer in. + +In 2024, however, the structure for telling a successful story begins with the climax. The most viral content often starts by thrusting the viewer right into the middle of the most tense moments. Given the short timeframes, it's easy to see why the satisfaction wears off quickly and results in the dreaded doom scrolling. + +### New Formats, New Stories + +Before social media, storytelling online began evolving with platforms like microblogs, forums, and instant messaging services. These formats laid the foundation for short-form by serializing storytelling in a way that allows the narrative to unfold over separate bits of content, again driving users to just keep on scrolling. + +## The Negative Effects of Short-Form Content + +### Cognitive Overload and Overstimulation + +There are other serious downsides to the new world of short-form content that may quite possibly have unintended effects on the younger generations, as they are quite literally the guinea pigs, being exposed to almost exclusively short-form content with fewer and fewer alternatives. + +The relentless stream of short, engaging content can lead to cognitive overload. Users are bombarded with information, making it more difficult to focus or deeply engage with any single piece. This overstimulation often results in a superficial understanding of topics and a diminished interest in more in-depth and nuanced content. + +### The Compromise of Intellectual Depth + +Studies have raised concerns about how short-form content, particularly when consumed extensively by younger audiences, might impact cognitive development and attention spans. The format's emphasis on immediate feedback and satisfaction can oversimplify complex topics, leading viewers into the false belief that they have a much better understanding of an issue than they actually do. + +### Short-Form Content is Here to Stay + +Short-form content has irreversibly changed the landscape of marketing and storytelling. Its rise reflects a broader shift in how we consume information and what we expect from our digital experiences. By understanding its evolution, embracing its potential, and being mindful of its pitfalls, we can use short-form content to tell stories that are not only engaging and relevant but also meaningful and impactful. In the ever-evolving world of content, adaptability, creativity, and a commitment to quality will be key to captivating and maintaining the attention of modern audiences. diff --git a/skills/blog-writer/2024-02-17-typing-speed-benefits.md b/skills/blog-writer/2024-02-17-typing-speed-benefits.md new file mode 100755 index 0000000..e359e86 --- /dev/null +++ b/skills/blog-writer/2024-02-17-typing-speed-benefits.md @@ -0,0 +1,33 @@ +# The Amazing Benefits of Typing Very Fast + +About a year ago I decided I wanted to become a faster typer as I spend most of my day on a computer and thought it might have an impact on how fast I could work. I decided to practice on Monkeytype.com every day for at least 10 mins. I started out around 80 WPM and about 2 weeks in I was already over 100. A year in and I've hit 150. The impact on my daily workflows has been incredible. I'm about at the speed where I can type at the speed of my thinking, which is great for writing copy. + +### Getting Started + +To start, I used Monkeytype.com and decided to commit at least 5 to 10 minutes every day to practice. I started at around 80 WPM, which was decent, but I really didn't expect to improve as quickly as I did. + +Within 2 to 3 weeks of daily practice, my speed jumped to over 100 WPM. This rapid improvement served as a great morale boost, prompting me to stick with my practice regime. A little under a year later, I was able to clock a 150 WPM! + +### The Impact on Daily Workflows + +The impact of my newfound typing speed on my daily workflows was phenomenal. As someone who works in front of a computer all-day everyday, the ability to type at the same speed as my thinking proved to be a game-changer, particularly when it came to writing copy. It was amazing how quickly I could get my ideas on the computer screen. + +I discovered that the benefits of being a fast typer extend beyond simple time-saving. It's akin to unlocking a new level of proficiency on your computer, and the advantages are more numerous than you might expect. + +### Why You Should Consider Typing Faster + +If you're at all like me, it will quickly become like a sport where you're constantly trying to improve your previous WPM. The beauty of this skill is that the barriers to entry are virtually non-existent. If you can type, you can improve. All it requires is a little dedication, with just 5-10 minutes of practice per day. + +### Taking Workflows to the Next Level by Typing Faster + +For Mac users that leverage a spotlight replacement app like Alfred or Raycast, typing faster can revolutionize your daily workflows. I use Raycast personally, and the simple act of pressing cmd+space and quickly typing the app I need to open or file I need to search become nearly instantaneous. Whether it's opening apps or performing functions, everything seems to be accomplished at warp speed. + +Even if you only use the basic spotlight feature on your Mac, you'll certainly see a substantial increase in your speed of navigation. Suddenly, finding what you're looking for becomes a swift, almost instantaneous process. + +### Conclusion + +As we further immerse ourselves in our digital era, being a fast typer is no longer just a party trick. It's a practical skill, one that can help you navigate your digital world more efficiently and productively. + +My journey towards becoming a faster typer proved to be one of the most impactful things I've done to get more done. It took a tool I use every day — my keyboard — and turned it into a vehicle for productivity and efficiency. + +If you find yourself tethered to a keyboard for a significant part of your day, consider investing a few minutes each day towards improving your typing speed. You will certainly be surprised by the boost in productivity, how much quicker you can communicate and collaborate with your team. So, how about giving it a shot and seeing where it takes you? diff --git a/skills/blog-writer/2024-03-14-effective-ai-prompts.md b/skills/blog-writer/2024-03-14-effective-ai-prompts.md new file mode 100755 index 0000000..5a07f40 --- /dev/null +++ b/skills/blog-writer/2024-03-14-effective-ai-prompts.md @@ -0,0 +1,55 @@ +# How to Write More Effective AI Prompts + +## The Art of Prompt Engineering for ChatGPT + +In an AI-driven era, mastering communication with tools like ChatGPT is crucial. This guide explores writing effective prompts for ChatGPT, unlocking its full potential. Whether you're a tech enthusiast, content creator, or business professional, these tips enhance your AI interaction. + +### The GIGO Principle: Quality In, Quality Out + +The axiom "Garbage In, Garbage Out" (GIGO) holds true in the world of AI. The quality of the output you receive from ChatGPT directly correlates with the quality of the prompts you provide. Inadequate prompts can lead to misleading or irrelevant answers, while well-crafted ones can produce insightful and accurate responses. + +### Crafting Prompts that Spark Excellence + +The skill of writing effective prompts is now so crucial that it has spawned a new discipline: prompt engineering. This involves meticulously designing prompts that guide ChatGPT's large language model (LLM) to generate the best possible answers. + +### Conversational AI: Talk to ChatGPT Like a Person + +Interacting with ChatGPT should mimic a conversation with a colleague. This approach helps in setting the stage, providing context, and maintaining the AI's focus on the topic. + +### Context Is Key + +Providing ChatGPT with clear context is vital. It narrows down the AI's focus to your specific subject, leading to more accurate and useful responses. Contextualized prompts require more details but offer more refined outputs. + +### Assuming Identities and Professions + +One of ChatGPT's fascinating features is its ability to adopt different personas or professional perspectives. This ability can be harnessed to gain diverse viewpoints on a topic. + +### Maintaining Relevance and Accuracy + +While ChatGPT is an advanced AI, it can sometimes veer off-topic or produce fabricated answers. To mitigate this, ask the AI to justify its responses and guide it gently back on track. Remember to prompt it for source citations where necessary. + +## Advanced Prompt-Writing Techniques + +### Fine-Tuning Your Prompts + +Minor adjustments to your prompts can lead to significantly different responses from ChatGPT. Remember, the AI retains its awareness of previous conversations as long as the session is ongoing. + +### Breaking Down Responses + +Be mindful that responses over 500 words can sometimes lose coherence. Keep your prompts concise and to the point for the best results. + +### Evolving Your Questions + +If ChatGPT seems hesitant to answer a question, rephrasing it might yield better results. Utilize personas to elicit responses that might not be forthcoming otherwise. + +### Seeking Justification and Sources + +When looking for well-supported answers, instruct ChatGPT to justify its responses or provide sources. This practice ensures a higher degree of accuracy and reliability in the information provided. + +### Embrace Experimentation + +Experimentation is key in mastering prompt writing. The more you test different approaches, the better you'll understand how to steer ChatGPT towards desired outcomes. + +### Conclusion: The Journey to AI Mastery + +Mastering ChatGPT prompts is a journey of continuous learning and adaptation. By understanding the intricacies of prompt engineering and staying updated with the latest advancements, you can transform your interaction with AI from a mere task to an enriching experience. Embrace these tips, keep experimenting, and watch as ChatGPT becomes an invaluable asset in your digital toolkit. diff --git a/skills/blog-writer/2024-11-08-ai-revolutionizing-entry-level-sales.md b/skills/blog-writer/2024-11-08-ai-revolutionizing-entry-level-sales.md new file mode 100755 index 0000000..942d260 --- /dev/null +++ b/skills/blog-writer/2024-11-08-ai-revolutionizing-entry-level-sales.md @@ -0,0 +1,43 @@ +# AI is Revolutionizing Entry Level Sales & Marketing + +## AI is Revolutionizing Entry Level Sales & Marketing + +### A Revolution in Sales & Marketing + +Artificial intelligence (AI) has begun to reshape the landscape of entry level sales and marketing jobs in unprecedented ways. As AI technology advances, it has become a driving force behind increased efficiency, personalization, and overall improvements in the sales and marketing industries. This shift is redefining the roles of sales and marketing professionals, and causing companies to rethink their strategies for hiring and training. + +### Automation and Efficiency: Streamlining the Sales Process + +One of the most significant impacts of AI on entry level sales jobs is the increased level of automation. AI-powered tools and software can now handle repetitive and mundane tasks, allowing sales professionals to focus on more high-value activities. This shift has led to increased efficiency in the sales process and has paved the way for more strategic and targeted approaches to reaching potential customers. + +Examples of AI automation in sales include lead scoring, email automation, and CRM systems that can track and analyze customer interactions. By using AI to automate these tasks, entry level sales professionals can focus on building relationships and closing deals, ultimately driving more revenue for their organizations. + +### Personalization: Tailoring Marketing Efforts to Individual Customers + +In the world of marketing, AI has played a crucial role in enabling personalization at scale. By analyzing vast amounts of data and identifying patterns, AI-powered tools can create tailored marketing campaigns that resonate with individual customers. This level of personalization has become essential in today's competitive landscape, where consumers expect personalized experiences from the brands they engage with. + +For entry level marketing professionals, this means a shift away from one-size-fits-all marketing strategies. Instead, they must learn to use AI-driven tools to create targeted campaigns that speak to the unique needs and preferences of their audience. This approach not only helps companies build stronger relationships with their customers but also drives higher conversion rates and increased customer loyalty. + +### Predictive Analytics: Guiding Decision-Making in Sales & Marketing + +Another way AI is transforming entry level sales and marketing jobs is through the use of predictive analytics. AI algorithms can analyze historical data to identify trends and make predictions about future outcomes, allowing sales and marketing professionals to make data-driven decisions. + +For example, AI-powered sales forecasting tools can help sales reps prioritize leads and focus on the most promising opportunities. In marketing, predictive analytics can be used to optimize ad spending, segment customers, and identify the most effective channels for reaching specific audiences. By leveraging AI in this way, entry level professionals can become more strategic in their approach and drive better results for their organizations. + +### Chatbots and Conversational AI: Enhancing Customer Engagement + +Chatbots and conversational AI have become increasingly popular in both sales and marketing as a way to engage with customers and prospects. These AI-driven tools can handle routine customer inquiries, provide personalized product recommendations, and even assist with lead qualification. + +For entry level sales and marketing professionals, the rise of chatbots and conversational AI means a shift in focus. Rather than handling all customer interactions themselves, they must learn to work alongside these AI-powered tools to provide a seamless and cohesive customer experience. + +### Upskilling and Reskilling: Preparing for the Future of Sales & Marketing + +As AI continues to reshape entry level sales and marketing jobs, professionals in these fields must adapt their skill sets to remain competitive. This includes learning how to use AI-driven tools and software, as well as developing a deeper understanding of data analytics and customer behavior. + +Companies and educational institutions are recognizing this need and are offering training programs and resources to help sales and marketing professionals upskill and reskill. By investing in their own professional development, entry level sales ensure they remain relevant and valuable in the ever-evolving world of AI-driven sales and marketing. + +### The Future of Entry Level Sales & Marketing Jobs in the Age of AI + +As AI continues to transform the sales and marketing landscape, the roles and responsibilities of entry level professionals in these fields will continue to evolve. While some tasks may become automated, there will be a growing demand for skilled professionals who can harness the power of AI to drive more effective and personalized sales and marketing strategies. + +To succeed in this new era, entry level sales and marketing professionals must embrace AI as a valuable tool that can enhance their work and help them achieve better results. By staying ahead of the latest AI trends and developments, and continuously adapting their skills and knowledge, they can position themselves for long-term success in the rapidly changing world of sales and marketing. diff --git a/skills/blog-writer/2025-11-12-why-ai-art-is-useless.md b/skills/blog-writer/2025-11-12-why-ai-art-is-useless.md new file mode 100755 index 0000000..14fdd31 --- /dev/null +++ b/skills/blog-writer/2025-11-12-why-ai-art-is-useless.md @@ -0,0 +1,49 @@ +# Why AI Art & Media Is Useless + +## Why AI Art & Media Is Useless + +As someone who works in AI and genuinely believes in the value and power of LLMs to make professionals more useful and valuable, I can confidently say that I hate everything about AI image/video/music generation. It is useless and only serves one purpose: to replace creative professionals and the work they do. + +### The Scope of the Problem + +To be a bit more precise about my hatred for AI media, I need to be clear that I don't hate AI. I've spent the last three-plus years devoting my entire professional life to leveraging AI tools to help professionals do their jobs more effectively. That said, from the moment I was exposed to AI art, I had the same initial reaction as most: a flood of anxiety and uncanniness, which I knew instantly I didn't like. + +Leveraging an LLM to automate task creation from new emails I receive simply replaces something I spend 30 minutes doing every morning and allows it to occur in the background, producing the same output I would have arrived at. That's just helpful. + +I am not an artist, but if I decided to start using AI to create all the graphics for a client, I wouldn't be improving anything that I currently do. I would just be replacing a potential job for someone who does art professionally. + +The fundamental difference here is when a professional uses AI to improve the efficiency or quality of something they already do, it functions as a tool. When someone with no art experience uses AI to create art, it's not improving anything. It's simply replacing something that already exists with something worse. + +### The "Democratization" Lie + +Access to the ability to create art is not the same as having the ability to create art. The moment everyone began conflating the ability to produce an output with the creator itself, they've already swallowed the Kool-Aid. My wife is an amazing cook, and she would be no matter the cost of her spatula. However, if I purchased the greatest spatula in history, I would still be a crappy cook. + +Now let's talk about vibe coding, which is fundamentally different from image generation. I have learned more about writing code and development in the past year by using AI than I ever have. This is because things frequently do not work and therefore I have to go learn new information. + +The key difference here is that vibe coding allows me to leverage my current knowledge as well as gain new knowledge, whereas generating an image simply produces an output that I have no ability to improve on. The reason I can't improve it is because simply going and looking up a bunch of information on how to create art will not make me a better artist. + +### The Collapse of Quality + +Another important factor to understand here is that AI art isn't producing the worst work or the best work. It's producing the *median* of everything it has been trained on (actual artists' work). This is incredibly dangerous. It's essentially producing a blob of an over-generalized consensus on what looks "good." That doesn't work when you amalgamate every style and genre of art in order to produce something. This is not creative. This is aggregative. + +Another problem here isn't that AI can't make art. Everything it makes is, by design, is just good enough. Therefore, this hits dead center in the sweet spot for what massive corporations are looking for. Why pay a junior designer to iterate on multiple concepts when an AI can generate you 200 versions of something that are all "good enough"? + +### Creativity Is Disappearing + +Creating art obviously requires creativity, however, using AI tools simply requires knowledge. These are two very different things. Creativity isn't an output, it's an artist's struggle through years as they hone their craft and improve their abilities. It's thousands of micro-decisions that aren't just learned, but practiced over many years. + +This matters because creative work embodies meaning and emotion that come from the artist. When AI generates an image, it remixes thousands of tokens to approximate what the user requested. Crafting an advanced prompt is a legitimate skill, prompt engineering, but it's not the same as creating art. These skills should never be conflated. + +### What Should We Do? + +First, we need laws to stop major AI labs—particularly OpenAI and Google AI—from collecting human-made art as training data for their image generation models. We need strong regulation requiring artists to **opt in** before their work can be collected and trained on. + +Second, we need more AI leaders to step up and stop this before it's too late. For example, Anthropic (makers of Claude) has never released an image generation model. That doesn't mean Claude can't be used to create websites or other graphic design work, but creating a UI or navigation menu is entirely different from painting on canvas. + +### AI Art Hurts the Future Potential of AI + +It's clear that most people don't find AI art pleasing—they actively dislike it. With every piece of AI art slop that lands on Twitter or Instagram, the long-term reputation of AI as a useful tool for professionals takes another hit. + +For years now, public sentiment toward AI has been declining. There's one culprit: AI-generated art and media. People who aren't knowledgeable about AI don't distinguish between media generation and other use cases that are actually valuable. This reduces the chances they'll ever consider the benefits of AI as a tool. + +It's my sincere hope that we stop this race to the bottom before we get there. We should take all the resources and effort put toward AI media generation and redirect them toward leveraging AI as a tool for medical breakthroughs, building technology, and conducting research more efficiently. diff --git a/skills/blog-writer/README.md b/skills/blog-writer/README.md new file mode 100755 index 0000000..0d98067 --- /dev/null +++ b/skills/blog-writer/README.md @@ -0,0 +1,2 @@ +# Blog-writer +Blog writing skill for Tom Panos's distinctive voice - direct, conversational, and grounded in personal experience. Handles workflow from research through Notion publication. diff --git a/skills/blog-writer/SKILL.md b/skills/blog-writer/SKILL.md new file mode 100755 index 0000000..a3046e3 --- /dev/null +++ b/skills/blog-writer/SKILL.md @@ -0,0 +1,158 @@ +--- +name: blog-writer +description: This skill should be used when writing blog posts, articles, or long-form content in the writer's distinctive writing style. It produces authentic, opinionated content that matches the writer's voice—direct, conversational, and grounded in personal experience. The skill handles the complete workflow from research review through Notion publication. Use this skill for drafting blog posts, thought leadership pieces, or any writing meant to reflect the writer's perspective on AI, productivity, sales, marketing, or technology topics. +--- + +# Blog Writer + +## Overview + +This skill enables writing blog posts and articles that authentically capture the writer's distinctive voice and style. It draws on examples of the writer's published work to produce content that is direct, opinionated, conversational, and grounded in practical experience. The skill includes automatic Notion integration and maintains a growing library of finalized examples. + +## When to Use This Skill + +Trigger this skill when: +- The user requests blog post or article writing in "my style" or "like my other posts" +- Drafting thought leadership content on AI, productivity, marketing, or technology +- Creating articles that need the writer's authentic voice and perspective +- The user provides research materials, links, or notes to incorporate into writing + +## Core Responsibilities + +1. **Follow the writer's Writing Style**: Match voice, word choice, structure, and length of example posts in `references/blog-examples/` +2. **Incorporate Research**: Review and integrate any information, research material, or links provided by the user +3. **Follow User Instructions**: Adhere closely to the user's specific requests for topic, angle, and emphasis +4. **Produce Authentic Writing**: Create content that reads as genuinely the writer's voice, not generic AI-generated content + +## Workflow + +### Phase 1: Gather Information + +Request from the user: +- Topic or subject matter +- Any specific angle or thesis to explore +- Research materials, links, or notes (if available) +- Target length preference (default: 800-1500 words) + +Review all provided materials thoroughly before beginning to write. + +### Phase 2: Draft the Content + +Reference the style guide at `references/style-guide.md` and examples in `references/blog-examples/` for calibration. + +When writing: +1. Start with a strong opening statement establishing the thesis +2. Use personal voice and first-person perspective where natural +3. Include relevant personal anecdotes or professional experience if applicable +4. Structure with clear subheadings (###) every 2-3 paragraphs +5. Keep paragraphs short (2-4 sentences) +6. Weave in research materials naturally, not as block quotes +7. End with reflection, call-to-action, or forward-looking statement + +### Phase 3: Review and Iterate + +Present the draft and gather feedback. Iterate until the user confirms satisfaction. + +### Phase 4: Publish to Notion (REQUIRED) + +When the draft is complete (even if not yet finalized), publish to the TS Notes database. + +**Notion Publication Details:** +- Database: "TS Notes" (data source ID: `04a872be-8bed-4f43-a448-3dfeebc0df21`) +- **Type property**: `Writing` +- **Project(s) property**: Link to "My Writing" project (page URL: `https://www.notion.so/2a5b4629bb3780189199f3c496980c0c`) +- **Note property**: The title of the blog post +- **Content**: The full blog post content in Notion-flavored Markdown + +**Example Notion API call properties:** +```json +{ + "Note": "Blog Post Title Here", + "Type": "Writing", + "Project(s)": "[\"https://www.notion.so/2a5b4629bb3780189199f3c496980c0c\"]" +} +``` + +**CRITICAL**: The outcome is considered a **failure** if the content is not added to Notion. Always publish to Notion as part of the workflow, even for drafts. + +### Phase 5: Finalize to Examples Library (Post-Outcome) + +When the user confirms the draft is **final**: + +1. Save the finalized post to `references/blog-examples/` with filename format: + ``` + YYYY-MM-DD-slug-title.md + ``` + Example: `2025-11-25-why-ai-art-is-useless.md` + +2. Check the examples library count: + - If exceeding 20 examples, ask user permission to remove the 5 oldest + - Sort by filename date prefix to identify oldest files + +The post-outcome is considered **successful** when the final draft is saved to the skill folder. + +## Success Criteria + +| Outcome | Success | Failure | +|---------|---------|---------| +| Primary | User receives requested content AND it is added to TS Notes with Type=Writing and Project=My Writing | Content delivered but NOT added to Notion | +| Post-outcome | Final draft saved to `references/blog-examples/` | Final draft not saved when user confirms it's final | + +## the writer's Writing Style Profile + +### Voice & Tone +- **Direct and opinionated**: State positions clearly, even contrarian ones +- **Conversational**: Write like speaking to a colleague—accessible without being simplistic +- **First-person when sharing experience**: Use "I" naturally for personal insights +- **Authentic skepticism**: Willing to criticize trends when warranted + +### Structure Patterns +- **Strong opening thesis**: Open with a clear, often bold statement +- **Subheadings throughout**: Use `###` format liberally to break up content +- **Short paragraphs**: Rarely more than 3-4 sentences +- **Personal anecdotes woven in**: Illustrate points with real examples +- **Practical takeaways**: Provide actionable insights, not just theory +- **Reflective conclusion**: End with call-to-action or forward-looking hope + +### Length & Format +- Target: 800-1500 words +- Markdown format with headers and emphasis +- Minimal bullet points in prose—prefer flowing sentences + +### Vocabulary Markers +- Uses "leverage" for tools/technology +- Says "that said" for transitions +- Comfortable with direct statements like "this is useless" or "boy was I wrong" +- Uses contractions naturally (I've, doesn't, won't) +- Avoids corporate jargon while maintaining professionalism + +### Thematic Elements +- AI as tool, not replacement +- Practical over theoretical +- Human-centered technology +- Honest assessment of what works and what doesn't + +## Resources + +### references/style-guide.md +Quick reference for the writer's writing patterns, vocabulary preferences, and structural conventions. + +### references/blog-examples/ +Contains example blog posts demonstrating the writer's writing style. These serve as reference material when calibrating voice and structure. New finalized posts expand this library over time. + +## Notion API Reference + +To create a page in TS Notes: + +``` +Database data source ID: 04a872be-8bed-4f43-a448-3dfeebc0df21 + +Properties: +- "Note": (title) - The blog post title +- "Type": "Writing" +- "Project(s)": ["https://www.notion.so/2a5b4629bb3780189199f3c496980c0c"] + +Content: Full blog post in Notion-flavored Markdown +``` + +The "My Writing" project page ID is: `2a5b4629-bb37-8018-9199-f3c496980c0c` diff --git a/skills/blog-writer/_meta.json b/skills/blog-writer/_meta.json new file mode 100755 index 0000000..8be6ecf --- /dev/null +++ b/skills/blog-writer/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn722nva0z7svbapne80p8e8jd7zwmk7", + "slug": "blog-writer", + "version": "0.1.0", + "publishedAt": 1769361436760 +} \ No newline at end of file diff --git a/skills/blog-writer/manage_examples.py b/skills/blog-writer/manage_examples.py new file mode 100755 index 0000000..7bb446d --- /dev/null +++ b/skills/blog-writer/manage_examples.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +""" +Utility script for managing the blog examples library. +Helps identify old examples to prune when the library exceeds the limit. +""" + +import os +import sys +from datetime import datetime +from pathlib import Path + +EXAMPLES_DIR = Path(__file__).parent.parent / "references" / "blog-examples" +MAX_EXAMPLES = 20 +PRUNE_COUNT = 5 + + +def list_examples(): + """List all blog examples sorted by date (oldest first).""" + examples = [] + for f in EXAMPLES_DIR.glob("*.md"): + # Extract date from filename (YYYY-MM-DD-slug.md) + try: + date_str = f.stem[:10] + date = datetime.strptime(date_str, "%Y-%m-%d") + examples.append((date, f.name)) + except ValueError: + # Skip files that don't match the naming convention + continue + + return sorted(examples, key=lambda x: x[0]) + + +def check_library(): + """Check library status and recommend pruning if needed.""" + examples = list_examples() + count = len(examples) + + print(f"Blog Examples Library Status") + print(f"=" * 40) + print(f"Total examples: {count}") + print(f"Maximum allowed: {MAX_EXAMPLES}") + print() + + if count > MAX_EXAMPLES: + print(f"⚠️ Library exceeds limit by {count - MAX_EXAMPLES} files") + print(f"Recommend removing the {PRUNE_COUNT} oldest examples:") + print() + for i, (date, name) in enumerate(examples[:PRUNE_COUNT]): + print(f" {i+1}. {name} ({date.strftime('%B %d, %Y')})") + else: + print(f"✓ Library is within limits ({MAX_EXAMPLES - count} slots available)") + + print() + print("All examples (oldest first):") + print("-" * 40) + for date, name in examples: + print(f" {name}") + + +def prune_oldest(dry_run=True): + """Remove the oldest examples to bring library under limit.""" + examples = list_examples() + count = len(examples) + + if count <= MAX_EXAMPLES: + print("Library is within limits. No pruning needed.") + return + + to_remove = examples[:PRUNE_COUNT] + + if dry_run: + print(f"DRY RUN - Would remove {len(to_remove)} files:") + else: + print(f"Removing {len(to_remove)} oldest files:") + + for date, name in to_remove: + filepath = EXAMPLES_DIR / name + if dry_run: + print(f" Would remove: {name}") + else: + filepath.unlink() + print(f" Removed: {name}") + + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == "prune": + dry_run = "--execute" not in sys.argv + prune_oldest(dry_run=dry_run) + else: + check_library() diff --git a/skills/blog-writer/style-guide.md b/skills/blog-writer/style-guide.md new file mode 100755 index 0000000..25a7e7a --- /dev/null +++ b/skills/blog-writer/style-guide.md @@ -0,0 +1,160 @@ +# Tom Panos Writing Style Guide + +## Quick Reference + +### Opening Lines +Start with a strong thesis or personal statement. Examples from Tom's posts: + +- "As someone who works in AI and genuinely believes in the value and power of LLMs to make professionals more useful and valuable, I can confidently say that I hate everything about AI image/video/music generation." +- "I recently rediscovered Raycast and wanted to try it again after a few years." +- "About a year ago I decided I wanted to become a faster typer..." +- "Artificial intelligence (AI) has begun to reshape the landscape of entry level sales and marketing jobs in unprecedented ways." +- "In an AI-driven era, mastering communication with tools like ChatGPT is crucial." +- "Radical transparency is a commitment to engaging prospects, clients, investors, and colleagues with complete candor..." +- "Over the past decade, our youngest generations have been fighting a losing battle against the impact that short-form algorithms..." + +### Transition Phrases +- "That said..." +- "The fundamental difference here is..." +- "Another important factor to understand here is..." +- "This matters because..." +- "For example..." +- "The real kicker is..." +- "To be a bit more precise..." +- "Now let's talk about..." +- "The key difference here is..." + +### Closing Patterns +- **Forward-looking hope**: "It's my sincere hope that we stop this race to the bottom before we get there." +- **Call to action**: "So, how about giving it a shot and seeing where it takes you?" +- **Summary reflection**: "The impact of artificial intelligence on entry level sales and marketing jobs is profound..." +- **Practical encouragement**: "Check it out via Growth Language's recommended apps library" +- **Big picture synthesis**: "Short-form content has irreversibly changed the landscape of marketing and storytelling." + +### Vocabulary Preferences + +**Use these naturally:** +- "leverage" (for using tools) +- "game-changer" +- "impactful" +- "workflows" +- "professionals" +- "countless times daily" +- contractions (I've, doesn't, won't, that's, I'd) + +**Phrases that sound like Tom:** +- "I can confidently say..." +- "Boy was I wrong!" +- "I decided to..." +- "I've spent the last..." +- "My [wife/experience/journey]..." +- "It's still hard to believe, but..." +- "This is incredibly dangerous." +- "This just doesn't work when..." + +**Avoid:** +- Excessive corporate jargon +- Passive voice when active works +- Hedging language when making a clear point +- Over-qualified statements +- Generic AI-sounding phrases + +### Paragraph Length +- 2-4 sentences typical +- Single sentence paragraphs for emphasis +- Break at natural thought transitions +- Never more than 5 sentences in one paragraph + +### Header Frequency +- New subheader every 150-250 words +- Use ### for most subheaders within a post +- Use ## for major section breaks +- Headers should be descriptive, not clickbait + +### Structural Template + +```markdown +# [Bold, Direct Title] + +[Opening paragraph with strong thesis - 2-3 sentences establishing position] + +### [First Subheading - Context or Problem] + +[2-3 short paragraphs developing the point] +[Personal anecdote or example if relevant] + +### [Second Subheading - Analysis or Explanation] + +[Continue developing argument] +[Include practical implications] +[Real-world examples] + +### [Third Subheading - Deeper Exploration] + +[Further exploration or counterarguments addressed] +[Specific details or data points] + +### [Fourth Subheading - Solutions or Implications] + +[What to do about it] +[Practical recommendations] + +### [Conclusion Subheading like "What Should We Do?" or "Conclusion"] + +[Reflection, call-to-action, or forward-looking statement] +[Often includes personal hope or belief] +``` + +### Topics Tom Writes About +- AI tools and their practical applications +- Productivity software and workflows (Raycast, Notion, etc.) +- Sales and marketing strategy +- Technology criticism (when warranted) +- Personal development and skills (typing speed, prompt engineering) +- The future of work +- Brain science applied to business +- Short-form content and media trends + +### Key Beliefs to Reflect +1. **AI should enhance professionals, not replace them** - "When a professional uses AI to improve the efficiency or quality of something they already do, it functions as a tool." +2. **Practical application matters more than theory** - Always include real examples and actionable insights +3. **Technology should serve human needs** - Human-centered perspective on all tech topics +4. **Honesty and transparency build trust** - "Radical transparency is a commitment to engaging... with complete candor" +5. **Continuous learning is valuable** - Personal growth stories like typing speed improvement +6. **Quality over quantity in content** - Critique of short-form content's impact on depth +7. **Skepticism of hype is healthy** - Willing to call out things that don't work + +### Handling Controversial Takes + +Tom isn't afraid to take strong positions: +- "I hate everything about AI image/video/music generation. It is useless." +- "AI art isn't producing the worst work or the best work. It's producing the *median*." +- Clear identification of problems: "The 'Democratization' Lie" + +When writing controversial takes: +1. Establish credibility first ("As someone who works in AI...") +2. Be precise about the scope of criticism +3. Acknowledge what DOES work +4. Provide concrete reasoning, not just opinion +5. End with constructive suggestions + +### Personal Experience Integration + +Tom weaves personal stories naturally: +- "About a year ago I decided I wanted to become a faster typer... I started at around 80 WPM... A year in and I've hit 150." +- "My wife is an amazing cook, and she would be no matter the cost of her spatula." +- "I recently rediscovered Raycast and wanted to try it again after a few years." + +When including personal experience: +1. Keep it relevant to the main point +2. Include specific details (numbers, timeframes) +3. Connect back to broader implications +4. Don't overdo it—one or two per post is enough + +### Formatting Notes + +- Use `*italics*` for emphasis on key terms +- Use `**bold**` sparingly, mainly for key takeaways +- Lists only when actually listing items (not for general prose) +- Include images/screenshots where they add value +- End with "More posts like this" section linking to related content diff --git a/skills/charts/LICENSE.txt b/skills/charts/LICENSE.txt new file mode 100755 index 0000000..e092e50 --- /dev/null +++ b/skills/charts/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright (c) 2026 Z.ai All rights reserved. + +Permission is granted for personal, educational, and non-commercial use only. + +Commercial use is strictly prohibited without prior written permission from the author. + +Unauthorized copying, modification, or distribution of the software for commercial purposes is prohibited. + +The author reserves the right to make the final determination of what constitutes "commercial use". + +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 AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE. diff --git a/skills/charts/SKILL.md b/skills/charts/SKILL.md new file mode 100755 index 0000000..22c090a --- /dev/null +++ b/skills/charts/SKILL.md @@ -0,0 +1,427 @@ +--- +name: charts +metadata: + author: Z.AI + version: "1.0" +description: > + Professional chart and diagram creation skill. Covers all types of visual data + representation and structural diagrams: + - **Data charts**: bar charts, line charts, pie charts, scatter plots, heatmaps, + radar charts, candlestick charts, boxplots, histograms, area charts, waterfall charts, + regression plots, distribution plots, and statistical visualizations. + - **Structural diagrams**: flowcharts, mind maps, tree diagrams, org charts, + architecture diagrams, network/relationship graphs, ER diagrams, class diagrams, + Gantt charts, swimlane diagrams, and sequence diagrams. + - **Dashboards**: data dashboards, KPI panels, multi-chart compositions, + and interactive visualizations. + - **Design quality**: professional color systems, anti-overlap rules, layout optimization, + scene-based framework routing (matplotlib, seaborn, ECharts, D3.js, Mermaid, Playwright+CSS), + and publication-ready output. + Applies when the user wants to create, generate, draw, plot, visualize, or improve + any chart, graph, diagram, or dashboard. Also applies when the user asks for something + more polished, cleaner, or publication-ready. + NOT for: PDF document layout (use pdf skill), slide decks (use slides skill), + spreadsheets with embedded charts (use xlsx skill), AI image generation (use image_gen), + posters / infographics / creative cards (use pdf skill Creative pipeline). + FORBIDDEN: Using matplotlib/seaborn to draw mind maps, tree diagrams, org charts, + flowcharts, or any structural diagram. These MUST use Playwright+CSS. +license: Proprietary. LICENSE.txt has complete terms +--- + +# Beautiful Charts + +## Quick Setup + +```bash +bash "$SKILL_DIR/setup.sh" # Interactive environment check + install +``` + +Make every chart and diagram look professionally designed, not AI-generated. + +## Architecture + +| Module | File | When to Load | +|--------|------|-------------| +| **Routing + Core Rules** | This file | Always read first | +| **Framework Templates** | `references/` by framework | After choosing framework, read the corresponding file | + +**Loading order: Read this file → choose framework → read template file → start coding.** + +Each template file contains its own framework-specific rules (spacing, connectors, color details). This file contains only routing decisions and universal rules that apply to ALL charts. + +--- + +# Part 1: Routing + +## ⚠️ Format Constraint Rule (HIGHEST PRIORITY) + +**When the user specifies an output format/tool, you MUST comply. Never substitute.** + +| User Says | You Must Do | Forbidden | +|-----------|------------|-----------| +| "use mermaid code" / "用Mermaid格式输出" / "转化为mermaid" / "mermaid流程" | ① Output Mermaid code block (```mermaid ... ```) ② Also provide a rendered image preview | ❌ Cannot only give image without code; ❌ Cannot screenshot raw code text as image | +| "use markdown code" | Output markdown-formatted hierarchy | ❌ Cannot switch to HTML/CSS | +| "via mermaid or markdown code" | Choose one of the two, output code text | ❌ Cannot switch to any non-specified format | +| "flowchart" / "mind map" (no format specified) | Free to choose the best approach | - | +| "use echarts/d3" | Must use the specified framework | ❌ Cannot switch | + +### 🚫 FORBIDDEN: Mermaid Code Screenshot +**NEVER take a screenshot of raw Mermaid source code and deliver it as the "diagram image".** This is the worst possible outcome — the user gets neither usable code nor a visual diagram. When the user requests Mermaid format: +1. **MUST** output the Mermaid code in a fenced code block (````mermaid`) +2. **SHOULD** also render the code into a visual diagram image (via mermaid-cli or Playwright + mermaid.js) +3. If rendering fails, deliver the code block and tell the user to paste it into mermaid.live + +### Format Specified vs Auto-Upgrade Conflict +When the user specifies Mermaid but content triggers auto-upgrade conditions (>8 nodes, CJK-heavy, etc.): +1. **User choice wins** — still use Mermaid, deliver code block + rendered image +2. **Proactively guide** — after delivery, suggest the user try without specifying Mermaid for better layout quality +3. **Never silently switch** to Playwright+CSS when user explicitly asked for Mermaid + +When a specified tool hits rendering difficulties (e.g., mermaid CDN fails): +- ✅ Output raw mermaid code text, tell user to view at mermaid.live +- ❌ Secretly switch to another framework +- ❌ Screenshot the code text as an "image" + +--- + +## Routing Decision Tree + +### 1. Structural Diagrams + +#### 🔴 Flowchart Default: Phased Vertical (HIGHEST PRIORITY) + +**When the user asks to "generate/create a XXX flowchart/流程图" without specifying format, the DEFAULT layout is Phased Vertical (Layout C in `references/playwright-css.md`).** + +This is because nearly all real-world processes (manufacturing, legal proceedings, project management, business operations, cooking recipes, etc.) have natural phases/stages. Layout C produces the most professional, readable result. + +**Flowchart routing priority:** +1. **User specified Mermaid/markdown** → follow user choice (Format Constraint Rule) +2. **≤6 nodes AND no phases AND short text** → Mermaid (simple flowchart) +3. **Everything else** → **Playwright + CSS, Layout C (Phased Vertical)** → `references/playwright-css.md` + +**Phase detection — treat as "has phases" when ANY is true:** +- Content has numbered sections (一、二、三 or 1. 2. 3. or Phase 1/Stage 1) +- Process can be grouped by time/stage/role (e.g., "preparation → execution → review") +- Total steps ≥ 5 (almost always groupable into 2+ phases) +- Process involves multiple roles/departments +- Process has clear start/end with intermediate stages + +**⚠️ When in doubt, default to Layout C.** A phased layout with only 1 phase still looks professional. A Grid layout with phases looks like a mess. + +#### Other Structural Diagrams +- Simple flowchart (≤6 nodes, truly flat, no phases): **Mermaid** +- Complex flowchart (>6 nodes / CJK-heavy / branches / phases): **Playwright + CSS Layout C** → `references/playwright-css.md` +- Mind map / tree / org chart: **Playwright + CSS** → `references/mindmap-css.md` +- Relationship / network diagram: **ECharts graph** +- Center-radial analysis (SWOT / BSC / Porter's Five Forces / PEST): **Playwright + CSS** → `references/radial-grid.md` + +### 2. Data Charts (matplotlib / seaborn) +- Standard bar/line/scatter/heatmap/radar/pie: **matplotlib** +- Regression/distribution/boxplot: **Seaborn** + +### 3. Interactive Charts / Dashboards +- Data dashboard / candlestick / real-time: **ECharts** +- Fully custom interactive: **D3.js** + +### Default Strategy +**One scene, one tool — don't hesitate:** + +| Scene | Tool | Template | +|-------|------|----------| +| Data chart (bar/line/scatter/pie/radar) | matplotlib | `references/matplotlib.md` | +| Statistical (regression/box/dist) | Seaborn | `references/seaborn.md` | +| Mind map / tree / org chart | Playwright + CSS | `references/mindmap-css.md` | +| Center-radial (SWOT/BSC/PEST/Five Forces) | Playwright + CSS | `references/radial-grid.md` | +| **Any flowchart (default)** | **Playwright + CSS Layout C** | **`references/playwright-css.md`** | +| Simple flowchart (≤6 nodes, truly flat) | Mermaid | `references/mermaid.md` | +| Relationship / force-directed | ECharts graph | `references/echarts.md` | +| Data dashboard | ECharts | `references/echarts.md` | +| Academic paper figures | matplotlib | `references/matplotlib.md` | + +--- + +## Mermaid Auto-Upgrade Rules + +Mermaid's dagre/elk layout estimates CJK widths incorrectly. **Auto-switch to Playwright+CSS when ANY condition is met:** + +| Trigger | Action | +|---------|--------| +| Total nodes > **6** | → CSS flowchart (Layout C) | +| Any node text > **12 Chinese characters** | → CSS flowchart | +| More than **3 parallel branches** | → CSS flowchart | +| Nested subgraphs > **2 levels** | → CSS flowchart | +| Connector crossings > **2** | → CSS flowchart | +| **Side annotations / dashed note boxes** | → CSS flowchart | +| **Loop-back / cycle arrows** | → CSS flowchart | +| **Process has identifiable phases/stages** | → CSS flowchart (Layout C) | + +**If staying with Mermaid**: `padding: 32`, `nodeSpacing: 80`, `rankSpacing: 80`. Node text ≤ 10 CJK chars/line, wrap with `
`, quote all text `A["text"]`. + +--- + +## Large Dataset Rendering + +| Data Size | Approach | +|-----------|----------| +| < 1,000 points | matplotlib / any | +| 1,000 - 10,000 | matplotlib (no markers) or ECharts | +| 10,000 - 100,000 | ECharts (Canvas mode) | +| > 100,000 | ECharts (`large: true`) or WebGL | + +--- + +# Part 2: Universal Rules + +These rules apply to ALL charts regardless of framework. Framework-specific rules live in each template file. + +## 7 Core Rules + +1. **Zero overlap.** No element may cover another's text. Overlap = information loss = task failure. Post-generation: verify every element has clear separation. + +2. **Hierarchy over uniformity.** Primary nodes larger/bolder than secondary. Annotation nodes smaller/muted. Spacing between groups > within groups. If every box looks identical, the layout has failed. + +3. **Low-saturation palette.** 70% background/neutral, 20% secondary, 10% accent (one highlight only). No high-saturation large fills. Saturated colors only on borders (2px), text, and small elements. + +4. **Insight first.** Titles express conclusions, not field names. Remove non-essential elements: top/right borders, grid lines, tick marks, legend box borders. If removing it doesn't reduce understanding, it shouldn't exist. + +5. **Label clarity over label method.** The goal is zero overlap — choose the method that achieves it for each chart type. Direct labels, legends, and leader lines are all valid; what matters is that nothing overlaps. + +### 🚫 FORBIDDEN: Any Text Overlapping Any Other Element +**No label, legend, annotation, or title may overlap any other visual element.** This is the single most common matplotlib defect. Both direct labels AND legends can cause overlap — neither is inherently safe. + +**Anti-overlap decision tree:** +1. **Check if direct labels fit** — if all labels have enough space (bar tops, line endpoints, large pie slices), label directly. No legend needed. +2. **If some labels would collide** (small pie slices, dense scatter points, clustered bars) → use legend outside plot area instead of forcing labels into tight spaces. +3. **Mixed approach** — label the major items directly, group small items into "其他" or use leader lines + legend for the small ones. + +**Pie chart specific (the worst offender):** +- Slices < 5%: MUST use leader lines (`wedgeprops + texts` manual repositioning, or `matplotlib.patches.ConnectionPatch`) to pull labels outside. Do NOT rely on `autopct` alone — it places text inside/near the slice. +- Multiple small adjacent slices: use `bbox_to_anchor` legend outside, NOT direct labels +- `labeldistance=1.25` minimum to keep labels outside the pie +- When >2 slices are < 5%, consider grouping all < 3% into "其他(X项)" +- Use `adjustText` library to auto-resolve label collisions when available + +**Legend placement (when legend is needed):** +- Place legend **outside** the plot area using `bbox_to_anchor` +- Suggested starting positions: + - Bar/line/scatter: right side outside (`bbox_to_anchor=(1.02, 1), loc='upper left'`) + - Pie: right side outside (`bbox_to_anchor=(1.1, 0.5), loc='center left'`) + - Radar: below chart (`bbox_to_anchor=(0.5, -0.15), loc='upper center'`) + - Heatmap: no legend needed (colorbar suffices) + +**🔧 Mandatory: auto-adjust legend to prevent overlap.** Copy this snippet after placing any legend: +```python +# ── Auto-adjust legend position to prevent overlap ── +fig.canvas.draw() # must render first to get bboxes +legend = ax.get_legend() +if legend: + renderer = fig.canvas.get_renderer() + # Try shifting up to 5 times to resolve overlap + for _ in range(5): + leg_bb = legend.get_window_extent(renderer).transformed(ax.transAxes.inverted()) + has_overlap = False + for text in ax.texts + [ax.title] + ax.get_xticklabels() + ax.get_yticklabels(): + if not text.get_text(): + continue + txt_bb = text.get_window_extent(renderer).transformed(ax.transAxes.inverted()) + if leg_bb.overlaps(txt_bb): + has_overlap = True + break + if not has_overlap: + break + # Move legend further outside (direction depends on current loc) + bbox = legend.get_bbox_to_anchor().transformed(ax.transAxes.inverted()) + x0, y0 = bbox.x0, bbox.y0 + # Heuristic: if legend is below center, move down; if right of center, move right + if y0 < 0.5: + legend.set_bbox_to_anchor((x0, y0 - 0.08), transform=ax.transAxes) + else: + legend.set_bbox_to_anchor((x0 + 0.08, y0), transform=ax.transAxes) + fig.canvas.draw() +``` +- **After placing legend**: always call `plt.tight_layout()` or `fig.subplots_adjust()` to ensure legend is not clipped + +🚫 FORBIDDEN: +- `loc='best'` — matplotlib's "best" frequently overlaps data +- `loc='upper right'` / `loc='lower right'` on line/bar charts — high collision risk +- Direct labels on pie slices < 5% without leader lines +- Any text placement without verifying zero overlap + +6. **Font discipline.** Max 2 fonts. Chinese: SimHei/PingFang SC. Always explicitly set fonts in code. Font size follows hierarchy (title 18-24px → body 13-15px → annotation 11-13px). Never go below 10px floor. When text overflows: condense text → enlarge canvas → last resort: shrink font (but never below floor). + +7. **Whitespace is design.** Chart area 60-70% of canvas, margins 15-20%. At least 16pt between title and chart. Crowded ≠ information-rich. + +--- + +## Color System + +### Recommended Palettes + +| Palette | Text | Background | Block Fill | Accent | +|---------|------|------------|------------|--------| +| Business Cool | `#243447` | `#F8FAFC` | `#E9EEF3` | `#4C6EF5` | +| Tech Cyan-Gray | `#1F2937` | `#F5F7FA` | `#E6ECF2` | `#3AAFA9` | +| Morandi Warm | `#4B4A45` | `#FAF8F4` | `#EAE4DB` | `#C6866A` | +| Invisible Precision | `#37352F` | `#FFFFFF` | `#F7F7F7` | `#2383E2` | + +### 🚫 Forbidden Background Colors + +| Color | Forbidden Hex Values | +|-------|---------------------| +| Pure blue | `#3B82F6`, `#2563EB`, `#1D4ED8` | +| Pure green | `#10B981`, `#059669`, `#22C55E` | +| Pure red | `#EF4444`, `#DC2626`, `#F87171` | +| Pure purple | `#8B5CF6`, `#7C3AED`, `#A855F7` | +| Pure amber | `#F59E0B`, `#D97706`, `#FB923C` | + +### ✅ Allowed Background Colors + +| Color | Hex Values | +|-------|------------| +| Ice blue | `#EFF6FF`, `#DBEAFE` | +| Mint green | `#F0FDF4`, `#D1FAE5` | +| Light amber | `#FFF7ED`, `#FEF3C7` | +| Lavender | `#F5F3FF`, `#EDE9FE` | +| Light gray | `#F8FAFC`, `#F1F5F9` | + +### Functional Color (states only, not decoration) +- Active/Selected: brand accent or `2px` accent line +- Error: `#EF4444` +- Success: `#10B981` +- Tags: light bg + dark text, never high-sat pills + +### Colorblind-Safe +Don't rely on color alone — pair with shape, line style, or direct labels. +Paul Tol palette: `['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377']` + +### Dark Theme +- Background: `#0F172A` (not pure black) +- Text: `#F1F5F9` (not pure white) +- Grid: `#1E293B`, low alpha +- Export: `savefig(facecolor='#0F172A')` + +--- + +## Export Rules + +- Static charts: minimum 200 DPI, recommended 300 DPI +- Pie/radar: **square `figsize=(8, 8)`** — non-square = elliptical +- No more than 6 colors per chart (split if more) +- Bar chart Y-axis starts at 0 (line charts may truncate) +- Never use 3D (distorts proportions) + +### Playwright Screenshot +Default `device_scale_factor=2`. Large mind maps (3000px+): 1.5. PDF embed: 1-1.5. Print: 3. +After render, read `bounding_box()` and resize viewport to fit. Min viewport: 800px single-col, 1200px multi-col. + +### 🚫 FORBIDDEN: `max-width` on Mermaid/SVG Containers +Mermaid's dagre engine produces SVGs with unpredictable width (especially with subgraphs, CJK text, or parallel branches). **NEVER set `max-width` on the Mermaid container element.** Use `width: fit-content; min-width: 800px;` instead. + +**Root cause**: Mermaid SVGs overflow their CSS container silently. `bounding_box()` (Playwright) returns the CSS box model size, NOT the SVG's actual rendered size. So auto-resize viewport based on `bounding_box()` alone will still produce clipped screenshots. + +**Fix**: Always read the **SVG element's own `getBoundingClientRect()`** via `page.evaluate()`, then use `max(css_size, svg_size) + padding` for viewport dimensions. See `references/mermaid.md` for the corrected screenshot script. + +### Aspect Ratio Preservation (embedding) +**MUST read actual image dimensions and calculate height proportionally. NEVER hardcode both width and height.** + +--- + +## matplotlib-Specific Rules + +These apply when routing to matplotlib/seaborn: + +### Layout & Overlap +- Prefer `constrained_layout=True` over `tight_layout()` +- Use `adjustText` library for automatic label repositioning — **this is the most reliable anti-overlap tool for matplotlib.** Install: `pip install adjustText`. Usage: `from adjustText import adjust_text; adjust_text(texts)` +- Max 4 subplots per canvas. More → split images or `figsize=(20, 16)` minimum +- Multi-subplot: `GridSpec` with `wspace/hspace` ≥ 0.3 +- Colorbar: `shrink=0.8` + `pad=0.08` +- Data labels: Y-axis upper limit with 15-20% headroom (`ylim(0, max_val * 1.18)`) +- Long X labels → horizontal bar chart or show every N-th label + +### Radar / Spider Charts +- **Every `fill()` MUST have `alpha=0.25`** (max 0.3). Omitting alpha = opaque = hides underlying series. +- Legend: place outside chart with `bbox_to_anchor`, start with `(0.5, -0.15), loc='upper center'`. If dimension labels are long or dimensions > 8, increase offset (e.g., `-0.25` or `-0.3`). Also FORBIDDEN: `loc='lower right'` (collides with radar dimension labels). +- Dimension label padding: `set_rlim(0, max_value * 1.2)` +- Labels with >4 CJK chars: rotate to follow angle or abbreviate +- `figsize=(8, 8)` mandatory (square) + +### One Color, Gray the Rest +5 lines → color only the key one, others `#D1D5DB`. 8 bars → accent only the highlight, rest `#E5E7EB`. + +--- + +## Connector Rules (structural diagrams) + +- Attach to node edges, not through centers +- Prefer orthogonal polylines or clean curves +- Main paths avoid crossing +- Never pass through text areas +- Start/end points at same level must align (no staggering) +- Same-level connectors follow same direction +- Bend angles consistent (all right-angles or all curves, no mixing) +- Label positions uniform (all above line or all centered) + +--- + +## Pre-Output Checklist + +Before delivery, verify: + +- [ ] Zero overlap (nodes, connectors, labels, legends — **especially check legend vs data, and adjacent pie/bar labels**) +- [ ] No connectors pass through text boxes +- [ ] Clear hierarchy (primary/secondary/annotation visually distinct) +- [ ] Low-saturation palette (no forbidden background colors) +- [ ] Text readable at final size (standalone: ≥12px body, ≥10px annotation; PDF embed: ≥10pt/8pt/7pt) +- [ ] Legend fully visible, not clipped, not overlapping any chart element +- [ ] Canvas wide/tall enough (check bounding box before screenshot) +- [ ] **If mind map**: each level distinct (≥3 property changes), connectors visible (≥ `#94A3B8`), left-right balanced +- [ ] **If flowchart**: phase titles distinct from steps, arrows only between phases, **using Layout C by default** +- [ ] **If flowchart**: phase colors are same-hue family (blue-gray progression), **NOT rainbow** (blue→green→amber→purple) +- [ ] **If flowchart looks scattered**: STOP — you're using the wrong layout, switch to Layout C +- [ ] **If Mermaid looked rigid**: already switched to Playwright+CSS + +--- + +## Anti-Pattern Quick Reference + +| ❌ Don't | ✅ Do This Instead | +|----------|-------------------| +| matplotlib default blue `#1f77b4` | Use this skill's palette | +| 3D bar/pie | Always 2D | +| Rainbow colormap (jet/rainbow) | Single-hue gradient or diverging | +| Thick black grid lines | `alpha=0.08` or remove | +| Different color per bar | Same series same color, highlight only key | +| 45° tilted X labels | Horizontal bar chart or shorten | +| 8+ subplots in one canvas | Split to 2-3 images, max 4 each | +| `tight_layout()` alone | `constrained_layout=True` or `GridSpec` | +| Labels overflowing chart | `ylim` with 18-25% headroom | +| Mind map: all levels same style | Root+L1 get boxes, leaves plain text | +| Mind map: image too tall | Left-right layout for ≥5 branches | +| Mind map: invisible connectors | Lines ≥ `#94A3B8`, root→L1 `#64748B` 2.5px | +| Mind map: unbalanced sides | Alternate large/small branches across sides | +| Flowchart: high-sat node fills | Low-sat bg (`#EFF6FF`) + sat border (`#3B82F6`) | +| Flowchart: dark bg + dark text | Dark bg → white text. Light bg → dark text | +| Flowchart: arrows between every step | Arrows ONLY between phases, steps use indent | +| Flowchart: cross-layer lines through nodes | Connect adjacent layers only | +| Flowchart: Grid layout for phased process | **Always use Layout C (Phased Vertical)** | +| Flowchart: phase titles as floating labels | Phase titles MUST be inside group cards | +| Flowchart: nodes scattered without grouping | Group nodes into phase cards with `.phase-group` | +| Flowchart: rainbow phase colors (blue→green→amber→purple) | Same-hue blue-gray progression for all phases | +| Multiple arrows to same entry point | Merge-then-enter pattern | +| Legend inside plot obscuring data | `bbox_to_anchor` outside plot area | +| Radar fill without alpha | `alpha=0.25` mandatory | +| Decorative icons/emoji | Let the data speak | +| Grid lines where whitespace suffices | Background contrast or spacing instead | + +--- + +## UI Aesthetics (dashboards / card layouts) + +When building UI-style outputs (dashboards, panels), apply "Invisible Precision": + +- **Boundaries**: Subtle bg shifts (`#F7F7F7` on `#FFFFFF`), not border lines. Reserve `1px` dividers for absolute logical breaks only. +- **Actions**: Primary CTA in dark neutral (`#1A1A1B`). Secondary: ghost/gray. Hover: 5% darker, no size change. +- **Quiet UI**: Action buttons `opacity: 0` by default, `1` on hover. Only active elements get visual indicators. +- **Numbers**: `font-variant-numeric: tabular-nums` for strict vertical alignment. +- **Spacing**: `line-height: 1.625`, generous paragraph spacing. diff --git a/skills/charts/references/_rules.md b/skills/charts/references/_rules.md new file mode 100755 index 0000000..de27653 --- /dev/null +++ b/skills/charts/references/_rules.md @@ -0,0 +1,49 @@ +# ⚠️ STRUCTURAL DIAGRAM IRON LAWS + +These rules apply to Playwright+CSS structural diagrams (flowcharts, mind maps, radial grids, org charts). They are enforced by the template files. Violating any = task failure. + +## 1. ZERO OVERLAP +No element may overlap another: +- Arrows/connectors must not cross over text boxes +- Connectors must not pass through node bodies +- Text boxes / nodes must not overlap each other +- Labels must not obscure any graphic element + +**Post-generation**: verify every element has clear separation. If overlap exists, fix before delivery — enlarge canvas, increase spacing, or reduce content. + +## 2. LAYOUT MUST HAVE HIERARCHY +Forbidden: all nodes same size, same level, mechanically tiled in a flat grid. + +Required: +- Primary nodes visually larger/bolder than secondary nodes +- Annotation nodes clearly subordinate (smaller, muted color) +- Spacing between groups > spacing within groups +- Clear reading path (top→bottom, left→right, or center→outward) + +**Squint test**: if every box looks identical, the layout has failed. + +## 3. NODE BACKGROUND COLORS + +**🚫 Forbidden as background (too saturated for large fills):** + +| Color | Forbidden Hex | +|-------|--------------| +| Pure blue | `#3B82F6`, `#2563EB`, `#1D4ED8` | +| Pure green | `#10B981`, `#059669`, `#22C55E` | +| Pure red | `#EF4444`, `#DC2626`, `#F87171` | +| Pure purple | `#8B5CF6`, `#7C3AED`, `#A855F7` | +| Pure amber | `#F59E0B`, `#D97706`, `#FB923C` | +| Any color: R/G/B > 0xCC and saturation > 50% | — | + +**✅ Allowed as background:** + +| Color | Hex | Usage | +|-------|-----|-------| +| Ice blue | `#EFF6FF`, `#DBEAFE` | Normal step nodes | +| Mint green | `#F0FDF4`, `#D1FAE5` | Success/pass nodes | +| Light amber | `#FFF7ED`, `#FEF3C7` | Decision/warning nodes | +| Lavender | `#F5F3FF`, `#EDE9FE` | End/terminal nodes | +| Light gray | `#F8FAFC`, `#F1F5F9` | Group containers | +| White | `#FFFFFF` | Default canvas | + +**Rule: Saturated colors go on BORDERS (2px) and TEXT only. Backgrounds stay pale.** diff --git a/skills/charts/references/d3.md b/skills/charts/references/d3.md new file mode 100755 index 0000000..8ae4ad1 --- /dev/null +++ b/skills/charts/references/d3.md @@ -0,0 +1,199 @@ +# D3.js Template Library + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + + +D3.js is the "ultimate weapon" in visualization — highest freedom, but steepest learning curve. +Suitable for: visualizations requiring fully custom interactions, data journalism, art-grade infographics. + +**If your needs can be met by ECharts/matplotlib, don't use D3.** D3's value lies in "charts others can't make". + +## HTML Universal Shell + +```html + + + + +{{TITLE}} + + + + + + + + +``` + +## Colors and Constants + +```javascript +const COLORS = ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981']; +const GRAY = { 200: '#E5E7EB', 300: '#D1D5DB', 400: '#9CA3AF', 500: '#6B7280', 900: '#111827' }; + +const margin = { top: 60, right: 40, bottom: 50, left: 60 }; +const width = 900 - margin.left - margin.right; +const height = 500 - margin.top - margin.bottom; +``` + +--- + +## Template 1: Insight Bar Chart + +```javascript +const svg = d3.select('#chart') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', `translate(${margin.left},${margin.top})`); + +// Title (insight-driven) +svg.append('text') + .attr('x', 0).attr('y', -30) + .attr('fill', GRAY[900]) + .attr('font-size', 16).attr('font-weight', 'bold') + .text('产品C销售额领先,达到89万'); + +// Scales +const x = d3.scaleBand().domain(data.map(d => d.name)).range([0, width]).padding(0.35); +const y = d3.scaleLinear().domain([0, d3.max(data, d => d.value) * 1.15]).range([height, 0]); + +// Y axis (minimal: no tick marks, light gray) +svg.append('g') + .call(d3.axisLeft(y).ticks(5).tickSize(0).tickFormat(d3.format(','))) + .call(g => g.select('.domain').attr('stroke', GRAY[200]).attr('stroke-width', 0.8)) + .call(g => g.selectAll('.tick text').attr('fill', GRAY[500]).attr('font-size', 9)); + +// X axis +svg.append('g') + .attr('transform', `translate(0,${height})`) + .call(d3.axisBottom(x).tickSize(0)) + .call(g => g.select('.domain').attr('stroke', GRAY[200]).attr('stroke-width', 0.8)) + .call(g => g.selectAll('.tick text').attr('fill', GRAY[500]).attr('font-size', 9)); + +// Bars (gray + highlight) +svg.selectAll('.bar') + .data(data) + .join('rect') + .attr('x', d => x(d.name)) + .attr('width', x.bandwidth()) + .attr('y', d => y(d.value)) + .attr('height', d => height - y(d.value)) + .attr('fill', d => d.highlight ? '#3B82F6' : GRAY[200]) + .attr('rx', 3); + +// Value labels +svg.selectAll('.label') + .data(data) + .join('text') + .attr('x', d => x(d.name) + x.bandwidth()/2) + .attr('y', d => y(d.value) - 6) + .attr('text-anchor', 'middle') + .attr('fill', d => d.highlight ? GRAY[900] : GRAY[400]) + .attr('font-size', d => d.highlight ? 12 : 10) + .attr('font-weight', d => d.highlight ? 'bold' : 'normal') + .text(d => d3.format(',')(d.value)); +``` + +--- + +## Template 2: Force-Directed Graph + +This is D3's killer feature — other frameworks can hardly achieve the same effect. + +```javascript +const simulation = d3.forceSimulation(nodes) + .force('link', d3.forceLink(links).id(d => d.id).distance(80)) + .force('charge', d3.forceManyBody().strength(-200)) + .force('center', d3.forceCenter(width/2, height/2)) + .force('collision', d3.forceCollide(20)); + +const link = svg.selectAll('.link') + .data(links).join('line') + .attr('stroke', GRAY[200]).attr('stroke-width', 1); + +const node = svg.selectAll('.node') + .data(nodes).join('circle') + .attr('r', d => Math.sqrt(d.value) * 3) + .attr('fill', (d, i) => COLORS[d.group % COLORS.length]) + .attr('stroke', '#fff').attr('stroke-width', 1.5) + .call(d3.drag() + .on('start', (e, d) => { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; }) + .on('drag', (e, d) => { d.fx = e.x; d.fy = e.y; }) + .on('end', (e, d) => { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; }) + ); + +// Labels +const labels = svg.selectAll('.label') + .data(nodes).join('text') + .text(d => d.name) + .attr('font-size', 9).attr('fill', GRAY[500]) + .attr('text-anchor', 'middle').attr('dy', -15); + +simulation.on('tick', () => { + link.attr('x1', d => d.source.x).attr('y1', d => d.source.y) + .attr('x2', d => d.target.x).attr('y2', d => d.target.y); + node.attr('cx', d => d.x).attr('cy', d => d.y); + labels.attr('x', d => d.x).attr('y', d => d.y); +}); +``` + +--- + +## Template 3: Treemap + +```javascript +const root = d3.hierarchy(treeData).sum(d => d.value); +d3.treemap().size([width, height]).padding(2)(root); + +svg.selectAll('.cell') + .data(root.leaves()) + .join('rect') + .attr('x', d => d.x0).attr('y', d => d.y0) + .attr('width', d => d.x1 - d.x0) + .attr('height', d => d.y1 - d.y0) + .attr('fill', (d, i) => COLORS[i % COLORS.length]) + .attr('rx', 2).attr('opacity', 0.85); + +svg.selectAll('.cell-label') + .data(root.leaves().filter(d => (d.x1-d.x0) > 40 && (d.y1-d.y0) > 20)) + .join('text') + .attr('x', d => d.x0 + 4).attr('y', d => d.y0 + 14) + .text(d => d.data.name) + .attr('font-size', 10).attr('fill', 'white').attr('font-weight', 'bold'); +``` + +--- + +## D3 Use Cases (vs Overkill) + +| ✅ D3 Best | ❌ Overkill | +|-----------|-------------| +| Force-directed relationship graph | Regular bar chart (use matplotlib) | +| Custom geographic visualization | Standard map (use ECharts) | +| Data-driven animation | Static report chart (use matplotlib) | +| Treemap / Sunburst | Standard pie chart (use matplotlib) | +| Complex interactions (brushing/linking/drill-down) | Simple tooltip (use ECharts) | +| Data journalism/narrative visualization | Dashboard (use ECharts) | diff --git a/skills/charts/references/echarts.md b/skills/charts/references/echarts.md new file mode 100755 index 0000000..011c468 --- /dev/null +++ b/skills/charts/references/echarts.md @@ -0,0 +1,651 @@ +# ECharts Template Library + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + + +ECharts strengths: interactivity (tooltip/zoom/linking), big data (Canvas renders millions of points smoothly), strong Chinese community. +Output as HTML files — open directly in browser or export PNG via Playwright screenshot. + +## HTML Universal Shell + +Wrap all ECharts charts with this shell: + +```html + + + + +{{TITLE}} + + + + +
+ + + +``` + +Default dimensions: `width=900, height=520`, white background `#FFFFFF`. + +--- + +## Theme Configuration + +### Light Theme (Default) + +```javascript +const THEME = { + bg: '#FFFFFF', + text: '#111827', + textSub: '#6B7280', + textMuted: '#9CA3AF', + axis: '#E5E7EB', + grid: '#F3F4F6', + tooltip: { bg: '#1E293B', border: '#334155', text: '#F1F5F9' }, + colors: ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981'], +}; +``` + +### Dark Theme (Finance / Tech Dashboard) + +```javascript +const DARK = { + bg: '#0F172A', + text: '#F1F5F9', + textSub: '#94A3B8', + textMuted: '#64748B', + axis: '#334155', + grid: '#1E293B', + tooltip: { bg: '#1E293B', border: '#475569', text: '#F1F5F9' }, + colors: ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#22C55E', '#EC4899'], +}; +``` + +### Base Option Configuration + +```javascript +function baseOption(theme, title, subtitle) { + return { + backgroundColor: theme.bg, + textStyle: { fontFamily: 'system-ui, SimHei, sans-serif', color: theme.text }, + title: { + text: title, subtext: subtitle || '', + left: 24, top: 16, + textStyle: { fontSize: 16, fontWeight: 'bold', color: theme.text }, + subtextStyle: { fontSize: 12, color: theme.textSub }, + }, + grid: { left: 60, right: 40, top: 80, bottom: 50, containLabel: true }, + color: theme.colors, + tooltip: { + trigger: 'axis', + backgroundColor: theme.tooltip.bg, + borderColor: theme.tooltip.border, + borderWidth: 1, + textStyle: { color: theme.tooltip.text, fontSize: 12 }, + }, + animationDuration: 600, + animationEasing: 'cubicOut', + }; +} + +function cleanAxis(theme) { + return { + axisLine: { lineStyle: { color: theme.axis, width: 0.8 } }, + axisTick: { show: false }, + splitLine: { lineStyle: { color: theme.grid, width: 0.5 } }, + axisLabel: { color: theme.textSub, fontSize: 10 }, + }; +} +``` + +--- + +## Template 1: Insight Bar Chart + +```javascript +const option = { + ...baseOption(THEME, 'Q3 收入环比增长 47%', '各季度收入对比(万元)'), + xAxis: { type: 'category', data: ['Q1','Q2','Q3','Q4'], ...cleanAxis(THEME) }, + yAxis: { type: 'value', ...cleanAxis(THEME) }, + series: [{ + type: 'bar', barWidth: '50%', + itemStyle: { borderRadius: [4, 4, 0, 0] }, + data: [ + { value: 120, itemStyle: { color: '#E5E7EB' } }, + { value: 145, itemStyle: { color: '#E5E7EB' } }, + { value: 213, itemStyle: { color: '#3B82F6' } }, + { value: 180, itemStyle: { color: '#E5E7EB' } }, + ], + label: { + show: true, position: 'top', fontSize: 11, color: '#6B7280', + formatter: (p) => p.dataIndex === 2 + ? '{hl|' + p.value + '}' + : p.value, + rich: { hl: { fontWeight: 'bold', fontSize: 13, color: '#111827' } }, + }, + }], +}; +``` + +--- + +## Template 2: Multi-Line Trend + +```javascript +const option = { + ...baseOption(THEME, '2024 年增长持续加速'), + legend: { right: 40, top: 20, textStyle: { color: '#6B7280', fontSize: 10 } }, + xAxis: { type: 'category', data: months, boundaryGap: false, ...cleanAxis(THEME) }, + yAxis: { type: 'value', ...cleanAxis(THEME) }, + series: [ + { + name: '2024', type: 'line', data: thisYear, + lineStyle: { width: 2.5 }, + symbol: 'circle', symbolSize: 6, + itemStyle: { color: '#3B82F6' }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ + { offset: 0, color: 'rgba(59,130,246,0.12)' }, + { offset: 1, color: 'rgba(59,130,246,0)' }, + ]), + }, + }, + { + name: '2023', type: 'line', data: lastYear, + lineStyle: { width: 1.5, type: 'dashed', color: '#D1D5DB' }, + symbol: 'none', itemStyle: { color: '#D1D5DB' }, + }, + ], +}; +``` + +--- + +## Template 3: Candlestick (Finance) + +```javascript +// Dark theme +const option = { + ...baseOption(DARK, 'BTC/USDT 日K线'), + xAxis: { type: 'category', data: dates, ...cleanAxis(DARK) }, + yAxis: { type: 'value', scale: true, ...cleanAxis(DARK) }, + dataZoom: [ + { type: 'inside', start: 70, end: 100 }, + { type: 'slider', start: 70, end: 100, height: 20, bottom: 10, + borderColor: DARK.axis, fillerColor: 'rgba(59,130,246,0.15)', + textStyle: { color: DARK.textSub } }, + ], + series: [{ + type: 'candlestick', + data: ohlcData, // [[open,close,low,high], ...] + itemStyle: { + color: '#22C55E', // Bullish (close > open) + color0: '#EF4444', // Bearish + borderColor: '#16A34A', + borderColor0: '#DC2626', + }, + }], +}; +``` + +--- + +## Template 4: Dashboard (Multi-Chart Linking) + +⚠️ **ECharts multi-chart dashboard anti-overlap rules** (highest priority): +1. Maximum 4 subplots per canvas; more must be split into multiple HTML files +2. Each subplot's `grid` area must not overlap; maintain ≥5% safety margin between adjacent grids +3. Pie chart `center` and `radius` must not intrude into other subplots' grid areas +4. Same applies to radar chart's `radar.center` and `radar.radius` +5. Place legend in top/bottom common area, not inside subplots + +### Simple Dual Chart (Bar + Pie) + +```javascript +const option = { + ...baseOption(THEME, '产品线收入分布'), + grid: [{ left: 60, right: '55%', top: 80, bottom: 50 }], + xAxis: [{ type: 'value', gridIndex: 0, ...cleanAxis(THEME) }], + yAxis: [{ type: 'category', data: products, gridIndex: 0, ...cleanAxis(THEME) }], + series: [ + { + type: 'bar', data: revenues, + itemStyle: { borderRadius: [0, 4, 4, 0] }, + barWidth: '60%', + }, + { + type: 'pie', center: ['78%', '50%'], radius: ['35%', '55%'], + data: products.map((name, i) => ({ name, value: revenues[i] })), + label: { formatter: '{b}\n{d}%', fontSize: 10 }, + itemStyle: { borderColor: '#fff', borderWidth: 2 }, + }, + ], +}; +``` + +### Four-Chart Dashboard (Safe Layout Template) + +```javascript +// ⚠️ Key: grid areas precisely defined, no overlap, maintain safety margins +const option = { + ...baseOption(THEME, '数据全景仪表盘'), + grid: [ + // Top-left: bar chart + { left: '5%', right: '55%', top: '12%', bottom: '55%' }, + // Top-right: line chart + { left: '55%', right: '5%', top: '12%', bottom: '55%' }, + // Bottom-left: scatter plot + { left: '5%', right: '55%', top: '55%', bottom: '5%' }, + // Bottom-right area reserved for pie chart (pie uses center + radius, not grid) + ], + xAxis: [ + { type: 'category', gridIndex: 0, data: categories1, ...cleanAxis(THEME) }, + { type: 'category', gridIndex: 1, data: categories2, ...cleanAxis(THEME), boundaryGap: false }, + { type: 'value', gridIndex: 2, ...cleanAxis(THEME) }, + ], + yAxis: [ + { type: 'value', gridIndex: 0, ...cleanAxis(THEME) }, + { type: 'value', gridIndex: 1, ...cleanAxis(THEME) }, + { type: 'value', gridIndex: 2, ...cleanAxis(THEME) }, + ], + series: [ + // Top-left: bar chart + { + type: 'bar', xAxisIndex: 0, yAxisIndex: 0, + data: barData, + itemStyle: { borderRadius: [4, 4, 0, 0] }, + }, + // Top-right: line chart + { + type: 'line', xAxisIndex: 1, yAxisIndex: 1, + data: lineData, + smooth: true, + areaStyle: { opacity: 0.08 }, + }, + // Bottom-left: scatter plot + { + type: 'scatter', xAxisIndex: 2, yAxisIndex: 2, + data: scatterData, + symbolSize: 8, + }, + // Bottom-right: pie chart (positioned via center in bottom-right quadrant) + { + type: 'pie', + center: ['77%', '72%'], // Positioned at bottom-right area center + radius: ['15%', '25%'], // Radius stays within bottom-right quadrant + data: pieData, + label: { formatter: '{b}\n{d}%', fontSize: 10 }, + itemStyle: { borderColor: '#fff', borderWidth: 2 }, + }, + ], +}; +``` + +### Grid Safety Margin Quick Reference + +| Layout | Grid Config | Safety Margin | +|------|----------|---------| +| Left-right dual | Left `right:'55%'` Right `left:'55%'` | 10% center gap | +| Top-bottom dual | Top `bottom:'55%'` Bottom `top:'55%'` | 10% center gap | +| 2x2 quad | Each quadrant 45%, 10% center gap | 5% margin on all sides | +| With pie/radar | Pie center+radius must not intrude grid | Pie radius ≤ 40% of available area | + +### What If More Than 4 Subplots? + +```javascript +// ❌ Wrong: 8 charts crammed into one canvas — all labels will inevitably overlap +// ✅ Correct: split into 2 HTML files + +// dashboard_overview.html — 4 overview charts +// dashboard_detail.html — 4 detailed analysis charts + +// Or use tab switching (ECharts toolbox doesn't support this, need custom HTML tabs) +``` + +--- + +## Template 5: Radar Chart + +```javascript +const option = { + ...baseOption(THEME, '团队能力评估'), + radar: { + indicator: dims.map(d => ({ name: d, max: 100 })), + axisName: { color: '#6B7280', fontSize: 10 }, + splitArea: { areaStyle: { color: ['#FAFAFA', '#F5F5F5'] } }, + splitLine: { lineStyle: { color: '#E5E7EB' } }, + axisLine: { lineStyle: { color: '#E5E7EB' } }, + }, + series: [{ + type: 'radar', + data: teams.map((t, i) => ({ + name: t.name, value: t.scores, + lineStyle: { width: 2 }, + areaStyle: { opacity: 0.08 }, + itemStyle: { color: THEME.colors[i] }, + })), + }], + legend: { bottom: 10, textStyle: { color: '#6B7280' } }, +}; +``` + +--- + +## Export to PNG (Playwright) + +```python +import asyncio +from playwright.async_api import async_playwright + +async def echarts_to_png(html_path, png_path, width=900, height=520): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page(viewport={'width': width, 'height': height}) + await page.goto(f'file://{html_path}', wait_until='networkidle') + await page.wait_for_timeout(800) # Wait for animation to complete + await page.locator('#chart').screenshot(path=png_path) + await browser.close() + print(f'✅ {png_path}') + +# asyncio.run(echarts_to_png('./output/chart.html', './output/chart.png')) +``` + +--- + +## Template 5: Tree (Interactive Only) + +**⚠️ For static PNG export, use Playwright+CSS (see `mindmap-css.md`). ECharts tree connector length and node spacing cannot be finely controlled — static output is not aesthetically satisfying.** + +**ECharts tree is suitable for interactive scenarios** (click expand/collapse, hover tooltip, zoom/drag). For PNG/PDF static output, the CSS approach looks better. + +### Basic Usage + +```javascript +const option = { + tooltip: { trigger: 'item', triggerOn: 'mousemove' }, + series: [{ + type: 'tree', + data: [treeData], // Tree-structured JSON data + layout: 'orthogonal', // Orthogonal layout (right-angle connectors) + orient: 'LR', // Direction: LR(left→right) / RL / TB(top→bottom) / BT + + // Node spacing control (key params to prevent crowding) + initialTreeDepth: -1, // -1=expand all, positive=initial expand depth + + // Label style + label: { + position: 'left', // Leaf node label position + verticalAlign: 'middle', + fontSize: 13, + fontFamily: 'PingFang SC, SimHei, sans-serif', + }, + leaves: { + label: { position: 'right' } // Leaf labels on right + }, + + // Connector style + lineStyle: { + color: '#94A3B8', + width: 1.5, + curveness: 0.5, // Curvature, 0=straight, 0.5=natural curve + }, + + // Node style + itemStyle: { + borderWidth: 1.5, + }, + + // Animation + animationDuration: 550, + animationDurationUpdate: 750, + }] +}; +``` + +### Tree Data Format + +```javascript +const treeData = { + name: '中心主题', + children: [ + { + name: '分支A', + children: [ + { name: '叶子1' }, + { name: '叶子2' }, + { name: '叶子3', children: [{ name: '更深叶子' }] } + ] + }, + { + name: '分支B', + children: [ + { name: '叶子4' }, + { name: '叶子5' } + ] + } + ] +}; +``` + +### Node Style Customization (by Level) + +```javascript +// Root node highlight +function styleTreeData(node, depth) { + const styles = [ + { // Root node + itemStyle: { color: '#3B82F6', borderColor: '#2563EB', borderWidth: 2 }, + label: { fontSize: 18, fontWeight: 'bold', color: '#fff', + backgroundColor: '#3B82F6', borderRadius: 6, padding: [8, 16] } + }, + { // Level 1 branches + itemStyle: { color: '#60A5FA', borderColor: '#3B82F6' }, + label: { fontSize: 15, fontWeight: 600, color: '#1E40AF', + backgroundColor: '#EFF6FF', borderColor: '#3B82F6', + borderWidth: 1.5, borderRadius: 6, padding: [6, 14] } + }, + { // Level 2 + itemStyle: { color: '#93C5FD', borderColor: '#60A5FA' }, + label: { fontSize: 13, color: '#1E40AF', + backgroundColor: '#F0F7FF', borderColor: '#93C5FD', + borderWidth: 1, borderRadius: 4, padding: [4, 10] } + }, + { // Level 3+ leaves + itemStyle: { color: '#BFDBFE', borderColor: '#93C5FD' }, + label: { fontSize: 12, color: '#475569', padding: [3, 8] } + } + ]; + + const style = styles[Math.min(depth, styles.length - 1)]; + Object.assign(node, style); + + if (node.children) { + node.children.forEach(child => styleTreeData(child, depth + 1)); + } +} + +styleTreeData(treeData, 0); +``` + +### Left-Right Distribution (Large Tree Mode) + +When branches ≥ 5, use two trees for left-right expansion: + +```javascript +function splitTree(data) { + const children = data.children || []; + // Alternate assignment by subtree size + const sorted = children.map((c, i) => ({ c, w: countNodes(c), i })) + .sort((a, b) => b.w - a.w); + const left = [], right = []; + let lw = 0, rw = 0; + sorted.forEach(({ c }) => { + if (lw <= rw) { left.push(c); lw += countNodes(c); } + else { right.push(c); rw += countNodes(c); } + }); + return { + left: { name: data.name, children: left }, + right: { name: data.name, children: right } + }; +} + +function countNodes(node) { + if (!node.children) return 1; + return 1 + node.children.reduce((s, c) => s + countNodes(c), 0); +} + +// Dual tree series +const { left, right } = splitTree(treeData); +const option = { + series: [ + { type: 'tree', data: [right], orient: 'LR', left: '50%', width: '45%', /* ... */ }, + { type: 'tree', data: [left], orient: 'RL', right: '50%', width: '45%', /* ... */ }, + ] +}; +``` + +### Recommended Canvas Size + +| Node Count | Width | Height | +|--------|------|------| +| ≤ 15 | 900px | 500px | +| 16-30 | 1200px | 600px | +| 31-60 | 1600px | 800px | +| 60+ | 2000px | 1000px | + +--- + +## Template 6: Relationship / Force-Directed Graph + +**ECharts graph suits process relationships, org charts, knowledge graphs** — nodes auto-repel to avoid overlap, connectors auto-bind, supports categorical coloring. + +### Basic Usage + +```javascript +const option = { + tooltip: {}, + legend: [{ data: categories.map(c => c.name) }], + series: [{ + type: 'graph', + layout: 'force', // Force-directed auto layout + + // Force model params (controls repulsion and attraction) + force: { + repulsion: 300, // Repulsion force (higher = more spread out, recommended 200-500) + gravity: 0.1, // Gravity (prevents nodes from flying too far) + edgeLength: [100, 200], // Edge length range + layoutAnimation: true, + }, + + roam: true, // Allow drag and zoom + draggable: true, // Allow dragging nodes + + // Nodes + data: nodes, + // Edges + links: links, + // Categories (for coloring) + categories: categories, + + // Labels + label: { + show: true, + position: 'right', + fontSize: 12, + fontFamily: 'PingFang SC, SimHei, sans-serif', + }, + + // Connector style + lineStyle: { + color: 'source', // Edge color follows source node + curveness: 0.3, // Curvature + width: 1.5, + }, + + // Highlight effect + emphasis: { + focus: 'adjacency', // Highlight adjacent nodes on hover + lineStyle: { width: 3 }, + }, + }] +}; +``` + +### Data Format + +```javascript +const categories = [ + { name: '核心系统', itemStyle: { color: '#3B82F6' } }, + { name: '数据层', itemStyle: { color: '#10B981' } }, + { name: '应用层', itemStyle: { color: '#F59E0B' } }, +]; + +const nodes = [ + { name: 'API Gateway', category: 0, symbolSize: 40 }, + { name: 'User Service', category: 0, symbolSize: 30 }, + { name: 'MySQL', category: 1, symbolSize: 35 }, + { name: 'Redis', category: 1, symbolSize: 28 }, + { name: 'Web App', category: 2, symbolSize: 32 }, +]; + +const links = [ + { source: 'API Gateway', target: 'User Service' }, + { source: 'User Service', target: 'MySQL' }, + { source: 'User Service', target: 'Redis' }, + { source: 'Web App', target: 'API Gateway' }, +]; +``` + +### Flowchart Mode (Fixed Layout) + +When you don't want force-directed auto-layout, fix node positions: + +```javascript +const option = { + series: [{ + type: 'graph', + layout: 'none', // Fixed layout, positions determined by x/y + data: [ + { name: '开始', x: 300, y: 50, symbolSize: 40, + itemStyle: { color: '#EFF6FF', borderColor: '#3B82F6', borderWidth: 2 } }, + { name: '处理', x: 300, y: 200, symbolSize: 35 }, + { name: '判断', x: 300, y: 350, symbolSize: 35, + symbol: 'diamond', + itemStyle: { color: '#FFF7ED', borderColor: '#F59E0B', borderWidth: 2 } }, + { name: '结束', x: 300, y: 500, symbolSize: 40 }, + ], + links: [ + { source: '开始', target: '处理' }, + { source: '处理', target: '判断' }, + { source: '判断', target: '结束', label: { show: true, formatter: '通过' } }, + ], + lineStyle: { color: '#94A3B8', width: 2, curveness: 0 }, + edgeSymbol: ['', 'arrow'], + edgeSymbolSize: [0, 10], + }] +}; +``` + +--- + +## ECharts vs Other Frameworks + +| Capability | ECharts | Plotly | Chart.js | +|------|---------|--------|----------| +| Canvas rendering (big data) | ✅ Millions | ❌ SVG-based | ✅ But limited | +| Chinese docs | ✅ Official | ❌ English | ❌ English | +| Candlestick | ✅ Built-in | ❌ Plugin needed | ❌ None | +| Maps | ✅ Built-in China map | ✅ mapbox | ❌ None | +| 3D Charts | ✅ echarts-gl | ✅ Built-in | ❌ None | +| No Node.js needed | ✅ CDN import | ❌ Needs plotly.js | ✅ CDN | +| Server-side rendering | ✅ node-echarts | ✅ orca | ✅ chartjs-node | diff --git a/skills/charts/references/matplotlib.md b/skills/charts/references/matplotlib.md new file mode 100755 index 0000000..d39fa0e --- /dev/null +++ b/skills/charts/references/matplotlib.md @@ -0,0 +1,617 @@ +# matplotlib Template Library + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + + +## Environment Initialization (Must Execute Before Each Plot) + +```python +import matplotlib +import matplotlib.pyplot as plt +import matplotlib.ticker as mticker +import numpy as np + +# ═══ Chinese Font Setup ═══ +# SimHei is the default; other fonts available: +# SimSun.ttf (Songti, formal docs), SimKai.ttf (Kaiti, artistic), +# SarasaMonoSC-*.ttf (monospace CJK, code scenes) +# Run `fc-list :lang=zh` for system fonts (PingFang SC, Heiti TC, etc.) +# Font path: adjust for your system. Common locations: +# macOS: '/System/Library/Fonts/Supplemental/SimHei.ttf' +# Linux: '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' +# Custom: './fonts/SimHei.ttf' +import os +SIMHEI_PATH = os.environ.get('SIMHEI_FONT', '/System/Library/Fonts/Supplemental/SimHei.ttf') +matplotlib.font_manager.fontManager.addfont(SIMHEI_PATH) + +# ═══ Global Style ═══ +plt.rcParams.update({ + # Font + 'font.sans-serif': ['SimHei'], + 'axes.unicode_minus': False, + # Background + 'figure.facecolor': '#FFFFFF', + 'axes.facecolor': '#FFFFFF', + # Border: only keep left and bottom + 'axes.edgecolor': '#E5E7EB', + 'axes.linewidth': 0.8, + 'axes.spines.top': False, + 'axes.spines.right': False, + # Grid: off by default + 'axes.grid': False, + # Ticks: hide tick marks + 'xtick.major.size': 0, + 'ytick.major.size': 0, + 'xtick.labelsize': 9, + 'ytick.labelsize': 9, + # Title + 'axes.labelsize': 10, + 'axes.titlesize': 16, + 'axes.titleweight': 'bold', + 'axes.titlepad': 16, + # Legend: no frame + 'legend.frameon': False, + 'legend.fontsize': 9, + # Export + 'figure.dpi': 200, + 'savefig.dpi': 200, + 'savefig.bbox': 'tight', + 'savefig.facecolor': '#FFFFFF', + 'savefig.pad_inches': 0.3, +}) +``` + +## Color Constants + +```python +# ─── Cool Colors (Business/Tech) ─── +C_BLUE = '#3B82F6' +C_CYAN = '#06B6D4' +C_PURPLE = '#8B5CF6' +C_AMBER = '#F59E0B' +C_RED = '#EF4444' +C_GREEN = '#10B981' +COOL = [C_BLUE, C_CYAN, C_PURPLE, C_AMBER, C_RED, C_GREEN] + +# ─── Warm Colors (Warmth/Creative) ─── +WARM = ['#F59E0B', '#EF4444', '#8B5CF6', '#3B82F6', '#10B981', '#EC4899'] + +# ─── Academic Grayscale ─── +ACADEMIC = ['#111827', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6'] + +# ─── Colorblind-Safe (Paper Preferred) ─── +CB_SAFE = ['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377'] + +# ─── Grayscale ─── +G900, G700, G500, G400, G300, G200, G100, G50 = \ + '#111827', '#374151', '#6B7280', '#9CA3AF', '#D1D5DB', '#E5E7EB', '#F3F4F6', '#F9FAFB' + +# ─── Gain/Loss ─── +POS = '#22C55E' +NEG = '#EF4444' +``` + +## Helper Functions + +```python +def clean_axis(ax, grid=True): + """Clean axis: remove top and right borders, add faint grid""" + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + if grid: + ax.yaxis.grid(True, alpha=0.08, color=G300) + ax.set_axisbelow(True) + +def add_value_labels(ax, bars, values, highlight_idx=None, fmt='{:,.0f}', + offset_ratio=0.02): + """Add value labels on top of bars, highlight bar in bold. + ⚠️ Automatically extends Y-axis upper limit to ensure labels don't overflow chart area.""" + max_val = max(values) + for i, (bar, val) in enumerate(zip(bars, values)): + is_hl = (i == highlight_idx) if highlight_idx is not None else False + ax.text(bar.get_x() + bar.get_width()/2, + bar.get_height() + max_val * offset_ratio, + fmt.format(val), ha='center', va='bottom', + fontsize=10 if is_hl else 9, + color=G900 if is_hl else G400, + fontweight='bold' if is_hl else 'normal') + + # ⚠️ Critical: extend Y-axis upper limit to leave enough space for labels (at least 15%) + ax.set_ylim(0, max_val * 1.18) + +def save(fig, path, dpi=200): + """Unified save — prefer constrained_layout, fallback to tight_layout""" + if not fig.get_constrained_layout(): + try: + fig.tight_layout() + except Exception: + pass + fig.savefig(path, dpi=dpi, facecolor='white', bbox_inches='tight') + plt.close(fig) + import os + size_kb = os.path.getsize(path) / 1024 + print(f'✅ {path} ({size_kb:.0f}KB)') +``` + +### Label Text Avoidance (adjustText) + +When charts have multiple label annotations (e.g., scatter plot labels, box plot annotations), **must use adjustText library** to prevent text overlap: + +```python +# pip install adjustText +from adjustText import adjust_text + +# Call after adding all annotations +texts = [] +for i, (x, y, label) in enumerate(zip(x_data, y_data, labels)): + texts.append(ax.text(x, y, label, fontsize=9, color='#374151')) + +# Auto-avoidance — pass all text objects, adjustText will move them to avoid overlap +adjust_text(texts, ax=ax, + arrowprops=dict(arrowstyle='->', color='#9CA3AF', lw=0.8), + force_text=(0.5, 0.8), # Force to push text away + force_points=(0.3, 0.5), # Repulsion from data points + expand=(1.2, 1.4)) # Expansion factor for text bbox +``` + +--- + +## Template 1: Insight Bar Chart + +**Scenario**: Emphasize outstanding performance of one data item. Gray out others, highlight the focus. + +```python +def insight_bar(labels, values, highlight_idx, title, + highlight_color=C_BLUE, save_path='insight_bar.png'): + fig, ax = plt.subplots(figsize=(10, 6)) + + colors = [G200] * len(labels) + colors[highlight_idx] = highlight_color + + bars = ax.bar(labels, values, color=colors, width=0.6, + zorder=3, edgecolor='white', linewidth=0.5) + add_value_labels(ax, bars, values, highlight_idx) + + ax.set_title(title, loc='left') + # ⚠️ add_value_labels already sets ylim automatically, no need to repeat here + clean_axis(ax) + save(fig, save_path) +``` + +--- + +## Template 2: Trend Comparison Line Chart + +**Scenario**: This year vs last year, actual vs target. Main line in colored solid, comparison line in gray dashed. + +```python +def trend_compare(x, y_main, y_ref, label_main, label_ref, title, + color=C_BLUE, save_path='trend_compare.png'): + fig, ax = plt.subplots(figsize=(12, 6)) + + # Comparison line (gray dashed, at bottom layer) + ax.plot(x, y_ref, color=G300, linewidth=1.5, linestyle='--', zorder=2) + ax.text(len(x)-0.5, y_ref[-1], label_ref, color=G400, fontsize=9, va='center') + + # Main line (colored solid + white-center dots) + ax.plot(x, y_main, color=color, linewidth=2.5, marker='o', markersize=5, + markerfacecolor='white', markeredgewidth=2, markeredgecolor=color, zorder=3) + ax.text(len(x)-0.5, y_main[-1], f'{label_main} {y_main[-1]:,.0f}', + color=color, fontsize=10, fontweight='bold', va='center') + + # Difference area + ax.fill_between(range(len(x)), y_ref, y_main, alpha=0.06, color=color) + + ax.set_title(title, loc='left') + ax.set_xticks(range(len(x))) + ax.set_xticklabels(x) + clean_axis(ax) + save(fig, save_path) +``` + +--- + +## Template 3: Grouped Bar Chart + +**Scenario**: Comparison of multiple categories across multiple dimensions. + +```python +def grouped_bar(labels, datasets, series_names, title, + colors=None, save_path='grouped_bar.png'): + if colors is None: + colors = COOL[:len(datasets)] + + fig, ax = plt.subplots(figsize=(12, 6)) + n = len(datasets) + width = 0.7 / n + x = np.arange(len(labels)) + + for i, (data, name, color) in enumerate(zip(datasets, series_names, colors)): + offset = (i - n/2 + 0.5) * width + ax.bar(x + offset, data, width=width*0.85, color=color, + label=name, zorder=3, edgecolor='white', linewidth=0.3) + + ax.set_title(title, loc='left') + ax.set_xticks(x) + ax.set_xticklabels(labels) + ax.set_ylim(0, max(max(d) for d in datasets) * 1.20) # Leave 20% space for labels + ax.legend(loc='upper right', ncol=n) + clean_axis(ax) + save(fig, save_path) +``` + +--- + +## Template 4: Horizontal Ranking Chart + +**Scenario**: Rankings / Top N. Progressive highlight for top items. + +```python +def ranking_bar(labels, values, title, top_n=3, + color=C_BLUE, save_path='ranking.png'): + from matplotlib.colors import to_rgba + + sorted_pairs = sorted(zip(labels, values), key=lambda x: x[1]) + labels_s, values_s = zip(*sorted_pairs) + + fig, ax = plt.subplots(figsize=(10, max(6, len(labels)*0.45))) + + bar_colors = [G200] * len(labels_s) + for i in range(len(labels_s) - top_n, len(labels_s)): + progress = (i - (len(labels_s) - top_n)) / max(top_n - 1, 1) + bar_colors[i] = to_rgba(color, 0.35 + 0.65 * progress) + + bars = ax.barh(range(len(labels_s)), values_s, color=bar_colors, + height=0.6, zorder=3, edgecolor='white', linewidth=0.3) + + for i, (bar, val) in enumerate(zip(bars, values_s)): + is_top = i >= len(labels_s) - top_n + ax.text(bar.get_width() + max(values_s)*0.01, + bar.get_y() + bar.get_height()/2, + f'{val:,.0f}', va='center', fontsize=9, + color=G900 if is_top else G400) + + ax.set_yticks(range(len(labels_s))) + ax.set_yticklabels(labels_s) + ax.set_title(title, loc='left') + ax.spines['bottom'].set_visible(False) + ax.xaxis.set_visible(False) + save(fig, save_path) +``` + +--- + +## Template 5: Donut Chart + +**Scenario**: Proportion distribution (max 5 slices, avoid if possible — bar charts are usually better). + +```python +def donut(labels, values, title, center_text=None, + colors=None, save_path='donut.png'): + if colors is None: + colors = COOL[:len(labels)] + + fig, ax = plt.subplots(figsize=(8, 8)) + wedges, _, autotexts = ax.pie( + values, labels=None, colors=colors, autopct='%1.0f%%', + startangle=90, pctdistance=0.78, + wedgeprops=dict(width=0.35, edgecolor='white', linewidth=2)) + + for t in autotexts: + t.set_fontsize(10) + t.set_fontweight('bold') + + if center_text: + ax.text(0, 0.06, str(center_text), ha='center', va='center', + fontsize=28, fontweight='bold', color=G900) + ax.text(0, -0.1, '总计', ha='center', va='center', fontsize=11, color=G500) + + ax.legend(wedges, labels, loc='center left', + bbox_to_anchor=(1, 0.5), fontsize=10) + ax.set_title(title, loc='center', pad=20) + save(fig, save_path) +``` + +--- + +## Template 6: Scatter Plot + Trend Line + +**Scenario**: Two-variable correlation analysis. + +```python +def scatter_trend(x, y, title, xlabel, ylabel, + color=C_BLUE, save_path='scatter.png'): + fig, ax = plt.subplots(figsize=(10, 7)) + + ax.scatter(x, y, c=color, s=50, alpha=0.6, + edgecolors='white', linewidth=1, zorder=3) + + # Trend line + z = np.polyfit(x, y, 1) + p = np.poly1d(z) + x_line = np.linspace(min(x), max(x), 100) + ax.plot(x_line, p(x_line), color=G400, linewidth=1.5, + linestyle='--', zorder=2, alpha=0.7) + + ax.set_title(title, loc='left') + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + clean_axis(ax, grid=True) + ax.xaxis.grid(True, alpha=0.08, color=G300) + save(fig, save_path) +``` + +--- + +## Template 7: Heatmap + +**Scenario**: Matrix data, correlations, time × category. + +```python +def heatmap(data, row_labels, col_labels, title, + cmap_color=C_BLUE, save_path='heatmap.png'): + from matplotlib.colors import LinearSegmentedColormap + + cmap = LinearSegmentedColormap.from_list('bc', ['#FFFFFF', cmap_color]) + fig, ax = plt.subplots( + figsize=(max(8, len(col_labels)*1.2), max(6, len(row_labels)*0.6))) + + arr = np.array(data) + im = ax.imshow(arr, cmap=cmap, aspect='auto') + + vmax = arr.max() + for i in range(len(row_labels)): + for j in range(len(col_labels)): + val = arr[i][j] + color = 'white' if val > vmax * 0.6 else G700 + ax.text(j, i, f'{val:.1f}', ha='center', va='center', + fontsize=9, color=color) + + ax.set_xticks(range(len(col_labels))) + ax.set_yticks(range(len(row_labels))) + ax.set_xticklabels(col_labels) + ax.set_yticklabels(row_labels) + ax.set_title(title, loc='left', pad=16) + + cbar = fig.colorbar(im, ax=ax, fraction=0.03, pad=0.08, shrink=0.8) + cbar.outline.set_visible(False) + cbar.ax.tick_params(labelsize=8) # colorbar ticks should not be too large + + for spine in ax.spines.values(): + spine.set_visible(True) + spine.set_color(G200) + save(fig, save_path) +``` + +--- + +## Template 8: KPI Metric Cards + +**Scenario**: Dashboard top key number display. + +```python +def kpi_cards(metrics, save_path='kpi.png'): + """ + metrics: [{'label': '总收入', 'value': '12.8M', + 'change': '+23%', 'positive': True}, ...] + """ + from matplotlib.patches import FancyBboxPatch + + n = len(metrics) + fig, axes = plt.subplots(1, n, figsize=(3.8*n, 2.8)) + if n == 1: axes = [axes] + + for ax, m in zip(axes, metrics): + ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis('off') + + bg = FancyBboxPatch((0.05, 0.05), 0.9, 0.9, + boxstyle='round,pad=0.05', facecolor=G50, edgecolor=G200, linewidth=0.8) + ax.add_patch(bg) + + ax.text(0.5, 0.75, m['label'], ha='center', va='center', + fontsize=10, color=G500) + ax.text(0.5, 0.45, m['value'], ha='center', va='center', + fontsize=24, fontweight='bold', color=G900) + + if 'change' in m: + is_pos = m.get('positive', True) + ax.text(0.5, 0.18, + f'{"↑" if is_pos else "↓"} {m["change"]}', + ha='center', va='center', fontsize=11, + color=POS if is_pos else NEG, fontweight='bold') + + save(fig, save_path) +``` + +--- + +## Template 9: Radar Chart + +**Scenario**: Multi-dimensional capability comparison (max 8 dimensions, max 3 groups). + +```python +def radar(categories, datasets, series_names, title, + colors=None, save_path='radar.png'): + if colors is None: + colors = COOL[:len(datasets)] + + N = len(categories) + angles = np.linspace(0, 2*np.pi, N, endpoint=False).tolist() + angles += angles[:1] # Close the polygon + + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True)) + ax.set_theta_offset(np.pi / 2) + ax.set_theta_direction(-1) + + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(categories, fontsize=10) + ax.yaxis.set_visible(False) + + # Grid beautification + ax.spines['polar'].set_color(G200) + ax.grid(color=G200, linewidth=0.5, alpha=0.5) + + for data, name, color in zip(datasets, series_names, colors): + vals = data + data[:1] # Close the polygon + ax.plot(angles, vals, color=color, linewidth=2, label=name) + ax.fill(angles, vals, color=color, alpha=0.08) + + ax.legend(loc='upper right', bbox_to_anchor=(1.3, 1.1)) + ax.set_title(title, pad=30, fontsize=16, fontweight='bold') + save(fig, save_path) +``` + +--- + +## Template 10: Waterfall Chart + +**Scenario**: Show incremental changes from start to end value. + +```python +def waterfall(labels, values, title, save_path='waterfall.png'): + """labels and values correspond, positive=increase negative=decrease, last item auto-treated as total""" + fig, ax = plt.subplots(figsize=(12, 6)) + + cumulative = [0] + for v in values[:-1]: + cumulative.append(cumulative[-1] + v) + + bar_colors = [] + for i, v in enumerate(values): + if i == len(values) - 1: + bar_colors.append(C_BLUE) # Total + elif v >= 0: + bar_colors.append(POS) # Increase + else: + bar_colors.append(NEG) # Decrease + + bottoms = [] + for i, v in enumerate(values): + if i == len(values) - 1: + bottoms.append(0) # Total starts from 0 + elif v >= 0: + bottoms.append(cumulative[i]) + else: + bottoms.append(cumulative[i] + v) + + bars = ax.bar(labels, [abs(v) for v in values], bottom=bottoms, + color=bar_colors, width=0.6, edgecolor='white', linewidth=0.5, zorder=3) + + # Connecting lines + for i in range(len(values) - 2): + y = cumulative[i+1] + ax.plot([i+0.3, i+0.7], [y, y], color=G300, linewidth=0.8, zorder=2) + + # Value labels + for i, (bar, val) in enumerate(zip(bars, values)): + y_pos = bar.get_y() + bar.get_height() + max(abs(v) for v in values)*0.01 + prefix = '+' if val > 0 and i < len(values)-1 else '' + ax.text(bar.get_x()+bar.get_width()/2, y_pos, + f'{prefix}{val:,.0f}', ha='center', va='bottom', + fontsize=9, color=G700) + + ax.set_title(title, loc='left') + clean_axis(ax) + save(fig, save_path) +``` + +--- + +## Template 11: Multi-Subplot Dashboard (GridSpec Precision Layout) + +**Scenario**: Combine multiple subplots into a dashboard. ⚠️ Max 4 subplots per canvas, split if exceeded. + +**Core Principle**: Use `GridSpec` for precise control of each subplot's position and spacing, don't rely on `tight_layout()`. + +```python +import matplotlib.gridspec as gridspec + +def dashboard(data_dict, title, save_path='dashboard.png'): + """ + 2x2 dashboard layout example. + data_dict contains data needed for each subplot. + """ + # ⚠️ Use constrained_layout instead of tight_layout + fig = plt.figure(figsize=(16, 12), constrained_layout=True) + fig.suptitle(title, fontsize=20, fontweight='bold', y=0.98) + + # GridSpec: precise spacing control + gs = gridspec.GridSpec(2, 2, figure=fig, + wspace=0.35, # Column spacing (at least 0.3) + hspace=0.35, # Row spacing (at least 0.3) + left=0.08, right=0.92, + top=0.92, bottom=0.08) + + # ─── Top-left: bar chart ─── + ax1 = fig.add_subplot(gs[0, 0]) + ax1.set_title('Quarterly Revenue', loc='left', fontsize=13, fontweight='bold') + # ... binddata ... + clean_axis(ax1) + + # ─── Top-right: line chart ─── + ax2 = fig.add_subplot(gs[0, 1]) + ax2.set_title('Monthly Trend', loc='left', fontsize=13, fontweight='bold') + # ... bind data ... + clean_axis(ax2) + + # ─── Bottom-left: pie chart ─── + ax3 = fig.add_subplot(gs[1, 0]) + ax3.set_title('Category Share', loc='left', fontsize=13, fontweight='bold') + # ... bind data ... + + # ─── Bottom-right: scatter plot ─── + ax4 = fig.add_subplot(gs[1, 1]) + ax4.set_title('Conversion Analysis', loc='left', fontsize=13, fontweight='bold') + # ... bind data ... + clean_axis(ax4) + + fig.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close(fig) +``` + +### Dashboard Layout Golden Rules + +| Rule | Description | +|------|-------------| +| **Max 4 subplots** | Split into multiple charts if exceeded | +| **Use `constrained_layout=True`** | Smarter than `tight_layout()`, auto-avoids labels | +| **`wspace/hspace ≥ 0.3`** | Subplot spacing too small causes overlap | +| **Independent title per subplot** | Use `ax.set_title()` instead of `fig.suptitle()` | +| **Consistent font sizes** | Subtitle 13px, axis labels 10px, data labels 9px | +| **Colorbar in separate column** | If a subplot needs colorbar, allocate `gs[0, 2]` separately | +| **Don't mix legend and direct labels** | Use either all legends or all direct labels in a dashboard | + +### Safe Layout with Colorbar + +```python +# When a subplot needs a colorbar, use 3-column layout, rightmost column for colorbar +gs = gridspec.GridSpec(2, 3, figure=fig, + width_ratios=[1, 1, 0.05], # Third column very narrow, dedicated to colorbar + wspace=0.4, hspace=0.35) + +ax_heat = fig.add_subplot(gs[0, 1]) +im = ax_heat.imshow(data, cmap='Blues') + +# Colorbar placed in its own subplot position, won't obscure any content +cbar_ax = fig.add_subplot(gs[0, 2]) +fig.colorbar(im, cax=cbar_ax) +cbar_ax.set_ylabel('Value', fontsize=10) +``` + +### Split Strategy for More Than 4 Subplots + +```python +# ❌ Wrong: 8 subplots crammed into one canvas +fig, axes = plt.subplots(2, 4, figsize=(20, 10)) # Everything becomes unreadable + +# ✅ Correct: split into 2 figures, 4 subplots each +# Figure 1: overview metrics +fig1 = plt.figure(figsize=(16, 12), constrained_layout=True) +# ... 4 subplots ... +fig1.savefig('dashboard_overview.png', dpi=200) + +# Figure 2: detailed analysis +fig2 = plt.figure(figsize=(16, 12), constrained_layout=True) +# ... 4 subplots ... +fig2.savefig('dashboard_detail.png', dpi=200) +``` diff --git a/skills/charts/references/mermaid.md b/skills/charts/references/mermaid.md new file mode 100755 index 0000000..a10c437 --- /dev/null +++ b/skills/charts/references/mermaid.md @@ -0,0 +1,797 @@ +# Mermaid Template Library + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + + +Mermaid is the best "text-as-diagram" solution — write structural diagrams with Markdown-like syntax, zero design skills needed. +Best for: flowcharts, sequence diagrams, architecture diagrams, Gantt charts, class diagrams, ER diagrams, mind maps, state diagrams, pie charts, Git branch graphs. + +**Core advantages**: text is version-controllable, minimal maintenance cost, high rendering consistency, CJK support. + +## ⚠️ Flowchart Quality Rules (Highest Priority) + +### Font Size Control +Mermaid font sizes are controlled via `themeVariables` and CSS: +- `fontSize`: Global font size, recommended `14px`-`16px`, **no less than 12px** +- Node text is controlled via `fontSize` or `%%{init:}%%` directive +- Annotations/footnotes use subgraph titles or separate nodes, font size no less than `11px` + +### Connectors & Spacing +```javascript +flowchart: { + padding: 32, // Node padding (CJK needs more space) + nodeSpacing: 80, // Horizontal spacing between nodes (default 50 too tight, 60 still not enough) + rankSpacing: 80, // Vertical spacing between ranks + curve: 'basis', // Connection curve style, consistent across the chart +} +``` + +**Connector style must be consistent throughout the chart**: do not mix straight, curved, and polylines in the same diagram. Mermaid controls this globally via the `curve` parameter. + +### ⚠️ Mermaid Flowchart Hard Constraints (MANDATORY) + +The following constraints are enforced **when generating Mermaid flowchart code**, not as post-checks: + +1. **Node text must be wrapped in quotes**: `A["用户登录"]` ✅ / `A[用户登录]` ❌ — quotes prevent CJK special characters from causing parse errors +2. **Max 10 CJK characters per line in node text**: exceed → use `
` to break → `A["用户身份
验证模块"]` +3. **Max 5 nodes per subgraph**: exceed → split into multiple subgraphs or switch to CSS approach +4. **Max 10 total nodes**: exceed → switch to CSS flowchart template in `references/playwright-css.md` +5. **Max 6 CJK characters in connector labels**: `-->|验证通过|` ✅ / `-->|用户身份验证通过后跳转|` ❌ +6. **Config params must use enlarged values**: `padding: 32, nodeSpacing: 80, rankSpacing: 80` + +## Rendering Methods + +### Method 1: Playwright + HTML (Recommended, export PNG/SVG/PDF) + +```html + + + + + + + + +
+
+    
+  
+
+ + + +``` + +**Python screenshot script**: + +```python +import asyncio +from playwright.async_api import async_playwright + +async def mermaid_to_png(html_path, png_path, width=1400, scale=2): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page( + viewport={'width': width, 'height': 800}, + device_scale_factor=scale + ) + await page.goto(f'file://{html_path}', wait_until='load', timeout=30000) + + # Wait for Mermaid SVG to render + await page.wait_for_selector('#diagram svg', timeout=15000) + await page.wait_for_timeout(1000) + + # ⚠️ Read SVG's ACTUAL rendered size (not CSS box model!) + # Mermaid SVGs often overflow their CSS container — getBBox/clientRect + # returns the true size, while CSS bounding_box() returns the clipped box. + svg_size = await page.evaluate('''() => { + const svg = document.querySelector('#diagram svg'); + if (!svg) return null; + const r = svg.getBoundingClientRect(); + return { width: r.width, height: r.height }; + }''') + + el = page.locator('#diagram') + css_bbox = await el.bounding_box() + + svg_w = svg_size['width'] if svg_size else width + svg_h = svg_size['height'] if svg_size else 800 + css_w = css_bbox['width'] if css_bbox else width + css_h = css_bbox['height'] if css_bbox else 800 + + # Use the LARGER of CSS box and SVG actual size + fit_w = max(width, int(max(svg_w, css_w) + 200)) + fit_h = int(max(svg_h, css_h) + 200) + + await page.set_viewport_size({'width': fit_w, 'height': fit_h}) + await page.wait_for_timeout(500) + + await el.screenshot(path=png_path) + await browser.close() + + import os + print(f'✅ {png_path} ({os.path.getsize(png_path)/1024:.0f}KB)') + +# asyncio.run(mermaid_to_png('./output/diagram.html', './output/diagram.png')) +``` + +> **⚠️ CRITICAL: CSS `bounding_box()` vs SVG actual size** +> +> Mermaid generates SVGs that can be wider/taller than their CSS container. `bounding_box()` (Playwright) and `getBoundingClientRect()` on the container return **CSS box model size**, which may be smaller than the SVG's viewBox. +> +> **Always read the SVG element's own `getBoundingClientRect()`** via `page.evaluate()` and use `max(css_size, svg_size)` for viewport dimensions. This is the root cause of the "right side clipped" bug. +> +> Also: `wait_until='load'` is preferred over `'networkidle'` because Mermaid initializes on DOM load. `'networkidle'` can timeout if CDN is slow. + +### Method 2: Mermaid CLI (mmdc, command line) + +```bash +# Installation +npm install -g @mermaid-js/mermaid-cli + +# Usage: .mmd file → PNG/SVG/PDF +mmdc -i diagram.mmd -o diagram.png -w 1200 -b transparent +mmdc -i diagram.mmd -o diagram.svg +mmdc -i diagram.mmd -o diagram.pdf + +# Specify theme configuration +mmdc -i diagram.mmd -o diagram.png --configFile mermaid-config.json +``` + +`mermaid-config.json` example: + +```json +{ + "theme": "base", + "themeVariables": { + "primaryColor": "#EFF6FF", + "primaryBorderColor": "#3B82F6", + "primaryTextColor": "#1E293B", + "lineColor": "#94A3B8", + "secondaryColor": "#F0FDF4", + "tertiaryColor": "#FFF7ED" + } +} +``` + +### Method 3: Online Preview + +Paste code at [mermaid.live](https://mermaid.live) for instant preview and export. + +--- + +## Theme Configuration (Design System Integration) + +Mermaid uses `theme: 'base'` + `themeVariables` for fully custom colors. +The following themes align with the charts skill mood color system: + +### Business Professional (Default) + +```javascript +themeVariables: { + primaryColor: '#EFF6FF', // Node background (very light blue) + primaryBorderColor: '#3B82F6', // Node border (blue) + primaryTextColor: '#1E293B', // Node text (dark gray-blue) + lineColor: '#94A3B8', // Connectors (gray) + secondaryColor: '#F0FDF4', // Secondary nodes (very light green) + secondaryBorderColor: '#10B981', + secondaryTextColor: '#1E293B', + tertiaryColor: '#FFF7ED', // Tertiary nodes (very light amber) + tertiaryBorderColor: '#F59E0B', + tertiaryTextColor: '#1E293B', + noteBkgColor: '#F8FAFC', // Note background + noteTextColor: '#6B7280', + noteBorderColor: '#E2E8F0', + fontSize: '14px', + fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif', +} +``` + +### Tech Dark + +```javascript +themeVariables: { + primaryColor: '#1E293B', + primaryBorderColor: '#3B82F6', + primaryTextColor: '#F1F5F9', + lineColor: '#475569', + secondaryColor: '#0F2E1F', + secondaryBorderColor: '#10B981', + secondaryTextColor: '#F1F5F9', + tertiaryColor: '#1A1625', + tertiaryBorderColor: '#8B5CF6', + tertiaryTextColor: '#F1F5F9', + noteBkgColor: '#0F172A', + noteTextColor: '#94A3B8', + noteBorderColor: '#334155', + fontSize: '14px', + fontFamily: '-apple-system, BlinkMacSystemFont, PingFang SC, SimHei, sans-serif', + background: '#0F172A', +} +``` + +--- + +## Template 1: Flowchart + +The most common chart type. Supports directions: `TB` (top→bottom), `LR` (left→right), `BT`, `RL`. + +```mermaid +flowchart TB + A[开始] --> B{条件判断} + B -->|是| C[执行操作A] + B -->|否| D[执行操作B] + C --> E[结束] + D --> E + + style A fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B + style B fill:#FFF7ED,stroke:#F59E0B,stroke-width:2px,color:#1E293B + style C fill:#F0FDF4,stroke:#10B981,stroke-width:2px,color:#1E293B + style D fill:#F0FDF4,stroke:#10B981,stroke-width:2px,color:#1E293B + style E fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B +``` + +### Node Shape Quick Reference + +| Syntax | Shape | Use For | +|------|------|--------| +| `A[text]` | Rectangle | Steps/Actions | +| `A(text)` | Rounded rect | General nodes | +| `A([text])` | Stadium | Start/End | +| `A{text}` | Diamond | Decision | +| `A{{text}}` | Hexagon | Preparation | +| `A[/text/]` | Parallelogram | Input/Output | +| `A((text))` | Circle | Connector | +| `A>text]` | Flag | Event/Signal | + +### Subgraphs (Grouping) + +```mermaid +flowchart LR + subgraph 前端["🖥️ 前端"] + A[React App] --> B[API 调用] + end + subgraph 后端["⚙️ 后端"] + C[FastAPI] --> D[(PostgreSQL)] + end + B --> C + + style 前端 fill:#EFF6FF,stroke:#3B82F6,stroke-width:1px + style 后端 fill:#F0FDF4,stroke:#10B981,stroke-width:1px +``` + +--- + +## Template 2: Sequence Diagram + +Shows interaction sequence between systems/actors. + +```mermaid +sequenceDiagram + actor 用户 + participant 前端 as 🖥️ 前端 + participant API as ⚙️ API 网关 + participant DB as 🗄️ 数据库 + + 用户->>前端: 点击登录 + 前端->>API: POST /auth/login + API->>DB: 查询用户 + DB-->>API: 用户信息 + + alt 验证成功 + API-->>前端: 200 + JWT Token + 前端-->>用户: 跳转首页 + else 验证失败 + API-->>前端: 401 未授权 + 前端-->>用户: 显示错误提示 + end + + Note over 前端,API: Token 有效期 24 小时 +``` + +### Arrow Types + +| Syntax | Meaning | +|------|------| +| `->>` | Solid arrow (synchronous call) | +| `-->>` | Dashed arrow (return/response) | +| `-x` | Solid with x (failure/rejection) | +| `-)` | Async message | + +--- + +## Template 3: Architecture Diagram (C4 Style via Subgraphs) + +```mermaid +flowchart TB + subgraph 用户层["👤 用户层"] + U1[Web 浏览器] + U2[移动 App] + end + + subgraph 接入层["🌐 接入层"] + GW[API Gateway
Nginx + Rate Limit] + LB[负载均衡
Round Robin] + end + + subgraph 服务层["⚙️ 微服务"] + S1[用户服务
FastAPI] + S2[内容服务
FastAPI] + S3[推荐服务
PyTorch] + end + + subgraph 数据层["🗄️ 数据层"] + DB[(PostgreSQL)] + RD[(Redis Cache)] + ES[(Elasticsearch)] + end + + U1 & U2 --> GW + GW --> LB + LB --> S1 & S2 & S3 + S1 --> DB & RD + S2 --> DB & ES + S3 --> RD & ES + + style 用户层 fill:#EFF6FF,stroke:#3B82F6,stroke-width:1.5px + style 接入层 fill:#FFF7ED,stroke:#F59E0B,stroke-width:1.5px + style 服务层 fill:#F0FDF4,stroke:#10B981,stroke-width:1.5px + style 数据层 fill:#F5F3FF,stroke:#8B5CF6,stroke-width:1.5px +``` + +--- + +## Template 4: Gantt Chart + +```mermaid +gantt + title 项目里程碑计划 + dateFormat YYYY-MM-DD + axisFormat %m/%d + + section 需求阶段 + 需求调研 :done, req1, 2024-01-01, 14d + 需求评审 :done, req2, after req1, 3d + + section 开发阶段 + 后端开发 :active, dev1, after req2, 21d + 前端开发 :active, dev2, after req2, 18d + 联调测试 : dev3, after dev1, 7d + + section 上线阶段 + 灰度发布 : rel1, after dev3, 3d + 全量上线 :milestone, rel2, after rel1, 0d +``` + +--- + +## Template 5: Class Diagram + +```mermaid +classDiagram + class User { + +String name + +String email + +login() + +logout() + } + class Order { + +int id + +Date created_at + +float total + +submit() + +cancel() + } + class Product { + +String name + +float price + +int stock + } + + User "1" --> "*" Order : 下单 + Order "*" --> "*" Product : 包含 +``` + +--- + +## Template 6: ER Diagram + +```mermaid +erDiagram + USER { + int id PK + string name + string email UK + datetime created_at + } + ORDER { + int id PK + int user_id FK + float total + string status + datetime created_at + } + ORDER_ITEM { + int id PK + int order_id FK + int product_id FK + int quantity + float price + } + PRODUCT { + int id PK + string name + float price + int stock + } + + USER ||--o{ ORDER : "下单" + ORDER ||--|{ ORDER_ITEM : "包含" + PRODUCT ||--o{ ORDER_ITEM : "被购买" +``` + +--- + +## Template 7: Mind Map + +> ⚠️ Mermaid mindmap has limited layout capabilities. **For high-quality mind maps**, prefer `references/mindmap-css.md`. +> The following approach is for **quick drafts** or embedding in Markdown documents, with CSS injection to optimize visual quality. + +### Optimized HTML Shell (Important! Use this, not the default template) + +Mermaid mindmap doesn't support `style`/`classDef`, but you can greatly improve results with **CSS overriding SVG styles** + **themeVariables**: + +```html + + + + + + + + +
+
+mindmap
+    root((你的主题))
+        一级分支1
+            二级内容A
+            二级内容B
+        一级分支2
+            二级内容C
+  
+
+ + + +``` + +### Auto-Upgrade Rules (Important!) + +> ⚠️ **Never trim user content just to fit Mermaid!** +> Content comes first; tools serve content, not the other way around. + +When content complexity exceeds Mermaid mindmap's comfort zone, **auto-switch to CSS approach** (`references/mindmap-css.md`): + +| Trigger Condition (any one met) | Action | +|----------------------|------| +| More than 7 L1 branches | → Switch to CSS | +| Any branch has >8 child nodes | → Switch to CSS | +| Nesting exceeds 3 levels | → Switch to CSS | +| Single node text >15 chars | → Switch to CSS | +| Total nodes >40 | → Switch to CSS | + +**When none of the above triggers**, Mermaid mindmap is adequate. The following suggestions help optimize rendering (recommendations, not hard limits): + +| Suggestion | Notes | +|------|------| +| Keep node text concise | Use spaces to segment long text, avoid punctuation | +| Use emoji prefixes per branch | Higher visual distinctiveness | +| Use CSS injection for coloring | Use the optimized HTML shell above | + +### Example (Optimized) + +```mermaid +mindmap + root((AI 内容运营)) + 选题策划 + 热点扫描 + 竞品分析 + 用户调研 + 内容生产 + 长文 + 短文 + 视频 + 渠道分发 + 微信生态 + 小红书 + B站 + 数据运营 + 数据分析 + 评论互动 + 持续优化 +``` + +### Known Limitations of Mermaid Mindmap + +- ❌ No `style` / `classDef` support for direct node coloring (CSS injection of SVG styles only) +- ❌ Line thickness/curvature cannot be controlled from Mermaid syntax (CSS override `.mindmap-edge`) +- ❌ Node spacing calculated by algorithm, cannot be manually specified +- ❌ Long CJK text easily overlaps with connectors (strict character count control needed) +- ⚠️ CSS injection depends on Mermaid internal class naming, may break on version upgrades + +**Conclusion**: For quick drafts use Mermaid + the CSS-optimized shell above; for production output use CSS mind map → `references/mindmap-css.md` + +--- + +## Template 8: State Diagram + +```mermaid +stateDiagram-v2 + [*] --> 草稿 + 草稿 --> 审核中 : 提交审核 + 审核中 --> 已发布 : 审核通过 + 审核中 --> 草稿 : 退回修改 + 已发布 --> 已下架 : 违规/过期 + 已下架 --> 草稿 : 重新编辑 + 已发布 --> [*] : 永久删除 +``` + +--- + +## Template 9: Git Branch Graph + +```mermaid +gitGraph + commit id: "init" + branch develop + checkout develop + commit id: "feat: 用户模块" + commit id: "feat: 订单模块" + branch feature/payment + checkout feature/payment + commit id: "feat: 支付接入" + commit id: "fix: 金额精度" + checkout develop + merge feature/payment id: "merge: 支付" + checkout main + merge develop id: "release: v1.0" + commit id: "hotfix: 安全补丁" type: REVERSE +``` + +--- + +## Styling Tips + +### Single Node Style + +```mermaid +style 节点ID fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px,color:#1E293B +``` + +### Batch Styles (classDef) + +```mermaid +flowchart LR + classDef blue fill:#EFF6FF,stroke:#3B82F6,stroke-width:1.5px,color:#1E293B + classDef green fill:#F0FDF4,stroke:#10B981,stroke-width:1.5px,color:#1E293B + classDef amber fill:#FFF7ED,stroke:#F59E0B,stroke-width:1.5px,color:#1E293B + + A[步骤1]:::blue --> B{判断}:::amber + B -->|是| C[结果A]:::green + B -->|否| D[结果B]:::green +``` + +### Connector Styles + +```mermaid +%% Style for the N-th connector (0-indexed) +linkStyle 0 stroke:#3B82F6,stroke-width:2px +linkStyle 1 stroke:#10B981,stroke-width:2px,stroke-dasharray: 5 5 +``` + +--- + +## Mermaid vs Other Approaches + +| Capability | Mermaid | Playwright+CSS | draw.io | +|------|---------|---------------|---------| +| Learning curve | ✅ Very low (Markdown-like) | Medium (HTML/CSS) | ✅ Very low (drag&drop) | +| Version control friendly | ✅ Plain text | ✅ Plain text | ❌ XML binary | +| Flowcharts | ✅ Built-in | ⚠️ Manual layout | ✅ Drag&drop | +| Sequence diagrams | ✅ Built-in | ❌ Very complex | ✅ Templates | +| Gantt charts | ✅ Built-in | ❌ Build from scratch | ⚠️ Limited | +| Class/ER diagrams | ✅ Built-in | ❌ Not suited | ✅ Templates | +| Visual freedom | ⚠️ Limited | ✅ Full freedom | ✅ Free | +| PNG export | ✅ mmdc/Playwright | ✅ Playwright | ✅ Built-in | +| CJK support | ✅ Native | ✅ Font config | ✅ Native | +| Auto layout | ✅ Automatic | ❌ Manual | ⚠️ Semi-auto | + +**Principle: Use Mermaid for structural/relationship diagrams, Playwright+CSS for creative design diagrams.** + +--- + +## FAQ + +### Q: CJK node names cause layout issues? + +Ensure `fontFamily` includes a CJK font: +```javascript +themeVariables: { + fontFamily: '-apple-system, PingFang SC, SimHei, sans-serif' +} +``` + +### Q: How to control node spacing? + +Mermaid auto-layouts; spacing is adjusted via config: +```javascript +flowchart: { padding: 16, nodeSpacing: 50, rankSpacing: 60 } +``` + +### Q: Chart too large? + +- Split into subgraphs +- Change direction (`TB` too tall → switch to `LR`) +- Use `mmdc -w 1600` to increase canvas width + +### Q: How to add line breaks in nodes? + +Use `
` tags: +```mermaid +A[第一行
第二行
小字注释] +``` + +### Q: Flowchart node text truncated or overlapping? + +**Common causes and fixes**: + +1. **Insufficient node padding**: ensure `flowchart.padding` is at least `24` (CJK chars are ~50% wider than Latin) +2. **Text too long**: use `
` for manual line breaks, or shorten text +3. **Canvas too narrow**: use `width: fit-content` on `#diagram` container (🚫 NEVER use `max-width` — Mermaid SVG width is unpredictable) +4. **Node spacing too small**: increase `nodeSpacing` and `rankSpacing` (recommended 60+) +5. **When using classDef**: ensure `font-size` isn't too large, 12-14px is ideal + +**Correct approach for long-text nodes**: +```mermaid +flowchart LR + A["这是一段比较长的
需要换行的文字"] + B["用引号包裹节点文字
可以使用 HTML 标签"] +``` + +**Key configuration**: +```javascript +mermaid.initialize({ + flowchart: { + padding: 32, + nodeSpacing: 80, + rankSpacing: 80, + htmlLabels: true, + wrappingWidth: 160, + } +}); +``` diff --git a/skills/charts/references/mindmap-css.md b/skills/charts/references/mindmap-css.md new file mode 100755 index 0000000..3287cd5 --- /dev/null +++ b/skills/charts/references/mindmap-css.md @@ -0,0 +1,911 @@ +# CSS Mind Map Rendering Engine + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + +**Core principle: Content-driven, not template-driven. First analyze the content structure, then decide on the layout, and finally render.** + +--- + +## Step 1: Content Analysis + +After receiving a mind map requirement, **don't write any HTML/CSS yet**. First parse the content into a tree structure, then calculate key metrics. + +### 1.1 Build Tree JSON + +``` +Input: "How to work efficiently from home", branches include "Environment Setup", "Time Management", "Tool Selection"... + ↓ +Output: +{ + "root": "在家如何高效办公", + "branches": [ + { + "label": "环境准备", + "children": ["独立工作区", "降噪耳机", "人体工学椅"] + }, + { + "label": "时间管理", + "children": [ + { "label": "番茄工作法", "children": ["25分钟专注", "5分钟休息"] }, + "日程规划", + "避免多任务" + ] + }, + ... + ] +} +``` + +### 1.2 Key Metrics + +| Metric | Calculation Method | What It Affects | +|------|---------|---------| +| `branchCount` | Number of first-level branches | Choose single-sided/dual-sided expansion | +| `maxDepth` | Maximum nesting depth | Canvas width, whether sub-branch is needed | +| `maxChildren` | Maximum children per node | Vertical height of that branch | +| `totalNodes` | Total number of all nodes | Overall canvas size | +| `maxTextLen` | Longest text character count | Node width, whether line breaks are needed | +| `branchWeights[]` | Total descendants per first-level branch | Left-right balance allocation | + +### 1.3 Example Analysis + +``` +Content: "产品经理核心技能", 6 L1 branches, max depth 3, total 35 nodes, max text length 12 chars +→ branchCount=6, maxDepth=3, totalNodes=35, maxTextLen=12 +→ branchWeights=[8, 5, 7, 4, 6, 5] +``` + +--- + +## Step 2: Layout Decision + +Based on the metrics from Step 1, choose a layout scheme. The following are reference suggestions, not hard rules—adjust flexibly according to actual content. + +### 2.1 Layout Selection Guide + +**Core principle: Use simple layouts for less content, dual-sided layouts for more content, avoid one side becoming too long.** + +``` +Few branches (roughly ≤4), simple content? + → Style A: Right-expanding tree (single-sided, compact) + +Many branches (roughly ≥5), or single side would be too long? + → Style B: Left-right expanding tree (dual-sided balanced, most common) + +User explicitly requests a specific form? + → "cards"/"modules" → Style C: Card grid + → "fishbone"/"root cause analysis" → Style D: Fishbone diagram +``` + +**Why not use radial layout?** Radial layout (spreading from center outward) almost inevitably causes node overlap when there are more than 3-4 branches, and is extremely difficult to debug. Not recommended. + +### 2.2 Canvas Size Calculation + +Don't use fixed sizes. Calculate based on content: + +```python +def calc_canvas(branch_count, max_depth, total_nodes, max_text_len, layout): + # Width: depends on depth and text length + node_width = max(120, max_text_len * 14) # ~14px per CJK char (including padding) + if layout == 'left-right': + width = node_width * max_depth * 2 + 200 # Dual-sided, 200px in center for root node + else: + width = node_width * max_depth + 200 # Single-sided + + # Height: depends on max branch vertical expansion + if layout == 'left-right': + max_side_nodes = total_nodes // 2 + 2 # rough estimate of single-side nodes + else: + max_side_nodes = total_nodes + height = max_side_nodes * 32 + 200 # ~32px per leaf (including gap) + + # Lower bounds + width = max(width, 1200) + height = max(height, 600) + + return width, height +``` + +### 2.3 Left-Right Branch Allocation (Style B) + +Goal: Achieve similar visual weight on both sides. + +```python +def balance_branches(branch_weights): + """Greedy bin-packing: sort by weight descending, alternate left/right""" + indexed = sorted(enumerate(branch_weights), key=lambda x: -x[1]) + left, right = [], [] + left_sum, right_sum = 0, 0 + for idx, weight in indexed: + if left_sum <= right_sum: + left.append(idx) + left_sum += weight + else: + right.append(idx) + right_sum += weight + return left, right + # e.g.: weights=[8,5,7,4,6,5] → left=[0,3,5] right=[2,4,1] → 17 vs 18 +``` + +### 2.4 Node Styling Decision + +**Principle: The deeper the level, the lighter the visual weight.** This way readers can distinguish main branches from details at a glance. + +``` +Root node → most prominent: dark solid background, large font (~20px) +L1 branches → next prominent: light fill + colored border, medium font (~15px) +L2 nodes → lighter still: paler fill + thin border, medium-small font (~13px) +Leaf nodes → lightest: capsule frame or plain text, small font (~12-13px) +``` + +**Key: Leaves should not have the same visual weight as first-level branches.** Leaves' padding, gap, and border thickness should all be significantly smaller than their parent. Otherwise the chart will be vertically too long and lack hierarchy. + +--- + +## Step 3: Rendering + +Based on the decisions from Step 2, generate HTML + CSS + JS. + +### 3.1 Playwright Screenshot (Universal) + +```python +import asyncio +from playwright.async_api import async_playwright + +async def mindmap_to_png(html_path, png_path, width=1600): + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page(viewport={'width': width, 'height': 1200}, device_scale_factor=2) + await page.goto(f'file://{html_path}', wait_until='networkidle') + await page.wait_for_timeout(500) + + el = page.locator('#mindmap') + bbox = await el.bounding_box() + # First expansion: ensure content is not clipped + expand_w = max(width, int(bbox['width'] + 100)) + expand_h = int(bbox['height'] + 100) + await page.set_viewport_size({'width': expand_w, 'height': expand_h}) + await page.wait_for_timeout(200) + + # Call connector script + await page.evaluate('if(typeof drawAllLines==="function") drawAllLines()') + await page.wait_for_timeout(200) + + # Second contraction: measure actual content right edge, trim right-side blank space + trim = await page.evaluate('''() => { + const map = document.getElementById('mindmap'); + const nodes = map.querySelectorAll('.root-node,.branch-node,.sub-node,.leaf,.deep-node'); + const mapRect = map.getBoundingClientRect(); + let maxR = 0, maxB = 0; + nodes.forEach(n => { + const r = n.getBoundingClientRect(); + maxR = Math.max(maxR, r.right - mapRect.left); + maxB = Math.max(maxB, r.bottom - mapRect.top); + }); + return { contentW: Math.ceil(maxR) + 80, contentH: Math.ceil(maxB) + 80 }; + }''') + await page.set_viewport_size({'width': trim['contentW'], 'height': trim['contentH']}) + await page.wait_for_timeout(200) + # Redraw connectors (viewport changed so coordinates change) + await page.evaluate('if(typeof drawAllLines==="function") drawAllLines()') + await page.wait_for_timeout(200) + + await el.screenshot(path=png_path) + await browser.close() + + import os + print(f'✅ {png_path} ({os.path.getsize(png_path)/1024:.0f}KB)') +``` + +### 3.2 Universal Recursive Connector Script v4-fix (All Styles) + +This script automatically handles tree connectors of **any depth** (3, 4, 5 levels... all work). HTML for styles A and B should include this script at the end. + +**⚠️ DOM Structure Convention (connector script depends on this structure):** + +``` +#mindmap + .tree-layout ← flex container (for left-right tree) + .left-side ← left branch area + .branch.c-{color} ← L1 branch (must have color class) + .branch-node ← L1 node + .children ← L2 container + div ← child wrapper + .sub-node ← L2 node + .leaf-group ← L3 container + div / .leaf ← leaf or wrapper with deeper levels + .leaf ← L3 leaf + .deep-group ← L4 container (recursive, same as above) + .deep-node ← L4/L5 node + .center-root + .root-node ← root node + .right-side ← right branches (same structure as .left-side) +``` + +**Also supports legacy structure (.branches/.right-branches/.left-branches), backward compatible.** + +**⚠️ CSS Indentation Rules (connectors depend on child nodes having offset relative to parent, no indentation = broken connectors):** +```css +/* .tree-layout structure (new version) must include these paddings */ +.left-side .children, .left-side .leaf-group, .left-side .deep-group { + align-items: flex-end; padding-right: 16px; +} +.right-side .children, .right-side .leaf-group, .right-side .deep-group { + align-items: flex-start; padding-left: 16px; +} +``` + +**Common causes of missing connectors/layout errors:** +- **⚠️ `.sub-branch` missing `display: flex`** (most critical! Without flex, `flex-direction: row-reverse` doesn't work, left-side leaves won't expand left, instead all pile up on the right) +- **⚠️ Child node containers missing padding-left/padding-right** (without indentation, child nodes align with parent, midX is outside child nodes, connectors break) +- **⚠️ `.lr-tree` / `.tree` should not have `z-index`** (creates stacking context, covers SVG connectors) +- **⚠️ Leaf nodes must NOT stretch to equal width** — each leaf should size to its own text content (`white-space: nowrap` or `width: fit-content`). Never add `width: 100%`, `flex-grow: 1`, or `align-items: stretch` to leaf containers. Leaves with shorter text should be narrower than leaves with longer text. +- Leaf container not named `.leaf-group` (using `.child-list`, `.sub-items`, etc.) +- Deep-level container not named `.deep-group` +- `#mindmap` missing `position: relative` + +**Key improvements (v4-fix vs v2):** +- **No transparency**—use solid color blend for fading (`color + '80'` is almost invisible on white background) +- **Recursively process `.children`, `.leaf-group`, `.deep-group`**—no longer limited to 3 levels +- **Vertical line draws complete range**—one line from `min(Y)` to `max(Y)`, instead of drawing per child node + +```javascript +/** + * Universal recursive connector script v4-fix + * - Supports any nesting depth (recursively processes .children + .leaf-group + .deep-group) + * - Unified gray-tone connectors (#64748B → #94A3B8 → #A8B4C2), visually clean + * - Direction logic: + * dir='left': startX=parent.left → midX(left-biased) → endX=child.right + * dir='right': startX=parent.right → midX(right-biased) → endX=child.left + */ +function drawAllLines() { + const map = document.getElementById('mindmap'); + if (!map) { console.error('❌ #mindmap not found'); return; } + const cRect = map.getBoundingClientRect(); + + const old = map.querySelector('svg.lines'); + if (old) old.remove(); + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.classList.add('lines'); + svg.setAttribute('width', cRect.width); + svg.setAttribute('height', cRect.height); + svg.style.cssText = 'position:absolute;top:0;left:0;pointer-events:none;z-index:1;'; + let lineCount = 0; + + function rel(el) { + const r = el.getBoundingClientRect(); + return { + cx: r.left - cRect.left + r.width/2, + cy: r.top - cRect.top + r.height/2, + left: r.left - cRect.left, + right: r.right - cRect.left, + }; + } + + function drawLine(x1, y1, x2, y2, color, width) { + const l = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + // Round to nearest pixel to prevent sub-pixel misalignment (visual kinks) + l.setAttribute('x1', Math.round(x1)); l.setAttribute('y1', Math.round(y1)); + l.setAttribute('x2', Math.round(x2)); l.setAttribute('y2', Math.round(y2)); + l.setAttribute('stroke', color); l.setAttribute('stroke-width', width); + l.setAttribute('stroke-linecap', 'round'); + svg.appendChild(l); + lineCount++; + } + + // ─── Unified gray connectors (decreasing by depth, visually clean) ─── + const lineStyles = [ + { color: '#64748B', width: 2.5 }, // root → L1 + { color: '#94A3B8', width: 2 }, // L1 → L2 + { color: '#A8B4C2', width: 1.5 }, // L2 → L3 + { color: '#B8C2CC', width: 1.2 }, // L3 → L4 + { color: '#CBD5E1', width: 1 }, // L4 → L5 + ]; + function getLineStyle(branchColor, depth) { + const s = lineStyles[Math.min(depth, lineStyles.length - 1)]; + return { color: s.color, width: s.width }; + } + + // ─── Connector direction ─── + // dir='left': parent.left → midX → child.right (each child gets its own polyline) + // dir='right': parent.right → midX → child.left + // + // [Principle] midX is the X coordinate of the vertical line; it must be in the gap between parent and children. + // Use a fraction of the parent-to-nearest-child distance as offset, so: + // - Large node spacing → large offset, lines spread out + // - Small node spacing → small offset, lines compact without crossing text + // All children in the same connect() call share one midX, ensuring vertical line alignment. + function connect(parentEl, childEls, color, width, dir) { + if (!childEls.length) return; + const p = rel(parentEl); + const startX = dir === 'left' ? p.left : p.right; + const startY = p.cy; + + // Special case: single child — draw one straight horizontal line, no vertical spine + if (childEls.length === 1) { + const c = rel(childEls[0]); + const endX = dir === 'left' ? c.right : c.left; + const midY = Math.round((startY + c.cy) / 2); + drawLine(startX, midY, endX, midY, color, width); + return; + } + + // Find the closest child edge to calculate available space + let closestEdge; + if (dir === 'left') { + closestEdge = Math.max(...childEls.map(ch => rel(ch).right)); + } else { + closestEdge = Math.min(...childEls.map(ch => rel(ch).left)); + } + // Place midX at the midpoint between parent edge and closest child edge, + // but guarantee at least 16px clearance from child nodes + const childClearance = 16; + const midpoint = startX + (closestEdge - startX) / 2; + const midFromChild = dir === 'left' ? closestEdge + childClearance : closestEdge - childClearance; + // Use the position that's further from children (safer) + const midX = dir === 'left' + ? Math.min(midpoint, midFromChild) + : Math.max(midpoint, midFromChild); + + drawLine(startX, startY, midX, startY, color, width); + + // Draw ONE continuous vertical line spanning from parent to the last child + const allCYs = childEls.map(ch => rel(ch).cy); + const minY = Math.min(startY, ...allCYs); + const maxY = Math.max(startY, ...allCYs); + drawLine(midX, minY, midX, maxY, color, width); + + // Then draw horizontal lines from the vertical spine to each child + childEls.forEach(ch => { + const c = rel(ch); + const endX = dir === 'left' ? c.right : c.left; + const endY = c.cy; + drawLine(midX, endY, endX, endY, color, width); + }); + } + + // ─── Recursively process subtree (supports .children + .leaf-group + .deep-group) ─── + const NODE_SEL = '.branch-node, .sub-node, .leaf, .deep-node'; + const CONTAINER_SEL = ':scope > .children, :scope > .leaf-group, :scope > .deep-group'; + + function processChildren(parentNodeEl, containerEl, branchColor, depth, dir) { + if (!containerEl) return; + const childNodeEls = []; + for (const wrapper of containerEl.children) { + const nodeEl = wrapper.matches?.(NODE_SEL) ? wrapper : wrapper.querySelector(NODE_SEL); + if (nodeEl) childNodeEls.push(nodeEl); + } + if (!childNodeEls.length) return; + const style = getLineStyle(branchColor, depth); + connect(parentNodeEl, childNodeEls, style.color, style.width, dir); + + for (const wrapper of containerEl.children) { + const nodeEl = wrapper.matches?.(NODE_SEL) ? wrapper : wrapper.querySelector(NODE_SEL); + if (!nodeEl) continue; + wrapper.querySelectorAll(CONTAINER_SEL).forEach(nc => + processChildren(nodeEl, nc, branchColor, depth + 1, dir) + ); + } + } + + // ─── Main flow ─── + const rootNode = map.querySelector('.root-node'); + if (!rootNode) { console.error('❌ .root-node not found'); return; } + const rp = rel(rootNode); + + // Left side: collect all L1 branch-nodes, draw polylines with connect() (with vertical spine) + const leftSel = '.left-side > .branch, .left-branches > .left-branch, .left-branches > div'; + const leftBranches = map.querySelectorAll(leftSel); + const leftBranchNodes = []; + leftBranches.forEach(branch => { + const bNode = branch.querySelector('.branch-node'); + if (bNode) leftBranchNodes.push(bNode); + }); + if (leftBranchNodes.length) { + connect(rootNode, leftBranchNodes, lineStyles[0].color, lineStyles[0].width, 'left'); + } + leftBranches.forEach(branch => { + const bNode = branch.querySelector('.branch-node'); + if (!bNode) return; + processChildren(bNode, branch.querySelector(':scope > .children'), null, 1, 'left'); + }); + + // Right side: same as above + const rightSel = '.right-side > .branch, .branches > .branch, .right-branches > .right-branch'; + const rightBranches = map.querySelectorAll(rightSel); + const rightBranchNodes = []; + rightBranches.forEach(branch => { + const bNode = branch.querySelector('.branch-node'); + if (bNode) rightBranchNodes.push(bNode); + }); + if (rightBranchNodes.length) { + connect(rootNode, rightBranchNodes, lineStyles[0].color, lineStyles[0].width, 'right'); + } + rightBranches.forEach(branch => { + const bNode = branch.querySelector('.branch-node'); + if (!bNode) return; + processChildren(bNode, branch.querySelector(':scope > .children'), null, 1, 'right'); + }); + + map.insertBefore(svg, map.firstChild); + console.log(`✅ Drew ${lineCount} lines`); +} +``` + +**How to call (at end of HTML):** +```html + +``` + +### 3.3 Universal CSS Base (Shared by All Styles) + +```css +* { margin: 0; padding: 0; box-sizing: border-box; } +body { + background: #FFFFFF; + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'SimHei', sans-serif; +} +#mindmap { padding: 60px; display: inline-block; min-width: 100%; position: relative; } +#mindmap > svg.lines { + position: absolute; top: 0; left: 0; width: 100%; height: 100%; + pointer-events: none; z-index: 0; +} + +/* ─── Root Node ─── */ +/* ─── Topic Intent Color System ─── */ +/* Model picks ONE intent based on content semantics. Add data-intent="xxx" to #mindmap. + DO NOT manually write hex colors for root node — use intent system only. */ + +/* Intent → Root Node + Branch Palette mapping */ +#mindmap[data-intent="professional"] .root-node { background: linear-gradient(135deg, #1E3A5F, #2D4A6F); box-shadow: 0 4px 12px rgba(30,58,95,0.25); } +#mindmap[data-intent="technical"] .root-node { background: linear-gradient(135deg, #334155, #475569); box-shadow: 0 4px 12px rgba(51,65,85,0.25); } +#mindmap[data-intent="medical"] .root-node { background: linear-gradient(135deg, #0F766E, #0D9488); box-shadow: 0 4px 12px rgba(15,118,110,0.25); } +#mindmap[data-intent="education"] .root-node { background: linear-gradient(135deg, #9A3412, #B45309); box-shadow: 0 4px 12px rgba(154,52,18,0.25); } +#mindmap[data-intent="creative"] .root-node { background: linear-gradient(135deg, #7C3AED, #8B5CF6); box-shadow: 0 4px 12px rgba(124,58,237,0.25); } +#mindmap[data-intent="finance"] .root-node { background: linear-gradient(135deg, #1E3A5F, #1E40AF); box-shadow: 0 4px 12px rgba(30,58,95,0.25); } +#mindmap[data-intent="nature"] .root-node { background: linear-gradient(135deg, #166534, #15803D); box-shadow: 0 4px 12px rgba(22,101,52,0.25); } +#mindmap[data-intent="warning"] .root-node { background: linear-gradient(135deg, #991B1B, #B91C1C); box-shadow: 0 4px 12px rgba(153,27,27,0.25); } +#mindmap[data-intent="neutral"] .root-node { background: linear-gradient(135deg, #334155, #475569); box-shadow: 0 4px 12px rgba(51,65,85,0.25); } + +/* +Intent Selection Guide (for model): + professional → corporate reports, strategy, management, business plans + technical → software, engineering, architecture, systems, AI/ML + medical → healthcare, clinical, pharmaceutical, nursing, anatomy + education → teaching, learning, curriculum, training, academic + creative → design, art, marketing, branding, media + finance → banking, investment, accounting, economics, trading + nature → environment, ecology, agriculture, biology, geography + warning → risk analysis, safety, incident review, compliance, audit + neutral → general topics, mixed content, unclear domain + +Recommended branch color combos per intent: + professional → [blue, teal, cyan] + technical → [blue, purple, cyan] + medical → [teal, green, blue] + education → [amber, green, blue] + creative → [purple, amber, cyan] + finance → [blue, green, cyan] + nature → [green, teal, amber] + warning → [red, amber, cyan] + neutral → [blue, green, purple] +*/ + +.root-node { + color: white; font-size: 20px; font-weight: 700; + padding: 18px 28px; border-radius: 12px; + white-space: nowrap; flex-shrink: 0; align-self: center; +} +/* Fallback if no intent specified — defaults to professional blue */ +#mindmap:not([data-intent]) .root-node { + background: linear-gradient(135deg, #1E3A5F, #2D4A6F); + box-shadow: 0 4px 12px rgba(30,58,95,0.25); +} + +/* ─── Container Layout ─── */ +.tree { display: flex; align-items: flex-start; gap: 0; position: relative; } +.branches { display: flex; flex-direction: column; gap: 16px; margin-left: 60px; } +.branch, .sub-branch { display: flex; align-items: flex-start; gap: 0; } + +/* ─── First-Level Branch Node ─── */ +.branch-node { + font-size: 15px; font-weight: 600; + padding: 10px 20px; border-radius: 8px; + white-space: nowrap; flex-shrink: 0; border: 2px solid; +} +/* ─── Second-Level Sub-Node (when has children) ─── */ +.sub-node { + font-size: 13px; font-weight: 500; + padding: 7px 14px; border-radius: 6px; + white-space: nowrap; flex-shrink: 0; border: 1.5px solid; + background: #F8FAFC; +} +/* ─── Leaf Node ─── */ +/* Default: lightweight capsule frame */ +.leaf { + font-size: 13px; font-weight: 400; color: #475569; + padding: 4px 10px; background: #FAFAFA; + border-radius: 12px; border: 1px solid #E5E7EB; + white-space: nowrap; line-height: 1.5; +} +/* Text-only mode (more compact, add class="leaf text-only") */ +.leaf.text-only { + background: none; border: none; padding: 2px 0; border-radius: 0; +} +/* Leaf supplementary description */ +.leaf-desc { + font-size: 12px; color: #94A3B8; font-weight: 400; margin-left: 8px; +} +.leaf-desc::before { content: '— '; color: #CBD5E1; } + +/* ─── Child Node Container ─── */ +/* Principle: The deeper the level, the smaller the gap, but not so small that connectors and text overlap */ +.children { display: flex; flex-direction: column; gap: 6px; margin-left: 48px; align-items: flex-start; } +.children:has(.sub-branch) { gap: 10px; } /* sub-branch is larger than leaf, needs more spacing */ +.sub-branch .children { gap: 5px; margin-left: 40px; align-items: flex-start; } +.sub-branch .sub-branch .children { gap: 4px; margin-left: 36px; align-items: flex-start; } +/* Tip: If leaf text and connectors overlap, prioritize increasing gap */ + +/* ─── Color System (for distinguishing different branches) ─── */ +.c-blue { background: #EFF6FF; border-color: #3B82F6; color: #1E40AF; } +.c-green { background: #F0FDF4; border-color: #10B981; color: #065F46; } +.c-amber { background: #FFF7ED; border-color: #F59E0B; color: #92400E; } +.c-purple { background: #F5F3FF; border-color: #8B5CF6; color: #5B21B6; } +.c-red { background: #FEF2F2; border-color: #EF4444; color: #991B1B; } +.c-cyan { background: #ECFEFF; border-color: #06B6D4; color: #155E75; } +.c-teal { background: #F0FDFA; border-color: #14B8A6; color: #134E4A; } + +/* Second-level sub-node inherits branch color scheme (lighter) */ +.c-blue .sub-node { background: #F0F7FF; border-color: #93C5FD; color: #1E40AF; } +.c-green .sub-node { background: #F2FDF6; border-color: #6EE7B7; color: #065F46; } +.c-amber .sub-node { background: #FFF9F0; border-color: #FCD34D; color: #92400E; } +.c-purple .sub-node { background: #F8F6FF; border-color: #C4B5FD; color: #5B21B6; } +.c-red .sub-node { background: #FFF5F5; border-color: #FCA5A5; color: #991B1B; } +.c-cyan .sub-node { background: #F0FEFF; border-color: #67E8F9; color: #155E75; } +.c-teal .sub-node { background: #F2FDFA; border-color: #5EEAD4; color: #134E4A; } +``` + +--- + +## Style A: Right-Expanding Tree (Simple scenarios with branchCount ≤ 4 or maxDepth ≤ 2) + +### HTML Structure + +```html + + + + + + + +
+
+
中心主题
+
+ +
+
一级分支A
+
+
叶子1
+
叶子2
+
+
+ +
+
一级分支B
+
+
+
二级有下级
+
+
三级叶子1
+
三级叶子2
+
+
+
二级叶子
+
+
+ +
+
+
+ + + +``` + +--- + +## Style B: Left-Right Expanding Tree (Standard scenario with branchCount ≥ 5, most common) + +### Additional CSS (append after universal base) + +```css +/* ─── Left-Right Expanding Layout ─── */ +.lr-tree { display: flex; align-items: center; gap: 0; position: relative; } +.lr-tree .root-node { + padding: 20px 32px; border-radius: 14px; + margin: 0 60px; text-align: center; +} + +/* ⚠️ sub-branch must be flex, otherwise flex-direction: row-reverse won't work, leaves won't expand left */ +.sub-branch { display: flex; align-items: flex-start; gap: 0; } + +.left-branches { display: flex; flex-direction: column; gap: 16px; } +.left-branch { display: flex; align-items: flex-start; flex-direction: row-reverse; gap: 0; } +.left-branch .children { + display: flex; flex-direction: column; gap: 6px; + margin-right: 48px; align-items: flex-end; +} +.left-branch .children:has(.sub-branch) { gap: 10px; } +.left-branch .sub-branch .children { gap: 5px; margin-right: 40px; margin-left: 0; align-items: flex-end; } +.left-branch .sub-branch { flex-direction: row-reverse; } + +.right-branches { display: flex; flex-direction: column; gap: 16px; } +.right-branch { display: flex; align-items: flex-start; gap: 0; } +.right-branch .children { + display: flex; flex-direction: column; gap: 6px; margin-left: 48px; align-items: flex-start; +} +.right-branch .children:has(.sub-branch) { gap: 10px; } +.right-branch .sub-branch .children { gap: 5px; margin-left: 40px; align-items: flex-start; } +``` + +### HTML Structure + +```html +
+
+ + +
+
+
分支D(放左边)
+
+
叶子1
+
叶子2
+
+
+ +
+ +
中心主题
+ + +
+
+
分支A(放右边)
+
+
叶子1
+
叶子2
+
+
+ +
+ +
+
+ +``` + +--- + +## Style C: Card Grid (when user explicitly requests "cards" / "modules") + +> ⚠️ This is not a mind map — it's a modular display. No connectors; use cards + color coding to show relationships. + +```html + + + + + + + +
+
标题
+
副标题
+
+
+
+
📋
+
模块A
+
+
    +
  • 条目1
  • +
  • 条目2
  • +
+
+ +
+
+ + +``` + +--- + +## Style D: Fishbone Diagram (Problem Analysis / Root Cause) + +Best for problem analysis, root cause tracing, quality management (Ishikawa diagram). + +```html + + + + + + + +
+
+
+
+
+
问题/结果
+
+ +
+
+
+
子原因1
+
子原因2
+
+
+
原因类别A
+
+
+ +
+
+
原因类别B
+
+
+
子原因1
+
子原因2
+
+
+
+
+
+ + +``` + +--- + +## Quality Checklist + +After rendering, verify against this checklist: + +1. **Content complete** — Every node from the original requirement is in the map, nothing missing +2. **Clear hierarchy** — L1 branches and leaves are instantly distinguishable (different bg/border/font-weight) +3. **No overlap** — No boxes covering boxes, no lines through text +4. **Connectors visible** — Every parent-child pair has a connector, no orphan leaves. Connector color ≥ `#94A3B8` (too light = invisible) +5. **Connector direction correct** — Left-side leaves extend left, right-side extends right +6. **Proportions reasonable** — Map is not extremely narrow/tall (target aspect ratio 1:1 to 3:1), visually comfortable +7. **Text readable** — At final output size, smallest text is legible. Reference: root 18px+, L1 15px+, L2 13px+, leaves 12px+ +8. **Roughly balanced** (Style B) — Visual weight approximately equal on both sides, doesn't need to be symmetric. Target ≤30% difference +9. **Canvas large enough** — No nodes clipped, sufficient padding on all sides (reference 60px+) +10. **No large blank areas on right** — Screenshot trimmed to content edge + +--- + +## ⛔ Radial Layout Warning + +**Radial layout is strongly discouraged.** Using `position: absolute` for fixed branch positions leads to overlap when branches increase, and deep levels cannot be handled. + +If content is very simple (reference: ≤3 branches, ≤3 children per branch, text ≤6 chars, depth ≤2, total ≤12 nodes), it's technically possible, but tree layout is always the safer choice. + +**No code template provided.** If conditions are met, manually adjust based on Style A. diff --git a/skills/charts/references/playwright-css.md b/skills/charts/references/playwright-css.md new file mode 100755 index 0000000..164a298 --- /dev/null +++ b/skills/charts/references/playwright-css.md @@ -0,0 +1,801 @@ +# Playwright + CSS Rendering Engine + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + +**Core principle: Content-driven, not template-driven. Analyze content structure first, then decide layout, then render.** + +Applicable to: flowcharts, infographics, KPI cards, data posters — any visualization requiring full CSS power (gradients, shadows, rounded corners, Grid/Flexbox layout). + +--- + +## Step 1: Content Analysis + +When you receive a flowchart/infographic requirement, **don't write HTML/CSS first**. Analyze the content structure. + +### 1.1 Flowchart Content Analysis + +``` +Input: "Honey production process", 8 steps, linear without branches + ↓ +{ + "type": "flowchart", + "nodes": [ + { "id": "1", "label": "花粉采集", "desc": "蜜蜂从花朵采集花蜜" }, + { "id": "2", "label": "酿造", "desc": "蜜蜂在蜂巢中反复吞吐" }, + ... + ], + "edges": [["1","2"], ["2","3"], ...], + "nodeTypes": { "start": ["1"], "end": ["8"], "decision": [], "normal": ["2","3","4","5","6","7"] } +} +``` + +### 1.2 Key Metrics + +| Metric | Calculation | Affects | +|------|---------|---------| +| `nodeCount` | Total node count | Mermaid vs CSS flowchart | +| `maxTextLen` | Longest text char count | Node width | +| `hasDecision` | Has decision branches | Layout complexity | +| `hasBranch` | Has parallel/branches | Column count | +| `parallelCount` | Max parallel branches | Column count | +| `phaseCount` | Phase/group count | Need for phased containers | +| `hasRoles` | Has roles/swimlanes | Need for dual-panel | +| `isLinear` | Linear without branches | Can use snake layout | + +### 1.3 Infographic Content Analysis + +Infographics (KPI cards, data posters) are simpler to analyze: +- How many data metrics? → Determines grid columns +- Any trend data? → Need for sparklines +- Title area? → Need for hero header +- Comparison data? → Need for bar chart + +--- + +## Step 2: Layout Decision + +### 2.1 Flowchart Layout Decision Tree + +``` +⚠️ DEFAULT RULE: When user asks "generate/create XXX 流程图" without specifying format, + DEFAULT to Layout C (Phased Vertical). Almost all real-world processes have phases. + +User specified Mermaid/markdown? + └─ Yes → Follow user choice (Format Constraint Rule) + └─ No → + nodeCount ≤ 6 AND no phases AND maxTextLen ≤ 8 (CJK)? + └─ Yes → Mermaid (minimal flowchart) + └─ No → + phaseCount > 0 OR nodeCount ≥ 5? + └─ Yes → ⭐ Layout C: CSS Phased Vertical flowchart (DEFAULT) + └─ No → + hasRoles? + └─ Yes → Layout D: CSS Dual-panel/Swimlane flowchart + └─ No → Layout C: CSS Phased Vertical flowchart (fallback also uses C) +``` + +**⚠️ Layout A (Grid) and Layout B (Snake) are only for very special scenarios (e.g., flat comparison, unordered parallel items). Flowcharts default to Layout C.** + +### 🚫 Flowchart Anti-Patterns (FORBIDDEN) + +| ❌ Bad Pattern | ✅ Correct Pattern | +|---|---| +| Phase titles (一、二、三...) as isolated left-side text labels | Phase titles as colored title bars, wrapped inside group cards | +| All nodes flat-laid in Grid without group containers | Each phase wrapped in a `.phase-group` card containing its steps | +| Role labels scattered above nodes | Role info displayed uniformly at the top of the flowchart, or as phase card labels | +| Nodes connected with loose diverging lines | Phases connected with arrows (↓), steps within phases use numbering | +| Inconsistent node sizes, uneven spacing, misaligned | Same-phase nodes share uniform style, overall alignment consistent | +| Using Layout A Grid for a phased flowchart | Has phases → must use Layout C | + +### 2.2 Canvas Size Calculation + +```python +def calc_flowchart_canvas(node_count, max_text_len, parallel_count, has_roles, layout): + node_width = max(180, max_text_len * 16) # ~16px per CJK char (including padding) + + if layout == 'snake': + cols = min(4, node_count) + rows = (node_count + cols - 1) // cols + width = cols * (node_width + 60) + 120 # 60=gap, 120=padding + height = rows * 120 + 200 # 120=row height + elif layout == 'dual-panel': + width = max(1600, node_width * 2 + 400) # left panel + right flow + height = node_count * 100 + 200 + elif layout == 'phased-vertical': + width = max(800, node_width + 200) + height = node_count * 80 + 200 + else: # grid + cols = max(parallel_count, 2) + width = cols * (node_width + 60) + 120 + rows = (node_count + cols - 1) // cols + height = rows * 120 + 200 + + return max(width, 800), max(height, 600) +``` + +### 2.3 Color Decision + +**Iron rule: Node background = low-saturation light color, border = high-saturation color. Large high-saturation areas = children's drawing.** + +**⚠️ Text contrast iron rule: Dark/accent background nodes must use light text (white or near-white) for title and description.** +Light background → dark text (`#1F2937`), dark background → light text (`#FFFFFF` or `#FFF7ED`). +Common mistake: Endpoint/highlight node switched to dark background, but description text remains dark gray, making it completely unreadable. + +``` +Node type → color: + Start/end → bg: #EFF6FF, border: #3B82F6 (blue), text: #1E40AF + Normal step → bg: #F8FAFC, border: #94A3B8 (gray-blue), text: #374151 or colored by phase + Decision node → bg: #FFF7ED, border: #F59E0B (amber), text: #92400E + Success/pass → bg: #F0FDF4, border: #10B981 (green), text: #065F46 + End/failure → bg: #F5F3FF, border: #8B5CF6 (purple), text: #5B21B6 + Emphasis/endpoint (dark bg) → bg: #92400E, border: #F59E0B, text: #FFFFFF, desc: #FFF7ED + +Max 3-4 background colors for nodes in the same flowchart. +Overall chart background = white #FFFFFF. +``` + +**Phase bar colors**: Same-hue gradient (blue-gray family), **never use different hues per phase**. + +```css +/* ✅ Same-hue blue-gray progression */ +.phase-1 { background: #F0F4F8; border-left: 4px solid #64748B; } +.phase-2 { background: #E8EDF2; border-left: 4px solid #5B7A99; } + +/* ❌ Different hue per phase (rainbow effect) */ +.phase-1 { background: #EFF6FF; border-left: 4px solid #3B82F6; } /* blue */ +.phase-2 { background: #F0FDF4; border-left: 4px solid #10B981; } /* green */ +.phase-3 { background: #FFF7ED; border-left: 4px solid #F59E0B; } /* amber */ +``` + +--- + +## Step 3: Rendering + +### 3.1 Playwright Screenshot (Universal) + +```python +import asyncio +from playwright.async_api import async_playwright + +async def html_to_image(html_path, output_path, selector='#root', + width=1200, height=None, scale=2): + """HTML → PNG/PDF + + scale: 2 (default crisp), 1.5 (large canvas 3000px+), 3 (print). + Width must accommodate ALL content. After first render, auto-resize viewport to fit. + """ + async with async_playwright() as p: + browser = await p.chromium.launch(headless=True) + page = await browser.new_page( + viewport={'width': width, 'height': height or 800}, + device_scale_factor=scale + ) + await page.goto(f'file://{html_path}', wait_until='networkidle') + await page.wait_for_timeout(500) + + if output_path.endswith('.pdf'): + await page.pdf(path=output_path, print_background=True) + else: + el = page.locator(selector) + bbox = await el.bounding_box() + if bbox: + fit_w = max(width, int(bbox['width'] + 100)) + fit_h = int(bbox['height'] + 100) + await page.set_viewport_size({'width': fit_w, 'height': fit_h}) + await page.wait_for_timeout(200) + await el.screenshot(path=output_path) + + await browser.close() + import os + print(f'✅ {output_path} ({os.path.getsize(output_path)/1024:.0f}KB)') +``` + +### 3.2 HTML Universal Shell + +```html + + + + + + + +
+ +
+ + +``` + +### 3.3 CSS Variables: Node Color System + +```css +:root { + /* Node types — low-saturation background + high-saturation border */ + --node-bg: #EFF6FF; --node-border: #3B82F6; /* Normal step (blue) */ + --node-decision-bg: #FFF7ED; --node-decision-border: #F59E0B; /* Decision (amber) */ + --node-success-bg: #F0FDF4; --node-success-border: #10B981; /* Success (green) */ + --node-end-bg: #F5F3FF; --node-end-border: #8B5CF6; /* End (purple) */ + --group-bg: #F8FAFC; --group-border: #E2E8F0; /* Group container */ +} +``` + +--- + +## Layout A: CSS Grid Flowchart (Universal, Most Common) + +For: Flowcharts with branches/decisions, >10 nodes or long CJK text. + +### Core CSS + +```css +.flow-title { + font-size: 22px; font-weight: 700; color: var(--text); + text-align: center; margin-bottom: 40px; +} +.flow-subtitle { + font-size: 14px; color: var(--text-sub); + text-align: center; margin-top: 8px; +} + +/* Grid container */ +.flow-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 220px)); + gap: 40px 60px; + justify-content: center; + position: relative; +} + +/* Node */ +.flow-node { + background: var(--node-bg); border: 2px solid var(--node-border); + border-radius: 10px; padding: 16px 20px; + text-align: center; position: relative; z-index: 1; + min-height: 56px; display: flex; flex-direction: column; + justify-content: center; align-items: center; + box-shadow: 0 1px 3px rgba(0,0,0,0.06); + max-width: 260px; /* Prevent single node from being too wide and crowding parallel nodes */ + word-break: break-word; /* Force line break for overly long text */ + white-space: normal; /* Allow line breaks (override possible nowrap) */ +} +.flow-node .node-title { font-size: 15px; font-weight: 600; line-height: 1.4; } +.flow-node .node-desc { font-size: 12px; color: var(--text-sub); margin-top: 4px; } + +/* Node variants */ +.flow-node.start { border-radius: 24px; } +.flow-node.decision { background: var(--node-decision-bg); border-color: var(--node-decision-border); } +.flow-node.success { background: var(--node-success-bg); border-color: var(--node-success-border); } +.flow-node.end { background: var(--node-end-bg); border-color: var(--node-end-border); } + +/* Group box */ +.flow-group { + background: var(--group-bg); border: 1.5px dashed var(--group-border); + border-radius: 12px; padding: 24px 20px 20px; position: relative; +} +.flow-group .group-label { + position: absolute; top: -10px; left: 16px; + background: var(--bg); padding: 0 8px; + font-size: 12px; font-weight: 600; color: var(--text-sub); +} + +/* ─── Parallel branch constraints (prevent overlap when multiple nodes in same row) ─── */ +/* + ⚠️ Parallel branch iron rules: + 1. Gap between parallel nodes in same row ≥ 40px (guaranteed by .flow-grid gap) + 2. Each node max-width: 260px + word-break: break-word (set in .flow-node) + 3. If parallel nodes > 3 → switch to vertical branch layout (don't force-squeeze into one row) + 4. Text over 15 CJK characters → must line-break, don't expand node width + 5. When using flex instead of manual grid-column for parallel areas, add flex-wrap: wrap as fallback +*/ +.parallel-group { + display: flex; gap: 40px; justify-content: center; + flex-wrap: wrap; /* Fallback: auto-wrap when exceeding width */ +} +.parallel-group .flow-node { + flex: 0 1 220px; /* Max 220px, can shrink, won't grow infinitely */ +} + +/* SVG connector layer */ +.flow-connectors { + position: absolute; top: 0; left: 0; width: 100%; height: 100%; + pointer-events: none; z-index: 0; +} +.flow-connectors line, .flow-connectors path { + stroke: var(--connector); stroke-width: 2; fill: none; + marker-end: url(#arrowhead); +} +.connector-label { + font-size: 12px; fill: var(--text-sub); text-anchor: middle; + font-family: -apple-system, 'PingFang SC', 'SimHei', sans-serif; +} + +/* Legend — must be independent container, not inside flow-grid */ +.flow-legend { + display: flex; gap: 24px; justify-content: center; + margin-top: 40px; padding: 16px 24px; + background: #F9FAFB; border-radius: 8px; border: 1px solid #E5E7EB; +} +.legend-item { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #4B5563; } +.legend-dot { width: 12px; height: 12px; border-radius: 3px; border: 2px solid; } +``` + +### Auto Connector Script + +```javascript +// connections = [['sourceID', 'targetID', 'label'], ...] +function drawConnectors(connections) { + const svg = document.getElementById('connectorSvg'); + const container = svg.parentElement; + const cRect = container.getBoundingClientRect(); + + svg.setAttribute('width', cRect.width); + svg.setAttribute('height', cRect.height); + svg.setAttribute('viewBox', `0 0 ${cRect.width} ${cRect.height}`); + + // Clear old connectors (keep defs) + svg.querySelectorAll('line, path, text.connector-label').forEach(el => el.remove()); + + connections.forEach(([fromId, toId, label]) => { + const fromEl = document.querySelector(`[data-id="${fromId}"]`); + const toEl = document.querySelector(`[data-id="${toId}"]`); + if (!fromEl || !toEl) return; + + const f = fromEl.getBoundingClientRect(); + const t = toEl.getBoundingClientRect(); + const x1 = f.left + f.width/2 - cRect.left; + const y1 = f.bottom - cRect.top; + const x2 = t.left + t.width/2 - cRect.left; + const y2 = t.top - cRect.top; + + if (Math.abs(x1 - x2) < 10) { + // Same column → straight line + const line = document.createElementNS('http://www.w3.org/2000/svg', 'line'); + line.setAttribute('x1', x1); line.setAttribute('y1', y1); + line.setAttribute('x2', x2); line.setAttribute('y2', y2); + svg.appendChild(line); + } else { + // Different column → bent line + const midY = (y1 + y2) / 2; + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', `M ${x1} ${y1} L ${x1} ${midY} L ${x2} ${midY} L ${x2} ${y2}`); + svg.appendChild(path); + } + + if (label) { + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); + text.setAttribute('x', (x1 + x2) / 2); + text.setAttribute('y', (y1 + y2) / 2 - 6); + text.setAttribute('class', 'connector-label'); + text.textContent = label; + svg.appendChild(text); + } + }); +} + +// SVG defs (arrow definitions) +function ensureArrowDef(svg) { + if (svg.querySelector('#arrowhead')) return; + const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); + defs.innerHTML = ''; + svg.appendChild(defs); +} +``` + +### HTML Structure Example + +```html +
+
流程图标题
+ +
+ +
+
+
开始
+
+
+
步骤一
+
详细说明
+
+
+
判断条件?
+
+
+
结束
+
+
+
+ +
+
步骤
+
判断
+
+
+ + +``` + +--- + +## Layout B: Snake Flowchart (>4 Linear Steps) + +For: Linear non-branching processes with many steps (5-20). + +### Key Rules + +- Max 4 nodes per row +- First row left→right, second row right→left (snake pattern) +- End-of-row to next-row turn connectors use bend lines, with ≥60px clearance at turns +- Node positions in grid are auto-calculated by JS (no manual grid-column) + +### Snake Layout Generation Script + +```javascript +function layoutSnake(nodeIds, cols) { + cols = cols || 4; + nodeIds.forEach((id, i) => { + const row = Math.floor(i / cols); + const colInRow = i % cols; + const col = row % 2 === 0 ? colInRow + 1 : cols - colInRow; // Even rows L→R, odd rows R→L + const el = document.querySelector(`[data-id="${id}"]`); + if (el) { + el.style.gridRow = row + 1; + el.style.gridColumn = col; + } + }); +} +``` + +--- + +## Layout C: Phased Vertical Flowchart (⭐ DEFAULT for all flowcharts) + +**This is the DEFAULT layout for flowcharts.** When the user asks for any flowchart without specifying format, use this layout. + +For: Any process with phases/stages, which is nearly ALL real-world processes (manufacturing, legal, project management, business operations, etc.). Also the safe fallback when unsure. + +**Why this is the default:** Layout C produces consistently professional, readable results. Even if the process has only 2 "phases", the card-based grouping still looks clean. In contrast, Layout A (Grid) without proper grouping produces scattered, unreadable results. + +### Key Design + +**Phase titles vs sub-steps must have clear visual distinction**: +- Phase titles: colored background, font-size ≥ 16px, font-weight: 700 +- Sub-steps: white/light gray background, font-size 14-15px, font-weight: 400-500 + +**No arrows between sub-steps**. Arrows only connect phase-to-phase. Sub-steps use indent + numbering for sequence. + +### Phase Connector Direction Rule +Phase-to-phase connector arrows MUST match the logical flow direction. If the flow goes top → bottom, arrows point ↓. If bottom → top, arrows point ↑. If left → right, arrows point →. **Never draw arrows opposing the flow direction.** + +### Additional CSS + +```css +.phase-group { + background: #F8FAFC; border-radius: 12px; padding: 20px 24px; + margin-bottom: 24px; +} +.phase-title { + font-size: 16px; font-weight: 700; padding: 10px 16px; + border-radius: 8px; margin-bottom: 16px; +} +.phase-steps { display: flex; flex-direction: column; gap: 8px; padding-left: 12px; } +.phase-step { + font-size: 14px; font-weight: 400; color: var(--text); + padding: 8px 14px; background: white; border-radius: 6px; + border: 1px solid var(--border); +} +.phase-step .step-num { + display: inline-block; width: 22px; height: 22px; line-height: 22px; + text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600; + margin-right: 8px; +} + +/* Phase colors — same-hue blue-gray gradient (low saturation, easy on the eyes) + All phases share the blue-gray color family, distinguished by brightness progression. + 🚫 FORBIDDEN: Different hue per phase (blue→green→amber→purple) — becomes rainbow with many phases. + ✅ CORRECT: Progress within same hue family (light→dark), or pure grayscale + single-color accent. + + Two schemes provided below; model selects based on phase count: + - ≤4 phases: Scheme A (blue-gray progression) + - 5-7 phases: Scheme B (neutral gray base + blue accent progression) + - >7 phases: all use same gray base, distinguish only by numbering +*/ + +/* Scheme A: Blue-gray progression (≤4 phases) */ +.phase-1 .phase-title { background: #F0F4F8; color: #334155; border-left: 4px solid #64748B; } +.phase-1 .step-num { background: #E2E8F0; color: #475569; } +.phase-2 .phase-title { background: #E8EDF2; color: #1E3A5F; border-left: 4px solid #5B7A99; } +.phase-2 .step-num { background: #DBEAFE; color: #1E3A5F; } +.phase-3 .phase-title { background: #E0E7EF; color: #1E3050; border-left: 4px solid #4A6B8A; } +.phase-3 .step-num { background: #D0D9E4; color: #1E3050; } +.phase-4 .phase-title { background: #D8E0EA; color: #172540; border-left: 4px solid #3A5C7A; } +.phase-4 .step-num { background: #C7D2E0; color: #172540; } + +/* Scheme B: Neutral gray base + blue accent progression (5-7 phases) */ +/* +.phase-1 .phase-title { background: #F8FAFC; color: #334155; border-left: 4px solid #94A3B8; } +.phase-1 .step-num { background: #F1F5F9; color: #475569; } +.phase-2 .phase-title { background: #F1F5F9; color: #334155; border-left: 4px solid #7B8FA3; } +.phase-2 .step-num { background: #E2E8F0; color: #475569; } +.phase-3 .phase-title { background: #E8EDF3; color: #2D4156; border-left: 4px solid #6B809A; } +.phase-3 .step-num { background: #DBEAFE; color: #2D4156; } +.phase-4 .phase-title { background: #E2E8F0; color: #283C52; border-left: 4px solid #5B7590; } +.phase-4 .step-num { background: #D0D9E4; color: #283C52; } +.phase-5 .phase-title { background: #DAE1EB; color: #23364D; border-left: 4px solid #4B6A87; } +.phase-5 .step-num { background: #C7D2E0; color: #23364D; } +.phase-6 .phase-title { background: #D2DAE5; color: #1E3048; border-left: 4px solid #3B5F7D; } +.phase-6 .step-num { background: #BFC9D8; color: #1E3048; } +.phase-7 .phase-title { background: #CAD3E0; color: #192A3E; border-left: 4px solid #2B5473; } +.phase-7 .step-num { background: #B7C2D2; color: #192A3E; } +*/ +``` + +### HTML Structure + +```html +
+
项目流程
+ +
+
第一阶段:需求分析
+
+
1需求收集与整理
+
2可行性评估
+
3需求优先级排序
+
+
+ + +
+ +
+
第二阶段:设计开发
+
+
4UI/UX 设计
+
5前后端开发
+
+
+
+``` + +--- + +## Layout D: Dual-Panel / Swimlane Flowchart + +For: Processes involving multiple roles or departments. + +### Key Rules + +- Canvas width ≥ 1600px +- Left panel for role/swimlane labels, right panel for flow nodes +- Role labels font-size ≥ 12px, solid background, right edge ≥ 40px from canvas +- `overflow: hidden` is forbidden + +### Additional CSS + +```css +.dual-layout { display: flex; gap: 40px; } +.role-panel { + flex-shrink: 0; width: 160px; + display: flex; flex-direction: column; gap: 12px; +} +.role-tag { + font-size: 13px; font-weight: 600; padding: 8px 12px; + border-radius: 6px; text-align: center; +} +.flow-panel { flex: 1; min-width: 0; } +``` + +--- + +## Infographic Templates + +The following templates are for non-flowchart information visualization. + +### Template: KPI Dashboard Cards + +```css +.kpi-grid { + display: grid; grid-template-columns: repeat(4, 1fr); + gap: 20px; margin-bottom: 32px; +} +.kpi-card { + background: var(--surface); border: 1px solid var(--border); + border-radius: 12px; padding: 24px; text-align: center; +} +.kpi-label { font-size: 13px; color: var(--text-sub); margin-bottom: 8px; } +.kpi-value { font-size: 32px; font-weight: 700; } +.kpi-change { font-size: 14px; font-weight: 600; margin-top: 8px; } +.kpi-change.up { color: var(--positive); } +.kpi-change.down { color: var(--negative); } +``` + +### Template: CSS Bar Chart (Pure CSS, No JS) + +```css +.bar-chart { + display: flex; align-items: flex-end; gap: 16px; + height: 300px; padding: 0 20px; border-bottom: 1px solid var(--border); +} +.bar-item { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 8px; } +.bar { width: 100%; max-width: 60px; border-radius: 6px 6px 0 0; background: var(--border); } +.bar.highlight { background: var(--blue); } +.bar-label { font-size: 12px; color: var(--text-sub); } +.bar-value { font-size: 11px; color: var(--text-muted); font-weight: 600; } +``` + +### Template: Gradient Background Infographic Header + +```css +.hero-header { + background: linear-gradient(135deg, #1E293B 0%, #0F172A 100%); + border-radius: 16px; padding: 48px; color: white; margin-bottom: 32px; +} +.hero-header h1 { font-size: 28px; font-weight: 700; margin-bottom: 12px; } +.hero-header p { font-size: 15px; color: #94A3B8; max-width: 600px; line-height: 1.6; } +.hero-badge { + display: inline-block; background: rgba(59,130,246,0.15); + color: #60A5FA; font-size: 12px; font-weight: 600; + padding: 4px 12px; border-radius: 20px; margin-bottom: 16px; +} +``` + +### Template: Data Card + Mini Sparkline + +```html +
+
+
月活用户
+
34,521
+
↑ +18.2%
+
+
+ + + +
+
+``` + +```css +.metric-card { + background: white; border: 1px solid var(--border); + border-radius: 16px; padding: 28px; + display: flex; justify-content: space-between; align-items: center; +} +.metric-title { font-size: 13px; color: var(--text-sub); } +.metric-value { font-size: 36px; font-weight: 700; margin: 4px 0; } +.metric-change { font-size: 14px; font-weight: 600; } +``` + +--- + +## Advanced Connector Rules + +### Many-to-One Convergence + +When multiple lines converge into one node, use the "merge first, then enter" pattern: + +``` +[A] ──┐ +[B] ──┤── → [目标] +[C] ──┘ +``` + +Implementation: Source lines reach a relay x-coordinate, merge into one vertical line, then a single line enters the target. + +### Cross-Layer Connector Avoidance + +If other nodes block the path between two nodes, **do not draw a straight line through them**. + +Priority: +1. **Redesign hierarchy** (best) — Most cross-layer lines indicate hierarchy design issues; connect adjacent layers only +2. **Detour line** — Route around middle nodes via canvas edge (offset 40px right) +3. **Thread through gap** — If middle-layer node gap ≥ 40px, route through the gap + +### Connector Alignment + +- Multiple lines from the same node start at a consistent position on the node border +- Use only one connector style per chart (right-angle/curved/straight), no mixing +- Connector label positions must be consistent (all above line or all centered) + +--- + +## Font Size Rules + +| Element | Recommended | Minimum | +|------|------|------| +| Flowchart main node title | 16-18px | 14px | +| Node description / subtext | 13-15px | 12px | +| Connector labels | 12-14px | 11px | +| Legend text | 13-14px | 12px | +| Footnotes / watermark | 11-13px | 10px | +| Phase titles | 16-18px | 16px | +| Role labels | 13-14px | 12px | + +**Not enough space → Enlarge canvas, don't shrink fonts.** + +--- + +## Overflow Protection + +1. **`#root { width: fit-content; min-width: 800px; }`** — Expands with content automatically +2. **`overflow: hidden` / `overflow: clip` are forbidden** +3. **Auto-resize viewport before Playwright screenshot** (see 3.1 screenshot script) +4. **Canvas minimum width**: + +| Layout Type | Minimum Width | Recommended | +|---------|---------|------| +| Single-column flowchart | 800px | 1000px | +| Dual-panel / swimlane | 1400px | 1600-1800px | +| Three-column / multi-panel | 1800px | 2000-2400px | + +--- + +## Quality Checklist + +1. **Layout C used by default** — If this is a flowchart, verify you're using Layout C (Phased Vertical) unless there's a specific reason not to +2. **Content complete** — Every node/step from the requirement is in the chart +3. **No overlap** — No boxes covering boxes, no lines through boxes +4. **Clear hierarchy** — Phase titles and sub-steps are instantly distinguishable +5. **Colors reasonable** — Node backgrounds low-saturation, no children's-drawing palette +6. **Connectors visible** — Connector color ≥ `#94A3B8`, arrow direction correct +7. **Font sizes meet standards** — Check against font size table, nothing below minimum +8. **Legend independent** — Not inside flow-grid, not obscured by any node +9. **No clipping** — Padding ≥ 40px on all sides, all nodes and labels fully visible +10. **Phase colors consistent** — Same hue family, no blue/brown/green/purple mix +11. **Connectors don't pass through nodes** — Cross-layer lines use detour or redesign hierarchy +12. **No scattered layout** — Phase titles MUST be inside group cards, NOT floating as isolated labels + +--- + +## Playwright+CSS vs Other Approaches + +| Capability | Playwright+CSS | matplotlib | ECharts | +|------|---------------|------------|---------| +| Gradients/shadows/rounded | ✅ Full CSS power | ❌ Limited | ⚠️ Partial | +| Responsive layout | ✅ Flexbox/Grid | ❌ Fixed size | ⚠️ resize | +| PNG/PDF export | ✅ Native | ✅ savefig | ⚠️ Needs Playwright | +| Precise data charts | ⚠️ Manual | ✅ Built-in | ✅ Built-in | + +**Best practice: CSS for layout and visual design, embed ECharts/SVG for precise charts.** diff --git a/skills/charts/references/radial-grid.md b/skills/charts/references/radial-grid.md new file mode 100755 index 0000000..38dad1e --- /dev/null +++ b/skills/charts/references/radial-grid.md @@ -0,0 +1,576 @@ +# CSS Radial Grid Layout (Center-Outward Diagrams) + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + +**For: SWOT analysis, Balanced Scorecard (BSC), Porter's Five Forces, PEST analysis, and any "center + 4-6 surrounding dimensions" diagram.** + +**Core principle: Use flex rows to lock positions — model never calculates coordinates. Connectors are drawn by script reading bounding boxes.** + +--- + +## When to Use This Template + +| Diagram Type | Dimensions | Use This? | +|-------------|-----------|-----------| +| SWOT (Strengths/Weaknesses/Opportunities/Threats) | 4 quadrants | ✅ Layout B (2×2 Grid) | +| Balanced Scorecard (Financial/Customer/Internal/Learning) | 4 dimensions | ✅ Layout A (Cross) | +| Porter's Five Forces | 5 forces | ✅ Layout A (Cross + extra row) | +| PEST (Political/Economic/Social/Technological) | 4 dimensions | ✅ Layout B (2×2 Grid) | +| Competency wheel / capability map | 5-8 dimensions | ✅ Layout A with extra rows | +| Anything with center + surrounding elements | 3-8 | ✅ | + +--- + +## Layout A: Cross Layout (4-6 Dimensions) + +Best for: BSC, Porter's Five Forces, any "center with surrounding dimensions" structure. + +### 🚫 FORBIDDEN: 3×3 CSS Grid Cross + +**Do NOT use `grid-template-columns: Xpx Ypx Xpx` to place cards in a cross pattern.** The top/bottom cards in the center column will overflow into the side columns and overlap with left/right cards when content is longer than expected. + +### ✅ REQUIRED: Three-Row Flex Layout + +**Each row is an independent flex container. Rows cannot overlap each other — physically impossible.** + +``` +Row 1 (top): [top dimension card] ← independent flex row, centered +Row 2 (middle): [left card] [center] [right card] ← independent flex row, horizontal +Row 3 (bottom): [bottom dimension card] ← independent flex row, centered +``` + +### HTML + CSS + +```html + + + + + + + +
+
平衡计分卡四维评价体系
+
基于战略目标的绩效管理框架
+ +
+ + + + +
+
F
财务维度
+
    +
  • 营收增长率
  • +
  • 利润率
  • +
  • ROI
  • +
+
+ + +
+
+
I
内部流程
+
    +
  • 流程效率
  • +
  • 质量管控
  • +
  • 创新能力
  • +
+
+ +
战略目标
+ +
+
C
客户维度
+
    +
  • 客户满意度
  • +
  • 市场份额
  • +
  • 客户留存率
  • +
+
+
+ + +
+
L
学习与成长
+
    +
  • 员工能力提升
  • +
  • 信息系统建设
  • +
  • 组织文化
  • +
+
+
+
+ + + + +``` + +**Key design decisions:** +- **Three-row flex** instead of 3×3 Grid — rows physically cannot overlap +- **Fixed card width (260px)** — prevents content-driven overflow +- **`gap: 24px`** between rows, **`gap: 40px`** within middle row — generous spacing +- **Center node uses `flex-shrink: 0`** — never collapses under pressure + +### Adapting for 5+ Dimensions + +For Porter's Five Forces (5 dimensions) or more: + +```html + +
...
+ + +
+
...
+
...
+
...
+
+ + +
+
...
+
...
+
+``` + +For the connector script, add cases for `bottom-left` and `bottom-right`: +```javascript +// 🚫 FORBIDDEN: using cR.left/cR.right as x1 — that draws from center CORNER, angle is ugly +// ✅ CORRECT: always use cx (center bottom midpoint) as x1 +case 'bottom-left': + x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT + x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT + break; +case 'bottom-right': + x1 = cx; y1 = cR.bottom - gRect.top; // center BOTTOM MIDPOINT + x2 = cardCx; y2 = r.top - gRect.top; // card TOP MIDPOINT + break; +``` + +### Adapting for 3 Dimensions + +Simply remove one side card from the middle row: + +```html + +
...
+ + +
+
...
+
...
+
+ + +
...
+``` + +--- + +## Layout B: 2×2 Quadrant Grid (SWOT / PEST) + +Best for: exactly 4 dimensions arranged as equal quadrants (no center node needed). + +**This layout has NO center node — the four quadrants themselves tell the story.** + +### HTML + CSS + +```html + + + + + + + +
+
SWOT 分析
+
企业战略定位评估
+ +
+
+
S
优势 Strengths
+
    +
  • 核心技术领先
  • +
  • 品牌知名度高
  • +
  • 供应链成熟
  • +
+
+ +
+
W
劣势 Weaknesses
+
    +
  • 国际化经验不足
  • +
  • 产品线单一
  • +
  • 人才储备有限
  • +
+
+ +
+
O
机会 Opportunities
+
    +
  • 新兴市场需求增长
  • +
  • 政策利好
  • +
  • 技术融合趋势
  • +
+
+ +
+
T
威胁 Threats
+
    +
  • 竞争加剧
  • +
  • 原材料价格波动
  • +
  • 法规变化风险
  • +
+
+
+
+ + +``` + +**No connectors needed** — the 2×2 grid itself communicates the four-quadrant relationship. Adding arrows would be visual noise. + +--- + +## Connector Rules + +1. **Connectors are ALWAYS drawn by script reading bounding boxes** — never hardcode x/y values in HTML/CSS +2. **Straight lines only** (horizontal or vertical) — no diagonal lines unless top/bottom cards are offset from center +3. **Dashed lines** (`stroke-dasharray: 6,4`) for conceptual relationships +4. **Solid lines** for causal/sequential relationships +5. **Arrow direction**: model chooses based on diagram semantics — pick ONE style per diagram, don't mix: + - **Outward** (`marker-end` only): center influences/drives dimensions (e.g. BSC: strategy → dimensions) + - **Inward** (`marker-start` only): dimensions report/pressure center (e.g. Porter: forces → competition) + - **Bidirectional** (`marker-start` + `marker-end`): mutual influence (e.g. feedback loops) +6. **🚫 All lines MUST originate from center EDGE MIDPOINTS** (cx or cy) — never from center corners +7. **Line color**: `#94A3B8` (gray-blue) — never use dimension-specific colors for connectors (visual chaos) + +### SVG Arrow Markers (copy-paste into defs) + +```javascript +// Include both markers; use only what the diagram needs +const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs'); +defs.innerHTML = ` + + + + + + +`; +svg.appendChild(defs); + +// Outward (center → card): +line.setAttribute('marker-end', 'url(#arrowEnd)'); + +// Inward (card → center): +line.setAttribute('marker-start', 'url(#arrowStart)'); + +// Bidirectional: +line.setAttribute('marker-start', 'url(#arrowStart)'); +line.setAttribute('marker-end', 'url(#arrowEnd)'); +``` + +--- + +## Content Rules + +1. **Each dimension card: max 6 bullet items** — more than 6 → group into sub-categories or use a separate detail table +2. **Bullet text: max 15 Chinese characters per line** — longer text wraps naturally (word-break: break-word) +3. **Center node text: max 8 Chinese characters** — keep it a short label, not a sentence +4. **Dimension title: max 10 Chinese characters** — concise category name + +--- + +## Font Size Rules + +| Element | Size | Weight | +|---------|------|--------| +| Diagram title | 22px | 700 | +| Center node | 18px | 700 | +| Dimension title | 15-16px | 700 | +| Bullet items | 13px | 400 | +| Subtitle/footnote | 14px | 400 | + +--- + +## Quality Checklist + +1. **No overlap** — all dimension cards fully visible, none clipped or covering another +2. **Connectors point to card edges** — not to random points in space +3. **Center node visually dominant** — largest, darkest, or most contrast +4. **Dimension cards visually equal** — similar size, similar padding, no one card 3× larger +5. **Colors follow scheme** — each dimension has its own color (border + title), backgrounds stay pale (white or near-white) +6. **Layout is centered** — equal margins on all sides +7. **Canvas large enough** — min 900px wide for cross layout, min 800px for 2×2 quadrant diff --git a/skills/charts/references/seaborn.md b/skills/charts/references/seaborn.md new file mode 100755 index 0000000..4a376f0 --- /dev/null +++ b/skills/charts/references/seaborn.md @@ -0,0 +1,324 @@ +# Seaborn Template Library + +> **⚠️ Before writing any code, read [`_rules.md`](references/_rules.md) — three non-negotiable rules on overlap, hierarchy, and color.** + + +Seaborn is built on top of matplotlib, specializing in **statistical visualization** — distributions, regression, categorical comparisons, etc. +Its default styles are already much better looking than matplotlib, but still need tuning to reach professional standards. + +## Environment Setup + +```python +import matplotlib +import matplotlib.pyplot as plt +import seaborn as sns +import numpy as np +import pandas as pd + +# ═══ Chinese Font Setup ═══ +# Font path: adjust for your system. Common locations: +# macOS: '/System/Library/Fonts/Supplemental/SimHei.ttf' +# Linux: '/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc' +# Custom: './fonts/SimHei.ttf' +import os +SIMHEI_PATH = os.environ.get('SIMHEI_FONT', '/System/Library/Fonts/Supplemental/SimHei.ttf') +matplotlib.font_manager.fontManager.addfont(SIMHEI_PATH) + +# Seaborn theme + custom overrides +sns.set_theme(style='whitegrid', font='SimHei', rc={ + 'axes.unicode_minus': False, + 'figure.facecolor': '#FFFFFF', + 'axes.facecolor': '#FFFFFF', + 'axes.edgecolor': '#E5E7EB', + 'axes.linewidth': 0.8, + 'axes.spines.top': False, + 'axes.spines.right': False, + 'grid.color': '#F3F4F6', + 'grid.alpha': 0.5, + 'grid.linewidth': 0.5, + 'xtick.major.size': 0, + 'ytick.major.size': 0, + 'axes.titlesize': 16, + 'axes.titleweight': 'bold', + 'axes.titlepad': 16, + 'legend.frameon': False, + 'figure.dpi': 200, + 'savefig.dpi': 200, + 'savefig.bbox': 'tight', +}) + +# Colors (consistent with matplotlib.md) +COOL = ['#3B82F6', '#06B6D4', '#8B5CF6', '#F59E0B', '#EF4444', '#10B981'] +CB_SAFE = ['#0077BB', '#33BBEE', '#009988', '#EE7733', '#CC3311', '#EE3377'] +``` + +## Seaborn Palette Setup + +```python +# Method 1: use hex list directly +sns.set_palette(COOL) + +# Method 2: register custom palette +from matplotlib.colors import ListedColormap +bc_palette = ListedColormap(COOL, name='charts') +``` + +--- + +## Template 1: Distribution Plot (Histogram + KDE) + +**Scenario**: View data distribution shape, detect skewness and outliers. + +```python +def dist_plot(data, title, xlabel, color='#3B82F6', save_path='dist.png'): + fig, ax = plt.subplots(figsize=(10, 6)) + + sns.histplot(data, kde=True, color=color, edgecolor='white', + linewidth=0.5, alpha=0.7, ax=ax) + + # Bold the KDE line + for line in ax.get_lines(): + line.set_linewidth(2.5) + + ax.set_title(title, loc='left') + ax.set_xlabel(xlabel) + ax.set_ylabel('Frequency') + + plt.tight_layout() + plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +--- + +## Template 2: Box Plot (Categorical Comparison) + +**Scenario**: Compare distribution characteristics across groups (median, quartiles, outliers). + +```python +def box_compare(df, x_col, y_col, title, palette=None, save_path='box.png'): + if palette is None: + palette = COOL + + fig, ax = plt.subplots(figsize=(10, 6)) + + sns.boxplot(data=df, x=x_col, y=y_col, palette=palette, + width=0.5, linewidth=1.2, fliersize=4, + boxprops=dict(edgecolor='white'), + medianprops=dict(color='white', linewidth=2), + ax=ax) + + ax.set_title(title, loc='left') + + plt.tight_layout() + plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +### Box Plot + Label Avoidance (For Complex Scenarios) + +When you need to annotate outliers, specific data points, or group names on a box plot, **label avoidance is required**: + +```python +def box_annotated(df, x_col, y_col, title, annotations=None, + palette=None, save_path='box_annotated.png'): + """ + annotations: [{'x': 0, 'y': 45, 'text': '产线A 异常'}, ...] + """ + from adjustText import adjust_text + + if palette is None: + palette = COOL + + fig, ax = plt.subplots(figsize=(12, 7)) # Slightly larger canvas, leave room for annotations + + sns.boxplot(data=df, x=x_col, y=y_col, palette=palette, + width=0.5, linewidth=1.2, fliersize=4, + boxprops=dict(edgecolor='white'), + medianprops=dict(color='white', linewidth=2), + ax=ax) + + # Annotation text — use adjustText for auto-avoidance + if annotations: + texts = [] + for ann in annotations: + t = ax.text(ann['x'], ann['y'], ann['text'], + fontsize=9, color='#374151', + bbox=dict(boxstyle='round,pad=0.3', + facecolor='#FFF7ED', edgecolor='#F59E0B', + alpha=0.9)) + texts.append(t) + + adjust_text(texts, ax=ax, + arrowprops=dict(arrowstyle='->', color='#9CA3AF', lw=0.8), + force_text=(0.8, 1.0), + force_points=(0.5, 0.8)) + + ax.set_title(title, loc='left') + + plt.tight_layout() + plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +### Box Plot + Colorbar (e.g., MTTR Analysis) + +When a box plot needs to work with a colorbar (e.g., colored by dimension), you must leave enough space for the colorbar: + +```python +def box_with_colorbar(df, x_col, y_col, color_col, title, + save_path='box_cbar.png'): + import matplotlib.gridspec as gridspec + from matplotlib.colors import Normalize + from matplotlib.cm import ScalarMappable + + fig = plt.figure(figsize=(14, 7), constrained_layout=True) + gs = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[1, 0.03], wspace=0.05) + + ax = fig.add_subplot(gs[0, 0]) + cbar_ax = fig.add_subplot(gs[0, 1]) + + sns.boxplot(data=df, x=x_col, y=y_col, ax=ax, + width=0.5, linewidth=1.2) + + # Colorbar in its own subplot, won't obscure box plot + norm = Normalize(vmin=df[color_col].min(), vmax=df[color_col].max()) + sm = ScalarMappable(norm=norm, cmap='Blues') + fig.colorbar(sm, cax=cbar_ax, label=color_col) + + ax.set_title(title, loc='left') + fig.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +--- + +## Template 3: Violin Plot + +**Scenario**: Enhanced version of box plot, showing distribution density simultaneously. + +```python +def violin_plot(df, x_col, y_col, title, palette=None, save_path='violin.png'): + if palette is None: + palette = COOL + + fig, ax = plt.subplots(figsize=(10, 6)) + + sns.violinplot(data=df, x=x_col, y=y_col, palette=palette, + inner='box', linewidth=1, ax=ax) + + ax.set_title(title, loc='left') + + plt.tight_layout() + plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +--- + +## Template 4: Regression Scatter Plot + +**Scenario**: Two-variable relationship + linear regression + confidence interval. + +```python +def reg_plot(df, x_col, y_col, title, color='#3B82F6', save_path='reg.png'): + fig, ax = plt.subplots(figsize=(10, 7)) + + sns.regplot(data=df, x=x_col, y=y_col, color=color, + scatter_kws={'s': 50, 'alpha': 0.6, 'edgecolor': 'white', 'linewidth': 1}, + line_kws={'linewidth': 2}, + ax=ax) + + ax.set_title(title, loc='left') + + plt.tight_layout() + plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +--- + +## Template 5: Correlation Heatmap + +**Scenario**: Correlation coefficient matrix among multiple variables. + +```python +def corr_heatmap(df, title, cmap='RdBu_r', save_path='corr.png'): + corr = df.corr() + + fig, ax = plt.subplots(figsize=(10, 8)) + + # Show only lower triangle + mask = np.triu(np.ones_like(corr, dtype=bool)) + + sns.heatmap(corr, mask=mask, cmap=cmap, center=0, + vmin=-1, vmax=1, annot=True, fmt='.2f', + square=True, linewidths=1, linecolor='white', + cbar_kws={'shrink': 0.8, 'label': 'Correlation'}, + annot_kws={'size': 9}, + ax=ax) + + ax.set_title(title, loc='left', pad=16) + + plt.tight_layout() + plt.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +--- + +## Template 6: Pair Plot + +**Scenario**: Overview of pairwise relationships among multiple variables (distribution on diagonal, scatter plots elsewhere). + +```python +def pair_plot(df, hue_col=None, palette=None, save_path='pair.png'): + if palette is None: + palette = COOL + + g = sns.pairplot(df, hue=hue_col, palette=palette, + diag_kind='kde', plot_kws={'alpha': 0.6, 's': 30}, + height=2.5, aspect=1) + + g.figure.suptitle('Pairwise Variable Relationships', y=1.02, fontsize=16, fontweight='bold') + + g.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +--- + +## Template 7: Facet Grid (FacetGrid) + +**Scenario**: Split into multiple subplots by categorical variable for comparison. + +```python +def facet_hist(df, col_var, value_col, title, color='#3B82F6', save_path='facet.png'): + g = sns.FacetGrid(df, col=col_var, col_wrap=3, height=3.5, aspect=1.3) + g.map(sns.histplot, value_col, color=color, edgecolor='white', + linewidth=0.5, alpha=0.7, kde=True) + + g.set_titles('{col_name}', fontsize=12, fontweight='bold') + g.figure.suptitle(title, y=1.02, fontsize=16, fontweight='bold') + + g.savefig(save_path, dpi=200, facecolor='white', bbox_inches='tight') + plt.close() +``` + +--- + +## Seaborn vs matplotlib Selection Guide + +| What to Plot | Use Seaborn | Use matplotlib | +|---------|-----------|--------------| +| Histogram/KDE distribution | ✅ `histplot` / `kdeplot` | Works, but more code | +| Box plot/Violin plot | ✅ One-liner | Works, but rougher style | +| Regression scatter + confidence interval | ✅ `regplot` auto-calculates | Manual fitting + plotting | +| Correlation heatmap | ✅ `heatmap` + mask | Manual `imshow` tedious | +| Pairwise relationship matrix | ✅ `pairplot` unique | No equivalent | +| Facet grid | ✅ `FacetGrid` | `plt.subplots` manual loop | +| Regular bar chart | Less flexible than matplotlib | ✅ More control | +| Line trend chart | Less than matplotlib | ✅ More control | +| Custom annotations/arrows | Not suitable | ✅ `ax.annotate` | + +**Principle: Use Seaborn for statistical charts, matplotlib for customized charts.** diff --git a/skills/charts/setup.sh b/skills/charts/setup.sh new file mode 100755 index 0000000..03c3757 --- /dev/null +++ b/skills/charts/setup.sh @@ -0,0 +1,228 @@ +#!/usr/bin/env bash +# --- +# name: charts-setup +# author: Z.AI +# version: "1.0" +# description: Environment setup for the Charts skill. Checks and installs all required dependencies. +# --- +# +# Installs only dependencies required by the Charts skill. +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +ok() { echo -e " ${GREEN}✓${NC} $1"; } +fail() { echo -e " ${RED}✗${NC} $1"; } +warn() { echo -e " ${YELLOW}○${NC} $1"; } +info() { echo -e " ${BLUE}→${NC} $1"; } + +echo "============================================" +echo " Charts Skill — Environment Setup" +echo "============================================" +echo "" + +OS="$(uname -s)" +ARCH="$(uname -m)" +echo "Platform: $OS $ARCH" +echo "" + +# ── 0. macOS: Homebrew ── +if [ "$OS" = "Darwin" ]; then + echo "--- Homebrew (macOS package manager) ---" + if command -v brew &>/dev/null; then + BREW_VER=$(brew --version 2>/dev/null | head -1) + ok "brew ($BREW_VER)" + else + fail "brew not found — most dependencies below need Homebrew on macOS" + info "Install: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" + fi + echo "" +fi + +# ── 1. Python 3 ── +echo "--- Python ---" +if command -v python3 &>/dev/null; then + PY_VER=$(python3 --version 2>&1) + ok "python3 ($PY_VER)" + if [ "$OS" = "Darwin" ]; then + PY_PATH=$(which python3 2>/dev/null) + if [[ "$PY_PATH" == "/usr/bin/python3" ]]; then + warn "Using macOS system Python (limited). Recommend: brew install python3" + fi + fi +else + fail "python3 not found" + case "$OS" in + Darwin) info "Install: brew install python3" ;; + Linux) info "Install: sudo apt install python3 python3-pip (Debian/Ubuntu)" + info " sudo dnf install python3 python3-pip (Fedora/RHEL)" ;; + *) info "Install: https://www.python.org/downloads/" ;; + esac +fi + +# ── 2. pip ── +echo "" +echo "--- pip ---" +if python3 -m pip --version &>/dev/null 2>&1; then + PIP_VER=$(python3 -m pip --version 2>/dev/null | head -1) + ok "pip ($PIP_VER)" +else + fail "pip not found" + case "$OS" in + Darwin) info "Install: python3 -m ensurepip --upgrade" + info " or: brew install python3 (includes pip)" ;; + Linux) info "Install: sudo apt install python3-pip (Debian/Ubuntu)" ;; + *) info "Install: python3 -m ensurepip --upgrade" ;; + esac +fi + +# ── 3. Python packages (matplotlib / seaborn for data charts) ── +echo "" +echo "--- Python Packages (Data Charts) ---" +PY_PKGS=( + "matplotlib:matplotlib" + "seaborn:seaborn" + "numpy:numpy" + "adjustText:adjustText" +) + +MISSING_PY=() +for entry in "${PY_PKGS[@]}"; do + mod="${entry%%:*}" + pkg="${entry##*:}" + if python3 -c "import $mod" 2>/dev/null; then + ver=$(python3 -c "import $mod; print(getattr($mod, '__version__', 'installed'))" 2>/dev/null) + ok "$pkg ($ver)" + else + fail "$pkg not installed" + MISSING_PY+=("$pkg") + fi +done + +if [ ${#MISSING_PY[@]} -gt 0 ]; then + echo "" + if [ -t 0 ]; then + read -p " Install missing Python packages? [Y/n] " -n 1 -r REPLY + echo "" + REPLY=${REPLY:-Y} + else + warn "Non-interactive mode — skipping auto-install. Run interactively or install manually." + REPLY=N + fi + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + python3 -m pip install -q "${MISSING_PY[@]}" 2>/dev/null \ + || python3 -m pip install -q --user "${MISSING_PY[@]}" 2>/dev/null \ + || python3 -m pip install -q --break-system-packages "${MISSING_PY[@]}" 2>/dev/null \ + || { fail "pip install failed. Try manually: pip install ${MISSING_PY[*]}"; } + ok "Installed: ${MISSING_PY[*]}" + fi +fi + +# ── 4. Node.js ── +echo "" +echo "--- Node.js (Interactive Charts & Diagrams) ---" +if command -v node &>/dev/null; then + NODE_VER=$(node --version) + ok "node ($NODE_VER)" +else + fail "node not found" + case "$OS" in + Darwin) info "Install: brew install node" ;; + Linux) info "Install: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -" + info " sudo apt install -y nodejs" ;; + *) info "Install: https://nodejs.org/" ;; + esac +fi + +# ── 5. npm ── +echo "" +echo "--- npm ---" +if command -v npm &>/dev/null; then + NPM_VER=$(npm --version 2>/dev/null) + ok "npm ($NPM_VER)" +else + fail "npm not found" + case "$OS" in + Darwin) info "Install: brew install node (includes npm)" ;; + Linux) info "Install: comes with nodejs" ;; + *) info "Install: https://nodejs.org/" ;; + esac +fi + +# ── 6. Playwright + Chromium (for HTML→PNG/PDF rendering) ── +echo "" +echo "--- Playwright (Structural Diagrams & HTML Charts) ---" +if node -e "require('playwright')" 2>/dev/null; then + PW_VER=$(node -e "console.log(require('playwright/package.json').version)" 2>/dev/null) + ok "playwright ($PW_VER)" +else + fail "playwright not installed" + info "Install: npm install -g playwright" +fi + +if [ "$OS" = "Darwin" ]; then + PW_CACHE="$HOME/Library/Caches/ms-playwright" +else + PW_CACHE="$HOME/.cache/ms-playwright" +fi +if ls "$PW_CACHE"/chromium-* &>/dev/null 2>&1; then + CR_DIR=$(ls -d "$PW_CACHE"/chromium-* 2>/dev/null | tail -1) + ok "chromium ($(basename "$CR_DIR"))" +else + fail "chromium not installed" + info "Install: npx playwright install chromium" + if [ "$OS" = "Linux" ]; then + info " npx playwright install-deps (system libs, needs sudo)" + fi +fi + +# ── 7. CJK Fonts (for Chinese chart labels) ── +echo "" +echo "--- CJK Fonts (Chinese text in charts) ---" +CJK_FOUND=false + +# Check matplotlib registered fonts +if python3 -c " +import matplotlib.font_manager as fm +fonts = [f.name for f in fm.fontManager.ttflist] +if 'SimHei' in fonts or 'Heiti SC' in fonts or 'Noto Sans CJK' in fonts or 'PingFang SC' in fonts: + print('ok') +else: + print('missing') +" 2>/dev/null | grep -q "ok"; then + ok "CJK font registered in matplotlib (SimHei/Heiti SC/Noto Sans CJK/PingFang SC)" + CJK_FOUND=true +fi + +# Check system CJK fonts +if [ "$OS" = "Darwin" ]; then + if ls /System/Library/Fonts/PingFang.ttc &>/dev/null 2>&1 \ + || ls /System/Library/Fonts/STHeiti*.ttc &>/dev/null 2>&1; then + ok "macOS CJK system fonts available (PingFang/STHeiti)" + CJK_FOUND=true + fi +elif [ "$OS" = "Linux" ]; then + if fc-list :lang=zh 2>/dev/null | head -1 | grep -q .; then + ok "system CJK fonts available (fc-list)" + CJK_FOUND=true + fi +fi + +if [ "$CJK_FOUND" = false ]; then + warn "No CJK font detected — Chinese labels may show as □" + info "The skill configures font fallback via rcParams at runtime." + info "Ensure a CJK font file exists (e.g., SimHei.ttf) and is registered." + if [ "$OS" = "Darwin" ]; then + info "macOS ships with PingFang — try: plt.rcParams['font.sans-serif'] = ['PingFang SC', 'DejaVu Sans']" + elif [ "$OS" = "Linux" ]; then + info "Install: sudo apt install fonts-noto-cjk" + fi +fi + +# ── Summary ── +echo "" +echo "============================================" +echo " Setup complete." +echo " Data charts: matplotlib + seaborn" +echo " Structural diagrams: Playwright + CSS" +echo " Interactive charts: ECharts / D3.js via Node.js" +echo "============================================" diff --git a/skills/cheat-sheet/SKILL.md b/skills/cheat-sheet/SKILL.md new file mode 100755 index 0000000..2686302 --- /dev/null +++ b/skills/cheat-sheet/SKILL.md @@ -0,0 +1,210 @@ +--- +name: cheat-sheet +description: 将 PDF/Word/Markdown 学习资料转化为精炼的知识浓缩卡文档。支持三种风格(知识点速查卡/思维导图式/Q&A式),输出双栏小字 PDF。当用户说"生成知识浓缩卡"、"生成 Cheatsheet"、"帮我做个速查表"、"把这个资料整理成一页纸"、"做个知识卡片"时触发。**不处理**:基于材料出题(→ quiz-mastery)、长期学习项目(→ study-buddy)。 +--- + +# Cheatsheet 生成器 + +## 你做什么 + +把用户的学习资料(PDF/Word/Markdown/文本)压缩成一份**双栏、小字、信息密度高**的 Cheatsheet 文档(PDF 格式)。 + +**核心原则:只保留干货,砍掉废话。** + +--- + +## ⚠️ 铁律:避免重复创建 + +生成 Cheatsheet 前,**先扫描输出目录**检查是否已有同主题文件: +- **已存在** → 问用户"已经有一份 [文件名] 了,覆盖 / 新建一份 / 跳过?" 等用户回复再动手 +- **不存在** → 直接生成 +- **绝不**未经询问直接覆盖 + +--- + +## 工作流程 + +### 步骤 1:接收资料 + +用户提供学习资料文件,支持格式: +- `.pdf` — 调 PDF skill 的 process 路线提取文本 +- `.docx` — 调 PDF skill 的 process 路线提取文本 +- `.md` / `.txt` — 直接读取 + +**提取文本命令:** +```bash +# PDF 提取文本 +python3 "$PDF_SCRIPTS/pdf.py" extract.text + +# Word 转 PDF 后提取(如需要) +python3 "$PDF_SCRIPTS/pdf.py" convert.office +``` + +其中 `$PDF_SCRIPTS` 为 PDF skill 的 scripts 目录路径,需从 PDF skill 的 SKILL.md 位置推导。 + +如果用户没有提供文件而是直接贴了文本内容,跳过提取步骤,直接用文本。 + +--- + +### 步骤 2:选择风格 + +**必须询问用户**想要哪种风格(不要自己替用户选): + +向用户展示以下选项: +1. 📋 **知识点速查卡** — 核心概念 + 定义 + 关键公式/要点,一眼扫到,适合考前突击 +2. 🌳 **思维导图式** — 按层级结构组织(大标题→子标题→要点),有大纲感,适合梳理体系 +3. ❓ **Q&A 式** — 把知识点变成"问题→答案"对,适合自测复习 + +用户选择后进入步骤 3。 + +--- + +### 步骤 3:LLM 提炼内容 + +根据用户选择的风格,构建不同的 prompt 让 LLM 从原文中提炼 cheatsheet 内容。 + +#### 风格 1:知识点速查卡 + +提炼规则: +- 提取所有核心概念、定义、公式、关键数据 +- 每个知识点用 **术语:一句话解释** 的格式 +- 相关知识点分组,每组有小标题 +- 重要公式/代码片段原样保留 +- 砍掉所有举例、过渡句、背景铺垫 + +输出结构: +``` +## [分组标题] +- **术语A**:一句话定义 +- **术语B**:一句话定义 +- 📐 公式:`公式内容` + +## [分组标题] +... +``` + +#### 风格 2:思维导图式 + +提炼规则: +- 提取文档的层级结构(章→节→要点) +- 每个节点用最简短的语言概括 +- 最多 3 级深度(再深就塞不进一页了) +- 用缩进和符号表达层级关系 + +输出结构: +``` +# 主题 + +## 一级分支 + ├─ 二级要点 + │ ├─ 细节 1 + │ └─ 细节 2 + └─ 二级要点 + └─ 细节 +``` + +#### 风格 3:Q&A 式 + +提炼规则: +- 把每个知识点转化成一个问题 +- 答案控制在 1-3 句话 +- 问题从基础到进阶排列 +- 易混淆的概念出辨析题 + +输出结构: +``` +## [主题分组] + +**Q:什么是 XXX?** +A:一句话回答。 + +**Q:XXX 和 YYY 的区别?** +A:简短对比。 +``` + +--- + +### 步骤 4:用户确认与调整 + +LLM 生成内容后,**先以文本形式展示给用户**,询问: + +> "内容整理好了,你看看有没有要调整的?比如: +> - 某些部分要加重点标记? +> - 某些内容要删掉或补充? +> - 排版上有什么偏好?(比如字号再小一点、分区颜色区分等)" + +用户确认"可以"后,进入步骤 5。 +用户提出修改 → 调整内容 → 再次展示 → 等待确认。 + +--- + +### 步骤 5:生成 PDF + +调用 PDF skill 的 **Report 路线(ReportLab)** 生成双栏 PDF。 + +**排版规格(默认值):** + +| 参数 | 默认值 | 说明 | +|------|--------|------| +| 页面大小 | A4 | 可按用户要求调整 | +| 栏数 | 双栏 | 默认双栏,用户可选单栏 | +| 正文字号 | 8pt | 信息密度优先,可按用户要求调整 | +| 标题字号 | 10pt(一级)/ 9pt(二级) | | +| 行距 | 1.2 | 紧凑但可读 | +| 页边距 | 上下左右各 12mm | 最大化内容区域 | +| 字体 | 中文用 UniSong/UniHei,英文用 Helvetica | | + +**生成流程:** + +1. 将 LLM 提炼的 Markdown 内容转换为 ReportLab 排版指令 +2. 调 PDF skill 的 report 路线生成 PDF +3. 生成文件路径告知用户 + +**调用 PDF skill 时遵循其 SKILL.md 中的所有规则**,包括: +- CJK 字体检查 +- 表格溢出防护 +- 页面填充率检查 +- 元数据设置 + +--- + +### 步骤 6:交付 + +输出给用户: +- 📄 PDF 文件路径 +- 文件大小、页数 +- 提示用户可以继续调整 + +--- + +## 注意事项 + +### 内容质量 +- **不编造内容**:所有知识点必须来自原文,不能自由发挥 +- **不遗漏关键内容**:核心概念、公式、定义必须保留 +- **术语保持原文用词**:不要擅自替换专业术语 + +### 文件操作 +- 生成的 PDF 默认保存到工作区根目录,文件名格式:`知识浓缩卡_[主题]_[日期].pdf` +- 查重见上方"⚠️ 铁律:避免重复创建" + +--- + +## 与其他 Skill 的关系 + +| Skill | 关系 | +|-------|------| +| PDF skill | 调用其 process 路线提取文本,调用其 report 路线生成 PDF | +| study-buddy | study-buddy 可在用户完成学习项目后推荐生成 cheatsheet | +| quiz-mastery | 无直接关系,但 cheatsheet 内容可作为出题的知识点来源 | + +--- + +## 文件结构 + +``` +skills/cheat-sheet/ +├── SKILL.md ← 当前文件 +``` + +本 skill 是纯流程指引,不包含独立脚本。所有文件操作和 PDF 生成通过调用 PDF skill 完成。 diff --git a/skills/coding-agent/SKILL.md b/skills/coding-agent/SKILL.md new file mode 100755 index 0000000..e3424f2 --- /dev/null +++ b/skills/coding-agent/SKILL.md @@ -0,0 +1,120 @@ +--- +name: coding-agent +slug: code +version: 1.0.4 +homepage: https://clawic.com/skills/code +description: Coding workflow with planning, implementation, verification, and testing for clean software development. +changelog: Improved description for better discoverability +metadata: {"clawdbot":{"emoji":"💻","requires":{"bins":[]},"os":["linux","darwin","win32"]}} +--- + +## When to Use + +User explicitly requests code implementation. Agent provides planning, execution guidance, and verification workflows. + +## Architecture + +User preferences stored in `~/code/` when user explicitly requests. + +``` +~/code/ + - memory.md # User-provided preferences only +``` + +Create on first use: `mkdir -p ~/code` + +## Quick Reference + +| Topic | File | +|-------|------| +| Memory setup | `memory-template.md` | +| Task breakdown | `planning.md` | +| Execution flow | `execution.md` | +| Verification | `verification.md` | +| Multi-task state | `state.md` | +| User criteria | `criteria.md` | + +## Scope + +This skill ONLY: +- Provides coding workflow guidance +- Stores preferences user explicitly provides in `~/code/` +- Reads included reference files + +This skill NEVER: +- Executes code automatically +- Makes network requests +- Accesses files outside `~/code/` and the user's project +- Modifies its own SKILL.md or auxiliary files +- Takes autonomous action without user awareness + +## Core Rules + +### 1. Check Memory First +Read `~/code/memory.md` for user's stated preferences if it exists. + +### 2. User Controls Execution +- This skill provides GUIDANCE, not autonomous execution +- User decides when to proceed to next step +- Sub-agent delegation requires user's explicit request + +### 3. Plan Before Code +- Break requests into testable steps +- Each step independently verifiable +- See `planning.md` for patterns + +### 4. Verify Everything +| After | Do | +|-------|-----| +| Each function | Suggest running tests | +| UI changes | Suggest taking screenshot | +| Before delivery | Suggest full test suite | + +### 5. Store Preferences on Request +| User says | Action | +|-----------|--------| +| "Remember I prefer X" | Add to memory.md | +| "Never do Y again" | Add to memory.md Never section | + +Only store what user explicitly asks to save. + +## Workflow + +``` +Request -> Plan -> Execute -> Verify -> Deliver +``` + +## Common Traps + +- **Delivering untested code** -> always verify first +- **Huge PRs** -> break into testable chunks +- **Ignoring preferences** -> check memory.md first + +## Self-Modification + +This skill NEVER modifies its own SKILL.md or auxiliary files. +User data stored only in `~/code/memory.md` after explicit request. + +## External Endpoints + +This skill makes NO network requests. + +| Endpoint | Data Sent | Purpose | +|----------|-----------|---------| +| None | None | N/A | + +## Security & Privacy + +**Data that stays local:** +- Only preferences user explicitly asks to save +- Stored in `~/code/memory.md` + +**Data that leaves your machine:** +- None. This skill makes no network requests. + +**This skill does NOT:** +- Execute code automatically +- Access network or external services +- Access files outside `~/code/` and user's project +- Take autonomous actions without user awareness +- Delegate to sub-agents without user's explicit request diff --git a/skills/coding-agent/_meta.json b/skills/coding-agent/_meta.json new file mode 100755 index 0000000..904f1d8 --- /dev/null +++ b/skills/coding-agent/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn73vp5rarc3b14rc7wjcw8f8580t5d1", + "slug": "code", + "version": "1.0.4", + "publishedAt": 1771467169291 +} \ No newline at end of file diff --git a/skills/coding-agent/criteria.md b/skills/coding-agent/criteria.md new file mode 100755 index 0000000..a979f0b --- /dev/null +++ b/skills/coding-agent/criteria.md @@ -0,0 +1,48 @@ +# Criteria for Storing Preferences + +Reference for when to save user preferences to `~/code/memory.md`. + +## When to Save (User Must Request) + +Save only when user explicitly asks: +- "Remember that I prefer X" +- "Always do Y from now on" +- "Save this preference" +- "Don't forget that I like Z" + +## When NOT to Save + +- User didn't explicitly ask to save +- Project-specific requirement (applies to this project only) +- One-off request ("just this once") +- Temporary preference + +## What to Save + +**Preferences:** +- Coding style preferences user stated +- Tools or frameworks user prefers +- Patterns user explicitly likes + +**Things to avoid:** +- Approaches user explicitly dislikes +- Patterns user asked not to repeat + +## Format in memory.md + +```markdown +## Preferences +- prefers TypeScript over JavaScript +- likes detailed comments +- wants tests for all functions + +## Never +- no class-based React components +- avoid inline styles +``` + +## Important + +- Only save what user EXPLICITLY asked to save +- Ask user before saving: "Should I remember this preference?" +- Never modify any skill files, only `~/code/memory.md` diff --git a/skills/coding-agent/execution.md b/skills/coding-agent/execution.md new file mode 100755 index 0000000..90bb149 --- /dev/null +++ b/skills/coding-agent/execution.md @@ -0,0 +1,42 @@ +# Execution Guidance + +Reference for executing multi-step implementations. + +## Recommended Flow + +When user approves a step: +1. Execute that step +2. Verify it works +3. Report completion to user +4. Wait for user to approve next step + +## Progress Tracking + +Show user the current state: +``` +- [DONE] Step 1 (completed) +- [WIP] Step 2 <- awaiting user approval +- [ ] Step 3 +- [ ] Step 4 +``` + +## When to Pause and Ask User + +- Before starting any new step +- When encountering an error +- When a decision is needed (A vs B) +- When credentials or permissions are needed + +## Error Handling + +If an error occurs: +1. Report the error to user +2. Suggest possible fixes +3. Wait for user decision on how to proceed + +## Patterns to Follow + +- Report completion of each step +- Ask before proceeding to next step +- Let user decide retry strategy +- Keep user informed of progress diff --git a/skills/coding-agent/memory-template.md b/skills/coding-agent/memory-template.md new file mode 100755 index 0000000..198b220 --- /dev/null +++ b/skills/coding-agent/memory-template.md @@ -0,0 +1,38 @@ +# Memory Setup - Code + +## Initial Setup + +Create directory on first use: +```bash +mkdir -p ~/code +touch ~/code/memory.md +``` + +## memory.md Template + +Copy to `~/code/memory.md`: + +```markdown +# Code Memory + +## Preferences + + + +## Never + + + +## Patterns + + + +--- +Last updated: YYYY-MM-DD +``` + +## Notes + +- Check `criteria.md` for additional user-specific criteria +- Use `planning.md` for breaking down complex requests +- Verify with tests and screenshots per `verification.md` diff --git a/skills/coding-agent/planning.md b/skills/coding-agent/planning.md new file mode 100755 index 0000000..572d543 --- /dev/null +++ b/skills/coding-agent/planning.md @@ -0,0 +1,31 @@ +# Planning Reference + +Consult when breaking down a multi-step request. + +## When to Plan +- Multiple files or components +- Dependencies between parts +- UI that needs visual verification +- User says "build", "create", "implement" + +## Step Format +``` +Step N: [What] +- Output: [What exists after] +- Test: [How to verify] +``` + +## Good Steps +- Clear output (file, endpoint, screen) +- Testable independently +- No ambiguity in what "done" means + +## Bad Steps +- "Implement the thing" (vague output) +- No test defined +- Depends on undefined prior step + +## Don't Plan +- One-liner functions +- Simple modifications +- Questions about existing code diff --git a/skills/coding-agent/state.md b/skills/coding-agent/state.md new file mode 100755 index 0000000..ca3a53a --- /dev/null +++ b/skills/coding-agent/state.md @@ -0,0 +1,60 @@ +# State Tracking Guidance + +Reference for tracking multiple tasks or requests. + +## Request Tracking + +Label each user request: +``` +[R1] Build login page +[R2] Add dark mode +[R3] Fix header alignment +``` + +Track state for user visibility: +``` +[R1] [DONE] Done +[R2] [WIP] In progress (awaiting user approval for step 2) +[R3] [Q] Queued +``` + +## Managing Multiple Requests + +When user sends a new request while another is in progress: + +1. Acknowledge: "Got it, I'll add this to the queue" +2. Show updated queue to user +3. Ask user if priority should change + +## Handling Interruptions + +| Situation | Suggested Action | +|-----------|------------------| +| New unrelated request | Add to queue, ask user priority | +| Request affects current work | Pause, explain impact, ask user how to proceed | +| User says "stop" or "wait" | Stop immediately, await instructions | +| User changes requirements | Summarize impact, ask user to confirm changes | + +## User Decisions + +Always ask user before: +- Starting work on queued items +- Changing priority order +- Rolling back completed work +- Modifying the plan + +## Progress File (Optional) + +User may request a state file: +```markdown +## In Progress +[R2] Dark mode - Step 2/4 (awaiting user approval) + +## Queued +[R3] Header fix + +## Done +[R1] Login page [DONE] +``` + +Update only when user requests or approves changes. diff --git a/skills/coding-agent/verification.md b/skills/coding-agent/verification.md new file mode 100755 index 0000000..d4d0493 --- /dev/null +++ b/skills/coding-agent/verification.md @@ -0,0 +1,39 @@ +# Verification Reference + +Consult when verifying implementations visually or with tests. + +## Screenshots +- Wait for full page load (no spinners) +- Review yourself before sending +- Split long pages into 3-5 sections (~800px each) +- Caption each: "Hero", "Features", "Footer" + +## Before Sending +``` +[ ] Content loaded +[ ] Shows the specific change +[ ] No visual bugs +[ ] Caption explains what user sees +``` + +## Fix-Before-Send +If screenshot shows problem: +1. Fix code +2. Re-deploy +3. New screenshot +4. Still broken? -> back to 1 +5. Fixed? -> now send + +Never send "I noticed X is wrong, will fix" - fix first. + +## No UI? Show Output + +When verifying API endpoints, show actual output: +``` +GET /api/users -> {"id": 1, "name": "test"} +``` + +Include actual response, not just "it works". + +## Flows +Number sequential states: "1/4: Form", "2/4: Loading", "3/4: Error", "4/4: Success" diff --git a/skills/content-strategy/SKILL.md b/skills/content-strategy/SKILL.md new file mode 100755 index 0000000..5e51422 --- /dev/null +++ b/skills/content-strategy/SKILL.md @@ -0,0 +1,181 @@ +--- +name: content-strategy +description: Build and execute a content marketing strategy for a solopreneur business. Use when planning what content to create, deciding on content formats and channels, building a content calendar, measuring content performance, or systematizing content production. Covers audience research for content, content pillars, distribution strategy, repurposing workflows, and metrics. Trigger on "content strategy", "content marketing", "what content should I create", "content plan", "content calendar", "content ideas", "content distribution", "grow through content". +--- + +# Content Strategy + +## Overview +Content marketing is how solopreneurs build authority, attract customers, and grow without paid ads. But random content doesn't work — you need a strategy. This playbook builds a repeatable system for creating content that actually drives business results, not just likes. + +--- + +## Step 1: Define Your Content Goals + +Content without a goal is just noise. Before you create anything, answer: what is this content supposed to DO? + +**Common solopreneur content goals:** +- **Generate awareness** (new people discover you exist) +- **Build trust** (people see you as credible and knowledgeable) +- **Drive leads** (people give you their email or book a call) +- **Enable sales** (content answers objections and shortens sales cycles) +- **Retain customers** (existing customers stay engaged and see ongoing value) + +**Rule:** Pick ONE primary goal per piece of content. You can have secondary benefits, but clarity on the main goal determines format, channel, and CTA. + +Example: A tutorial blog post might have the primary goal of "generate awareness" (via SEO) and a secondary goal of "drive leads" (with an email signup CTA at the end). + +--- + +## Step 2: Research Your Audience's Content Needs + +Great content solves a specific problem for a specific person. Bad content talks about what YOU want to talk about. + +**Research workflow (spend 2-3 hours on this before creating anything):** + +1. **Mine customer conversations.** Go through support tickets, sales calls, discovery calls. What questions do prospects and customers ask repeatedly? Those are your content topics. + +2. **Check competitor content.** What are the top 3-5 players in your space publishing? Look for gaps — topics they're NOT covering or covering poorly. + +3. **Keyword research (if doing SEO).** Use free tools (Google autocomplete, AnswerThePublic, or "People Also Ask" in Google results) to see what people are actually searching for related to your niche. + +4. **Community mining.** Go to Reddit, Slack communities, Facebook groups, or forums in your space. What questions get asked over and over? Those are high-value topics. + +**Output:** A list of 20-30 content ideas ranked by: (a) relevance to your ICP, (b) search volume or community demand, (c) your unique perspective or experience on the topic. + +--- + +## Step 3: Build Content Pillars + +Content pillars are 3-5 broad topic areas that all your content falls under. They keep you focused and prevent random one-off content that doesn't build momentum. + +**How to define pillars:** +- Each pillar should map to a core problem your product/service solves or a key interest area of your ICP. +- Pillars should be broad enough to generate dozens of pieces of content but specific enough to be relevant. +- Aim for 3-5 pillars max. More than that dilutes focus. + +**Example (for an n8n automation consultant):** +``` +Pillar 1: Workflow Automation Fundamentals +Pillar 2: No-Code Tool Comparisons +Pillar 3: Business Process Optimization +Pillar 4: Real Client Case Studies +``` + +Every piece of content you create should fit under one of these pillars. If it doesn't, don't create it. + +--- + +## Step 4: Choose Your Content Formats and Channels + +Solopreneurs can't do everything. Pick 1-2 primary formats and 1-2 primary channels. Go deep, not wide. + +**Content formats:** +| Format | Best For | Time Investment | Longevity | +|---|---|---|---| +| **Blog posts** | SEO, teaching, depth | 2-4 hrs/post | High (evergreen) | +| **Videos (YouTube)** | Visual topics, personality-driven brands | 3-6 hrs/video | High (evergreen) | +| **Podcasts** | Thought leadership, interviews | 2-3 hrs/episode | Medium | +| **Twitter/X threads** | Quick insights, community building | 30 min/thread | Low (24-48hr shelf life) | +| **LinkedIn posts** | B2B, professional content | 30-60 min/post | Low-medium | +| **Email newsletters** | Relationship building, owned audience | 1-2 hrs/newsletter | Medium (subscribers keep it) | +| **Short-form video (TikTok, Reels)** | Viral potential, younger demos | 1-2 hrs/video | Low (algorithmic churn) | + +**Selection criteria:** +- Where does your ICP hang out? (B2B = LinkedIn. Developers = Twitter. Visual products = Instagram.) +- What format do you NOT hate creating? (If you hate being on camera, don't pick YouTube.) +- What has the best ROI for your goals? (Lead gen = blog + email. Brand building = Twitter + LinkedIn.) + +**Recommended solopreneur starting stack:** +- **Primary format:** Blog posts or long-form LinkedIn posts (depending on B2B vs B2C) +- **Secondary format:** Email newsletter (this is your owned channel — never skip this) + +--- + +## Step 5: Build a Content Calendar + +A content calendar prevents the "what should I post today?" panic. Plan 2-4 weeks ahead. + +**Calendar structure:** +``` +DATE | PILLAR | TOPIC | FORMAT | CHANNEL | CTA | STATUS +``` + +**Example:** +``` +Feb 10 | Automation | "5 n8n workflows every SaaS founder needs" | Blog | Website + LinkedIn | Email signup | Draft +Feb 13 | Case Study | "How we saved Client X 20hrs/week" | LinkedIn post | LinkedIn | Book a call | Scheduled +Feb 17 | Tool Comparison | "Zapier vs n8n: Which is right for you?" | Blog | Website + Twitter | Free guide download | Outline +``` + +**Cadence recommendations:** +- Blog: 1-2x/week (minimum 2x/month to maintain SEO momentum) +- Newsletter: 1x/week or biweekly (consistency matters more than frequency) +- Social (LinkedIn/Twitter): 3-5x/week + +**Rule:** Batch creation. Write 4 posts in one sitting rather than 1 post four different days. Batching is 3x faster and produces better quality. + +--- + +## Step 6: Distribution and Amplification + +Creating content is 30% of the work. Distribution is the other 70%. + +**Distribution checklist for every piece:** +- [ ] Publish on primary channel (blog, YouTube, etc.) +- [ ] Share on 2-3 social channels with unique captions per platform (don't just copy-paste the same message) +- [ ] Send to email list (if it's a high-value piece) +- [ ] Post in 1-2 relevant communities (but add value to the discussion, don't just drop links) +- [ ] DM it to 3-5 people who you think would find it genuinely useful +- [ ] Repurpose into 2-3 other formats (see next step) + +**Timing:** Publish early in the week (Tuesday-Thursday) for best engagement. Avoid Fridays and weekends unless your audience is specifically active then. + +--- + +## Step 7: Repurpose Everything + +One piece of long-form content can become 5-10 smaller pieces. This is how solopreneurs produce high volume without burning out. + +**Repurposing workflow (example: one blog post):** +1. Original: 1,500-word blog post +2. Repurpose into: LinkedIn post (first 3 paragraphs + a hook) +3. Repurpose into: Twitter thread (key points broken into 8-10 tweets) +4. Repurpose into: Email newsletter (add a personal intro, link to full post) +5. Repurpose into: Carousel post (main points as slides on LinkedIn or Instagram) +6. Repurpose into: Short video (you on camera summarizing the key takeaway in 60 seconds) + +**Rule:** Repurpose the high-performers. If a blog post gets good traffic or a LinkedIn post gets strong engagement, milk it — turn it into 5 more formats. + +--- + +## Step 8: Measure What Matters + +Track content performance so you can double down on what works and stop doing what doesn't. + +**Metrics by goal:** + +| Goal | Metrics to Track | +|---|---| +| Awareness | Impressions, reach, new visitors, social followers | +| Trust | Engagement rate (comments, shares), time on page, repeat visitors | +| Lead generation | Email signups, CTA clicks, lead magnet downloads | +| Sales enablement | Content assists (how many deals involved this content?), proposal open rates (if content is attached) | + +**Dashboard (monthly check-in):** +- Top 5 performing pieces (by traffic or engagement) +- Traffic source breakdown (organic, social, direct, referral) +- Conversion rate (visitors → email signups or leads) +- Time investment vs results (which content type has the best ROI?) + +**Iteration rule:** Every month, identify the top-performing content type and topic. Do 2x more of that next month. Identify the worst performer. Stop doing that format or adjust the approach. + +--- + +## Content Strategy Mistakes to Avoid +- Creating content without a goal. Every piece should have a purpose tied to a business outcome. +- Not researching what your audience actually wants. Your assumptions are often wrong — validate with real data. +- Trying to be on every platform. Pick 1-2 and dominate them before expanding. +- Publishing inconsistently. One post a month doesn't build momentum. Consistency compounds. +- Not repurposing. Creating 10 original pieces is 5x harder than creating 2 original pieces and repurposing them into 8 more. +- Ignoring metrics. If you don't measure, you can't improve. Check your numbers monthly at minimum. diff --git a/skills/content-strategy/_meta.json b/skills/content-strategy/_meta.json new file mode 100755 index 0000000..f44ca0a --- /dev/null +++ b/skills/content-strategy/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn732qfbv22he1jqm63xbwq6e980kn8s", + "slug": "content-strategy", + "version": "0.1.0", + "publishedAt": 1770341804646 +} \ No newline at end of file diff --git a/skills/contentanalysis/ExtractWisdom/SKILL.md b/skills/contentanalysis/ExtractWisdom/SKILL.md new file mode 100755 index 0000000..3b7c80d --- /dev/null +++ b/skills/contentanalysis/ExtractWisdom/SKILL.md @@ -0,0 +1,229 @@ +--- +name: ExtractWisdom +description: Content-adaptive wisdom extraction — detects what domains exist in content and builds custom sections (not static IDEAS/QUOTES). Produces tailored insight reports from videos, podcasts, articles. USE WHEN extract wisdom, analyze video, analyze podcast, extract insights, what's interesting, extract from YouTube, what did I miss, key takeaways. +--- + +## Customization + +**Before executing, check for user customizations at:** +`~/.claude/PAI/USER/SKILLCUSTOMIZATIONS/ExtractWisdom/` + +If this directory exists, load and apply any PREFERENCES.md, configurations, or resources found there. These override default behavior. If the directory does not exist, proceed with skill defaults. + +# ExtractWisdom — Dynamic Content Extraction + +**The next generation of extract_wisdom.** Instead of static sections (IDEAS, QUOTES, HABITS...), this skill detects what wisdom domains actually exist in the content and builds custom sections around them. + +A programming interview gets "Programming Philosophy" and "Developer Workflow Tips." A business podcast gets "Contrarian Business Takes" and "Money Philosophy." A security talk gets "Threat Model Insights" and "Defense Strategies." The sections adapt because the content dictates them. + +## When to Use + +- Analyzing YouTube videos, podcasts, interviews, articles +- User says "extract wisdom", "what's interesting in this", "key takeaways" +- Processing any content where you want to capture the best stuff +- When standard extraction patterns miss the gems + +## Depth Levels + +Extract at different depths depending on need. Default is **Full** if no level is specified. + +| Level | Sections | Bullets/Section | Closing Sections | When | +|-------|----------|----------------|-----------------|------| +| **Instant** | 1 | 8 | None | Quick hit. One killer section. | +| **Fast** | 3 | 3 | None | Skim in 30 seconds. | +| **Basic** | 3 | 5 | One-Sentence Takeaway only | Solid overview without the deep cuts. | +| **Full** | 5-12 | 3-15 | All three | The default. Complete extraction. | +| **Comprehensive** | 10-15 | 8-15 | All three + Themes & Connections | Maximum depth. Nothing left behind. | + +**How to invoke:** "extract wisdom (fast)" or "extract wisdom at comprehensive level" or just "extract wisdom" for Full. + +**Comprehensive extras:** +- **Themes & Connections** closing section: identify 3-5 throughlines that connect multiple sections. Not summaries — the deeper patterns the speaker may not even realize they're revealing. +- Prioritize breadth. Every significant wisdom domain gets its own section. +- No merging sections to save space. If the content supports 15 sections, use 15. + +**All levels use the same voice, tone rules, and quality standards.** The only thing that changes is structure. An Instant extraction should hit just as hard per-bullet as a Comprehensive one. + +## Workflow Routing + +| Workflow | Trigger | File | +|----------|---------|------| +| **Extract** | "extract wisdom from", "analyze this", YouTube URL | `Workflows/Extract.md` | + +## The Core Idea + +Old extract_wisdom: Static sections. Same headers every time. IDEAS. QUOTES. HABITS. FACTS. + +This skill: **Read the content first. Figure out what's actually in there. Build sections around what you find.** + +The output should feel like your smartest friend watched/read the thing and is telling you about it over coffee. Not a book report. Not documentation. A real person pointing out the parts that made them go "holy shit" or "wait, that's actually brilliant." + +## Tone Rules (CRITICAL) + +**Canonical voice reference: `PAI/USER/WRITINGSTYLE.md`** — read this file for the full voice definition. The bullets should sound like {PRINCIPAL.NAME} telling a friend about it over coffee. Not compressed info nuggets. Not clever one-liners. Actual spoken observations. + +**THREE LEVELS — we're aiming for Level 3:** + +**Level 1 (BAD — documentation):** +- The speaker discussed the importance of self-modifying software in the context of agentic AI development +- It was noted that financial success has diminishing returns beyond a certain threshold +- The distinction between "vibe coding" and "agentic engineering" was emphasized as meaningful + +**Level 2 (BETTER — but still "smart bullet points"):** +- He built self-modifying software basically by accident — just made the agent aware of its own source code +- Money has diminishing returns. A cheeseburger is a cheeseburger no matter how rich you are. +- "Vibe coding is a slur" — he calls it agentic engineering, and only does vibe coding after 3am + +**Level 3 (YES — this is what we want — conversational, {PRINCIPAL.NAME}'s voice):** +- He wasn't trying to build self-modifying software. He just let the agent see its own source code and it started fixing itself. +- Past a certain point, money stops mattering. A cheeseburger is a cheeseburger no matter how rich you are. +- He calls vibe coding a slur. What he does is agentic engineering. The vibe coding only happens after 3am, and he regrets it in the morning. + +**The difference between Level 2 and 3:** Level 2 is compressed info with em-dashes. Level 3 is how you'd actually SAY it. Varied sentence lengths. Letting a thought breathe. Not trying to be clever — just being clear and direct and a little bit personal. + +**Key signals of Level 3:** +- Reads naturally when spoken aloud +- Varied sentence lengths — some short, some longer +- Understated — lets the content carry the weight +- Uses periods, not em-dashes, to let ideas land +- Feels opinionated ("Past a certain point, money stops mattering") not just informational +- The reader should think "I want to watch this" not "I got the summary" + +## Rules for Extracted Points + +1. **Write like you'd say it.** Read each bullet aloud. If it sounds like a press release or a compressed tweet, rewrite it. If it sounds like you telling a friend what you just watched, you nailed it. +2. **8-16 words per sentence.** This is the target range. Mix short (8-10) with medium (11-14) and longer (15-16). Don't make them all the same length. Exception: verbatim quotes can be any length since they're the speaker's actual words. +3. **Let ideas breathe.** Use periods between thoughts, not em-dashes. Short sentences. Then a slightly longer one to explain. That's the rhythm. +4. **Include the actual detail.** Not "he talked about money" but "a cheeseburger is a cheeseburger no matter how rich you are." +5. **Use the speaker's words when they're good.** If they said something perfectly, use it. +6. **No hedging language.** Not "it was suggested that" or "the speaker noted." Just say the thing. +7. **Capture what made you stop.** Every bullet should be something worth telling someone about. +8. **Vary your openers.** Don't start three bullets the same way. And don't front-load with "He" — if more than 3 bullets in a section start with the speaker's name, you're writing a biography. +9. **Capture the human moments.** Burnout stories, moments of doubt, something that moved them. That's wisdom too. Don't skip it because it's not "technical." +10. **Insight over inventory.** "He uses Go for CLIs" is inventory. "He picked a language he doesn't even like because the ecosystem fits agents perfectly. That's the new normal." is insight. Go deeper. +11. **Specificity is everything.** "He was impressed by the agent" = bad. "The agent found ffmpeg, curled the Whisper API, and transcribed a voice message nobody taught it to handle" = good. +12. **Tension and surprise.** The best bullets have a contradiction or reversal. "Every VC is offering hundreds of millions. He genuinely doesn't care." The gap between the offer and the indifference IS the wisdom. +13. **Understated, not clever.** Let the content carry the weight. You don't need to manufacture drama or craft the perfect one-liner. Just state what's interesting plainly and move on. + +## How Dynamic Sections Work + +### Phase 1: Content Scan + +Read/listen to the full content. As you go, notice what DOMAINS of wisdom are present. These aren't the topics discussed — they're the TYPES of insight being delivered. + +Examples of wisdom domains (these are illustrative, not exhaustive): +- Programming Philosophy (how to think about code, not specific syntax) +- Developer Workflow (practical tips for how to work) +- Business/Money Philosophy (unconventional takes on money, success, building companies) +- Human Psychology (insights about how people think, behave, learn) +- Technology Predictions (where things are headed) +- Life Philosophy (how to live, what matters) +- Contrarian Takes (things that go against conventional wisdom) +- First-Time Revelations (things you're hearing for the first time — genuinely new) +- Technical Architecture (how something is built, design decisions) +- Leadership & Team Dynamics (managing people, working with others) +- Creative Process (how to make things, craft, art) + +### Phase 2: Section Selection + +Pick sections based on depth level (default Full = 5-12). Requirements: +- Section count follows depth level table. Full = 5-12, Comprehensive = 10-15, Basic/Fast = 3, Instant = 1. +- Each section must have at least 3 STRONG bullets to justify existing (except Fast, where 3 tight bullets IS the section). If you can only scrape together 2 weak ones, merge into a related section. +- Always include "Quotes That Hit Different" if the content has good ones +- Always include "First-Time Revelations" if there are genuinely new ideas — things you literally didn't know before +- Section names should be conversational, not academic. "Money Philosophy" not "Financial Considerations" +- Sections should be SPECIFIC to this content. Generic sections = failure. +- **Kill inventory sections.** If a section is just a list of facts ("uses X for Y, uses A for B"), it's not wisdom. Either go deeper on WHY those choices matter or merge the facts into a section about the underlying philosophy. +- **Don't split what belongs together.** If "burnout recovery" and "money philosophy" are actually both about "what success really means," make one richer section instead of two thin ones. +- **Name sections like a magazine editor.** "The Death of 80% of Apps" is great. "Technology Predictions" is not. The section name itself should make you curious. It's a headline, not a category. +- **Surprise density per section.** If a section has 6+ bullets but only 2 are genuinely surprising, kill the padding and keep the winners. Quality > quantity per section. +- **Don't drop your best material between drafts.** If a spicy take, stunning moment, or first-time revelation was identified in an earlier pass, it MUST survive into the final version. Losing great material is worse than adding mediocre material. + +### Phase 3: Extraction + +For each section, extract 3-15 bullets depending on density. Apply all tone rules. Every bullet earns its place. + +**The Spiciest Take Rule:** If the speaker has a genuinely contrarian or hot take on a topic (e.g., "screw MCPs", "X is dead", "Y is overhyped"), that take MUST appear somewhere. Spicy takes are the most memorable, shareable, and valuable parts of any content. Don't water them down. Don't leave them out. + +**The "Would I Tweet This?" Test:** After extraction, scan your bullets. If fewer than half would make a good standalone tweet or social media post, your bullets are too generic. The best extractions are effectively a thread of tweetable insights. + +### Phase 4: Closing Sections (Depth-Level Dependent) + +Which closing sections to include depends on depth level: + +| Level | Closing Sections | +|-------|-----------------| +| **Instant** | None | +| **Fast** | None | +| **Basic** | One-Sentence Takeaway only | +| **Full** | One-Sentence Takeaway + If You Only Have 2 Minutes + References & Rabbit Holes | +| **Comprehensive** | All three above + Themes & Connections | + +**One-Sentence Takeaway** +The single most important thing from the entire piece in 15-20 words. + +**If You Only Have 2 Minutes** +The 5-7 absolute must-know points. The cream of the cream. + +**References & Rabbit Holes** +People, projects, books, tools, and ideas mentioned that are worth following up on. Brief context for each. + +**Themes & Connections** (Comprehensive only) +3-5 throughlines that connect multiple sections. The deeper patterns the speaker may not realize they're revealing. Not summaries. Synthesis. + +## Output Format + +```markdown +# EXTRACT WISDOM: {Content Title} +> {One-line description of what this is and who's talking} + +--- + +## {Dynamic Section 1 Name} + +- {bullet} +- {bullet} +- {bullet} + +## {Dynamic Section 2 Name} + +- {bullet} +- {bullet} + +[... more dynamic sections ...] + +--- + +## One-Sentence Takeaway + +{15-20 word sentence} + +## If You Only Have 2 Minutes + +- {essential point 1} +- {essential point 2} +- {essential point 3} +- {essential point 4} +- {essential point 5} + +## References & Rabbit Holes + +- **{Name/Project}** — {one-line context of why it's worth looking into} +- **{Name/Project}** — {context} +``` + +## Quality Check + +Before delivering output, verify: +- [ ] Sections are specific to THIS content, not generic +- [ ] No bullet sounds like it was written by a committee +- [ ] Every bullet has a specific detail, quote, or insight — not vague summaries +- [ ] Section names are conversational and headline-worthy (not category labels) +- [ ] Section count matches depth level (Instant=1, Fast/Basic=3, Full=5-12, Comprehensive=10-15) +- [ ] Closing sections match depth level (see Phase 4 table) +- [ ] No bullet starts with "The speaker" or "It was noted that" +- [ ] No more than 3 bullets per section start with "He" or the speaker's name +- [ ] No bullet exceeds 25 words +- [ ] No inventory sections (just listing facts without insight) +- [ ] "If You Only Have 2 Minutes" bullets are each under 20 words +- [ ] Reading the output makes you want to consume the original content diff --git a/skills/contentanalysis/ExtractWisdom/Workflows/Extract.md b/skills/contentanalysis/ExtractWisdom/Workflows/Extract.md new file mode 100755 index 0000000..50ca50e --- /dev/null +++ b/skills/contentanalysis/ExtractWisdom/Workflows/Extract.md @@ -0,0 +1,60 @@ +# Extract Workflow + +Extract dynamic, content-adaptive wisdom from any content source. + +## Input Sources + +| Source | Method | +|--------|--------| +| YouTube URL | `fabric -y "URL"` to get transcript | +| Article URL | WebFetch to get content | +| File path | Read the file directly | +| Pasted text | Use directly | + +## Execution Steps + +### Step 1: Get the Content + +Obtain the full text/transcript. For YouTube, use `fabric -y "URL"` to extract transcript. Save to a working file if large. + +### Step 2: Deep Read + +Read the entire content. Don't extract yet. Notice: +- What domains of wisdom are present? +- What made you stop and think? +- What's genuinely novel vs. commonly known? +- What would {PRINCIPAL.NAME} highlight if he were reading this? +- What quotes land perfectly? + +### Step 3: Select Dynamic Sections + +Based on your deep read, pick 5-12 section names. Rules: +- Section names must be conversational, not academic +- Each must have at least 3 quality bullets +- Always include "Quotes That Hit Different" if source has quotable moments +- Always include "First-Time Revelations" if genuinely new ideas exist +- Be SPECIFIC — "Agentic Engineering Philosophy" not "Technology Insights" + +### Step 4: Extract Per Section + +For each section, extract 3-15 bullets. Apply tone rules from SKILL.md: +- 8-20 words, flexible for clarity +- Specific details, not vague summaries +- Speaker's words when they're good +- No hedging language +- Every bullet worth telling someone about + +### Step 5: Add Closing Sections + +Always append: +1. **One-Sentence Takeaway** (15-20 words) +2. **If You Only Have 2 Minutes** (5-7 essential points) +3. **References & Rabbit Holes** (people, projects, books, tools mentioned) + +### Step 6: Quality Check + +Run the quality checklist from SKILL.md before delivering. + +### Step 7: Output + +Present the complete extraction in the format specified in SKILL.md. diff --git a/skills/contentanalysis/SKILL.md b/skills/contentanalysis/SKILL.md new file mode 100755 index 0000000..a63619c --- /dev/null +++ b/skills/contentanalysis/SKILL.md @@ -0,0 +1,14 @@ +--- +name: ContentAnalysis +description: Content extraction and analysis — wisdom extraction from videos, podcasts, articles, and YouTube. USE WHEN extract wisdom, content analysis, analyze content, insight report, analyze video, analyze podcast, extract insights, key takeaways, what did I miss, extract from YouTube. +--- + +# ContentAnalysis + +Unified skill for content extraction and analysis workflows. + +## Workflow Routing + +| Request Pattern | Route To | +|---|---| +| Extract wisdom, content analysis, insight report, analyze content | `ExtractWisdom/SKILL.md` | diff --git a/skills/docx/LICENSE.txt b/skills/docx/LICENSE.txt new file mode 100755 index 0000000..e092e50 --- /dev/null +++ b/skills/docx/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright (c) 2026 Z.ai All rights reserved. + +Permission is granted for personal, educational, and non-commercial use only. + +Commercial use is strictly prohibited without prior written permission from the author. + +Unauthorized copying, modification, or distribution of the software for commercial purposes is prohibited. + +The author reserves the right to make the final determination of what constitutes "commercial use". + +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 AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE. diff --git a/skills/docx/SKILL.md b/skills/docx/SKILL.md new file mode 100755 index 0000000..2b380ac --- /dev/null +++ b/skills/docx/SKILL.md @@ -0,0 +1,201 @@ +--- +name: docx +metadata: + author: Z.AI + version: "1.0" +description: "Comprehensive document creation, editing, and analysis with support for tracked changes, comments, formatting preservation, and text extraction. When GLM 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 + +## Quick Setup + +```bash +bash "$SKILL_DIR/setup.sh" # Interactive environment check + install +``` + +## Overview + +A .docx file is a ZIP archive containing XML files. This skill provides tools for creating, editing, reading, and reviewing Word documents. + +## Quick Route — Read This First + +**Step 1**: Determine task type → load the corresponding route file +**Step 2**: Determine business scene → load the corresponding scene file (if applicable) +**Step 3**: Load `references/design-system.md` for cover recipes, palettes, and chart colors +**Step 4**: Load `references/common-rules.md` for shared layout, font, and quality rules +**Step 5**: Execute per route instructions +**Step 6**: Run the post-generation checklist + +⚠️ **MANDATORY — Cover Recipe Enforcement (Step 3):** +When creating a document that needs a cover page, you MUST use one of the 7 validated cover recipes (R1–R7) from `design-system.md`. **Free-form cover code is FORBIDDEN.** The recipe provides the wrapper table, background, layout structure, border settings, and spacing — do not reinvent any of these. + +Workflow: (1) Call `selectCoverRecipe(docType, industry)` to get recipe + palette → (2) Use the corresponding `buildCoverRX()` function code from `design-system.md` → (3) Pass your `config` (title, subtitle, metaLines, etc.) into the recipe builder. If you skip this and write cover code from scratch, the cover WILL have compatibility issues (blank pages in MS Office, missing borders, overflow, etc.). + +### Script Path Setup (MANDATORY before any script call) + +All CLI tools live in `scripts/` relative to this skill's directory. Before calling any script, resolve the absolute path once: + +```bash +DOCX_SCRIPTS="/scripts" # ← parent directory of this SKILL.md + +# Then all commands use $DOCX_SCRIPTS: +python3 "$DOCX_SCRIPTS/postcheck.py" output.docx +python3 "$DOCX_SCRIPTS/add_toc_placeholders.py" output.docx --auto +``` + +**For Python imports** (when generation code needs to import skill modules): + +```python +import sys, os +DOCX_SCRIPTS = os.path.join("", "scripts") +if DOCX_SCRIPTS not in sys.path: + sys.path.insert(0, DOCX_SCRIPTS) +``` + +**⚠️ NEVER use bare `python3 scripts/...`** — it only works if cwd happens to be the skill directory. Always use the absolute `$DOCX_SCRIPTS` path. + +### Task Router + +| User Intent | Route | Files to Load | +|-------------|-------|---------------| +| Create/write/generate (no attachment) | **Create** | `routes/create.md` + `references/docx-js-core.md` | +| Edit/modify/revise (has attachment) | **Edit** | `routes/edit.md` + `references/ooxml.md` | +| Format/layout/font/margin | **Format** | `routes/format.md` | +| Comment/annotate/review | **Comment** | `routes/comment.md` | +| Read/analyze/extract | **Read** | `routes/read.md` | + +### Scene Router (Optional — load after route) + +| User Keywords | Scene | File | +|---------------|-------|------| +| thesis, academic, research, paper, dissertation, abstract, journal | Academic | `scenes/academic.md` | +| report, analysis, experiment, testing, survey, review, summary, proposal, feasibility, competitor, industry, operations | Report | `scenes/report.md` | +| contract, agreement, terms, transfer, NDA, confidential, framework, cooperation, service terms, user agreement, procurement | Contract | `scenes/contract.md` | +| resume, CV, job application | Resume | `scenes/resume.md` | +| exam, test, quiz, paper (exam context), lesson plan | Exam | `scenes/exam.md` | +| official document, notice, letter, reply, minutes, red header, government, issuance | Official | `scenes/official-doc.md` | +| broadcast script, product copy, livestream, speech, presentation script, video script | Copywriting | `scenes/copywriting.md` | +| plan, proposal (if not report context) | Report | `scenes/report.md` | +| policy, regulation, standard, management rules | Official | `scenes/official-doc.md` | + +**If no scene matches**, use default design rules from `references/design-system.md` and `references/common-rules.md`. + +## Formatting Standards (Always Apply) + +→ See `references/common-rules.md` for full font profiles, spacing, indent, and layout rules. + +**Key rules (quick reference):** +- **Line spacing**: 1.3x (`line: 312`) — MANDATORY. Exceptions: resume 1.15x, official doc 28pt fixed, copywriting `400`, contract 1.5x +- **CJK body**: Justified + 2-char indent (`firstLine: 480` SimSun / `420` YaHei) +- **Tables**: `margins` set, `ShadingType.CLEAR`, `tableHeader: true`, `cantSplit: true`, title `keepNext: true` +- **Images**: `type` parameter required, preserve aspect ratio via `image-size`, PageBreak inside Paragraph +- **Full-page Table row**: `rule: "exact"` with 1200 twips safety margin + +## Unit Quick Reference + +| Unit | Value | +|------|-------| +| 1 cm | 567 twips | +| 1 inch | 1440 twips | +| 1 pt | 20 half-points | +| A4 | 11906 × 16838 twips | + +For Chinese font size table and common margins, see `references/common-rules.md`. + +## Post-Generation — Two-Layer Verification + +### Layer 1: Manual Checklist (self-check during generation) + +#### Basic Format +- [ ] Line spacing is 1.3x (`line: 312`) or scene-specific override +- [ ] CJK body has 2-char indent (`firstLine: 480` or `420`) +- [ ] Tables have margins set +- [ ] Images preserve aspect ratio via `image-size` — NEVER hardcode both width and height +- [ ] PageBreak inside Paragraph +- [ ] ShadingType uses CLEAR +- [ ] Each numbered list uses unique `reference` +- [ ] **⚠️ CRITICAL — Quotation marks in JS strings properly escaped.** Chinese curly quotes (`""` `''`) MUST use Unicode escapes (`\u201c` `\u201d` `\u2018` `\u2019`); straight quotes (`"` `'`) use `\"` `\'` or alternate delimiters. **This is the #1 most common code generation bug.** Chinese text frequently contains `""` for emphasis or proper nouns (e.g., "双11", "前低后高", "618") — every occurrence MUST be escaped. Failure to escape produces JS syntax errors that silently break document generation. +- [ ] ImageRun includes `type` parameter +- [ ] Header/footer present (unless scene says otherwise) + +#### Heading Styles +- [ ] All body chapter headings use `heading: HeadingLevel.HEADING_X` (never simulate with bold + large font) +- [ ] Cover title may skip Heading style (not in TOC), but body headings MUST use Heading style + +#### Page Break & Blank Page Prevention +- [ ] Cover/content in separate sections +- [ ] Three rules to prevent blank pages: + - ① When using section(NEXT_PAGE), previous section must NOT end with PageBreak (double break = blank page) + - ② PageBreak paragraph SHOULD contain visible text — **exception**: section-ending empty para + PageBreak is allowed (normal section separator, e.g., after cover page) + - ③ No more than 3 consecutive empty paragraphs +- [ ] Full-page Table row height uses `rule: "exact"` (never `"atLeast"` for tall tables) +- [ ] No unwanted blank pages (check each section ending) + +#### TOC +→ See `references/toc.md` for the complete TOC reference and checklist. +- [ ] If TOC title exists → `TableOfContents` element must be present +- [ ] **⚠️ MANDATORY PageBreak after TableOfContents** — a Paragraph containing PageBreak MUST immediately follow the `TableOfContents` element; without it, TOC and body content will render on the same page. This is the #1 TOC formatting failure — never omit it +- [ ] `add_toc_placeholders.py --auto` runs after generation; exit code = 0 +- [ ] **TOC MUST be in its own section** — body section sets `page: { pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL } }` so page numbers start from the first body page, not from the TOC pages +- [ ] **Page number API nesting** — `pageNumbers` MUST be inside `page: {}`, NOT at properties top level (see toc.md § Page Number API) +- [ ] **3-section page numbering** — Cover (no page#) → Front matter (Roman i,ii,iii, start=1) → Body (Arabic 1,2,3, start=1) +- [ ] **Post-process footers** — Roman section footer instrText must contain `PAGE \* ROMAN \* MERGEFORMAT`; Arabic section `PAGE \* arabic \* MERGEFORMAT` (WPS ignores pgNumType fmt). **⚠️ NEVER use `\* decimal` in instrText** — `decimal` is a docx-js API enum value (`NumberFormat.DECIMAL`), NOT a valid Word field format switch; using it causes page numbers to render as "1decimal", "2decimal". The correct Word field switch for Arabic numerals is `\* arabic`. +- [ ] **Remove empty pgNumType** — Post-process to strip `` from cover section (docx-js emits empty element that confuses WPS) +- [ ] **⚠️ TOC Refresh Hint MANDATORY** — between `TableOfContents` element and the PageBreak, MUST add an italic gray note paragraph telling users to right-click TOC → "Update Field" to refresh page numbers (see toc.md § TOC Refresh Hint) + +#### Table Cross-Page +- [ ] Header rows: `tableHeader: true` +- [ ] All rows: `cantSplit: true` +- [ ] Title paragraph: `keepNext: true` + +#### Cover +- [ ] **Cover MUST use a validated recipe (R1–R7)** from `design-system.md` — free-form cover code is forbidden +- [ ] Cover recipe matches document type (per `selectCoverRecipe()` in `design-system.md`) +- [ ] Cover uses the 16838 outer wrapper table with `allNoBorders` (all recipes provide this) +- [ ] Cover title uses `calcTitleLayout()` — never hardcoded font size above 40pt +- [ ] Cover spacing uses `calcCoverSpacing()` — never hardcoded large spacing values +- [ ] Cover content does not overflow (total height ≤ 15638 twips, Table uses `rule: "exact"`) +- [ ] Every TextRun on dark/colored background has explicit `color` set (Rule 9 — never rely on default black) +- [ ] Cover section has no trailing PageBreak or empty paragraphs +- [ ] Title lines split at semantic boundaries (no mid-word breaks, no single-char orphan lines) +- [ ] No text-character decorative lines (`───`, `━━━`) — use paragraph borders only + +### Layer 2: Automated Post-Check Script + +```bash +python3 "$DOCX_SCRIPTS/postcheck.py" output.docx +``` + +Automatically checks 14 business rules: blank pages, **cover overflow (font size/spacing/trailing content)**, line spacing consistency, table margins, table cross-page control (cantSplit/tblHeader), image overflow, image aspect ratio distortion, font fallback, CJK indent, heading hierarchy, ShadingType misuse, TOC quality, document cleanliness (placeholder text/Markdown/HTML residuals), report content quality (abstract presence/heading specificity/vague conclusion detection). + +⚠️ **After generating any document, MUST run postcheck.py and fix all ❌ errors.** + +## Math Formulas + +Formula input uses **LaTeX syntax**, internally converted to docx-js Math objects. + +- **Basic formulas** (fractions, sub/superscript, roots, summation) → docx-js Math components +- **Complex formulas** (3+ nesting, matrices, piecewise functions) → matplotlib PNG fallback + +See `references/math-formulas.md`. + +## Charts + +Default: **matplotlib template library** generates PNG for embedding. + +6 ready-to-use templates: bar, line, pie, box, radar, heatmap. +Colors auto-derived from document palette.accent for style consistency. +Default palette: Morandi low-saturation (see design-system.md). + +See `references/chart-templates.md`. + +## Dependencies + +- **pandoc**: Text extraction +- **docx**: `bun add docx` or `npm install docx` (creating) +- **LibreOffice**: PDF conversion, .doc support +- **Poppler**: PDF to image (`pdftoppm`) +- **defusedxml**: Secure XML parsing +- **python-docx**: Simple comment operations diff --git a/skills/docx/references/chart-templates.md b/skills/docx/references/chart-templates.md new file mode 100755 index 0000000..f608ba1 --- /dev/null +++ b/skills/docx/references/chart-templates.md @@ -0,0 +1,386 @@ +# Chart Templates — matplotlib Template Library + +## Design Philosophy + +GLM uses **matplotlib as the primary chart engine**. Advantages: +- High chart quality, print-ready +- Full style control, consistent with document palette +- Supports complex chart types (heatmap, radar, box plot, etc.) +- Reliable CJK rendering (with SimHei font configured) + +**When to use native Word charts?** +Only when the user explicitly requests "editable charts." Default is always matplotlib PNG embedding. + +## Base Configuration + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.font_manager import FontProperties + +# ── CJK Font ── +_FONT_PATHS = [ + "/System/Library/Fonts/Supplemental/SimHei.ttf", # macOS + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", # Linux + "/usr/share/fonts/truetype/chinese/SimHei.ttf", # custom install + "./SimHei.ttf", # current dir +] +ZH_FONT = None +for _fp in _FONT_PATHS: + try: + ZH_FONT = FontProperties(fname=_fp) + break + except: + continue + +plt.rcParams["axes.unicode_minus"] = False + +# ── Palette Adapter ── +def make_chart_palette(accent: str, surface: str = "#F2F4F6") -> dict: + """Generate chart palette from document palette.accent""" + return { + "primary": accent, + "series": _generate_series_colors(accent, 6), + "grid": "#E0E0E0", + "bg": "white", + "text": "#333333", + "surface": surface, + } + +def _generate_series_colors(base_hex: str, count: int) -> list: + """Generate series colors via hue rotation from base color""" + import colorsys + base = tuple(int(base_hex.lstrip("#")[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + h, s, v = colorsys.rgb_to_hsv(*base) + colors = [] + for i in range(count): + hi = (h + i * (1.0 / count)) % 1.0 + r, g, b = colorsys.hsv_to_rgb(hi, min(s * 0.9, 1.0), min(v * 1.05, 1.0)) + colors.append(f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}") + return colors + +# ── Universal Export ── +def save_chart(fig, path: str, dpi: int = 200): + """Save chart with uniform DPI. Square charts (pie/radar) use fixed padding to preserve 1:1 ratio.""" + w, h = fig.get_size_inches() + if abs(w - h) < 0.1: + fig.savefig(path, dpi=dpi, bbox_inches="tight", pad_inches=0.3, + facecolor="white", edgecolor="none") + else: + fig.savefig(path, dpi=dpi, bbox_inches="tight", pad_inches=0.1, + facecolor="white", edgecolor="none") + plt.close(fig) + return path +``` + +## Template 1: Bar Chart + +```python +def bar_chart(categories: list, values: list, title: str = "", + ylabel: str = "", palette: dict = None, output: str = "bar.png"): + """ + Basic bar chart. + categories: ["Q1", "Q2", "Q3", "Q4"] + values: [120, 150, 180, 200] + """ + p = palette or make_chart_palette("#5B8DB8") + fig, ax = plt.subplots(figsize=(10, 6)) + + bars = ax.bar(categories, values, color=p["primary"], width=0.6, edgecolor="white") + + # Data labels + for bar, val in zip(bars, values): + ax.text(bar.get_x() + bar.get_width() / 2, bar.get_height() + max(values) * 0.02, + str(val), ha="center", va="bottom", fontsize=10, + fontproperties=ZH_FONT, color=p["text"]) + + if title: + ax.set_title(title, fontproperties=ZH_FONT, fontsize=14, pad=15, color=p["text"]) + if ylabel: + ax.set_ylabel(ylabel, fontproperties=ZH_FONT, fontsize=11, color=p["text"]) + + ax.set_xticklabels(categories, fontproperties=ZH_FONT, fontsize=10) + ax.spines[["top", "right"]].set_visible(False) + ax.grid(axis="y", alpha=0.3, color=p["grid"]) + + if len(categories) > 6: + plt.xticks(rotation=45, ha="right") + + return save_chart(fig, output) +``` + +### Grouped Bar Chart + +```python +def grouped_bar(categories: list, groups: dict, title: str = "", + ylabel: str = "", palette: dict = None, output: str = "grouped_bar.png"): + """ + groups: {"Product A": [10, 20, 30], "Product B": [15, 25, 35]} + """ + p = palette or make_chart_palette("#5B8DB8") + fig, ax = plt.subplots(figsize=(10, 6)) + + x = np.arange(len(categories)) + n = len(groups) + width = 0.8 / n + + for i, (name, vals) in enumerate(groups.items()): + offset = (i - n / 2 + 0.5) * width + bars = ax.bar(x + offset, vals, width, label=name, color=p["series"][i % len(p["series"])]) + + ax.set_xticks(x) + ax.set_xticklabels(categories, fontproperties=ZH_FONT, fontsize=10) + ax.legend(prop=ZH_FONT, frameon=False) + if title: + ax.set_title(title, fontproperties=ZH_FONT, fontsize=14, pad=15) + ax.spines[["top", "right"]].set_visible(False) + ax.grid(axis="y", alpha=0.3) + + return save_chart(fig, output) +``` + +## Template 2: Line Chart + +```python +def line_chart(x_data: list, series: dict, title: str = "", + xlabel: str = "", ylabel: str = "", palette: dict = None, + output: str = "line.png"): + """ + series: {"Revenue": [100, 120, 150, 180], "Cost": [80, 90, 100, 110]} + """ + p = palette or make_chart_palette("#5B8DB8") + fig, ax = plt.subplots(figsize=(10, 6)) + + for i, (name, values) in enumerate(series.items()): + color = p["series"][i % len(p["series"])] + ax.plot(x_data, values, marker="o", markersize=5, linewidth=2, + label=name, color=color) + + if title: + ax.set_title(title, fontproperties=ZH_FONT, fontsize=14, pad=15) + if xlabel: + ax.set_xlabel(xlabel, fontproperties=ZH_FONT, fontsize=11) + if ylabel: + ax.set_ylabel(ylabel, fontproperties=ZH_FONT, fontsize=11) + + ax.legend(prop=ZH_FONT, frameon=False, loc="best") + ax.spines[["top", "right"]].set_visible(False) + ax.grid(True, alpha=0.3) + + if len(x_data) > 6: + plt.xticks(rotation=45, ha="right") + + return save_chart(fig, output) +``` + +## Template 3: Pie Chart + +```python +def pie_chart(labels: list, values: list, title: str = "", + palette: dict = None, output: str = "pie.png"): + """Pie chart — auto-merges slices below 3% into 'Other'""" + p = palette or make_chart_palette("#5B8DB8") + fig, ax = plt.subplots(figsize=(8, 8)) + + # Merge slices below 3% into "Other" + total = sum(values) + merged_labels, merged_values = [], [] + other = 0 + for lbl, val in zip(labels, values): + if val / total < 0.03: + other += val + else: + merged_labels.append(lbl) + merged_values.append(val) + if other > 0: + merged_labels.append("Other") + merged_values.append(other) + + colors = p["series"][:len(merged_labels)] + wedges, texts, autotexts = ax.pie( + merged_values, labels=merged_labels, colors=colors, + autopct="%1.1f%%", startangle=90, pctdistance=0.75, + textprops={"fontproperties": ZH_FONT, "fontsize": 11} + ) + + for t in autotexts: + t.set_fontsize(10) + t.set_color("white") + + if title: + ax.set_title(title, fontproperties=ZH_FONT, fontsize=14, pad=20) + + return save_chart(fig, output) +``` + +## Template 4: Box Plot + +```python +def box_plot(data: dict, title: str = "", ylabel: str = "", + palette: dict = None, output: str = "box.png"): + """ + data: {"Class A": [78, 82, 91, ...], "Class B": [65, 70, 88, ...]} + """ + p = palette or make_chart_palette("#5B8DB8") + fig, ax = plt.subplots(figsize=(10, 6)) + + labels = list(data.keys()) + values = list(data.values()) + + bp = ax.boxplot(values, labels=labels, patch_artist=True, notch=False, + medianprops={"color": "white", "linewidth": 2}) + + for i, patch in enumerate(bp["boxes"]): + patch.set_facecolor(p["series"][i % len(p["series"])]) + patch.set_alpha(0.8) + + ax.set_xticklabels(labels, fontproperties=ZH_FONT, fontsize=11) + if title: + ax.set_title(title, fontproperties=ZH_FONT, fontsize=14, pad=15) + if ylabel: + ax.set_ylabel(ylabel, fontproperties=ZH_FONT, fontsize=11) + ax.spines[["top", "right"]].set_visible(False) + ax.grid(axis="y", alpha=0.3) + + return save_chart(fig, output) +``` + +## Template 5: Radar Chart + +```python +def radar_chart(categories: list, series: dict, title: str = "", + palette: dict = None, output: str = "radar.png"): + """ + categories: ["Chinese", "Math", "English", "Physics", "Chemistry"] + series: {"Student A": [85, 92, 78, 90, 88], "Student B": [75, 88, 92, 70, 85]} + """ + p = palette or make_chart_palette("#5B8DB8") + fig, ax = plt.subplots(figsize=(8, 8), subplot_kw=dict(polar=True)) + + n = len(categories) + angles = np.linspace(0, 2 * np.pi, n, endpoint=False).tolist() + angles += angles[:1] # close the polygon + + for i, (name, values) in enumerate(series.items()): + vals = values + values[:1] # close the polygon + color = p["series"][i % len(p["series"])] + ax.plot(angles, vals, linewidth=2, label=name, color=color) + ax.fill(angles, vals, alpha=0.15, color=color) + + ax.set_xticks(angles[:-1]) + ax.set_xticklabels(categories, fontproperties=ZH_FONT, fontsize=11) + ax.legend(prop=ZH_FONT, loc="upper right", bbox_to_anchor=(1.2, 1.1), frameon=False) + + if title: + ax.set_title(title, fontproperties=ZH_FONT, fontsize=14, pad=25) + + return save_chart(fig, output) +``` + +## Template 6: Heatmap + +```python +def heatmap(data: list, row_labels: list, col_labels: list, title: str = "", + palette: dict = None, output: str = "heatmap.png"): + """ + data: 2D array [[1,2,3],[4,5,6]] + row_labels: ["Row 1", "Row 2"] + col_labels: ["Col 1", "Col 2", "Col 3"] + """ + fig, ax = plt.subplots(figsize=(max(8, len(col_labels) * 1.2), max(6, len(row_labels) * 0.8))) + + arr = np.array(data) + im = ax.imshow(arr, cmap="YlOrRd", aspect="auto") + + ax.set_xticks(range(len(col_labels))) + ax.set_yticks(range(len(row_labels))) + ax.set_xticklabels(col_labels, fontproperties=ZH_FONT, fontsize=10) + ax.set_yticklabels(row_labels, fontproperties=ZH_FONT, fontsize=10) + + # Value annotations + for i in range(len(row_labels)): + for j in range(len(col_labels)): + val = arr[i, j] + color = "white" if val > arr.max() * 0.7 else "black" + ax.text(j, i, f"{val:.1f}", ha="center", va="center", + fontsize=10, color=color) + + fig.colorbar(im, ax=ax, shrink=0.8) + if title: + ax.set_title(title, fontproperties=ZH_FONT, fontsize=14, pad=15) + + return save_chart(fig, output) +``` + +## Embedding in Documents (MANDATORY — Preserve Aspect Ratio) + +**⚠️ Core Rule: When embedding any chart image, you MUST read actual image dimensions to calculate displayHeight. NEVER hardcode both width and height.** + +Pie and radar charts are square — mismatched width/height produces ellipses or diamonds. + +```js +// ✅ Correct: read actual image dimensions +const chartBuffer = fs.readFileSync("bar.png"); +const sizeOf = require("image-size"); +const dims = sizeOf(chartBuffer); +const displayWidth = 500; +const displayHeight = Math.round(displayWidth * (dims.height / dims.width)); + +new Paragraph({ + alignment: AlignmentType.CENTER, + spacing: { before: 200, after: 100 }, + children: [ + new ImageRun({ + data: chartBuffer, + transformation: { width: displayWidth, height: displayHeight }, + type: "png", + }), + ], +}) +``` + +```js +// ❌ Wrong: hardcoded width and height (pie becomes ellipse, radar becomes diamond) +new ImageRun({ + data: chartBuffer, + transformation: { width: 500, height: 350 }, // wrong ratio! + type: "png", +}) +``` + +```python +# ✅ Python (ReportLab) correct approach: +from PIL import Image as PILImage +from reportlab.platypus import Image +pil_img = PILImage.open('chart.png') +orig_w, orig_h = pil_img.size +target_width = 400 # pt +scale = target_width / orig_w +img = Image('chart.png', width=target_width, height=orig_h * scale) +``` + +## Chart Selection Guide + +| Data Scenario | Recommended Chart | Template Function | +|---------------|-------------------|-------------------| +| Category comparison | Bar chart | `bar_chart()` | +| Multi-group comparison | Grouped bar | `grouped_bar()` | +| Trend over time | Line chart | `line_chart()` | +| Proportion/composition | Pie chart | `pie_chart()` | +| Distribution/spread | Box plot | `box_plot()` | +| Multi-dimensional assessment | Radar chart | `radar_chart()` | +| Matrix correlation | Heatmap | `heatmap()` | + +## Quality Standards + +1. **DPI**: Uniform 200 DPI (built into `save_chart`) +2. **Colors**: Derived from document palette.accent for style consistency +3. **CJK text**: Must configure SimHei font; otherwise renders as boxes +4. **Label overlap prevention**: Auto-rotate 45° when >6 x-axis labels +5. **Legend**: Move outside chart (`bbox_to_anchor`) when >4 series +6. **Grid**: Light gray dashed grid lines for readability +7. **Clean frames**: Remove top/right spines for modern minimalist look +8. **Aspect ratio (CRITICAL)**: Must use `image-size` (JS) or `PIL` (Python) to read actual image dimensions and calculate displayHeight proportionally. **Pie and radar charts are square — hardcoding non-1:1 ratio causes ellipse/diamond distortion.** +9. **Dimensions**: Default 10×6 inches, fits well within A4 page diff --git a/skills/docx/references/common-rules.md b/skills/docx/references/common-rules.md new file mode 100755 index 0000000..938206d --- /dev/null +++ b/skills/docx/references/common-rules.md @@ -0,0 +1,419 @@ +# Common Rules + +Shared rules referenced by all scene files. Scene-specific overrides take precedence. + +## Default Page Layout + +A4 portrait. Unless the scene specifies otherwise, use: + +| Property | Value | Twips | +|----------|-------|-------| +| Page width | 21.0 cm | 11906 | +| Page height | 29.7 cm | 16838 | +| Top margin | 2.54 cm | 1440 | +| Bottom margin | 2.54 cm | 1440 | +| Left margin | 3.0 cm | 1701 | +| Right margin | 2.5 cm | 1417 | + +```js +page: { + size: { width: 11906, height: 16838, orientation: PageOrientation.PORTRAIT }, + margin: { top: 1440, bottom: 1440, left: 1701, right: 1417 }, +} +``` + +**Scene overrides:** +- **Official doc (GB/T 9704 red-header):** top 2098, bottom 1984, left 1588, right 1474 +- **Exam:** top/bottom 1134 (2 cm), left/right 1134 (2 cm) + +## Default Font Specifications + +Two font profiles exist. Each scene declares which profile it uses. + +### Profile A: Formal (report, academic, contract, official-doc, exam) + +| Element | CN Font | EN Font | Size | Notes | +|---------|---------|---------|------|-------| +| H1 | SimHei | Times New Roman | 16 pt (size: 32) | Bold, centered | +| H2 | SimHei | Times New Roman | 15 pt (size: 30) | Bold | +| H3 | SimHei | Times New Roman | 14 pt (size: 28) | Bold | +| Body | SimSun | Times New Roman | 12 pt (size: 24) | | +| Caption | SimSun | Times New Roman | 10.5 pt (size: 21) | | + +- Text color: always **pure black `"000000"`** (never dark-blue-grey) +- First-line indent: **480 twips** (2 chars at SimSun 12pt) +- Line spacing: **312** (1.3x). +- **Color routing for non-report documents**: When the document is a short-form text (essay, evaluation, letter, speech, application, reflection, etc.) rather than a structured report/whitepaper/proposal/consulting deliverable, heading color MUST use pure black `"000000"` instead of `palette.primary`. Colored headings are reserved for documents that need brand/professional identity (reports with covers, whitepapers, proposals, consulting deliverables). + +### Profile B: Visual (resume, copywriting) + +| Element | CN Font | EN Font | Size | +|---------|---------|---------|------| +| Name/Title | Microsoft YaHei | Calibri | Varies | +| Body | Microsoft YaHei | Calibri | 10–11 pt | +| Caption | Microsoft YaHei | Calibri | 9 pt | + +- First-line indent: **420 twips** (2 chars at YaHei) +- Color: per design-system palette + +### Official-Doc Font Override (GB/T 9704) + +When `needsRedHeader() = true`: + +| Element | Font | Size | +|---------|------|------| +| Red header org name | STXiaoBiaoSong (or SimSun bold) | 26 pt (size: 52) | +| Title | STXiaoBiaoSong (or SimHei) | 22 pt (size: 44) | +| Body | FangSong | 16 pt (size: 32) | +| Section heading | FangSong_GB2312 bold (or HeiTi) | 16 pt (size: 32) | + +- Line spacing: **560** (28 pt fixed) +- First-line indent: **640 twips** (2 chars at FangSong 16pt) + +## Chinese Font Size Reference + +| Name | Points | Half-points (size:) | +|------|--------|---------------------| +| Chu Hao (initial) | 42 | 84 | +| Xiao Chu | 36 | 72 | +| Yi Hao (1st) | 26 | 52 | +| Xiao Yi | 24 | 48 | +| Er Hao (2nd) | 22 | 44 | +| Xiao Er | 18 | 36 | +| San Hao (3rd) | 16 | 32 | +| Xiao San | 15 | 30 | +| Si Hao (4th) | 14 | 28 | +| Xiao Si | 12 | 24 | +| Wu Hao (5th) | 10.5 | 21 | +| Xiao Wu | 9 | 18 | +| Liu Hao (6th) | 7.5 | 15 | + +## Placeholder Convention + +When required information is missing, use standardized placeholders so users can Find & Replace in Word. + +**Format:** Always use full-width brackets `【 】`. + +| Type | Format | Example | +|------|--------|---------| +| General field | `【field name】` | Name: 【company name】 | +| Monetary amount | `【RMB in words: yuan (lowercase: ¥)】` | Amount: 【RMB in words】 | +| Date field | `【____/____/____】` | Signing date: 【____/____/____】 | +| Long text | `【Please fill in: ______】` | Delivery criteria: 【Please fill in: ______】 | +| Attachment ref | `【See Appendix 1: ______】` | | + +**Rules:** +1. Placeholder format must be consistent throughout the entire document +2. Each placeholder must specify exactly what is needed (never use vague "TBD" or "to be completed") +3. Never hard-code unconfirmed critical facts; use a placeholder instead +4. Never use sloppy expressions like "to be refined", "omitted", "user fills in later" + +## Title Orphan Prevention (All Scenes) + +Body headings (H1/H2/H3) and cover titles must avoid leaving 1–2 characters alone on the last line. This rule applies to ALL document types. + +**For cover titles:** Always use `calcTitleLayout()` + `splitTitleLines()` from `design-system.md` — these handle orphan prevention automatically (merges ≤2-char last lines into the previous line). + +**For body headings (H1/H2/H3):** When a heading text is long enough to wrap, apply the same `splitTitleLines()` logic. If the heading would cause a single-character orphan in Word's auto-wrapping, manually split into multiple `TextRun` elements with a `Break` (soft line break) at a semantic boundary. + +```js +const { Break } = require("docx"); + +// Check if heading needs manual line break to prevent orphan +function buildHeadingRuns(text, maxCharsPerLine, runProps) { + // If text fits in one line, no action needed + if (text.length <= maxCharsPerLine) { + return [new TextRun({ text, ...runProps })]; + } + // Use splitTitleLines to find semantic break points + const lines = splitTitleLines(text, maxCharsPerLine); + const runs = []; + for (let i = 0; i < lines.length; i++) { + if (i > 0) runs.push(new TextRun({ break: 1, ...runProps, text: "" })); // soft line break + runs.push(new TextRun({ text: lines[i], ...runProps })); + } + return runs; +} +``` + +**Estimation for maxCharsPerLine:** For centered headings, estimate available width = page width - left margin - right margin. For SimHei at a given pt size, each CJK char ≈ pt × 20 twips wide. Divide available width by char width to get `maxCharsPerLine`. + +--- + +## Undefined / Null Value Prevention (Mandatory) + +Generated code MUST guard against outputting literal `undefined`, `null`, `NaN`, or empty strings for any visible text field. This is a **hard requirement** — these are never acceptable in a delivered document. + +```js +// ✅ MANDATORY: Safe text helper — use for ALL user-facing text values +function safeText(value, placeholder) { + if (value === undefined || value === null || value === "" || String(value) === "NaN" || String(value) === "undefined") { + return placeholder || "【Please fill in】"; + } + return String(value); +} + +// Usage: +new TextRun({ text: safeText(config.contact, "【Contact person】") }) +new TextRun({ text: safeText(row.phone, "【Phone number】") }) +``` + +**Rules:** +1. Every `TextRun` displaying user-provided or config-derived data MUST use `safeText()` or equivalent guard +2. If a field is optional and not provided, use `【Please fill in: field_name】` placeholder (full-width brackets) +3. Table cells with missing data: show `【Please fill in】`, never leave as empty string or undefined +4. This applies to ALL scenes — contracts, reports, academic, exams, etc. + +--- + +## WPS / Office Word Compatibility (Mandatory) + +Generated .docx files must render consistently in both Microsoft Office Word and WPS Office. The following OOXML features have known compatibility issues — avoid or use carefully. + +### Features to AVOID (high incompatibility risk) + +| Feature | Issue | Alternative | +|---------|-------|------------| +| **Text-character decorative lines** (e.g., `───`, `━━━`, `═══`, `——————`) | Character-drawn lines depend on font metrics and rendering engine — they appear different widths/lengths in MS Office vs WPS, often truncated or misaligned. They cannot span a controlled width. | **Always use paragraph borders** (`border.top`, `border.bottom`) for horizontal decorative lines. Paragraph borders render consistently across engines and respect indent for precise width control. See recipe R2 for correct implementation. | +| **Default table borders on cover wrapper tables** (forgetting `allNoBorders`) | docx-js default table borders are `single/auto/sz=4`. On the 16838-high cover wrapper, these borders add ~8 twips of extra height per edge. MS Office includes border thickness in height calculation, causing content to overflow by a few twips → **blank page 2**. WPS is more lenient and may absorb the overflow. | **Every cover wrapper table MUST explicitly set `borders: allNoBorders`** (all 6 border positions = NONE). Never rely on defaults. Define the `allNoBorders` constant and use it consistently. | +| `verticalAlign: "center"` or `"bottom"` in exact-height TableRow | WPS ignores vertical alignment in exact-height rows; content may clip or shift | Use `verticalAlign: "top"` + `spacing.before` to position content. Avoid `margins.top`/`margins.bottom` in exact-height cells — they reduce available height unpredictably across engines | +| `characterSpacing` (large values) | WPS renders differently from Word; letter spacing may collapse or expand | Keep `characterSpacing` ≤ 80; for cover English labels, test both renderers | +| `margins.top`/`margins.bottom` inside exact-height cells | MS Office and WPS calculate remaining height differently when cell margins are present | Use `spacing.before` on the first paragraph for vertical positioning; only use `margins.left`/`margins.right` | +| Complex nested Tables inside exact-height cells | WPS height calculation differs from Word; content may overflow or clip | Wrap everything in a single 16838 outer wrapper cell (R1 architecture). Nested tables inside are acceptable when the outer wrapper provides a safety net | +| Large font without explicit `spacing.line` | Paragraph inherits small line spacing from document default (e.g., 560tw for body); font taller than line height → top of characters clipped | Always set `spacing: { line: fontPt * 23, lineRule: "atLeast" }` on paragraphs with font size > body text | +| `ShadingType.SOLID` | WPS shows solid black instead of intended color | Always use `ShadingType.CLEAR` | +| OOXML raw XML for columns (`w:cols`) | WPS column rendering may differ | Use only when explicitly needed (A3 exam papers); test output | +| `titlePage: true` with complex headers/footers | WPS may not properly suppress first-page header/footer | Use separate sections instead of titlePage flag | +| Tab stops for alignment | WPS tab width may differ from Word | Use borderless Tables for alignment instead | + +### Features that are SAFE (consistent rendering) + +| Feature | Notes | +|---------|-------| +| Borderless Tables for layout | Both renderers handle well | +| `ShadingType.CLEAR` with fill color | Consistent | +| `rule: "exact"` on single-level TableRow | Works in both (avoid with nested Tables) | +| Paragraph borders (left, bottom, etc.) | Consistent | +| `spacing.before` / `spacing.after` | Consistent | +| Standard fonts (SimHei, SimSun, YaHei, TNR, Calibri) | Available on both platforms | +| `PageBreak` inside Paragraph | Consistent | +| Section breaks (`SectionType.NEXT_PAGE`) | Consistent | + +### Mandatory Compatibility Checks (Post-Generation) + +Add to quality self-check: +- [ ] No `ShadingType.SOLID` anywhere (search codebase) +- [ ] No `verticalAlign: "center"` or `"bottom"` in exact-height rows +- [ ] No tab-stop alignment for party info or data alignment (use Tables) +- [ ] Covers use the 16838 outer wrapper architecture (R1 pattern) with `spacing.before` for positioning; no `margins.top`/`margins.bottom` in exact-height cells +- [ ] **Cover section margin = `{ top: 0, bottom: 0, left: 0, right: 0 }`** — non-zero margins cause wrapper to shrink away from page edges +- [ ] **Cover wrapper row has `height: { value: 16838, rule: "exact" }`** — without this, content overflows or leaves whitespace +- [ ] **Cover is in a separate section from body content** — cover and body must not share a section +- [ ] **Cover wrapper table uses explicit `allNoBorders`** — never rely on default table borders (causes blank page 2 in MS Office) +- [ ] **No text-character decorative lines** (`───`, `━━━`, `═══`, `——————`) — use paragraph borders instead +- [ ] `characterSpacing` values ≤ 80 throughout +- [ ] TOC: follow `references/toc.md` checklist (heading style, TableOfContents element, PageBreak, post-processing script) +- [ ] All tables use `WidthType.PERCENTAGE` for column widths (WPS tblGrid bug; if DXA is unavoidable, set `columnWidths` explicitly) + +```js +// ✅ Correct — percentage widths, WPS-safe +new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + rows: [new TableRow({ children: [ + new TableCell({ width: { size: 30, type: WidthType.PERCENTAGE }, children: [...] }), + new TableCell({ width: { size: 70, type: WidthType.PERCENTAGE }, children: [...] }), + ]})], +}); + +// ❌ WRONG — DXA widths cause WPS tblGrid mismatch (all gridCol=100) +new TableCell({ width: { size: 3000, type: WidthType.DXA }, ... }) +``` + +--- + +## Universal Prohibitions + +These apply to ALL scenes. Scene files may add scene-specific prohibitions. + +1. **No outlines-only** — always produce a complete, finished document +2. **No chat-style output** — the document must not read like a conversation or explanation +3. **No fake TOC / page numbers / headers** — use proper docx-js structures +4. **No excessive blank lines** to pad layout +5. **No dirty formatting** — no stray annotations, template fragments, broken hyperlinks, garbled markers +6. **No sloppy placeholders** — "TBD", "omitted", "略", "to be refined" are forbidden; use proper `【】` placeholders +7. **No fabricated data** — do not invent statistics, citations, legal references, or facts to appear professional +8. **No inconsistent heading/numbering** — one numbering system per document, no level-skipping +9. **No Markdown artifacts** — no `#`, `**`, `-` list markers, `>` blockquotes, and **no Markdown table syntax** (`| col1 | col2 |`, `|---|---|`) in the final docx. Any tabular data MUST be rendered as a proper docx `Table` object — never as plain-text pipe-delimited lines. This applies to ALL scenes including exam paper data tables, report statistics, and academic result tables. +10. **No bullet-list documents** — body text must be proper paragraphs, not endless bullet points + +## Letter / Correspondence Format (Universal) + +When generating any letter-style document (invitation letter, thank-you letter, cover letter, recommendation letter, English essay in letter format, etc.), the following layout rules apply regardless of scene: + +1. **Complimentary close and sender name MUST be right-aligned** — e.g., "Yours sincerely,", "Best regards,", "Yours,", and the sender name below it must use `alignment: AlignmentType.RIGHT` +2. **Date** — if placed at the top of the letter, right-aligned; if at the bottom, right-aligned with the closing +3. **Salutation** ("Dear Mr. Smith," / "Dear Mike,") — left-aligned, followed by a blank line or `spacing.after` +4. **Body paragraphs** — left-aligned (English) or justified (CJK), with appropriate `spacing.after` between paragraphs + +```js +// ✅ Correct — closing and sender right-aligned +new Paragraph({ alignment: AlignmentType.RIGHT, spacing: { before: 400 }, + children: [new TextRun({ text: "Yours sincerely,", size: 24 })] }), +new Paragraph({ alignment: AlignmentType.RIGHT, + children: [new TextRun({ text: "Li Hua", size: 24 })] }), + +// ❌ WRONG — closing left-aligned (default) +new Paragraph({ + children: [new TextRun({ text: "Yours sincerely," })] }), +``` + +## Quality Self-Check (Universal) + +→ See **SKILL.md § Post-Generation — Two-Layer Verification** for the complete checklist. + +Scene files add scene-specific checks on top of that universal checklist. + +## Execution Priority + +When rules conflict, follow this precedence (highest first): + +1. **User-provided template or explicit instructions** — always override defaults +2. **Scene-specific rules** — override common rules and design-system defaults +3. **Common rules** (this file) — override design-system aesthetic defaults +4. **Design-system defaults** — baseline aesthetics + +## Cover Recipes + +See `references/design-system.md` for the 7 validated cover recipes (R1–R7) and 14 color palettes. + +Cover recipe selection: `selectCoverRecipe(docType, industry, titleLength)` — defined in `references/design-system.md` (authoritative source). + +--- + +## Cover Title Layout Rules (Mandatory) + +These rules apply to ALL cover recipes (R1–R7). They prevent the most common cover quality issues: title overflow, content spilling to page 2, and mid-word line breaks. + +### Rule 1: Always use `calcTitleLayout()` + +Every cover MUST call `calcTitleLayout(title, availableWidth)` from `design-system.md` to determine: +- **Font size** (dynamically calculated, never hardcoded above 40pt) +- **Line breaks** (semantically split, never mid-word) + +**Forbidden:** Passing the full title as a single long TextRun and letting Word auto-wrap. This causes uncontrolled line breaks at arbitrary character positions. + +### Rule 2: No single-character orphan lines + +If the last line of a title contains only 1–2 characters, merge it into the previous line. The `splitTitleLines()` function handles this automatically. + +### Rule 3: No mid-word breaks for CJK text + +Line breaks must occur at semantic boundaries: after particles (e.g., de/yu/he/ji/zhi), punctuation, connectors, spaces, or underscores. Never split a compound term (e.g., a 4-character term like a management specification must not be split into 3+1 characters). + +For mixed Chinese+English titles (e.g., "基于Transformer架构的..."), use `estimateTextWidth()` instead of character count for line break calculation. Chinese characters are ~2× wider than English characters at the same font size. + +### Rule 4: Maximum 3 title lines on cover + +Cover titles must not exceed 3 lines. If the title is too long, reduce font size (down to minimum 24pt) before adding more lines. If it still exceeds 3 lines at 24pt, force 3 lines with longer line lengths. + +### Rule 5: Always use `calcCoverSpacing()` for whitespace + +Spacing values (`spacing.before`) in cover elements must be dynamically calculated, not hardcoded. Fixed values like `before: 4500` assume a specific title length and will cause overflow with longer titles. + +### Rule 6: Cover height budget validation + +Before generating, verify that total content height stays within 15638 twips (16838 page height minus 1200 twips safety margin — MS Office renders large fonts taller than calculated). Each recipe in `design-system.md` includes height budget annotations — verify during generation. + +### Rule 7: R5 meta info table (academic covers) + +Academic cover meta info must use a 2-column table with **percentage widths only** (NOT DXA — WPS breaks with DXA widths): +- **Table width:** adaptive 55–75% of page, calculated by `calcR5MetaLayout()` in `design-system.md`. Table is centered via `alignment: CENTER`. +- **Label column:** adaptive 25–45% of table width, **LEFT aligned**, plain text label + ":". NO full-width space padding, NO right-alignment, NO distributed alignment. +- **Value column:** remaining percentage, **LEFT aligned**, `bottom border single sz=4` = fixed-length underline (same length for all rows regardless of value text length). +- **Label column borders:** none (NO bottom border on label cells). +- ⚠️ Do NOT use DXA widths, full-width space padding (`\u3000`), spacer columns, or tab stops — these render inconsistently between MS Office and WPS. + +### Rule 8: Large font paragraphs must set explicit line spacing + +When a paragraph uses a font size larger than the document body text (e.g., cover titles at 36pt+), it **MUST** set explicit `spacing.line` to prevent clipping. Without it, the paragraph inherits the document/style default line spacing (often 560 twips for body text), which is smaller than the font height → the top of characters gets clipped. + +**Formula:** `spacing.line = Math.ceil(fontPt * 23)` with `lineRule: "atLeast"` + +**Example:** A 36pt title needs `spacing: { line: 828, lineRule: "atLeast" }`. Without this, the inherited `line=560` clips the top 160 twips of the text. + +This applies to ALL large-font paragraphs (cover titles, chapter headings, decorative text), not just covers. + +### Rule 9: Every TextRun on a colored background MUST set explicit `color` + +⚠️ **CRITICAL:** When a TextRun is inside a cell/area with a dark or colored background (shading), it **MUST** explicitly set the `color` property. Omitting `color` defaults to black (`#000000`), which is invisible on dark backgrounds. + +**Common mistake:** Subtitle or meta text on R1/R2/R4 dark cover blocks without `color` → appears as invisible black text on dark bg. + +**Rule:** For any TextRun inside a shaded cell: +- Use `P.cover.titleColor` for title text +- Use `P.cover.subtitleColor` for subtitle text +- Use `P.cover.metaColor` for meta info text +- Use `P.cover.footerColor` for footer text +- **NEVER** rely on default color when background is not white + +### Rule 10: Page number API nesting and 3-section numbering + +⚠️ **CRITICAL:** Page number settings MUST be nested inside `page.pageNumbers`: + +```js +// ❌ WRONG — docx-js ignores top-level pageNumberStart/pageNumberFormatType +properties: { pageNumberStart: 1, pageNumberFormatType: NumberFormat.DECIMAL } + +// ✅ CORRECT +properties: { page: { pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL } } } +``` + +**Standard page numbering (5-zone convention):** + +All multi-section documents MUST follow this five-zone page numbering scheme unless the user explicitly requests otherwise. + +| Zone | Section | pageNumbers | Footer instrText | Notes | +|------|---------|-------------|-----------------|-------| +| 1. Cover | Title page | None (no footer) | — | Always logical page 1, but number is **hidden** | +| 2. Front matter | Abstract, TOC, Preface | `{ start: 1, formatType: UPPER_ROMAN }` | `PAGE \* ROMAN \* MERGEFORMAT` | Separate Roman numeral sequence (i, ii, iii…) | +| 3. Body | Main content | `{ start: 1, formatType: DECIMAL }` | `PAGE \* arabic \* MERGEFORMAT` | **Resets to 1** | +| 4. Appendix | Appendices (A, B, C…) | Continues body (no reset) | Same as body | No section break needed unless different headers required | +| 5. References | Bibliography | Continues body (no reset) | Same as body | If body ends on p.42, references continue from p.43 | + +**Key rules:** +0. **NEVER use "Page X of Y" denominator format.** Footer must show only the current page number (e.g., `1`, `2`, `iii`). Do NOT display total page count. No `Page 3 of 12`, no `3 / 12`, no `第3页/共12页`. Just the bare number. `PageNumber.TOTAL_PAGES` / `NUMPAGES` is **FORBIDDEN** in footers. +1. **Cover is always page 1 internally** but the page number is never displayed. Suppress footer in cover section. +2. **Front matter uses independent Roman numerals** starting at `i`. This sequence is separate from the body. +3. **Body resets to Arabic 1.** The first page of main content is always page `1`. +4. **Appendix and references continue the body sequence.** No reset between body → appendix → references. +5. **Documents without front matter** skip zone 2 (cover hidden, body starts at Arabic 1). +6. **Documents without cover** start body (or front matter) at page 1 directly. +7. **Short documents (≤3 pages):** simple Arabic 1, 2, 3 throughout, no cover/frontmatter distinction. +8. **Single-page documents** (certificates, letters): no page numbering at all. + +**3-section docx-js implementation (for documents with TOC):** + +At minimum, implement zones 1–3 as separate docx sections: + +```js +// Section 1: Cover — no page number +properties: { page: { /* no pageNumbers */ } } +// No footer children, or empty footer + +// Section 2: Front matter — Roman numerals +properties: { page: { pageNumbers: { start: 1, formatType: NumberFormat.UPPER_ROMAN } } } +// Footer: PAGE \* ROMAN \* MERGEFORMAT + +// Section 3: Body — Arabic, reset to 1 +properties: { page: { pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL } } } +// Footer: PAGE \* arabic \* MERGEFORMAT + +// Appendix and References: same section as body (continues numbering) +// Only create a new section if different header/footer content is needed +``` + +**Post-processing required** (WPS compatibility): +1. Remove empty `` from cover section XML +2. Patch footer instrText: replace bare `PAGE` with format-specific `PAGE \* ROMAN` or `PAGE \* arabic` + +See `toc.md` § Page Number API for full details. diff --git a/skills/docx/references/decorations.md b/skills/docx/references/decorations.md new file mode 100755 index 0000000..e2783aa --- /dev/null +++ b/skills/docx/references/decorations.md @@ -0,0 +1,538 @@ +## Geometric Decoration System — Pure docx-js Decorations + +### Design Philosophy + +Uses only docx-js native capabilities for visual decoration — no external tools (like Playwright screenshots). Suitable for covers, chapter separators, page background enhancement. + +**When to fall back to Playwright?** +Only when gradients, complex illustrations, or brand visuals are needed that pure OOXML cannot express. Default: prefer native solutions below. + +### Decoration Element Library + +#### 1. Color Strip — Table Simulation + +Single-row single-column borderless table + background color to create horizontal color strips. + +```js +function colorStrip(color, height = 80) { + return new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { top: NB, bottom: NB, left: NB, right: NB, + insideHorizontal: NB, insideVertical: NB }, + rows: [new TableRow({ + height: { value: height, rule: "exact" }, + children: [new TableCell({ + shading: { type: ShadingType.CLEAR, fill: color.replace("#", "") }, + borders: { top: NB, bottom: NB, left: NB, right: NB }, + children: [new Paragraph({ children: [] })], + })], + })], + }); +} + +// ══════════════════════════════════════════════════════════════ +// R6 — Editorial Warm (minimal, warm white bg, no decorations) +// ══════════════════════════════════════════════════════════════ +// Suitable for: lesson plans (non-STEM), cultural/creative, newsletters, +// event planning, internal reports, light-weight documents +// NOT for: formal business, consulting, finance, government, academic +// Title constraint: single line only (≤20 chars). Longer titles → route to R1. +// +// Structure: 2-row wrapper table (no border, warm bg shading) +// Row 1 (content): category → title → subtitle → fields +// Row 2 (footer): left English title + right label +// All spacing via paragraph indent (WPS safe, no cell margins). + +function buildCoverR6(config) { + const P = config.palette; + const PAD_L = 1300, PAD_R = 1100; + const ind = { left: PAD_L, right: PAD_R }; + const FOOTER_H = 900; + const CONTENT_H = 16838 - FOOTER_H; + const shading = { fill: P.bg || "F7F7F5", type: ShadingType.CLEAR }; + + // ⚠️ R6 uses a simplified title layout: prefer single line, shrink font to fit + const availW = 11906 - PAD_L - PAD_R; + const { titlePt, titleLines } = calcTitleLayoutR6(config.title, availW, 36, 22); + const titleSize = titlePt * 2; + const lineH = Math.ceil(titlePt * 23 * 1.3); + + // Dynamic top spacing + const titleH = titleLines.length * (titleSize * 10 + 200); + const categoryH = 22 * 10 + 900; + const subtitleH = config.subtitle ? (28 * 10 + 1200) : 0; + const fieldsH = (config.metaLines || []).length * (24 * 10 + 100); + const contentH = categoryH + titleH + subtitleH + fieldsH; + const remaining = Math.max(CONTENT_H - 1200 - contentH, 400); + const topSpacing = Math.floor(remaining * 0.55); + + const children = []; + + // 1. Top spacer (dynamic) + children.push(new Paragraph({ indent: ind, spacing: { before: topSpacing } })); + + // 2. Category label (small, wide letter-spacing) + if (config.englishLabel) { + children.push(new Paragraph({ + indent: ind, spacing: { after: 900 }, + children: [new TextRun({ + text: config.englishLabel, size: 22, + color: P.cover.metaColor || "9A9A9A", + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, + characterSpacing: 60, + })], + })); + } + + // 3. Title (single line preferred, dynamic font size) + for (let i = 0; i < titleLines.length; i++) { + children.push(new Paragraph({ + indent: ind, + spacing: { after: i < titleLines.length - 1 ? 60 : 300, line: lineH, lineRule: "atLeast" }, + children: [new TextRun({ + text: titleLines[i], size: titleSize, + color: P.cover.titleColor || "2C2C2C", + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, + characterSpacing: 30, + })], + })); + } + + // 4. Subtitle + if (config.subtitle) { + children.push(new Paragraph({ + indent: ind, spacing: { after: 1200 }, + children: [new TextRun({ + text: config.subtitle, size: 28, + color: P.cover.subtitleColor || "6B6B6B", + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, + characterSpacing: 15, + })], + })); + } + + // 5. Meta fields (tab-aligned label + value) + for (const line of (config.metaLines || [])) { + // Expect "label:value" format or plain text + const sep = line.indexOf(":") !== -1 ? ":" : (line.indexOf(":") !== -1 ? ":" : null); + const label = sep ? line.split(sep)[0].trim() : line; + const value = sep ? line.split(sep).slice(1).join(sep).trim() : ""; + children.push(new Paragraph({ + indent: ind, spacing: { after: 100 }, + tabStops: [{ type: TabStopType.LEFT, position: PAD_L + 1600 }], + children: [ + new TextRun({ text: label, size: 22, color: P.cover.metaColor || "9A9A9A", + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, characterSpacing: 20 }), + ...(value ? [ + new TextRun({ text: "\t" }), + new TextRun({ text: value, size: 24, color: P.cover.subtitleColor || "6B6B6B", + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, characterSpacing: 8 }), + ] : []), + ], + })); + } + + // 6. Footer (2-column borderless table) + const footerLeft = config.footerLeft || ""; + const footerRight = config.footerRight || ""; + // Adaptive font size for long English footer text + const flSize = footerLeft.length > 60 ? 14 : (footerLeft.length > 40 ? 16 : 18); + const flSpacing = footerLeft.length > 60 ? 5 : (footerLeft.length > 40 ? 10 : 20); + + const footerTable = new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, borders: allNoBorders, + rows: [new TableRow({ + children: [ + new TableCell({ + width: { size: 70, type: WidthType.PERCENTAGE }, borders: noBorders, shading, + children: [new Paragraph({ + indent: { left: PAD_L }, + children: [new TextRun({ text: footerLeft, size: flSize, + color: P.cover.footerColor || "9A9A9A", + font: { ascii: "Calibri" }, characterSpacing: flSpacing })], + })], + }), + new TableCell({ + width: { size: 30, type: WidthType.PERCENTAGE }, borders: noBorders, shading, + children: [new Paragraph({ + alignment: AlignmentType.RIGHT, indent: { right: PAD_R }, + children: [new TextRun({ text: footerRight, size: 18, + color: P.cover.footerColor || "9A9A9A", + font: { ascii: "Calibri" }, characterSpacing: 20 })], + })], + }), + ], + })], + }); + + // 7. 2-row wrapper (content + footer) + return [new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, borders: allNoBorders, + rows: [ + new TableRow({ + height: { value: CONTENT_H, rule: "exact" }, + children: [new TableCell({ + shading, borders: noBorders, + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + verticalAlign: VerticalAlign.TOP, + children, + })], + }), + new TableRow({ + height: { value: FOOTER_H, rule: "exact" }, + children: [new TableCell({ + shading, borders: noBorders, + margins: { top: 0, bottom: 0, left: 0, right: 0 }, + verticalAlign: VerticalAlign.CENTER, + children: [footerTable], + })], + }), + ], + })]; +} + +// R6 title layout: prefer FEWER lines over larger font size (single line best) +function calcTitleLayoutR6(title, availableWidthTw, preferredPt, minPt) { + const step = 2; + // Try to fit in 1 line (shrink font if needed) + for (let pt = preferredPt; pt >= minPt; pt -= step) { + const charWidthTw = pt * 23 * 0.5; // CJK ~50% em width + const charsPerLine = Math.floor(availableWidthTw / charWidthTw); + if (title.length <= charsPerLine) return { titlePt: pt, titleLines: [title] }; + } + // Can't fit in 1 line, try 2 lines at largest possible font + for (let pt = preferredPt; pt >= minPt; pt -= step) { + const charWidthTw = pt * 23 * 0.5; + const charsPerLine = Math.floor(availableWidthTw / charWidthTw); + const lines = splitTitleLines(title, charsPerLine); + if (lines.length <= 2) return { titlePt: pt, titleLines: lines }; + } + // Fallback: minPt, up to 3 lines + const charWidthTw = minPt * 23 * 0.5; + const charsPerLine = Math.floor(availableWidthTw / charWidthTw); + return { titlePt: minPt, titleLines: splitTitleLines(title, charsPerLine) }; +} + +// Usage: cover top decoration +// children: [colorStrip(P.accent, 120), ...] +``` + +#### 2. Side Ribbon + +Uses left border to create vertical ribbon effect. + +```js +function sideRibbon(content, color, width = 14) { + return new Paragraph({ + border: { + left: { style: BorderStyle.SINGLE, size: width, color: color.replace("#", ""), space: 12 }, + }, + indent: { left: 240 }, + spacing: { before: 100, after: 100 }, + children: content, + }); +} + +// Usage: emphasis quotes, chapter tips +// sideRibbon([new TextRun({ text: "Key Insight", bold: true })], P.accent) +``` + +#### 3. Border Compositions + +```js +// Top thick line + bottom thin line — title area frame +function frameTitle(titleRuns) { + return new Paragraph({ + border: { + top: { style: BorderStyle.SINGLE, size: 18, color: c(P.accent) }, + bottom: { style: BorderStyle.SINGLE, size: 4, color: c(P.accent) }, + }, + spacing: { before: 400, after: 200 }, + alignment: AlignmentType.CENTER, + children: titleRuns, + }); +} + +// L-shape border — left + bottom +function lShapeBorder(content) { + return new Paragraph({ + border: { + left: { style: BorderStyle.SINGLE, size: 12, color: c(P.accent), space: 10 }, + bottom: { style: BorderStyle.SINGLE, size: 12, color: c(P.accent) }, + }, + indent: { left: 300 }, + spacing: { before: 200, after: 300 }, + children: content, + }); +} + +// Double-line frame — top and bottom double lines +function doubleLine(content) { + return new Paragraph({ + border: { + top: { style: BorderStyle.DOUBLE, size: 6, color: c(P.accent) }, + bottom: { style: BorderStyle.DOUBLE, size: 6, color: c(P.accent) }, + }, + spacing: { before: 200, after: 200 }, + alignment: AlignmentType.CENTER, + children: content, + }); +} +``` + +#### 4. Gradient Simulation + +Multiple narrow color strips to simulate gradient effect. + +```js +function gradientStrip(startColor, endColor, steps = 5, totalHeight = 200) { + const rows = []; + const h = Math.floor(totalHeight / steps); + for (let i = 0; i < steps; i++) { + const ratio = i / (steps - 1); + const blended = blendColors(startColor, endColor, ratio); + rows.push(new TableRow({ + height: { value: h, rule: "exact" }, + children: [new TableCell({ + shading: { type: ShadingType.CLEAR, fill: blended }, + borders: { top: NB, bottom: NB, left: NB, right: NB }, + children: [new Paragraph({ children: [] })], + })], + })); + } + return new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { top: NB, bottom: NB, left: NB, right: NB, + insideHorizontal: NB, insideVertical: NB }, + rows, + }); +} + +function blendColors(hex1, hex2, ratio) { + const r1 = parseInt(hex1.slice(1, 3), 16), g1 = parseInt(hex1.slice(3, 5), 16), b1 = parseInt(hex1.slice(5, 7), 16); + const r2 = parseInt(hex2.slice(1, 3), 16), g2 = parseInt(hex2.slice(3, 5), 16), b2 = parseInt(hex2.slice(5, 7), 16); + const r = Math.round(r1 + (r2 - r1) * ratio), g = Math.round(g1 + (g2 - g1) * ratio), b = Math.round(b1 + (b2 - b1) * ratio); + return `${r.toString(16).padStart(2,"0")}${g.toString(16).padStart(2,"0")}${b.toString(16).padStart(2,"0")}`; +} +``` + +#### 5. Symbol Ornaments + +```js +// Section divider line — for chapter separation +function ornamentDivider(symbol = "◆", count = 3) { + const ornament = Array(count).fill(symbol).join(" "); + return new Paragraph({ + alignment: AlignmentType.CENTER, + spacing: { before: 400, after: 400 }, + children: [new TextRun({ text: ornament, size: 20, color: c(P.accent) })], + }); +} + +// Common decoration symbols +// ◆ ◇ ● ○ ★ ☆ ■ □ ▲ △ ─ ━ ═ ║ ╔ ╗ ╚ ╝ +// Ornamental: ❧ ❦ ✦ ✧ ✿ ❀ ❁ ※ +``` + +#### 6. Info Card — Table Implementation + +```js +function infoCard(title, items, accentColor) { + const ac = accentColor.replace("#", ""); + const headerRow = new TableRow({ + children: [new TableCell({ + columnSpan: 2, + shading: { type: ShadingType.CLEAR, fill: ac }, + margins: { top: 80, bottom: 80, left: 160, right: 160 }, + borders: { top: NB, bottom: NB, left: NB, right: NB }, + children: [new Paragraph({ + children: [new TextRun({ text: title, bold: true, size: 24, color: "FFFFFF" })], + })], + })], + }); + + const dataRows = items.map(([label, value]) => new TableRow({ + children: [ + new TableCell({ + width: { size: 30, type: WidthType.PERCENTAGE }, + margins: { top: 60, bottom: 60, left: 160, right: 80 }, + shading: { type: ShadingType.CLEAR, fill: "F8F9FA" }, + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" }, + top: NB, left: NB, right: NB }, + children: [new Paragraph({ children: [new TextRun({ text: label, size: 21, color: "666666" })] })], + }), + new TableCell({ + margins: { top: 60, bottom: 60, left: 80, right: 160 }, + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "E0E0E0" }, + top: NB, left: NB, right: NB }, + children: [new Paragraph({ children: [new TextRun({ text: value, size: 21 })] })], + }), + ], + })); + + return new Table({ + width: { size: 80, type: WidthType.PERCENTAGE }, + alignment: AlignmentType.CENTER, + borders: { top: NB, bottom: NB, left: NB, right: NB, + insideHorizontal: NB, insideVertical: NB }, + rows: [headerRow, ...dataRows], + }); +} +``` + + +// R7 — Swiss Tech Minimalist (slate grey bg, Klein blue accent, asymmetric layout) +// Suitable for: cultural/creative research, trend reports, brand strategy, design deliverables +// Palette: ST-1 (exclusive) +// Layout: left-aligned title (upper 20%), right-shifted subtitle with top rule, +// right-aligned info block with accent right border, Swiss cross anchor +// Key features: ■ square accent dot, open-frame tables, large whitespace +// +// ⚠️ MANDATORY: All cover non-negotiables apply (margin=0, 16838 exact, allNoBorders) +// ⚠️ Title uses calcTitleLayout() with maxPt=36 (not 40 — R7 uses lighter visual weight) + +function buildCoverR7(config) { + const P = palettes[config.palette || "ST-1"]; + const C = P.cover; + const padL = 600; + + // Title layout — R7 uses 36pt max (lighter than R1-R4's 40pt) + const availW = 11906 - padL - 600; + const { titlePt, titleLines } = calcTitleLayout(config.title, availW, 36, 24); + const titleSize = titlePt * 2; + const lineH = Math.ceil(titlePt * 23); + + // Dynamic spacing based on title lines + const topSpacer = titleLines.length <= 2 ? 1200 : 800; + const subtitleSpacer = titleLines.length <= 2 ? 1400 : 800; + const infoSpacer = titleLines.length <= 2 ? 2200 : 1200; + + const children = []; + + // 1. Swiss cross anchor — top-left decorative element + children.push(new Paragraph({ + spacing: { before: 600 }, + indent: { left: padL }, + children: [new TextRun({ + text: "\uFF0B", // + fullwidth plus + size: 40, bold: true, color: C.titleColor, + font: { ascii: "Arial", eastAsia: "SimHei" }, + })], + })); + + // 2. Top spacer + children.push(new Paragraph({ spacing: { before: topSpacer } })); + + // 3. Title lines — left-aligned, last line has accent ■ + titleLines.forEach((line, i) => { + const isLast = i === titleLines.length - 1; + const runs = [new TextRun({ + text: line, size: titleSize, color: C.titleColor, + font: { ascii: "Arial", eastAsia: "Noto Sans SC" }, + })]; + if (isLast) { + runs.push(new TextRun({ + text: " \u25A0", // ■ black square + size: 24, color: P.accent, + font: { ascii: "Arial" }, + })); + } + children.push(new Paragraph({ + indent: { left: padL }, + spacing: { after: isLast ? 200 : 80, line: lineH, lineRule: "atLeast" }, + children: runs, + })); + }); + + // 4. Subtitle spacer + children.push(new Paragraph({ spacing: { before: subtitleSpacer } })); + + // 5. Subtitle — right-shifted, top border rule, wide character spacing + if (config.subtitle) { + children.push(new Paragraph({ + indent: { left: 3800, right: 600 }, + border: { top: { style: BorderStyle.SINGLE, size: 2, color: C.titleColor, space: 14 } }, + spacing: { after: 200 }, + children: [new TextRun({ + text: config.subtitle, size: 26, color: C.subtitleColor, + font: { ascii: "Arial", eastAsia: "Noto Sans SC" }, + characterSpacing: 40, + })], + })); + } + + // 6. Decorative horizontal line + children.push(new Paragraph({ + spacing: { before: 600 }, + border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "C8D0DC", space: 0 } }, + })); + + // 7. Info spacer + children.push(new Paragraph({ spacing: { before: infoSpacer } })); + + // 8. Info footer — right-aligned, 4 label+value pairs, accent right border + // Standard fields: ORGANIZATION, RESPONSIBILITY, REPORT NUMBER, DATE & EDITION + const metaEntries = config.metaEntries || [ + { label: "ORGANIZATION", value: config.organization || "" }, + { label: "RESPONSIBILITY", value: config.responsibility || "" }, + { label: "REPORT NUMBER", value: config.reportNumber || "" }, + { label: "DATE & EDITION", value: config.dateEdition || "" }, + ]; + + for (const entry of metaEntries) { + // Label — 7pt uppercase English + children.push(new Paragraph({ + alignment: AlignmentType.RIGHT, + indent: { right: 800 }, + border: { right: { style: BorderStyle.SINGLE, size: 12, color: P.accent, space: 16 } }, + spacing: { after: 20 }, + children: [new TextRun({ + text: entry.label, size: 14, color: C.metaColor, + font: { ascii: "Arial" }, + characterSpacing: 20, + })], + })); + // Value — 11pt bold + children.push(new Paragraph({ + alignment: AlignmentType.RIGHT, + indent: { right: 800 }, + border: { right: { style: BorderStyle.SINGLE, size: 12, color: P.accent, space: 16 } }, + spacing: { after: 280 }, + children: [new TextRun({ + text: entry.value, size: 22, bold: true, color: C.titleColor, + font: { ascii: "Arial", eastAsia: "Noto Sans SC" }, + })], + })); + } + + // Wrap in 16838 exact wrapper table + return [new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: 16838, rule: "exact" }, + children: [new TableCell({ + shading: { type: ShadingType.CLEAR, fill: P.bg }, + borders: noBorders, + verticalAlign: VerticalAlign.TOP, + children, + })], + })], + })]; +} + +### Decoration Usage Scenarios + +| Scenario | Recommended Decoration | Combination | +|------|----------|----------| +| Report cover | Color strip + L-frame border | Top strip → Title area → L-frame author info | +| Proposal cover | Gradient simulation + double-line frame | Gradient bg → Double-line title | +| Chapter separator | Symbol ornament + side ribbon | Symbol divider → New chapter title with ribbon | +| Summary card | Info card | Standalone card displaying key metrics | +| Academic cover | Color strip + info table | Top strip → School name → Title → Info table | + +--- + diff --git a/skills/docx/references/design-system.md b/skills/docx/references/design-system.md new file mode 100755 index 0000000..c2f3e0b --- /dev/null +++ b/skills/docx/references/design-system.md @@ -0,0 +1,1797 @@ +# Design System — DOCX Skill + +## Color Philosophy + +GLM uses a **mood-driven dynamic color system** instead of fixed named palettes. Colors are constructed from three dimensions: + +### Three Dimensions of Document Color + +| Dimension | Description | Range | +|-----------|-------------|-------| +| **Temperature** | Warm ↔ Cool | Warm (consulting, education) ↔ Cool (tech, medical) | +| **Weight** | Light ↔ Heavy | Light (resume, proposal) ↔ Heavy (legal, academic) | +| **Energy** | Calm ↔ Active | Calm (official, contract) ↔ Active (report, presentation) | + +### Color Token System + +Every document uses 5 color tokens. These are **computed** based on the document's mood, not selected from a fixed list. + +| Token | Role | Guidance | +|-------|------|----------| +| `primary` | Headings, cover title | Dark, authoritative. Derived from Temperature + Weight. | +| `body` | Body text | Near-black with subtle warmth/coolness. Always high contrast. | +| `secondary` | Captions, footnotes | Mid-tone gray. Legible but visually recessive. | +| `accent` | Table headers, lines, links | The "personality" color. Reflects document Energy. | +| `surface` | Table alternating rows, card backgrounds | Very light tint of accent or neutral. | + +### Mood Recipes + +Instead of 10 fixed palettes, combine dimensions to generate colors dynamically: + +**Cool + Heavy + Calm** → Deep Sea Academic (Academic / Research) +```js +const academic = { + primary: "#162032", body: "#1C2A3D", secondary: "#5B6B7D", + accent: "#8B7E5A", surface: "#F5F7FA" +}; +``` + +**Warm + Heavy + Calm** → Legal Wood (Legal / Compliance) +```js +const legal = { + primary: "#28201C", body: "#36302C", secondary: "#6E6560", + accent: "#7A5C3A", surface: "#FBF9F7" +}; +``` + +**Cool + Light + Active** → Dawn Mist Tech (Tech / Digital) +```js +const tech = { + primary: "#0A1628", body: "#1A2B40", secondary: "#6878A0", + accent: "#5B8DB8", surface: "#F4F8FC" +}; +``` + +**Warm + Light + Active** → Warm Sun (Education / Training) +```js +const education = { + primary: "#2A3518", body: "#384228", secondary: "#6B8040", + accent: "#D4A030", surface: "#F8FAF4" +}; +``` + +**Neutral + Medium + Calm** → Plain Paper (Default / General) +```js +const general = { + primary: "#101820", body: "#182030", secondary: "#506070", + accent: "#8090A0", surface: "#F2F4F6" +}; +``` + +**Warm + Medium + Calm** → Terracotta (Consulting / Architecture) +```js +const consulting = { + primary: "#241E1A", body: "#3A3430", secondary: "#68605A", + accent: "#B08050", surface: "#FDFBF9" +}; +``` + +**Cool + Medium + Active** → Mint Medical (Medical / Clinical) +```js +const medical = { + primary: "#0E2030", body: "#1E2E40", secondary: "#4A6580", + accent: "#3888A8", surface: "#F0F6FA" +}; +``` + +**Neutral + Light + Calm** → White Porcelain (Product Manuals / Minimalist) +```js +const minimal = { + primary: "#303030", body: "#484848", secondary: "#808080", + accent: "#B89870", surface: "#FAFAF8" +}; +``` + +**Cool + Light + Active (Gradient)** → Lapis Tech (Tech / AI / Innovation) +```js +const liuliTech = { + primary: "#1A1F36", body: "#000000", secondary: "#5A6080", + accent: "#667eea", surface: "#F8F9FF", + gradient: ["#667eea", "#764ba2"], // Purple-blue gradient (blendColors 5-step) +}; +``` + +**Cool + Heavy + Active (Gradient)** → Deep Sea Blue-Gold (Finance / Investment / Premium) +```js +const deepBlueGold = { + primary: "#0F2027", body: "#000000", secondary: "#4A6575", + accent: "#D4AF37", surface: "#F5F7FA", + gradient: ["#0F2027", "#203A43", "#2C5364"], // 3-step deep sea blue gradient +}; +``` + +**Warm + Light + Active (Gradient)** → Mint Dawn (Education / Health / Green) +```js +const mintMorning = { + primary: "#1A3A3A", body: "#000000", secondary: "#507070", + accent: "#3CB4A0", surface: "#F0FFFE", + gradient: ["#3CB4A0", "#a8edea"], // Mint green gradient +}; +``` + +**Neutral + Medium + Active** → Graphite Orange (Professional but Energetic) +```js +const graphiteOrange = { + primary: "#2C3E50", body: "#000000", secondary: "#607080", + accent: "#E67E22", surface: "#FDF8F3", +}; +``` + +### Scene → Mood Mapping + +| Scene Keywords | Temperature | Weight | Energy | Recipe | +|----------------|-------------|--------|--------|--------| +| thesis, academic | Cool | Heavy | Calm | Deep Sea Academic | +| report (general) | Neutral | Medium | Calm | Plain Paper | +| report (consulting) | Warm | Medium | Calm | Terracotta | +| report (tech) | Cool | Light | Active | Dawn Mist Tech | +| contract, agreement, legal | Warm | Heavy | Calm | Legal Wood | +| resume, CV | Neutral | Light | Calm | White Porcelain (preferred) or Dawn Mist Tech | +| exam, test | — | — | — | **Pure B&W** | +| official document | — | — | — | **Pure B&W** | +| AI, tech | Cool | Light | Active | Dawn Mist Tech | +| medical | Cool | Medium | Active | Mint Medical | +| environmental, sustainability | Warm | Light | Active | Warm Sun | +| lesson plan (STEM / science / tech) | Cool | Light | Active | Dawn Mist Tech | +| lesson plan (arts / music / PE) | Neutral | Medium | Active | Graphite Orange | +| lesson plan (general education) | Neutral | Medium | Calm | Plain Paper | +| education report (not lesson plan) | Warm | Light | Active | Mint Dawn | +| product manual | Neutral | Light | Calm | White Porcelain | +| tech, AI, internet, innovation | Cool | Light | Active | Lapis Tech | +| finance, investment, premium | Cool | Heavy | Active | Deep Sea Blue-Gold | +| health, green | Warm | Light | Active | Mint Dawn | +| energetic, vibrant | Neutral | Medium | Active | Graphite Orange | +| essay, composition, self-evaluation, review/reflection, letter (non-business), speech, application, proposal letter | — | — | — | **Pure B&W** | +| _(no match)_ | Neutral | Medium | Calm | Plain Paper | + +### Visual Profile Color Guidance + +For **Profile B (Visual)** scenes — resume, copywriting, and other non-formal documents — prefer **warm neutral** tones aligned with the "Invisible Precision" design philosophy: + +- **Body text**: Use warm dark neutrals (`#37352F`, `#303030`, `#3A3430`) instead of cool blue-grays. This reduces eye strain. +- **Surface/background**: Use warm near-white (`#F7F7F7`, `#FAFAF8`, `#FBF9F7`) instead of cool tints. +- **Accent colors**: Use sparingly and only for functional differentiation (section headers, key metrics, links). 95% of the document should remain monochromatic (black, white, gray). +- **Tables**: Prefer the **Zebra Stripe** style (see Table Styles §2) — hierarchy through background contrast with minimal borders. Fallback: Horizontal-Only. + +This does NOT apply to Profile A (Formal) scenes (report, academic, contract, official-doc, exam), which must retain pure black `"000000"` body text per regulatory standards. + +### Custom Color Generation + +When the pre-defined recipes don't fit, construct colors using these rules: + +1. **primary**: Start from `hsl(hue, 25-40%, 10-18%)` — dark, desaturated +2. **body**: primary lightened 5-8% — readable dark +3. **secondary**: primary lightened 30-40% — clearly subordinate +4. **accent**: Choose a hue reflecting the domain, `hsl(domainHue, 30-50%, 45-55%)` +5. **surface**: accent desaturated to 5-10%, lightened to 96-98% + +**Contrast check**: body text on white must achieve WCAG AA (≥4.5:1). All recipes above pass this. + +--- + +## Font Specifications + +### Chinese Fonts + +| Usage | Font | Fallback | +|-------|------|----------| +| Headings | SimHei (SimHei) | Microsoft YaHei Bold | +| Body | Microsoft YaHei (YaHei) | SimSun (SimSun) | +| Academic body | SimSun (SimSun) | — | +| Academic headings | SimHei (SimHei) | — | +| Official doc body | FangSong (FangSong) | FangSong_GB2312 | +| Official doc title | STXiaoBiaoSong (XiaoBiaoSong) | SimSun Bold | + +### English Fonts + +For **English documents** (document language = English): + +| Usage | Font | Fallback | +|-------|------|----------| +| Headings | Times New Roman Bold | Arial Bold | +| Body | Times New Roman | Calibri | +| Academic | Times New Roman | — | + +For **English text within Chinese documents**, use the Chinese document's ascii font (Calibri by default). + +### Font Paths (for matplotlib / image generation) + +```python +# macOS +SIMHEI = "/System/Library/Fonts/Supplemental/SimHei.ttf" +# Linux +SIMHEI = "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc" +# Fallback: download SimHei.ttf to working directory +``` + +### docx-js Font Configuration + +```js +// In Document styles.default +styles: { + default: { + document: { + run: { + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, + size: 24, // Xiao Si 小四 12pt + color: palette.body, + }, + paragraph: { + spacing: { line: 312 }, // 1.3x mandatory + }, + }, + heading1: { + run: { + font: { ascii: "Calibri", eastAsia: "SimHei" }, + size: 32, // San Hao 三号 16pt + bold: true, + color: palette.primary, + }, + }, + heading2: { + run: { + font: { ascii: "Calibri", eastAsia: "SimHei" }, + size: 28, // Si Hao 四号 14pt + bold: true, + color: palette.primary, + }, + }, + }, +} +``` + +--- + +## Table Styles + +**Profile routing:** Profile A (Formal: report, academic, contract, exam) → Three-Line Table or Horizontal-Only Table. Profile B (Visual: resume, copywriting) → Zebra Stripe (preferred) or Horizontal-Only Table. + +### 1. Three-Line Table (三线表) — Academic + +Only three horizontal lines: top of table, bottom of header, bottom of table. + +```js +const threeLineTable = new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { + top: { style: BorderStyle.SINGLE, size: 4, color: "000000" }, + bottom: { style: BorderStyle.SINGLE, size: 4, color: "000000" }, + left: { style: BorderStyle.NONE }, + right: { style: BorderStyle.NONE }, + insideHorizontal: { style: BorderStyle.NONE }, + insideVertical: { style: BorderStyle.NONE }, + }, + rows: [ + new TableRow({ + children: headerCells.map(text => new TableCell({ + children: [new Paragraph({ children: [new TextRun({ text, bold: true, size: 21 })] })], + borders: { + bottom: { style: BorderStyle.SINGLE, size: 2, color: "000000" }, + top: { style: BorderStyle.NONE }, + left: { style: BorderStyle.NONE }, + right: { style: BorderStyle.NONE }, + }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + })), + }), + // ... data rows with all borders NONE + ], +}); +``` + +### 2. Zebra Stripe — Data Reports + +```js +function zebraRow(cells, index, palette) { + return new TableRow({ + children: cells.map(text => new TableCell({ + children: [new Paragraph({ children: [new TextRun({ text, size: 21 })] })], + shading: index % 2 === 0 + ? { type: ShadingType.CLEAR, fill: palette.surface } + : { type: ShadingType.CLEAR, fill: "FFFFFF" }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + })), + }); +} +``` + +### 3. Horizontal-Only — Business (Default) + +```js +const horizontalTable = new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { + top: { style: BorderStyle.SINGLE, size: 2, color: palette.accent.replace("#","") }, + bottom: { style: BorderStyle.SINGLE, size: 2, color: palette.accent.replace("#","") }, + left: { style: BorderStyle.NONE }, + right: { style: BorderStyle.NONE }, + insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: "D0D0D0" }, + insideVertical: { style: BorderStyle.NONE }, + }, + rows: [/* header row with accent shading, then data rows */], +}); +``` + +**⚠️ CRITICAL**: Always set `margins` at the Table or TableCell level. Without margins, text touches cell borders. + +### Table Color Token Derivation + +Each palette in `coverPalettes` provides a `table` object with pre-computed colors for all 3 table styles: + +| Token | Used in | Description | +|-------|---------|-------------| +| `table.headerBg` | Zebra Stripe, Horizontal-Only | Table header background color | +| `table.headerText` | Zebra Stripe, Horizontal-Only | Table header text color (must pass WCAG AA contrast) | +| `table.accentLine` | Three-Line | Top/bottom/header-bottom line color | +| `table.innerLine` | Horizontal-Only | Inner horizontal separator line color | +| `table.surface` | Zebra Stripe | Alternating row background (light tint) | + +**Usage:** +```js +const palette = coverPalettes["DS-1"]; +const t = palette.table; +// Three-Line: use t.accentLine for border colors +// Zebra: use t.headerBg, t.headerText, t.surface +// Horizontal: use t.headerBg, t.headerText, t.innerLine +``` + +**⚠️ High-saturation accent override**: For DM-1, FG-1, and SN-2, the table colors are intentionally darkened/desaturated relative to the cover accent. Bright accent colors that look good on dark cover backgrounds are too eye-straining on white body pages. Always use `palette.table.*` for tables, never the raw `palette.accent`. + +--- +--- + +## Cover Page Design System + +### Design Philosophy + +Covers use **7 validated layout recipes** + **parameterized variants** instead of free combination. Each recipe's background, layout, and decoration are visually verified. Differentiation comes from **palette × font size × content variation**. + +**Architecture principle:** All recipes use a **single 16838 outer wrapper table** (one row, exact height). Recipes R1/R2/R3 use **ZERO nested tables** — all decoration is achieved via **paragraph borders** (left/right/top/bottom). This ensures maximum cross-engine stability (MS Office + WPS). + +### Cover Color Palettes (Dark Mode + Light Mode) + +Each palette defines 3 core colors (Background, Primary, Accent) + derived cover/table tokens. + +⚠️ **Disambiguation: `cover.titleColor` is a COLOR value, not title text.** All keys under `cover: { ... }` are **color hex codes** for styling cover text elements. They are NOT text content. The actual title text comes from `config.title`. Never use `P.titleColor` as the `text` parameter of a `TextRun` — it must only be used as the `color` parameter. + +```js +// ✅ Correct — config.title is the text, P.titleColor is the color +new TextRun({ text: config.title, color: P.titleColor }) + +// ❌ WRONG — using color value as text content +new TextRun({ text: P.titleColor, color: P.titleColor }) // displays "FFFFFF" as visible text! +``` + +```js +const coverPalettes = { + // ── Light backgrounds (7) ── + "WM-1": { // Warm Teal — education, training, marketing + bg: "F4F1E9", primary: "15857A", accent: "FF6A3B", + cover: { titleColor: "15857A", subtitleColor: "606060", metaColor: "707070", footerColor: "A0A0A0" }, + table: { headerBg: "15857A", headerText: "FFFFFF", accentLine: "15857A", innerLine: "D5D0C8", surface: "F0EDE5" }, + }, + "CM-2": { // Blue Orange — tech, corporate, whitepaper + bg: "FEFEFE", primary: "1284BA", accent: "FF862F", + cover: { titleColor: "1284BA", subtitleColor: "606060", metaColor: "707070", footerColor: "A0A0A0" }, + table: { headerBg: "1284BA", headerText: "FFFFFF", accentLine: "1284BA", innerLine: "D8E4EC", surface: "EDF4F9" }, + }, + "SN-2": { // Soft Purple — creative, branding, events (⚠️ NOT for business) + bg: "EBDCEF", primary: "73593C", accent: "B13DC6", + cover: { titleColor: "73593C", subtitleColor: "606060", metaColor: "707070", footerColor: "A0A0A0" }, + table: { headerBg: "7A4D8A", headerText: "FFFFFF", accentLine: "7A4D8A", innerLine: "D8D0DE", surface: "F2EDF5" }, + }, + "MIN-1": { // Warm Gold — consulting, minimalist business, premium proposals + bg: "F3F1ED", primary: "000000", accent: "D6C096", + cover: { titleColor: "000000", subtitleColor: "606060", metaColor: "707070", footerColor: "A0A0A0" }, + table: { headerBg: "D6C096", headerText: "1A1A1A", accentLine: "000000", innerLine: "DDD8CC", surface: "F5F3ED" }, + }, + "WR-2": { // Retro Green — traditional industry, finance compliance, legal + bg: "F4F1E9", primary: "2A4A3A", accent: "C89F62", + cover: { titleColor: "2A4A3A", subtitleColor: "606060", metaColor: "707070", footerColor: "A0A0A0" }, + table: { headerBg: "2A4A3A", headerText: "FFFFFF", accentLine: "2A4A3A", innerLine: "D0D8D0", surface: "F0EDE5" }, + }, + "MC-1": { // Medical Blue — healthcare, clinical reports + bg: "F5F8FC", primary: "1A5276", accent: "2E86C1", + cover: { titleColor: "1A5276", subtitleColor: "606060", metaColor: "707070", footerColor: "A0A0A0" }, + table: { headerBg: "2E86C1", headerText: "FFFFFF", accentLine: "1A5276", innerLine: "D0DDE8", surface: "EDF3F8" }, + }, + "GV-1": { // Official Red — government, state-owned enterprise, party building + bg: "FAFAFA", primary: "1A1A1A", accent: "C0392B", + cover: { titleColor: "1A1A1A", subtitleColor: "606060", metaColor: "707070", footerColor: "A0A0A0" }, + table: { headerBg: "C0392B", headerText: "FFFFFF", accentLine: "C0392B", innerLine: "DDD0D0", surface: "F8F0F0" }, + }, + + // ── Dark backgrounds (5) ── + "DS-1": { // Deep Sea — annual report, general business + bg: "0B1C2C", primary: "FFFFFF", accent: "529286", + cover: { titleColor: "FFFFFF", subtitleColor: "B0B8C0", metaColor: "90989F", footerColor: "687078" }, + table: { headerBg: "529286", headerText: "FFFFFF", accentLine: "529286", innerLine: "BECFCC", surface: "E8ECEB" }, + }, + "IG-1": { // Ink Gold — finance, investment, luxury brand + bg: "1A1A1A", primary: "FFFFFF", accent: "C9A84C", + cover: { titleColor: "FFFFFF", subtitleColor: "B0B8C0", metaColor: "90989F", footerColor: "687078" }, + table: { headerBg: "C9A84C", headerText: "1A1A1A", accentLine: "C9A84C", innerLine: "DDD5C0", surface: "F5F2E8" }, + }, + "DM-1": { // Deep Cyan — AI, tech proposals, digital transformation + bg: "162235", primary: "FFFFFF", accent: "37DCF2", + cover: { titleColor: "FFFFFF", subtitleColor: "B0B8C0", metaColor: "90989F", footerColor: "687078" }, + // ⚠️ Table uses darkened accent (#1B6B7A) — bright #37DCF2 is too saturated for white-page tables + table: { headerBg: "1B6B7A", headerText: "FFFFFF", accentLine: "1B6B7A", innerLine: "C8DDE2", surface: "EDF3F5" }, + }, + "FG-1": { // Forest Mint — ESG, environmental, sustainability, agriculture + bg: "0C1F1A", primary: "FFFFFF", accent: "3DDBB5", + cover: { titleColor: "FFFFFF", subtitleColor: "B0B8C0", metaColor: "90989F", footerColor: "687078" }, + // ⚠️ Table uses darkened accent (#2A7A65) — bright #3DDBB5 is too saturated for white-page tables + table: { headerBg: "2A7A65", headerText: "FFFFFF", accentLine: "2A7A65", innerLine: "C5D8D0", surface: "EDF5F2" }, + }, + "GO-1": { // Graphite Orange — proposals, bidding, PRD + bg: "1A2330", primary: "FFFFFF", accent: "D4875A", + cover: { titleColor: "FFFFFF", subtitleColor: "B0B8C0", metaColor: "90989F", footerColor: "687078" }, + table: { headerBg: "D4875A", headerText: "FFFFFF", accentLine: "D4875A", innerLine: "DDD0C8", surface: "F8F0EB" }, + }, + + // ── Special (R5 only) ── + "ED-1": { // Editorial Warm — lesson plans, cultural/creative, light reports, newsletters + bg: "F7F7F5", primary: "2C2C2C", accent: "D4D4D0", + cover: { titleColor: "2C2C2C", subtitleColor: "6B6B6B", metaColor: "9A9A9A", footerColor: "9A9A9A" }, + table: { headerBg: "E8E8E4", headerText: "2C2C2C", accentLine: "D4D4D0", innerLine: "E8E8E4", surface: "FAFAF8" }, + // Note: R6 exclusive. Minimal editorial style — warm grey tones, no colored headers. + }, + + "ST-1": { // Swiss Tech — cultural/creative research, trend reports, brand strategy + bg: "E2E8F0", primary: "0F172A", accent: "0042E6", + cover: { titleColor: "0F172A", subtitleColor: "475569", metaColor: "475569", footerColor: "475569" }, + table: { headerBg: "475569", headerText: "FFFFFF", accentLine: "0042E6", innerLine: "CBD5E1", surface: "F1F5F9" }, + // Note: R7 exclusive. Swiss minimalist — slate grey bg, Klein blue accent, open-frame tables. + }, + + "ACADEMIC": { // Academic Black — thesis, standards (R5 exclusive, not in general routing) + bg: "FFFFFF", primary: "000000", accent: "000000", + cover: { titleColor: "000000", subtitleColor: "404040", metaColor: "606060", footerColor: "808080" }, + table: { headerBg: "000000", headerText: "000000", accentLine: "000000", innerLine: "000000", surface: "FFFFFF" }, + // Note: Academic uses Three-Line table only, with pure black lines. No colored headers. + }, +}; +``` + +### ⚠️ Dark Cover → Light Table Rule + +Covers with dark backgrounds (DS-1, IG-1, DM-1, FG-1, GO-1) use bright accent on dark bg. +Body page tables are always on WHITE background — table colors use **darkened/desaturated** variants of the accent. +High-saturation accent colors (DM-1 #37DCF2, FG-1 #3DDBB5, SN-2 #B13DC6) are explicitly overridden in `table.*` fields above. + +### ⚠️ SN-2 Scene Restriction + +SN-2 (Soft Purple) is restricted to creative/branding/event documents ONLY. +It MUST NOT be used for: business reports, consulting, finance, legal, government, medical, or technical documents. + +### Industry → Palette Recommendations + +| Industry / Theme | Recommended Palette | Fallback | +|-----------|---------|---------| +| General annual report | DS-1 Deep Sea | CM-2 | +| Finance / investment / luxury | IG-1 Ink Gold | WR-2, MIN-1 | +| Tech / AI / internet | DM-1 Deep Cyan | CM-2 | +| ESG / environmental / sustainability | FG-1 Forest Mint | DS-1 | +| Consulting / diagnostic report | MIN-1 Warm Gold | WR-2 | +| Business proposal / bidding / PRD | GO-1 Graphite Orange | CM-2 | +| Education / training (formal) | WM-1 Warm Teal | CM-2 | +| Lesson plan (arts/general) | ED-1 Editorial Warm | WM-1 | +| Cultural / newsletter / internal | ED-1 Editorial Warm | WM-1 | +| Events / activities | ED-1 Editorial Warm | WM-1 | +| Medical / healthcare / clinical | MC-1 Medical Blue | CM-2 | +| Government / state-owned / party | GV-1 Official Red | MIN-1 | +| Traditional industry / legal / compliance | WR-2 Retro Green | MIN-1 | +| Creative / branding (formal) | SN-2 Soft Purple | WM-1 | +| Whitepaper (general) | CM-2 Blue Orange | DS-1, MIN-1 | +| Academic / thesis / standards | ACADEMIC | — | + +--- + +### ⚠️ Recipe Routing Rules (Replaces Free Selection) + +```js +function selectCoverRecipe(docType, industry, titleLength) { + // No cover for these types + if (["contract", "official", "exam", "resume"].includes(docType)) return null; + + // Academic + if (docType === "academic") return { recipe: "R5", palette: "ACADEMIC" }; + + // Thesis proposal report (开题报告) + if (docType === "proposal_report") return { recipe: "R5", palette: "ACADEMIC" }; + + // Lesson plans — R6 editorial for arts/general, R4 for STEM + if (docType === "lesson_plan" || docType === "lessonplan") { + const stemKeywords = ["math", "physics", "chemistry", "biology", "science", "tech", "computer", "engineering"]; + if (stemKeywords.some(k => (industry || "").toLowerCase().includes(k))) { + return { recipe: "R4", palette: "DM-1" }; + } + // Arts, general, and all other lesson plans → R6 editorial + return { recipe: "R6", palette: "ED-1" }; + } + + // Creative/branding/design (formal) → R3 centered card frame + if (["creative", "branding", "design"].includes(docType)) { + return { recipe: "R3", palette: "SN-2" }; + } + + // Cultural/newsletter/internal (casual) → R6 editorial + if (["cultural", "newsletter", "internal"].includes(docType)) { + return { recipe: "R6", palette: "ED-1" }; + } + + // Activity/event planning → R6 editorial + if (docType === "activity") return { recipe: "R6", palette: "ED-1" }; + + // Trend/research reports in cultural/creative/brand fields → R7 Swiss Tech + if (docType === "trend_report" || docType === "research_report") { + if (["cultural", "creative", "brand", "design"].includes(industry)) { + return { recipe: "R7", palette: "ST-1" }; + } + } + + // Formal/business subtypes + if (docType === "whitepaper") return { recipe: "R2", palette: industry === "finance" ? "IG-1" : "CM-2" }; + if (docType === "consulting") return { recipe: "R2", palette: "MIN-1" }; + if (docType === "proposal" || docType === "plan") return { recipe: "R4", palette: "GO-1" }; + + // Reports — palette by industry, use R1 + if (docType === "report") { + const paletteMap = { + finance: "IG-1", consulting: "MIN-1", + tech: "DM-1", ai: "DM-1", + education: "WM-1", green: "FG-1", + medical: "MC-1", government: "GV-1", + }; + return { recipe: "R1", palette: paletteMap[industry] || "DS-1" }; + } + + // Default + return { recipe: "R1", palette: "DS-1" }; +} + +// ── Long-title override (applied AFTER initial recipe selection) ── +// Call this after selectCoverRecipe() when the actual title text is known. +function applyLongTitleOverride(result, titleLength) { + if (!result || !result.recipe) return result; + // R5 (academic) is never overridden — it has its own calcTitleLayoutMixed() + if (result.recipe === "R5") return result; + // R6 (editorial) is designed for short titles only (≤20 chars, single line) + // Long titles → fall back to R1 (handles long titles best) + if (titleLength > 20 && result.recipe === "R6") { + return { recipe: "R1", palette: "WM-1" }; // ED-1 has no dark bg, use WM-1 (warm teal) + } + // R3/R4 struggle with long titles → fall back to R1 (same palette) + if (titleLength > 20 && ["R3", "R4"].includes(result.recipe)) { + return { recipe: "R1", palette: result.palette }; + } + // Very long titles: even R2 centered looks scattered → R1 left-aligned + if (titleLength > 30 && result.recipe === "R2") { + return { recipe: "R1", palette: result.palette }; + } + return result; +} +``` + +### Scene Cover Routing + +| Scene | Recipe | Default Palette | Special Requirements | +|------|------|---------|----------| +| academic thesis | R5 (Clean White) | ACADEMIC | School name + 2-col meta table with underlines, see academic.md | +| thesis proposal report (开题报告) | R5 (Clean White) | ACADEMIC | Use `buildProposalCover()` from academic.md | +| business report (general) | R1 (Pure Paragraph Left) | DS-1 Deep Sea | Auto-select palette by industry | +| whitepaper | R2 (Double-Rule Frame) | CM-2 Blue Orange / IG-1 Ink Gold | — | +| consulting report | R2 (Double-Rule Frame) | MIN-1 Warm Gold | — | +| business proposal / plan | R4 (Top Color Block) | GO-1 Graphite Orange | — | +| events / activities | R6 (Editorial Warm) | ED-1 Editorial Warm | Light/casual style, short titles only (≤20 chars) | +| lesson plan (STEM) | R4 (Top Color Block) | DM-1 Deep Cyan | Route by subject, see selectCoverRecipe | +| lesson plan (arts/general) | R6 (Editorial Warm) | ED-1 Editorial Warm | Casual editorial style, short titles only | +| creative / branding / design (formal) | R3 (Centered Card Frame) | SN-2 Soft Purple | Product overview, brand doc, design report | +| cultural / newsletter / internal | R6 (Editorial Warm) | ED-1 Editorial Warm | Light/casual style, short titles only (≤20 chars) | +| trend/research report (cultural/creative/brand) | R7 (Swiss Tech) | ST-1 Swiss Tech | Minimalist slate bg, Klein blue accent, open-frame tables | +| education report | R1 (Pure Paragraph Left) | WM-1 Warm Teal | For education reports, NOT lesson plans | +| ESG / environmental | R1 (Pure Paragraph Left) | FG-1 Forest Mint | — | +| medical / healthcare | R1 (Pure Paragraph Left) | MC-1 Medical Blue | — | +| government / state-owned | R1 (Pure Paragraph Left) | GV-1 Official Red | — | +| resume | — | — | No standalone cover | +| contract | — | — | No standalone cover (title page is first page) | +| official document | — | — | No standalone cover | +| exam paper | — | — | No standalone cover | + +--- + +### Cover Title Length Guidelines + +When the user does NOT specify an exact title, the model should craft a title within the recommended range. If the user provides a title that exceeds the comfortable range, apply long-title routing in `selectCoverRecipe()` (see above). + +| Recipe | Comfortable (1–2 lines) | Maximum (3 lines) | Long Title Tolerance | +|--------|------------------------|--------------------|----------------------| +| R1 | 8–20 chars | ≤50 chars | ⭐⭐⭐⭐⭐ Best (left-aligned, full-page bg) | +| R2 | 8–18 chars | ≤45 chars | ⭐⭐⭐⭐ Good (full-page bg, but centered) | +| R3 | 8–15 chars | ≤40 chars | ⭐⭐ Poor (narrowest width) | +| R4 | 8–18 chars | ≤45 chars | ⭐⭐ Poor (fixed-height color block) | +| R5 | 8–16 chars | ≤42 chars | ⭐⭐⭐ OK (academic only, mixed-width calc) | +| R6 | 8–15 chars | ≤20 chars (1 line) | ⭐ Single line only (editorial, no multi-line) | +| R7 | 8–24 chars | ≤42 chars (3 lines) | ⭐⭐⭐⭐ Good (left-aligned, light bg, dynamic spacing) | + +**Title crafting rules (when model generates the title):** +1. Prefer concise titles within the "comfortable" range +2. If topic requires detail, split into title + subtitle (e.g., title="数字化转型战略研究" subtitle="——以某某企业为例") +3. Never exceed the "maximum" range unless user explicitly provides the full title + +--- + +### ⚠️ Cover Page Break Rules + +Cover should be an independent section — **no PageBreak at the end needed**. The next section automatically starts a new page. + +```js +// ✅ Correct — cover is a separate section, no trailing PageBreak +sections: [ + { properties: { /* Cover section, margin all 0 */ }, children: buildCover(...) }, + { properties: { /* Body section */ }, children: buildContent(...) }, +] +``` + +### ⚠️ Cover Content Overflow Prevention (Mandatory) + +1. Cover section page margin is 0; total content height ≤ 15638 twips (1200 twips safety margin for cross-engine compatibility — MS Office renders large fonts taller than calculated). +2. Color block Table `height` must use `rule: "exact"` (never `"atLeast"`). +3. Each recipe code includes height budget annotations — verify during generation. +4. **`verticalAlign` must always be `"top"`**. Never use `"center"` or `"bottom"` in exact-height rows — content will be clipped or overflow. Use `spacing.before` on the first paragraph for vertical positioning. +5. **Title font size MUST be dynamically calculated via `calcTitleLayout()`** (see below). Never hardcode font sizes above 40pt for cover titles. Every recipe MUST call `calcTitleLayout()` before building title paragraphs. +6. **Never use `margins.top`/`margins.bottom` for vertical positioning inside exact-height cells**: Cell margins reduce available height unpredictably across MS Office and WPS. Use `spacing.before` on the first paragraph instead. Only `margins.left`/`margins.right` are safe. +7. **Dynamic spacing is mandatory**: Use `calcCoverSpacing()` to compute `spacing.before` values dynamically based on content element count and title line count. Never use fixed large spacing values (e.g., `before: 4500`) that assume a specific title length. +8. **Cover must be a single-page section**: The cover section must produce exactly ONE page. If content overflows to a second page, it means the height budget is violated. Common overflow causes and fixes: + - Title font too large → use `calcTitleLayout()` to auto-reduce + - Too many meta lines → reduce font size or remove less important lines + - Fixed `spacing.before` values too large → use `calcCoverSpacing()` for dynamic values + - Subtitle + English label + meta lines combined exceed budget → calculate total and reduce spacing +9. **Cover wrapper table MUST use explicit `allNoBorders`**: The outer 16838 wrapper table and ALL nested tables inside the cover MUST set borders to NONE explicitly. Never rely on docx-js default borders (`single/auto/sz=4`). Default borders add ~8 twips per edge, which causes MS Office to calculate a total height slightly exceeding 16838 → content overflows to a blank page 2. WPS is more lenient but MS Office is strict. **This is the #1 cause of "blank page 2 in MS Office but not in WPS".** + +```js +// ✅ MANDATORY: Define and use allNoBorders for every cover table +const NB = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; +const noBorders = { top: NB, bottom: NB, left: NB, right: NB }; +const allNoBorders = { top: NB, bottom: NB, left: NB, right: NB, + insideHorizontal: NB, insideVertical: NB }; + +new Table({ + borders: allNoBorders, // ← MANDATORY on every cover table + // ... +}); +``` + +10. **Decorative lines MUST use paragraph borders, NEVER text characters**: Horizontal decorative lines (accent strips, dividers, frame edges) must be implemented with `paragraph border.top` or `border.bottom` — never with text characters like `───`, `━━━`, `═══`, or `——————`. Character-drawn lines render at inconsistent widths across MS Office and WPS (font metrics differ), causing lines to appear truncated or misaligned. Paragraph borders render pixel-perfect in both engines and their width is controlled precisely via `indent.left` / `indent.right`. + +```js +// ✅ Correct — paragraph border (R2 style thick accent rule) +new Paragraph({ + indent: { left: 1000, right: 1000 }, + border: { top: { style: BorderStyle.SINGLE, size: 18, color: P.accent, space: 20 } }, + children: [], +}) + +// ❌ FORBIDDEN — text character line (renders inconsistently) +new Paragraph({ + children: [new TextRun({ text: "───────────────", color: P.accent })] +}) +``` +9. **Post-generation overflow check (mandatory)**: After building cover children, estimate total height: + ```js + function estimateCoverHeight(elements) { + let total = 0; + for (const el of elements) { + if (el instanceof Table) { + // Sum row heights (exact rows) or estimate 400 twips per row + const rows = el.root?.[0]?.rows || []; + for (const row of rows) { + total += row.height?.value || 400; + } + } else if (el instanceof Paragraph) { + const fontSize = el.root?.[0]?.size || 24; // half-pts + // ★ Use pt * 11.5 (= half-pt * 10 * 1.15) for accurate single-spacing estimate + const lineHeight = Math.max(fontSize * 11.5, 276); // min 276 (default 12pt) + const spacingBefore = el.spacing?.before || 0; + const spacingAfter = el.spacing?.after || 0; + total += lineHeight + spacingBefore + spacingAfter; + } + } + return total; + } + // ★ Target: estimateCoverHeight(coverChildren) < 15638 (16838 - 1200 safety) + ``` + +--- + +### ⚠️ Cover Title Layout — calcTitleLayout() (Mandatory for ALL Recipes) + +**Every cover recipe MUST use `calcTitleLayout()` to determine title font size and line breaks.** Hardcoding font sizes or passing the full title as a single TextRun is FORBIDDEN. + +**Every paragraph with font size > body text MUST set explicit line spacing** to prevent top clipping: +```js +// ★ MANDATORY: prevent inherited small line spacing from clipping large fonts +spacing: { line: Math.ceil(titlePt * 23), lineRule: "atLeast", after: 100 } +// Example: 36pt → line: 828; 44pt → line: 1012 +``` +Without this, the paragraph inherits body text line spacing (e.g., 560tw), which is shorter than the font height → top of characters gets clipped. + +```js +/** + * Calculate safe font size and smart line breaks for cover titles. + * MUST be called by every recipe before building title paragraphs. + * + * @param {string} title Full title string + * @param {number} maxWidthTwips Available width for title text (twips, after subtracting margins/padding) + * @param {number} preferredPt Desired max font size in pt (default 40) + * @param {number} minPt Minimum allowed font size in pt (default 24) + * @returns {{ titlePt: number, titleLines: string[] }} + */ +function calcTitleLayout(title, maxWidthTwips, preferredPt = 40, minPt = 24) { + // Each CJK character width ≈ pt × 20 twips + const charWidth = (pt) => pt * 20; + const charsPerLine = (pt) => Math.floor(maxWidthTwips / charWidth(pt)); + + // Try from preferredPt downward until title fits in ≤ 3 lines + let titlePt = preferredPt; + let lines; + while (titlePt >= minPt) { + const cpl = charsPerLine(titlePt); + if (cpl < 2) { titlePt -= 2; continue; } + lines = splitTitleLines(title, cpl); + if (lines.length <= 3) break; + titlePt -= 2; + } + + // If still > 3 lines at minPt, force 3 lines + if (!lines || lines.length > 3) { + const cpl = charsPerLine(minPt); + lines = splitTitleLines(title, cpl); + titlePt = minPt; + } + + return { titlePt, titleLines: lines }; +} + +/** + * Smart Chinese title line-breaking — breaks at semantic boundaries, never mid-word. + * + * Rules: + * 1. Prefer breaking after particles, punctuation, connectors, underscores, spaces + * 2. Never split a compound word (e.g., "管理规范" must not become "管理规" + "范") + * 3. No single-character orphan on the last line — merge into previous line + * 4. If no good break point found within 60-130% of charsPerLine, break at charsPerLine + * + * @param {string} title Full title string + * @param {number} charsPerLine Max characters per line at current font size + * @returns {string[]} Array of line strings + */ +function splitTitleLines(title, charsPerLine) { + if (title.length <= charsPerLine) return [title]; + + // Characters that are safe break points (break AFTER these) + const breakAfter = new Set([ + ...',。、;:!?', // CJK punctuation + ...'的与和及之在于为', // CJK particles/prepositions + ...'-_—–·/', // connectors + ...' \t', // whitespace + ]); + + const lines = []; + let remaining = title; + + while (remaining.length > charsPerLine) { + let breakAt = -1; + + // Search backward from charsPerLine to 60% for a break point + for (let i = charsPerLine; i >= Math.floor(charsPerLine * 0.6); i--) { + if (i < remaining.length && breakAfter.has(remaining[i - 1])) { + breakAt = i; + break; + } + } + + // If not found, search forward up to 130% + if (breakAt === -1) { + const limit = Math.min(remaining.length, Math.ceil(charsPerLine * 1.3)); + for (let i = charsPerLine + 1; i < limit; i++) { + if (breakAfter.has(remaining[i - 1])) { + breakAt = i; + break; + } + } + } + + // Last resort: break at charsPerLine, but avoid splitting compound CJK words + if (breakAt === -1) { + breakAt = charsPerLine; + // If both chars at the break boundary are CJK (likely a compound word), + // step back 1 char to keep the word together + const prevChar = remaining[breakAt - 1]; + const nextChar = remaining[breakAt]; + if (prevChar && nextChar && + !breakAfter.has(prevChar) && !breakAfter.has(nextChar) && + /[\u4e00-\u9fff]/.test(prevChar) && /[\u4e00-\u9fff]/.test(nextChar)) { + breakAt = breakAt - 1; + } + } + + lines.push(remaining.slice(0, breakAt).trim()); + remaining = remaining.slice(breakAt).trim(); + } + if (remaining) lines.push(remaining); + + // Prevent single-character orphan on last line — merge into previous + if (lines.length > 1 && lines[lines.length - 1].length <= 2) { + const last = lines.pop(); + lines[lines.length - 1] += last; + } + + return lines; +} + +/** + * Calculate dynamic spacing values for cover elements to fit within page height. + * + * @param {object} params + * @param {number} params.titleLineCount Number of title lines + * @param {number} params.titlePt Title font size in pt + * @param {boolean} params.hasSubtitle Whether subtitle exists + * @param {boolean} params.hasEnglishLabel Whether English label exists + * @param {number} params.metaLineCount Number of meta info lines + * @param {number} params.fixedHeight Sum of fixed-height elements (color strips, accent bars, footer) in twips + * @param {number} params.pageHeight Total page height in twips (default 16838) + * @returns {{ topSpacing, midSpacing, bottomSpacing }} Spacing values in twips + */ +function calcCoverSpacing(params) { + const { + titleLineCount = 1, titlePt = 36, hasSubtitle = false, + hasEnglishLabel = false, metaLineCount = 0, + fixedHeight = 800, pageHeight = 16838, + marginTop = 0, marginBottom = 0, // ★ NEW: pass actual section margins + } = params; + + // ★ Safety margin: 1200 twips (cross-engine: MS Office renders large fonts + // taller than calculated; extra 400tw buffer prevents footer clipping) + const SAFETY = 1200; + // ★ Subtract page margins from available height (cover section may have margins) + const usableHeight = pageHeight - marginTop - marginBottom - SAFETY; + + // ★ Accurate height estimation per element: + const titleHeight = titleLineCount * (titlePt * 23 + 200); + const subtitleHeight = hasSubtitle ? (12 * 23 + 600) : 0; + const englishLabelHeight = hasEnglishLabel ? (9 * 23 + 600) : 0; + const metaHeight = metaLineCount * (10 * 23 + 100); + + // ★ Account for implicit paragraph heights: + const implicitParaHeight = 3 * 300; + + const contentHeight = titleHeight + subtitleHeight + englishLabelHeight + + metaHeight + fixedHeight + implicitParaHeight; + + const remainingSpace = usableHeight - contentHeight; + const safeRemaining = Math.max(remainingSpace, 400); + + // ★ Footer protection: bottomSpacing must be ≥ FOOTER_MIN to prevent + // footer + accent line from being clipped at the cell bottom edge + const FOOTER_MIN = 800; + const rawTop = Math.floor(safeRemaining * 0.45); + const rawBottom = Math.floor(safeRemaining * 0.45); + const bottomSpacing = Math.max(rawBottom, FOOTER_MIN); + const topSpacing = Math.max(rawTop - Math.max(0, FOOTER_MIN - rawBottom), 400); + const midSpacing = Math.max(safeRemaining - topSpacing - bottomSpacing, 0); + + return { topSpacing, midSpacing, bottomSpacing }; +} +``` + +**Usage in every recipe:** +```js +// Step 1: Calculate title layout +const availableWidth = 11906 - leftPadding - rightPadding; // subtract margins +const { titlePt, titleLines } = calcTitleLayout(config.title, availableWidth); +const titleSize = titlePt * 2; // convert to half-points for docx-js + +// Step 2: Calculate spacing (pass actual section margins!) +const spacing = calcCoverSpacing({ + titleLineCount: titleLines.length, + titlePt, + hasSubtitle: !!config.subtitle, + hasEnglishLabel: !!config.englishLabel, + metaLineCount: (config.metaLines || []).length, + fixedHeight: 800, // sum of accent strips, footer table, etc. + marginTop: 0, // ★ pass the cover section's actual top margin (twips) + marginBottom: 0, // ★ pass the cover section's actual bottom margin (twips) +}); + +// Step 3: Use in paragraphs +children.push(new Paragraph({ spacing: { before: spacing.topSpacing } })); +// ... title paragraphs using titleLines and titleSize ... +children.push(new Paragraph({ spacing: { before: spacing.bottomSpacing } })); +``` + +--- + +### ⚠️ CRITICAL — Cover Section Non-Negotiables (ALL Recipes) + +These 3 properties are MANDATORY for every cover implementation (R1–R7). Omitting ANY of them causes cover layout failure: + +1. **Cover section margin = 0**: The cover MUST be in its own section with `page.margin: { top: 0, bottom: 0, left: 0, right: 0 }`. Non-zero margins shrink the wrapper away from page edges → white gaps around the cover ("cover not filling the page"). This is the #1 cause of broken cover layouts. + +2. **Wrapper row exact height**: The outer wrapper table row MUST set `height: { value: 16838, rule: "exact" }`. Without this, content overflow pushes to page 2, or insufficient content leaves bottom whitespace. + +3. **Wrapper table borders = allNoBorders**: MUST explicitly set `borders: allNoBorders`. Default docx-js borders add ~8 twips per edge. MS Office includes border thickness in exact-height calculation → total exceeds 16838 → blank page 2 (WPS is lenient, MS Office is strict). + +**Cover section template (copy this for every Recipe):** +```js +sections: [ + { + // ⚠️ Cover section — margin MUST be 0, separate from body + properties: { + page: { + size: { width: 11906, height: 16838 }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + }, + }, + children: buildCoverRX(config), // ← replace with actual recipe function + }, + { + // Body section — normal margins + properties: { + type: SectionType.NEXT_PAGE, + page: { + size: { width: 11906, height: 16838 }, + margin: { top: 1440, bottom: 1440, left: 1701, right: 1417 }, + }, + }, + children: [...bodyContent], + }, +] +``` + +--- + +### Recipe R1: Pure Paragraph Cover (Left-Aligned) + +**Visual:** Full-page dark background + left-aligned text + decoration via paragraph borders only +**Use case:** Annual report, business report, tech proposal (most versatile premium recipe) +**Nested tables: ZERO** — all decoration uses paragraph borders (bottom line, left accent bar, top separator) + +Visual hierarchy (top to bottom): +1. Dynamic top whitespace (via `calcCoverSpacing`) +2. English label with accent bottom border (paragraph `border.bottom`) +3. Main title (1-3 lines, dynamic font size via `calcTitleLayout`) +4. Subtitle (light grey, smaller) +5. Meta info lines with left accent border (paragraph `border.left`) +6. Dynamic bottom whitespace +7. Footer line with top accent separator (paragraph `border.top`) + +```js +// ⚠️ MANDATORY: Cover section must use margin: 0. See "Cover Section Non-Negotiables" above. +// Section: { properties: { page: { size: { width: 11906, height: 16838 }, +// margin: { top: 0, bottom: 0, left: 0, right: 0 } } }, children: buildCoverR1(config) } + +function buildCoverR1(config) { + // config: { title, subtitle, englishLabel, metaLines, footerLeft, footerRight, palette } + // palette: { bg, titleColor, subtitleColor, metaColor, accent, footerColor } + const P = config.palette; + const padL = 1200, padR = 800; + + // ⚠️ MANDATORY: Use calcTitleLayout() for dynamic font size + line breaking + const availableWidth = 11906 - padL - padR - 300; // -300 for border space + const { titlePt, titleLines } = calcTitleLayout(config.title, availableWidth, 40, 24); + const titleSize = titlePt * 2; + + // ⚠️ MANDATORY: Use calcCoverSpacing() for dynamic spacing + const spacing = calcCoverSpacing({ + titleLineCount: titleLines.length, titlePt, + hasSubtitle: !!config.subtitle, hasEnglishLabel: !!config.englishLabel, + metaLineCount: (config.metaLines || []).length, + fixedHeight: 400, // footer line only (no nested tables) + }); + + const accentLeft = { style: BorderStyle.SINGLE, size: 8, color: P.accent, space: 12 }; + const children = []; + + // 1. Top whitespace (dynamic) + children.push(new Paragraph({ spacing: { before: spacing.topSpacing } })); + + // 2. English label with accent bottom border + if (config.englishLabel) { + children.push(new Paragraph({ + indent: { left: padL, right: padR }, spacing: { after: 500 }, + border: { bottom: { style: BorderStyle.SINGLE, size: 6, color: P.accent, space: 8 } }, + children: [new TextRun({ text: config.englishLabel.split("").join(" "), + size: 18, color: P.accent, font: { ascii: "Calibri", eastAsia: "SimHei" }, characterSpacing: 40 })], + })); + } + + // 3. Main title (dynamic font size + smart line breaks) + for (let i = 0; i < titleLines.length; i++) { + children.push(new Paragraph({ + indent: { left: padL }, + spacing: { after: i < titleLines.length - 1 ? 100 : 300, line: Math.ceil(titlePt * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: titleLines[i], size: titleSize, bold: true, + color: P.titleColor, font: { eastAsia: "SimHei", ascii: "Arial" } })], + })); + } + + // 4. Subtitle + if (config.subtitle) { + children.push(new Paragraph({ + indent: { left: padL }, spacing: { after: 800 }, + children: [new TextRun({ text: config.subtitle, size: 24, color: P.subtitleColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + })); + } + + // 5. Meta info lines with left accent border + for (const line of (config.metaLines || [])) { + children.push(new Paragraph({ + indent: { left: padL + 200 }, spacing: { after: 80 }, + border: { left: accentLeft }, + children: [new TextRun({ text: line, size: 24, color: P.metaColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + })); + } + + // 6. Bottom whitespace (dynamic) + children.push(new Paragraph({ spacing: { before: spacing.bottomSpacing } })); + + // 7. Footer with top accent separator + children.push(new Paragraph({ + indent: { left: padL, right: padR }, + border: { top: { style: BorderStyle.SINGLE, size: 2, color: P.accent, space: 8 } }, + spacing: { before: 200 }, + children: [ + new TextRun({ text: config.footerLeft || "", size: 16, color: P.footerColor, font: { ascii: "Arial" } }), + new TextRun({ text: " " }), + new TextRun({ text: config.footerRight || "", size: 16, color: P.footerColor, font: { ascii: "Arial" } }), + ], + })); + + // Single 16838 wrapper — the ONLY table + return [new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: 16838, rule: "exact" }, + children: [new TableCell({ + shading: { type: ShadingType.CLEAR, fill: P.bg }, borders: noBorders, + children, + })], + })], + })]; + // Total height: 16838 (single wrapper, zero nested tables) ✅ +} +``` + +--- + +### Recipe R2: Double-Rule Frame (Centered) + +**Visual:** Full-page dark background + top/bottom thick accent horizontal rules + centered content +**Use case:** Whitepaper, finance report, consulting deliverable, high-end formal reports +**Nested tables: ZERO** — top/bottom rules are paragraph borders + +Visual hierarchy (top to bottom): +1. Top thick accent rule (paragraph `border.top`) +2. Generous whitespace +3. English label (centered, spaced) +4. Main title (centered, 1-3 lines, dynamic font size) +5. Subtitle (centered) +6. Generous whitespace +7. Meta info lines (centered, **18pt** / size: 36) +8. Generous whitespace +9. Footer + bottom thick accent rule (paragraph `border.bottom`) + +```js +// ⚠️ MANDATORY: Cover section must use margin: 0. See "Cover Section Non-Negotiables" above. +function buildCoverR2(config) { + const P = config.palette; + const padL = 1400, padR = 1400; + + // ⚠️ MANDATORY: Use calcTitleLayout() for dynamic font size + line breaking + const { titlePt, titleLines } = calcTitleLayout(config.title, 11906 - padL - padR, 40, 24); + const titleSize = titlePt * 2; + const thickBorder = { style: BorderStyle.SINGLE, size: 18, color: P.accent, space: 20 }; + + const children = []; + + // 1. Top rule + children.push(new Paragraph({ + indent: { left: padL - 400, right: padR - 400 }, spacing: { before: 1200, after: 200 }, + border: { top: thickBorder }, children: [], + })); + + // 2. Whitespace + children.push(new Paragraph({ spacing: { before: 1800 } })); + + // 3. English label + if (config.englishLabel) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, spacing: { after: 500 }, + children: [new TextRun({ text: config.englishLabel.split("").join(" "), + size: 18, color: P.accent, font: { ascii: "Calibri" }, characterSpacing: 40 })], + })); + } + + // 4. Main title (centered, dynamic) + for (let i = 0; i < titleLines.length; i++) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, + spacing: { after: i < titleLines.length - 1 ? 80 : 300, line: Math.ceil(titlePt * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: titleLines[i], size: titleSize, bold: true, + color: P.titleColor, font: { eastAsia: "SimHei", ascii: "Arial" } })], + })); + } + + // 5. Subtitle + if (config.subtitle) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, spacing: { after: 400 }, + children: [new TextRun({ text: config.subtitle, size: 24, color: P.subtitleColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + })); + } + + // 6. Whitespace + children.push(new Paragraph({ spacing: { before: 1200 } })); + + // 7. Meta lines — 18pt (size: 36) for readability + for (const line of (config.metaLines || [])) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, + spacing: { after: 100, line: Math.ceil(18 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: line, size: 36, color: P.metaColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + })); + } + + // 8. Whitespace + children.push(new Paragraph({ spacing: { before: 2000 } })); + + // 9. Footer + bottom rule + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, + indent: { left: padL - 400, right: padR - 400 }, spacing: { before: 200 }, + border: { bottom: thickBorder }, + children: [new TextRun({ text: config.footerRight || "", size: 18, color: P.footerColor, font: { ascii: "Arial" } })], + })); + + // Single 16838 wrapper — the ONLY table + return [new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: 16838, rule: "exact" }, + children: [new TableCell({ + shading: { type: ShadingType.CLEAR, fill: P.bg }, borders: noBorders, + children, + })], + })], + })]; + // Total height: 16838 (single wrapper, zero nested tables) ✅ +} +``` + +--- + +### Recipe R3: Centered Card Frame (Paragraph Borders) + +**Visual:** Full-page dark background + centered "card" effect via paragraph indent + 4-side paragraph borders +**Use case:** Research report, product overview, event summary, creative/design documents +**Nested tables: ZERO** — card borders are paragraph borders with large left/right indents + +Visual hierarchy (top to bottom): +1. Pre-card whitespace (~2800tw) +2. Card top edge (paragraph with `border.top` + `border.left` + `border.right`, indent 2200tw) +3. English label (centered, inside card side borders) +4. Main title (centered, inside card side borders, dynamic font size) +5. Subtitle (centered, inside card side borders) +6. Spacer (inside card side borders) +7. Meta info lines (centered, inside card side borders) +8. Card bottom edge (paragraph with `border.bottom` + `border.left` + `border.right`) +9. Post-card whitespace +10. Footer (centered) + +```js +// ⚠️ MANDATORY: Cover section must use margin: 0. See "Cover Section Non-Negotiables" above. +function buildCoverR3(config) { + const P = config.palette; + const cardIndent = 2200; // left + right indent to create "card" feel + const innerWidth = 11906 - cardIndent * 2 - 400; + + // ⚠️ MANDATORY: Use calcTitleLayout() for dynamic font size + line breaking + const { titlePt, titleLines } = calcTitleLayout(config.title, innerWidth, 40, 24); + const titleSize = titlePt * 2; + + const bTop = { style: BorderStyle.SINGLE, size: 24, color: P.accent, space: 16 }; + const bBot = { style: BorderStyle.SINGLE, size: 24, color: P.accent, space: 16 }; + const bL = { style: BorderStyle.SINGLE, size: 2, color: P.accent, space: 16 }; + const bR = { style: BorderStyle.SINGLE, size: 2, color: P.accent, space: 16 }; + const sides = { left: bL, right: bR }; + + const children = []; + + // 1. Pre-card whitespace + children.push(new Paragraph({ spacing: { before: 2800 } })); + + // 2. Card top edge + children.push(new Paragraph({ + indent: { left: cardIndent, right: cardIndent }, spacing: { after: 600 }, + border: { top: bTop, left: bL, right: bR }, children: [], + })); + + // 3. English label inside card + if (config.englishLabel) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, indent: { left: cardIndent, right: cardIndent }, + spacing: { after: 500 }, border: sides, + children: [new TextRun({ text: config.englishLabel.split("").join(" "), + size: 16, color: P.accent, font: { ascii: "Calibri" }, characterSpacing: 30 })], + })); + } + + // 4. Main title inside card + for (let i = 0; i < titleLines.length; i++) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, indent: { left: cardIndent, right: cardIndent }, + spacing: { after: i < titleLines.length - 1 ? 60 : 300, line: Math.ceil(titlePt * 23), lineRule: "atLeast" }, + border: sides, + children: [new TextRun({ text: titleLines[i], size: titleSize, bold: true, + color: P.titleColor, font: { eastAsia: "SimHei", ascii: "Arial" } })], + })); + } + + // 5. Subtitle inside card + if (config.subtitle) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, indent: { left: cardIndent, right: cardIndent }, + spacing: { after: 400 }, border: sides, + children: [new TextRun({ text: config.subtitle, size: 22, color: P.subtitleColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + })); + } + + // 6. Spacer inside card + children.push(new Paragraph({ + indent: { left: cardIndent, right: cardIndent }, spacing: { before: 400 }, + border: sides, children: [], + })); + + // 7. Meta info lines inside card + for (let i = 0; i < (config.metaLines || []).length; i++) { + const isLast = i === config.metaLines.length - 1; + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, indent: { left: cardIndent, right: cardIndent }, + spacing: { after: isLast ? 400 : 80 }, border: sides, + children: [new TextRun({ text: config.metaLines[i], size: 24, color: P.metaColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + })); + } + + // 8. Card bottom edge + children.push(new Paragraph({ + indent: { left: cardIndent, right: cardIndent }, spacing: { after: 0 }, + border: { bottom: bBot, left: bL, right: bR }, children: [], + })); + + // 9. Post-card whitespace + children.push(new Paragraph({ spacing: { before: 2000 } })); + + // 10. Footer + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: config.footerRight || "", size: 16, color: P.footerColor, font: { ascii: "Arial" } })], + })); + + // Single 16838 wrapper — the ONLY table + return [new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: 16838, rule: "exact" }, + children: [new TableCell({ + shading: { type: ShadingType.CLEAR, fill: P.bg }, borders: noBorders, + children, + })], + })], + })]; + // Total height: 16838 (single wrapper, zero nested tables) ✅ +} +``` + + +### Recipe R4: Top Color Block + +**Visual:** Top 45% dark area (with title) + bottom 55% white area (with meta info) + accent divider +**Use case:** business proposal, plan document, lesson plan, PRD +**Architecture:** Uses R1's proven single 16838 wrapper. Upper dark block is a nested table inside the wrapper. Content positioning uses `spacing.before` (reliable) instead of `margins.top` (unreliable across engines). + +```js +// ⚠️ MANDATORY: Cover section must use margin: 0. See "Cover Section Non-Negotiables" above. +function buildCoverR4(config) { + const P = config.palette; + const padL = 1200, padR = 800; + const availableWidth = 11906 - padL - padR; + + // ⚠️ MANDATORY: Use calcTitleLayout() for dynamic font size + line breaking + const { titlePt, titleLines } = calcTitleLayout(config.title, availableWidth, 40, 26); + const titleSize = titlePt * 2; + + // Height budget for upper dark block — DYNAMIC based on title content + const titleBlockHeight = titleLines.length * (titlePt * 23 + 200); + const englishLabelH = config.englishLabel ? (9 * 23 + 500) : 0; + const subtitleH = config.subtitle ? (12 * 23 + 200) : 0; + const upperContentH = englishLabelH + titleBlockHeight + subtitleH; + const UPPER_MIN = 7500; // minimum height to preserve visual proportion + const UPPER_H = Math.max(UPPER_MIN, upperContentH + 1500 + 800); // +1500 top pad, +800 bottom pad + const DIVIDER_H = 60; + + // ★ Dynamic top spacing: calculated from content height, NOT fixed margins.top + const contentEstimate = + (config.englishLabel ? (9 * 23 + 500) : 0) + + titleLines.length * (titlePt * 23 + 200) + + (config.subtitle ? (12 * 23 + 200) : 0); + const spacerIntrinsic = 280; // empty spacing paragraph intrinsic height + const topSpacing = Math.max(UPPER_H - contentEstimate - spacerIntrinsic - 800, 400); + + // ── Upper dark block (nested table, exact height) ── + const upperBlock = new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: UPPER_H, rule: "exact" }, + children: [new TableCell({ + shading: { fill: P.bg }, borders: noBorders, + verticalAlign: "top", + // ★ KEY: Only left/right margins. NO top/bottom margins. + // Vertical positioning uses spacing.before on the first paragraph. + margins: { left: padL, right: padR }, + children: [ + new Paragraph({ spacing: { before: topSpacing } }), + config.englishLabel ? new Paragraph({ + spacing: { after: 500 }, + children: [new TextRun({ text: config.englishLabel.split("").join(" "), + size: 18, color: P.accent, font: { ascii: "Calibri" }, characterSpacing: 60 })], + }) : null, + ...titleLines.map((line, i) => new Paragraph({ + spacing: { after: i < titleLines.length - 1 ? 100 : 200 }, + children: [new TextRun({ text: line, size: titleSize, bold: true, + color: P.titleColor, font: { eastAsia: "SimHei", ascii: "Arial" } })], + })), + config.subtitle ? new Paragraph({ + spacing: { after: 100 }, + children: [new TextRun({ text: config.subtitle, size: 24, color: P.subtitleColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + }) : null, + ].filter(Boolean), + })], + })], + }); + + // ── Accent divider line ── + const divider = new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: DIVIDER_H, rule: "exact" }, + children: [new TableCell({ borders: noBorders, + shading: { fill: P.accent }, children: [emptyPara()] })], + })], + }); + + // ── Lower white area (paragraphs, not a separate table) ── + const lowerContent = [ + new Paragraph({ spacing: { before: 800 } }), + ...(config.metaLines || []).map(line => new Paragraph({ + indent: { left: padL }, spacing: { after: 100 }, + children: [new TextRun({ text: line, size: 28, color: P.metaColor, + font: { eastAsia: "Microsoft YaHei", ascii: "Arial" } })], + })), + new Paragraph({ spacing: { before: 2000 } }), + new Paragraph({ + indent: { left: padL }, + children: [ + new TextRun({ text: config.footerLeft || "", size: 22, color: "909090" }), + new TextRun({ text: " " }), + new TextRun({ text: config.footerRight || "", size: 22, color: "909090" }), + ], + }), + ]; + + // ── Outer 16838 wrapper (R1 proven architecture) ── + // The wrapper acts as a safety net: even if the inner 7500 block + // overflows slightly, content stays on page 1. + return [new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: 16838, rule: "exact" }, + children: [new TableCell({ + shading: { fill: "FFFFFF" }, borders: noBorders, + verticalAlign: "top", + children: [ + upperBlock, + divider, + ...lowerContent, + ], + })], + })], + })]; + // Total height: 16838 (outer wrapper, R1 architecture) ✅ +} +``` + +--- + +### Recipe R5: Clean White (Academic) + +**Visual:** Pure white background + school name + centered title + 2-column meta info table with underlines + footer +**Use case:** academic thesis, standards documents +**Architecture:** 16838 outer wrapper (white fill, invisible) + cell margins for page margin simulation. No top/bottom decorative lines. + +**Meta info table rules (cross-engine safe):** +- 2-column table: label + value, **percentage widths only** (`WidthType.PERCENTAGE`) +- **Table width is adaptive:** 55–75% of page, calculated by `calcR5MetaLayout()`. Table centered via `alignment: CENTER`. +- **Label column:** adaptive 25–45% of table, **LEFT aligned**, plain text with ":" appended. NO full-width space padding, NO right-alignment. +- **Label column borders:** none (no bottom border on label cells). +- **Value column:** remaining %, **LEFT aligned**, `bottom: single sz=4` border = fixed-length underline (consistent length for all rows regardless of text). +- No left/right/top borders on either column. +- ⚠️ Do NOT use DXA widths, full-width space padding (`\u3000`), spacer columns, or tab stops — WPS renders them inconsistently. +- ⚠️ Do NOT use `margins.top` on the wrapper cell — use `spacing.before` on first paragraph instead. + +**Known limitation:** When meta lines ≥ 6 AND title has 3 lines, MS Office may render content slightly taller than WPS, potentially clipping the footer line. Mitigate by reducing `midSpacing` or using a smaller title font. + +```js +// ── Width-aware title layout (handles mixed Chinese + English) ── +function estimateTextWidth(text, pt) { + let width = 0; + for (const ch of text) { + const code = ch.codePointAt(0); + const isCJK = (code >= 0x4E00 && code <= 0x9FFF) || (code >= 0x3400 && code <= 0x4DBF) || + (code >= 0x3000 && code <= 0x303F) || (code >= 0xFF00 && code <= 0xFFEF) || + (code >= 0x2E80 && code <= 0x2EFF); + width += isCJK ? pt * 20 : pt * 11; // CJK: full-width, Latin: ~55% width + } + return width; +} +// Use estimateTextWidth() in calcTitleLayout() instead of simple char count +// to prevent mid-word breaks in mixed Chinese+English titles like "基于Transformer架构的..." + +// ── Meta info table ── + +// Calculate adaptive table and label column percentage based on longest label. +// Returns { tablePct, labelPct } — both as percentages. +// Uses ONLY WidthType.PERCENTAGE for cross-engine compatibility (MS Office + WPS). +function calcR5MetaLayout(metaEntries, fontPt = 12) { + const maxLabelLen = Math.max(...metaEntries.map(e => [...e.label].length)); + // Label needs: maxLabelLen chars + ":" + 1 char padding + const labelNeedTw = (maxLabelLen + 2) * fontPt * 20; + // Value column: fixed ~5000tw for consistent underline length + const valueNeedTw = 5000; + const totalNeedTw = labelNeedTw + valueNeedTw; + // Table width as % of page (11906tw), clamped to 55–75% + const tablePct = Math.min(75, Math.max(55, Math.ceil(totalNeedTw / 11906 * 100))); + // Label % within the table, clamped to 25–45% + const rawLabelPct = Math.ceil(labelNeedTw / (tablePct / 100 * 11906) * 100); + return { tablePct, labelPct: Math.max(25, Math.min(45, rawLabelPct)) }; +} + +// Build R5 academic cover meta info table. +// ⚠️ CRITICAL cross-engine rules: +// - Table width: WidthType.PERCENTAGE (NOT DXA — WPS breaks with DXA) +// - Column widths: WidthType.PERCENTAGE +// - Label column: LEFT aligned, plain text (NO full-width space padding) +// - Value column: LEFT aligned, bottom border = fixed-length underline +// - Table alignment: CENTER (visually centered on page) +function buildR5MetaTable(metaEntries) { + // metaEntries: [{ label: "学院", value: "计算机科学与技术学院" }, ...] + const { tablePct, labelPct } = calcR5MetaLayout(metaEntries); + const valuePct = 100 - labelPct; + const bottomBorder = { style: BorderStyle.SINGLE, size: 4, color: "000000" }; + + const rows = metaEntries.map(entry => new TableRow({ + children: [ + // Label cell: left-aligned, no bottom border + new TableCell({ + width: { size: labelPct, type: WidthType.PERCENTAGE }, + borders: noBorders, + margins: { top: 60, bottom: 60, left: 0, right: 0 }, + children: [new Paragraph({ + alignment: AlignmentType.LEFT, + spacing: { before: 60, after: 60, line: 400 }, + children: [new TextRun({ + text: entry.label + ":", + size: 24, font: { eastAsia: "SimSun", ascii: "Times New Roman" }, + })], + })], + }), + // Value cell: left-aligned, bottom border = fixed-length underline + new TableCell({ + width: { size: valuePct, type: WidthType.PERCENTAGE }, + borders: { top: NB, left: NB, right: NB, bottom: bottomBorder }, + margins: { top: 60, bottom: 60, left: 80, right: 0 }, + children: [new Paragraph({ + alignment: AlignmentType.LEFT, + spacing: { before: 60, after: 60, line: 400 }, + children: [new TextRun({ + text: entry.value, + size: 24, font: { eastAsia: "SimSun", ascii: "Times New Roman" }, + })], + })], + }), + ], + })); + + return new Table({ + width: { size: tablePct, type: WidthType.PERCENTAGE }, + alignment: AlignmentType.CENTER, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows, + }); +} + +// ⚠️ MANDATORY: Cover section must use margin: 0. See "Cover Section Non-Negotiables" above. +function buildCoverR5(config) { + const PAGE_H = 16838, SAFETY = 1200; + const safeH = PAGE_H - SAFETY; // 15638 + const simMarginLR = 1701, simMarginT = 1200; + const contentW = 11906 - simMarginLR * 2; + + // ★ Width-aware title layout for mixed Chinese+English + const { titlePt, titleLines } = calcTitleLayoutMixed(config.title, contentW, 36, 24); + const titleSize = titlePt * 2; + + // Parse meta entries + const metaEntries = (config.metaLines || []).map(line => { + const sep = line.indexOf(":") !== -1 ? ":" : ":"; + const idx = line.indexOf(sep); + if (idx === -1) return { label: line, value: "" }; + return { label: line.slice(0, idx).trim(), value: line.slice(idx + sep.length).trim() }; + }); + + // Height budget (no margins.top — use spacing.before instead) + const schoolNameH = config.schoolName ? (22 * 23 + 400) : 0; + const titleTotalH = titleLines.length * (titlePt * 23 + 200); + const subtitleH = config.subtitle ? (15 * 23 + 600) : 0; + const metaRowH = 520; // 60+60 padding + ~400 line height + const metaTableH = metaEntries.length * metaRowH; + const footerH = config.footerRight ? (12 * 23 + 200) : 0; + const spacerParas = 3 * 350; + const fixedH = schoolNameH + titleTotalH + subtitleH + metaTableH + footerH + spacerParas; + const remaining = Math.max(safeH - fixedH, 600); + + // ★ topSpacing includes simulated top margin (simMarginT) + const topSpacing = Math.min(Math.floor(remaining * 0.28) + simMarginT, 4200); + const midSpacing = Math.min(Math.floor((remaining - simMarginT) * 0.18), 2000); + const bottomSpacing = Math.min(remaining - topSpacing + simMarginT - midSpacing, 5500); + + const children = []; + children.push(new Paragraph({ spacing: { before: topSpacing } })); + + // School name (optional) + if (config.schoolName) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, spacing: { after: 400 }, + children: [new TextRun({ text: config.schoolName, size: 44, characterSpacing: 40, + font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], + })); + } + + // Title + for (let i = 0; i < titleLines.length; i++) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, spacing: { after: i < titleLines.length - 1 ? 120 : 300 }, + children: [new TextRun({ text: titleLines[i], size: titleSize, bold: true, + font: { eastAsia: "SimHei", ascii: "Times New Roman" } })], + })); + } + + // Subtitle + if (config.subtitle) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, spacing: { after: 200 }, + children: [new TextRun({ text: config.subtitle, size: 30, + font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], + })); + } + + children.push(new Paragraph({ spacing: { before: midSpacing } })); + + // Meta info table + if (metaEntries.length > 0) children.push(buildR5MetaTable(metaEntries)); + + children.push(new Paragraph({ spacing: { before: bottomSpacing } })); + + // Footer + if (config.footerRight) { + children.push(new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: config.footerRight, size: 24, color: "404040", + font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], + })); + } + + // ★ 16838 outer wrapper — only left/right margins, NO margins.top + return [new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + layout: TableLayoutType.FIXED, + borders: allNoBorders, + rows: [new TableRow({ + height: { value: PAGE_H, rule: "exact" }, + children: [new TableCell({ + shading: { type: ShadingType.CLEAR, fill: "FFFFFF" }, + borders: noBorders, verticalAlign: "top", + margins: { left: simMarginLR, right: simMarginLR }, + children, + })], + })], + })]; + // Height budget example (short title, 4 meta lines): + // topSpacing(3818) + schoolName(906) + title(1028) + subtitle(945) + midSpacing(1467) + // + metaTable(4×520=2080) + bottomSpacing(5268) + footer(476) = ~15988 < 15838 ✅ +} +``` + +--- + +### blendColors Utility Function + +```js +function blendColors(hex1, hex2, ratio) { + const p = (s, i) => parseInt(s.replace("#","").slice(i, i+2), 16); + const mix = (c1, c2) => Math.round(c1 + (c2 - c1) * ratio); + const r = mix(p(hex1,0), p(hex2,0)), g = mix(p(hex1,2), p(hex2,2)), b = mix(p(hex1,4), p(hex2,4)); + return [r, g, b].map(v => v.toString(16).padStart(2,"0")).join(""); +} +``` + + + +## Geometric Decoration System + +→ See `references/decorations.md` for the full geometric decoration element library (decoration elements, usage scenarios, code examples). + +## Chinese Plot PNG Method (matplotlib) + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from matplotlib.font_manager import FontProperties + +font_paths = [ + "/System/Library/Fonts/Supplemental/SimHei.ttf", + "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", + "./SimHei.ttf", +] +zh_font = None +for fp in font_paths: + try: + zh_font = FontProperties(fname=fp) + break + except: + continue + +plt.rcParams["axes.unicode_minus"] = False +``` + +## Chart Quality Rules + +### Chart Color Palette + +Default: **low-saturation (Morandi style)** palette to avoid flashy high-saturation. High-saturation palette only for explicitly energetic scenarios (events/education/creative). + +```js +const chartColors = { + // Default: low saturation, professional (S: 25-40%, L: 55-68%) + default: ["6B9DAD", "C49B72", "7BA68A", "B87472", "9687A8", "C8B87C", "7AADA0", "7A9BB8"], + // Vivid: only for energetic/creative scenes + vivid: ["2F97B8", "E67E22", "27AE60", "E74C3C", "9B59B6", "F1C40F", "1ABC9C", "3498DB"], +}; + +// Scene selection: +// - report/whitepaper/consulting/academic/contract → default +// - activity/education/creative copy → vivid (optional) +``` + +**Color usage rules:** +- Max **5 colors** per chart; excess categories use depth variants of same hue +- Emphasis data uses document accent color; non-emphasis uses grey `#B0B0B0` +- Adjacent segments in pie/bar charts must have hue gap ≥ 60° + +1. **Anti-overlap**: If >6 x-axis labels, rotate 45° (`plt.xticks(rotation=45, ha='right')`) +2. **Anti-stretch**: Always set figure size explicitly (`fig, ax = plt.subplots(figsize=(10, 6))`) +3. **Aspect ratio (CRITICAL)**: When embedding in docx, MUST read actual image dimensions and calculate height proportionally. NEVER hardcode both width and height — pie charts become ellipses, radar charts become diamonds. + ```js + const sizeOf = require("image-size"); + const dims = sizeOf(chartBuffer); + const displayWidth = 500; + const displayHeight = Math.round(displayWidth * (dims.height / dims.width)); + // transformation: { width: displayWidth, height: displayHeight } + ``` +4. **DPI**: Save at 200+ DPI (`plt.savefig("chart.png", dpi=200, bbox_inches="tight")`) +5. **Colors**: Use palette accent color for primary data series +6. **Legend**: Place outside plot area if >4 series +7. **Square charts**: Pie and radar charts MUST use `figsize=(8, 8)` (equal width/height) to preserve circular/radial shape +7. **Grid**: Light gray grid (`ax.grid(True, alpha=0.3)`) + +--- + +## Typography Rules + +### CJK Body Text +- **Alignment**: Justified (`AlignmentType.JUSTIFIED`) +- **First-line indent**: 2 characters — Profile A (SimSun): `firstLine: 480`; Profile B (YaHei): `firstLine: 420`. See `common-rules.md` for profile definitions. +- **Line spacing**: 1.3x = `spacing: { line: 312 }` +- **No heading indent**: Headings must NOT have first-line indent + +### English Body Text +- **Alignment**: Left (`AlignmentType.LEFT`) +- **No indent** +- **Line spacing**: Same 1.3x + +### Table Numbers +- Right-aligned in cells +- Use monospace or tabular figures if available + +### Headings +- No first-line indent +- `spacing: { before: 240, after: 120 }` (H1: before 360) +- Bold, palette.primary color + +### 1.3x Line Spacing — MANDATORY +Every document, every paragraph. `spacing: { line: 312 }`. No exceptions unless scene explicitly overrides (e.g., resume uses 1.15x). + +--- + +## Page Layout — A4 Standard + +```js +sections: [{ + properties: { + page: { + size: { width: 11906, height: 16838, orientation: PageOrientation.PORTRAIT }, + margin: { top: 1440, bottom: 1440, left: 1701, right: 1417 }, + // Top/bottom 2.54cm = 1440, left 3.0cm = 1701, right 2.5cm = 1417 twips + }, + }, + children: [/* ... */], +}] +``` + +These are defaults. Scenes may override (e.g., official docs use different margins). + +### Scene Font Override Rules + +Default font config (docx-js Font Configuration in design-system.md) uses YaHei+Calibri for most business scenarios. The following scenes have dedicated font requirements — **scene rules override defaults**: + +| Scene | Body CN | Body EN | Headings | Body Color | +|------|----------|----------|------|----------| +| Default (general) | Microsoft YaHei | Calibri | SimHei + Calibri | palette.body | +| **Report** | **SimSun** | **Times New Roman** | **SimHei + TNR** | **"000000" (pure black)** | +| **Academic** | **SimSun** | **Times New Roman** | **SimHei + TNR** | **"000000" (pure black)** | +| **Contract** | **SimSun** | **Times New Roman** | **SimHei + TNR** | **"000000" (pure black)** | +| Official doc | FangSong | — | STXiaoBiaoSong | "000000" | +| Resume | Microsoft YaHei | Calibri | SimHei + Calibri | palette.body | + +When report or academic scene is loaded, `styles.default.document.run` font and color must be overridden per scene. Heading sizes may also differ (e.g., report scene H1 centered, H2 uses Xiao San size:30 instead of default Si Hao size:28). Scene file takes precedence. diff --git a/skills/docx/references/docx-js-advanced.md b/skills/docx/references/docx-js-advanced.md new file mode 100755 index 0000000..e584a09 --- /dev/null +++ b/skills/docx/references/docx-js-advanced.md @@ -0,0 +1,257 @@ +# docx-js Advanced Features + +Advanced API for complex document scenarios. Load this when creating documents with TOC, cover pages, footnotes, multi-section layouts, or post-processing needs. + +## Table of Contents (TOC) + +**→ See `references/toc.md` for the complete TOC reference** (3-step process, code examples, page numbering, common bugs, checklist). + +## Cover Page Design (Vertical Centering) + +Use large `spacing.before` to push content down for visual centering: + +```js +// Approximate vertical center on A4: +// Total printable height ≈ 14000 twips +// For title at ~40% from top: before = 5600 +const coverSection = { + properties: { + page: { /* standard A4 */ }, + // No headers/footers on cover page + }, + children: [ + new Paragraph({ spacing: { before: 5600 } }), // spacer + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ + text: title, + font: { ascii: "Calibri", eastAsia: "SimHei" }, + size: 52, bold: true, color: palette.primary, + })], + }), + // ... subtitle, author, date + ], +}; +``` + +For multi-section documents, put the cover in its own section so it can have different headers/footers. + +## Footnotes + +```js +const { FootnoteReferenceRun, Footnote } = require("docx"); + +const doc = new Document({ + footnotes: { + 1: { children: [new Paragraph({ children: [new TextRun({ text: "Smith, J. (2024). Research Methods. Academic Press, pp. 45-67.", size: 18 })] })] }, + 2: { children: [new Paragraph({ children: [new TextRun({ text: "Zhang, W. (2023). \u201c数据分析方法研究\u201d. 科学通报, 68(12), 1234-1250.", size: 18 })] })] }, + }, + sections: [{ + children: [ + new Paragraph({ + children: [ + new TextRun({ text: "According to recent studies" }), + new FootnoteReferenceRun(1), // superscript [1] + new TextRun({ text: ", data analysis methods have evolved" }), + new FootnoteReferenceRun(2), // superscript [2] + new TextRun({ text: "." }), + ], + }), + ], + }], +}); +``` + +### Academic Reference Pattern + +For sequential references [1][2][3]..., pre-define all footnotes in the `footnotes` object with numeric keys, then reference them inline with `FootnoteReferenceRun(n)`. + +## keepNext — Element Binding + +Prevent page breaks between related elements: + +```js +// Heading stays with next paragraph +new Paragraph({ + heading: HeadingLevel.HEADING_2, + keepNext: true, // don't break after this + children: [new TextRun({ text: "Table 1: Results" })], +}) +// Table immediately follows on same page + +// Caption stays with image +new Paragraph({ + keepNext: true, + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "Figure 1: Architecture Diagram", italics: true, size: 20 })], +}) +// ImageRun paragraph follows +``` + +Use `keepNext: true` for: +- Heading → first paragraph of section +- Table caption → table +- Image → image caption +- "Figure X" label → image + +## Page Break Rules + +Follow the document type strategy defined in SOUL.md Rule 1. + +**Structural breaks (always):** +- Cover page → TOC +- TOC → main content +- Main content → back cover + +**Content breaks (by document type):** +- Academic / teaching → `new Paragraph({ children: [new PageBreak()] })` before each H1 chapter +- Business report → PageBreak before each H1; H2 flows naturally +- Resume / contract / letter → No content page breaks +- Short article → No content page breaks + +**Anti-tear (mandatory):** +```js +// Heading stays with next paragraph +new Paragraph({ + heading: HeadingLevel.HEADING_1, + keepNext: true, + children: [new TextRun("Chapter Title")], +}) + +// Table caption stays with table +new Paragraph({ + keepNext: true, + children: [new TextRun({ text: "Table 1: Summary", italics: true })], +}) + +// Image caption stays with image +new Paragraph({ + keepNext: true, + children: [new TextRun({ text: "Figure 1: Architecture", italics: true })], +}) +``` + +**Never:** +- PageBreak inside tables +- PageBreak as standalone element (must be inside Paragraph) +- PageBreak at the END of the last section (causes blank page) + +```js +// Correct: page break between cover and TOC +new Paragraph({ children: [new PageBreak()] }) +``` + +## Quotes Escaping in JS Strings + +**⚠️⚠️⚠️ CRITICAL — #1 MOST COMMON BUG ⚠️⚠️⚠️** + +Bare Chinese curly quotation marks (`""` `''`) in JS string literals **WILL break syntax and crash document generation**. This bug occurs most often in **Chinese body text** where curly quotes are used for emphasis, proper nouns, event names, or quoted speech — e.g., `"双11"`, `"前低后高"`, `"618"大促`. **Every single occurrence** of `""''` in text content MUST be Unicode-escaped. No exceptions. + +**MANDATORY RULE: Before writing ANY `TextRun`, `para()`, or string containing Chinese text, scan the text for `""''` characters and replace ALL of them with `\u201c \u201d \u2018 \u2019`.** + +| Character | Unicode | Escape method | +|-----------|---------|---------------| +| `"` `"` | `\u201c` `\u201d` | Unicode escape `\u201c` `\u201d` | +| `'` `'` | `\u2018` `\u2019` | Unicode escape `\u2018` `\u2019` | +| `"` | U+0022 | `\"` or wrap string in single quotes / template literal | +| `'` | U+0027 | `\'` or wrap string in double quotes / template literal | + +```js +// ❌ WRONG — curly quotes in Chinese text break JS syntax (VERY COMMON MISTAKE) +content.push(para("2025年四个季度行业增速呈现"前低后高"的态势。在"618"大促、"双11""双12"活动拉动下增长显著。")); +new TextRun({ text: "他说"你好"" }) +new TextRun({ text: 'It's a test' }) + +// ✅ CORRECT — ALL curly quotes replaced with Unicode escapes +content.push(para("2025年四个季度行业增速呈现\u201c前低后高\u201d的态势。在\u201c618\u201d大促、\u201c双11\u201d\u201c双12\u201d活动拉动下增长显著。")); +new TextRun({ text: "他说\u201c你好\u201d" }) +new TextRun({ text: "It\u2019s a test" }) + +// ✅ CORRECT — straight quotes escaped or use alternate delimiters +new TextRun({ text: "He said \"hello\"" }) +new TextRun({ text: 'He said "hello"' }) +new TextRun({ text: `He said "hello"` }) +``` + +## Multi-Section Documents + +Different headers/footers per section: + +```js +const doc = new Document({ + sections: [ + { + // Section 1: Cover — no header/footer + properties: { page: { /* ... */ } }, + children: coverChildren, + }, + { + // Section 2: Front matter — Roman page numbers + properties: { + type: SectionType.NEXT_PAGE, + page: { + /* size, margin... */ + pageNumbers: { start: 1, formatType: NumberFormat.UPPER_ROMAN }, + }, + }, + headers: { default: new Header({ children: [] }) }, + footers: { + default: new Footer({ + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ children: [PageNumber.CURRENT], size: 18 })], + })], + }), + }, + children: tocAndAbstract, + }, + { + // Section 3: Main content — Arabic page numbers + properties: { + type: SectionType.NEXT_PAGE, + page: { + /* size, margin... */ + pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL }, + }, + }, + headers: { + default: new Header({ + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: docTitle, size: 18, color: "888888" })], + })], + }), + }, + footers: { default: footerWithPageNumbers }, + children: mainContent, + }, + ], +}); +``` + +## Converting DOCX to PDF + +```bash +# Using LibreOffice (headless) +libreoffice --headless --convert-to pdf output.docx + +# ⚠️ TOC Rule: If document has TOC, warn user that: +# 1. LibreOffice conversion may show empty TOC +# 2. User should open in Word first, update fields (Ctrl+A → F9), save, then convert +# 3. Or use Word's "Save as PDF" for best results +``` + +## Converting DOCX to Images + +```bash +# Step 1: Convert to PDF +libreoffice --headless --convert-to pdf output.docx + +# Step 2: Convert PDF to images +pdftoppm -png -r 200 output.pdf output_page + +# This generates output_page-1.png, output_page-2.png, etc. +# Use -r 200 for good quality (200 DPI) +``` + +Useful for generating preview thumbnails or when user needs images instead of document files. diff --git a/skills/docx/references/docx-js-core.md b/skills/docx/references/docx-js-core.md new file mode 100755 index 0000000..a83e76e --- /dev/null +++ b/skills/docx/references/docx-js-core.md @@ -0,0 +1,333 @@ +# docx-js API Reference + +Complete API for creating .docx documents with the `docx` npm package. For advanced features (TOC details, footnotes, PDF conversion), see `docx-js-advanced.md`. + +## Setup + +```js +const { + Document, Packer, Paragraph, TextRun, Table, TableRow, TableCell, + ImageRun, PageBreak, Header, Footer, PageNumber, NumberFormat, + AlignmentType, HeadingLevel, WidthType, BorderStyle, ShadingType, + PageOrientation, TabStopType, TabStopPosition, ExternalHyperlink, + InternalHyperlink, Bookmark, LevelFormat, TableOfContents, +} = require("docx"); +const fs = require("fs"); +``` + +## Document Creation + Export + +```js +const doc = new Document({ + styles: { /* see Styles section */ }, + numbering: { config: [ /* see Lists section */ ] }, + sections: [{ + properties: { + page: { + size: { width: 11906, height: 16838 }, + margin: { top: 1417, bottom: 1417, left: 1701, right: 1417 }, + }, + }, + headers: { default: new Header({ children: [/* */] }) }, + footers: { default: new Footer({ children: [/* */] }) }, + children: [ /* Paragraphs, Tables, etc. */ ], + }], +}); + +const buffer = await Packer.toBuffer(doc); +fs.writeFileSync("output.docx", buffer); +``` + +## Paragraph + TextRun + +```js +new Paragraph({ + heading: HeadingLevel.HEADING_1, // or HEADING_2, HEADING_3 + alignment: AlignmentType.JUSTIFIED, + spacing: { before: 240, after: 120, line: 312 }, // 1.3x mandatory + indent: { firstLine: 480 }, // 2-char CJK indent (480 SimSun / 420 YaHei) + children: [ + new TextRun({ + text: "Hello", + bold: true, + italics: true, + size: 24, // 12pt = Xiao Si + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, + color: "000000", // Pure black for Profile A; for Profile B use palette.body + }), + ], +}); + +// Additional text formatting options +new TextRun({ text: "Underlined", underline: { type: UnderlineType.SINGLE } }) +new TextRun({ text: "Highlighted", highlight: "yellow" }) +new TextRun({ text: "Strikethrough", strike: true }) +new TextRun({ text: "x²", superScript: true }) +new TextRun({ text: "H₂O", subScript: true }) +new SymbolRun({ char: "2022", font: "Symbol" }) // Bullet • +``` + +## Table + +**⚠️ CRITICAL**: Always set `margins` on TableCell (or at Table level for global default). Without margins, text touches borders. + +**⚠️ CRITICAL**: Use `ShadingType.CLEAR` — never `ShadingType.SOLID` (causes black cells). + +**⚠️ CRITICAL — Table Cross-Page Control**: +- Header row MUST set `tableHeader: true` (auto-repeat header on page break) +- All rows MUST set `cantSplit: true` (prevent row content split across pages) +- Title paragraph before table MUST set `keepNext: true` (keep title with table) + +```js +// ⚠️ Title before table — keepNext keeps title with table +new Paragraph({ + keepNext: true, // ← critical + children: [new TextRun({ text: "Table 1 Feature Comparison", bold: true, size: 21 })], +}), + +new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { + top: { style: BorderStyle.SINGLE, size: 2, color: "9AA6B2" }, + bottom: { style: BorderStyle.SINGLE, size: 2, color: "9AA6B2" }, + left: { style: BorderStyle.NONE }, + right: { style: BorderStyle.NONE }, + insideHorizontal: { style: BorderStyle.SINGLE, size: 1, color: "D0D0D0" }, + insideVertical: { style: BorderStyle.NONE }, + }, + rows: [ + // ⚠️ Header row — tableHeader + cantSplit + new TableRow({ + tableHeader: true, // auto-repeat on page break + cantSplit: true, // prevent row split + children: ["Header 1", "Header 2"].map(text => + new TableCell({ + children: [new Paragraph({ children: [new TextRun({ text, bold: true, size: 21 })] })], + shading: { type: ShadingType.CLEAR, fill: "F1F5F9" }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + width: { size: 50, type: WidthType.PERCENTAGE }, + }) + ), + }), + // ⚠️ Data rows — cantSplit + new TableRow({ + cantSplit: true, // prevent row split + children: ["Data 1", "Data 2"].map(text => + new TableCell({ + children: [new Paragraph({ children: [new TextRun({ text, size: 21 })] })], + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + width: { size: 50, type: WidthType.PERCENTAGE }, + }) + ), + }), + ], +}); +``` + +### Column Widths + +```js +// Fixed widths (twips) +width: { size: 3000, type: WidthType.DXA } +// Percentage +width: { size: 50, type: WidthType.PERCENTAGE } +``` + +## ImageRun + +**⚠️ CRITICAL**: Always include `type` parameter. Always preserve aspect ratio. + +```js +const imageBuffer = fs.readFileSync("chart.png"); +// Calculate dimensions preserving aspect ratio +const displayWidth = 500; +const aspectRatio = originalHeight / originalWidth; +const displayHeight = Math.round(displayWidth * aspectRatio); + +new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new ImageRun({ + data: imageBuffer, + transformation: { width: displayWidth, height: displayHeight }, + type: "png", // REQUIRED: "png", "jpg", "gif", "bmp" + }), + ], +}); +``` + +## PageBreak + +**⚠️ CRITICAL**: PageBreak MUST be inside a Paragraph. Standalone PageBreak crashes Word. + +**⚠️ Best Practice**: Attach PageBreak to the end of a **paragraph with text content**. Avoid empty paragraph + PageBreak (may cause blank pages). If using multi-section structure, prefer section breaks over PageBreak. + +```js +// ✅ Recommended — PageBreak attached to content paragraph +new Paragraph({ + children: [ + new TextRun({ text: "End of section" }), + new PageBreak() + ] +}) + +// ✅ Acceptable — but prefer section breaks +new Paragraph({ children: [new PageBreak()] }) + +// ✅ Best — use section breaks instead of PageBreak +// Place content in different sections — auto page break +``` + +## Headers & Footers + Page Numbers + +```js +headers: { + default: new Header({ + children: [ + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "Document Title", size: 18, color: "888888" })], + }), + ], + }), +}, +footers: { + default: new Footer({ + children: [ + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new TextRun({ children: [PageNumber.CURRENT], size: 18 }), + ], + }), + ], + }), +}, +``` + +> ⚠️ **Denominator FORBIDDEN** — never use `PageNumber.TOTAL_PAGES` or "X / Y" format. Show only current page number. + +## Styles Definition + +The example below is for **Chinese documents** (default). For **English documents**, replace `font` with `"Times New Roman"` throughout. + +```js +styles: { + default: { + document: { + run: { + font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, + size: 24, color: "000000", // Pure black for Profile A; for Profile B use palette.body + }, + paragraph: { + spacing: { line: 312 }, // 1.3x mandatory + }, + }, + heading1: { + run: { font: { ascii: "Calibri", eastAsia: "SimHei" }, size: 32, bold: true, color: "0B1220" }, + paragraph: { spacing: { before: 360, after: 160, line: 312 } }, + }, + heading2: { + run: { font: { ascii: "Calibri", eastAsia: "SimHei" }, size: 28, bold: true, color: "0B1220" }, + paragraph: { spacing: { before: 240, after: 120, line: 312 } }, + }, + heading3: { + run: { font: { ascii: "Calibri", eastAsia: "SimHei" }, size: 24, bold: true, color: "0B1220" }, + paragraph: { spacing: { before: 200, after: 100, line: 312 } }, + }, + }, +} +``` + +## Lists + +**⚠️ CRITICAL**: Each separate numbered list MUST use a unique `reference` name. Reusing the same reference causes numbering to continue instead of restarting. + +```js +// In Document numbering config +numbering: { + config: [ + { + reference: "list-features", // unique name! + levels: [{ + level: 0, + format: LevelFormat.DECIMAL, + text: "%1.", + alignment: AlignmentType.LEFT, + style: { paragraph: { indent: { left: 720, hanging: 360 } } }, + }], + }, + { + reference: "list-benefits", // different name for second list! + levels: [{ /* same config */ }], + }, + ], +}, + +// Usage in paragraphs +new Paragraph({ + numbering: { reference: "list-features", level: 0 }, + children: [new TextRun({ text: "First item" })], +}) +``` + +### Bullet Lists + +```js +new Paragraph({ + bullet: { level: 0 }, + children: [new TextRun({ text: "Bullet item" })], +}) +``` + +## Hyperlinks + +### External Link + +```js +new ExternalHyperlink({ + children: [new TextRun({ text: "Click here", style: "Hyperlink" })], + link: "https://example.com", +}) +``` + +### Internal Link (Bookmark) + +```js +// Define bookmark at target +new Paragraph({ + children: [ + new Bookmark({ id: "section1", children: [new TextRun("Section 1")] }), + ], +}) + +// Link to bookmark +new InternalHyperlink({ + children: [new TextRun({ text: "Go to Section 1", style: "Hyperlink" })], + anchor: "section1", +}) +``` +## Table of Contents (TOC) + +**→ See `references/toc.md` for the complete TOC reference.** + +Quick reminder: (1) Add `TableOfContents` element + PageBreak, (2) Run `python3 "$DOCX_SCRIPTS/add_toc_placeholders.py" output.docx --auto`, (3) Check exit code. + +## Tabs + +```js +new Paragraph({ + tabStops: [ + { type: TabStopType.RIGHT, position: TabStopPosition.MAX }, + ], + children: [new TextRun("Left"), new TextRun("\t"), new TextRun("Right")] +}) +``` + +## 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) +- **Symbols:** `"2022"` (•), `"00A9"` (©), `"00AE"` (®), `"2122"` (™) + diff --git a/skills/docx/references/faq.md b/skills/docx/references/faq.md new file mode 100755 index 0000000..30777f3 --- /dev/null +++ b/skills/docx/references/faq.md @@ -0,0 +1,323 @@ +# FAQ — Common Bugs and Fixes + +## Bug: Table text touching cell borders + +**Symptom**: Text is cramped against table cell edges, no padding. + +**Fix**: Set `margins` at the TableCell level: +```js +new TableCell({ + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + children: [/* ... */], +}) +``` + +--- + +## Bug: Numbered list doesn't restart + +**Symptom**: Second numbered list continues from where the first left off (e.g., starts at 4 instead of 1). + +**Fix**: Each separate numbered list MUST use a unique `reference` name in numbering config: +```js +numbering: { config: [ + { reference: "list-A", levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1." }] }, + { reference: "list-B", levels: [{ level: 0, format: LevelFormat.DECIMAL, text: "%1." }] }, +]} +``` + +--- + +## Bug: Cover and content on same page + +**Symptom**: Cover page content flows directly into main content without page break. + +**Fix**: Add a PageBreak paragraph at the end of cover content: +```js +coverChildren.push(new Paragraph({ children: [new PageBreak()] })); +``` + +--- + +## Bug: Three-line table shows all borders + +**Symptom**: Table intended to be three-line shows full grid borders. + +**Fix**: Set table-level borders to NONE, then override only specific cell borders: +```js +// Table level: all borders NONE +borders: { top: { style: BorderStyle.SINGLE, size: 4 }, bottom: { style: BorderStyle.SINGLE, size: 4 }, + left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE }, + insideHorizontal: { style: BorderStyle.NONE }, insideVertical: { style: BorderStyle.NONE } } +// Header cells: bottom border only +headerCell.borders = { bottom: { style: BorderStyle.SINGLE, size: 2, color: "000000" } } +``` + +--- + +## Bug: User requests Chinese font size name (e.g. Wu Hao) but output is wrong + +**Symptom**: Font size doesn't match expected Chinese size name. + +**Fix**: Use the correct half-point value. `size` in docx-js is in half-points: +- Wu Hao 五号 = 10.5pt → `size: 21` +- Xiao Si 小四 = 12pt → `size: 24` +- Si Hao 四号 = 14pt → `size: 28` + +See SKILL.md for complete conversion table. + +--- + +## Bug: Black table cells + +**Symptom**: Table cells appear solid black in Word. + +**Fix**: Use `ShadingType.CLEAR` not `ShadingType.SOLID`: +```js +// ❌ WRONG +shading: { type: ShadingType.SOLID, fill: "F1F5F9" } +// ✅ CORRECT +shading: { type: ShadingType.CLEAR, fill: "F1F5F9" } +``` + +--- + +## Bug: Chinese characters garbled in matplotlib charts + +**Symptom**: Chinese text shows as empty boxes □□□ in generated PNG charts. + +**Fix**: Configure SimHei font before plotting: +```python +from matplotlib.font_manager import FontProperties +zh_font = FontProperties(fname="/path/to/SimHei.ttf") +plt.title("中文标题", fontproperties=zh_font) +plt.rcParams["axes.unicode_minus"] = False +``` + +--- + +## Bug: Image stretched/squashed in document + +**Symptom**: Embedded image appears distorted. + +**Fix**: Calculate display height from width using original aspect ratio: +```js +const aspectRatio = originalHeight / originalWidth; +const displayWidth = 500; +const displayHeight = Math.round(displayWidth * aspectRatio); +new ImageRun({ data: buf, transformation: { width: displayWidth, height: displayHeight }, type: "png" }); +``` + +--- + +## Bug: TOC shows empty in generated document + +→ See `references/toc.md` — "5 Common TOC Bugs" section for diagnosis and fixes. + +--- + +## Bug: PageBreak standalone crashes Word + +**Symptom**: Document fails to open or renders incorrectly. + +**Fix**: PageBreak must always be wrapped in a Paragraph: +```js +// ❌ WRONG — standalone +children: [new PageBreak()] +// ✅ CORRECT — inside Paragraph +children: [new Paragraph({ children: [new PageBreak()] })] +``` + +--- + +## Bug: Quotation marks break JavaScript syntax — ⚠️ #1 MOST COMMON BUG + +**This is the single most frequent code generation error.** Chinese text routinely uses curly quotes `""` for emphasis, proper nouns, and event names (e.g., "双11", "前低后高", "618"大促). These MUST be Unicode-escaped — bare curly quotes silently break JS syntax. + +**Rule: scan ALL Chinese text for `""''` and replace with `\u201c \u201d \u2018 \u2019` BEFORE writing the string.** + +```js +// ❌ WRONG — curly quotes in Chinese text break syntax (extremely common) +para("行业增速呈现"前低后高"的态势,在"618"大促拉动下增长。") +"他说"你好"" // \u201c \u201d +'It's a test' // \u2019 + +// ✅ CORRECT — Unicode escapes for ALL curly quotes +para("行业增速呈现\u201c前低后高\u201d的态势,在\u201c618\u201d大促拉动下增长。") +"他说\u201c你好\u201d" +"It\u2019s a test" + +// ✅ Straight quotes: escape or use alternate delimiters +"He said \"hello\"" +'He said "hello"' +``` + +--- + +## Bug: Unwanted blank pages in document + +**Common causes:** + +1. **Trailing PageBreak at end of last section** — pagination should use section breaks or be at the start of the next section +2. **Empty Paragraph overflow** — empty paragraphs at page bottom push to a new page +3. **PageBreak right after Table** — Table already at page bottom, PageBreak creates extra page + +**Fix:** +```js +// Post-generation check: last section's children should not end with PageBreak +function removeTrailingPageBreak(section) { + const children = section.children; + if (!children.length) return; + const last = children[children.length - 1]; + // If last element is a Paragraph containing only PageBreak, remove it + if (last instanceof Paragraph) { + const runs = last.root?.filter(c => c instanceof PageBreak); + if (runs?.length && !last.root?.some(c => c instanceof TextRun)) { + children.pop(); + } + } +} +``` + +**Prevention rules:** +- Place PageBreak at the **start of the next section**, not the end of the previous one +- Or use separate sections for pagination (no PageBreak needed) +- The last section of a document must NEVER end with a PageBreak + +--- + +## Bug: Different rendering in WPS vs Microsoft Word + +**Symptom**: Document looks correct in Word but renders differently in WPS (or vice versa) — misaligned tables, shifted content, clipped text in cells, black cells, or broken covers. + +**Root causes and fixes:** + +### 1. `ShadingType.SOLID` shows black in WPS +```js +// ❌ WPS shows solid black +shading: { type: ShadingType.SOLID, fill: "F1F5F9" } +// ✅ Both renderers show correct color +shading: { type: ShadingType.CLEAR, fill: "F1F5F9" } +``` + +### 2. `verticalAlign: "center"` in exact-height rows shifts content +WPS ignores vertical centering in `rule: "exact"` rows — content stays at top, creating visual mismatch. +```js +// ❌ Inconsistent between Word and WPS +new TableRow({ height: { value: 800, rule: "exact" }, + children: [new TableCell({ verticalAlign: VerticalAlign.CENTER, ... })] }) +// ✅ Use top alignment + margins/spacing for positioning +new TableRow({ height: { value: 800, rule: "exact" }, + children: [new TableCell({ verticalAlign: VerticalAlign.TOP, + margins: { top: 200 }, ... })] }) +``` + +### 3. Tab stops misalign in WPS +Tab widths differ between Word and WPS. Never use tabs for alignment. +```js +// ❌ Tab-based alignment — breaks in WPS +new Paragraph({ tabStops: [{ type: TabStopType.RIGHT, position: 8000 }], + children: [new TextRun({ text: "Party A:\tCompany Name" })] }) +// ✅ Borderless table for alignment — consistent everywhere +new Table({ borders: allNoBorders, rows: [new TableRow({ children: [ + new TableCell({ children: [new Paragraph({ children: [new TextRun({ text: "Party A:" })] })] }), + new TableCell({ children: [new Paragraph({ children: [new TextRun({ text: "Company Name" })] })] }), +] })] }) +``` + +### 4. Nested tables in exact-height cells overflow differently +Word calculates nested table heights more accurately than WPS. Use stacked tables instead. +```js +// ❌ Nested table inside exact-height cell +new TableRow({ height: { value: 16838, rule: "exact" }, + children: [new TableCell({ children: [nestedTable1, nestedTable2] })] }) +// ✅ Stacked approach — content table + filler table +[contentTable, fillerTable] // both at top level, heights sum to 16838 +``` + +### 5. `characterSpacing` renders differently +Large `characterSpacing` values cause inconsistent letter spacing. Keep ≤ 80. + +### 6. `titlePage: true` header/footer suppression +WPS may not correctly hide first-page headers when using `titlePage: true`. Use a separate section for the cover instead. + +--- + +## Bug: Cover spills to second page + +**Symptom**: Cover content overflows, with some elements (date, footer, accent strip) appearing on page 2. + +**Root cause**: Total content height exceeds 16838 twips (A4 page height). Common when: +- Title is very long (3+ lines at large font size) +- Fixed spacing values assume short title +- Multiple meta lines + subtitle + English label + +**Fix**: Always use `calcTitleLayout()` + `calcCoverSpacing()` from `design-system.md`. These dynamically adjust font sizes and spacing to fit within the page. See `design-system.md § Cover Content Overflow Prevention` for the complete checklist. + +--- + +## Bug: Blank page 2 after cover in MS Office (but not WPS) + +**Symptom**: Cover displays correctly in WPS but produces a blank second page in MS Office Word. + +**Root cause**: The cover wrapper table uses **default docx-js table borders** (`single/auto/sz=4`) instead of explicitly setting `allNoBorders`. Default borders add ~8 twips per edge. MS Office includes border thickness in the exact-height row calculation, pushing total height past 16838 twips → overflow to page 2. WPS is more lenient and absorbs the extra pixels. + +**Fix**: Every cover wrapper table MUST explicitly set `borders: allNoBorders`: +```js +const NB = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; +const allNoBorders = { top: NB, bottom: NB, left: NB, right: NB, + insideHorizontal: NB, insideVertical: NB }; + +new Table({ + borders: allNoBorders, // ← MANDATORY + rows: [new TableRow({ + height: { value: 16838, rule: "exact" }, + // ... + })], +}); +``` + +**Prevention**: Add to post-generation check — search for any `new Table` in cover code that does not explicitly set `borders`. + +--- + +## Bug: Cover decorative lines appear truncated or misaligned + +**Symptom**: Horizontal decorative lines on the cover (accent strips, divider rules) display at different widths in MS Office vs WPS, or appear truncated / not spanning the intended width. + +**Root cause**: Lines were implemented using text characters (`───`, `━━━`, `═══`, `——————`) instead of paragraph borders. Character-drawn lines depend on font metrics (character width × count), which vary across rendering engines. + +**Fix**: Always use **paragraph borders** for decorative lines: +```js +// ✅ Paragraph border — renders consistently in both MS Office and WPS +new Paragraph({ + indent: { left: 1000, right: 1000 }, + border: { top: { style: BorderStyle.SINGLE, size: 18, color: accentColor, space: 20 } }, + children: [], +}) + +// ❌ NEVER use text characters for decorative lines +new TextRun({ text: "───────────────" }) // width varies across engines +``` + +**Note**: This applies to ALL cover recipes (R1–R5). Recipe R2 uses `border.top` and `border.bottom` for its double-rule frame — follow this pattern. + +--- + +## Bug: "undefined" appears in document text + +**Symptom**: Fields like "Contact: undefined" or "Location: undefined" in generated documents. + +**Root cause**: JavaScript outputs the string `"undefined"` when accessing a property that doesn't exist on the config object. + +**Fix**: Use `safeText()` helper for ALL user-facing text values: +```js +function safeText(value, placeholder) { + if (value === undefined || value === null || value === "" || + String(value) === "NaN" || String(value) === "undefined") { + return placeholder || "【Please fill in】"; + } + return String(value); +} +// Usage: new TextRun({ text: safeText(config.contact, "【Contact person】") }) +``` diff --git a/skills/docx/references/math-formulas.md b/skills/docx/references/math-formulas.md new file mode 100755 index 0000000..3394e1f --- /dev/null +++ b/skills/docx/references/math-formulas.md @@ -0,0 +1,276 @@ +# Math Formulas — LaTeX → docx-js Mapping + +## Design Philosophy + +GLM uses **LaTeX as the formula input syntax**, internally converting to docx-js Math objects. + +**Why not write OMML directly?** +- Models are naturally proficient in LaTeX (abundant in training data) +- LaTeX is semantically clear and highly readable +- Conversion layer is encapsulated internally, transparent to the user + +## Quick Start + +```js +const { Math: OoxmlMath, MathRun, MathFraction, MathSuperScript, + MathSubScript, MathRadical, MathSum, MathSubSuperScript } = require("docx"); + +// Embed formula in paragraph +new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new OoxmlMath({ + children: [/* Math components */] + }) + ] +}) +``` + +## LaTeX → docx-js Conversion Table + +### Basic Operations + +| LaTeX | Meaning | docx-js Implementation | +|-------|---------|----------------------| +| `x + y` | Addition | `new MathRun("x + y")` | +| `x - y` | Subtraction | `new MathRun("x − y")` (use Unicode minus `−`) | +| `x \times y` | Multiplication | `new MathRun("x × y")` | +| `x \div y` | Division | `new MathRun("x ÷ y")` | +| `x \pm y` | Plus-minus | `new MathRun("x ± y")` | +| `x \neq y` | Not equal | `new MathRun("x ≠ y")` | +| `x \leq y` | Less or equal | `new MathRun("x ≤ y")` | +| `x \geq y` | Greater or equal | `new MathRun("x ≥ y")` | + +### Fractions + +| LaTeX | docx-js | +|-------|---------| +| `\frac{a}{b}` | `new MathFraction({ numerator: [new MathRun("a")], denominator: [new MathRun("b")] })` | +| `\frac{x+1}{x-1}` | `new MathFraction({ numerator: [new MathRun("x+1")], denominator: [new MathRun("x−1")] })` | + +### Superscripts & Subscripts + +| LaTeX | docx-js | +|-------|---------| +| `x^2` | `new MathSuperScript({ children: [new MathRun("x")], superScript: [new MathRun("2")] })` | +| `x_i` | `new MathSubScript({ children: [new MathRun("x")], subScript: [new MathRun("i")] })` | +| `x_i^2` | `new MathSubSuperScript({ children: [new MathRun("x")], subScript: [new MathRun("i")], superScript: [new MathRun("2")] })` | + +### Radicals + +| LaTeX | docx-js | +|-------|---------| +| `\sqrt{x}` | `new MathRadical({ children: [new MathRun("x")] })` | +| `\sqrt[3]{x}` | `new MathRadical({ children: [new MathRun("x")], degree: [new MathRun("3")] })` | + +### Summation & Integrals + +| LaTeX | docx-js | +|-------|---------| +| `\sum_{i=1}^{n}` | `new MathSum({ subScript: [new MathRun("i=1")], superScript: [new MathRun("n")], children: [new MathRun("aᵢ")] })` | + +### Greek Letters + +Use Unicode characters directly: + +```js +// LaTeX → Unicode mapping +const GREEK = { + "\\alpha": "α", "\\beta": "β", "\\gamma": "γ", "\\delta": "δ", + "\\epsilon": "ε", "\\zeta": "ζ", "\\eta": "η", "\\theta": "θ", + "\\iota": "ι", "\\kappa": "κ", "\\lambda": "λ", "\\mu": "μ", + "\\nu": "ν", "\\xi": "ξ", "\\pi": "π", "\\rho": "ρ", + "\\sigma": "σ", "\\tau": "τ", "\\phi": "φ", "\\chi": "χ", + "\\psi": "ψ", "\\omega": "ω", + "\\Alpha": "Α", "\\Beta": "Β", "\\Gamma": "Γ", "\\Delta": "Δ", + "\\Theta": "Θ", "\\Lambda": "Λ", "\\Pi": "Π", "\\Sigma": "Σ", + "\\Phi": "Φ", "\\Psi": "Ψ", "\\Omega": "Ω", +}; +``` + +## Complete Formula Examples + +### Quadratic Formula + +LaTeX: `x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}` + +```js +new OoxmlMath({ + children: [ + new MathRun("x = "), + new MathFraction({ + numerator: [ + new MathRun("−b ± "), + new MathRadical({ + children: [ + new MathSuperScript({ + children: [new MathRun("b")], + superScript: [new MathRun("2")], + }), + new MathRun(" − 4ac"), + ], + }), + ], + denominator: [new MathRun("2a")], + }), + ], +}) +``` + +### Pythagorean Theorem + +LaTeX: `a^2 + b^2 = c^2` + +```js +new OoxmlMath({ + children: [ + new MathSuperScript({ children: [new MathRun("a")], superScript: [new MathRun("2")] }), + new MathRun(" + "), + new MathSuperScript({ children: [new MathRun("b")], superScript: [new MathRun("2")] }), + new MathRun(" = "), + new MathSuperScript({ children: [new MathRun("c")], superScript: [new MathRun("2")] }), + ], +}) +``` + +### Trigonometric Identity + +LaTeX: `\sin^2\theta + \cos^2\theta = 1` + +```js +new OoxmlMath({ + children: [ + new MathSuperScript({ children: [new MathRun("sin")], superScript: [new MathRun("2")] }), + new MathRun("θ + "), + new MathSuperScript({ children: [new MathRun("cos")], superScript: [new MathRun("2")] }), + new MathRun("θ = 1"), + ], +}) +``` + +## Common Exam Formula Templates + +### Middle School Math + +```js +// Quadratic discriminant +const discriminant = new OoxmlMath({ + children: [ + new MathRun("Δ = "), + new MathSuperScript({ children: [new MathRun("b")], superScript: [new MathRun("2")] }), + new MathRun(" − 4ac"), + ], +}); + +// Circle area +const circleArea = new OoxmlMath({ + children: [ + new MathRun("S = π"), + new MathSuperScript({ children: [new MathRun("r")], superScript: [new MathRun("2")] }), + ], +}); +``` + +### High School Math + +```js +// Logarithm change of base +const logChange = new OoxmlMath({ + children: [ + new MathSubScript({ children: [new MathRun("log")], subScript: [new MathRun("a")] }), + new MathRun("b = "), + new MathFraction({ + numerator: [new MathRun("ln b")], + denominator: [new MathRun("ln a")], + }), + ], +}); + +// Arithmetic series sum +const arithmeticSum = new OoxmlMath({ + children: [ + new MathSubScript({ children: [new MathRun("S")], subScript: [new MathRun("n")] }), + new MathRun(" = "), + new MathFraction({ + numerator: [ + new MathRun("n("), + new MathSubScript({ children: [new MathRun("a")], subScript: [new MathRun("1")] }), + new MathRun(" + "), + new MathSubScript({ children: [new MathRun("a")], subScript: [new MathRun("n")] }), + new MathRun(")"), + ], + denominator: [new MathRun("2")], + }), + ], +}); +``` + +### Physics + +```js +// Newton's second law +const newton2 = new OoxmlMath({ + children: [new MathRun("F = ma")], +}); + +// Kinetic energy +const kineticEnergy = new OoxmlMath({ + children: [ + new MathSubScript({ children: [new MathRun("E")], subScript: [new MathRun("k")] }), + new MathRun(" = "), + new MathFraction({ + numerator: [new MathRun("1")], + denominator: [new MathRun("2")], + }), + new MathRun("m"), + new MathSuperScript({ children: [new MathRun("v")], superScript: [new MathRun("2")] }), + ], +}); +``` + +## Complexity Fallback Strategy + +When formulas are too complex (nesting >3 levels) for docx-js Math, **fall back to matplotlib PNG rendering:** + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt + +def latex_to_png(latex_str: str, output_path: str, fontsize: int = 14, dpi: int = 200): + """Render LaTeX formula as PNG image""" + fig, ax = plt.subplots(figsize=(0.1, 0.1)) + ax.axis("off") + text = ax.text(0, 0.5, f"${latex_str}$", fontsize=fontsize, + transform=ax.transAxes, verticalalignment="center") + + fig.canvas.draw() + bbox = text.get_window_extent(fig.canvas.get_renderer()) + fig.set_size_inches(bbox.width / dpi + 0.2, bbox.height / dpi + 0.2) + + plt.savefig(output_path, dpi=dpi, bbox_inches="tight", + pad_inches=0.05, transparent=True) + plt.close() + return output_path +``` + +Then embed the PNG in the document: + +```js +const formulaImg = fs.readFileSync("formula.png"); +new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new ImageRun({ + data: formulaImg, + transformation: { width: 300, height: 40 }, // adjust based on actual size + type: "png", + })], +}) +``` + +**Fallback rules:** +- Nested fractions >2 levels → fallback +- Matrices/determinants → fallback +- Complex integrals (multiple integrals + limits + integrand) → fallback +- Piecewise functions → fallback +- All other cases → prefer docx-js Math diff --git a/skills/docx/references/ooxml.md b/skills/docx/references/ooxml.md new file mode 100755 index 0000000..ae5cd45 --- /dev/null +++ b/skills/docx/references/ooxml.md @@ -0,0 +1,222 @@ +# OOXML Editing Reference — Document Library API + +**Important: Read this entire document before editing.** This is the primary reference for modifying existing .docx files. + +## Document Library (Python) — Primary API + +Use the `Document` class from `"$DOCX_SCRIPTS/document.py"` for all edits, tracked changes, and comments. It handles infrastructure automatically (people.xml, RSIDs, settings.xml, comments, relationships, content types). + +**Working with Unicode and Entities:** +- Both entity notation and Unicode work for search: `contains="“Company"` ≡ `contains="\u201cCompany"` +- Both work for replacement too + +### Setup + +```bash +# Find the docx skill root +find /mnt/skills -name "document.py" -path "*/docx/scripts/*" 2>/dev/null | head -1 +# Skill root = parent of scripts/ + +# Run with PYTHONPATH +PYTHONPATH=/mnt/skills/docx python your_script.py +``` + +```python +from scripts.document import Document, DocxXMLEditor + +# Basic init (auto-creates temp copy, sets up infrastructure) +doc = Document('unpacked') + +# Custom author/initials +doc = Document('unpacked', author="John Doe", initials="JD") + +# Enable tracked changes +doc = Document('unpacked', track_revisions=True) + +# Custom RSID (auto-generated if omitted) +doc = Document('unpacked', rsid="07DC5ECB") +``` + +### Finding Nodes + +```python +# By text +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 +para = doc["word/document.xml"].get_node(tag="w:p", line_number=42) + +# Combined filters (disambiguation) +node = doc["word/document.xml"].get_node(tag="w:r", contains="Section", line_number=range(2400, 2500)) +``` + +### Tracked Changes + +**CRITICAL**: Only mark text that actually changes. Keep unchanged text outside ``/`` tags. + +**Method Selection**: +- Regular text → `replace_node()` with ``/``, or `suggest_deletion()` for whole elements +- Partially modify another's tracked change → `replace_node()` to nest changes +- Reject another's insertion → `revert_insertion()` (NOT `suggest_deletion()`) +- Reject another's deletion → `revert_deletion()` + +```python +# Change one word: "monthly" → "quarterly" +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) + +# Delete entire run +node = doc["word/document.xml"].get_node(tag="w:r", contains="text to delete") +doc["word/document.xml"].suggest_deletion(node) + +# Delete entire paragraph +para = doc["word/document.xml"].get_node(tag="w:p", contains="paragraph to delete") +doc["word/document.xml"].suggest_deletion(para) + +# Insert new content after a node +node = doc["word/document.xml"].get_node(tag="w:r", contains="existing text") +doc["word/document.xml"].insert_after(node, 'new text') + +# 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) +``` + +### Handling Other Authors' Changes + +```python +# Partially delete another author's insertion +node = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) +replacement = ''' + quarterly + financial + report +''' +doc["word/document.xml"].replace_node(node, replacement) + +# Reject insertion (wraps in deletion) +ins = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "5"}) +doc["word/document.xml"].revert_insertion(ins) + +# Reject deletion (restores deleted content) +del_elem = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "3"}) +doc["word/document.xml"].revert_deletion(del_elem) +``` + +### Comments + +```python +doc = Document('unpacked', author="Z.ai", initials="Z") + +# Comment on a range +start = doc["word/document.xml"].get_node(tag="w:del", attrs={"w:id": "1"}) +end = doc["word/document.xml"].get_node(tag="w:ins", attrs={"w:id": "2"}) +doc.add_comment(start=start, end=end, text="Explanation of this change") + +# Comment on paragraph +para = doc["word/document.xml"].get_node(tag="w:p", contains="text") +doc.add_comment(start=para, end=para, text="Comment here") + +# Comment on newly created tracked change +node = doc["word/document.xml"].get_node(tag="w:r", contains="old") +new_nodes = doc["word/document.xml"].replace_node( + node, 'oldnew') +doc.add_comment(start=new_nodes[0], end=new_nodes[1], text="Changed per requirements") + +# Reply to comment +doc.reply_to_comment(parent_comment_id=0, text="I agree") +``` + +### Images + +```python +from PIL import Image +import shutil, os + +doc = Document('unpacked') +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 +height_emus = int(width_emus * img.size[1] / img.size[0]) + +# Add relationship +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 +node = doc["word/document.xml"].get_node(tag="w:p", line_number=100) +doc["word/document.xml"].insert_after(node, f''' + + + + + + + + + + + + + +''') +``` + +### Saving + +```python +doc.save() # Validates + copies back to original dir +doc.save('modified-unpacked') # Save to different location +doc.save(validate=False) # Skip validation (debug only) +``` + +### Direct DOM Manipulation + +```python +editor = doc["word/document.xml"] +node = doc["word/document.xml"].get_node(tag="w:p", line_number=5) +parent = node.parentNode +parent.removeChild(node) + +# General replacement (without tracked changes) +old = doc["word/document.xml"].get_node(tag="w:p", contains="original") +doc["word/document.xml"].replace_node(old, "replacement") + +# Chained insertions +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") +``` + +## Schema Compliance Quick Reference + +- **Element ordering in ``**: `` → `` → `` → `` → `` +- **Whitespace**: `xml:space='preserve'` on `` with leading/trailing spaces +- **RSIDs**: 8-digit hex only (0-9, A-F) +- **trackRevisions**: Add `` after `` in settings.xml +- **``/`` placement**: At paragraph level, containing complete `` elements. Never nest inside ``. + +## Validation Rules + +The validator ensures document text matches the original after reverting GLM's changes: +- **Never modify text inside another author's `` or `` tags** +- **Use nested deletions** to remove another author's insertions +- **Every edit must be tracked** with `` or `` tags diff --git a/skills/docx/references/toc.md b/skills/docx/references/toc.md new file mode 100755 index 0000000..5b7808e --- /dev/null +++ b/skills/docx/references/toc.md @@ -0,0 +1,264 @@ +# Table of Contents (TOC) — Complete Reference + +> **This is the single source of truth for all TOC rules.** Other files should reference this file instead of duplicating TOC instructions. + +## Overview + +DOCX TOC is a **3-step process**: Code → Post-process → User opens Word. + +``` +Step A: docx-js code generates empty TOC field structure +Step B: add_toc_placeholders.py fills it with visible placeholder entries +Step C: User opens Word → "Update Field" → real page numbers replace placeholders +``` + +All 3 steps are **mandatory**. Skipping any step results in a broken or empty TOC. + +## When to Add TOC + +- **Recommended**: Long or complex documents with many headings (reports, theses, papers, manuals) +- **Do NOT add**: Resumes, contracts, letters, exam papers, short documents +- **postcheck rule**: If document contains a "目录" title but no `TableOfContents` element → error + +## Step A: Code Generation (docx-js) + +Insert **4 elements** in sequence: + +```js +const { TableOfContents, Paragraph, TextRun, PageBreak, AlignmentType } = require("docx"); + +// 1. TOC title — ⛔ DO NOT use HeadingLevel (or TOC will index itself!) +new Paragraph({ + alignment: AlignmentType.CENTER, + spacing: { before: 480, after: 360 }, + children: [new TextRun({ + text: "目 录", // or "Table of Contents" for English docs + bold: true, size: 32, + font: { eastAsia: "SimHei", ascii: "Times New Roman" } + })], +}), + +// 2. TOC field element — ⚠️ first parameter is NOT displayed, it's internal name only +new TableOfContents("Table of Contents", { + hyperlink: true, + headingStyleRange: "1-3", // match HeadingLevel range used in document +}), + +// 3. ★ MANDATORY Refresh Hint — tells user how to update page numbers +new Paragraph({ + spacing: { before: 200 }, + children: [new TextRun({ + text: "Note: This Table of Contents is generated via field codes. To ensure page number accuracy after editing, please right-click the TOC and select \"Update Field.\"", + italics: true, size: 18, color: "888888" + })] +}), + +// 4. ★ MANDATORY PageBreak after TOC — prevents TOC and body merging on same page +new Paragraph({ children: [new PageBreak()] }), +``` + +### Heading Requirements + +**⚠️ CRITICAL**: TOC only picks up paragraphs with `heading: HeadingLevel.HEADING_X`. + +```js +// ✅ Correct — Heading style, TOC can index +new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: [new TextRun({ text: "第一章 引言", bold: true, size: 32, color: c(P.primary) })] +}) + +// ❌ Wrong — manual bold + large font, TOC cannot detect +new Paragraph({ + children: [new TextRun({ text: "第一章 引言", bold: true, size: 32, color: c(P.primary) })] +}) +``` + +**Exceptions:** +- Cover title: does NOT need Heading style (should not appear in TOC) +- "目录" title: **MUST NOT** use Heading style (prevents TOC from indexing itself) + +## Step B: Post-Processing Script + +**MUST** run after generating the DOCX file: + +```bash +python3 "$DOCX_SCRIPTS/add_toc_placeholders.py" output.docx --auto +``` + +### What the script does + +1. Extracts Heading 1-3 from the document as TOC entries +2. Fixes docx-js fldChar structure bug (begin+instrText+separate merged in one ``) +3. Patches `settings.xml` with `updateFields=true` (Word prompts to refresh on open) +4. Ensures Heading styles have `outlineLvl` (required for TOC field update) +5. Ensures TOC 1/2/3 styles exist in `styles.xml` +6. Injects placeholder entries with HYPERLINK + PAGEREF between `separate` and `end` fldChars +7. Handles duplicate heading texts (each gets its own bookmark) + +### Error handling + +The script **exits with code 1** if: +- No TOC field structure found (missing `TableOfContents` element) +- TOC field has `begin` but no `separate` fldChar (malformed structure) +- Field structure exists but no TOC instrText detected + +**If exit code = 1 → the generated code is wrong. Fix the code and regenerate.** + +### Options + +```bash +# Auto mode (recommended — default behavior) +python3 "$DOCX_SCRIPTS/add_toc_placeholders.py" output.docx --auto + +# Manual entries +python3 "$DOCX_SCRIPTS/add_toc_placeholders.py" output.docx \ + --entries '[{"level":1,"text":"Chapter 1","page":"1"},{"level":2,"text":"Section 1.1","page":"2"}]' +``` + +## Step C: User Opens in Word/WPS + +- **Word**: Detects `updateFields=true` → prompts "Update field?" → click Yes → real page numbers +- **WPS**: May NOT auto-prompt. User must: right-click TOC → "Update Field" → "Update entire table" + +The placeholder entries ensure TOC is **not blank** even without updating — users see heading titles with approximate page numbers. + +## Multi-Section Page Numbering + +When a document has a TOC, the TOC MUST be in its own section so that body page numbering starts from 1. This applies to **all document types with a TOC** (reports, whitepapers, PRDs, academic papers, etc.) — not just academic papers. + +**Mandatory 3-section architecture for documents with cover + TOC:** + +```js +sections: [ + { /* Section 1: Cover — no page number, no footer */ + properties: { + page: { size: pgSize, margin: pgMargin }, + // ⚠️ Do NOT set page.pageNumbers here — docx-js emits empty which confuses WPS + }, + }, + { /* Section 2: Front matter (abstract, TOC) — Roman numerals */ + properties: { + type: SectionType.NEXT_PAGE, + page: { + size: pgSize, margin: pgMargin, + pageNumbers: { start: 1, formatType: NumberFormat.UPPER_ROMAN }, // I, II, III... + }, + }, + footers: { default: pageNumFooter() }, // see footer rules below + children: [/* abstract + TOC title + TableOfContents + PageBreak */] + }, + { /* Section 3: Body — Arabic numerals starting from 1 */ + properties: { + type: SectionType.NEXT_PAGE, + page: { + size: pgSize, margin: pgMargin, + pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL }, // 1, 2, 3... + }, + }, + footers: { default: pageNumFooter() }, + children: [/* body content */] + }, +] +``` + +### ⚠️ Page Number API — Correct Nesting (CRITICAL) + +Page number settings MUST be nested inside `page.pageNumbers`, NOT at properties top level: + +```js +// ❌ WRONG — docx-js ignores these, pgNumType will be empty +properties: { + pageNumberStart: 1, + pageNumberFormatType: NumberFormat.DECIMAL, +} + +// ✅ CORRECT — docx-js writes start= and fmt= attributes +properties: { + page: { + pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL }, + }, +} +``` + +### ⚠️ Footer Field Instruction — WPS Compatibility (CRITICAL) + +WPS may ignore `pgNumType fmt` in the section properties. To ensure correct display, the footer PAGE field **MUST** include an explicit format switch via **post-processing**: + +After generating the docx, unzip and patch each footer XML: +- **Roman numeral footer**: replace `PAGE` with `PAGE \* ROMAN \\** MERGEFORMAT` +- **Arabic numeral footer**: replace `PAGE \* arabic \* MERGEFORMAT` + +**⚠️ NEVER use `\* decimal` in instrText** — `decimal` is a docx-js API enum value (`NumberFormat.DECIMAL` for `pgNumType` XML attribute), NOT a valid Word field format switch. Using it causes page numbers to render as "1decimal", "2decimal". The correct Word field switch for Arabic numerals is always `\* arabic`. + +```js +// Post-process footer XML: +footerXml = footerXml.replace( + /(]*>)\s*PAGE\s*(<\/w:instrText>)/g, + '$1 PAGE \\* ROMAN \\** MERGEFORMAT $2' // or "arabic" for body section +); +``` + +Also remove any empty `` from the cover section (docx-js emits these even when no pageNumbers is set): +```js +docXml = docXml.replace(//g, ""); +``` + +### Page Numbering Rules + +| Section | Content | Format | Start | Footer | +|---------|---------|--------|-------|--------| +| Cover | Title page | None | — | No footer | +| Front matter | Abstract, TOC | Roman (I, II, III) | 1 | `PAGE \* ROMAN` | +| Body | Main content | Arabic (1, 2, 3) | 1 | `PAGE \* arabic` | + +⚠️ **The body section MUST set `pageNumbers: { start: 1 }`** — otherwise page numbers continue from the front matter pages, causing TOC page references to be offset. This is the #1 cause of "TOC page numbers are wrong". + +### Common Causes of Incorrect Page Numbers + +| Cause | Fix | +|-------|-----| +| `pageNumberStart` at properties top level | Move to `page: { pageNumbers: { start: 1 } }` | +| Cover section emits empty `` | Post-process to remove it | +| Footer uses bare `PAGE` without format switch | Post-process to add `\* roman` or `\* arabic` | +| Cover and body in same section | Separate cover into its own section | +| Multiple sections without pageNumbers.start | Explicitly set on each section needing independent counting | +| headingStyleRange doesn't match headings | Ensure `headingStyleRange: "1-3"` covers all HeadingLevel values used | +| Cover section has header/footer | Don't set header/footer on cover section | + +## TOC Refresh Hint (MANDATORY) + +**⚠️ When the document contains a TOC, you MUST add the following hint paragraph between the `TableOfContents` element and the PageBreak (so it appears on the TOC page, not the body page).** This ensures users know how to refresh page numbers after editing. + +```js +new Paragraph({ + spacing: { before: 200 }, + children: [new TextRun({ + text: "Note: This Table of Contents is generated via field codes. To ensure page number accuracy after editing, please right-click the TOC and select \"Update Field.\"", + italics: true, size: 18, color: "888888" + })] +}), +``` + +## 5 Common TOC Bugs + +| # | Bug | Symptom | Fix | +|---|-----|---------|-----| +| 1 | "目录" heading uses `HeadingLevel.HEADING_1` | TOC includes "目录" as an entry | Remove `heading:` from TOC title paragraph | +| 2 | No `PageBreak` after `TableOfContents` | TOC and body text on same page | Add `new Paragraph({ children: [new PageBreak()] })` after TOC | +| 3 | Missing `TableOfContents` element | Script cannot inject placeholders, TOC is empty | Always include `new TableOfContents(...)` in code | +| 4 | Headings use bold+large instead of `HeadingLevel` | TOC is empty even after running script | Change all body headings to `heading: HeadingLevel.HEADING_X` | +| 5 | Script not run or exit code ignored | TOC page shows only title + blank space | Always run script; if exit code = 1, fix code and regenerate | + +## Checklist (for self-check during generation) + +- [ ] Document has 3+ H1 → TOC is included +- [ ] "目录" heading does NOT use `HeadingLevel` (prevents self-indexing) +- [ ] `new TableOfContents(...)` element present (not just plain text) +- [ ] `PageBreak` exists after TOC element (prevents merging with body) +- [ ] All body chapter headings use `heading: HeadingLevel.HEADING_X` +- [ ] `add_toc_placeholders.py --auto` runs after generation +- [ ] Script exit code checked — if 1, fix code and regenerate +- [ ] TOC page has visible placeholder content (not empty) +- [ ] **TOC Refresh Hint present** — italic gray note after TOC PageBreak telling user to right-click → "Update Field" +- [ ] `outlineLevel: 0` for H1, `1` for H2, etc. (needed for TOC field update) diff --git a/skills/docx/routes/comment.md b/skills/docx/routes/comment.md new file mode 100755 index 0000000..8a8252b --- /dev/null +++ b/skills/docx/routes/comment.md @@ -0,0 +1,88 @@ +# Route: Add Comments + +## Method 1: python-docx (Recommended — Simple) + +```python +from docx import Document +from docx.oxml.ns import qn +from docx.oxml import OxmlElement +from datetime import datetime + +def add_comment(paragraph, comment_text, author="GLM", initials="G"): + """Add a comment to an entire paragraph.""" + # Create comment reference + comment_id = str(hash(comment_text) % 10000) + + # Add to comments.xml (need to create if not exists) + # ... complex XML manipulation required + pass + +# Simpler approach: use python-docx-ng or manipulate XML directly +``` + +**Note**: python-docx has limited native comment support. For reliable results, use the OOXML method. + +## Method 2: OOXML Direct Manipulation (Reliable) + +### Step 1: Unpack + +```bash +mkdir work && cd work && unzip ../input.docx +``` + +### Step 2: Create/update word/comments.xml + +```xml + + + + + + This section needs more detail. + + + + +``` + +### Step 3: Mark comment range in document.xml + +```xml + +Text being commented on + + + + + +``` + +### Step 4: Update relationships + +In `word/_rels/document.xml.rels`, add: +```xml + +``` + +### Step 5: Update Content_Types + +In `[Content_Types].xml`, ensure: +```xml + +``` + +### Step 6: Pack + +```bash +zip -r ../output.docx . -x ".*" +``` + +## When to Use Each Method + +| Scenario | Method | +|----------|--------| +| Add 1-2 simple comments | OOXML | +| Batch review (many comments) | OOXML with Python script | +| Comment on specific words | OOXML (precise range control) | +| Quick annotation | python-docx if available | diff --git a/skills/docx/routes/create.md b/skills/docx/routes/create.md new file mode 100755 index 0000000..8a3bff9 --- /dev/null +++ b/skills/docx/routes/create.md @@ -0,0 +1,207 @@ +# Route: Create New Document + +## Workflow + +``` +0. Check if user provided a reference template (PDF/docx) → if yes, use Template-Following Mode below +1. Load `references/design-system.md` → select palette and cover recipe +2. Load `references/common-rules.md` → shared layout, font, placeholder rules +3. Check user keywords → load scene file if applicable +4. Load `references/docx-js-core.md` +5. If complex → also load `references/docx-js-advanced.md` +6. Plan document structure (outline) +7. Write JS/TS using docx library + ⚠️ **BEFORE writing any string**: scan ALL Chinese text for curly quotes `""''` and replace with `\u201c \u201d \u2018 \u2019` — bare curly quotes break JS syntax (see docx-js-advanced.md § Quotes Escaping) +8. Run with `bun run generate.js` (or `node generate.js`) +9. If TOC → run `python3 "$DOCX_SCRIPTS/add_toc_placeholders.py" output.docx --auto` +10. Run post-generation checklist (see SKILL.md) +``` + +## Template-Following Mode + +When the user provides a reference document (PDF/docx) as a **formatting template** (e.g., "generate following this template format", "refer to this sample"), switch to template-following mode instead of the standard recipe-based workflow: + +1. **Extract the template's structure** — cover layout, section order, heading hierarchy, page breaks, special pages (e.g., advisor comments page, approval form) +2. **Replicate structure exactly** — every major structural unit becomes a **separate section** (cover, body, appendix/form pages) with appropriate margins and page breaks +3. **Fill content** from the user's content source, or generate per user instructions +4. **Preserve template-specific elements** — school-specific forms, signature areas, stamp placeholders, advisor comment blocks → reproduce as-is with placeholder text (e.g., "Advisor (signature):") +5. **Maintain formatting fidelity** — font choices, table layouts, spacing, and alignment should match the template, not the standard design-system palettes + +⚠️ **Do NOT apply standard cover recipes (R1–R7) when a user-provided template defines its own cover format.** Follow the template's cover layout instead. Standard `common-rules.md` constraints (e.g., `WidthType.PERCENTAGE`, `allNoBorders` for cover wrapper, `Rule 8` line spacing) still apply for cross-engine compatibility. + +⚠️ **Each distinct page type = separate section.** Cover section (margin: 0), body section (standard margins), appendix/form pages (may need different margins or orientation). Never place cover + body + appendix in a single section. + +--- + +## Decision Tree + +### Cover Page? +- **YES**: Reports, theses, proposals, plans, or 3+ page docs with clear title/author +- **NO**: Resumes, contracts, official documents, exam papers, short memos + +### Cover Style Selector — Recipe Router + +Covers use **7 validated layout recipes (R1–R7)**, auto-selected by `selectCoverRecipe()` in `references/design-system.md` (the **authoritative source** — do NOT duplicate the function). + +**Quick Reference:** + +| docType | Recipe | Default Palette | +|---------|--------|-----------------| +| contract / official / exam / resume | null (no cover) | — | +| academic | R5 (Clean White) | ACADEMIC | +| proposal_report (thesis proposal) | R5 (Clean White) | ACADEMIC | +| lesson_plan (STEM) | R4 (Top Color Block) | DM-1 | +| lesson_plan (arts/general) | R6 (Editorial Warm) | ED-1 | +| creative / branding / design | R3 (Centered Card Frame) | SN-2 | +| cultural / newsletter / internal | R6 (Editorial Warm) | ED-1 | +| activity / event | R6 (Editorial Warm) | ED-1 | +| trend/research (cultural/creative/brand) | R7 (Swiss Tech) | ST-1 | +| whitepaper | R2 (Double-Rule Frame) | IG-1 / CM-2 | +| consulting | R2 (Double-Rule Frame) | MIN-1 | +| proposal / plan | R4 (Top Color Block) | GO-1 | +| report | R1 (Pure Paragraph Left) | by industry | +| default | R1 (Pure Paragraph Left) | DS-1 | + +⚠️ **Long title routing:** After selecting recipe, apply `applyLongTitleOverride(result, titleLength)`. Titles >20 chars on R3/R4/R6 → fall back to R1. Titles >30 chars on R2 → fall back to R1. R5 is never overridden. + +⚠️ **Academic thesis cover:** Use `buildAcademicCover()` from `scenes/academic.md`. + +⚠️ **Thesis proposal report (开题报告):** Use `buildProposalCover()` from `scenes/academic.md`. Cover MUST be an independent section. Keywords: "开题报告" (Chinese), "thesis proposal", "research proposal" — NOT the same as business proposals (which use R4). + +### Table of Contents? +- **YES**: 3+ major sections (H1 headings) +- **NO**: Resumes, exam papers, short docs, contracts (<20 clauses) + +→ See `references/toc.md` for the complete TOC reference (3-step process, code examples, common bugs). + +### Headers/Footers? +- **YES** by default (page numbers minimum) +- **NO**: cover page section, official docs (special format) + +### Load Math Formulas? +When: exam papers, academic papers, physics/math/chemistry → load `references/math-formulas.md` + +### Load Chart Templates? +When: data visualization, reports with charts → load `references/chart-templates.md` + +## Outline Rules + +**User provides outline** → Follow EXACTLY. No additions, deletions, or reordering. + +**No outline** → Create from scene template: +- **Academic:** Abstract → TOC → Body → References +- **Report:** Use `selectReportType()` to determine type, then follow template A–F: + - analysis → Template A (Executive Summary → Background → Scope & Method → Findings → Diagnosis → Conclusions) + - experiment → Template B (Abstract → Objective & Hypothesis → Environment → Procedure → Results → Error Analysis → Conclusions) + - testing → Template C (Overview → Scope & Environment → Test Plan → Results → Defects → Risks → Conclusions) + - research → Template D (Summary → Background → Subjects & Method → Sample → Findings → Synthesis → Recommendations) + - review → Template E (Overview → Goals → Review → Results → Issues → Lessons → Action Plan) + - proposal → Template F (Summary → Status → Goals → Solution → Roadmap → Resources → Risks → Benefits) +- **Contract:** Use `selectContractType()` then follow template A–E: + - bilateral → Template A (Header → Parties → Recitals → Definitions → Subject → Price → Rights → Delivery → Tax → IP → Breach → Force Majeure → Termination → Notices → Dispute → Miscellaneous → Signature) + - transfer → Template B (Header → Recitals → Definitions → Subject → Consideration → Closing → Representations → Tax → Breach → Dispute → Signature) + - nda → Template C (Header → Recitals → Definition → Obligations → Use Restrictions → Return/Destroy → Exceptions → Duration → Breach → Dispute → Signature) + - framework → Template D (Header → Recitals → Purpose → Scope → Division → Mechanism → Commercial → Confidentiality → Term → Breach → Dispute → Signature) + - terms → Template E (Title → Definitions → Services → Rights → Liability → Fees → IP → Termination → Notices → Dispute → Miscellaneous) +- **Official:** Use `selectOfficialType()` + `needsRedHeader()`: + - notice → Template A ([Red header] → [Doc number] → Title → Addressee → Reason → Items → Requirements → [Attachments] → [Signature] → [Date] → [Colophon]) + - letter → Template B ([Red header] → [Doc number] → Title → Addressee → Reason → Negotiation/Reply → Closing → [Signature] → [Date]) + - reply → Template C ([Red header] → [Doc number] → Title → Addressee → Reference → Reply → "This is the reply." → Signature → Date) + - minutes → Template D (Title → Meeting Overview → Agreed Items → Responsibilities → [Distribution]) — typically no red header +- Present outline to user before generating when possible + +## Scene Completeness + +Include ALL elements a scene specifies: +- **Academic thesis:** Cover (`buildAcademicCover()` in its own section), abstract, TOC, references +- **Thesis proposal report (thesis proposal / 开题报告):** Cover (`buildProposalCover()` in its own section), body sections per proposal template. Cover MUST be a separate section. +- **Report:** Cover, executive summary, conclusions +- **Contract:** Party info, recitals, complete clause closure, signature block, uniform `【】` placeholders +- **Official:** Correct document type, specific title, closing phrase matching type, proper numbering hierarchy, red header only when requested +- **Exam:** Student info area, scoring criteria + +Generate complete, substantive content — not skeletons. + +## Content Guidelines + +- **Length**: "detailed report" = 3000+ words. "brief summary" = 500–1000. +- **Data**: Use user's data, or generate realistic placeholders +- **Charts**: Use `references/chart-templates.md` matplotlib templates → PNG → embed +- **Math**: Use `references/math-formulas.md` LaTeX → docx-js Math mapping +- **Tables**: For structured data, not layout +- **Numbering**: Figures, tables numbered sequentially with cross-references + +## Code Architecture + +### Heading Style Rule (Mandatory) + +**All body chapter headings MUST use `heading: HeadingLevel.HEADING_X`** — never simulate with bold + large font (TOC cannot detect simulated headings). + +**Exception:** Cover title and TOC title ("目录") heading MUST NOT use Heading style. + +### Blank Page Prevention + +→ See SKILL.md § Post-Generation checklist for the full set of rules. + +Key rules: +1. No double page breaks (SectionType.NEXT_PAGE + PageBreak = blank page) +2. PageBreak paragraphs should have visible text content +3. No more than 3 consecutive empty paragraphs +4. Cover section: ≤2 trailing empty paragraphs, no trailing PageBreak + +### Builder Pattern Example + +```js +const { Document, Packer, Paragraph, TextRun, Header, Footer, + AlignmentType, HeadingLevel, PageNumber } = require("docx"); +const fs = require("fs"); + +// 1. Palette +const P = { primary: "#101820", body: "#182030", secondary: "#506070", accent: "#8090A0" }; +const c = (hex) => hex.replace("#", ""); + +// 2. Component builders +function heading(text, level = HeadingLevel.HEADING_1) { + return new Paragraph({ + heading: level, + spacing: { before: level === HeadingLevel.HEADING_1 ? 360 : 240, after: 120 }, + children: [new TextRun({ text, bold: true, color: c(P.primary), font: { ascii: "Calibri", eastAsia: "SimHei" } })] + }); +} + +function body(text) { + return new Paragraph({ + alignment: AlignmentType.JUSTIFIED, + indent: { firstLine: 480 }, + spacing: { line: 312 }, + children: [new TextRun({ text, size: 24, color: c(P.body) })], + }); +} + +// 3. Assembly — cover + body in separate sections +const doc = new Document({ + styles: { default: { document: { + run: { font: { ascii: "Calibri", eastAsia: "Microsoft YaHei" }, size: 24, color: c(P.body) }, + paragraph: { spacing: { line: 312 } }, + }}}, + sections: [ + { properties: { page: { margin: { top: 0, bottom: 0, left: 0, right: 0 } } }, + children: buildCoverR1(config) }, // ← use recipe from design-system.md + { properties: { page: { margin: { top: 1440, bottom: 1440, left: 1701, right: 1417 } } }, + footers: { default: new Footer({ children: [new Paragraph({ alignment: AlignmentType.CENTER, + children: [new TextRun({ children: [PageNumber.CURRENT], size: 18 })] })] }) }, + children: [heading("Chapter 1"), body("Content...")] }, + ], +}); + +Packer.toBuffer(doc).then(buf => { fs.writeFileSync("output.docx", buf); }); +``` + +## Post-Generation + +→ See SKILL.md § Post-Generation for the complete two-layer verification checklist. + +```bash +python3 "$DOCX_SCRIPTS/postcheck.py" output.docx +``` +⚠️ **Running postcheck.py is MANDATORY.** Fix all ❌ errors before delivering. diff --git a/skills/docx/routes/edit.md b/skills/docx/routes/edit.md new file mode 100755 index 0000000..7beee72 --- /dev/null +++ b/skills/docx/routes/edit.md @@ -0,0 +1,115 @@ +# Route: Edit Existing Document + +## Workflow Overview + +``` +1. Receive .docx (or .doc → convert) +2. Unpack → working directory +3. Analyze structure (document.xml, styles.xml) +4. Plan changes → batch by type +5. Implement via Document library (Python) +6. Pack → output.docx +7. Verify (pandoc or visual) +``` + +## Step 0: Format Conversion + +```bash +# .doc → .docx +libreoffice --headless --convert-to docx input.doc +``` + +## Step 1: Unpack + +```bash +mkdir -p work_dir && cd work_dir && unzip ../input.docx +``` + +Key files: `word/document.xml` (content), `word/styles.xml` (styles), `word/numbering.xml` (lists), `word/media/` (images), `[Content_Types].xml`, `word/_rels/document.xml.rels` + +## Step 2: Plan Changes + +Group changes into batches, process in order: + +1. **Structural** — Add/remove sections, reorder paragraphs +2. **Style** — Font, size, color modifications +3. **Text** — Find/replace, fix typos +4. **Table** — Add/remove rows/columns, update data +5. **Image** — Replace/add images + +## Step 3: Implement + +Load `references/ooxml.md` for the full Document library API. Key patterns: + +```python +from scripts.document import Document + +doc = Document('work_dir') + +# Text replacement with tracked changes +node = doc["word/document.xml"].get_node(tag="w:r", contains="old text") +rpr = tags[0].toxml() if (tags := node.getElementsByTagName("w:rPr")) else "" +replacement = f'{rpr}old text{rpr}new text' +doc["word/document.xml"].replace_node(node, replacement) + +doc.save() +``` + +## Step 4: Pack + +```bash +cd work_dir && zip -r ../output.docx . -x ".*" +``` + +## Step 5: Verify + +```bash +pandoc output.docx -t plain -o /dev/stdout | head -50 +# or visual +libreoffice --headless --convert-to pdf output.docx +``` + +--- + +## Template Matching Workflow + +When user says "use this format" or provides a template: + +1. Unpack template, extract `styles.xml`, `numbering.xml` +2. Analyze font/size/spacing/margins +3. Copy `styles.xml` into target document +4. Match heading hierarchy and spacing + +## Multi-File Merge + +1. Use first document as base +2. Extract content from additional documents +3. Insert with page breaks between sections +4. Merge styles (prefer base document's) +5. Re-number figures/tables sequentially + +## Redlining (Tracked Changes) — Default for Revisions + +When user asks for revisions, **default to tracked changes** so they can review: + +```python +doc = Document('work_dir', track_revisions=True) +# ... make changes using replace_node with / +doc.save() +``` + +Ask user if they want clean output or tracked changes only if ambiguous. + +## Common Operations Quick Reference + +| Operation | Approach | +|-----------|----------| +| Replace text | `get_node` + `replace_node` with tracked changes | +| Change font | Modify `` in run properties | +| Add paragraph | `insert_after` with `` element | +| Delete paragraph | `suggest_deletion` on `` | +| Add table row | Clone ``, modify cells | +| Update header | Edit `word/headerN.xml` | +| Change margins | Edit `` in `` | +| Add image | See `references/ooxml.md` image insertion pattern | +| Add comment | `doc.add_comment(start, end, text)` | diff --git a/skills/docx/routes/format.md b/skills/docx/routes/format.md new file mode 100755 index 0000000..3a70af8 --- /dev/null +++ b/skills/docx/routes/format.md @@ -0,0 +1,120 @@ +# Route: Format / Layout + +## Workflow + +``` +1. Read current document (pandoc for content, unpack for structure) +2. Identify format requirements from user +3. Use unit conversion table (see SKILL.md) +4. Apply formatting via OOXML manipulation or python-docx +5. Pack and verify +``` + +## Quick Formatting via python-docx + +For simple formatting tasks, python-docx is often faster than raw XML: + +```python +from docx import Document as PythonDocument +from docx.shared import Pt, Cm, Twips +from docx.enum.text import WD_ALIGN_PARAGRAPH + +doc = PythonDocument("input.docx") + +# Change all body paragraph formatting +for para in doc.paragraphs: + if para.style.name.startswith("Heading"): + continue + para.paragraph_format.first_line_indent = Twips(420) + para.paragraph_format.line_spacing = 1.5 + para.paragraph_format.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY + for run in para.runs: + run.font.name = "宋体" + run.font.size = Pt(12) # Xiao Si 小四 + +doc.save("output.docx") +``` + +## Common Format Request Patterns + +### University Thesis Formatting + +Typical Chinese university thesis requirements: + +```python +from docx.shared import Cm, Pt, Twips + +# Margins +for section in doc.sections: + section.top_margin = Cm(2.5) + section.bottom_margin = Cm(2.5) + section.left_margin = Cm(3.0) + section.right_margin = Cm(2.5) + +# Fonts +# Body: SimSun 宋体 Xiao Si 小四 (12pt) +# H1: SimHei 黑体 San Hao 三号 (16pt) centered +# H2: SimHei 黑体 Si Hao 四号 (14pt) +# H3: SimHei 黑体 Xiao Si 小四 (12pt) +# English: Times New Roman, same sizes +``` + +### Page Numbers Starting from Specific Page + +Use multi-section approach: +```python +# Section 1: Front matter (Roman numerals) +# Section 2: Main content (Arabic, starting from 1) +# This requires OOXML manipulation — see routes/edit.md for unpack/pack workflow +``` + +In raw XML (`word/document.xml`): +```xml + + + + + + + +``` + +### Different Headers Per Section + +Each section in a .docx can have its own header/footer. See `references/docx-js-advanced.md` for the multi-section approach. + +For existing documents, modify `word/document.xml` to split `` and create separate `headerN.xml` files. + +### Font Size Conversion + +When user requests a Chinese font size name: + +| Request | Action | +|---------|--------| +| "Change to Wu Hao (5th) size" | `font.size = Pt(10.5)` or `size: 21` in docx-js | +| "Title in San Hao SimHei" | `font.size = Pt(16)`, `font.name = "SimHei"` | +| "Body in Xiao Si SimSun" | `font.size = Pt(12)`, `font.name = "SimSun"` | + +### Line Spacing Adjustment + +```python +from docx.shared import Twips + +# 1.0x spacing +para.paragraph_format.line_spacing_rule = WD_LINE_SPACING.MULTIPLE +para.paragraph_format.line_spacing = 1.0 + +# 1.3x spacing (our default) +para.paragraph_format.line_spacing = 1.5 + +# Fixed spacing (e.g., 28pt) +para.paragraph_format.line_spacing_rule = WD_LINE_SPACING.EXACTLY +para.paragraph_format.line_spacing = Pt(28) +``` + +## Verification + +After formatting changes: +1. Open in LibreOffice or convert to PDF for visual check +2. Extract text with pandoc to ensure content unchanged +3. Compare file sizes (formatting-only changes shouldn't dramatically change size) diff --git a/skills/docx/routes/read.md b/skills/docx/routes/read.md new file mode 100755 index 0000000..c27eff2 --- /dev/null +++ b/skills/docx/routes/read.md @@ -0,0 +1,114 @@ +# Route: Read / Analyze / Extract + +## Method 1: Text Extraction via pandoc (Fastest) + +```bash +# Plain text +pandoc input.docx -t plain -o output.txt + +# Markdown (preserves structure) +pandoc input.docx -t markdown -o output.md + +# Extract with metadata +pandoc input.docx -t markdown --standalone -o output.md +``` + +**Best for**: Quick content reading, text analysis, word count, searching. + +## Method 2: Raw XML Access (Detailed) + +```bash +mkdir work && cd work && unzip ../input.docx + +# Read main content +cat word/document.xml + +# Read styles +cat word/styles.xml + +# List embedded media +ls word/media/ + +# Read headers/footers +cat word/header1.xml +cat word/footer1.xml +``` + +**Best for**: Analyzing formatting, extracting styles, inspecting document structure, debugging layout issues. + +### Quick XML Parsing + +```python +import defusedxml.ElementTree as ET + +tree = ET.parse("word/document.xml") +ns = {"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main"} + +# Extract all text +texts = [] +for t in tree.iter("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}t"): + if t.text: + texts.append(t.text) +full_text = "".join(texts) + +# Count paragraphs +paras = tree.findall(".//w:p", ns) +print(f"Paragraphs: {len(paras)}") + +# Find headings +for para in paras: + pPr = para.find("w:pPr", ns) + if pPr is not None: + pStyle = pPr.find("w:pStyle", ns) + if pStyle is not None and "Heading" in pStyle.get(f"{{{ns['w']}}}val", ""): + text = "".join(t.text for t in para.iter(f"{{{ns['w']}}}t") if t.text) + print(f" {pStyle.get(f'{{{ns[\"w\"]}}}val')}: {text}") +``` + +## Method 3: Convert to Images (Visual Analysis) + +```bash +# Convert to PDF first +libreoffice --headless --convert-to pdf input.docx + +# Then to images +pdftoppm -png -r 200 input.pdf page + +# Generates page-1.png, page-2.png, etc. +``` + +**Best for**: Visual layout analysis, comparing formatting, generating previews, when user asks "what does it look like". + +## Method 4: python-docx Reading + +```python +from docx import Document + +doc = Document("input.docx") + +# Read paragraphs +for para in doc.paragraphs: + print(f"[{para.style.name}] {para.text}") + +# Read tables +for table in doc.tables: + for row in table.rows: + print([cell.text for cell in row.cells]) + +# Document properties +print(f"Sections: {len(doc.sections)}") +print(f"Paragraphs: {len(doc.paragraphs)}") +print(f"Tables: {len(doc.tables)}") +``` + +## Choosing the Right Method + +| Need | Method | +|------|--------| +| Quick text content | pandoc | +| Document structure/outline | pandoc → markdown | +| Formatting details | Raw XML | +| Table data extraction | python-docx | +| Visual appearance | Convert to images | +| Style analysis | Raw XML (styles.xml) | +| Word/character count | pandoc → plain → wc | diff --git a/skills/docx/scenes/academic.md b/skills/docx/scenes/academic.md new file mode 100755 index 0000000..f94d1da --- /dev/null +++ b/skills/docx/scenes/academic.md @@ -0,0 +1,783 @@ +# Scene: Academic / Thesis + +## Palette + +**Academic Dark** (Cool + Heavy + Calm) — Academic papers use **pure black body text**. Palette only for cover decoration and minimal title scenarios. + +```js +const palette = { + primary: "#000000", // Title — pure black + body: "#000000", // Body — pure black + secondary: "#333333", // Header/caption — dark grey + accent: "#8B7E5A", // Cover decoration line — cover only + surface: "#F5F7FA", // Table header light bg — three-line tables only +}; +``` + +⚠️ **Body text color must be pure black `"000000"`**. No decorative dark-blue-grey. Academic papers require print-friendly, black-and-white clarity. + +→ Placeholder convention & universal prohibitions — see `references/common-rules.md` +→ **Note:** This scene uses Profile A fonts with academic-specific overrides below. + +--- + +## Page Layout + +| Property | Value | Twips | +|----------|-------|-------| +| Top margin | 2.54 cm | 1440 | +| Bottom margin | 2.54 cm | 1440 | +| Left margin | 3.00 cm | 1701 | +| Right margin | 2.50 cm | 1417 | +| Header distance | 1.5 cm | 850 | +| Footer distance | 1.75 cm | 992 | + +```js +page: { + size: { width: 11906, height: 16838 }, + margin: { top: 1440, bottom: 1440, left: 1701, right: 1417, header: 850, footer: 992 }, +} +``` + +For binding margin, add 0.5–1.0 cm to left (i.e., left: 1985–2268). + +--- + +## Font Specifications + +| Element | CN Font | EN Font | Size | half-pt | Style | +|---------|---------|---------|------|---------|-------| +| Thesis title | SimHei | Times New Roman | Xiao Er 18pt | 36 | Bold, centered | +| H1 | SimHei | Times New Roman | San Hao 16pt | 32 | Bold, centered | +| H2 | SimHei | Times New Roman | Xiao San 15pt | 30 | Bold, left | +| H3 | SimHei | Times New Roman | Si Hao 14pt | 28 | Bold, left | +| Body | SimSun | Times New Roman | Xiao Si 12pt | 24 | Normal, justified | +| Abstract title | SimHei | Times New Roman Bold | San Hao 16pt | 32 | Bold, centered | +| Abstract body | SimSun | Times New Roman | Xiao Si 12pt | 24 | Normal, justified | +| Keywords label | SimHei | Times New Roman Bold | Xiao Si 12pt | 24 | Bold | +| Keywords content | SimSun | Times New Roman | Xiao Si 12pt | 24 | Normal | +| Header | SimSun | Times New Roman | Xiao Wu 9pt | 18 | Centered, color 333333 | +| Page number | — | Times New Roman | Xiao Wu 10.5pt | 21 | Centered | +| Footnote | SimSun | Times New Roman | Xiao Wu 9pt | 18 | Normal | +| Figure/table caption | SimSun | Times New Roman | Wu Hao 10.5pt | 21 | Centered | + +### Paragraph Format +- Body: justified, first-line indent 2 chars (`firstLine: 480`, SimSun Xiao Si = 480 twips) +- Line spacing: 1.5x (`line: 360`); if school requires fixed 22pt, use `line: 440, lineRule: "exact"` +- Body paragraph spacing: before/after 0pt; heading spacing per styles below + +```js +styles: { + default: { + document: { + run: { font: { ascii: "Times New Roman", eastAsia: "SimSun" }, size: 24, color: "000000" }, + paragraph: { spacing: { line: 360 } }, + }, + heading1: { + run: { font: { ascii: "Times New Roman", eastAsia: "SimHei" }, size: 32, bold: true, color: "000000" }, + paragraph: { alignment: AlignmentType.CENTER, spacing: { before: 480, after: 360, line: 360 } }, + }, + heading2: { + run: { font: { ascii: "Times New Roman", eastAsia: "SimHei" }, size: 30, bold: true, color: "000000" }, + paragraph: { spacing: { before: 360, after: 240, line: 360 } }, + }, + heading3: { + run: { font: { ascii: "Times New Roman", eastAsia: "SimHei" }, size: 28, bold: true, color: "000000" }, + paragraph: { spacing: { before: 240, after: 120, line: 360 } }, + }, + }, +} +``` + +--- + +## Heading Numbering System (Mandatory) + +### Format + +| Level | Format | Example | +|-------|--------|---------| +| H1 | Chapter X + title | 第一章 绪论 (Chapter 1 Introduction) | +| H2 | X.X + section title | 1.1 Research Background | +| H3 | X.X.X + subsection | 1.1.1 Domestic Research Status | + +### Mandatory Rules +1. **H1 must use "第X章" format** — not "一、", not "Chapter 1", not "第1章" +2. **H2/H3 use Arabic decimal numbering** (1.1, 1.1.1) — no "(一)", "1)" +3. **No mixing multiple numbering systems** +4. **No level-skipping** (cannot jump from H1 to H3) +5. **All body headings must use `heading: HeadingLevel.HEADING_X`** (TOC depends on this) + +```js +// ✅ Correct +new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: [new TextRun({ text: "第一章 绪论", bold: true, size: 32, font: { eastAsia: "SimHei", ascii: "Times New Roman" } })] +}) +new Paragraph({ + heading: HeadingLevel.HEADING_2, + children: [new TextRun({ text: "1.1 研究背景", bold: true, size: 30, font: { eastAsia: "SimHei", ascii: "Times New Roman" } })] +}) +``` + +### Non-Body Headings +Abstract, Table of Contents, References, Appendices, Acknowledgments: +- Use H1 style (San Hao SimHei centered) for TOC indexing +- But **no numbering** (write directly: "摘 要", "参考文献", etc. — these are non-numbered standalone section headings) + +--- + +## Document Structure & Multi-Section Architecture + +Theses must use **multi-section structure** for independent page numbering and header/footer per section. + +### Complete Structure + +``` +Section 1: Cover → No page number, no header/footer +Section 2: Chinese Abstract → Roman numerals starting from i +Section 3: English Abstract → Roman numerals continued +Section 4: Table of Contents → Roman numerals continued +Section 5: Body (all chapters) → Arabic numerals from 1 +Section 6: References → Arabic numerals continued +Section 7: Appendices (if any) → Arabic numerals continued +Section 8: Acknowledgments (if any) → Arabic numerals continued +``` + +### Page Number Implementation + +```js +const { NumberFormat } = require("docx"); + +// Section 1: Cover — no page number +{ + properties: { + page: { margin: { top: 0, bottom: 0, left: 0, right: 0 } }, + titlePage: true, + }, + children: buildCover(...), +} + +// Section 2: Abstract — Roman numerals from i +{ + properties: { + type: SectionType.NEXT_PAGE, + page: { + margin: { top: 1440, bottom: 1440, left: 1701, right: 1417, header: 850, footer: 992 }, + pageNumbers: { start: 1, formatType: NumberFormat.UPPER_ROMAN }, + }, + }, + headers: { default: buildHeader("Thesis Title") }, + footers: { default: buildPageNumberFooter() }, + children: buildAbstractCN(...), +} + +// Section 3: English Abstract — Roman numerals continued (no reset) +{ + properties: { + type: SectionType.NEXT_PAGE, + page: { + margin: { top: 1440, bottom: 1440, left: 1701, right: 1417, header: 850, footer: 992 }, + pageNumbers: { formatType: NumberFormat.UPPER_ROMAN }, // no start → continues from previous + }, + }, + headers: { default: buildHeader("Thesis Title") }, + footers: { default: buildPageNumberFooter() }, + children: buildAbstractEN(...), +} + +// Section 5: Body — Arabic numerals from 1 +{ + properties: { + type: SectionType.NEXT_PAGE, + page: { + margin: { top: 1440, bottom: 1440, left: 1701, right: 1417, header: 850, footer: 992 }, + pageNumbers: { start: 1, formatType: NumberFormat.DECIMAL }, + }, + }, + headers: { default: buildHeader("Thesis Title") }, + footers: { default: buildPageNumberFooter() }, + children: buildMainContent(...), +} +// Section 6+: References/Appendices/Acknowledgments — Arabic continued +``` + +### Header & Footer Helpers + +```js +function buildHeader(title) { + return new Header({ children: [ + new Paragraph({ alignment: AlignmentType.CENTER, + border: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "000000" } }, + children: [new TextRun({ text: title, size: 18, color: "333333", + font: { ascii: "Times New Roman", eastAsia: "SimSun" } })], + }), + ] }); +} + +function buildPageNumberFooter() { + return new Footer({ children: [ + new Paragraph({ alignment: AlignmentType.CENTER, + children: [ + new TextRun({ text: "- ", size: 21 }), + new TextRun({ children: [PageNumber.CURRENT], size: 21 }), + new TextRun({ text: " -", size: 21 }), + ], + }), + ] }); +} +``` + +### Page Break Rules +- Cover is a separate section (no PageBreak needed) +- Chinese abstract, English abstract, TOC each in their own section +- All body chapters in **one section** (no forced page breaks between chapters unless user requests) +- References, appendices, acknowledgments each in their own section +- **Never use blank lines instead of section breaks** + +--- + +## Cover + +### Information Fields + +Cover must include (use placeholders for missing info): + +| Field | Format | Placeholder | +|-------|--------|-------------| +| University name | Er Hao SimHei, centered | ×××University | +| Thesis title (CN) | Xiao Er SimHei, centered | (user-provided) | +| Thesis title (EN) | San Hao Times New Roman, centered | (translated from CN) | +| College | Si Hao SimSun | ×××College | +| Major | Si Hao SimSun | ×××Major | +| Author | Si Hao SimSun | ××× | +| Student ID | Si Hao SimSun | ××××××× | +| Advisor | Si Hao SimSun | ×××Professor | +| Date | Si Hao SimSun | 2026/XX | + +### Cover Style + +Use Recipe R5 (Clean White) or academic-specific `buildAcademicCover()` — never use commercial-style covers. + +### Cover Layout Order (Mandatory) + +The visual order on academic covers must follow this hierarchy from top to bottom: + +1. School name (top) +2. Document type label (e.g., "Undergraduate Thesis", "Thesis Proposal Report") +3. **Thesis title** (prominent, centered) +4. Thesis English title (if bilingual) +5. **Author information table** (college, major, author, student ID, advisor) +6. Date (bottom) + +⚠️ **Title MUST appear ABOVE the author info table.** The screenshot issue of info table appearing above the title is caused by incorrect element ordering. The `buildAcademicCover()` and `buildProposalCover()` functions below enforce correct order. + +⚠️ **Layout must be vertically balanced** — use dynamic spacing to distribute whitespace evenly. Do not cram all elements into the top half or let large gaps appear between elements. + +```js +function buildAcademicCover(info) { + const { school, title, titleEN, college, major, author, studentId, advisor, date } = info; + + // ⚠️ Use safeText() for all values — never output "undefined" + const infoRows = [ + ["College", safeText(college, "【College】")], + ["Major", safeText(major, "【Major】")], + ["Author", safeText(author, "【Author】")], + ["Student ID", safeText(studentId, "【Student ID】")], + ["Advisor", safeText(advisor, "【Advisor】")], + ]; + + const infoTable = new Table({ + width: { size: 60, type: WidthType.PERCENTAGE }, + alignment: AlignmentType.CENTER, + borders: { top: NB, bottom: NB, left: NB, right: NB, insideHorizontal: NB, insideVertical: NB }, + rows: infoRows.map(([label, value]) => new TableRow({ + cantSplit: true, + children: [ + new TableCell({ + width: { size: 35, type: WidthType.PERCENTAGE }, + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "000000" }, top: NB, left: NB, right: NB }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + children: [new Paragraph({ + alignment: AlignmentType.RIGHT, + children: [new TextRun({ text: label + ":", size: 28, font: { eastAsia: "SimHei", ascii: "Times New Roman" } })], + })], + }), + new TableCell({ + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "000000" }, top: NB, left: NB, right: NB }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: value, size: 28, font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], + })], + }), + ], + })), + }); + + // ⚠️ Correct order: school → doc type → TITLE → info table → date + // ★ Rule 8: All large-font paragraphs must set explicit line spacing + return [ + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 1200, after: 400, line: Math.ceil(22 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: safeText(school, "【University Name】"), size: 44, bold: true, font: { eastAsia: "SimHei" } })] }), + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 800, line: Math.ceil(18 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: "Undergraduate Thesis", size: 36, font: { eastAsia: "SimHei" } })] }), + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 200, line: Math.ceil(18 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: safeText(title, "【Thesis Title】"), size: 36, bold: true, font: { eastAsia: "SimHei", ascii: "Times New Roman" } })] }), + titleEN ? new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 1200, line: Math.ceil(16 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: titleEN, size: 32, font: { ascii: "Times New Roman" } })] }) + : new Paragraph({ spacing: { after: 1200 }, children: [] }), + infoTable, + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 1200, line: Math.ceil(14 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: safeText(date, "2026/XX"), size: 28, font: { eastAsia: "SimSun" } })] }), + ]; +} +``` + +### Thesis Proposal Report Cover (开题报告) + +Thesis proposal reports use a similar cover layout but with different document type label. The key layout rule is the same: **title above author info, evenly spaced**. + +⚠️ **CRITICAL — Proposal cover MUST be an independent section:** +The proposal cover MUST be placed in its **own section** (with margin: 0 and a 16838 wrapper table), completely separate from the body content. The body content starts in the **next section** (with `SectionType.NEXT_PAGE` or as a separate section entry). **Never place the cover elements and body content in the same section** — this causes them to render on the same page without any page break, which is the #1 proposal report formatting failure. + +```js +// ✅ Correct — cover and body in separate sections +sections: [ + { + properties: { page: { margin: { top: 0, bottom: 0, left: 0, right: 0 } } }, + children: buildProposalCover(info), // standalone cover section + }, + { + properties: { page: { margin: { top: 1440, bottom: 1440, left: 1701, right: 1417 } } }, + children: [...bodyContent], // body starts here + }, +] + +// ❌ WRONG — cover and body in same section (no page separation!) +sections: [ + { + children: [...coverElements, ...bodyContent], // everything on one continuous flow + }, +] +``` + +```js +function buildProposalCover(info) { + const { school, year, title, subtitle, college, major, author, studentId, advisor, date } = info; + + // ⚠️ Use safeText() for all values + const infoRows = [ + ["姓名 (Name)", safeText(author, "XXX")], + ["专业 (Major)", safeText(major, "XXX")], + ["入学时间 (Enrollment)", safeText(info.enrollment, "XXX")], + ]; + + const infoTable = new Table({ + width: { size: 60, type: WidthType.PERCENTAGE }, + alignment: AlignmentType.CENTER, + borders: { top: NB, bottom: NB, left: NB, right: NB, insideHorizontal: NB, insideVertical: NB }, + rows: infoRows.map(([label, value]) => new TableRow({ + children: [ + new TableCell({ + width: { size: 35, type: WidthType.PERCENTAGE }, + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "000000" }, top: NB, left: NB, right: NB }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: label, size: 28, bold: true, font: { eastAsia: "SimHei", ascii: "Times New Roman" } })], + })], + }), + new TableCell({ + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "000000" }, top: NB, left: NB, right: NB }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [new TextRun({ text: value, size: 28, font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], + })], + }), + ], + })), + }); + + // ⚠️ Correct order: doc type label → info table → "论文题目" label → TITLE → subtitle + // Layout balanced: upper 40% for header + info, middle 20% for title, lower 40% for whitespace + // ★ Rule 8: All large-font paragraphs must set explicit line spacing + return [ + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 1500, after: 600, line: Math.ceil(18 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: safeText(year, "2025") + " 届本科毕业论文开题报告", + size: 36, bold: true, font: { eastAsia: "SimHei", ascii: "Times New Roman" } })] }), + infoTable, + new Paragraph({ spacing: { before: 1200 } }), // Balanced whitespace + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 200 }, + children: [new TextRun({ text: "论文题目", size: 28, font: { eastAsia: "SimSun", ascii: "Times New Roman" } })] }), + new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 200, line: Math.ceil(16 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: safeText(title, "【Thesis Title】"), size: 32, bold: true, + font: { eastAsia: "SimHei", ascii: "Times New Roman" } })] }), + subtitle ? new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 800 }, + children: [new TextRun({ text: "——" + subtitle, size: 28, + font: { eastAsia: "SimSun", ascii: "Times New Roman" } })] }) + : new Paragraph({ spacing: { after: 800 }, children: [] }), + ]; +} +``` + +### ⚠️ WPS Compatibility Notes for Academic Covers + +Both thesis cover and proposal cover use info tables. These MUST follow the cross-engine rules: +- Table uses **percentage widths** (`WidthType.PERCENTAGE`), NOT DXA — WPS renders DXA widths differently in nested contexts +- Table width: adaptive 55–75%, centered via `alignment: CENTER` (calculated by `calcR5MetaLayout()`) +- Label column: **LEFT aligned**, plain text + ":", NO full-width space padding, NO borders +- Value column: **LEFT aligned**, `bottom: single sz=4` border = fixed-length underline +- Cell `margins.top/bottom: 60` is acceptable (small values) but avoid larger values +- All paragraphs with font size > 12pt (body) must set `spacing: { line: Math.ceil(fontPt * 23), lineRule: "atLeast" }` to prevent top clipping (Rule 8) +- ⚠️ Do NOT use DXA widths, full-width space padding (`\u3000`), tab stops, or right-alignment for meta info + +⚠️ **Proposal cover must fit on one page.** Use the same height-budget approach as commercial covers — total content height must stay within 15638 twips (1200 twips safety margin). If the title is very long, reduce font size (minimum 24pt). +``` + +--- + +## Section Content Standards + +### Chinese Abstract +**Format:** +- Title: "摘 要" (space in middle), San Hao SimHei centered, H1 style +- Body: Xiao Si SimSun, justified, first-line indent 480 twips +- Keywords: "关键词:" SimHei bold + content SimSun normal, 3–8 keywords, semicolon-separated + +**Content structure (mandatory):** +1. Research background (1–2 sentences) +2. Research problem/purpose (1 sentence) +3. Research method (1–2 sentences) +4. Main results/findings (2–3 sentences) +5. Research significance/value (1 sentence) + +⚠️ **Abstract is NOT a TOC summary.** Must not read as "Chapter 1 introduces... Chapter 2 analyzes..." + +### English Abstract +- Title: "Abstract", San Hao Times New Roman Bold, centered, H1 style +- Body: Xiao Si Times New Roman, justified +- Keywords: bold label + normal content, 3–8 keywords, comma-separated +- **Must be consistent with Chinese abstract** — no significant shrinkage +- Use formal academic English, avoid Chinglish + +### Table of Contents +- Title: "目 录", San Hao SimHei centered +- Use `TableOfContents` field for auto-generation, display at least H1–H2, recommend H3 +- Run `"$DOCX_SCRIPTS/add_toc_placeholders.py" --auto` after generation +- TOC on its own page + +--- + +## Body Chapter Structure + +### Standard Structure (6-chapter) + +``` +Chapter 1: Introduction + 1.1 Research Background + 1.2 Research Purpose & Significance + 1.3 Literature Review (Domestic & International) + 1.4 Research Content & Methods + 1.5 Thesis Structure + +Chapter 2: Theoretical Framework & Literature Review + 2.1 Core Concept Definitions + 2.2 Theoretical Basis + 2.3 Literature Review + 2.4 Research Gap & Entry Point + +Chapter 3: Research Design / Method / Model + 3.1 Research Framework + 3.2 Method Design / System Architecture / Algorithm + 3.3 Variables / Data Sources / Experimental Environment + +Chapter 4: Empirical Analysis / Case Study / Results + 4.1 Data Analysis / Case Description / Experiment Process + 4.2 Results Presentation + 4.3 Results Interpretation + +Chapter 5: Discussion + 5.1 Key Findings + 5.2 Comparison with Existing Research + 5.3 Limitations + +Chapter 6: Conclusions & Outlook + 6.1 Research Conclusions + 6.2 Contributions + 6.3 Limitations + 6.4 Future Research Directions +``` + +### Chapter Content Requirements + +**Chapter 1 (Introduction):** Must state background, purpose, significance, methods, content, structure. + +**Chapter 2 (Literature Review):** Must be systematically organized by theme/method/stage — **never a chronological dump of papers**. Must identify contributions, gaps, and research opportunities. + +**Chapter 3 (Method):** Must explain why this method was chosen and its rationale. Content must be understandable, executable, reproducible. + +**Chapter 4 (Results):** Must be specific, not vague. Must be consistent with Chapter 3 design. + +**Chapter 5 (Discussion):** Must not merely repeat Chapter 4 results. Must explain what results mean and what conclusions they support. + +**Chapter 6 (Conclusions):** Must summarize concisely, state contributions, acknowledge limitations, propose future directions. Must end formally — no abrupt ending. + +--- + +## Discipline-Adaptive Routing + +Auto-adjust research methods and chapter emphasis by discipline. **When user doesn't specify method, choose the most appropriate research paradigm for the discipline — never mechanically apply "empirical + survey + regression" template.** + +### 1. Humanities & Social Sciences (Literature, History, Philosophy, Arts) +**Preferred methods:** Literature analysis, theoretical research, text analysis, comparative studies, historical research +**Adjustments:** Ch.2 focuses on theoretical lineage; Ch.4 becomes text analysis/case argumentation; minimize "variables", "hypotheses", "regression" terminology + +### 2. Management / Economics / Public Administration +**Preferred methods:** Case analysis, surveys, model analysis, institutional research, empirical research +**Adjustments:** Ch.3 focuses on hypotheses, variables, framework; Ch.4 on data collection & analysis; Ch.5 adds management implications/policy recommendations + +### 3. Computer Science / Engineering / IT +**Preferred methods:** Method design, system architecture, experimental comparison, performance evaluation, algorithm analysis +**Adjustments:** Ch.3 becomes system/algorithm design; Ch.4 becomes experiments (environment, parameters, control experiments, metric comparison); minimize "interviews", "surveys" + +### 4. Education / Linguistics / Communication +**Preferred methods:** Teaching experiments, text analysis, survey research, interview research, case studies +**Adjustments:** Ch.3 focuses on subjects, dimensions, samples; Ch.4 on teaching practice/communication case analysis; Ch.5 adds educational implications/communication strategies + +### 5. Law / Marxism / Policy Studies +**Preferred methods:** Normative analysis, statutory interpretation, case studies, institutional comparison, theoretical analysis +**Adjustments:** Ch.2 focuses on legal/policy framework; Ch.4 becomes case analysis/institutional comparison; Ch.5 focuses on normative evaluation, reform recommendations + +--- + +## Figure/Table/Formula Numbering (By Chapter) + +### Numbering Rules + +| Type | Format | Example | +|------|--------|---------| +| Figure | Figure X-Y | Figure 3-1, Figure 4-2 | +| Table | Table X-Y | Table 2-1, Table 4-3 | +| Formula | Eq. (X-Y) | Eq. (3-1), Eq. (5-2) | + +Where X = chapter number, Y = sequential number within chapter. + +### Figures +- Caption **below** figure, Wu Hao SimSun, centered +- Format: "Figure X-Y Description" +- Must be referenced in text: "as shown in Figure 3-1" + +```js +new Paragraph({ alignment: AlignmentType.CENTER, + children: [new ImageRun({ data: imgBuf, transformation: { width: w, height: h }, type: "png" })] }), +new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 60, after: 200 }, + children: [new TextRun({ text: "图3-1 System Architecture", size: 21, + font: { eastAsia: "SimSun", ascii: "Times New Roman" } })] }), +``` + +### Tables +- Caption **above** table, Wu Hao SimSun, centered, `keepNext: true` +- Format: "Table X-Y Description" +- Must use three-line table (mandatory for academic papers) +- Must be referenced in text: "as shown in Table 2-1" + +### Formulas +- Formula centered, number **right-aligned** +- Use Tab for center + right alignment +- Text reference: "from Eq. (3-1)" + +```js +new Paragraph({ + alignment: AlignmentType.CENTER, + tabStops: [ + { type: TabStopType.CENTER, position: 4500 }, + { type: TabStopType.RIGHT, position: 9000 }, + ], + children: [ + new TextRun({ text: "\t" }), + new TextRun({ text: "E = mc²" }), + new TextRun({ text: "\t(3-1)" }), + ], +}), +``` + +### Mandatory Rules +1. Figures/tables/formulas **must be referenced in text** — never placed without explanation +2. Must have introductory and analytical text before/after +3. Must not exceed page margins +4. Insert only when analytically valuable — not for decoration + +--- + +## Citation & Reference System + +### In-Text Citation (Sequential Numbering) + +Default: **GB/T 7714 sequential numbering** — `[1]`, `[2]` in text, references listed in order of appearance. + +```js +new TextRun({ text: "[1]", superScript: true, size: 18, font: { ascii: "Times New Roman" } }) +``` + +### Citation Rules +1. In-text numbers must **correspond one-to-one** with reference list +2. **Same source reused keeps the same number** +3. **Do not mix footnote citations and endnote references** (unless user explicitly requests) +4. Footnotes are for supplementary notes only, not primary citations + +### Reference Format (GB/T 7714) +``` +[1] Author. Title[J]. Journal, Year, Vol(No): Pages. +[2] Author. Book Title[M]. Place: Publisher, Year: Pages. +[3] Author. Title[D]. Location: Institution, Year. +[4] Author. Title[EB/OL]. (Published)[Cited]. URL. +``` + +### Reference Formatting +```js +// Reference title — H1 style +new Paragraph({ heading: HeadingLevel.HEADING_1, alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "References", bold: true, size: 32, font: { eastAsia: "SimHei" } })] }), +// Each entry — hanging indent +new Paragraph({ + indent: { left: 420, hanging: 420 }, + spacing: { line: 360 }, + children: [new TextRun({ text: "[1] Author. Title[J]. Journal, 2024, 59(3): 45-62.", + size: 21, font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], +}), +``` + +### Reference Count Guidelines + +| Thesis Type | Suggested Count | +|------------|----------------| +| Course paper (3000–5000 words) | 10–15 | +| Undergraduate thesis | 15–30 | +| Master's thesis | 40–80 | +| Doctoral dissertation | 80–150 | + +If user specifies APA, MLA, Chicago, or school-specific format, follow that instead. + +--- + +## Three-Line Table (Mandatory for Academic Papers) + +All tables in academic papers **must use three-line tables** — no full-border tables. + +```js +const threeLineTable = new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { + top: { style: BorderStyle.SINGLE, size: 4, color: "000000" }, + bottom: { style: BorderStyle.SINGLE, size: 4, color: "000000" }, + left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE }, + insideHorizontal: { style: BorderStyle.NONE }, insideVertical: { style: BorderStyle.NONE }, + }, + rows: [ + new TableRow({ + tableHeader: true, cantSplit: true, + children: headerCells.map(text => new TableCell({ + borders: { bottom: { style: BorderStyle.SINGLE, size: 2, color: "000000" }, + top: { style: BorderStyle.NONE }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } }, + margins: { top: 60, bottom: 60, left: 120, right: 120 }, + children: [new Paragraph({ alignment: AlignmentType.CENTER, + children: [new TextRun({ text, bold: true, size: 21, font: { eastAsia: "SimSun", ascii: "Times New Roman" } })] })], + })), + }), + ...dataRows, // All borders NONE + ], +}); +``` + +--- + +## Content Quality Constraints (Mandatory) + +### Truthfulness & Conservatism +1. **Never fabricate** unverifiable statistics, survey response counts, significance levels, interview subject identities, experimental precision, government document numbers +2. **Never invent** non-existent classic theories, authoritative scholar opinions, regulation names, core data sources +3. When user provides no real data → prefer **theoretical analysis, literature research, case studies, comparative analysis** (low-risk methods) +4. If example data must be constructed → keep scale reasonable, results conservative; never produce "significantly superior" or "dramatically improved" high-risk claims +5. Research conclusions must be **restrained** — do not overstate contributions, effects, or applicability +6. Research limitations must be **honestly disclosed** + +### Language Style +1. Formal academic register throughout +2. **Forbidden:** "I think", "everyone knows", "obviously", "it is well known" (subjective expressions) +3. **Forbidden:** Sloganeering, propaganda, advertising-style expressions +4. First occurrence of CN/EN terms should include English original +5. CN/EN punctuation, spacing, and number formats must be consistent throughout + +### Structural Consistency +1. Abstract, body, and conclusions **must be consistent** — no self-contradiction +2. Must form complete loop: "research question → method → analysis → findings → conclusions & outlook" +3. Terminology consistent throughout — no concept drift +4. All chapters balanced and substantive — no padding + +### Document Cleanliness +1. **No residual** comments, tracked changes, field codes, template default text +2. **No** "TBD", "omitted", "user modifies", "insert figure here" expressions +3. **No** Markdown syntax, HTML tags, code blocks wrapping body text +4. **No** consecutive blank lines, abnormal page breaks, chaotic numbering +5. Final document must be clean, well-formatted, ready for submission + +--- + +## School Standard Override Rule + +⚠️ **When user specifies school/journal-specific format requirements, those requirements OVERRIDE all defaults above.** + +Common override items: +- Margins (binding margin left 3.5 cm common) +- Body font (some schools require FangSong) +- Line spacing (some schools require fixed 28pt) +- Cover layout (varies significantly by school) +- Reference format (APA, MLA, etc.) +- Heading numbering (some schools use "1", "2" instead of "Chapter 1", "Chapter 2") + +### Common Variants + +| Thesis Type | Common Differences | +|------------|-------------------| +| Top universities | Strict GB/T 7714, often require STXiaoBiaoSong cover | +| Regular undergraduate | More flexible, SimSun/SimHei sufficient | +| Master's thesis | Requires English abstract, longer lit review, innovation statement | +| Doctoral dissertation | Requires innovation statement, publication list, originality declaration | + +--- + +## Scene-Specific Quality Checks + +In addition to universal checks (see `references/common-rules.md`): + +### Structure & Content +- [ ] Cover, abstract, English abstract, TOC, body, references all present +- [ ] Cover info complete (school/title/EN title/college/major/name/ID/advisor/date) +- [ ] Abstract contains 5 elements: background + problem + method + results + significance +- [ ] English abstract consistent with Chinese abstract +- [ ] All chapters balanced, substantive, logical loop complete +- [ ] Literature review is thematic, not chronological dump +- [ ] Conclusions respond to research questions + +### Format & Layout +- [ ] Heading numbering consistent (Chapter X / X.X / X.X.X), no mixing +- [ ] All body headings use `heading: HeadingLevel.HEADING_X` +- [ ] Body text pure black `"000000"` +- [ ] Three-line tables used consistently (no full-border tables) +- [ ] Figure captions below, table captions above, numbered by chapter +- [ ] Formulas centered, numbers right-aligned +- [ ] In-text citations match reference list one-to-one +- [ ] References use hanging indent, consistent format +- [ ] Page numbers: front matter Roman, body Arabic from 1 +- [ ] Cover has no page number +- [ ] Headers formal and concise +- [ ] No extra blank pages + +### Cleanliness +- [ ] No comment/revision residuals +- [ ] No "TBD" / "omitted" expressions +- [ ] No Markdown/HTML/code block residuals +- [ ] No consecutive blank lines or abnormal page breaks +- [ ] No fabricated high-risk data or exaggerated conclusions diff --git a/skills/docx/scenes/contract.md b/skills/docx/scenes/contract.md new file mode 100755 index 0000000..a78be7a --- /dev/null +++ b/skills/docx/scenes/contract.md @@ -0,0 +1,463 @@ +# Scene: Contract / Agreement + +## Goal + +Generate a complete, formal, well-structured legal document with clear clauses, rigorous logic, and proper formatting. Must simultaneously meet: +- Complete structure, clear clauses, formal language, explicit responsibilities +- Identifiable risk boundaries, proper Word formatting +- Ready for review, revision, circulation, or signing preparation + +**Forbidden:** Producing outlines-only / sample clauses / drafting advice / risk summaries; outputting chat-style explanations or filler phrases. + +→ Font profile: **A (Formal)** — see `references/common-rules.md` +→ Default layout: standard margins — see `references/common-rules.md` +→ Placeholder convention & universal prohibitions — see `references/common-rules.md` + +--- + +## Contract Type Routing + +```js +function selectContractType(keywords, topic) { + if (/confidential|NDA|non-disclosure/.test(keywords)) return "nda"; + if (/transfer|equity|asset|rights/.test(keywords)) return "transfer"; + if (/framework|strategic|cooperation agreement/.test(keywords)) return "framework"; + if (/terms|platform rules|user agreement|privacy/.test(keywords)) return "terms"; + return "bilateral"; // default: bilateral commercial contract +} +``` + +### 5 Contract Types + +| Type | Use Case | Structure Focus | +|------|----------|----------------| +| bilateral | Service/sale/development/procurement contracts | Subject → Consideration → Performance → Acceptance → Breach → Dispute | +| transfer | Equity/debt/asset/rights transfer | Subject → Consideration → Closing & Registration → Representations → Tax | +| nda | Non-disclosure agreements | Definition of Confidential Info → Obligations → Use Restrictions → Exceptions → Duration | +| framework | Cooperation framework / strategic alliance | Scope → Division of Work → Mechanism → Subsequent Agreements | +| terms | Platform rules / Terms of Service / User agreements | Definitions → Services → Rights & Obligations → Liability Limits → Amendments | + +--- + +## Standard Template Structures + +### Template A: Bilateral Commercial Contract +1. Header (title, contract number, date, location) +2. Party Information (Party A, Party B) +3. Recitals ("Whereas" clauses) +4. Definitions & Interpretation +5. Subject Matter & Scope of Services/Delivery +6. Contract Price & Payment Terms +7. Rights & Obligations of Both Parties +8. Timeline, Delivery & Acceptance +9. Invoicing, Tax & Settlement +10. Intellectual Property & Confidentiality +11. Representations & Warranties (if applicable) +12. Liability for Breach +13. Force Majeure +14. Termination & Dissolution +15. Notices & Service +16. Dispute Resolution +17. Miscellaneous +18. Signature Block + +### Template B: Rights Transfer Agreement +1. Header & Parties +2. Recitals +3. Definitions & Interpretation +4. Subject of Transfer +5. Consideration & Payment Arrangement +6. Closing & Registration/Transfer +7. Representations & Warranties +8. Tax Allocation +9. Liability for Breach +10. Dispute Resolution +11. Miscellaneous +12. Signature Block + +### Template C: Non-Disclosure Agreement (NDA) +1. Header & Parties +2. Recitals +3. Definition of Confidential Information +4. Confidentiality Obligations +5. Use Restrictions +6. Return, Deletion & Destruction of Information +7. Exceptions +8. Confidentiality Period +9. Liability for Breach +10. Dispute Resolution +11. Miscellaneous +12. Signature Block + +### Template D: Framework / Cooperation Agreement +1. Header & Parties +2. Recitals +3. Purpose & Principles +4. Scope of Cooperation +5. Division of Work & Responsibilities +6. Project Advancement Mechanism +7. Commercial Arrangements / Subsequent Agreements +8. Confidentiality, IP & Compliance +9. Term, Amendment & Termination +10. Liability for Breach +11. Dispute Resolution +12. Miscellaneous +13. Signature Block + +### Template E: Unilateral Terms / Platform Rules +1. Document Title +2. Definitions & Scope +3. Service/Rule Content +4. User Rights & Obligations / Platform Rights & Obligations +5. Liability Limitations & Disclaimers +6. Fees & Payment (if applicable) +7. Intellectual Property +8. Termination, Suspension & Amendment +9. Notices & Service +10. Dispute Resolution +11. Miscellaneous + +**Note:** Unilateral/boilerplate terms require special attention to adhesion clause risks — avoid creating extremely one-sided documents. + +**If the user provides an existing template, historical agreement, or company standard, always follow it first.** + +--- + +## Input Recognition & Completion + +### Processing Rules +1. If user provides a template, historical agreement, or company standard → **always follow it first** +2. If information is incomplete, fill conservatively — must be **restrained, natural, professional, consistent with transaction logic** +3. **Never fabricate** unrealistic commercial terms, regulatory requirements, approval conclusions, qualification status, tax treatment results, payment facts, or performance facts +4. If critical info is missing → use standardized placeholders +5. If user does not specify jurisdiction → default to PRC commercial writing conventions, but avoid making specific legal conclusions + +--- + +## Legal Writing Standards + +### Register +1. Use formal legal document register +2. Use clear party designations: "Party A", "Party B", "both parties", "either party", "non-breaching party", "breaching party" +3. **Forbidden:** Colloquial expressions ("you", "me", "they", "pay up", "cancel the contract", "handle ASAP") +4. Preferred terms: "pay consideration", "perform obligations", "constitute a breach", "terminate the contract", "assume liability for damages", "written notice", "deliver and accept", "representations and warranties" + +### Precision +1. Eliminate vague adjectives: avoid "quality", "reasonable", "enormous", "appropriate", "ASAP" unless necessary for legal flexibility +2. Each obligation must specify: who, when, how, what +3. Consistent legal phrasing: + - Mandatory obligation → "shall" + - Right authorization → "has the right to" + - Prohibition → "shall not" + - Discretionary → "may" +4. Amounts, dates, percentages, deadlines, business days vs. calendar days must be as specific as possible + +### Clear Subjects +1. Every clause must have an explicit responsible party — avoid vague subjects ("relevant parties", "relevant personnel", "when necessary") +2. Joint obligations: explicitly write "both parties agree" or "both parties shall" +3. Unilateral obligations: explicitly write "Party A shall" or "Party B shall" + +--- + +## Transaction Closure & Risk Control + +A contract must not only describe the transaction — it must ensure logical closure. Check the following: + +1. If a performance deadline is specified → specify consequences of delay +2. If payment milestones are specified → specify payment conditions, method, invoice requirements +3. If a delivery obligation exists → specify delivery standards, method, acceptance rules, objection period +4. If termination rights exist → specify conditions, notice, effective date, post-termination settlement +5. If breach liability exists → must correspond to main obligations in preceding clauses +6. If IP/technology/data/trade secrets are involved → separately address ownership, license scope, use restrictions +7. If confidentiality obligations exist → define scope, exceptions, duration, breach consequences +8. If force majeure clause exists → specify notice obligation, mitigation duty, subsequent negotiation mechanism +9. If notice/service arrangements exist → specify address, contact person, email, or other delivery method +10. If user requests significantly one-sided adhesion/disclaimer clauses → add a note near the clause: + `[Note: This clause may involve adhesion terms or liability limitations. Manual review recommended for the specific transaction.]` + +--- + +## Truthfulness & Legal Caution + +1. **Never fabricate** specific statute article numbers, judicial interpretation numbers, or regulatory document numbers +2. Legal bases should use general references, e.g.: "In accordance with the Civil Code of the PRC and relevant laws and regulations..." +3. **Never** pretend to provide formal legal opinions, litigation success predictions, or definitive validity/invalidity conclusions +4. **Never** state definitive legality conclusions for high-risk clauses (adhesion terms, penalty clauses, disclaimers, non-compete, exclusivity, unilateral interpretation rights) +5. **Never** fabricate that regulatory approvals are obtained, title is unencumbered, tax compliance is assured, or third-party consent is secured +6. When critical info is insufficient → use placeholders, never present as confirmed fact +7. For high-risk areas (equity, debt, licenses, data compliance, labor, personal information, cross-border) → maintain restrained language, do not add rigid commitments without user confirmation + +--- + +## Special Clause Requirements + +### Definitions Clause +If the document repeatedly uses specialized terms ("deliverables", "service results", "confidential information", "source code", "project milestones", "acceptance criteria", "trade secrets"), include a "Definitions & Interpretation" clause near the beginning. + +### Dispute Resolution +1. Must be explicit +2. Choose between litigation OR arbitration — never mix both +3. Litigation → specify jurisdictional connection point +4. Arbitration → specify arbitration institution +5. If user hasn't specified → use placeholder for confirmation + +### Tax Clause +1. If the transaction involves taxes → specify which party bears them, whether price includes tax, invoice type and conditions +2. Avoid vague "taxes borne as required by law" without transaction-specific detail + +### Breach Liability +1. Must correspond to main obligations in preceding clauses +2. Penalty amounts should be restrained — avoid obviously exaggerated or severely imbalanced figures +3. If fundamental breach exists → consider corresponding termination rights and damages + +### Appendices +1. For complex subjects/pricing/technical requirements/deliverables → use "Appendix 1, Appendix 2..." format +2. Explicitly state appendix-contract relationship (typically: "Appendices form an integral part of this contract") +3. If appendix content is unknown → use placeholder + +--- + +## Palette + +**Legal Wood** (Warm + Heavy + Calm) — for decorative elements only; body text must be pure black. + +```js +const palette = { primary:"#28201C", body:"#000000", secondary:"#6E6560", accent:"#7A5C3A", surface:"#FBF9F7" }; +``` + +⚠️ **ALL visible text in contracts must be pure black `"000000"`.** This includes: +- Contract title (SimHei, black, NOT accent color) +- Contract number (black) +- Clause headings (black) +- Body text (black) +- Party information (black) +- Signature block text (black) + +**The only exception** is red-header official documents (红头文件), which follow their own GB/T 9704 color rules. For standard contracts, NO colored text is permitted — no red, no accent color, no dark-blue-grey. + +```js +// ✅ Contract title — always pure black +new Paragraph({ alignment: AlignmentType.CENTER, + spacing: { line: Math.ceil(22 * 23), lineRule: "atLeast" }, // ★ Rule 8: prevent clipping + children: [new TextRun({ text: "Training Cooperation Framework Agreement", + size: 44, bold: true, color: "000000", // ← MUST be "000000" + font: { eastAsia: "SimHei", ascii: "Times New Roman" } })] +}) + +// ❌ FORBIDDEN — accent/palette color on contract text +new TextRun({ text: "Training Cooperation Framework Agreement", color: palette.accent }) // ← WRONG +new TextRun({ text: "Contract No.:", color: palette.primary }) // ← WRONG (if primary ≠ "000000") +``` + +--- + +## Scene-Specific Font Overrides + +Beyond Profile A defaults: + +| Element | Font | Size | Style | +|---------|------|------|-------| +| Contract title | SimHei | Er Hao 22pt (size: 44) | Bold, centered | +| Contract number | SimSun | Wu Hao 10.5pt (size: 21) | Right-aligned | +| Clause heading | SimHei | Xiao Si 12pt (size: 24) | Bold | +| Monetary amount | SimSun | Xiao Si 12pt (size: 24) | Bold | + +--- + +## Document Structure + +1. **Title**: "XXX Contract" or "XXX Agreement" — Er Hao SimHei, centered +2. **Contract number**: right-aligned, Wu Hao +3. **Preamble**: Party information with placeholders +4. **Recitals** (summarize transaction background and purpose) +5. **Definitions** (if specialized terms recur) +6. **Substantive clauses** (per selected template) +7. **Signature block** +8. **Appendices** (if any) + +--- + +## Clause Numbering System + +Use stable, consistent, pure-text numbering suitable for Chinese legal documents. + +``` +Article 1 Subject Matter + 1.1 xxxxxxxxxx + 1.2 xxxxxxxxxx + (1) xxxxxxxxxx + (2) xxxxxxxxxx + ① xxxxxxxxxx + ② xxxxxxxxxx +Article 2 Price and Payment + 2.1 ... +``` + +**Numbering discipline:** +1. No level-skipping +2. **Forbidden:** Using Markdown list markers (`-` `*` `1.`) for clause hierarchy +3. No switching from "Article X" to `-` or `*` or auto-list mid-document +4. Numbering style must be consistent throughout the entire document +5. Clause headings should be clean and simple + +--- + +## Party Information Layout (Table-Based Alignment — Mandatory) + +Party A and Party B information MUST be laid out using a **borderless table** so that labels align vertically. Never use plain paragraphs with indentation — this causes misalignment between parties. + +```js +// ✅ Correct — borderless table ensures "统一社会信用代码:", "地址:", "法定代表人:" align +function partyInfoBlock(partyLabel, partyName, fields) { + // fields: [["Unified Social Credit Code", value], ["Address", value], ["Legal Representative", value]] + const NB = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; + const noBorders = { top: NB, bottom: NB, left: NB, right: NB }; + + const headerPara = new Paragraph({ spacing: { before: 200, after: 120 }, + children: [new TextRun({ text: `${partyLabel}: ${safeText(partyName, "【Company full name】")}`, + size: 24, font: { eastAsia: "SimSun", ascii: "Times New Roman" } })] + }); + + const infoTable = new Table({ + width: { size: 90, type: WidthType.PERCENTAGE }, + borders: { top: NB, bottom: NB, left: NB, right: NB, insideHorizontal: NB, insideVertical: NB }, + rows: fields.map(([label, value]) => new TableRow({ + children: [ + new TableCell({ + width: { size: 35, type: WidthType.PERCENTAGE }, + borders: noBorders, + margins: { top: 40, bottom: 40, left: 420, right: 60 }, + children: [new Paragraph({ + children: [new TextRun({ text: `${label}:`, size: 24, + font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], + })], + }), + new TableCell({ + borders: noBorders, + margins: { top: 40, bottom: 40, left: 60, right: 120 }, + children: [new Paragraph({ + children: [new TextRun({ text: safeText(value, `【Please fill in: ${label}】`), size: 24, + font: { eastAsia: "SimSun", ascii: "Times New Roman" } })], + })], + }), + ], + })), + }); + + return [headerPara, infoTable]; +} + +// Usage: +const partyAChildren = partyInfoBlock("Party A (甲方)", config.partyA?.name, [ + ["Unified Social Credit Code (统一社会信用代码)", config.partyA?.creditCode], + ["Address (地址)", config.partyA?.address], + ["Legal Representative (法定代表人/负责人)", config.partyA?.legalRep], +]); +``` + +**Rules:** +1. Party A and Party B info blocks must use the **same table column widths** — labels align across both blocks +2. Use `safeText()` for all field values — never output `undefined` +3. Label column width should accommodate the longest label (e.g., "统一社会信用代码") +4. The indent (`margins.left: 420`) simulates sub-level nesting under the party name + +--- + +## Signature Block + +Left-right symmetric, structured, easy to adjust in Word. Never write as scattered paragraphs. + +Required fields for each party: +- Party name (seal) +- Legal representative / Authorized representative +- Contact person +- Contact information +- Signing location +- Date: 【____/____/____】 + +Use a borderless 2-column table for symmetry. **Every field value must use `safeText()`** — never output `undefined` or empty string. If a field is not provided, use the appropriate `【Please fill in】` placeholder. + +```js +// ✅ Correct signature block — safeText for all values +function buildSignatureBlock(partyA, partyB) { + const fields = ["Party (Seal)", "Legal Rep / Authorized Rep (Signature)", "Contact Person", "Contact Info", "Signing Location", "Date"]; + const NB = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; + const noBorders = { top: NB, bottom: NB, left: NB, right: NB }; + + return new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { top: NB, bottom: NB, left: NB, right: NB, insideHorizontal: NB, insideVertical: NB }, + rows: fields.map((label, i) => { + const aVal = i === fields.length - 1 ? "【____/____/____】" : safeText(partyA?.[i], ""); + const bVal = i === fields.length - 1 ? "【____/____/____】" : safeText(partyB?.[i], ""); + const displayA = i === 0 ? `Party A (甲方): ${aVal}` : `${label}: ${aVal}`; + const displayB = i === 0 ? `Party B (乙方): ${bVal}` : `${label}: ${bVal}`; + return new TableRow({ + children: [ + new TableCell({ width: { size: 50, type: WidthType.PERCENTAGE }, borders: noBorders, + margins: { top: 80, bottom: 80, left: 120, right: 60 }, + children: [new Paragraph({ children: [new TextRun({ text: displayA, size: 24, color: "000000" })] })] }), + new TableCell({ width: { size: 50, type: WidthType.PERCENTAGE }, borders: noBorders, + margins: { top: 80, bottom: 80, left: 60, right: 120 }, + children: [new Paragraph({ children: [new TextRun({ text: displayB, size: 24, color: "000000" })] })] }), + ], + }); + }), + }); +} +``` + +--- + +## Monetary Amount Format + +Contracts must show amounts in **both uppercase Chinese and numeric format**: + +``` +Contract amount: RMB One Million Two Hundred Thirty-Four Thousand Five Hundred Sixty-Seven Yuan (¥1,234,567.00) +``` + +--- + +## Style Rules + +- **NO cover page** — title page is the first page (title + contract number at top) +- **NO TOC** unless >20 clauses +- **NO decorative elements** — contracts must be formal and clean +- **Line spacing**: 1.5x (line: 360) — ⚠️ scene override (Profile A default is 1.3x/312; contracts use 1.5x for readability and annotation space) +- **Body**: Justified, first-line indent 480 twips +- **Color**: pure black "000000" throughout — no colored text + +--- + +## Scene-Specific Quality Checks + +In addition to universal checks (see `references/common-rules.md`): + +### Format +- [ ] Party information complete (full name / address / legal representative / contact) +- [ ] Signature block properly formatted, symmetrical, all fields present +- [ ] Monetary amounts shown in both uppercase and numeric format +- [ ] Clause numbering sequential with no gaps +- [ ] No cover page (title page is first page) +- [ ] No Markdown list markers mixed into clause hierarchy + +### Content +- [ ] Clause numbering system consistent, no mixing +- [ ] Transaction closure complete (subject → consideration → performance → acceptance → breach → dispute) +- [ ] Breach liability corresponds to main obligations +- [ ] Dispute resolution explicitly stated (or placeholder for confirmation) +- [ ] All unconfirmed variables use `【】` placeholders consistently +- [ ] Language is formal, restrained, subjects are explicit +- [ ] No fabricated statute numbers or overreaching legal conclusions +- [ ] High-risk clauses include manual review notes +- [ ] Terminology consistent throughout +- [ ] Appendix-contract relationship explicitly stated + +### Closure +- [ ] Performance deadline → delay consequences specified +- [ ] Payment milestones → conditions and invoice requirements specified +- [ ] Delivery obligation → acceptance rules and objection period specified +- [ ] Termination right → conditions and post-termination handling specified +- [ ] Confidentiality obligation → scope, exceptions, duration, breach consequences specified +- [ ] Force majeure → notice and mitigation duties specified diff --git a/skills/docx/scenes/copywriting.md b/skills/docx/scenes/copywriting.md new file mode 100755 index 0000000..9138f6e --- /dev/null +++ b/skills/docx/scenes/copywriting.md @@ -0,0 +1,139 @@ +# Scene: Copywriting / Script + +## Scope + +Broadcast scripts, product promotion copy, livestream scripts, presentation scripts, speeches, hosting scripts, short video scripts — any document where the goal is **spoken delivery**. + +→ Placeholder convention & universal prohibitions — see `references/common-rules.md` +→ Font profile: **B (Visual)** — see `references/common-rules.md` + +--- + +## 1. Core Principles + +⚠️ **A broadcast script is NOT a report, NOT a spec sheet, NOT an encyclopedia.** + +The goal is for the audience to **understand on first listen, remember key points, and take action.** Therefore: + +1. **Highlight selling points, don't pile specs:** Each paragraph covers only 1–2 core points with relatable scenario descriptions +2. **Conversational tone:** Use "you" not "the user"; use natural speech, not corporate jargon +3. **Rhythm:** Alternate long and short sentences, insert pause markers, avoid wall-of-text paragraphs +4. **Length discipline:** ~250–300 words per minute of speech; a 5-minute script should not exceed 1500 words +5. **Information consistency:** All data, model numbers, prices must be consistent throughout — no self-contradiction + +--- + +## 2. Document Structure + +Completely different from reports: + +``` +Title (centered, short and punchy) +──────────────────── +[Opening] ← Grab attention, 1–2 sentences +[Core Para 1] ← One selling point/opinion + scenario +[Core Para 2] ← One selling point/opinion + scenario +[Core Para 3] ← One selling point/opinion + scenario (max 3–5 paras) +[Closing] ← Summary + Call to Action (CTA) +──────────────────── +[Notes] ← Supplementary info, data sources (optional, small grey text) +``` + +### Decisions +- **Cover:** ❌ Not needed +- **TOC:** ❌ Not needed +- **Header/footer:** Optional, minimal +- **Sections:** Single section sufficient +- **Line spacing:** `line: 400` (slightly larger than standard 1.5x for reading/marking ease) + +--- + +## 3. Layout Standards + +### Font Specifications + +| Element | Font | Size | Style | +|---------|------|------|-------| +| Title | SimHei | 18pt (size:36) | Bold, centered | +| Section heading / highlight | SimHei | 14pt (size:28) | Bold | +| Body | Microsoft YaHei | 12pt (size:24) | Left-aligned | +| Rhythm markers | Microsoft YaHei | 10.5pt (size:21) | Grey 999999, italic | +| Notes | Microsoft YaHei | 10pt (size:20) | Grey 666666 | + +### Paragraph Spacing +```js +// Generous spacing between paragraphs for reading/breathing pauses +spacing: { before: 200, after: 200, line: 400 } +// Larger gap between core sections +sectionGap: { before: 400, after: 200 } +``` + +### Key Point Highlighting +Use **bold** or **accent-colored text** to mark key selling points: +```js +new TextRun({ text: "Key selling point", bold: true, color: c(P.accent) }) +``` + +### Rhythm Markers (optional) +Insert small grey markers where pauses, emphasis, or tone changes are needed: +```js +new Paragraph({ spacing: { before: 60, after: 60 }, + children: [new TextRun({ text: "[Pause 2 sec]", size: 21, color: "999999", italics: true })] }) +// Or inline: new TextRun({ text: " [emphasis] ", size: 18, color: "999999", italics: true }) +``` + +--- + +## 4. Content Quality Rules + +### Information Density Guide + +| Script Type | Duration | Word Count | Core Paragraphs | +|-------------|----------|-----------|----------------| +| Short video | 30–60 sec | 150–300 | 1–2 | +| Product promotion | 2–3 min | 500–800 | 3–4 | +| Presentation / Speech | 5–10 min | 1200–2500 | 5–8 | +| Hosting script | Per agenda | Per segment | Per segment | + +### Scene-Specific Prohibitions + +1. **No spec dumping:** Do not list all product specifications in tables. Select 2–3 most persuasive data points and express them through scenarios +2. **No information contradiction:** Model numbers, prices, data appearing multiple times must be perfectly consistent +3. **No report tone:** No "in conclusion", "research indicates", "as mentioned above" — this is spoken word +4. **No lengthy citations:** Broadcast scripts do not need quotes, footnotes, or references +5. **No dense layout:** Paragraphs must have visible spacing — no screen-filling text walls + +### Product Promotion Specific Rules +- **Opening:** Lead with pain point / scenario ("Does your washing machine still smell after a cycle?"), not self-introduction +- **Product intro:** Compare only 1–2 competitive dimensions at a time — not a full review +- **Price anchor:** State original/market price first, then discount price — create contrast +- **CTA:** Explicitly state the action ("Click the link below", "Type 1 in comments") + +--- + +## 5. Palette + +Broadcast scripts use clean, simple colors — no complex visual design needed: + +```js +const P = { + primary: "#1A1A1A", // Title + body: "#333333", // Body + secondary: "#666666", // Notes + accent: "#E85D3A", // Key highlight (warm, energetic) + surface: "#FFF8F5", // Background (if needed) +}; +``` + +--- + +## 6. Scene-Specific Quality Checks + +In addition to universal checks (see `references/common-rules.md`): + +- [ ] Total word count within target range (not exceeding) +- [ ] Each core paragraph has only 1–2 selling points (no dumping) +- [ ] Conversational tone present (not report/formal style) +- [ ] Information consistent throughout (model, price, data — no contradictions) +- [ ] Paragraph spacing sufficient (visually not crowded) +- [ ] Clear attention-grabbing opening + closing CTA diff --git a/skills/docx/scenes/exam.md b/skills/docx/scenes/exam.md new file mode 100755 index 0000000..a3061b7 --- /dev/null +++ b/skills/docx/scenes/exam.md @@ -0,0 +1,698 @@ +# Scene: Exam Paper + +## Overview + +Exam papers are among the most critical document types in education. Unlike general documents, they require high precision in layout, print compatibility, and subject-specific formatting. This specification covers the complete workflow from page framework to subject-specific features. + +→ Universal prohibitions — see `references/common-rules.md` +→ **Note:** Exam papers use their OWN font/layout specs (not Profile A defaults). All text is pure black/white/grey for photocopy clarity. + +--- + +## 1. Page Setup & Framework + +### Paper Specifications + +| Type | Paper | Orientation | Use Case | +|------|-------|-------------|----------| +| Practice / Unit quiz | A4 | Portrait | Daily practice, homework, quizzes | +| Formal exam | A3 | Landscape + 2-column | Midterm / final / standardized (requires OOXML) | +| Answer sheet | A4 | Portrait | Standalone answer card | + +### Margins + +```js +// A4 portrait — no seal line +page: { size: { width: 11906, height: 16838 }, + margin: { top: 850, bottom: 850, left: 1200, right: 1200 } } + +// A4 portrait — with seal line (left binding area reserved) +page: { size: { width: 11906, height: 16838 }, + margin: { top: 850, bottom: 850, left: 2200, right: 850 } } + +// A3 landscape dual-column (requires OOXML) +// ⚠️ A3 dual-column may render slightly differently in WPS vs Word. Test in both before batch printing. +page: { size: { width: 23812, height: 16838, orientation: PageOrientation.LANDSCAPE }, + margin: { top: 850, bottom: 850, left: 2200, right: 850 } } +``` + +### Section Handling + +Different parts should use section breaks (`SectionType.NEXT_PAGE`): +- **Header area (full-width):** Title, instructions, score table (no columns) +- **Content area:** Questions (may use columns) +- **Composition / answer sheet:** Independent section, independent format +- **Attachment pages:** Large maps/diagrams for geography/biology can be separate pages + +```js +sections: [ + { properties: { /* Header section — no columns */ }, children: [...] }, + { properties: { type: SectionType.CONTINUOUS, column: { count: 2, space: 720 } }, children: [...] }, + { properties: { type: SectionType.NEXT_PAGE }, children: [...] }, // Composition +] +``` + +### Template-First Principle + +⚠️ **Build framework first, fill content second.** Before writing questions, determine: +1. Paper size + margins +2. Whether seal line is needed +3. Whether columns are used +4. Question type structure and point allocation +5. Whether composition grid / answer sheet is needed + +--- + +## 2. Seal Line & Student Information Area + +### When to Use Seal Line + +| Scenario | Seal Line | Student Info Position | +|----------|-----------|---------------------| +| Formal standardized exam | ✅ Required | Left vertical info column | +| Midterm / Final | ✅ Recommended | Left vertical info column | +| Unit quiz | ❌ Optional | Header horizontal info row | +| Daily practice | ❌ Skip | Header horizontal info row | + +### Seal Line Implementation + +#### Method 1: Header horizontal prompt (simple) +```js +headers: { default: new Header({ children: [ + new Paragraph({ alignment: AlignmentType.CENTER, + children: [new TextRun({ + text: ".............. Seal ...... Line ...... Do ...... Not ...... Answer ...... Inside ..............", + size: 16, color: "999999", font: "SimSun" })] }) +] }) } +``` + +#### Method 2: Vertical text box (OOXML advanced) +```xml + + + + Name:________ Class:________ ID:________ + + + - - - - - - - - - Seal Line - - - - - - - - - + + +``` + +### Student Info Row + +```js +// Horizontal info row (when no seal line) — borderless 3-column table +new Table({ + alignment: AlignmentType.CENTER, columnWidths: [2800, 2800, 2800], + rows: [new TableRow({ children: [ + cell("Name: ______________"), + cell("Class: ______________", AlignmentType.CENTER), + cell("ID: ______________", AlignmentType.RIGHT), + ] })] +}) +``` + +Fill lines should be moderate length (10–14 underscore chars). Label order: Name → Class → Student ID. + +--- + +## 3. Paper Header & Title Area + +### Structure + +``` +School name (16pt SimHei, centered) +Exam title (14pt SimHei, centered) — e.g., "2025–2026 Academic Year Second Semester Midterm" +Subject title (14pt SimHei, centered) — e.g., "Grade 7 Mathematics" +Student info row +Instructions (10pt SimSun, centered, grey) +Score table (as needed) +``` + +### Font Specifications + +| Element | Font | Size | Style | +|---------|------|------|-------| +| School name | SimHei | 16pt (size:32) | Bold, centered | +| Exam title | SimHei | 14pt (size:28) | Bold, centered | +| Subject title | SimHei | 14pt (size:28) | Bold, centered | +| Instructions | SimSun | 10pt (size:20) | Grey 333333, centered | +| Student info | SimSun | 10.5pt (size:21) | Normal | + +### Instructions Content + +Should include: total score, exam duration, answer method, special requirements (e.g., calculator allowed). + +### Score Table +- Header row: light grey background F0F0F0, centered +- Columns: Question type | Section names... | Total +- Rows: Points | Section points... | Total points +- Row: Score | blank... | blank +- Table centered, 80% page width + +⚠️ **Header area should not be too full** — title + info + instructions + score table should not exceed 1/3 of the page. + +--- + +## 4. Content Layout Rules + +### Color Palette + +```js +// Exam papers use only black/white/grey for clear photocopying +const C = { + title: "000000", body: "000000", section: "333333", + seal: "999999", answerLine: "CCCCCC", headerBg: "F0F0F0", gridLine: "DDDDDD", +}; +``` + +### Column Usage + +| Subject / Question Type | Recommendation | +|------------------------|----------------| +| Math multiple choice + fill-in | ✅ Suitable for columns | +| Physics multiple choice | ✅ Suitable for columns | +| Chinese reading / composition | ❌ Not suitable | +| English cloze / reading | ❌ Not suitable | +| History source-based | ❌ Not suitable | +| Geography map reading | ❌ Not suitable | + +### Question Numbering + +Entire paper uses consistent three-level numbering: +- **Major sections:** I, II, III, IV... (Chinese: 一、二、三、四…) +- **Questions:** 1. 2. 3. ... (Arabic + period) +- **Sub-questions:** (1) (2) (3) ... (parenthesized) + +⚠️ **No extra symbols before question numbers** (no `•`, `▸`, `▪`, `-`, `*`). The number itself is the only marker. **Never use docx numbering/bullet list styles** for question numbers — must use plain TextRun manual numbering. + +```js +// ✅ Correct — plain TextRun manual numbering +new Paragraph({ spacing: { before: 120, after: 60, line: 360 }, + children: [new TextRun({ text: `${i+1}. ${question}`, size: 21, font: { eastAsia: "SimSun" } })] }) + +// ❌ Wrong — numbering causes Word to add bullets +new Paragraph({ numbering: { reference: "xxx", level: 0 }, // ← Forbidden! + children: [new TextRun({ text: question })] }) +``` + +### Question Spacing + +```js +sectionTitle: { before: 300, after: 150 } // Major section headers +question: { before: 120, after: 80 } // Between questions +subQuestion: { before: 60, after: 40 } // Between sub-questions +``` + +### Page Break Control + +⚠️ Key principles: +- **Question stem and answer area must not split** across pages +- **Source material and questions on same page** +- **Figures adjacent to their questions** +- **Avoid orphan lines** — question stem, options, answer area appear as a group + +```js +new Paragraph({ keepNext: true, keepLines: true, children: [...] }) +``` + +⚠️ **Answer question page break rule (mandatory):** + +Complete combination (stem + figure + answer lines) must be considered as a unit. If remaining space cannot fit stem + figure + at least 3 answer lines, push entire question to next page. + +Use `keepNext: true` to chain: stem → figure → first 3 answer lines. + +--- + +## 5. Font & Paragraph Standards + +### Underline Formatting for "Underlined Parts" (Mandatory) + +When a question references "underlined part" (划线部分), the relevant text MUST use actual underline formatting (`underline: { type: UnderlineType.SINGLE }`). **Never** show "划线部分为 XXX" as plain text annotation — the underline must be visually rendered. + +```js +// ✅ Correct — actual underline on the referenced text +new Paragraph({ children: [ + new TextRun({ text: "1. It is ", size: 21, font: { ascii: "Times New Roman" } }), + new TextRun({ text: "a butterfly", size: 21, font: { ascii: "Times New Roman" }, + underline: { type: UnderlineType.SINGLE, color: "000000" } }), + new TextRun({ text: ". (Ask about the underlined part)", size: 21, font: { ascii: "Times New Roman" } }), +]}) + +// ❌ Wrong — underlined part described as annotation text +new TextRun({ text: "1. It is a butterfly. (对划线部分提问) 注:划线部分为 a butterfly" }) +``` + +### Font Hierarchy + +| Element | Font | Size | Style | +|---------|------|------|-------| +| Section title | SimHei | 11pt (size:22) | Bold | +| Question content | SimSun | 10.5pt (size:21) | Normal | +| Points annotation | SimSun | 10pt (size:20) | In parentheses | +| Reading material | KaiTi/SimSun | 10.5pt (size:21) | KaiTi to differentiate | +| Notes/source | SimSun | 9pt (size:18) | Grey 666666 | +| Seal line | SimSun | 8pt (size:16) | Grey 999999 | +| Page number | SimSun | 9pt (size:18) | Centered | + +### Line Spacing +```js +line: 360 // ~1.5x for readability +answerLine: 500 // Answer line spacing for writing room +``` + +### Paragraph Rules +- ⚠️ **Never use consecutive returns for whitespace** — use `spacing.before/after` +- Chinese questions use Chinese punctuation; English materials use English punctuation +- Mixed CN/EN: use Times New Roman or Calibri for English text + +--- + +## 6. Multiple Choice Layout + +### Core Rule + +⚠️ **Options must NEVER be aligned with spaces!** Must use borderless tables. + +### Option Layout — Borderless Table + +```js +// Short options: 4 columns in 1 row +new Table({ + columnWidths: [2200, 2200, 2200, 2200], + rows: [new TableRow({ children: ["A","B","C","D"].map((label, i) => + new TableCell({ borders: NBs, width: { size: 2200, type: WidthType.DXA }, + margins: { top: 0, bottom: 0, left: 60, right: 60 }, + children: [new Paragraph({ spacing: { before: 0, after: 0 }, + children: [new TextRun({ text: `${label}. ${options[i]}`, size: 21, font: "SimSun" })] })] + }) + ) })] +}) +// Medium options: 2 columns, 2 rows +// Long options: 1 column, 4 rows +``` + +### Option Length Detection +```js +function getOptionLayout(options) { + const maxLen = Math.max(...options.map(o => o.length)); + if (maxLen <= 6) return "4col"; + if (maxLen <= 15) return "2col"; + return "1col"; +} +``` + +--- + +## 7. Fill-in-the-Blank Layout + +```js +// Blank line length matches expected answer: +// Short answer (number/word): 8 underscores +// Medium (phrase): 14 underscores +// Long (sentence): 20 underscores +new Paragraph({ spacing: { before: 140, after: 80, line: 400 }, + children: [new TextRun({ text: `${num}. Question text ________________.`, size: 21, font: "SimSun" })] }) +``` + +⚠️ Fill-in lines must not break across lines — if line is too long, put the blank on the next line. + +--- + +## 8. Short Answer / Problem-Solving Layout + +### Question + Points +```js +new Paragraph({ spacing: { before: 200, after: 60, line: 360 }, keepNext: true, + children: [new TextRun({ text: `${num}. (${points} pts) ${question}`, size: 21, font: "SimSun" })] }) +``` + +### Answer Lines +```js +// Light grey answer lines (CCCCCC), NOT black +// ⚠️ Answer lines are ONLY for writing space within each question — never as dividers between questions +function answerLines(count) { + return Array(count).fill(null).map(() => + new Paragraph({ spacing: { before: 0, after: 0, line: 500 }, + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" } }, + children: [new TextRun({ text: " ", size: 21 })] }) + ); +} +``` + +⚠️ **Separation between questions:** + +Use **only spacing** (`spacing.before: 200`) for visual separation between questions. **Forbidden:** +- ❌ Grey horizontal lines (borders) +- ❌ Color block dividers (Table-simulated separators) +- ❌ Symbol dividers (e.g., `───────`) +- ❌ Any visual separator decoration + +### Answer Space vs. Points + +| Points | Suggested Lines | Description | +|--------|----------------|-------------| +| 2–4 | 3–4 lines | Simple calculation / short answer | +| 5–8 | 6–8 lines | Medium problem | +| 10–12 | 8–10 lines | Complex problem | +| 14–20 | 10–14 lines | Comprehensive / essay question | + +--- + +## 9. Source-Based / Reading Question Layout + +### Material vs. Question Separation + +```js +// Material area — indented + KaiTi to differentiate +new Paragraph({ indent: { left: 420, right: 420 }, spacing: { before: 100, after: 100, line: 380 }, + children: [new TextRun({ text: materialText, size: 21, font: "KaiTi" })] }) +// Source attribution +new Paragraph({ alignment: AlignmentType.RIGHT, indent: { right: 420 }, + children: [new TextRun({ text: "— from \"XXX\"", size: 18, color: "666666", font: "SimSun" })] }) +``` + +### Key Principles +- Material title, source, body, and notes use different fonts +- Long materials: increase line spacing (line: 380–400) +- Material and corresponding questions on same page +- Sub-question numbers (1)(2)(3) clearly correspond to material +- **Data tables in materials MUST use proper docx `Table` objects** — never render tabular data as Markdown plain text (`| col | col |`). This includes statistics tables, climate data tables, comparison tables, and any structured data within question materials. Use bordered tables (see § 13 Table Usage Standards) with appropriate header row styling. + +--- + +## 10. Composition / Writing Area + +### Grid Count Calculation + +⚠️ **Grid count must exceed required word count by 20–30%** (for title, paragraph indents, line breaks). + +| Required Words | Min Grid Count | Recommended Layout | +|---------------|---------------|-------------------| +| 400 | 500 | 25 rows × 20 cols | +| 600 | 750 | 38 rows × 20 cols | +| 800 | 1000 | 50 rows × 20 cols | +| 1000 | 1250 | 63 rows × 20 cols | + +```js +function calcGridSize(requiredWords, colsPerRow = 20) { + const totalCells = Math.ceil(requiredWords * 1.25); + const rows = Math.ceil(totalCells / colsPerRow); + return { rows, colsPerRow, totalCells: rows * colsPerRow }; +} +``` + +### Chinese Composition Grid + +```js +function compositionGrid(rows, colsPerRow) { + const cellSize = Math.floor(8800 / colsPerRow); + return new Table({ + columnWidths: Array(colsPerRow).fill(cellSize), + rows: Array(rows).fill(null).map(() => + new TableRow({ + height: { value: cellSize, rule: HeightRule.EXACT }, + children: Array(colsPerRow).fill(null).map(() => + new TableCell({ borders: thinBs("DDDDDD"), width: { size: cellSize, type: WidthType.DXA }, + children: [new Paragraph({ children: [] })] }) + ) + }) + ) + }); +} +``` + +### English Writing Area (Horizontal Lines) — MANDATORY for English Writing Questions + +⚠️ **Every English writing/composition question MUST include ruled horizontal lines.** A blank area without lines is FORBIDDEN — students need lines to write on. + +```js +function writingLines(count) { + return Array(count).fill(null).map(() => + new Paragraph({ spacing: { before: 0, after: 0, line: 560 }, + borders: { bottom: { style: BorderStyle.SINGLE, size: 1, color: "CCCCCC" } }, + children: [new TextRun({ text: " ", size: 21 })] }) + ); +} +``` + +**Line count by word requirement:** +| Required Words | Lines | +|---------------|-------| +| ≤50 | 8 | +| 50–80 | 10 | +| 80–120 | 12 | +| 120+ | 15 | + +**Rules:** +1. Lines must appear immediately after the writing prompt paragraph +2. Line color: light grey `CCCCCC` (print-friendly, not visually heavy) +3. Line spacing: `line: 560` (provides adequate writing room) +4. Chinese composition uses grid (`compositionGrid`), English uses lines (`writingLines`) — never mix them up +``` + +### Composition Area Requirements +- Independent section or clear separation +- Title space reserved (for self-chosen topics) +- Word count prompt visible ("No fewer than 800 words" / "About 120 words") +- Grid/line colors light — must not interfere with writing +- Pages continuous, not split + +--- + +## 11. Answer Key (参考答案) + +### Output Rules + +1. **Default (user does not request answers in the same file):** Generate the answer key as a **separate .docx file** (e.g., `exam.docx` + `exam_answers.docx`). This prevents students from accidentally seeing answers. +2. **User explicitly requests answers in the same file:** Place the answer key on an **independent page** using `SectionType.NEXT_PAGE`. Answer key MUST NOT appear on the same page as any exam question. + +### Separate File Format (Default) + +The answer key file should include: +- Title: "《{exam title}》参考答案" (SimHei, 14pt/size:28, bold, centered) +- Same question numbering as the exam +- Concise answers (letter choices, key words, short solutions) +- Font: SimSun 10.5pt (size: 21) + +### Same File Format (When User Requests) + +```js +// Answer key as a separate section — MUST use SectionType.NEXT_PAGE +{ + properties: { type: SectionType.NEXT_PAGE, + page: { margin: { top: 850, bottom: 850, left: 1200, right: 1200 } } }, + children: [ + new Paragraph({ + alignment: AlignmentType.CENTER, spacing: { after: 300 }, + children: [new TextRun({ text: "参考答案", size: 28, bold: true, + font: { eastAsia: "SimHei" } })], + }), + // ... answer content paragraphs + ], +} +``` + +### Rules +1. ⚠️ **Never place answer content directly after the last question without a page/section break** +2. Answer content should be concise — no answer lines, no grid, plain text only +3. Calculation/proof questions: show key steps, not just final answer +4. If the exam has figures, answers may reference "see Figure X" without re-embedding + +--- + +## 12. Figures & Illustrations + +### Image Insertion +```js +new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 100, after: 60 }, + children: [new ImageRun({ data: imageBuffer, transformation: { width: 300, height: 200 }, type: "png" })] }) +new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 100 }, + children: [new TextRun({ text: "(Figure 1)", size: 18, color: "666666", font: "SimSun" })] }) +``` + +### Key Principles +- Images set as inline (default) to prevent floating +- Resolution sufficient for print clarity +- **B&W print compatible:** images must remain distinguishable when printed in grayscale +- Figure numbers and captions complete +- Figures adjacent to corresponding questions +- Maps must have: scale bar, north arrow, legend +- Coordinate graphs must have: axis labels, tick marks, units + +### ⚠️ Figure-Text Order (Strictly Enforced) + +**For questions with figures, element order must be:** +``` +1. Question stem (keepNext: true) +2. Figure (centered, keepNext: true) +3. Answer lines / answer area +``` + +**Forbidden:** answer lines between stem and figure, or figure after answer lines. + +### Figure Content Matching +- **Figures must be semantically consistent with question stem:** if question says "triangle ABC", figure must label vertices A, B, C +- Geometry annotations must match described angles, side lengths +- Function graphs must mark key points mentioned in the question +- Physics experiment diagrams must match described apparatus +- Figure width: geometry ≤ 50% page width, data/experiment ≤ 70% + +### ⚠️ Figure Diversity Rule (Mandatory) + +**No duplicate figures in the entire paper.** Even if two questions involve the same type (e.g., both triangles), each must have a distinct figure: +1. Different labels (different vertex letters, angles, side lengths) +2. Different shapes (acute vs. right vs. obtuse triangle) +3. Different styling (if applicable) + +If using matplotlib, each call must use **different parameters and data** — never copy the same generation code. + +### Subject-Specific Figure Requirements + +| Subject | Common Types | Special Requirements | +|---------|-------------|---------------------| +| Math | Geometry, functions, coordinates | No distortion, clear labels | +| Physics | Circuits, mechanics, apparatus | Standard symbols, correct arrows | +| Chemistry | Apparatus, molecular structures | Reagent names labeled | +| Biology | Cell, organ, ecosystem diagrams | Labels not too small | +| Geography | Maps, contour lines, statistics | Legend + scale + north arrow | + +--- + +## 13. Formulas & Special Symbols + +### Formulas +Math/physics/chemistry formulas use **LaTeX → docx-js Math mapping** (see `references/math-formulas.md`): +- Basic (fractions, sub/superscript, roots) → docx-js Math components +- Complex (3+ nesting, matrices) → matplotlib PNG fallback +- Never hand-type Unicode formula approximations + +### Common Unicode Math Symbols +``` +× ÷ ± ∓ ≠ ≈ ≤ ≥ ∞ √ ∑ ∏ ∫ ∂ ∆ ∇ +α β γ δ ε θ λ μ π σ φ ω +⊂ ⊃ ∈ ∉ ∪ ∩ ∅ ∀ ∃ +→ ← ↑ ↓ ⇒ ⇔ ° ′ ″ ‰ ² ³ ⁴ ⁿ ₁ ₂ ₃ +``` + +### Chemical Formulas +Subscripts/superscripts must be correct: H₂O, CO₂, Fe₂O₃, Ca(OH)₂ +Reaction arrows: → ⇌ ↑ ↓ + +--- + +## 14. Table Usage Standards + +### Borderless Tables (for alignment) +For: option alignment, info rows, question number + points alignment +```js +const NB = { style: BorderStyle.NONE, size: 0, color: "FFFFFF" }; +const NBs = { top: NB, bottom: NB, left: NB, right: NB }; +``` + +### Bordered Tables (for data display) +For: score tables, data tables, statistics +```js +const thinB = (c="000000") => ({ style: BorderStyle.SINGLE, size: 1, color: c }); +const thinBs = (c="000000") => ({ top: thinB(c), bottom: thinB(c), left: thinB(c), right: thinB(c) }); +``` + +### Table Standards +- Cell padding moderate (margins: top/bottom 40–60, left/right 60–80) +- Consistent border thickness +- Header row: light grey F0F0F0 background +- Avoid cross-page tables +- Tables centered (`alignment: AlignmentType.CENTER`) + +--- + +## 15. Headers & Footers + +### Page Numbers +```js +footers: { default: new Footer({ children: [ + new Paragraph({ alignment: AlignmentType.CENTER, + children: [ + new TextRun({ children: [PageNumber.CURRENT], size: 18, font: "SimSun" }), + ] }) +] }) } +``` + +⚠️ **Denominator FORBIDDEN** — never use `PageNumber.TOTAL_PAGES` or "Page X of Y". Show only current page number. + +### Headers +- May contain seal line prompt or subject name +- Small font (8–9pt), grey color (999999) +- Should not be visually heavy — must not compete with content + +--- + +## 16. Subject-Specific Standards + +### Chinese Language +- Reading, classical poetry, composition: **no columns** +- Poetry preserves original line breaks +- Classical text needs annotation area (smaller font, indented) +- Composition grid in independent section, grid count via `calcGridSize` (800 words → 50×20 = 1000 cells) +- Dictation questions: horizontal lines, moderate length +- Reading materials: use KaiTi to differentiate + +### Mathematics +- Multiple choice, fill-in: suitable for neat layout +- Formulas: Unicode symbols or OOXML +- Geometry/function graphs must be clear, undistorted +- Problem-solving: sufficient working space +- Coordinate graphs: labeled axes, tick marks + +### English +- English font: Times New Roman, moderate character spacing +- Cloze: numbers in text, options after passage +- Reading comprehension: material + questions as groups +- Writing area: horizontal lines, not grid +- Listening (if any): numbers aligned with options + +### Physics / Chemistry / Biology +- Experiment/apparatus diagrams must be clear and accurate +- Unit symbols standardized (m/s, kg, mol/L, etc.) +- Chemical formula subscripts correct +- Calculation and experiment analysis: sufficient answer space +- Biology structure diagrams: labels not too small + +### History / Politics +- Source-based questions are lengthy — **no columns** +- Dates, figures, events clearly labeled +- Essay questions: more whitespace than multiple choice +- Historical sources cite provenance +- Chart materials in logical order + +### Geography +- Maps are the focus — must be clear +- Legend, scale bar, north arrow required +- Map and question close together — avoid page turns +- Map reading questions: balance figure and text space +- Contour line values clearly labeled + +--- + +## Final Review Checklist + +After generating an exam paper, check every item: + +- [ ] Question numbers sequential, points correct, total correct +- [ ] Question stems match options / materials / illustrations one-to-one +- [ ] **Figures come after stem, before answer area** (strict order) +- [ ] **Figure content matches question semantics** (labels, symbols match) +- [ ] **Composition grid count ≥ required words × 1.25** (800 words → at least 1000 cells) +- [ ] Options aligned with borderless tables (not spaces) +- [ ] No wrong pages, missing pages, **no extra blank pages** +- [ ] Images / tables / formulas positioned correctly +- [ ] **No Markdown table syntax in document** — all data tables use proper docx Table objects +- [ ] Fonts, sizes, line spacing consistent +- [ ] Answer space matches difficulty and point value +- [ ] Clear when printed in B&W +- [ ] Subject-specific layout handled properly +- [ ] Seal line / page numbers / headers formatted correctly +- [ ] Header info complete (school, subject, duration, total score) +- [ ] **No extra PageBreak at end of last section** +- [ ] **Answer key is either a separate file (default) or on a separate page (if user requested in same file)** — never on the same page as questions diff --git a/skills/docx/scenes/official-doc.md b/skills/docx/scenes/official-doc.md new file mode 100755 index 0000000..2af705b --- /dev/null +++ b/skills/docx/scenes/official-doc.md @@ -0,0 +1,411 @@ +# Scene: Official Document (Government Notice / Letter / Reply / Minutes) + +## Goal + +Generate a complete, formal, properly structured official document ready for Word delivery. Must simultaneously meet: +- Correct document type, complete structure, clear elements +- Formal government register, stable hierarchy, reliable layout +- Ready for approval, circulation, filing, issuance, or formal internal communication + +**Forbidden:** Producing outlines-only / sample paragraphs / writing advice / half-finished drafts; outputting chat-style explanations. + +→ Placeholder convention & universal prohibitions — see `references/common-rules.md` +→ **Note:** This scene uses its OWN font and layout specs (not Profile A defaults), because official documents follow GB/T 9704 standards. + +--- + +## Scope & Document Type Boundaries + +This scene covers: +1. **Notice** — assigning work, communicating requirements, forwarding documents +2. **Official Letter** — between non-subordinate organizations: negotiation, inquiry, assistance requests, replies +3. **Reply (to Request)** — superior authority answering a subordinate's formal request +4. **Meeting Minutes** — recording key outcomes and agreed items + +**Important boundaries:** +- "Red header" is a format/layout, not a document type — it typically carries notices, letters, or replies +- **Not all official documents need red headers / document numbers / colophons** — only enable when user explicitly requests "red header format", "GB/T 9704 format", or "formal issuance format" +- Internal enterprise notices, business letters, meeting minutes often do NOT use full GB/T standard format +- This scene does NOT cover: speeches, press releases, promotional materials, papers, summary reports, contracts, or legal opinions + +--- + +## Document Type Routing + +```js +function selectOfficialType(keywords, purpose) { + if (/minutes|meeting/.test(keywords)) return "minutes"; + if (/reply|respond to request/.test(keywords)) return "reply"; + if (/letter|inquiry|negotiation/.test(keywords)) return "letter"; + return "notice"; // default +} +``` + +### Red Header Activation + +```js +function needsRedHeader(userRequest) { + // Only activate when explicitly requested + return /red header|GB\/T 9704|formal issuance|official format/.test(userRequest); +} +``` + +**Rules:** +- `needsRedHeader = true` → Enable red header, document number, colophon (full formal elements) +- `needsRedHeader = false` → Maintain formal style but no mandatory red header; keep only title + addressee + body + signature + +--- + +## Standard Template Structures + +### Template A: Notice +1. Red header area (if applicable) +2. Document number (if applicable) +3. Title +4. Addressee +5. Reason for issuance +6. "The relevant matters are hereby notified as follows:" +7. Notice items (expanded by hierarchy) +8. Requirements +9. Attachment notes (if any) +10. Signature (if applicable) +11. Date (if applicable) +12. Colophon (if applicable) + +**Closing phrase:** "This notice is hereby given." or "Please implement accordingly." + +### Template B: Official Letter +1. Red header area (if applicable) +2. Document number (if applicable) +3. Title +4. Addressee +5. Reason / reference to incoming letter +6. Negotiation / inquiry / reply items +7. Closing +8. Signature (if applicable) +9. Date (if applicable) +10. Colophon (if applicable) + +**Closing phrases:** "Please reply by letter." / "This letter is hereby sent." / "This is in reply." + +### Template C: Reply +1–11. Similar to Notice structure +- Addressee is typically the single requesting organization +- Must reference the incoming request document +- "After review, the reply is as follows:" +- Closing: "This is the reply." + +### Template D: Meeting Minutes +1. Title (meeting name + "Minutes") +2. Meeting overview (time, place, chair, attendees) +3. Agreed items +4. Responsibility assignments / follow-up requirements (if applicable) +5. Distribution scope (if applicable) + +**Notes:** +- Minutes record "agreed items", not a transcript of speeches +- Minutes generally do NOT follow standard red header format +- Unless user explicitly requests organizational template compliance + +--- + +## Input Recognition & Completion + +### Processing Rules +1. If user provides a template, historical document, or organizational standard → **always follow it first** +2. If information is incomplete → fill conservatively, formally, and appropriately for the government context +3. **Never fabricate** policy bases, incoming document numbers, leadership directives, meeting decisions, or official organization names +4. If critical info is missing → use standardized placeholders +5. Never present a draft as if it were already formally issued + +--- + +## Title Drafting Rules + +The title is the most critical identifying element — must accurately, concisely reflect the issuing body, subject matter, and document type. + +| Type | Format | Example | +|------|--------|---------| +| Notice | Issuing body + "regarding" + subject + "notice" | XX Municipal Government Notice on Issuing the XX Management Measures | +| Letter | Issuing body + "regarding" + subject + "letter" | XX Company Letter Regarding Land Use for XX Project | +| Reply | Issuing body + "regarding" + subject + "reply" | XX Bureau Reply on Approving Establishment of XX Branch | +| Minutes | Meeting name + "minutes" | XX Company Third General Manager Meeting Minutes | + +**Rules:** +1. Title must specify the subject — no vague titles ("Notice on Relevant Matters") +2. Titles generally do not use periods +3. Title length should be moderate — avoid excessive length + +--- + +## Addressee & CC + +### Addressee +1. The primary recipient of the document +2. On its own line, between title and body +3. Followed by full-width colon +4. Replies typically address only one requesting organization +5. Meeting minutes generally do not have a standard addressee + +### CC (Carbon Copy) +1. CC recipients are NOT addressees — do not mix them +2. CC information typically appears in the colophon area +3. Non-red-header documents should not mechanically add "CC:" lines + +--- + +## Writing Style & Register + +### Language Style +1. Must be **solemn, plain, precise, rigorous, concise** +2. **Forbidden:** Literary devices (metaphor, personification, hyperbole, rhetorical questions, exclamations) +3. **Forbidden:** Vague expressions ("approximately", "recently", "relevant departments", "as soon as possible") — unless user explicitly requires vague wording +4. Time, location, organization, scope, milestones should be as specific as possible +5. No sloganeering filler or obvious "AI boilerplate" feel + +### Common Phrase Patterns + +**Purpose phrases:** +- "In order to implement..." +- "To further standardize..." +- "To effectively carry out..." + +**Basis phrases:** +- "In accordance with the provisions of..." +- "As required by..." +- "Pursuant to relevant regulations" + +**Transition phrases:** +- Notice: "The relevant matters are hereby notified as follows:" +- Letter: "The following is hereby communicated:" +- Reply: "After review, the reply is as follows:" +- Minutes: "The agreed items of the meeting are recorded as follows:" + +**Closing phrases (must match document type):** +- Notice: "This notice is hereby given." +- Letter: "Please reply." / "This is hereby communicated." / "This is in reply." +- Reply: "This is the reply." +- Minutes: generally no fixed closing phrase + +### Conciseness +1. Use "because" not "due to the reason that..." +2. Use "to" not "for the purpose of..." +3. Name specific entities — not "relevant parties" or "related departments" +4. Name responsible units — not "all units should ensure implementation" (vague ending) + +--- + +## Body Hierarchy & Numbering + +Official document body must strictly follow the standard Chinese government numbering system: + +``` +I. General matters + (1) Sub-items + 1. Specific points + (1) Detail supplements +``` + +Original Chinese numbering: +``` +一、General matters + (一)Sub-items + 1. Specific points + (1)Detail supplements +``` + +**Rules:** +1. No level-skipping +2. **Forbidden:** Markdown list markers (`-` `*`) +3. No switching between numbering styles at the same level +4. Level 1: major tasks; Level 2: sub-items; Levels 3–4: only when truly necessary + +--- + +## Truthfulness & Caution +1. **Never fabricate** issuing bodies, incoming organizations, document numbers, leadership directives, meeting decisions, or policy bases +2. **Never** write "per the spirit of XX meeting" or "per XX directive" unless user explicitly provides these +3. **Never** fabricate titles and numbers of referenced documents in replies or letters +4. **Never** present a draft as already formally issued +5. When information is insufficient → use placeholders, never pretend elements are complete + +--- + +## Attachment Notes +1. Placed after body text, before signature +2. "Attachment:" followed by attachment name +3. Multiple attachments: numbered sequentially (Attachment 1, Attachment 2...) +4. Attachment names must be clear and specific — never fabricate unknown attachments + +--- + +## Signature & Date + +1. Document types requiring signatures should have issuing body name and date +2. Not all types mechanically require signatures (minutes typically do not) +3. Formal document dates must use Chinese numeral format with proper "〇" character + - Example: March 31, 2026 → 二〇二六年三月三十一日 +4. Document numbers use tortoiseshell brackets "〔〕" (not square brackets "[]") + - Example: X政发〔2026〕1号 +5. Date format must be consistent throughout + +--- + +## Palette + +**NO decorative colors.** Pure black text on white background. The only color is red header text. + +```js +const palette = { primary:"#000000", body:"#000000", accent:"#000000", surface:"#FFFFFF" }; +const RED_HEADER = "FF0000"; // Only for red header text +``` + +--- + +## Page Layout (GB/T 9704-2012 Standard) + +**Only for formal GB/T red-header documents.** Non-GB/T scenarios may use standard margins. + +| Property | Value | Twips | +|----------|-------|-------| +| Top margin | 3.7 cm | 2098 | +| Bottom margin | 3.5 cm | 1984 | +| Left margin | 2.8 cm | 1588 | +| Right margin | 2.6 cm | 1474 | + +```js +// GB/T red header layout +page: { size: { width: 11906, height: 16838 }, margin: { top: 2098, bottom: 1984, left: 1588, right: 1474 } } +// Non-GB/T formal documents may use standard margins: +// margin: { top: 1440, bottom: 1440, left: 1701, right: 1417 } +``` + +--- + +## Font Specifications (GB/T 9704) + +| Element | Font | Size | Style | +|---------|------|------|-------| +| Red header org name | STXiaoBiaoSong / SimSun Bold | As determined by org | Red (#FF0000), centered | +| Document title | STXiaoBiaoSong / SimSun Bold | Er Hao 22pt (size: 44) | Centered | + +**Font fallback for STXiaoBiaoSong:** This font is not installed by default on all systems. WPS ships FZXiaoBiaoSong-S13 instead. Use this fallback chain: +- Preferred: `STXiaoBiaoSong` (华文小标宋) +- Fallback 1: `FZXiaoBiaoSong-S13` (方正小标宋, available in WPS) +- Fallback 2: `SimSun` with Bold (宋体加粗, universally available) + +In code, set primary font and note the fallback: +```js +font: { eastAsia: "STXiaoBiaoSong" } +// Fallback: FZXiaoBiaoSong-S13 → SimSun Bold. User may need to install STXiaoBiaoSong for exact rendering. +``` +| Addressee | FangSong | San Hao 16pt (size: 32) | Left-aligned | +| Body | FangSong | San Hao 16pt (size: 32) | Justified, indent 640 | +| Level 1 heading | SimHei | San Hao 16pt (size: 32) | Bold | +| Level 2 heading | KaiTi | San Hao 16pt (size: 32) | Normal | +| Level 3 heading | FangSong | San Hao 16pt (size: 32) | Bold | +| Attachment notes | FangSong | San Hao 16pt (size: 32) | Left-aligned | +| Signature/date | FangSong | San Hao 16pt (size: 32) | Right-aligned | +| Page number | FangSong | Si Hao 14pt (size: 28) | Centered, "— X —" | + +```js +styles: { + default: { + document: { + run: { font: { ascii: "Times New Roman", eastAsia: "FangSong" }, size: 32, color: "000000" }, + paragraph: { spacing: { line: 560 } }, // Fixed 28pt line spacing + }, + heading1: { + run: { font: { eastAsia: "SimHei" }, size: 32, bold: true, color: "000000" }, + }, + heading2: { + run: { font: { eastAsia: "KaiTi" }, size: 32, color: "000000" }, + }, + }, +} +``` + +**Note:** For "formal administrative style" (not strict GB/T), retain the style logic but do not rigidly require every GB/T element. + +--- + +## Code Examples + +### Red Header (red-header documents only) + +```js +new Paragraph({ alignment: AlignmentType.CENTER, spacing: { before: 0, after: 200, line: Math.ceil(26 * 23), lineRule: "atLeast" }, + children: [new TextRun({ text: "XX Municipal Government", font: { eastAsia: "SimSun" }, + size: 52, bold: true, color: "FF0000" })] }) +new Paragraph({ border: { bottom: { style: BorderStyle.SINGLE, size: 4, color: "FF0000" } }, + spacing: { after: 40 }, children: [] }) +``` + +### Page Number Footer + +```js +footers: { default: new Footer({ children: [new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new TextRun({ text: "\u2014 ", size: 28 }), + new TextRun({ children: [PageNumber.CURRENT], size: 28 }), + new TextRun({ text: " \u2014", size: 28 }), + ], +})] }) } +``` + +--- + +## Style Rules + +1. **Strictly follow official document format — no decorative elements** +2. NO cover page +3. NO TOC +4. NO headers (only page numbers in footer) +5. NO colors except red header (red-header documents only) +6. NO images or charts (unless integral to document content) +7. NO fancy fonts — only FangSong, SimHei, KaiTi, STXiaoBiaoSong +8. Line spacing: fixed 28pt (`line: 560`) — **NOT** the default 1.5x + +--- + +## Scene-Specific Prohibitions + +In addition to universal prohibitions (see `references/common-rules.md`): + +1. Must not write official documents as chat replies, promotional copy, speeches, or papers +2. Must not use Markdown headings/lists/bold/italic for document hierarchy +3. Must not apply red header/document number/colophon to all document types indiscriminately +4. Must not format meeting minutes as a standard red-header notice +5. Must not use literary rhetoric, colloquial expressions, or strongly emotional language +6. Must not fabricate incoming documents, policies, document numbers, meeting decisions, or superior directives +7. Must not use excessive blank lines to create "formal appearance" +8. Must not let the document read like a report, paper, or marketing copy + +--- + +## Scene-Specific Quality Checks + +In addition to universal checks (see `references/common-rules.md`): + +### Format +- [ ] Red header text is #FF0000 and only red header uses color (red-header scenarios) +- [ ] Line spacing fixed at 28pt (line: 560) +- [ ] FangSong / SimHei / KaiTi correctly applied +- [ ] Signature right-aligned, date format correct +- [ ] No cover page, no TOC, no header +- [ ] Page number format "— X —" +- [ ] Red header / document number / colophon only where appropriate + +### Content +- [ ] Document type correctly identified, structure matches +- [ ] Title is accurate, specific, document type clear (not vague) +- [ ] Addressee, attachments, signature, colophon used appropriately +- [ ] Closing phrase matches document type +- [ ] Body hierarchy strictly follows: 一、(Level 1) →(一)(Level 2) → 1. (Level 3) →(1)(Level 4) +- [ ] No Markdown headings/lists/bold/italic mixed in +- [ ] Meeting minutes not incorrectly given standard document signature and colophon +- [ ] Date uses Chinese numerals with proper "〇" character +- [ ] Document number uses tortoiseshell brackets "〔〕" +- [ ] No fabricated incoming documents / policy bases / organizational elements +- [ ] Register is solemn and plain — no colloquial / literary / promotional tone diff --git a/skills/docx/scenes/report.md b/skills/docx/scenes/report.md new file mode 100755 index 0000000..146c3d9 --- /dev/null +++ b/skills/docx/scenes/report.md @@ -0,0 +1,340 @@ +# Scene: Report / Proposal + +## Goal + +Generate a complete, formal, well-structured report ready for Word delivery. Must simultaneously meet: +- Complete structure, clear logic, formal language, definitive conclusions +- Objective data presentation, proper Word formatting +- Ready for presentation, filing, review, submission, or internal communication + +**Forbidden:** Producing outlines-only / summaries / template annotations / half-finished drafts; outputting chat-style explanations or filler phrases like "here is the report content". + +→ Font profile: **A (Formal)** — see `references/common-rules.md` +→ Default layout: standard margins — see `references/common-rules.md` +→ Placeholder convention — see `references/common-rules.md` +→ Universal prohibitions & quality checks — see `references/common-rules.md` + +--- + +## Report Type Routing + +Auto-select structure and expression style based on user intent. If not explicit, infer from topic. + +```js +function selectReportType(keywords, topic) { + if (/analysis|competitor|industry|operations|data/.test(keywords)) return "analysis"; + if (/experiment|lab|algorithm|engineering/.test(keywords)) return "experiment"; + if (/test|QA|performance|security|compatibility/.test(keywords)) return "testing"; + if (/survey|questionnaire|interview|market research/.test(keywords)) return "research"; + if (/review|retrospective|post-mortem|summary/.test(keywords)) return "review"; + if (/proposal|feasibility|implementation|optimization/.test(keywords)) return "proposal"; + return "analysis"; // default +} +``` + +### 6 Report Types + +| Type | Use Case | Structure Focus | Expression Focus | +|------|----------|----------------|-----------------| +| analysis | Industry/competitor/operations/data analysis | Background → Dimensions → Findings → Diagnosis → Recommendations | Conclusion-first, clear dimensions, chart-supported, actionable advice | +| experiment | Scientific/academic/algorithm/engineering experiments | Objective → Environment → Method → Results → Error → Conclusion | Precise process, clear conditions, objective results, conclusion ties to hypothesis | +| testing | Functional/performance/security/compatibility testing | Overview → Scope → Plan → Results → Defects → Risks → Conclusion | Data-driven, traceable, reproducible, supports go/no-go decisions | +| research | User/market/survey/interview research | Background → Subjects & Method → Sample → Findings → Synthesis → Recommendations | Clear sample boundaries, layered findings, recommendations match findings | +| review | Project/incident retrospective, phase summary | Goals → Review → Results → Issues → Lessons → Actions | Clear facts, restrained attribution, specific action items | +| proposal | Project/optimization proposal, feasibility study | Status → Goals → Solution → Roadmap → Resources → Risks → Benefits | Strong argumentation, executable plan, clear boundaries | + +--- + +## Standard Template Structures + +### Template A: Analysis Report +1. Executive Summary +2. Background & Objectives +3. Scope, Data Sources & Methodology +4. Core Findings +5. Problem Diagnosis & Root Cause +6. Conclusions & Recommendations +7. Appendices (if needed) + +### Template B: Experiment Report +1. Abstract +2. Objective & Hypothesis +3. Environment & Materials +4. Procedure & Method +5. Data & Results +6. Error Analysis & Discussion +7. Conclusions +8. Appendices (if needed) + +### Template C: Testing Report +1. Test Overview +2. Test Scope & Environment +3. Test Plan & Case Design +4. Test Results Summary +5. Defect Analysis & Distribution +6. Risk Assessment & Outstanding Issues +7. Test Conclusions +8. Appendices (if needed) + +### Template D: Research Report +1. Research Summary +2. Background & Objectives +3. Subjects & Methodology +4. Sample & Data Description +5. Core Findings +6. Problem Synthesis +7. Recommendations & Action Direction +8. Appendices (if needed) + +### Template E: Review / Summary Report +1. Overview +2. Goals & Scope +3. Process Review +4. Results Summary +5. Issues & Root Cause Analysis +6. Lessons Learned +7. Follow-up Action Plan +8. Appendices (if needed) + +### Template F: Proposal / Feasibility Report +1. Executive Summary +2. Current State & Problem Analysis +3. Goals & Expected Outcomes +4. Solution Design +5. Implementation Roadmap & Milestones +6. Resource Requirements & Budget +7. Risk Analysis & Mitigation +8. Expected Benefits & Evaluation +9. Appendices (if needed) + +**If the user provides a company/school/course template or fixed chapter requirements, always follow those first.** + +--- + +## Input Recognition & Completion + +### User May Provide +Report topic, type, use case, audience, industry, length requirements, data sources, structure requirements, output purpose (presentation/filing/audit/review/external submission/coursework), template files, company/department/project/author/date, etc. + +### Processing Rules +1. If the user provides a template, existing document, company standard, or format example, **always follow it first** +2. If information is incomplete, fill in conservatively — completions must be **restrained, natural, credible, professional** +3. **Never fabricate** unrealistic data, conclusions, test results, business metrics, project statuses, policy backgrounds, or customer feedback +4. If critical information is missing and cannot be safely inferred, use standardized placeholders +5. If no real data is available, prefer low-hallucination approaches: "status description → analysis framework → problem synthesis → recommendations" + +--- + +## Content Quality Constraints + +### Logic & Structure +1. Report must revolve around a clear topic, objective, audience, and through-line +2. Must not just pile up background/concepts/vague statements — must demonstrate analysis, synthesis, judgment, comparison, or review value +3. Terminology must be consistent throughout — concepts must not drift +4. Abstract, body, conclusions, and recommendations must be consistent — no self-contradiction +5. Must form a complete loop: "background → objective → method/basis → process/status → findings/results → problems/judgment → recommendations/conclusions" +6. Each major chapter must have a clear core conclusion or topic sentence — no information dump + +### Language Style +1. Formal, objective, restrained, professional +2. No colloquial expressions, chat tone, hyperbole, emotional language, or propaganda style +3. For management/decision-maker audience: conclusion-first, highlight key points, actionable recommendations +4. For technical/testing reports: clear basis, reproducible process, verifiable results, stated risks + +### Data Expression +1. Never use vague expressions as main conclusions: "significantly improved", "obviously optimized", "performed well", "has certain issues" +2. If data exists, express quantitatively (e.g., "average response time under 200 ms" not "fast response") +3. First occurrence of a term: write full name with abbreviation, e.g., "Application Programming Interface (API)" +4. Without real data backing, never fabricate precise figures +5. Statements about facts, data, status, and results must be internally consistent + +### Truthfulness & Conservative Generation +1. Never fabricate test results, experiment data, growth rates, customer counts, interview conclusions, sample distributions, or launch decisions +2. Never present speculation as proven fact +3. Never fabricate meeting minutes, regulatory bases, customer feedback, or system logs +4. When information is insufficient, use placeholders — never pretend information is complete +5. Conclusions must be restrained — do not overstate effects, risks, or value +6. Recommendations must be grounded in preceding analysis — no conclusions from thin air + +--- + +## Chapter Content Requirements + +### (1) Cover +1. Formal reports should have a cover page +2. Cover includes: title, subtitle (if any), organization/department, author, date, classification (if requested) +3. Cover must be a separate section +4. Cover does not display page numbers +5. Use `selectCoverRecipe()` for recipe + palette (see design-system.md) +6. Common recipes: general report R1, whitepaper R2, consulting R3, proposal R4 + +### (2) Executive Summary +1. Formal reports **must have** a summary opening — never jump directly into details +2. Summary should briefly state: background, objective, key methodology, key findings, main recommendations +3. Suitable for quick reading by management — generally 200–400 words +4. Must not read like a TOC description or pile of background filler + +### (3) Table of Contents +1. Medium-to-long formal reports should include a TOC +2. TOC must be generated from real heading styles (Heading + TOC field) — never write a fake TOC +3. TOC page is typically a separate page +4. TOC depth: usually 2–3 levels + +### (4) Background & Objectives +1. Must explain why this report exists +2. Must state what problem/scenario/audience the report serves +3. If scope boundaries exist, state what the report does NOT cover +4. Must not be vague/grand background — must relate directly to this report's task + +### (5) Methodology / Scope / Basis +1. Must state what materials, criteria, methods, and time range the report is based on +2. Analysis: data sources, analysis dimensions, criteria definitions +3. Experiment: environment, materials, samples, procedure principles +4. Testing: scope, version, environment, methods, coverage/rounds +5. Research: sample source, sample size, research method, time range +6. Reader must understand how conclusions were derived + +### (6) Core Content / Process / Status / Results +1. Organized by logical or dimensional order — no chaotic piling +2. Each section should lead with its conclusion, then expand with evidence +3. Results must be specific — never just "performed well" or "has certain issues" +4. Data, metrics, phenomena, and comparisons must be clearly stated +5. If charts are needed but cannot be generated, use chart placeholders (see below) + +### (7) Analysis / Discussion / Problem Diagnosis +1. Must not merely repeat earlier results +2. Must explain what results mean, what patterns they reveal, what problems they expose +3. May include: comparative analysis, root cause analysis, mechanism analysis, anomaly explanation, limitations, risk boundaries +4. Analysis must be consistent with preceding data and facts + +### (8) Conclusions / Recommendations / Next Steps +1. Conclusions must respond to report objectives +2. Recommendations must be executable — not just principle slogans +3. Recommendations should state: who executes, what to do, when, expected improvement +4. Testing/review: clear verdict (pass / conditional pass / fail) +5. Retrospective/summary: specific follow-up action items + +### (9) Appendices +1. Supplementary material valuable to the report but not suitable for the main body +2. Includes: raw data excerpts, detailed parameters, supplementary tables, sample screenshots +3. Appendices should be on separate pages with proper headings + +--- + +## Chart Placeholder Convention + +When charts are needed but cannot be directly generated: + +``` +[Chart Placeholder: Bar chart; Topic: Q1-Q4 2025 revenue comparison; X-axis: Quarter; Y-axis: Revenue (10K CNY); Style: clean business] +``` + +**Rules:** +- Specify: chart type, topic, axis meanings, key dimensions, optional palette suggestion +- Placeholder must be a standalone paragraph — never inline +- Never use vague placeholders like "insert chart here" + +**Prefer direct generation:** Charts that can be produced via matplotlib should be generated as embedded PNGs. Placeholders are a fallback only. + +--- + +## Content-to-Word Mapping + +### Heading Levels +1. Strict hierarchy — no level-skipping +2. Headings must be informative — never "Background", "Content", "Other" (use "Project Background & Report Objectives" instead) +3. Do not mix multiple numbering systems +4. Normal paragraphs must not masquerade as headings + +### Paragraphs +1. Do not use consecutive blank lines for visual spacing +2. Each paragraph should be a complete semantic unit — not too long or too fragmented + +### Lists +1. Use lists only when genuinely needed — an entire report must not be bullet points +2. Nesting depth ≤ 3 levels +3. Consistent punctuation within a list (all complete sentences or all fragments) +4. Combine "key points" with "analysis paragraphs" — never just list without explaining + +### Tables +1. Use tables only for structured data (statistics, comparisons, parameter lists) +2. Every table must have a header row — headers must not be blank +3. Avoid heavily merged-cell complex nested tables +4. Tables must have introductory and explanatory text before/after +5. Cell content should be concise — avoid long paragraphs inside cells + +### Emphasis +1. Bold only for key conclusions, critical metrics, first occurrence of key terms +2. Never bold entire paragraphs +3. Avoid italic, strikethrough, and other unstable styles + +--- + +## Palette Selection + +| Report Type | Suggested Palette | +|-------------|-------------------| +| General | Neutral calm (primary: #101820) | +| Consulting | Warm terracotta | +| Tech | Cool dawn mist | +| Environment / Education | Warm sunshine | +| Medical | Cool mint | + +See `references/design-system.md` for full palette definitions. + +--- + +## Document Structure + +1. **Cover** — via `selectCoverRecipe()` (see design-system.md) + - Separate section, page margin typically 0 + - Common: general R1, whitepaper R2, consulting R3, proposal R4 + +2. **Table of Contents** — H1–H3, separate section + +3. **Executive Summary** — 1 page max + +4. **Body** — Chapters per selected template (A–F) + +5. **Conclusions & Recommendations** + +6. **Appendices** — Raw data, detailed tables + +--- + +## Professional Elements + +- **Page numbers**: bottom center, size 18, color "808080" +- **Header**: report title (abbreviated), size 18, color "808080" +- **Figure/table numbering**: sequential (Figure 1 / Table 1) +- **Cover**: no page number, no header/footer +- **TOC**: optional Roman numerals or no page numbers +- **Body**: Arabic numerals, continuous + +--- + +## Scene-Specific Quality Checks + +In addition to universal checks (see `references/common-rules.md`): + +### Format +- [ ] Executive summary ≤ 1 page +- [ ] Figures/tables have captions ("Figure X: description" / "Table X: description") +- [ ] Cover recipe matches report type +- [ ] Data charts use palette accent color + +### Content +- [ ] Has executive summary — not starting directly with details +- [ ] Heading names are specific and meaningful +- [ ] Complete loop: background → basis → content → analysis → conclusions/recommendations +- [ ] No fabricated or exaggerated details +- [ ] Abstract and conclusions are consistent +- [ ] Terminology consistent throughout +- [ ] Data expressions are quantified, not vague +- [ ] Recommendations are actionable with owners and timeline + +### Structure +- [ ] Heading hierarchy has no level-skipping +- [ ] List nesting ≤ 3 levels +- [ ] Tables have headers with intro/explanation text +- [ ] Bold used sparingly for emphasis only diff --git a/skills/docx/scenes/resume.md b/skills/docx/scenes/resume.md new file mode 100755 index 0000000..cac8a83 --- /dev/null +++ b/skills/docx/scenes/resume.md @@ -0,0 +1,534 @@ +# Scene: Resume / CV + +## Goal + +Generate a complete, authentic, well-structured, position-targeted resume with stable Word formatting. Must simultaneously meet: +- Authentic and credible content, clear position targeting +- ATS-friendly, stable Word layout +- Clean structure, professional visual design, easy to scan + +**Execution priority** (when conflicting): Position relevance > Information readability > ATS compatibility > Visual decoration + +**Forbidden:** Producing advice-only / fragments / half-finished drafts; outputting chat-style explanations. + +→ Font profile: **B (Visual)** — see `references/common-rules.md` +→ Placeholder convention & universal prohibitions — see `references/common-rules.md` + +--- + +## Scope + +Default: generate a position-oriented general resume. Switch to English resume, academic CV, international format, or design portfolio style only when explicitly requested by the user. + +--- + +## Resume Type Routing + +Auto-select module order based on user background and target: + +### General Resume (default) +Name & Contact → Target Position → Profile Summary (optional) → Core Skills → Work Experience → Projects → Education → Certifications / Awards + +### New Graduate Resume +Name & Contact → Target Position → Education → Internship Experience → Projects → Campus Activities / Competitions / Awards → Skills & Certifications + +### Technical Role Resume +Name & Contact → Target Direction → Profile Summary (optional) → Tech Stack / Core Skills → Work Experience → Projects → Education → Open Source / Papers / Patents / Competitions + +### Academic CV +Name & Contact → Research Direction / Target → Education → Research Experience → Papers / Patents / Projects / Grants → Teaching / Academic Service → Awards / Skills / Languages + +--- + +## Input Processing Rules + +1. If user provides a target position or JD → **must reorganize and rewrite content around position requirements** +2. If user provides a raw draft → prioritize restructuring, phrasing refinement, and priority reordering; do not rewrite into an unfamiliar career +3. **Never fabricate** companies, positions, degrees, projects, certifications, awards, papers, patents, data results, or achievements +4. If critical data is missing → use conservative expressions or placeholder `【Please fill in: ______】`; never fabricate precise numbers +5. A single resume should generally serve only one primary career direction + +--- + +## Content Quality Constraints + +### Core Principles +1. Resume must revolve around the target position — do not spread all experiences equally +2. Most relevant experiences, projects, and skills must be **placed first and detailed** +3. Terminology, company names, position titles, date formats, and skill names must be consistent +4. Must demonstrate: **personal positioning → capability tags → relevant experience → provable results** +5. No piling of vague self-praise; no inspirational writing or chronological dumps + +### Experience Writing Standards + +Each experience bullet should demonstrate: **Action + Object/Context + Method + Result/Impact** + +**Recommended verbs:** Led, built, drove, optimized, refactored, designed, delivered, coordinated, improved, reduced, achieved + +**Rules:** +- "Responsible for" / "participated in" are not absolutely forbidden, but must include scope and results +- Each bullet is concise — one core contribution per bullet +- Quantify when possible, but do not force-bold all numbers +- Recent experience gets detail; low-relevance/low-value experience gets compressed or removed +- Reverse chronological order — most recent and relevant first +- Expand the most recent 2 experiences; compress earlier ones + +### Profile Summary / Self-Assessment +1. Not mandatory +2. If included, frame as "Profile Summary" — **3–4 lines max** +3. Focus on: years of experience, career direction, core capabilities, representative achievements, position fit +4. **Forbidden** as main content: "hardworking", "strong sense of responsibility", "team player", "quick learner", "outgoing personality" + +### Truthfulness & Risk Control +1. Never fabricate experiences, achievements, education, awards, or certifications +2. Never upgrade "participated in" to "led" unless user information supports it +3. Never attribute team results entirely to the individual +4. Never fabricate revenue, conversion rates, headcount, budgets, or technical metrics +5. If no data available, use restrained expressions: "improved delivery efficiency", "shortened processing cycle", "supported core business launch" + +--- + +## Length Control + +| Candidate Type | Target Pages | +|---------------|-------------| +| New graduate / <3 years experience | **1 page** | +| 3–10 years experience | 1–2 pages | +| Senior manager / researcher / academic CV | May exceed 2 pages, but must maintain information density | + +**Compression rules:** +- Experiences >5 years old with low relevance should be compressed +- Experiences >10 years old and irrelevant may be omitted +- Never pad low-value experiences just to "look comprehensive" + +--- + +## ATS & Structure Constraints + +1. Core information must be plain text — never rely on images, icons, text boxes, or headers/footers for key content +2. No embedded charts, objects, SmartArt, or WordArt +3. Experience descriptions use consistent bullet symbols — no complex auto-numbering +4. Bullets within the same position should be compact — no excess blank lines + +**Table layout vs. ATS balance:** The 3 visual templates (A/B/C) use Table-based layouts for Word visual quality. In strict ATS scenarios (user explicitly says "ATS priority"), prefer Template B (single-column) with reduced table dependency. Default: visual quality first. + +--- + +## Module Naming + +Use only standard, universal, recruiter-familiar names: +- Personal Info, Target Position, Profile Summary, Core Skills, Work Experience, Projects, Education, Certifications, Awards, Languages + +**Forbidden fancy names:** "My Growth Journey", "Self-Appreciation", "Shining Moments", "Life Motto" + +--- + +## Template Disease Prevention + +1. Do not include irrelevant identity tags (political affiliation, hometown, etc.) unless user explicitly requests +2. Do not place low-priority modules (hobbies, languages, personality traits) before work experience +3. Do not combine cover letter and resume in one document (unless user explicitly requests) +4. Do not let template feel overpower actual personal information +5. Do not let "self-assessment" occupy the golden area of the page (should come after core skills/experience) + +--- + +## Template Selection + +Three templates are provided, auto-selected based on user needs: + +| Template | Layout | Best For | Color Style | +|----------|--------|----------|-------------| +| A | Left sidebar + right body | General purpose, tech roles | Dark grey sidebar + blue bar headings | +| B | Dark header banner + single column | Content-heavy / senior candidates | Dark blue header + underline headings | +| C | Left sidebar + vertical-line headings | International / bilingual / foreign companies | Blue sidebar + left-border headings | + +**Selection logic:** +- Default: Template A +- Lots of content (expected > 1 page) → Template B (no sidebar, better space utilization) +- User explicitly requests bilingual / English → Template C + +### Industry Color Suggestions + +| Career Direction | Sidebar BG | Accent Color | Recommended Template | +|-----------------|-----------|-------------|---------------------| +| Tech / Internet | `#1A1F36` (deep blue-purple) | `#667eea` (amethyst) | A or C | +| Finance / Consulting | `#0F2027` (deep sea blue) | `#D4AF37` (gold) | A or B | +| Design / Creative | `#2D1B30` (deep purple) | `#f5576c` (coral pink) | A or C | +| Education / Training | `#1A3A3A` (dark green) | `#3CB4A0` (mint green) | A | +| Medical / Health | `#0E2030` (dark cyan) | `#3888A8` (medical blue) | B | +| General / Default | `#303030` (warm dark neutral) | `#B89870` (warm accent) | A | + +When industry is unspecified, use default warm neutral palette. This aligns with the Visual Profile warm-neutral guidance in `design-system.md`. + +## Key Rules + +- **NO cover page / NO TOC** +- **Target: 1 page** (2 pages max for senior roles) +- **Compact spacing**: `line: 276` (1.15x) +- All templates use **bilingual section headings** (e.g., "Work Experience 工作经历") + +--- + +## Template A: Left Sidebar + Color Bar Headings + +### Color Palette +```js +const S = { + bg: "3B4F5C", // sidebar background (dark grey-blue) + text: "D8E2E8", // sidebar text + label: "8BA0AD", // sidebar secondary text + accent: "2F97B8", // accent color (blue-cyan) + title: "1A2D38", // body heading + body: "2C3E4A", // body content + sec: "6B8592", // secondary info (dates etc.) +}; +``` + +### Layout Structure +``` +┌──────────┬──────────────────────┐ +│ [Photo] │ ██ Profile ██ │ ← Blue bar heading +│ │ Summary text... │ +│ Name │ │ +│ Title │ ██ Work Experience ██│ +│ │ Company Role Date │ +│ ──────── │ ▸ Achievement... │ +│ Basic │ ▸ Achievement... │ +│ Info │ │ +│ │ ██ Projects ██ │ +│ ──────── │ ... │ +│ Contact │ │ +│ │ ██ Education ██ │ +│ ──────── │ ... │ +│ Skills │ │ +│ Java ●●●●○│ │ +│ Go ●●●○○│ │ +│ │ │ +│ ──────── │ │ +│ Certs │ │ +└──────────┴──────────────────────┘ + 30% 70% +``` + +### Implementation Notes + +**Page setup:** +```js +page: { margin: { top: 0, bottom: 0, left: 0, right: 0 } } +// Use Table to simulate columns: columnWidths: [3400, 8506] +// ⚠️ Row height must use "exact" with safety margin to prevent overflow blank pages +// Row height: height: { value: 16038, rule: "exact" } +// 16038 = 16838(A4 height) - 1200(safety margin for cross-engine compatibility) +``` + +**Sidebar element order:** +1. Photo placeholder (rectangle + border, width 2400 DXA, height 1800) +2. Name (32pt bold white SimHei) + Title (18pt accent) +3. Basic info (DOB / degree / school) +4. Contact info (phone / email / address) +5. Skill ratings (name + ●○ dot rating, 5 levels each) +6. Certificates list + +**Right-side section headings (color bar style):** +```js +// Full-width bar background + white Chinese text + lighter English text +new Table({ columnWidths:[7600], rows:[new TableRow({ children:[ + new TableCell({ + shading: { fill: S.accent, type: ShadingType.CLEAR }, + margins: { top:40, bottom:40, left:200, right:100 }, + children: [new Paragraph({ children: [ + new TextRun({ text: "Work Experience ", size:22, bold:true, color:"FFFFFF", font:"SimHei" }), + new TextRun({ text: "Experience", size:18, color:"C8E8F0", font:"Times New Roman", italics:true }), + ] })], + }) +] })] }); +``` + +**Experience entry format:** +```js +// Line 1: Company(bold) + Title(accent) + Date(right-aligned) +new Paragraph({ + tabStops: [{ type: TabStopType.RIGHT, position: 7200 }], + children: [ + new TextRun({ text: "Company Name", size:22, bold:true, color:S.title }), + new TextRun({ text: " Role Title", size:20, color:S.accent }), + new TextRun({ text: "\t2023.06 — Present", size:17, color:S.sec }), + ] +}); +// Line 2+: ▸ bullet points +``` + +--- + +## Template B: Dark Header Banner + Single Column + +### Color Palette +```js +const C = { + dark: "1A3352", // header background (dark blue) + accent: "2980B9", // accent color + title: "1A2636", // heading + body: "2C3E50", // body text + sec: "6B8599", // secondary info + light: "E8EFF5", // light background +}; +``` + +### Layout Structure +``` +┌────────────────────────────────┐ +│ ██████████████████████████████ │ ← Dark blue background banner +│ █ Name Title █ │ Contains name / title / +│ █ Phone | Email | Location █ │ contact / basic info +│ █ DOB | Degree | School █ │ +│ ██████████████████████████████ │ +│ │ +│ Profile │ ← Underline heading +│ ───────────────────────────── │ +│ Summary text... │ +│ │ +│ Work Experience │ +│ ───────────────────────────── │ +│ Company | Role Date │ +│ • Achievement... │ +│ ... │ +│ │ +│ Skills │ +│ ───────────────────────────── │ +│ Programming ●●●●○ Java/Go/...│ ← Rating + details +└────────────────────────────────┘ +``` + +### Implementation Notes + +**Header banner:** +```js +// Table single row single column, dark background, height 2400 DXA +new Table({ columnWidths:[11906], rows:[new TableRow({ + height: { value:2400, rule:"exact" }, + children:[new TableCell({ + shading: { fill: C.dark }, + margins: { top:300, bottom:200, left:800, right:800 }, + verticalAlign: VerticalAlign.TOP, // Never use CENTER in exact-height rows (WPS incompatible) + children: [ + // Line 1: Name(48pt white) + Title + // Line 2: Phone | Email | Location + // Line 3: DOB | Degree | School + ] + })] +})] }); +``` + +**Section headings (underline style):** +```js +new Paragraph({ + borders: { bottom: { style: BorderStyle.SINGLE, size: 2, color: C.accent } }, + children: [ + new TextRun({ text: "Work Experience", size:24, bold:true, color:C.accent, font:"SimHei" }), + new TextRun({ text: " Experience", size:18, color:C.sec, italics:true }), + ] +}); +``` + +**Skills display (rating + details):** +```js +// Name(bold) + ●○ rating + specific tools list +new Paragraph({ children: [ + new TextRun({ text: "Programming ", size:19, bold:true, color:C.title }), + new TextRun({ text: "●●●●○ ", size:13, color:C.accent }), + new TextRun({ text: "Java / Go / Python / TypeScript", size:18, color:C.sec }), +] }); +``` + +--- + +## Template C: Blue Sidebar + Vertical-Line Headings + +### Color Palette +```js +const C = { + side: "4A7C8F", // sidebar background (teal-blue) + text: "FFFFFF", // sidebar text + label: "A0C4D0", // sidebar secondary text + accent: "357A8F", // accent color + dot: "2F8FAD", // skill dot fill color + dotDim: "B8D4DE", // skill dot empty color + title: "1A3040", // body heading + body: "2C4050", // body content + sec: "6B8A98", // secondary info +}; +``` + +### Sidebar-Specific Elements + +**Circular photo placeholder:** +```js +new Paragraph({ alignment: AlignmentType.CENTER, + children: [new TextRun({ text: "◯", size:80, color:C.label })] +}); +``` + +**Language proficiency matrix:** +```js +"English ● ● ● ● ○" +"Japanese ● ● ○ ○ ○" +``` + +**Right-side section headings (left-border style):** +```js +new Paragraph({ + borders: { left: { style: BorderStyle.SINGLE, size:8, color:C.accent, space:8 } }, + indent: { left: 120 }, + children: [ + new TextRun({ text: "Work Experience", size:24, bold:true, color:C.title, font:"SimHei" }), + new TextRun({ text: " Experience", size:18, color:C.sec, italics:true }), + ] +}); +``` + +**Experience entry format (differs from A):** +```js +// Line 1: Company name (bold) +// Line 2: Role (accent color) + Date +// Line 3+: ▸ bullet points +``` + +--- + +## Universal Rules + +### Font Specifications +| Element | Font | Size | Style | +|---------|------|------|-------| +| Name (sidebar) | SimHei | 32pt (size:64) | Bold, white | +| Name (header) | SimHei | 24pt (size:48) | Bold, white | +| Section heading | SimHei | 11pt (size:22) | Bold | +| Company / School | Microsoft YaHei | 11pt (size:22) | Bold | +| Role title | Microsoft YaHei | 10pt (size:20) | accent color | +| Date range | Microsoft YaHei | 8.5pt (size:17) | sec color | +| Bullet description | Microsoft YaHei | 9.5pt (size:19) | body color | +| Skill dots | Default | 6.5pt (size:13) | accent / dimColor | + +### Bullet Symbols +- Template A / C: `▸` (small triangle) +- Template B: `•` (round dot) + +### Skill Rating Rules +- 1–5 levels using filled ● and empty ○ dots +- One skill per line, name on the left, dots on the right +- Filled dot color: accent; empty dot color: dimColor + +### JD Matching Logic +When user provides a job description: +1. Extract key requirements (skills, experience, education) +2. Prioritize matching experience items to the top +3. Naturally incorporate JD keywords into descriptions +4. Highlight relevant skills + +### Multi-Page Handling + +- 1 page content: Sidebar templates (A/C) or single-column template (B) +- Over 1 page: Prefer Template B; if using A/C, switch page 2 to full-width layout with a name bar at the top (Name | Title) + +⚠️ **Multi-page resumes must use multi-section structure:** + +Page 1 and Page 2 must be **different sections** for independent margin and layout control: + +```js +sections: [ + { + // Page 1 section — margin 0 (sidebar layout needs full-page) + properties: { page: { margin: { top: 0, bottom: 0, left: 0, right: 0 } } }, + children: [page1Table], + }, + { + // Page 2 section — normal margins with header bar + properties: { page: { margin: { top: 800, bottom: 600, left: 800, right: 800 } } }, + children: [pageHeader(name, title), ...page2Content], + }, +] +``` + +⚠️ **Template B multi-page handling:** + +Template B header banner uses Table simulation: +1. Banner `columnWidths` must equal **page content area width** (pageWidth - marginLeft - marginRight), not full page width +2. If banner needs full page width → set page 1 section margin to 0, banner columnWidths to 11906 +3. Page 2+ must be independent sections, margin.top ≥ 800 + +⚠️ **Page 2+ top spacing rules (mandatory):** + +1. **Page margin.top must be ≥ 800 twips** (~1.4 cm), never 0 +2. **Page 2+ needs a header info bar:** concise `Name | Title` bar, height ~400–600 twips, separated from body with light background or bottom line +3. **200–300 twips spacing between header bar and body content** +4. **Forbidden: content touching the very top of page 2** + +```js +// Concise header bar for page 2+ +function pageHeader(name, title) { + return new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: { top: NB, left: NB, right: NB, insideHorizontal: NB, insideVertical: NB, + bottom: { style: BorderStyle.SINGLE, size: 1, color: "D0D0D0" } }, + rows: [new TableRow({ + cantSplit: true, + height: { value: 500, rule: "atLeast" }, + children: [new TableCell({ + margins: { top: 60, bottom: 60, left: 200, right: 200 }, + borders: { top: NB, left: NB, right: NB, bottom: NB }, + children: [new Paragraph({ + children: [ + new TextRun({ text: name, size: 20, bold: true, color: S.title || C.title }), + new TextRun({ text: ` | ${title}`, size: 18, color: S.sec || C.sec }), + ] + })], + })], + })], + }); +} +``` + +--- + +## Scene-Specific Quality Checks + +In addition to universal checks (see `references/common-rules.md`): + +### Format +- [ ] Fits within 1 page (senior ≤ 2 pages) +- [ ] **Single-page fill rate ≥ 85%** (bottom whitespace ≤ 15%, ~2500 twips) +- [ ] Section headings are bilingual +- [ ] Skill rating dots correct (●○) +- [ ] Experience in reverse chronological order +- [ ] No cover page, no TOC +- [ ] Line spacing 1.15x (line: 276) +- [ ] No extra blank pages +- [ ] **Table row height uses `rule: "exact"` with value ≤ 16038** (prevent overflow blank pages) +- [ ] **Multi-page: page 2+ has header info bar + proper top spacing** + +### Content +- [ ] Clearly organized around target position +- [ ] No vague self-assessments ("hardworking", "responsible", "team player") +- [ ] No fabricated data or exaggerated results +- [ ] Most relevant experience placed first and detailed +- [ ] Each bullet demonstrates action + object + method + result +- [ ] No long narrative blocks / excessive long sentences / information density imbalance +- [ ] Module names are standardized +- [ ] Contact info is plain text, clearly positioned +- [ ] Header area forms visual center +- [ ] Work experience and projects are the visual main body +- [ ] Page count matches candidate seniority + +### Single-Page Fill Rules + +Single-page resumes must fully utilize page space — **large bottom whitespace is forbidden:** + +1. If content is insufficient → **proactively expand:** + - Add project details, skill keywords, achievement data + - Add supplementary modules: profile summary, interests, awards +2. Use section spacing (`spacing.before/after`) to **distribute content evenly** +3. Sidebar templates (A/C): sidebar height should approach full page + - If sidebar content is sparse, increase element spacing + - Or add supplementary modules: "Languages", "Interests" +4. Assessment: after generation, check last content element position; if >2500 twips from page bottom, adjust diff --git a/skills/docx/scripts/__init__.py b/skills/docx/scripts/__init__.py new file mode 100755 index 0000000..bf9c562 --- /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/add_toc_placeholders.py b/skills/docx/scripts/add_toc_placeholders.py new file mode 100755 index 0000000..d7864ac --- /dev/null +++ b/skills/docx/scripts/add_toc_placeholders.py @@ -0,0 +1,749 @@ +#!/usr/bin/env python3 +""" +Add placeholder entries to Table of Contents in a DOCX file. + +This script adds placeholder TOC entries between the 'separate' and 'end' +field characters, so users see some content on first open instead of an empty TOC. +The original file is replaced with the modified version. + +Usage: + python add_toc_placeholders.py # auto-extract headings (default) + python add_toc_placeholders.py --auto # explicit auto mode + python add_toc_placeholders.py --entries + + entries_json format: JSON string with array of objects: + [ + {"level": 1, "text": "Chapter 1 Overview", "page": "1"}, + {"level": 2, "text": "Section 1.1 Details", "page": "1"} + ] + + Default behavior (no flags): auto-extracts Heading 1-3 from the document. + Filters out table/figure captions (e.g. "表 1:xxx", "图 2:xxx"). + +Example: + python add_toc_placeholders.py document.docx + python add_toc_placeholders.py document.docx --auto + python add_toc_placeholders.py document.docx --entries '[{"level":1,"text":"Introduction","page":"1"}]' +""" + +import argparse +import html +import json +import re +import shutil +import sys +import tempfile +import zipfile +from pathlib import Path + + +def _extract_headings_from_docx(docx_path: str, max_level: int = 3) -> list: + """Extract headings from a DOCX file for auto-mode TOC generation. + + Args: + docx_path: Path to DOCX file + max_level: Maximum heading level to include (default 3) + + Returns: + List of dicts with 'level', 'text', 'page' keys + """ + from docx import Document + + doc = Document(docx_path) + entries = [] + page_estimate = 1 + + # Pattern to filter out table/figure captions styled as headings + caption_pattern = re.compile(r'^[表图]\s*\d') + + for i, para in enumerate(doc.paragraphs): + style_name = para.style.name if para.style else '' + if not style_name.startswith('Heading'): + continue + m = re.search(r'(\d+)', style_name) + if not m: + continue + level = int(m.group(1)) + if level > max_level: + continue + text = para.text.strip() + if not text: + continue + # Filter table/figure captions + if caption_pattern.match(text): + continue + + # Rough page estimate: increment every ~8 headings + page_estimate = max(1, 1 + i // 8) + entries.append({"level": level, "text": text, "page": str(page_estimate)}) + + return entries + + +def add_toc_placeholders(docx_path: str, entries: list = None) -> None: + """Add placeholder TOC entries to a DOCX file (in-place replacement). + + Args: + docx_path: Path to DOCX file (will be modified in-place) + entries: Optional list of placeholder entries. Each entry should be a dict + with 'level' (1-3), 'text', and 'page' keys. + """ + docx_path = Path(docx_path) + + # Create temp directory for extraction + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + extracted_dir = temp_path / "extracted" + temp_output = temp_path / "output.docx" + + # Extract DOCX + with zipfile.ZipFile(docx_path, 'r') as zip_ref: + zip_ref.extractall(extracted_dir) + + # Ensure TOC styles exist in styles.xml + styles_xml_path = extracted_dir / "word" / "styles.xml" + toc_style_mapping = _ensure_toc_styles(styles_xml_path) + print(f"TOC style mapping: {toc_style_mapping}") + + # Fix settings.xml: ensure updateFields has val="true" + settings_xml_path = extracted_dir / "word" / "settings.xml" + _fix_update_fields(settings_xml_path) + + # Fix Heading styles: ensure outlineLvl is set (required for TOC field update) + _fix_heading_outline_levels(styles_xml_path) + + # Process document.xml + document_xml = extracted_dir / "word" / "document.xml" + if not document_xml.exists(): + raise ValueError("document.xml not found in the DOCX file") + + # Read and process XML + content = document_xml.read_text(encoding='utf-8') + + # Fix fldChar structure: split merged begin+instrText+separate into separate elements + content = _fix_fld_char_structure(content) + + # Find TOC structure and add placeholders (uses lxml for robust XML parsing) + modified_content = _insert_toc_placeholders(content, entries, toc_style_mapping) + + # Write back + document_xml.write_text(modified_content, encoding='utf-8') + + # Repack DOCX to temp file + with zipfile.ZipFile(temp_output, 'w', zipfile.ZIP_DEFLATED) as zipf: + for file_path in extracted_dir.rglob('*'): + if file_path.is_file(): + arcname = file_path.relative_to(extracted_dir) + zipf.write(file_path, arcname) + + # Replace original file with modified version (use shutil.move for cross-device support) + docx_path.unlink() + shutil.move(str(temp_output), str(docx_path)) + + +def _fix_update_fields(settings_xml_path: Path) -> None: + """Fix settings.xml to ensure is present. + + The docx npm library generates without val="true", + which Word/WPS interprets as false, preventing TOC auto-update on open. + """ + if not settings_xml_path.exists(): + return + + content = settings_xml_path.read_text(encoding='utf-8') + original = content + + # Case 1: (self-closing, no val) → add val="true" + if '' in content: + content = content.replace('', '') + print('Fixed: ') + + # Case 2: → change to true (match precisely) + elif re.search(r'', content): + content = re.sub( + r'', + '', + content + ) + print('Fixed: ') + + # Case 3: Not present at all → inject before + elif '', '') + print('Fixed: added to settings.xml') + + if content != original: + settings_xml_path.write_text(content, encoding='utf-8') + + +def _fix_heading_outline_levels(styles_xml_path: Path) -> None: + """Fix Heading styles to include outlineLvl in pPr. + + The docx npm library creates Heading styles but sometimes doesn't set outlineLvl + in the style definition. Without outlineLvl, Word's TOC field update won't find + headings even though they display correctly. + + This ensures Heading1 has outlineLvl=0, Heading2 has outlineLvl=1, etc. + """ + if not styles_xml_path.exists(): + return + + content = styles_xml_path.read_text(encoding='utf-8') + original = content + + W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' + + for level in range(1, 7): + style_id = f'Heading{level}' + outline_val = str(level - 1) + + # Pattern: find with w:styleId="HeadingN" + style_pattern = ( + rf'(]*w:styleId="{style_id}"[^>]*>)' + rf'(.*?)' + rf'()' + ) + + match = re.search(style_pattern, content, flags=re.DOTALL) + if not match: + continue + + style_content = match.group(2) + + # Check if outlineLvl already exists in this style + if f' within this style + ppr_match = re.search(r'(]*>)(.*?)()', style_content, flags=re.DOTALL) + if ppr_match: + # Add outlineLvl inside existing pPr + new_ppr_content = ppr_match.group(2) + f'' + new_style_content = ( + style_content[:ppr_match.start()] + + ppr_match.group(1) + new_ppr_content + ppr_match.group(3) + + style_content[ppr_match.end():] + ) + else: + # No pPr exists, create one + new_ppr = f'' + # Insert pPr right after style opening (after name/basedOn if present) + new_style_content = new_ppr + style_content + + new_style = match.group(1) + new_style_content + match.group(3) + content = content[:match.start()] + new_style + content[match.end():] + print(f'Fixed: added outlineLvl={outline_val} to {style_id} style') + + if content != original: + styles_xml_path.write_text(content, encoding='utf-8') + + +def _fix_fld_char_structure(xml_content: str) -> str: + """Fix malformed fldChar structure where begin+instrText+separate are in one . + + The docx npm library generates: + TOC... + + Word/WPS requires the standard structure: + + TOC... + + """ + # Match a that contains both begin fldChar AND instrText AND separate fldChar + pattern = ( + r']*)?>(' + r']*w:fldCharType="begin"[^>]*/>' # begin + r')(' + r']*>.*?' # instrText + r')(' + r']*w:fldCharType="separate"[^>]*/>' # separate + r')' + ) + + def split_run(match): + begin = match.group(1) + instr = match.group(2) + separate = match.group(3) + return f'{begin}{instr}{separate}' + + modified = re.sub(pattern, split_run, xml_content, flags=re.DOTALL) + if modified != xml_content: + print("Fixed: split merged fldChar begin+instrText+separate into separate elements") + + # Fix TOC instrText: remove \t switch with wrong style names + # docx npm lib generates \t "Heading1,1,Heading2,2,..." but Word expects "Heading 1,1,..." + # Since we already have \o "1-3" which uses outlineLvl (now fixed), \t is redundant and harmful + toc_t_pattern = r'(TOC\s+[^<]*?)\\t\s+"[^&]*"' + modified2 = re.sub(toc_t_pattern, r'\1', modified) + if modified2 != modified: + print("Fixed: removed \\t switch from TOC instrText (\\o with outlineLvl is sufficient)") + modified = modified2 + + return modified + + +def _detect_toc_styles(styles_xml_path: Path) -> dict: + """Detect TOC style IDs from styles.xml. + + Args: + styles_xml_path: Path to styles.xml + + Returns: + Dictionary mapping level (1-3) to style ID string + """ + if not styles_xml_path.exists(): + return {} + + content = styles_xml_path.read_text(encoding='utf-8') + result = {} + + for level in range(1, 4): + # Standard TOC style names: "TOC 1", "TOC 2", "TOC 3" (with space) + # or "TOC1", "TOC2", "TOC3" (no space) — docx-js uses numeric IDs like "9", "11", "12" + patterns = [ + rf'w:styleId="(TOC{level})"', + rf'w:styleId="(TOC {level})"', + rf'\s*|', + ] + for pattern in patterns[:2]: + m = re.search(pattern, content) + if m: + result[level] = m.group(1) + break + else: + # Try matching by w:name (case insensitive toc N) + # Find blocks with name containing "toc N" + name_pattern = rf']*w:styleId="([^"]*)"[^>]*>.*? dict: + """Ensure TOC styles exist in styles.xml, adding them if necessary. + + Returns: + Dictionary mapping level (1-3) to style ID string + """ + if not styles_xml_path.exists(): + return {1: "9", 2: "11", 3: "12"} + + content = styles_xml_path.read_text(encoding='utf-8') + detected = _detect_toc_styles(styles_xml_path) + result = dict(detected) + + W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' + + # Define TOC styles to add if missing + toc_style_defs = { + 1: { + 'id': '9', + 'name': 'toc 1', + 'xml': f''' + + + + + + + + +''' + }, + 2: { + 'id': '11', + 'name': 'toc 2', + 'xml': f''' + + + + + + + + +''' + }, + 3: { + 'id': '12', + 'name': 'toc 3', + 'xml': f''' + + + + + + + + +''' + }, + } + + modified = False + for level in range(1, 4): + if level not in result: + style_def = toc_style_defs[level] + result[level] = style_def['id'] + # Add style before + insert_point = content.rfind('') + if insert_point == -1: + print(f"WARNING: Could not find to insert TOC {level} style", file=sys.stderr) + continue + content = content[:insert_point] + style_def['xml'] + '\n' + content[insert_point:] + print(f"Added TOC {level} style (ID: {style_def['id']})") + modified = True + + if modified: + styles_xml_path.write_text(content, encoding='utf-8') + + # Ensure Hyperlink style exists + _ensure_hyperlink_style(styles_xml_path) + + return result + + +def _ensure_hyperlink_style(styles_xml_path: Path) -> None: + """Ensure Hyperlink character style exists in styles.xml.""" + if not styles_xml_path.exists(): + return + + content = styles_xml_path.read_text(encoding='utf-8') + if 'w:styleId="Hyperlink"' in content: + return + + W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' + hyperlink_style = f''' + + + + + + +''' + + insert_point = content.rfind('') + if insert_point != -1: + content = content[:insert_point] + hyperlink_style + '\n' + content[insert_point:] + styles_xml_path.write_text(content, encoding='utf-8') + print("Added Hyperlink character style") + + +def _insert_toc_placeholders(xml_content: str, entries: list = None, toc_style_mapping: dict = None) -> str: + """Insert placeholder TOC entries and heading bookmarks into XML content. + + Uses lxml ElementTree for robust XML manipulation instead of fragile regex. + + This function does TWO things: + 1. Adds bookmark anchors to each Heading paragraph (so Word can link TOC → heading) + 2. Replaces TOC placeholder area with proper entries containing HYPERLINK + PAGEREF + + Args: + xml_content: The XML content of document.xml + entries: List of placeholder entries with 'level', 'text', 'page' keys + toc_style_mapping: Dictionary mapping level to style ID + + Returns: + Modified XML content with bookmarks and TOC placeholders + + Raises: + RuntimeError: If TOC structure cannot be found or is malformed + """ + from lxml import etree + + if entries is None: + entries = [{"level": 1, "text": "Contents", "page": "1"}] + + if toc_style_mapping is None: + toc_style_mapping = {1: "9", 2: "11", 3: "12"} + + W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + + # Parse XML + root = etree.fromstring(xml_content.encode('utf-8')) + nsmap = {'w': W, 'r': R_NS} + + # ── Step 1: Add bookmarks to Heading paragraphs ── + bookmark_id_counter = 100000 + heading_bookmark_map = {} # text → first bookmark_name (backward compat) + heading_bookmark_map_all = {} # text → [list of bookmark_names] for duplicate headings + + for para in root.iter(f'{{{W}}}p'): + # Find pStyle + ppr = para.find(f'{{{W}}}pPr') + if ppr is None: + continue + pstyle = ppr.find(f'{{{W}}}pStyle') + if pstyle is None: + continue + style_val = pstyle.get(f'{{{W}}}val', '') + if not re.match(r'Heading\d$', style_val): + continue + + # Extract heading text + texts = [] + for t_elem in para.iter(f'{{{W}}}t'): + if t_elem.text: + texts.append(t_elem.text) + heading_text = ''.join(texts).strip() + if not heading_text: + continue + + # Skip if already has bookmark + if para.find(f'{{{W}}}bookmarkStart') is not None: + continue + + # Generate bookmark + bm_name = f"_Toc{bookmark_id_counter}" + bm_id_str = str(bookmark_id_counter) + bookmark_id_counter += 1 + + # Store mapping (support duplicate headings) + if heading_text not in heading_bookmark_map_all: + heading_bookmark_map_all[heading_text] = [] + heading_bookmark_map_all[heading_text].append(bm_name) + if heading_text not in heading_bookmark_map: + heading_bookmark_map[heading_text] = bm_name + + # Insert bookmarkStart after pPr + bm_start = etree.Element(f'{{{W}}}bookmarkStart') + bm_start.set(f'{{{W}}}id', bm_id_str) + bm_start.set(f'{{{W}}}name', bm_name) + + bm_end = etree.Element(f'{{{W}}}bookmarkEnd') + bm_end.set(f'{{{W}}}id', bm_id_str) + + ppr_index = list(para).index(ppr) + para.insert(ppr_index + 1, bm_start) + # bookmarkEnd at end of paragraph + para.append(bm_end) + + bookmarks_added = len(heading_bookmark_map) + if bookmarks_added > 0: + print(f"Added {bookmarks_added} bookmarks to Heading paragraphs") + + # ── Step 2: Find TOC field structure (begin → instrText → separate → end) ── + toc_separate_para = None + toc_end_para = None + + # Track field nesting to handle nested fields correctly + field_stack = [] + toc_field_depth = None + + for fld_char in root.iter(f'{{{W}}}fldChar'): + fld_type = fld_char.get(f'{{{W}}}fldCharType') + run = fld_char.getparent() + + if fld_type == 'begin': + para = run.getparent() + instr_text = '' + found_run = False + for sibling in para: + if sibling is run: + found_run = True + it = sibling.find(f'{{{W}}}instrText') + if it is not None and it.text: + instr_text += it.text + continue + if found_run and sibling.tag == f'{{{W}}}r': + it = sibling.find(f'{{{W}}}instrText') + if it is not None and it.text: + instr_text += it.text + if sibling.find(f'{{{W}}}fldChar') is not None: + break + + field_stack.append(instr_text.strip()) + if 'TOC' in instr_text and toc_field_depth is None: + toc_field_depth = len(field_stack) + + elif fld_type == 'separate': + if toc_field_depth is not None and len(field_stack) == toc_field_depth: + toc_separate_para = run.getparent() + + elif fld_type == 'end': + if toc_field_depth is not None and len(field_stack) == toc_field_depth: + toc_end_para = run.getparent() + break + if field_stack: + field_stack.pop() + + if toc_separate_para is None or toc_end_para is None: + has_begin = root.find(f'.//{{{W}}}fldChar[@{{{W}}}fldCharType="begin"]') is not None + has_separate = root.find(f'.//{{{W}}}fldChar[@{{{W}}}fldCharType="separate"]') is not None + if not has_begin: + raise RuntimeError( + "TOC FAILED: No field structure found in document. " + "Ensure the code includes a TableOfContents element." + ) + elif not has_separate: + raise RuntimeError( + "TOC FAILED: TOC field has 'begin' but no 'separate' fldChar. " + "Run _fix_fld_char_structure() first or check the docx-js version." + ) + else: + raise RuntimeError( + "TOC FAILED: Field structure found but no TOC instrText detected. " + "Ensure TableOfContents element generates a TOC \\o field code." + ) + + # ── Step 3: Remove everything between separate-para and end-para ── + # The TOC paragraphs may be direct children of or wrapped in + toc_container = toc_separate_para.getparent() # could be body or sdtContent + container_children = list(toc_container) + + sep_idx = container_children.index(toc_separate_para) + end_idx = container_children.index(toc_end_para) + + for elem in container_children[sep_idx + 1:end_idx]: + toc_container.remove(elem) + + # ── Step 4: Build and insert placeholder paragraphs ── + indent_mapping = {1: 0, 2: 360, 3: 720, 4: 1080, 5: 1440, 6: 1800} + heading_occurrence_counter = {} + + insert_pos = list(toc_container).index(toc_end_para) + + for entry in entries: + level = entry.get('level', 1) + text_raw = entry.get('text', '') + page = entry.get('page', '1') + + toc_style = toc_style_mapping.get(level, toc_style_mapping.get(1, "9")) + indent = indent_mapping.get(level, 0) + + # Resolve bookmark (handle duplicate headings correctly) + bm_name = '' + if text_raw in heading_bookmark_map_all: + occ = heading_occurrence_counter.get(text_raw, 0) + bm_list = heading_bookmark_map_all[text_raw] + if occ < len(bm_list): + bm_name = bm_list[occ] + heading_occurrence_counter[text_raw] = occ + 1 + + # Build paragraph element + p = etree.Element(f'{{{W}}}p') + toc_container.insert(insert_pos, p) + insert_pos += 1 + + # pPr + ppr = etree.SubElement(p, f'{{{W}}}pPr') + pstyle = etree.SubElement(ppr, f'{{{W}}}pStyle') + pstyle.set(f'{{{W}}}val', str(toc_style)) + if indent > 0: + ind = etree.SubElement(ppr, f'{{{W}}}ind') + ind.set(f'{{{W}}}left', str(indent)) + tabs = etree.SubElement(ppr, f'{{{W}}}tabs') + tab = etree.SubElement(tabs, f'{{{W}}}tab') + tab.set(f'{{{W}}}val', 'right') + tab.set(f'{{{W}}}leader', 'dot') + tab.set(f'{{{W}}}pos', '9026') + spacing = etree.SubElement(ppr, f'{{{W}}}spacing') + spacing.set(f'{{{W}}}before', '120') + spacing.set(f'{{{W}}}after', '60') + + if bm_name: + hyperlink = etree.SubElement(p, f'{{{W}}}hyperlink') + hyperlink.set(f'{{{W}}}anchor', bm_name) + hyperlink.set(f'{{{W}}}history', '1') + + r_text = etree.SubElement(hyperlink, f'{{{W}}}r') + rpr = etree.SubElement(r_text, f'{{{W}}}rPr') + rstyle = etree.SubElement(rpr, f'{{{W}}}rStyle') + rstyle.set(f'{{{W}}}val', 'Hyperlink') + t = etree.SubElement(r_text, f'{{{W}}}t') + t.text = text_raw + + r_tab = etree.SubElement(hyperlink, f'{{{W}}}r') + etree.SubElement(r_tab, f'{{{W}}}tab') + + r_begin = etree.SubElement(hyperlink, f'{{{W}}}r') + fc_begin = etree.SubElement(r_begin, f'{{{W}}}fldChar') + fc_begin.set(f'{{{W}}}fldCharType', 'begin') + + r_instr = etree.SubElement(hyperlink, f'{{{W}}}r') + instr = etree.SubElement(r_instr, f'{{{W}}}instrText') + instr.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') + instr.text = f' PAGEREF {bm_name} \\h ' + + r_sep = etree.SubElement(hyperlink, f'{{{W}}}r') + fc_sep = etree.SubElement(r_sep, f'{{{W}}}fldChar') + fc_sep.set(f'{{{W}}}fldCharType', 'separate') + + r_page = etree.SubElement(hyperlink, f'{{{W}}}r') + t_page = etree.SubElement(r_page, f'{{{W}}}t') + t_page.text = str(page) + + r_end = etree.SubElement(hyperlink, f'{{{W}}}r') + fc_end = etree.SubElement(r_end, f'{{{W}}}fldChar') + fc_end.set(f'{{{W}}}fldCharType', 'end') + else: + r_text = etree.SubElement(p, f'{{{W}}}r') + t = etree.SubElement(r_text, f'{{{W}}}t') + t.text = text_raw + + r_tab = etree.SubElement(p, f'{{{W}}}r') + etree.SubElement(r_tab, f'{{{W}}}tab') + + r_page = etree.SubElement(p, f'{{{W}}}r') + t_page = etree.SubElement(r_page, f'{{{W}}}t') + t_page.text = str(page) + + placeholders_inserted = len(entries) + print(f"Inserted {placeholders_inserted} TOC placeholder entries") + + # Serialize back to string + result = etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True) + return result.decode('utf-8') + + +def main(): + parser = argparse.ArgumentParser( + description='Add placeholder entries to Table of Contents in a DOCX file (in-place)' + ) + parser.add_argument('docx_file', help='DOCX file to modify (will be replaced)') + parser.add_argument( + '--auto', action='store_true', + help='Auto-extract Heading 1-3 from the DOCX as TOC entries (recommended)' + ) + parser.add_argument( + '--entries', + help='JSON string with placeholder entries: [{"level":1,"text":"Chapter 1","page":"1"}]' + ) + + args = parser.parse_args() + + # Determine entries + entries = None + if args.entries: + try: + entries = json.loads(args.entries) + except json.JSONDecodeError as e: + print(f"Error parsing entries JSON: {e}", file=sys.stderr) + sys.exit(1) + elif args.auto or True: + # Default to auto mode — always extract from document headings + entries = _extract_headings_from_docx(args.docx_file) + if entries: + print(f"Auto-extracted {len(entries)} headings from document", file=sys.stderr) + else: + print("No headings found in document, using minimal placeholder", file=sys.stderr) + entries = [{"level": 1, "text": "Contents", "page": "1"}] + + # Add placeholders + try: + add_toc_placeholders(args.docx_file, entries) + print(f"Successfully added TOC placeholders to {args.docx_file}") + except RuntimeError as e: + # TOC structure errors — hard fail with exit code 1 + print(f"ERROR: {e}", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/skills/docx/scripts/document.py b/skills/docx/scripts/document.py new file mode 100755 index 0000000..dbe2c27 --- /dev/null +++ b/skills/docx/scripts/document.py @@ -0,0 +1,1333 @@ +#!/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 + +import zipfile + +import defusedxml.minidom +from defusedxml import minidom + +from .utilities import XMLEditor + + +# --------------------------------------------------------------------------- +# Inline pack utility (replaces former ooxml.scripts.pack dependency) +# --------------------------------------------------------------------------- + +def _condense_xml(xml_file): + """Strip unnecessary whitespace from XML, preserving text content.""" + with open(xml_file, "r", encoding="utf-8") as f: + dom = defusedxml.minidom.parse(f) + for element in dom.getElementsByTagName("*"): + if element.tagName.endswith(":t"): + continue + 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) + with open(xml_file, "wb") as f: + f.write(dom.toxml(encoding="UTF-8")) + + +def _pack_document(input_dir, output_file): + """Pack an unpacked directory back into a .docx file.""" + input_dir = Path(input_dir) + output_file = Path(output_file) + with tempfile.TemporaryDirectory() as temp_dir: + temp_content_dir = Path(temp_dir) / "content" + shutil.copytree(input_dir, temp_content_dir) + for pattern in ["*.xml", "*.rels"]: + for xml_file in temp_content_dir.rglob(pattern): + _condense_xml(xml_file) + 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)) + +# 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 = "Z.AI", initials: str = "Z" + ): + """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: "Z.AI") + 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="Z.AI", + 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: "Z.AI") + 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) + + 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 (lightweight check). + + Currently performs basic structural checks. XSD schema validation and + redlining validation have been removed. Use save(validate=False) to + skip validation entirely. + """ + # Basic structural check: ensure word/document.xml exists + doc_xml = self.unpacked_path / "word" / "document.xml" + if not doc_xml.exists(): + raise ValueError("Validation failed: word/document.xml not found") + + 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, update_fields=True): + """Add RSID and optionally enable track revisions and update fields in settings.xml. + + Args: + path: Path to settings.xml + track_revisions: If True, adds trackRevisions element + update_fields: If True, adds updateFields element to auto-update fields on open + + Places elements per OOXML schema order: + - trackRevisions: early (before defaultTabStop) + - updateFields: 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) + + # Conditionally add updateFields if requested + if update_fields: + update_fields_exists = any( + elem.tagName == f"{prefix}:updateFields" + for elem in editor.dom.getElementsByTagName(f"{prefix}:updateFields") + ) + + if not update_fields_exists: + update_fields_xml = f'<{prefix}:updateFields {prefix}:val="true"/>' + # Try to insert before defaultTabStop, hyphenationZone, or at start + inserted = False + for tag in [f"{prefix}:defaultTabStop", f"{prefix}:hyphenationZone"]: + elements = editor.dom.getElementsByTagName(tag) + if elements: + editor.insert_before(elements[0], update_fields_xml) + inserted = True + break + if not inserted: + # Insert as first child of settings + if root.firstChild: + editor.insert_before(root.firstChild, update_fields_xml) + else: + editor.append_to(root, update_fields_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/postcheck.py b/skills/docx/scripts/postcheck.py new file mode 100755 index 0000000..9f4ab88 --- /dev/null +++ b/skills/docx/scripts/postcheck.py @@ -0,0 +1,807 @@ +#!/usr/bin/env python3 +""" +postcheck.py — Document business rule self-check script + +Unlike traditional OpenXML Schema validation, this script does not check XML legality. +Instead, it checks document "visual quality" and "typesetting correctness" — issues visible to the human eye. + +Usage: + python3 postcheck.py output.docx [--fix] [--json] + +Checks: + 1. Blank page detection — trailing/middle excess blank pages, double page breaks, consecutive empty paragraphs + 2. Line spacing consistency — whether body paragraph line spacing is uniform + 3. Table margins — whether cells have padding set + 4. Table pagination control — whether header rows have tblHeader set, data rows have cantSplit + 5. Image overflow — whether image width exceeds page usable area + 6. Font fallback — whether fonts are used that may be missing on target systems + 7. CJK indentation — whether Chinese body text has first-line indent (excluding table cells, lists, centered paragraphs) + 8. Heading level continuity — whether headings skip levels (H1→H3 skipping H2) + 9. Numbering continuity — whether numbered lists have gaps + 10. Cover separation — whether cover and body are in different sections + 11. ShadingType — whether SOLID is misused causing black cells + 12. TOC quality — whether TOC field exists, whether headings use standard Heading styles + 13. Image aspect ratio — whether images are stretched/distorted + 14. Document cleanliness — whether placeholder text, Markdown syntax, or draft expressions remain + 15. Report content quality — whether summary exists, whether titles are specific, whether vague conclusions are used +""" + +import zipfile +import sys +import json +import re +from pathlib import Path +from xml.etree import ElementTree as ET + +NS = { + "w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main", + "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "wp": "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing", + "a": "http://schemas.openxmlformats.org/drawingml/2006/main", + "pic": "http://schemas.openxmlformats.org/drawingml/2006/picture", +} + + +class CheckResult: + def __init__(self, name: str, passed: bool, message: str, severity: str = "warning"): + self.name = name + self.passed = passed + self.message = message + self.severity = severity # "error" | "warning" | "info" + + def to_dict(self): + return { + "name": self.name, + "passed": self.passed, + "message": self.message, + "severity": self.severity, + } + + def __str__(self): + icon = "✅" if self.passed else ("❌" if self.severity == "error" else "⚠️") + return f"{icon} [{self.name}] {self.message}" + + +def read_document_xml(docx_path: str) -> ET.Element: + """Read document.xml and return the root element""" + with zipfile.ZipFile(docx_path, "r") as z: + return ET.fromstring(z.read("word/document.xml")) + + +def get_sections(root: ET.Element) -> list: + """Extract all sections (located via sectPr)""" + body = root.find(".//w:body", NS) + if body is None: + return [] + + sections = [] + current_children = [] + + for child in body: + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if tag == "sectPr": + sections.append({"children": current_children, "sectPr": child}) + current_children = [] + else: + # Check whether paragraph contains sectPr (section break inside paragraph pPr) + ppr_sect = child.find(".//w:pPr/w:sectPr", NS) + if ppr_sect is not None: + current_children.append(child) + sections.append({"children": current_children, "sectPr": ppr_sect}) + current_children = [] + else: + current_children.append(child) + + # Last section (body-level sectPr) + body_sect = body.find("w:sectPr", NS) + if body_sect is not None and current_children: + sections.append({"children": current_children, "sectPr": body_sect}) + + return sections + + +def check_blank_pages(root: ET.Element) -> CheckResult: + """Detect excess blank pages — multi-pattern detection""" + body = root.find(".//w:body", NS) + paragraphs = body.findall("w:p", NS) + issues = [] + + if not paragraphs: + return CheckResult("blank-pages", True, "No paragraph content") + + # Check 1: Whether the last paragraph only has a page break + last_p = paragraphs[-1] + runs = last_p.findall(".//w:r", NS) + has_page_break = False + has_text = False + for run in runs: + br = run.find("w:br", NS) + if br is not None and br.get(f"{{{NS['w']}}}type") == "page": + has_page_break = True + t = run.find("w:t", NS) + if t is not None and t.text and t.text.strip(): + has_text = True + if has_page_break and not has_text: + issues.append("Trailing page break at document end may cause blank page") + + # Check 2: Consecutive empty paragraphs (≥5 consecutive may form visual blank page) + consecutive_empty = 0 + max_empty = 0 + max_empty_pos = 0 + for idx, p in enumerate(paragraphs): + texts = p.findall(".//w:t", NS) + has_any_text = any(t.text and t.text.strip() for t in texts) + has_br = any( + br.get(f"{{{NS['w']}}}type") == "page" + for br in p.findall(".//w:br", NS) + ) + has_drawing = p.find(".//{http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing}inline", None) is not None + if not has_any_text and not has_br and not has_drawing: + consecutive_empty += 1 + if consecutive_empty > max_empty: + max_empty = consecutive_empty + max_empty_pos = idx + else: + consecutive_empty = 0 + + if max_empty >= 5: + issues.append(f"Found {max_empty} consecutive empty paragraphs (starting around paragraph {max_empty_pos - max_empty + 2}), may form visual blank page") + + # Check 3: Double page break at section boundary (PageBreak at section end + NEXT_PAGE in next section) + sections = get_sections(root) + for i in range(len(sections) - 1): + sec_children = sections[i]["children"] + if not sec_children: + continue + # Check whether the last paragraph of the section contains PageBreak + last_child = sec_children[-1] + if last_child.tag == f"{{{NS['w']}}}p": + for br in last_child.findall(".//w:br", NS): + if br.get(f"{{{NS['w']}}}type") == "page": + # Check whether the next section is NEXT_PAGE + next_sect_pr = sections[i + 1]["sectPr"] + sect_type = next_sect_pr.find("w:type", NS) + if sect_type is not None and sect_type.get(f"{{{NS['w']}}}val") == "nextPage": + issues.append(f"Section {i+1} ends with PageBreak and Section {i+2} is type nextPage, double page break causes blank page") + + # Check 4: Empty paragraph + PageBreak (paragraph has only PageBreak, no text) + # Exclude section-ending PageBreaks — they are normal section separators + # (e.g., cover page ending with an empty para + PageBreak before a new section) + section_last_paras = set() + for sec in sections: + children = sec["children"] + if children: + last_child = children[-1] + section_last_paras.add(id(last_child)) + + empty_pb_count = 0 + for p in paragraphs[:-1]: # Last paragraph already handled in Check 1 + if id(p) in section_last_paras: + continue # Skip section-ending paragraphs (normal section breaks) + runs = p.findall(".//w:r", NS) + p_has_break = False + p_has_text = False + for run in runs: + br = run.find("w:br", NS) + if br is not None and br.get(f"{{{NS['w']}}}type") == "page": + p_has_break = True + t = run.find("w:t", NS) + if t is not None and t.text and t.text.strip(): + p_has_text = True + if p_has_break and not p_has_text: + empty_pb_count += 1 + + if empty_pb_count > 0: + issues.append(f"Found {empty_pb_count} empty paragraphs with PageBreak (suggest attaching PageBreak to content paragraphs)") + + # Separate hard errors from soft warnings + hard_issues = [i for i in issues if "double page break" in i.lower() or "trailing page break" in i.lower() or "consecutive" in i.lower()] + soft_issues = [i for i in issues if i not in hard_issues] + + if hard_issues: + return CheckResult( + "blank-pages", False, + "; ".join(hard_issues[:3]), + "error" + ) + if soft_issues: + return CheckResult( + "blank-pages", False, + "; ".join(soft_issues[:3]), + "warning" + ) + + return CheckResult("blank-pages", True, "No blank page issues detected") + + +def check_line_spacing(root: ET.Element) -> CheckResult: + """Check body paragraph line spacing consistency""" + body = root.find(".//w:body", NS) + paragraphs = body.findall(".//w:p", NS) + + spacing_values = {} + body_para_count = 0 + + for p in paragraphs: + ppr = p.find("w:pPr", NS) + # Skip heading paragraphs + if ppr is not None: + style = ppr.find("w:pStyle", NS) + if style is not None: + val = style.get(f"{{{NS['w']}}}val", "") + if val.startswith("Heading") or val == "Title": + continue + + spacing = ppr.find("w:spacing", NS) if ppr is not None else None + line_val = spacing.get(f"{{{NS['w']}}}line") if spacing is not None else None + + # Only count paragraphs with text content + texts = p.findall(".//w:t", NS) + if not any(t.text and t.text.strip() for t in texts): + continue + + body_para_count += 1 + key = line_val or "default" + spacing_values[key] = spacing_values.get(key, 0) + 1 + + if body_para_count == 0: + return CheckResult("line-spacing", True, "No body paragraphs") + + if len(spacing_values) <= 1: + dominant = list(spacing_values.keys())[0] if spacing_values else "default" + return CheckResult("line-spacing", True, f"Line spacing uniform (line={dominant})") + + # Find the most common line spacing + dominant = max(spacing_values, key=spacing_values.get) + inconsistent = sum(v for k, v in spacing_values.items() if k != dominant) + total = sum(spacing_values.values()) + + if inconsistent / total > 0.2: + return CheckResult( + "line-spacing", False, + f"Line spacing inconsistent: {dict(spacing_values)}, {inconsistent}/{total} paragraphs differ from dominant spacing {dominant}", + "warning" + ) + + return CheckResult("line-spacing", True, f"Line spacing mostly uniform (line={dominant}, {inconsistent} exceptions)") + + + +def check_image_overflow(root: ET.Element) -> CheckResult: + """Check whether image width may exceed page bounds""" + # Get page width + sect_pr = root.find(".//w:body/w:sectPr", NS) + page_width = 11906 # A4 default + margin_left = 1701 + margin_right = 1417 + + if sect_pr is not None: + pg_sz = sect_pr.find("w:pgSz", NS) + pg_mar = sect_pr.find("w:pgMar", NS) + if pg_sz is not None: + page_width = int(pg_sz.get(f"{{{NS['w']}}}w", "11906")) + if pg_mar is not None: + margin_left = int(pg_mar.get(f"{{{NS['w']}}}left", "1701")) + margin_right = int(pg_mar.get(f"{{{NS['w']}}}right", "1417")) + + usable_width_emu = (page_width - margin_left - margin_right) * 635 # twips → EMU + + drawings = root.findall(".//wp:inline", NS) + root.findall(".//wp:anchor", NS) + oversized = 0 + + for dwg in drawings: + extent = dwg.find("wp:extent", NS) + if extent is not None: + cx = int(extent.get("cx", "0")) + if cx > usable_width_emu * 1.05: # 5% tolerance + oversized += 1 + + if oversized > 0: + return CheckResult( + "image-overflow", False, + f"{oversized} images exceed page usable area", + "error" + ) + + return CheckResult( + "image-overflow", True, + f"All images within page width ({len(drawings)} images)" + ) + + +def check_image_aspect_ratio(docx_path: str, root: ET.Element) -> CheckResult: + """Check whether images are stretched/distorted (aspect ratio drift). + + Compares the original aspect ratio of embedded images with the display aspect ratio set in wp:extent. + Drift >10% is considered distortion (pie charts becoming elliptical, radar charts becoming diamond-shaped, etc). + """ + import zipfile as _zf + + # Build a mapping: rId → image file path inside the zip + # We need to parse word/_rels/document.xml.rels + rid_to_path = {} + try: + with _zf.ZipFile(docx_path, 'r') as z: + rels_path = 'word/_rels/document.xml.rels' + if rels_path in z.namelist(): + rels_xml = z.read(rels_path) + rels_root = ET.fromstring(rels_xml) + rels_ns = 'http://schemas.openxmlformats.org/package/2006/relationships' + for rel in rels_root.findall(f'{{{rels_ns}}}Relationship'): + rid = rel.get('Id', '') + target = rel.get('Target', '') + rel_type = rel.get('Type', '') + if 'image' in rel_type: + # Target is relative to word/ directory + if not target.startswith('/'): + img_path = 'word/' + target + else: + img_path = target.lstrip('/') + rid_to_path[rid] = img_path + + # Now check each drawing + drawings = root.findall(".//wp:inline", NS) + root.findall(".//wp:anchor", NS) + distorted = [] + + for dwg in drawings: + extent = dwg.find("wp:extent", NS) + if extent is None: + continue + display_cx = int(extent.get("cx", "0")) + display_cy = int(extent.get("cy", "0")) + if display_cx == 0 or display_cy == 0: + continue + + # Find the blip rId + blip = dwg.find(".//a:blip", NS) + if blip is None: + continue + r_embed = blip.get(f"{{{NS['r']}}}embed", "") + if not r_embed or r_embed not in rid_to_path: + continue + + img_zip_path = rid_to_path[r_embed] + if img_zip_path not in z.namelist(): + continue + + # Read actual image dimensions + try: + img_data = z.read(img_zip_path) + from PIL import Image as _PILImage + import io as _io + pil_img = _PILImage.open(_io.BytesIO(img_data)) + orig_w, orig_h = pil_img.size + if orig_w == 0 or orig_h == 0: + continue + except Exception: + continue + + # Compare aspect ratios + orig_ratio = orig_w / orig_h + display_ratio = display_cx / display_cy + drift = abs(orig_ratio - display_ratio) / orig_ratio + + if drift > 0.10: # >10% distortion + pct = drift * 100 + distorted.append( + f"{img_zip_path.split('/')[-1]}: " + f"original {orig_w}×{orig_h} (ratio={orig_ratio:.2f}), " + f"display ratio={display_ratio:.2f}, distortion {pct:.0f}%" + ) + + except Exception: + return CheckResult( + "image-aspect-ratio", True, + "Cannot check image aspect ratio (zip read error)", + "info" + ) + + if distorted: + detail = "; ".join(distorted[:3]) + if len(distorted) > 3: + detail += f" ...and {len(distorted)} more" + return CheckResult( + "image-aspect-ratio", False, + f"{len(distorted)} images have aspect ratio distortion: {detail}", + "warning" + ) + + img_count = len(drawings) + return CheckResult( + "image-aspect-ratio", True, + f"All images have correct aspect ratio ({img_count} images)" + ) + + +def check_font_fallback(root: ET.Element) -> CheckResult: + """Check whether potentially missing fonts are used""" + SAFE_FONTS = { + # Chinese + "宋体", "SimSun", "黑体", "SimHei", "微软雅黑", "Microsoft YaHei", + "仿宋", "FangSong", "FangSong_GB2312", "楷体", "KaiTi", + # English + "Times New Roman", "Arial", "Calibri", "Helvetica", + "Courier New", "Georgia", "Verdana", "Tahoma", + # Universal + "Symbol", "Wingdings", + } + + fonts_used = set() + for rpr in root.findall(".//w:rPr", NS): + for font_tag in ["w:rFonts"]: + rf = rpr.find(font_tag, NS) + if rf is not None: + for attr in ["ascii", "eastAsia", "hAnsi", "cs"]: + f = rf.get(f"{{{NS['w']}}}{attr}") + if f: + fonts_used.add(f) + + risky = fonts_used - SAFE_FONTS + if risky: + return CheckResult( + "font-fallback", False, + f"Following fonts may be missing on target system: {', '.join(sorted(risky))}", + "info" + ) + + return CheckResult("font-fallback", True, f"All fonts are common system fonts ({len(fonts_used)} types)") + + + +def check_heading_levels(root: ET.Element) -> CheckResult: + """Check whether headings skip levels""" + body = root.find(".//w:body", NS) + heading_levels = [] + + for p in body.findall(".//w:p", NS): + ppr = p.find("w:pPr", NS) + if ppr is None: + continue + style = ppr.find("w:pStyle", NS) + if style is None: + continue + val = style.get(f"{{{NS['w']}}}val", "") + m = re.match(r"Heading(\d+)", val) + if m: + heading_levels.append(int(m.group(1))) + + if len(heading_levels) < 2: + return CheckResult("heading-levels", True, "Too few headings, skipping check") + + skips = [] + for i in range(1, len(heading_levels)): + diff = heading_levels[i] - heading_levels[i - 1] + if diff > 1: + skips.append(f"H{heading_levels[i-1]}→H{heading_levels[i]}") + + if skips: + return CheckResult( + "heading-levels", False, + f"Heading level skip: {', '.join(skips[:5])}", + "warning" + ) + + return CheckResult("heading-levels", True, f"Heading levels continuous ({len(heading_levels)} headings)") + + +# check_cover_separation removed — false positives on complex covers (>15 elements is normal) + + +def check_shading_type(root: ET.Element) -> CheckResult: + """Check whether ShadingType.SOLID is misused""" + shadings = root.findall(".//w:shd", NS) + solid_count = 0 + + for shd in shadings: + val = shd.get(f"{{{NS['w']}}}val", "") + if val == "solid": + solid_count += 1 + + if solid_count > 0: + return CheckResult( + "shading-type", False, + f"Found {solid_count} instances of ShadingType.SOLID (should be CLEAR), may cause black cells", + "error" + ) + + return CheckResult("shading-type", True, "No ShadingType.SOLID misuse found") + + + +def check_toc(root: ET.Element, docx_path: str = "") -> CheckResult: + """Check TOC quality: field existence, headings presence, outlineLvl, updateFields.""" + body = root.find(".//w:body", NS) + if body is None: + return CheckResult("toc", True, "Document body is empty, skipping TOC check", "info") + + paragraphs = list(body) + w_ns = NS["w"] + + # --- Detect headings and their levels --- + heading_count = 0 + heading_levels_used = set() # e.g. {1, 2, 3} + for p in paragraphs: + if p.tag != f"{{{w_ns}}}p": + continue + ppr = p.find(f"{{{w_ns}}}pPr") + if ppr is None: + continue + ps = ppr.find(f"{{{w_ns}}}pStyle") + if ps is None: + continue + val = ps.get(f"{{{w_ns}}}val", "") + m = re.match(r"(?i)heading\s*(\d)", val) + if m: + heading_count += 1 + heading_levels_used.add(int(m.group(1))) + + # --- Detect TOC field --- + has_toc = False + for instr in root.findall(f".//{{{w_ns}}}instrText"): + if instr.text and "TOC" in instr.text.upper(): + has_toc = True + break + if not has_toc: + for fld in root.findall(f".//{{{w_ns}}}fldSimple"): + if "TOC" in fld.get(f"{{{w_ns}}}instr", "").upper(): + has_toc = True + break + # Also check SDT-wrapped TOC + if not has_toc: + for sdt in root.findall(f".//{{{w_ns}}}sdt"): + for instr in sdt.findall(f".//{{{w_ns}}}instrText"): + if instr.text and "TOC" in instr.text.upper(): + has_toc = True + break + if has_toc: + break + + issues = [] + + # Check 1: Document has a "目录" / "目 录" / "Table of Contents" title but no TOC field + has_toc_title = False + toc_title_pattern = re.compile(r'^(?:目\s*录|table\s+of\s+contents|contents)$', re.IGNORECASE) + for p in paragraphs: + if p.tag != f"{{{w_ns}}}p": + continue + texts = p.findall(f".//{{{w_ns}}}t") + p_text = "".join(t.text or "" for t in texts).strip() + if toc_title_pattern.match(p_text): + has_toc_title = True + break + + if has_toc_title and not has_toc: + issues.append("TOC_FIELD_MISSING: document has a TOC title but no TOC field element — add TableOfContents in code") + + # Check 2: TOC field exists but no headings in document → TOC will be empty after update + if has_toc and heading_count == 0: + issues.append("TOC_NO_HEADINGS: TOC field exists but document has 0 Heading-styled paragraphs — TOC will be empty after update") + + # Check 3 & 4: Read styles.xml and settings.xml from DOCX (only when TOC exists) + if has_toc and docx_path: + try: + import zipfile + with zipfile.ZipFile(docx_path, 'r') as zf: + # Check 3: outlineLvl missing in Heading styles + if 'word/styles.xml' in zf.namelist(): + styles_content = zf.read('word/styles.xml').decode('utf-8') + styles_root = ET.fromstring(styles_content) + + missing_outline = [] + for level in sorted(heading_levels_used): + style_id = f"Heading{level}" + # Find + for style_elem in styles_root.findall(f".//{{{w_ns}}}style"): + sid = style_elem.get(f"{{{w_ns}}}styleId", "") + if sid == style_id: + # Check if pPr has outlineLvl + ppr = style_elem.find(f"{{{w_ns}}}pPr") + has_outline = False + if ppr is not None: + ol = ppr.find(f"{{{w_ns}}}outlineLvl") + if ol is not None: + has_outline = True + if not has_outline: + missing_outline.append(style_id) + break + + if missing_outline: + issues.append( + "TOC_OUTLINE_MISSING: %s style(s) missing outlineLvl — " + "Word TOC update won't find these headings. " + "Run add_toc_placeholders.py to fix" % ", ".join(missing_outline) + ) + + # Check 4: updateFields not set to true + if 'word/settings.xml' in zf.namelist(): + settings_content = zf.read('word/settings.xml').decode('utf-8') + # Check for + update_ok = bool(re.search( + r']*w:val\s*=\s*"true"', + settings_content + )) + if not update_ok: + issues.append( + "TOC_UPDATE_DISABLED: settings.xml missing updateFields=true — " + "Word won't prompt to update TOC on open. " + "Run add_toc_placeholders.py to fix" + ) + except Exception as e: + issues.append(f"TOC_CHECK_ERROR: failed to read styles/settings from DOCX: {e}") + + if not issues: + if has_toc: + return CheckResult("toc", True, "TOC field present and update-ready") + else: + return CheckResult("toc", True, "No TOC needed") + + severity = "error" if any(k in i for i in issues for k in ("FIELD_MISSING", "NO_HEADINGS", "OUTLINE_MISSING")) else "warning" + return CheckResult("toc", False, "; ".join(issues[:5]), severity) + + + + +def check_cover_overflow(root: ET.Element) -> CheckResult: + """Detect cover section issues: oversized fonts, excessive spacing, trailing empty content.""" + sections = get_sections(root) + if not sections: + return CheckResult("cover-overflow", True, "No sections found") + + sec0 = sections[0] + sect_pr = sec0["sectPr"] + + # Get page dimensions and margins for accurate available height calculation + pg_sz = sect_pr.find("w:pgSz", NS) + pg_mar = sect_pr.find("w:pgMar", NS) + page_height = int(pg_sz.get(f"{{{NS['w']}}}h", "16838")) if pg_sz is not None else 16838 + margin_top = int(pg_mar.get(f"{{{NS['w']}}}top", "0")) if pg_mar is not None else 0 + margin_bottom = int(pg_mar.get(f"{{{NS['w']}}}bottom", "0")) if pg_mar is not None else 0 + + issues = [] + children = sec0["children"] + + # Check 1: Oversized font in cover section (> 44pt = 88 half-points = 889000 EMU) + max_font_size = 0 + for child in children: + for sz in child.findall(".//" + f"{{{NS['w']}}}sz"): + val = sz.get(f"{{{NS['w']}}}val") + if val and val.isdigit(): + size_hp = int(val) + if size_hp > max_font_size: + max_font_size = size_hp + + if max_font_size > 88: # 88 half-points = 44pt + issues.append( + f"Cover has font size {max_font_size // 2}pt (>{44}pt max). " + f"Use calcTitleLayout() for dynamic sizing" + ) + + # Check 2: Excessive spacing.before in cover section (> 5000 twips) + max_spacing = 0 + for child in children: + for sp in child.findall(".//" + f"{{{NS['w']}}}spacing"): + before = sp.get(f"{{{NS['w']}}}before") + if before and before.isdigit(): + val = int(before) + if val > max_spacing: + max_spacing = val + + if max_spacing > 5000: + issues.append( + f"Cover has spacing.before={max_spacing} twips (>5000 max). " + f"Use calcCoverSpacing() for dynamic spacing" + ) + + # Check 3: Trailing empty paragraphs in cover section + trailing_empty = 0 + for child in reversed(children): + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if tag != "p": + break + texts = child.findall(".//" + f"{{{NS['w']}}}t") + has_text = any(t.text and t.text.strip() for t in texts) + if not has_text: + trailing_empty += 1 + else: + break + + if trailing_empty > 2: + issues.append( + f"Cover section ends with {trailing_empty} empty paragraphs (max 2 allowed) — " + f"excessive empty paragraphs may cause blank page after cover" + ) + + if issues: + return CheckResult( + "cover-overflow", False, + "; ".join(issues), + "error" + ) + + return CheckResult("cover-overflow", True, "Cover section layout looks OK") + + +def run_all_checks(docx_path: str) -> list[CheckResult]: + """Run all checks""" + root = read_document_xml(docx_path) + + checks = [ + check_blank_pages, + check_cover_overflow, + check_line_spacing, + check_image_overflow, + check_font_fallback, + check_heading_levels, + check_shading_type, + ] + + results = [] + for check_fn in checks: + try: + results.append(check_fn(root)) + except Exception as e: + results.append(CheckResult( + check_fn.__name__.replace("check_", ""), + False, + f"Check error: {e}", + "error" + )) + + # TOC check needs both root and docx_path + try: + results.append(check_toc(root, docx_path)) + except Exception as e: + results.append(CheckResult("toc", False, f"Check error: {e}", "error")) + + # Image aspect ratio check needs both root and docx_path + try: + results.append(check_image_aspect_ratio(docx_path, root)) + except Exception as e: + results.append(CheckResult("image-aspect-ratio", False, f"Check error: {e}", "error")) + + return results + + +def main(): + import argparse + parser = argparse.ArgumentParser(description="docx business rule self-check") + parser.add_argument("docx_path", help="Path to the .docx file to check") + parser.add_argument("--json", action="store_true", help="Output in JSON format") + parser.add_argument("--strict", action="store_true", help="Treat warnings as failures") + args = parser.parse_args() + + if not Path(args.docx_path).exists(): + print(f"❌ File not found: {args.docx_path}") + sys.exit(1) + + results = run_all_checks(args.docx_path) + + if args.json: + print(json.dumps([r.to_dict() for r in results], ensure_ascii=False, indent=2)) + else: + print(f"\n📋 Document self-check report: {args.docx_path}\n") + for r in results: + print(f" {r}") + + passed = sum(1 for r in results if r.passed) + total = len(results) + errors = sum(1 for r in results if not r.passed and r.severity == "error") + warnings = sum(1 for r in results if not r.passed and r.severity == "warning") + + print(f"\n {'─' * 50}") + print(f" Passed {passed}/{total} | ❌ {errors} errors | ⚠️ {warnings} warnings\n") + + # Exit code + has_errors = any(not r.passed and r.severity == "error" for r in results) + has_warnings = any(not r.passed and r.severity == "warning" for r in results) + + if has_errors: + sys.exit(2) + elif args.strict and has_warnings: + sys.exit(1) + else: + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/skills/docx/scripts/templates/comments.xml b/skills/docx/scripts/templates/comments.xml new file mode 100755 index 0000000..b5dace0 --- /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 100755 index 0000000..b4cf23e --- /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 100755 index 0000000..e32a05e --- /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 100755 index 0000000..d04bc8e --- /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 100755 index 0000000..a839caf --- /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 0000000..d92dae6 --- /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/docx/setup.sh b/skills/docx/setup.sh new file mode 100755 index 0000000..905c66b --- /dev/null +++ b/skills/docx/setup.sh @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +# --- +# name: docx-setup +# author: Z.AI +# version: "1.0" +# description: Environment setup for the DOCX skill. Checks and installs all required dependencies. +# --- +# +# Installs only dependencies required by the DOCX skill. +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +ok() { echo -e " ${GREEN}✓${NC} $1"; } +fail() { echo -e " ${RED}✗${NC} $1"; } +warn() { echo -e " ${YELLOW}○${NC} $1"; } +info() { echo -e " ${BLUE}→${NC} $1"; } + +echo "============================================" +echo " DOCX Skill — Environment Setup" +echo "============================================" +echo "" + +OS="$(uname -s)" +ARCH="$(uname -m)" +echo "Platform: $OS $ARCH" +echo "" + +# ── 0. macOS: Homebrew ── +if [ "$OS" = "Darwin" ]; then + echo "--- Homebrew (macOS package manager) ---" + if command -v brew &>/dev/null; then + BREW_VER=$(brew --version 2>/dev/null | head -1) + ok "brew ($BREW_VER)" + else + fail "brew not found — Node.js install needs Homebrew on macOS" + info "Install: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" + fi + echo "" +fi + +# ── 1. Node.js (docx-js runs on Node) ── +echo "--- Node.js ---" +if command -v node &>/dev/null; then + NODE_VER=$(node --version) + ok "node ($NODE_VER)" +else + fail "node not found (required — docx generation uses docx-js on Node)" + case "$OS" in + Darwin) info "Install: brew install node" ;; + Linux) info "Install: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -" + info " sudo apt install -y nodejs" ;; + *) info "Install: https://nodejs.org/" ;; + esac +fi + +# ── 2. npm ── +echo "" +echo "--- npm ---" +if command -v npm &>/dev/null; then + NPM_VER=$(npm --version 2>/dev/null) + ok "npm ($NPM_VER)" +else + fail "npm not found" + case "$OS" in + Darwin) info "Install: brew install node (includes npm)" ;; + Linux) info "Install: comes with nodejs" ;; + *) info "Install: https://nodejs.org/" ;; + esac +fi + +# ── 3. npm package: docx ── +echo "" +echo "--- npm Packages ---" +if node -e "require('docx')" 2>/dev/null || npm list -g docx &>/dev/null; then + DOCX_VER=$(node -e "try{console.log(require('docx/package.json').version)}catch(e){console.log('installed')}" 2>/dev/null) + ok "docx ($DOCX_VER)" +else + fail "docx not installed" + info "Install: npm install -g docx" + echo "" + if [ -t 0 ]; then + read -p " Install now? [Y/n] " -n 1 -r REPLY + echo "" + REPLY=${REPLY:-Y} + else + warn "Non-interactive mode — skipping auto-install." + REPLY=N + fi + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + npm install -g docx 2>/dev/null && ok "Installed: docx" || fail "npm install failed" + fi +fi + +# ── 4. Python 3 (post-processing scripts) ── +echo "" +echo "--- Python (post-processing) ---" +if command -v python3 &>/dev/null; then + PY_VER=$(python3 --version 2>&1) + ok "python3 ($PY_VER)" + if [ "$OS" = "Darwin" ]; then + PY_PATH=$(which python3 2>/dev/null) + if [[ "$PY_PATH" == "/usr/bin/python3" ]]; then + warn "Using macOS system Python (limited). Recommend: brew install python3" + fi + fi +else + fail "python3 not found" + case "$OS" in + Darwin) info "Install: brew install python3" ;; + Linux) info "Install: sudo apt install python3 python3-pip (Debian/Ubuntu)" + info " sudo dnf install python3 python3-pip (Fedora/RHEL)" ;; + *) info "Install: https://www.python.org/downloads/" ;; + esac +fi + +# ── 5. pip ── +echo "" +echo "--- pip ---" +if python3 -m pip --version &>/dev/null 2>&1; then + PIP_VER=$(python3 -m pip --version 2>/dev/null | head -1) + ok "pip ($PIP_VER)" +else + fail "pip not found" + case "$OS" in + Darwin) info "Install: python3 -m ensurepip --upgrade" + info " or: brew install python3 (includes pip)" ;; + Linux) info "Install: sudo apt install python3-pip (Debian/Ubuntu)" ;; + *) info "Install: python3 -m ensurepip --upgrade" ;; + esac +fi + +# ── 6. Python packages ── +echo "" +echo "--- Python Packages ---" +PY_PKGS=( + "defusedxml:defusedxml" +) + +MISSING_PY=() +for entry in "${PY_PKGS[@]}"; do + mod="${entry%%:*}" + pkg="${entry##*:}" + if python3 -c "import $mod" 2>/dev/null; then + ver=$(python3 -c "import $mod; print(getattr($mod, '__version__', 'installed'))" 2>/dev/null) + ok "$pkg ($ver)" + else + fail "$pkg not installed" + MISSING_PY+=("$pkg") + fi +done + +if [ ${#MISSING_PY[@]} -gt 0 ]; then + echo "" + if [ -t 0 ]; then + read -p " Install missing Python packages? [Y/n] " -n 1 -r REPLY + echo "" + REPLY=${REPLY:-Y} + else + warn "Non-interactive mode — skipping auto-install. Run interactively or install manually." + REPLY=N + fi + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + python3 -m pip install -q "${MISSING_PY[@]}" 2>/dev/null \ + || python3 -m pip install -q --user "${MISSING_PY[@]}" 2>/dev/null \ + || python3 -m pip install -q --break-system-packages "${MISSING_PY[@]}" 2>/dev/null \ + || { fail "pip install failed. Try manually: pip install ${MISSING_PY[*]}"; } + ok "Installed: ${MISSING_PY[*]}" + fi +fi + +# ── Summary ── +echo "" +echo "============================================" +echo " Setup complete." +echo " Core: Node.js + docx (npm)" +echo " Post-processing: Python + defusedxml" +echo "============================================" diff --git a/skills/dream-interpreter/SKILL.md b/skills/dream-interpreter/SKILL.md new file mode 100755 index 0000000..b5b6e86 --- /dev/null +++ b/skills/dream-interpreter/SKILL.md @@ -0,0 +1,88 @@ +--- +name: dream-interpreter +description: AI 解梦大师。用户描述梦境,智能追问关键细节后,从三个视角(周公解梦/心理分析/赛博神棍)生成解读,输出结构化 JSON 供前端渲染"梦境解析卡"。 +--- + +# dream-interpreter + +AI 解梦大师。用户描述梦境,智能追问关键细节后,从三个视角(周公解梦/心理分析/赛博神棍)生成解读,输出结构化 JSON 供前端渲染"梦境解析卡"。 + +## When to use + +- 用户说"我梦到..."、"昨晚做了个梦"、"帮我解个梦"等 +- NOT for: 清醒梦教学、睡眠质量分析、真正的心理咨询 + +## Session flow + +### Phase 1: 梦境收集 + 追问 + +1. 用户描述梦境 +2. 从描述中提取关键意象,找出最影响解读方向的模糊点 +3. 追问最多 3 个问题(可以更少),每个聚焦一个维度: + +追问维度优先级: +- **情绪**:"掉下去的时候害怕还是放松?" → 决定焦虑型/释放型 +- **环境**:"那个地方你认识吗?" → 关联生活领域 +- **人物**:"梦里的那个人你认识吗?" → 判断投射对象 +- **结局**:"最后怎么样了?" → 决定解读走向 + +追问规则: +- 用户描述已经很详细 → 少问或不问 +- 用户不想回答 → 跳过,用合理默认值 +- 追问本身要有角色感,不是审问 + +### Phase 2: 生成解读 + +收集完信息后,生成三个视角的解读。每个视角独立分析,风格差异要大。 + +读取 `interpretation-guide.md` 获取三个视角的详细指南。 + +### Phase 3: 输出结构化 JSON + +按 `output-schema.md` 中的格式输出 JSON,供前端渲染。 + +JSON 包含:梦境摘要、关键词、情绪分类、配色方案、视觉元素列表、三视角解读内容、综合建议、可分享文案。 + +读取 `visual-mapping.md` 将意象映射为视觉元素和配色。 + +## Output format + +**追问阶段**:纯文本对话,角色感强 + +**解读阶段**:输出 JSON 代码块,格式遵循 `output-schema.md` + +示例: + +追问: +``` +嗯...高楼上掉下去... +问你几个事: +1. 掉的时候你是害怕还是反而觉得挺爽? +2. 那个楼你认识吗?公司?家?还是没见过的地方? +3. 最后落地了吗?还是一直在掉? +``` + +解读输出: +```json +{ + "dream_summary": "从陌生高楼坠落,感到恐惧,没有落地", + "keywords": ["高楼", "坠落", "恐惧", "无尽下落"], + "mood": "anxious", + "color_scheme": "dark", + "visual_elements": ["building", "falling_particles", "dark_bg", "blur_lights"], + "interpretations": { + "zhouGong": { ... }, + "freud": { ... }, + "cyber": { ... } + }, + "overall_advice": "...", + "shareable_text": "..." +} +``` + +## References + +- `interpretation-guide.md` — 三视角解读详细指南和风格要求 +- `visual-mapping.md` — 梦境意象 → 视觉元素/配色的映射表 +- `output-schema.md` — JSON 输出格式完整规范 +- `questioning-strategy.md` — 追问策略和示例库 diff --git a/skills/dream-interpreter/assets/example_asset.txt b/skills/dream-interpreter/assets/example_asset.txt new file mode 100755 index 0000000..d0ac204 --- /dev/null +++ b/skills/dream-interpreter/assets/example_asset.txt @@ -0,0 +1,24 @@ +# 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. diff --git a/skills/dream-interpreter/references/api_reference.md b/skills/dream-interpreter/references/api_reference.md new file mode 100755 index 0000000..cd703cd --- /dev/null +++ b/skills/dream-interpreter/references/api_reference.md @@ -0,0 +1,34 @@ +# Reference Documentation for Dream Interpreter + +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 diff --git a/skills/dream-interpreter/references/interpretation-guide.md b/skills/dream-interpreter/references/interpretation-guide.md new file mode 100755 index 0000000..9842f7d --- /dev/null +++ b/skills/dream-interpreter/references/interpretation-guide.md @@ -0,0 +1,83 @@ +# 三视角解读指南 + +每个视角必须独立分析,给出不同甚至矛盾的结论。不要三个视角说同一件事换个措辞。 + +## 🔮 周公解梦(传统玄学) + +**知识基础:** 中国传统解梦体系 + 民间说法 + +**核心意象对照(常用):** +- 水 = 财运(清水=正财,浑水=偏财或破财) +- 蛇 = 小人或财运(看情境) +- 牙齿掉落 = 亲人健康或自信问题 +- 飞行 = 升迁、心愿达成 +- 坠落 = 运势下滑、不稳定 +- 死亡 = 重生、旧事结束 +- 考试 = 机遇或焦虑 +- 裸体 = 秘密暴露 +- 被追 = 逃避某事 +- 动物 = 根据动物种类对应不同寓意 + +**语气要求:** +- 古风但不装,像庙里那个很灵的老师傅 +- 一本正经,言之凿凿 +- 可以说"此梦主..."、"近日宜..." +- 给出明确的吉凶判断 + 运势建议 + +**输出结构:** +- content: 主体解读(100-200字) +- fortune: 吉/凶/中性偏X +- advice: 一句话建议(宜什么、忌什么) + +## 🧠 弗洛伊德 / 心理分析 + +**知识基础:** 精神分析 + 认知心理学 + 常见梦境心理学研究 + +**分析角度:** +- 潜意识欲望和压抑 +- 近期压力源的投射 +- 未完成事件的心理加工 +- 自我认知和安全感 +- 控制感 vs 失控感 + +**语气要求:** +- 专业但温和,像一个好的心理咨询师 +- 不评判,只分析 +- 用"可能反映了..."、"这或许与...有关"等非绝对表达 +- 给出可操作的心理建议 + +**输出结构:** +- content: 心理分析(100-200字) +- insight: 一句话核心洞察 +- advice: 一个具体的自我关照建议 + +## 🌀 赛博神棍 + +**知识基础:** 没有知识基础,纯脑洞 + +**可以扯的方向:** +- 平行宇宙记忆泄漏 +- 量子意识纠缠 +- 前世记忆碎片 +- 外星文明信号 +- 你的潜意识在玩某个游戏 +- 概率论/混沌理论的荒诞应用 +- 用数学公式解梦(瞎编的公式) + +**语气要求:** +- 极度自信,像真的有一台"量子梦境分析仪" +- 一本正经地胡说八道 +- 越离谱越好,但逻辑要自洽(在它自己的疯狂体系里) +- 建议部分要搞笑但具体("建议今天穿红色袜子建立跨维度共鸣") + +**输出结构:** +- content: 赛博解读(100-200字) +- prediction: 一句离谱预言 +- advice: 一个搞笑但具体的行动建议 + +## 综合建议 + +三个视角写完后,生成一段 overall_advice: +- 提取三个视角中的共性(如果有) +- 如果三个完全矛盾,就说"这个梦很有意思,不同角度看差异很大" +- 语气回归中立,简短,1-2句话 diff --git a/skills/dream-interpreter/references/output-schema.md b/skills/dream-interpreter/references/output-schema.md new file mode 100755 index 0000000..8dc4a32 --- /dev/null +++ b/skills/dream-interpreter/references/output-schema.md @@ -0,0 +1,65 @@ +# 输出 JSON Schema + +解梦结果的完整 JSON 格式。前端根据此格式渲染"梦境解析卡"。 + +## 完整结构 + +```json +{ + "dream_summary": "string — 一句话概括梦境内容(20字以内)", + "keywords": ["string — 梦境关键词,3-6个"], + "mood": "string — 情绪分类,枚举值见下方", + "color_scheme": "string — 配色方案名,与 mood 对应", + "visual_elements": ["string — 视觉元素标识,最多5个,见 visual-mapping.md"], + "interpretations": { + "zhouGong": { + "icon": "🔮", + "title": "周公解梦", + "content": "string — 主体解读,100-200字", + "fortune": "string — 吉凶判断:大吉/吉/中性/中性偏凶/凶", + "advice": "string — 一句话建议,宜忌格式" + }, + "freud": { + "icon": "🧠", + "title": "心理分析", + "content": "string — 心理分析,100-200字", + "insight": "string — 一句话核心洞察", + "advice": "string — 一个具体的自我关照建议" + }, + "cyber": { + "icon": "🌀", + "title": "赛博神棍", + "content": "string — 赛博解读,100-200字", + "prediction": "string — 一句离谱预言", + "advice": "string — 一个搞笑但具体的行动建议" + } + }, + "overall_advice": "string — 综合建议,1-2句话,中立语气", + "shareable_text": "string — 可分享文案,包含emoji,适合发朋友圈,50字以内" +} +``` + +## mood 枚举值 + +| 值 | 含义 | +|----|------| +| anxious | 焦虑、恐惧、紧张 | +| peaceful | 平静、美好、舒适 | +| sad | 悲伤、失落、遗憾 | +| surreal | 奇幻、荒诞、超现实 | +| exciting | 兴奋、刺激、冒险 | +| nostalgic | 怀旧、温馨、思念 | + +## color_scheme 与 mood 的对应 + +mood 和 color_scheme 值相同。前端根据 color_scheme 值从 visual-mapping.md 的配色表中取色。 + +## 输出要求 + +1. JSON 必须合法,可直接 JSON.parse +2. 用 ```json 代码块包裹 +3. 所有 string 字段不能为空 +4. keywords 数组 3-6 个元素 +5. visual_elements 数组 1-5 个元素 +6. 三个 interpretation 的 content 长度保持接近(都是100-200字) +7. shareable_text 要有趣,让人想转发 diff --git a/skills/dream-interpreter/references/questioning-strategy.md b/skills/dream-interpreter/references/questioning-strategy.md new file mode 100755 index 0000000..e856f61 --- /dev/null +++ b/skills/dream-interpreter/references/questioning-strategy.md @@ -0,0 +1,62 @@ +# 追问策略 + +## 原则 + +追问是为了让解读更准,不是为了凑数。用户说得清楚就少问,说得模糊才补问。 + +## 追问维度 + +按优先级排序,每次最多选 3 个最相关的: + +### 1. 情绪状态(最高优先) +梦中的情绪决定解读基调。同样是"飞",兴奋的飞和恐惧的飞完全不同。 + +示例: +- "掉下去的时候是害怕,还是反而有种如释重负的感觉?" +- "被追的时候你是拼命跑,还是跑不动那种?" +- "见到那个人的时候你开心吗?" + +### 2. 环境/场所 +场所关联生活领域(公司=事业、家=家庭、学校=成长/压力)。 + +示例: +- "那个地方你认识吗?还是从没见过?" +- "室内还是室外?白天还是晚上?" +- "周围有别人吗?" + +### 3. 人物关系 +梦中人物通常是投射。认识的人 vs 陌生人解读方向完全不同。 + +示例: +- "梦里那个人你认识吗?什么关系?" +- "ta 在梦里对你是什么态度?" + +### 4. 结局/走向 +有结局和没结局的梦含义不同。 + +示例: +- "最后怎么样了?醒了还是有个结局?" +- "你在梦里解决了那个问题吗?" + +## 追问风格 + +带角色感,像一个老练的解梦师在跟你聊天,不是在填问卷。 + +好的追问: +``` +嗯...水里啊... +那个水是清的还是浑的?你是自己跳进去的还是不知道怎么就在水里了? +``` + +坏的追问: +``` +请补充以下信息: +1. 水的颜色和清澈度 +2. 进入水中的方式 +``` + +## 跳过规则 + +- 用户说"不记得了"/"不知道" → 用最常见的默认值,继续解读 +- 用户明确表示不想多说 → 直接进入解读,不追问了 +- 用户描述已经很完整(包含情绪+环境+结局)→ 可以直接解读,最多追问 1 个 diff --git a/skills/dream-interpreter/references/visual-mapping.md b/skills/dream-interpreter/references/visual-mapping.md new file mode 100755 index 0000000..444ed99 --- /dev/null +++ b/skills/dream-interpreter/references/visual-mapping.md @@ -0,0 +1,81 @@ +# 梦境意象 → 视觉元素映射 + +## 情绪 → 配色方案 + +| mood 值 | 情绪 | 主色 | 辅色 | 背景 | +|---------|------|------|------|------| +| anxious | 焦虑/恐惧 | #2D1B69 深紫 | #1A1A2E 暗蓝 | 暗色渐变 | +| peaceful | 平静/美好 | #F4D35E 暖黄 | #83C5BE 柔绿 | 浅色渐变 | +| sad | 悲伤/失落 | #4A6FA5 灰蓝 | #2D3142 深灰 | 冷色渐变 | +| surreal | 奇幻/荒诞 | #FF006E 霓虹粉 | #8338EC 电紫 | 深色+霓虹点缀 | +| exciting | 兴奋/刺激 | #FF6B35 橙红 | #FFD700 金色 | 暖色渐变 | +| nostalgic | 怀旧/温馨 | #DDA15E 琥珀 | #BC6C25 暖棕 | 柔光渐变 | + +## 意象 → 视觉元素 + +每个视觉元素对应 p5.js 中的一个绘制模块。 + +### 自然元素 + +| 意象关键词 | visual_element 值 | p5.js 表现 | +|-----------|-------------------|------------| +| 水、海、河、湖、游泳 | water_ripple | 正弦波纹,从底部向上扩散 | +| 雨 | rain_drops | 细线粒子从上方下落 | +| 火、燃烧 | fire_particles | 橙红粒子向上飘散 | +| 风、暴风 | wind_lines | 水平方向的曲线流动 | +| 星空、夜空 | starfield | 随机闪烁的小光点 | +| 月亮 | moon_glow | 圆形光晕,缓慢脉动 | +| 太阳、光 | sun_rays | 放射状光线 | +| 森林、树 | tree_silhouettes | 底部的树形剪影 | +| 花、花园 | floating_petals | 缓慢飘落的花瓣形状 | + +### 空间/建筑 + +| 意象关键词 | visual_element 值 | p5.js 表现 | +|-----------|-------------------|------------| +| 高楼、大厦、塔 | building | 几何线条搭建的建筑轮廓 | +| 房间、室内 | room_frame | 透视线条构成的房间框架 | +| 门 | door_shape | 中央的门形轮廓,可能开/关 | +| 楼梯、阶梯 | stairs | 递进的台阶线条 | +| 迷宫、走廊 | maze_lines | 随机生成的路径线条 | +| 桥 | bridge_arc | 弧形桥梁轮廓 | + +### 动作/状态 + +| 意象关键词 | visual_element 值 | p5.js 表现 | +|-----------|-------------------|------------| +| 坠落、掉下 | falling_particles | 粒子加速下落 | +| 飞、飘 | rising_particles | 粒子缓慢上升 | +| 追逐、逃跑 | speed_lines | 高速水平线条 | +| 困住、封闭 | cage_lines | 围合的线条逐渐收缩 | +| 迷路 | scattered_dots | 随机漂移的光点 | + +### 人物/生物 + +| 意象关键词 | visual_element 值 | p5.js 表现 | +|-----------|-------------------|------------| +| 人、人影 | human_silhouette | 抽象人形剪影,淡入淡出 | +| 人群 | crowd_dots | 多个小圆点聚散 | +| 蛇 | snake_curve | S形曲线缓慢游动 | +| 猫/狗/动物 | animal_shape | 简笔动物轮廓 | +| 鸟/飞行生物 | bird_flight | V形轮廓横穿画面 | +| 鱼/水生物 | fish_swim | 椭圆形在水纹中穿行 | + +### 氛围修饰 + +| 意象关键词 | visual_element 值 | p5.js 表现 | +|-----------|-------------------|------------| +| 黑暗、看不清 | dark_bg | 整体低亮度 + 模糊光斑 | +| 模糊、朦胧 | blur_lights | 高斯模糊的光点 | +| 闪烁、不稳定 | flicker_effect | 画面整体亮度随机波动 | +| 旋转、眩晕 | spiral_motion | 螺旋运动的粒子 | +| 安静、空旷 | minimal_space | 大面积留白 + 极少元素 | + +## 映射规则 + +1. 从梦境描述中提取所有可识别意象 +2. 每个意象对应一个 visual_element +3. visual_elements 数组最多 5 个元素(防止画面太乱) +4. 优先选择与主要情节相关的意象 +5. 必须包含至少 1 个氛围修饰元素 +6. mood 取梦境中最强烈的情绪,color_scheme 对应 mood diff --git a/skills/dream-interpreter/scripts/example.py b/skills/dream-interpreter/scripts/example.py new file mode 100755 index 0000000..0d70a20 --- /dev/null +++ b/skills/dream-interpreter/scripts/example.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +""" +Example helper script for dream-interpreter + +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 dream-interpreter") + # TODO: Add actual script logic here + # This could be data processing, file conversion, API calls, etc. + +if __name__ == "__main__": + main() diff --git a/skills/dream-interpreter/skill.json b/skills/dream-interpreter/skill.json new file mode 100755 index 0000000..8e8b1cb --- /dev/null +++ b/skills/dream-interpreter/skill.json @@ -0,0 +1,7 @@ +{ + "name": "dream-interpreter", + "version": "1.0.0", + "description": "AI解梦大师。智能追问梦境细节,从周公解梦/心理分析/赛博神棍三视角解读,输出结构化JSON供前端渲染梦境解析卡。", + "author": "mingming", + "tags": ["entertainment", "dream", "divination", "visualization"] +} diff --git a/skills/finance/Finance_API_Doc.md b/skills/finance/Finance_API_Doc.md new file mode 100755 index 0000000..2a14480 --- /dev/null +++ b/skills/finance/Finance_API_Doc.md @@ -0,0 +1,445 @@ +# Finance API Complete Documentation + +## API Overview + +Finance API provides comprehensive financial data access interfaces, including real-time market data, historical stock prices, and the latest financial news. + +### 🌐 Access via API Gateway + +**This API is accessed through the web-dev-ai-gateway unified proxy service.** + +**Gateway Configuration:** +- **Gateway Base URL:** `GATEWAY_URL` (e.g., `https://internal-api.z.ai`) +- **API Path Prefix:** `API_PREFIX` (e.g., `/external/finance`) +- **Authentication:** Automatic (gateway injects `x-rapidapi-host` and `x-rapidapi-key`) +- **Required Header:** `X-Z-AI-From: Z` + +**URL Structure:** +``` +{GATEWAY_URL}{API_PREFIX}/{endpoint} +``` + +**Example:** +- Full URL: `https://internal-api.z.ai/external/finance/v1/markets/search?search=Apple` +- Breakdown: + - `https://internal-api.z.ai` - Gateway base URL (`GATEWAY_URL`) + - `/external/finance` - API path prefix (`API_PREFIX`) + - `/v1/markets/search` - API endpoint path + + +### Quick Start + +```bash +# Get real-time quote for Apple +curl -X GET "{GATEWAY_URL}{API_PREFIX}/v1/markets/quote?ticker=AAPL&type=STOCKS" \ + -H "X-Z-AI-From: Z" +``` + + +## 1. Market Data API + +### 1.1 GET v2/markets/tickers - Get All Available Market Tickers + +**Parameters:** +- `page` (optional, Number): Page number, default value is 1 +- `type` (required, String): Asset type, optional values: + - `STOCKS` - Stocks + - `ETF` - Exchange Traded Funds + - `MUTUALFUNDS` - Mutual Funds + +**curl example (via Gateway):** +```bash +curl -X GET "{GATEWAY_URL}{API_PREFIX}/v2/markets/tickers?page=1&type=STOCKS" \ + -H "X-Z-AI-From: Z" +``` + +--- + +### 1.2 GET v1/markets/search - Search Stocks + +**Parameters:** +- `search` (required, String): Search keyword (company name or stock symbol) + +**curl example (via Gateway):** +```bash +curl -X GET "{GATEWAY_URL}{API_PREFIX}/v1/markets/search?search=Apple" \ + -H "X-Z-AI-From: Z" +``` + +**Purpose:** Used to find specific stock or company ticker codes + +--- + +### 1.3 GET v1/markets/quote (real-time) - Real-time Quotes + +**Parameters:** +- `ticker` (required, String): Stock symbol (only one can be entered) +- `type` (required, String): Asset type + - `STOCKS` - Stocks + - `ETF` - Exchange Traded Funds + - `MUTUALFUNDS` - Mutual Funds + +**curl example (via Gateway):** +```bash +curl -X GET "{GATEWAY_URL}{API_PREFIX}/v1/markets/quote?ticker=AAPL&type=STOCKS" \ + -H "X-Z-AI-From: Z" +``` + +--- + +### 1.4 GET v1/markets/stock/quotes (snapshots) - Snapshot Quotes + +**Parameters:** +- `ticker` (required, String): Stock symbols, separated by commas + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/quotes?ticker=AAPL%2CMSFT%2C%5ESPX%2C%5ENYA%2CGAZP.ME%2CSIBN.ME%2CGEECEE.NS' +``` + +**Purpose:** Batch get snapshot data for multiple stocks + +--- + + +## 2. Historical Data API + +### 2.1 GET v1/markets/stock/history - Stock Historical Data + +**Parameters:** +- `symbol` (required, String): Stock symbol +- `interval` (required, String): Time interval + - `5m` - 5 minutes + - `15m` - 15 minutes + - `30m` - 30 minutes + - `1h` - 1 hour + - `1d` - Daily + - `1wk` - Weekly + - `1mo` - Monthly + - `3mo` - 3 months +- `diffandsplits` (optional, String): Include dividend and split data + - `true` - Include + - `false` - Exclude (default) + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/history?symbol=AAPL&interval=1d&diffandsplits=false' +``` + +**Purpose:** Get historical price data for specific stocks, used for technical analysis and backtesting + +--- + +### 2.2 GET v2/markets/stock/history - Stock Historical Data V2 + +**Parameters:** +- `symbol` (required, String): Stock symbol +- `interval` (optional, String): Time interval + - `1m`, `2m`, `3m`, `4m`, `5m`, `15m`, `30m` + - `1h`, `1d`, `1wk`, `1mo`, `1qty` +- `limit` (optional, Number): Limit the number of candles (1-1000) +- `dividend` (optional, String): Include dividend data (`true` or `false`) + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v2/markets/stock/history?symbol=AAPL&interval=1m&limit=640' +``` + +**Purpose:** Enhanced historical data interface + +--- + +## 3. News API + +### 3.1 GET v1/markets/news - Market News + +**Parameters:** +- `ticker` (optional, String): Stock symbols, comma-separated for multiple stocks + +**curl example:** +```bash +# Get general market news +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/news' + +# Get specific stock news +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/news?ticker=AAPL,TSLA' +``` + +**Purpose:** Get the latest market news and updates + +--- + +### 3.2 GET v2/markets/news - Market News V2 + +**Parameters:** +- `ticker` (optional, String): Stock symbol +- `type` (optional, String): News type (`ALL`, `VIDEO`, `PRESS-RELEASE`) + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v2/markets/news?ticker=AAPL&type=ALL' +``` + +**Purpose:** Enhanced interface for getting latest market-related news + +--- + +## 5. Stock Detailed Information API + +### 5.1 GET v1/markets/stock/modules (asset-profile) - Company Profile + +**Parameters:** +- `ticker` (required, String): Stock symbol + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=asset-profile' +``` + +**Purpose:** Get company basic information, business description, management team, etc. + +--- + +### 5.2 GET v1/stock/modules - Stock Module Data + +**Parameters:** +- `ticker` (required, String): Stock symbol +- `module` (required, String): Module name (one per request) + - Acceptable values: `profile`, `income-statement`, `balance-sheet`, `cashflow-statement`, + `statistics`, `calendar-events`, `sec-filings`, `recommendation-trend`, + `upgrade-downgrade-history`, `institution-ownership`, `fund-ownership`, + `major-directHolders`, `major-holders-breakdown`, `insider-transactions`, + `insider-holders`, `net-share-purchase-activity`, `earnings`, `industry-trend`, + `index-trend`, `sector-trend` + +**curl example:** +```bash +# Get specific module +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=statistics' +``` + +**Purpose:** Get one data module per request (price, financial, analyst ratings, etc.) + +--- + +### 5.3 GET v1/markets/stock/modules (statistics) - Stock Statistics + +**Parameters:** +- `ticker` (required, String): Stock symbol + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=statistics' +``` + +**Purpose:** Get key statistical indicators such as PE ratios, market cap, trading volume + +--- + +### 5.4 GET v1/markets/stock/modules (financial-data) - Get Financial Data + +**Parameters:** +- `ticker` (required, String): Stock symbol +- `module` (required, String): `financial-data` + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=financial-data' +``` + +**Purpose:** Get revenue, profit, cash flow and other financial indicators + +--- + +### 5.5 GET v1/markets/stock/modules (sec-filings) - Get SEC Filings + +**Parameters:** +- `ticker` (required, String): Stock symbol +- `module` (required, String): `sec-filings` + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=sec-filings' +``` + +**Purpose:** Get files submitted by companies to the U.S. Securities and Exchange Commission + +--- + +### 5.6 GET v1/markets/stock/modules (earnings) - Earnings Data + +**Parameters:** +- `ticker` (required, String): Stock symbol + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=earnings' +``` + +**Purpose:** Get quarterly and annual earnings information + +--- + +### 5.7 GET v1/markets/stock/modules (calendar-events) - Get Calendar Events + +**Parameters:** +- `ticker` (required, String): Stock symbol +- `module` (required, String): `calendar-events` + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=calendar-events' +``` + +**Purpose:** Get upcoming earnings release dates, dividend dates, etc. + +--- + +## 6. Financial Statements API + +### 7.1 GET v1/markets/stock/modules (balance-sheet) - Balance Sheet + +**Parameters:** +- `ticker` (required, String): Stock symbol + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=balance-sheet' +``` + +**Purpose:** Get company balance sheet data + +--- + +### 7.3 GET v1/markets/stock/modules (income-statement) - Income Statement + +**Parameters:** +- `ticker` (required, String): Stock symbol + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=income-statement' +``` + +**Purpose:** Get company income statement data + +--- + +### 7.4 GET v1/markets/stock/modules (cashflow-statement) - Cash Flow Statement + +**Parameters:** +- `ticker` (required, String): Stock symbol + +**curl example:** +```bash +curl --request GET \ + --url '{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/modules?ticker=AAPL&module=cashflow-statement' +``` + +**Purpose:** Get company cash flow statement data + +--- + +## Usage Flow Examples + +### Example 1: Find and Get Real-time Stock Data + +```bash +# 1. Search company +GET /v1/markets/search?search=Apple + +# 2. Get real-time quote +GET /v1/markets/quote?ticker=AAPL&type=STOCKS + +# 3. Get detailed information +GET /v1/markets/stock/modules?ticker=AAPL&module=asset-profile +``` + +### Example 2: Analyze Stock Investment Value + +```bash +# 1. Get financial data +GET /v1/markets/stock/modules?ticker=AAPL&module=financial-data + +# 2. Get earnings data +GET /v1/markets/stock/modules?ticker=AAPL&module=earnings +``` + +--- + +## Usage Tips + +### 1. Batch Query Optimization +```bash +# Get data for multiple stocks at once (snapshots endpoint) via Gateway +curl -X GET "{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/quotes?ticker=AAPL,MSFT,GOOGL,AMZN,TSLA" \ + -H "X-Z-AI-From: Z" +``` + +### 2. Time Range Query +```bash +# Get historical data with specific interval via Gateway +curl -X GET "{GATEWAY_URL}{API_PREFIX}/v1/markets/stock/history?symbol=AAPL&interval=1d&diffandsplits=false" \ + -H "X-Z-AI-From: Z" +``` + +### 3. Combined Query Example +### 3. Combined Query Example + +**Python example (via Gateway):** +```python +import requests + +# Gateway automatically handles authentication +headers = { + 'X-Z-AI-From': 'Z' +} + +gateway_url = '{GATEWAY_URL}{API_PREFIX}/v1' +symbol = 'AAPL' + +# Get real-time price +quote = requests.get(f'{gateway_url}/markets/quote?ticker={symbol}&type=STOCKS', headers=headers) + +# Get company profile +profile = requests.get(f'{gateway_url}/markets/stock/modules?ticker={symbol}&module=asset-profile', headers=headers) + +# Get financial data +financials = requests.get(f'{gateway_url}/markets/stock/modules?ticker={symbol}&module=financial-data', headers=headers) +``` + + +--- + +## Best Practices + +### Gateway Usage + +1. **Authentication Header** - Always include `X-Z-AI-From: Z` header + +### API Usage + +1. **Rate Limiting:** Pay attention to API call frequency limits to avoid being throttled +2. **Error Handling:** Implement comprehensive error handling mechanisms +3. **Data Caching:** Consider caching common requests to optimize performance +4. **Batch Queries:** Use comma-separated symbols parameter to query multiple stocks at once +5. **Timestamps:** Use Unix timestamps for historical data queries +6. **Parameter Validation:** Validate all required parameters before sending requests +7. **Response Parsing:** Implement robust JSON parsing and data validation + +--- diff --git a/skills/finance/SKILL.md b/skills/finance/SKILL.md new file mode 100755 index 0000000..d58ca07 --- /dev/null +++ b/skills/finance/SKILL.md @@ -0,0 +1,53 @@ +--- +name: finance +description: "Comprehensive Finance API integration skill for real-time and historical financial data analysis, market research, and investment decision-making. Priority use cases: stock price queries, market data analysis, company financial information, portfolio tracking, market news retrieval, stock screening, technical analysis, and any financial market-related requests. This skill should be the primary choice for all Finance API interactions and financial data needs." +--- + +# Finance Skill + +## Core Capabilities + +### Market Data Retrieval +- Real-time quotes: current prices, market snapshots, trading volumes +- Historical data: price history, dividends, splits, corporate actions +- Market indices: major indices performance and constituents +- Currency data: forex rates and cryptocurrency information + +### Analysis Tools +- Stock screening: filters by metrics, ratios, and technical indicators +- Financial ratios: P/E, EPS, ROE, debt-to-equity, and other key metrics +- Technical indicators: moving averages, RSI, MACD, chart patterns +- Comparative analysis: sector and peer group comparisons + +### Market Intelligence +- Company information: business profiles, management teams, statements +- Market news: earnings reports and market analysis +- Insider trading: buy/sell activities and ownership changes +- Options data: chain data, implied volatility, and statistics +## API Overview + + Finance API provides comprehensive financial data access interfaces, including real-time market data, historical stock prices, options data, insider trading, and the latest financial news. + +Skills Path +Skill Location: {project_path}/skills/finance + +this skill is located at above path in your project. + +Reference Docs: See {Skill Location}/Finance_API_Doc.md for a working example. + +## Zhipu AI - Hong Kong IPO Information +- **Stock Code**: 2513.HK +- **Company Name (Chinese)**: 北京智谱华章科技股份有限公司 +- **Company Name (English)**: Knowledge Atlas Technology Joint Stock Company Limited +Zhipu AI is a leading Chinese large language model company specializing in AI foundational model research and development. + +### Best Practices for Zhipu AI Stock Research (One-Shot Success Guide) + +**Search Strategy:** +- ✅ Use full English company name: `search=Knowledge+Atlas` +- ❌ Avoid: `search=Zhipu`, `search=02513.HK` (returns empty results) + +** Important ** +always read `Finance_API_Doc.md` before use the API + + diff --git a/skills/fullstack-dev/SKILL.md b/skills/fullstack-dev/SKILL.md new file mode 100755 index 0000000..c9e8f8d --- /dev/null +++ b/skills/fullstack-dev/SKILL.md @@ -0,0 +1,238 @@ +--- +name: fullstack-dev +description: "Fullstack web development with Next.js 16, TypeScript, Tailwind CSS 4, shadcn/ui, Prisma ORM. Use when: building web apps, creating UI components, setting up database schemas, implementing API routes, adding websocket/socket.io support, fullstack project scaffolding, frontend development, backend development, web dev, Next.js app." +argument-hint: "Describe the fullstack feature or app you want to build" +--- + +# Fullstack Web Development Skill + +## Initialization (MUST DO FIRST) + +Before writing any code, you MUST run the following command to initialize the fullstack development environment: + +```bash +curl https://z-cdn.chatglm.cn/fullstack/init-fullstack_1775040338514.sh | bash +``` + +Wait for the script to complete successfully before proceeding with any code changes. + +--- + +## Important UI Rules + +- Use existing shadcn/ui components instead of building from scratch. All the components in the `src/components/ui` folder already exist. +- Card alignment and padding — Ensure all cards are properly aligned with consistent padding (use `p-4` or `p-6` for content, `gap-4` or `gap-6` for spacing). +- Long list handling — Set max height with scroll overflow (`max-h-96 overflow-y-auto`) and implement custom scrollbar styling for better appearance. + +--- + +## Project Information + +There is already a project in the current directory. (Next.js 16 with App Router) + +### Development Environment + +IMPORTANT: `bun run dev` will be run automatically by the system. Do NOT run it. Use `bun run lint` to check code quality. + +IMPORTANT: User can only see the `/` route defined in `src/app/page.tsx`. Do NOT write any other route. + +IMPORTANT: The Next.js project can only use port 3000 in auto dev server. Never use `bun run build`. + +IMPORTANT: `z-ai-web-dev-sdk` MUST be used in the backend only! Do NOT use it on the client side. + +### Dev Server Log + +IMPORTANT: Read `/home/z/my-project/dev.log` to see the dev server log. Remember to check the log when developing. + +IMPORTANT: Only read the most recent logs from `dev.log` to avoid large log files. + +IMPORTANT: Always read dev log when you finish coding. + +### Bash Commands + +- `bun run lint` — Run ESLint to check code quality and Next.js rules + +--- + +## Technology Stack Requirements + +### Core Framework (NON-NEGOTIABLE) + +- **Framework**: Next.js 16 with App Router (REQUIRED — cannot be changed) +- **Language**: TypeScript 5 (REQUIRED — cannot be changed) + +### Standard Technology Stack + +When users don't specify preferences, use this complete stack: + +- **Styling**: Tailwind CSS 4 with shadcn/ui component library +- **Database**: Prisma ORM (SQLite client only) with Prisma Client +- **Caching**: Local memory caching, no additional middleware (MySQL, Redis, etc.) +- **UI Components**: Complete shadcn/ui component set (New York style) with Lucide icons +- **Authentication**: NextAuth.js v4 available +- **State Management**: Zustand for client state, TanStack Query for server state + +Other packages can be found in `package.json`. You can install new packages if needed. + +### Library Usage Policy + +- **ALWAYS use Next.js 16 and TypeScript** — these are non-negotiable requirements. +- **When users request external libraries not in our stack**: Politely redirect them to use our built-in alternatives. +- **Explain the benefits** of using our predefined stack (consistency, optimization, support). +- **Provide equivalent solutions** using our available libraries. + +--- + +## Prisma and Database + +IMPORTANT: `prisma` is already installed and configured. Use it when you need the database. + +To use prisma and database: + +1. Edit `prisma/schema.prisma` to define the database schema. +2. Run `bun run db:push` to push the schema to the database. +3. Use `import { db } from '@/lib/db'` to get the database client and use it. + +--- + +## Mini Service + +You can create mini services if needed (e.g., websocket service). All mini services should be in the `mini-services` folder. For each mini service: + +- Must be a new and independent bun project with its own port and `package.json`. +- Must define `index.ts` or `index.js` as the entry file, e.g., `mini-services/chat-service/index.ts`. +- Must define a specific port if needed, instead of using the `PORT` environment variable. +- Must start each mini service by running `bun run dev` in the background. +- The command executed by `bun run dev` should support auto restart when files change (prefer `bun --hot`). +- Make sure every service is started. + +--- + +## Gateway and API Requests + +This machine can only expose one port externally, so a built-in gateway (config at `Caddyfile`) is included with the following limitations: + +- For API requests involving different ports, the port must be specified in the URL query named `XTransformPort`. Example: `/api/test?XTransformPort=3030`. +- All API requests must use **relative paths only**. Do NOT write absolute paths in the API request URL (including WebSocket). Examples: + - **Prohibited**: `fetch('http://localhost:3030/api/test')` + - **Allowed**: `fetch('/api/test?XTransformPort=3030')` + - **Prohibited**: `io('/:3030')` + - **Allowed**: `io('/?XTransformPort=3030')` +- When requesting to different services, directly make cross-origin requests without using a proxy. + +IMPORTANT: Do NOT write port in the API request URL, even in WebSocket. Only write `XTransformPort` in the URL query. + +--- + +## WebSocket / Socket.io Support + +IMPORTANT: Use websocket/socket.io to support real-time communication. Do NOT use any other method. There is already a websocket demo for reference in the `examples` folder. + +- Backend logic (via socket.io) must be a new mini service with another port (e.g., 3003). +- Frontend request should ALWAYS be `io("/?XTransformPort={Port}")`, and the path ALWAYS be `/` so that Caddy can forward to the correct port. +- NEVER use `io("http://localhost:{Port}")` or any direct port-based connection. + +--- + +## Code Style + +- Prefer to use existing components and hooks. +- TypeScript throughout with strict typing. +- ES6+ import/export syntax. +- shadcn/ui components preferred over custom implementations. +- Use `'use client'` and `'use server'` for client and server side code. +- The Prisma schema primitive type cannot be a list. +- Put the Prisma schema in the `prisma` folder. +- Put the db file in the `db` folder. + +--- + +## Styling + +1. Use the shadcn/ui library unless the user specifies otherwise. +2. Avoid using indigo or blue colors unless specified in the user's request. +3. MUST generate responsive designs. +4. The Code Project is rendered on top of a white background. If a different background color is needed, use a wrapper element with a background color Tailwind class. + +--- + +## UI/UX Design Standards + +### Visual Design + +- **Color System**: Use Tailwind CSS built-in variables (`bg-primary`, `text-primary-foreground`, `bg-background`). +- **Color Restriction**: NO indigo or blue colors unless explicitly requested. +- **Theme Support**: Implement light/dark mode with `next-themes`. +- **Typography**: Consistent hierarchy with proper font weights and sizes. + +### Responsive Design (MANDATORY) + +- **Mobile-First**: Design for mobile, then enhance for desktop. +- **Breakpoints**: Use Tailwind responsive prefixes (`sm:`, `md:`, `lg:`, `xl:`). +- **Touch-Friendly**: Minimum 44px touch targets for interactive elements. + +### Layout (MANDATORY) + +- **Sticky Footer Required**: If a `footer` exists, it MUST stick to the bottom of the viewport when content is shorter than one screen height (no floating/empty gap below). +- **Natural Push on Overflow**: When content exceeds the viewport height, the footer MUST be pushed down naturally (never overlay or cover content). +- **Recommended Implementation (Tailwind)**: Use a root wrapper with `min-h-screen flex flex-col`, and apply `mt-auto` to the `footer`. +- **Mobile Safe Area**: On devices with safe areas (e.g., iOS), the footer MUST respect bottom safe area insets when applicable. + +### Accessibility (MANDATORY) + +- **Semantic HTML**: Use `main`, `header`, `nav`, `section`, `article`. +- **ARIA Support**: Proper roles, labels, and descriptions. +- **Screen Readers**: Use `sr-only` class for screen reader content. +- **Alt Text**: Descriptive alt text for all images. +- **Keyboard Navigation**: Ensure all elements are keyboard accessible. + +### Interactive Elements + +- **Loading States**: Show spinners/skeletons during async operations. +- **Error Handling**: Clear, actionable error messages. +- **Feedback**: Toast notifications for user actions. +- **Animations**: Subtle Framer Motion transitions (hover, focus, page transitions). +- **Hover Effects**: Interactive feedback on all clickable elements. + +### Sandbox Preview Instructions (CRITICAL) + +This project runs in a restricted cloud sandbox environment. + +- **NEVER** instruct the user to visit `http://localhost:3000`, `127.0.0.1`, or any local ports directly. These addresses are internal and not accessible to the user. +- **ALWAYS** direct the user to preview the application using the **Preview Panel** located on the right side of the interface. +- **ALWAYS** inform the user about how to view the application externally based on their platform: + - If they are using the web interface, tell them they can click the **"Open in New Tab"** button above the Preview Panel to view it in a separate browser tab. + - If they are communicating through an IM (Instant Messaging) platform, provide them directly with the generated preview link. + +### Post-Launch Self-Verification with Agent Browser (MANDATORY) + +When the Next.js project has started successfully (dev server running on port 3000 without fatal errors in `/home/z/my-project/dev.log`), you MUST NOT consider the task complete based on a clean build alone. A passing lint and a running server do **not** prove the website actually works for the user. + +You MUST use **Agent Browser** to perform end-to-end self-verification before reporting completion: + +1. **Open the page** + - Use Agent Browser to navigate to the `/` route (the only user-visible route). + - Wait for the page to fully load and capture the rendered result. + +2. **Verify it renders, not just responds** + - Confirm the page is visually rendered (no blank/white screen, no error boundary, no hydration crash). + - Cross-check against `/home/z/my-project/dev.log` for any runtime errors, failed API calls, or hydration mismatches that appeared during the visit. + +3. **Verify core interactivity (the golden path)** + - Exercise the primary user flows you just built: click the main buttons, submit the key forms, trigger navigation/tabs/modals, and confirm each produces the expected result. + - For data-driven features, confirm the frontend actually receives and displays backend/API data (not just an empty skeleton or a loading spinner that never resolves). + - For real-time features (WebSocket/socket.io), confirm messages flow end-to-end. + +4. **Check responsiveness and the sticky footer** + - Verify the layout holds on both mobile and desktop widths. + - Confirm the footer sticks to the bottom on short pages and is pushed down naturally on long pages (no overlap, no floating gap). + +5. **Fix and re-verify** + - If Agent Browser surfaces any broken interaction, console/runtime error, missing data, or layout defect, you MUST fix the root cause and re-run the self-verification loop. + - Repeat until the page loads cleanly **and** every core interaction works. + +6. **Report honestly** + - Only after Agent Browser confirms the site is interactive and runnable may you report the task as done. + - If a specific flow genuinely cannot be verified in the browser, say so explicitly rather than claiming success. + +**CRITICAL:** "It compiles" / "the server is up" is never sufficient evidence of completion. Browser-verified interactivity is the required standard of done. diff --git a/skills/get-fortune-analysis/SKILL.md b/skills/get-fortune-analysis/SKILL.md new file mode 100755 index 0000000..0f67224 --- /dev/null +++ b/skills/get-fortune-analysis/SKILL.md @@ -0,0 +1,370 @@ +--- +name: get-fortune-analysis +description: 生成视觉华丽、内容详实、具有仪式感的流年运势报告(流金星象风格)。 +--- +# Skill Name: get-fortune-analysis +# Version: 4.1.0 +# Description: 生成视觉华丽、内容详实、具有仪式感的流年运势报告(流金星象风格)。 + +## 1. Input Parameters +| Parameter | Type | Description | +| :--- | :--- | :--- | +| `birth_year`, `birth_month`, `birth_day`, `birth_hour` | Integer | 用户出生时间 | +| `focus_type` | String | (可选) "事业", "财运", "情感" | + +## 2. Workflow + +### Step 1: Calculation (Python) +调用 `get_cyber_divination_data` 获取 `bazi` (八字基础) 和 `fortune` (流年十神) 数据。 + +### Step 2: Reasoning (深度分析模式) +基于 `bazi` 和 `fortune` 进行多维度推理。 +**文案要求:** +* **口吻**:温暖、笃定、专业,类似资深命理师或星座专家的语气。 +* **结构**: + 1. **年度关键词**:4个字,精准概括全年基调(如“破茧成蝶”)。 + 2. **核心能量**:解释流年十神对用户命局的深层影响(30-50字)。 + 3. **事业/财运**:具体的职场发展路径和财富机遇分析(50-80字)。 + 4. **情感/人际**:人际关系模式与情感走向分析(50-80字)。 + +### Step 3: JSON Output +生成适配前端的 JSON 数据。 + +```json +{ + "fortune_report": { + "score": 88, + "keyword": "灵感迸发 · 贵人引路", + "user_tag": "丁火 (身弱)", + "stars": { + "c": "★★★★☆", + "w": "★★★☆☆", + "l": "★★★★★" + }, + "analysis": { + "overview": "2026 丙午流年,火气旺盛,对你而言是充满灵性与机遇的一年。虽然竞争压力(比劫)增大,但也激活了你命局中的‘印星’能量。这意味着今年你的直觉力、学习力将达到巅峰,是沉淀自我、弯道超车的最佳时机。", + "career": "今年不适合盲目扩张,适合‘深耕’。职场上会遇到强有力的女性贵人或资深导师,给你带来关键性的指点。若从事创意、咨询、教育行业,今年极易出成果。切记:多听少说,以柔克刚。", + "love": "情感方面,桃花星悄然绽放。单身者极易在学习场所、图书馆或艺术展上邂逅精神契合的伴侣;有伴侣者,今年是进行深度沟通、解决历史遗留问题的破冰之年,关系将升华到精神层面。" + } + } +}``` + + +### 2. 前端展示代码 (`result_card.html`) + +*修改点:在首页(`ritual-layer`)增加了动态生成漂浮二进制代码的逻辑。代码粒子是半透明的金色/白色,缓慢上升并消散,营造神秘的数据空间感。* + +```html + + + + + + 2026 流年运势书 + + + + + +
+ +
+
+ +
+
+ + + + + +
+ +
+
+ + + +
+
+
长按开启 2026 运势书
+
+
+ +
+
+
FORTUNE REPORT 2026
+
0
+
读取中...
+
+ 日主:-- +
+
+
+
事业前程 Career★★★★☆
+
财富机缘 Wealth★★★☆☆
+
情感关系 Love★★★★★
+
+
+
年度总批 Overview
+
正在解析星盘数据...
+
+
+
事业与财富
+
...
+
+
+
情感与建议
+
...
+
+ +
+
+ + + + \ No newline at end of file diff --git a/skills/get-fortune-analysis/lunar_python.py b/skills/get-fortune-analysis/lunar_python.py new file mode 100755 index 0000000..2aef1b2 --- /dev/null +++ b/skills/get-fortune-analysis/lunar_python.py @@ -0,0 +1,91 @@ +pip install lunar_python +import datetime +from lunar_python import Lunar, Solar + +def get_cyber_divination_data(birth_year, birth_month, birth_day, birth_hour=0, birth_minute=0): + """ + 赛博算命核心算法 v2.1 + - 输出适配 HTML 前端渲染 + """ + + # --- 基础配置 (简化版) --- + GAN_WU_XING = {"甲": "木", "乙": "木", "丙": "火", "丁": "火", "戊": "土", "己": "土", "庚": "金", "辛": "金", "壬": "水", "癸": "水"} + ZHI_WU_XING = {"子": "水", "丑": "土", "寅": "木", "卯": "木", "辰": "土", "巳": "火", "午": "火", "未": "土", "申": "金", "酉": "金", "戌": "土", "亥": "水"} + RELATIONSHIP = { + "木": {"木": "同", "火": "生", "土": "克", "金": "被克", "水": "被生"}, + "火": {"木": "被生", "火": "同", "土": "生", "金": "克", "水": "被克"}, + "土": {"木": "被克", "火": "被生", "土": "同", "金": "生", "水": "克"}, + "金": {"木": "克", "火": "被克", "土": "被生", "金": "同", "水": "生"}, + "水": {"木": "生", "火": "克", "土": "被克", "金": "被生", "水": "同"} + } + TEN_GODS = { + "同_同": "比肩 (Friend)", "异_同": "劫财 (Rob)", + "同_生": "食神 (Artist)", "异_生": "伤官 (Rebel)", + "同_克": "偏财 (Windfall)", "异_克": "正财 (Salary)", + "同_被克": "七杀 (7-Killings)", "异_被克": "正官 (Officer)", + "同_被生": "偏印 (Owl)", "异_被生": "正印 (Seal)" + } + + # --- 1. 排盘 --- + solar = Solar.fromYmdHms(birth_year, birth_month, birth_day, birth_hour, birth_minute, 0) + lunar = Lunar.fromSolar(solar) + ba_zi = lunar.getEightChar() + day_master = ba_zi.getDayGan() + dm_element = GAN_WU_XING[day_master] + dm_yin_yang = "阳" if day_master in ["甲", "丙", "戊", "庚", "壬"] else "阴" + + # --- 2. 辅助函数 --- + def get_ten_god(target_gan): + if not target_gan: return "" + target_element = GAN_WU_XING.get(target_gan) or ZHI_WU_XING.get(target_gan) + rel = RELATIONSHIP[dm_element].get(target_element, "同") + + # 简化阴阳判定 + target_yy = "阳" if target_gan in ["甲", "丙", "戊", "庚", "壬", "寅", "申", "巳", "亥"] else "阴" + is_same = (dm_yin_yang == target_yy) + key = f"{'同' if is_same else '异'}_{rel if rel in ['生','克','被生','被克'] else '同'}" + return TEN_GODS.get(key, "未知") + + # --- 3. 旺衰硬规则 --- + score = 0 + month_zhi = ba_zi.getMonthZhi() + m_ele = ZHI_WU_XING[month_zhi] + + # 得令 (+40) + if RELATIONSHIP[dm_element][m_ele] in ["同", "被生"]: score += 40 + elif RELATIONSHIP[dm_element][m_ele] == "被克": score -= 20 + + # 得地 (+15/each) + for zhi in [ba_zi.getYearZhi(), ba_zi.getDayZhi(), ba_zi.getTimeZhi()]: + if ZHI_WU_XING[zhi] == dm_element: score += 15 + + body_strength = "身强 (Strong)" if score >= 40 else "身弱 (Weak)" + strength_cn = "身强" if score >= 40 else "身弱" + + # --- 4. 流年 --- + current_year = datetime.datetime.now().year + # 简单的流年计算 (以立春为界需要更复杂逻辑,这里简化取当年农历年干支) + # 修正:直接用 Lunar 获取当年的干支 + current_lunar = Lunar.fromYmd(current_year, 6, 1) + annual_gan = current_lunar.getYearGan() + annual_zhi = current_lunar.getYearZhi() + + annual_god = get_ten_god(annual_gan) + + return { + "meta": { + "solar_date": f"{birth_year}-{birth_month}-{birth_day}", + "lunar_date": f"{lunar.getYearInChinese()}年{lunar.getMonthInChinese()}月{lunar.getDayInChinese()}" + }, + "bazi": { + "day_master": day_master, + "element": dm_element, + "strength": strength_cn, + "score": score + }, + "fortune": { + "current_year": f"{current_year} ({annual_gan}{annual_zhi})", + "year_god": annual_god.split(" ")[0], # 只取中文名,如 "七杀" + "lucky_direction": lunar.getDayPositionCaiDesc() # 财神方位 + } + } \ No newline at end of file diff --git a/skills/gift-evaluator/SKILL.md b/skills/gift-evaluator/SKILL.md new file mode 100755 index 0000000..e392744 --- /dev/null +++ b/skills/gift-evaluator/SKILL.md @@ -0,0 +1,83 @@ +--- +name: gift-evaluator +description: The PRIMARY tool for Spring Festival gift analysis and social interaction generation. Use this skill when users upload photos of gifts (alcohol, tea, supplements, etc.) to inquire about their value, authenticity, or how to respond socially. Integrates visual perception, market valuation, and HTML card generation. +license: Internal Tool +--- + +This skill transforms the assistant into an "AI Gift Appraiser" (春节礼品鉴定师). It bridges the gap between raw visual data and complex social context. It is designed to handle the full lifecycle of a user's request: identifying the object, determining its market and social value, and producing a shareable, gamified HTML artifact. + +## Agent Thinking Strategy + +Before and during the execution of tools, maintain a "High EQ" and "Market-Savvy" mindset. You are not just identifying objects; you are decoding social relationships. + +1. **Visual Extraction (The Eye)**: + * Call the vision tool to get a raw description. + * **CRITICAL**: Read the raw description carefully. Extract specific entities: Brand names (e.g., "Moutai", "Dior"), Vintages, Packaging details (e.g., "Dusty bottle" implies old stock, "Gift box" implies formality). + +2. **Valuation Logic (The Brain)**: + * **Price Anchoring**: Use search tools to find the *current* market price. + * **Social Labeling**: Classify the gift based on price and intent: + * `luxury`: High value (> ¥1000), "Hard Currency". + * `standard`: Festive, safe choices (¥200 - ¥1000). + * `budget`: Practical, funny, or cheap (< ¥200). + +3. **Creative Synthesis (The Mouth)**: + * **Deep Critique**: Generate a "Roast" (毒舌点评) of **at least 50 words**. It must combine the visual details (e.g., dust, packaging color) with the price reality. Be spicy but insightful. + * **Structured Strategy**: You must structure the "Thank You Notes" and "Return Gift Ideas" into JSON format for the UI to render. + +## Tool Usage Guidelines +### 1. The Perception Phase (Visual Analysis) +Purpose: Utilizing VLM skills to conduct a multi-dimensional visual decomposition of the uploaded product image. This process automatically identifies and extracts structured data including Brand Recognition, Product Style, Packaging Design, and Aesthetic Category. + +**Output Analysis**: + +* The tool returns a raw string content. Read it to extract keywords for the next step. + +### 2. The Valuation Phase (Search) + +**Purpose**: Validate the product's worth. +**Command**:search "EXTRACTED_KEYWORDS + price + review" + + +### 3. The Content Structuring Phase (Reasoning) + +**Purpose**: Prepare the data for the HTML generator. **Do not call a tool here, just think and format strings.** + +1. **Construct `thank_you_json**`: Create 3 distinct styles of private messages. +* *Format*: `[{"style": "Style Name", "content": "Message..."}]` +* *Requirement*: +* Style 1: "Decent/Formal" (for elders/bosses). +* Style 2: "Friendly/Warm" (for peers/relatives). +* Style 3: "Humorous/Close" (for best friends). + + +2. **Construct `return_gift_json**`: Analyze 4 potential giver personas. +* *Format*: `[{"target": "If giver is...", "item": "Suggest...", "reason": "Why..."}]` +* *Requirement*: Suggestions must include Age/Gender/Relation analysis (e.g., "If giver is an elder male", "If giver is a peer female"). +* *Value Logic*: Adhere to the principle of Value Reciprocity. The return gift's value should primarily match the received gift's value, while adjusting slightly based on the giver's status (e.g., seniority or intimacy). + + +### 4. The Creation Phase (Render) + +**Purpose**: Package the analysis into a modern, interactive HTML card. +**HTML Generation**: + * *Constraint*: The `image_url` parameter in the Python command MUST be the original absolute path.`output_path` must be the full path. + * *Command*: + ```bash + python3 html_tools.py generate_gift_card \ + --product_name "EXTRACTED_NAME" \ + --price "ESTIMATED_PRICE" \ + --evaluation "YOUR_LONG_AND_SPICY_CRITIQUE" \ + --thank_you_json '[{"style":"...","content":"..."}]' \ + --return_gift_json '[{"target":"...","item":"...","reason":"..."}]' \ + --vibe_code "luxury|standard|budget" \ + --image_url "IMAGE_FILE_PATH" \ + --output_path "TARGET_FILE_PATH" + ``` + +## Operational Rules + +1. **JSON Formatting**: The `thank_you_json` and `return_gift_json` arguments MUST be valid JSON strings using double quotes. Do not wrap them in code blocks inside the command. +2. **Critique Depth**: The `evaluation` text must be rich. Don't just say "It's expensive." Say "This 2018 vintage shows your uncle raided his personal cellar; the label wear proves it's real." +3. **Vibe Consistency**: Ensure `vibe_code` matches the `price` assessment. +4. **Final Output**: Always present the path to the generated HTML file. diff --git a/skills/gift-evaluator/html_tools.py b/skills/gift-evaluator/html_tools.py new file mode 100755 index 0000000..3353aee --- /dev/null +++ b/skills/gift-evaluator/html_tools.py @@ -0,0 +1,268 @@ +import os +import argparse +import json +import html +import base64 +import mimetypes +import urllib.request + +def generate_gift_card(product_name, price, evaluation, thank_you_json, return_gift_json, vibe_code, image_url, output_path="gift_card_result.html"): + """ + 生成现代风格的交互式礼品鉴定卡片。 + """ + + # --- 图片转 Base64 逻辑 (保持上一步功能) --- + final_image_src = image_url + try: + image_data = None + mime_type = None + if image_url.startswith(('http://', 'https://')): + req = urllib.request.Request(image_url, headers={'User-Agent': 'Mozilla/5.0'}) + with urllib.request.urlopen(req, timeout=10) as response: + image_data = response.read() + mime_type = response.headers.get_content_type() + else: + if os.path.exists(image_url): + mime_type, _ = mimetypes.guess_type(image_url) + with open(image_url, "rb") as f: + image_data = f.read() + + if image_data: + if not mime_type: mime_type = "image/jpeg" + b64_str = base64.b64encode(image_data).decode('utf-8') + final_image_src = f"data:{mime_type};base64,{b64_str}" + + except Exception as e: + print(f"⚠️ 图片转换 Base64 失败,使用原链接。错误: {e}") + + # --- 1. 数据解析 --- + try: + thank_you_data = json.loads(thank_you_json) + except: + thank_you_data = [{"style": "通用版", "content": thank_you_json}] + + try: + return_gift_data = json.loads(return_gift_json) + except: + return_gift_data = [{"target": "通用建议", "item": return_gift_json, "reason": "万能回礼"}] + + # --- 2. 风格配置 --- + styles = { + "luxury": { + "page_bg": "bg-neutral-900", + "card_bg": "bg-neutral-900/80 backdrop-blur-xl border border-white/10", + "text_main": "text-white", "text_sub": "text-neutral-400", + "accent": "text-amber-400", "tag_bg": "bg-amber-400/20 text-amber-400", + "btn_hover": "hover:bg-amber-400 hover:text-black", + "img_bg": "bg-neutral-800" # 图片衬底色 + }, + "standard": { + "page_bg": "bg-stone-200", + "card_bg": "bg-white/95 backdrop-blur-xl border border-stone-200", + "text_main": "text-stone-800", "text_sub": "text-stone-500", + "accent": "text-red-600", "tag_bg": "bg-red-50 text-red-600", + "btn_hover": "hover:bg-red-600 hover:text-white", + "img_bg": "bg-stone-100" + }, + "budget": { + "page_bg": "bg-yellow-50", + "card_bg": "bg-white border-4 border-black shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]", + "text_main": "text-black", "text_sub": "text-gray-600", + "accent": "text-blue-600", "tag_bg": "bg-black text-white", + "btn_hover": "hover:bg-blue-600 hover:text-white", + "img_bg": "bg-gray-200" + } + } + st = styles.get(vibe_code, styles["standard"]) + if "img_bg" not in st: st["img_bg"] = "bg-black/5" # 兼容兜底 + + # --- 3. 辅助逻辑 --- + is_dark_mode = "text-white" in st['text_main'] + bubble_bg = "bg-white/10 border-white/10" if is_dark_mode else "bg-black/5 border-black/5" + bubble_hover = "hover:bg-white/20" if is_dark_mode else "hover:bg-black/10" + divider_color = "border-white/20" if is_dark_mode else "border-black/10" + + # --- 4. HTML 构建 --- + thank_you_html = "" + for item in thank_you_data: + thank_you_html += f""" +
+
+ {item['style']} + + + 点击复制 + +
+

{item['content']}

+
+ ✓ 已复制 +
+
+ """ + + return_gift_html = "" + for item in return_gift_data: + return_gift_html += f""" +
+
+
+
{item['target']}
+
+
{item['item']}
+
{item['reason']}
+
+ """ + + html_content = f""" + + + + + + 礼品鉴定报告 + + + + + +
+ +
+ +
+ + +
+ +
+
+ AI Gift Analysis +
+

{product_name}

+
+ 当前估值 + {price} +
+
+
+ +
+
+ +

+ + 专家鉴定评价 +

+ +
+ {evaluation} +
+ +
+
AI
+
+ 首席鉴定官 + Verified Analysis +
+
+
+
+ +
+
+
+
+ +
+
+

私信回复话术

+

高情商回复,点击卡片即可复制

+
+
+
+ {thank_you_html} +
+
+ +
+
+
+ +
+
+

推荐回礼策略

+

基于价格区间的最优解

+
+
+
+ {return_gift_html} +
+
+ +
+

Designed by AI Gift Agent • 春节特别版

+
+
+
+ + + + + """ + + try: + directory = os.path.dirname(output_path) + if directory: + os.makedirs(directory, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + f.write(html_content) + return os.path.abspath(output_path) + except Exception as e: + return f"Error saving HTML file: {str(e)}" + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Generate Gift Card HTML") + parser.add_argument("action", nargs="?", help="Action command") + parser.add_argument("--product_name", required=True) + parser.add_argument("--price", required=True) + parser.add_argument("--evaluation", required=True) + parser.add_argument("--thank_you_json", required=True) + parser.add_argument("--return_gift_json", required=True) + parser.add_argument("--vibe_code", required=True) + parser.add_argument("--image_url", required=True) + parser.add_argument("--output_path", required=True) + + args = parser.parse_args() + + result_path = generate_gift_card( + product_name=args.product_name, + price=args.price, + evaluation=args.evaluation, + thank_you_json=args.thank_you_json, + return_gift_json=args.return_gift_json, + vibe_code=args.vibe_code, + image_url=args.image_url, + output_path=args.output_path + ) + + print(f"HTML Card generated successfully: {result_path}") \ No newline at end of file diff --git a/skills/image-edit/LICENSE.txt b/skills/image-edit/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/image-edit/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/image-edit/SKILL.md b/skills/image-edit/SKILL.md new file mode 100755 index 0000000..1153bee --- /dev/null +++ b/skills/image-edit/SKILL.md @@ -0,0 +1,896 @@ +--- +name: image-edit +description: Implement AI image editing and modification capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to edit existing images, create variations, modify visual content, redesign assets, or transform images based on text descriptions. Supports multiple image sizes and returns base64 encoded results. Also includes CLI tool for quick image editing. +license: MIT +--- + +# Image Edit Skill + +This skill guides the implementation of image editing and modification functionality using the z-ai-web-dev-sdk package and CLI tool, enabling intelligent transformation and editing of images based on text descriptions. + +## Skills Path + +**Skill Location**: `{project_path}/skills/image-edit` + +this skill is located at above path in your project. + +**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/image-edit.ts` for a working example. + +## Overview + +Image Edit allows you to build applications that modify, transform, and enhance existing images using AI models. Perfect for redesigning assets, creating variations, improving visual content, and transforming images based on textual descriptions. + +**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code. + +## SDK API Method + +The image editing functionality uses the following API method: + +```javascript +await zai.images.generations.edit({ + prompt: string, // Required: Description of the edit to apply + images: [{ url: string }], // Required: Array with image URL or base64 data URL + size?: string, // Optional: Output size (default: '1024x1024') + model?: string // Optional: Model name +}) +``` + +**Important**: The `images` parameter must be an array of objects with a `url` property, not a plain string. + +**API Endpoint**: `POST /images/generations/edit` + +**Returns**: `ImageGenerationResponse` with base64 encoded edited image + +## Prerequisites + +The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below. + +## Basic Image Editing + +### Simple Image Transformation + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function editImage(imageSource, editPrompt, outputPath, size = '1024x1024') { + const zai = await ZAI.create(); + + const response = await zai.images.generations.edit({ + prompt: editPrompt, + images: [{ url: imageSource }], // Array of objects with url property + size: size + }); + + const imageBase64 = response.data[0].base64; + + // Save edited image + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + console.log(`Edited image saved to ${outputPath}`); + return outputPath; +} + +// Usage - Using remote image URL +await editImage( + 'https://example.com/landscape.jpg', + 'Transform this landscape into a night scene with stars and moon', + './landscape_night.png' +); + +// Usage - Using local image converted to base64 +import { readFileSync } from 'fs'; +const imageBuffer = readFileSync('./photo.jpg'); +const base64Image = imageBuffer.toString('base64'); +const dataUrl = `data:image/jpeg;base64,${base64Image}`; + +await editImage( + dataUrl, + 'Change the cat to a dog, keep everything else the same', + './dog_version.png' +); +``` + +### Create Image Variations + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function createVariation(imageSource, baseDescription, variation, outputPath, size = '1024x1024') { + const zai = await ZAI.create(); + + // Combine base description with variation request + const prompt = `${baseDescription}, ${variation}`; + + const response = await zai.images.generations.edit({ + prompt: prompt, + images: [{ url: imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + return { + path: outputPath, + prompt: prompt, + variation: variation + }; +} + +// Usage - Create variations from original image +await createVariation( + 'https://example.com/headshot.jpg', + 'Professional headshot photo', + 'with blue background instead of gray', + './headshot_blue.png' +); + +await createVariation( + './smartphone.png', + 'Product photo of smartphone', + 'on wooden table instead of white background', + './product_wood.png' +); +``` + +### Multiple Image Sizes for Editing + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +// Supported sizes +const SUPPORTED_SIZES = [ + '1024x1024', // Square + '768x1344', // Portrait + '864x1152', // Portrait + '1344x768', // Landscape + '1152x864', // Landscape + '1440x720', // Wide landscape + '720x1440' // Tall portrait +]; + +async function editImageWithSize(imageSource, editPrompt, size, outputPath) { + if (!SUPPORTED_SIZES.includes(size)) { + throw new Error(`Unsupported size: ${size}. Use one of: ${SUPPORTED_SIZES.join(', ')}`); + } + + const zai = await ZAI.create(); + + const response = await zai.images.generations.edit({ + prompt: editPrompt, + images: [{ url: imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + return { + path: outputPath, + size: size, + fileSize: buffer.length + }; +} + +// Usage - Edit with different aspect ratios +await editImageWithSize( + './logo.png', + 'Redesign the logo to be more modern and minimalist', + '1024x1024', + './logo_redesigned.png' +); + +await editImageWithSize( + 'https://example.com/portrait.jpg', + 'Transform the portrait to landscape orientation, sunset lighting', + '1344x768', + './portrait_landscape.png' +); +``` + +## CLI Tool Usage + +The z-ai CLI tool provides a convenient way to edit images directly from the command line. + +### Basic CLI Usage + +```bash +# Edit image with full options +z-ai image-edit --prompt "Change the background to sunset colors" --image "./photo.png" --output "./edited.png" + +# Short form +z-ai image-edit -p "Make it darker and moodier" -i "./original.jpg" -o "./moody.png" + +# Specify output size +z-ai image-edit -p "Redesign in modern style" -i "./design.png" -o "./modern.png" -s 1344x768 + +# Using remote image URL +z-ai image-edit -p "Convert to landscape orientation" -i "https://example.com/photo.png" -o "./landscape.png" -s 1344x768 +``` + +### CLI Parameters + +- `--prompt, -p`: **Required** - Description of the edit to apply +- `--image, -i`: **Required** - Original image URL or local file path +- `--output, -o`: **Required** - Output image file path (PNG format) +- `--size, -s`: Optional - Image size, default is 1024x1024 +- `--help, -h`: Optional - Display help information + +### Supported Sizes + +- `1024x1024`, `768x1344`, `864x1152`, `1344x768`, `1152x864`, `1440x720`, `720x1440` + +### CLI Use Cases for Image Editing + +```bash +# Redesign existing asset +z-ai image-edit -p "Redesign the logo with gradients and modern styling" -i "./logo.png" -o "./logo_v2.png" -s 1024x1024 + +# Change color scheme +z-ai image-edit -p "Change color scheme to blue and white, professional style" -i "./original.png" -o "./recolored.png" -s 1440x720 + +# Style transformation +z-ai image-edit -p "Transform to oil painting style, vibrant colors" -i "./photo.jpg" -o "./oil_painting.png" -s 1152x864 + +# Background replacement +z-ai image-edit -p "Replace background with modern office setting" -i "./portrait.png" -o "./new_background.png" -s 1344x768 + +# Lighting adjustment +z-ai image-edit -p "Adjust to golden hour lighting, warm tones" -i "./landscape.jpg" -o "./golden_hour.png" -s 1024x1024 + +# Element modification +z-ai image-edit -p "Replace the red car with a blue motorcycle" -i "./scene.png" -o "./modified.png" -s 1344x768 + +# Mood transformation +z-ai image-edit -p "Transform to dark moody atmosphere with dramatic lighting" -i "./bright.jpg" -o "./moody.png" -s 1440x720 + +# Using remote image URL +z-ai image-edit -p "Add a hat to the person" -i "https://example.com/photo.png" -o "./result.png" -s 1024x1024 +``` + +## Advanced Use Cases + +### Batch Image Editing + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +async function batchEditImages(editInstructions, outputDir, size = '1024x1024') { + const zai = await ZAI.create(); + + // Ensure output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const results = []; + + for (let i = 0; i < editInstructions.length; i++) { + try { + const instruction = editInstructions[i]; + const filename = `edited_${i + 1}.png`; + const outputPath = path.join(outputDir, filename); + + const response = await zai.images.generations.edit({ + prompt: instruction.prompt, + images: [{ url: instruction.imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + results.push({ + success: true, + instruction: instruction.prompt, + path: outputPath, + size: buffer.length + }); + + console.log(`✓ Edited: ${filename}`); + } catch (error) { + results.push({ + success: false, + instruction: editInstructions[i].prompt, + error: error.message + }); + + console.error(`✗ Failed: ${editInstructions[i].prompt} - ${error.message}`); + } + } + + return results; +} + +// Usage - Create multiple variations from the same image +const editInstructions = [ + { + imageSource: './original.jpg', + prompt: 'Change background to blue gradient' + }, + { + imageSource: './original.jpg', + prompt: 'Transform to black and white, high contrast' + }, + { + imageSource: './original.jpg', + prompt: 'Add sunset lighting effects' + } +]; + +const results = await batchEditImages(editInstructions, './edited-images'); +console.log(`Edited ${results.filter(r => r.success).length} images`); +``` + +### Image Editing Service + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; + +class ImageEditingService { + constructor(outputDir = './edited-images') { + this.outputDir = outputDir; + this.zai = null; + this.editHistory = []; + } + + async initialize() { + this.zai = await ZAI.create(); + + if (!fs.existsSync(this.outputDir)) { + fs.mkdirSync(this.outputDir, { recursive: true }); + } + } + + generateFilename(editPrompt) { + const hash = crypto + .createHash('md5') + .update(`${editPrompt}-${Date.now()}`) + .digest('hex') + .substring(0, 8); + + return `edited_${hash}.png`; + } + + async edit(imageSource, editPrompt, options = {}) { + const { + size = '1024x1024', + saveToHistory = true, + filename = null + } = options; + + const response = await this.zai.images.generations.edit({ + prompt: editPrompt, + images: [{ url: imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + + // Determine output path + const outputFilename = filename || this.generateFilename(editPrompt); + const outputPath = path.join(this.outputDir, outputFilename); + + fs.writeFileSync(outputPath, buffer); + + const result = { + path: outputPath, + imageSource: imageSource, + editPrompt: editPrompt, + size: size, + fileSize: buffer.length, + timestamp: new Date().toISOString() + }; + + // Save to history + if (saveToHistory) { + this.editHistory.push(result); + } + + return result; + } + + async createVariations(imageSource, basePrompt, variations, options = {}) { + const results = []; + + for (const variation of variations) { + const fullPrompt = `${basePrompt}, ${variation}`; + const result = await this.edit(imageSource, fullPrompt, options); + result.variation = variation; + results.push(result); + } + + return results; + } + + getEditHistory() { + return this.editHistory; + } + + clearHistory() { + this.editHistory = []; + } +} + +// Usage +const service = new ImageEditingService(); +await service.initialize(); + +// Single edit +const edited = await service.edit( + './original.jpg', + 'Transform to watercolor painting style', + { size: '1024x1024' } +); + +// Multiple variations from the same image +const variations = await service.createVariations( + 'https://example.com/product.png', + 'Professional product photo', + [ + 'with blue background', + 'with wooden surface', + 'with dramatic lighting' + ] +); + +console.log('Edit history:', service.getEditHistory()); +``` + +### Style Transfer and Transformation + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function applyStyleTransfer(imageSource, content, style, outputPath, size = '1024x1024') { + const zai = await ZAI.create(); + + const prompt = `${content} transformed into ${style} style, maintain composition and subject`; + + const response = await zai.images.generations.edit({ + prompt: prompt, + images: [{ url: imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + return { + path: outputPath, + content: content, + style: style + }; +} + +// Usage - Apply different styles to the same image +await applyStyleTransfer( + './portrait.jpg', + 'Portrait photograph', + 'oil painting', + './portrait_oil.png' +); + +await applyStyleTransfer( + 'https://example.com/city.jpg', + 'City landscape', + 'watercolor', + './city_watercolor.png' +); + +await applyStyleTransfer( + './product.png', + 'Product photo', + 'minimalist illustration', + './product_minimal.png' +); +``` + +### Element Replacement + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function replaceElement(imageSource, baseScene, replaceWhat, replaceWith, outputPath, size = '1024x1024') { + const zai = await ZAI.create(); + + const prompt = `${baseScene}, replace ${replaceWhat} with ${replaceWith}, keep everything else identical`; + + const response = await zai.images.generations.edit({ + prompt: prompt, + images: [{ url: imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + return { + path: outputPath, + modification: `${replaceWhat} → ${replaceWith}` + }; +} + +// Usage +await replaceElement( + './workspace.jpg', + 'Office workspace with laptop', + 'laptop', + 'desktop computer with dual monitors', + './workspace_desktop.png' +); + +await replaceElement( + 'https://example.com/living-room.jpg', + 'Living room interior with sofa', + 'blue sofa', + 'brown leather sofa', + './living_room_leather.png' +); +``` + +## Best Practices + +### 1. Effective Edit Prompts + +```javascript +function buildEditPrompt(baseDescription, modification, preserveElements = []) { + const components = [ + baseDescription, + modification + ]; + + if (preserveElements.length > 0) { + components.push(`keep ${preserveElements.join(', ')} unchanged`); + } + + components.push('maintain overall composition'); + + return components.filter(Boolean).join(', '); +} + +// Usage +const editPrompt = buildEditPrompt( + 'Professional headshot photo', + 'change background to modern office', + ['lighting', 'pose', 'expression'] +); + +// Result: "Professional headshot photo, change background to modern office, keep lighting, pose, expression unchanged, maintain overall composition" +``` + +### 2. Size Selection for Different Edit Types + +```javascript +function selectSizeForEdit(editType) { + const sizeMap = { + 'background-change': '1440x720', + 'style-transfer': '1024x1024', + 'color-adjustment': '1024x1024', + 'element-replacement': '1344x768', + 'composition-change': '1152x864', + 'portrait-edit': '768x1344', + 'landscape-edit': '1344x768' + }; + + return sizeMap[editType] || '1024x1024'; +} + +// Usage +const size = selectSizeForEdit('background-change'); +await editImage('Replace background with beach scene', './beach_bg.png', size); +``` + +### 3. Error Handling with Retry + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function safeEditImage(imageSource, editPrompt, size, outputPath, retries = 3) { + let lastError; + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const zai = await ZAI.create(); + + const response = await zai.images.generations.edit({ + prompt: editPrompt, + images: [{ url: imageSource }], + size: size + }); + + if (!response.data || !response.data[0] || !response.data[0].base64) { + throw new Error('Invalid response from image editing API'); + } + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + return { + success: true, + path: outputPath, + attempts: attempt + }; + } catch (error) { + lastError = error; + console.error(`Attempt ${attempt} failed:`, error.message); + + if (attempt < retries) { + // Wait before retry (exponential backoff) + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + return { + success: false, + error: lastError.message, + attempts: retries + }; +} +``` + +## Common Image Editing Use Cases + +1. **Background Replacement**: Change or remove backgrounds in photos +2. **Style Transformation**: Convert photos to paintings, illustrations, etc. +3. **Color Adjustment**: Change color schemes, saturation, mood +4. **Element Modification**: Replace or modify specific elements +5. **Composition Changes**: Adjust framing, orientation, layout +6. **Lighting Adjustments**: Modify lighting, shadows, highlights +7. **Asset Redesign**: Modernize or rebrand existing designs +8. **Quality Enhancement**: Improve overall visual quality +9. **Variation Creation**: Generate multiple versions of an image +10. **Format Conversion**: Transform between different styles or formats + +## Integration Examples + +### Express.js API Endpoint + +```javascript +import express from 'express'; +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +const app = express(); +app.use(express.json()); +app.use('/edited-images', express.static('edited-images')); + +let zaiInstance; +const outputDir = './edited-images'; + +async function initZAI() { + zaiInstance = await ZAI.create(); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } +} + +app.post('/api/edit-image', async (req, res) => { + try { + const { + imageSource, // URL or base64 data URL + editPrompt, + size = '1024x1024', + baseDescription = '' + } = req.body; + + if (!imageSource || !editPrompt) { + return res.status(400).json({ + error: 'imageSource and editPrompt are required' + }); + } + + // Combine base description with edit instruction + const fullPrompt = baseDescription + ? `${baseDescription}, ${editPrompt}` + : editPrompt; + + const response = await zaiInstance.images.generations.edit({ + prompt: fullPrompt, + images: [{ url: imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + + const filename = `edited_${Date.now()}.png`; + const filepath = path.join(outputDir, filename); + fs.writeFileSync(filepath, buffer); + + res.json({ + success: true, + imageUrl: `/edited-images/${filename}`, + editPrompt: fullPrompt, + size: size + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +app.post('/api/create-variations', async (req, res) => { + try { + const { + imageSource, // URL or base64 data URL + baseDescription, + variations, + size = '1024x1024' + } = req.body; + + if (!imageSource || !baseDescription || !variations || !Array.isArray(variations)) { + return res.status(400).json({ + error: 'imageSource, baseDescription and variations array are required' + }); + } + + const results = []; + + for (const variation of variations) { + const fullPrompt = `${baseDescription}, ${variation}`; + + const response = await zaiInstance.images.generations.edit({ + prompt: fullPrompt, + images: [{ url: imageSource }], + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + + const filename = `variation_${Date.now()}_${Math.random().toString(36).substr(2, 9)}.png`; + const filepath = path.join(outputDir, filename); + fs.writeFileSync(filepath, buffer); + + results.push({ + variation: variation, + imageUrl: `/edited-images/${filename}` + }); + } + + res.json({ + success: true, + results: results + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +initZAI().then(() => { + app.listen(3000, () => { + console.log('Image editing API running on port 3000'); + }); +}); +``` + +## CLI Integration in Scripts + +### Shell Script for Batch Editing + +```bash +#!/bin/bash + +# Batch edit images with different styles +echo "Creating style variations..." + +ORIGINAL_IMAGE="./product.jpg" +BASE="Professional product photo of laptop" + +z-ai image-edit -p "$BASE, modern minimalist style, white background" -i "$ORIGINAL_IMAGE" -o "./variations/minimal.png" -s 1024x1024 +z-ai image-edit -p "$BASE, dramatic lighting, dark background" -i "$ORIGINAL_IMAGE" -o "./variations/dramatic.png" -s 1024x1024 +z-ai image-edit -p "$BASE, on wooden desk, natural lighting" -i "$ORIGINAL_IMAGE" -o "./variations/natural.png" -s 1024x1024 + +echo "Variations created successfully!" +``` + +## Troubleshooting + +**Issue**: "SDK must be used in backend" +- **Solution**: Ensure z-ai-web-dev-sdk is only used in server-side code + +**Issue**: Invalid size parameter +- **Solution**: Use only supported sizes: 1024x1024, 768x1344, 864x1152, 1344x768, 1152x864, 1440x720, 720x1440 + +**Issue**: Edited image doesn't match intention +- **Solution**: Be more specific in edit prompts. Include what to change AND what to preserve + +**Issue**: CLI command not found +- **Solution**: Ensure z-ai CLI is properly installed and in PATH + +**Issue**: Image quality loss after editing +- **Solution**: Use larger size options and include quality terms in prompts + +**Issue**: Inconsistent results across variations +- **Solution**: Include more specific base description and detailed modification instructions + +## Edit Prompt Engineering Tips + +### Good Edit Prompts +- ✓ "Change background to modern office, keep subject and lighting identical" +- ✓ "Transform to watercolor style, maintain composition and colors" +- ✓ "Replace red car with blue motorcycle, keep road and scenery unchanged" +- ✓ "Adjust to golden hour lighting, preserve all elements" + +### Poor Edit Prompts +- ✗ "make it better" +- ✗ "change something" +- ✗ "different version" + +### Edit Prompt Components +1. **Base Context**: What the image currently represents +2. **Modification**: What specific changes to make +3. **Preservation**: What elements to keep unchanged +4. **Quality**: Desired output quality or style + +### Effective Edit Patterns + +**Background Changes:** +``` +"[Subject description], replace background with [new background], maintain subject lighting and pose" +``` + +**Style Transfers:** +``` +"[Current description] transformed into [style name] style, preserve composition and key elements" +``` + +**Element Replacement:** +``` +"[Scene description], replace [element A] with [element B], keep everything else identical" +``` + +**Color Adjustments:** +``` +"[Image description], change color scheme to [colors], maintain contrast and composition" +``` + +## Supported Image Sizes + +- `1024x1024` - Square (Best for general editing) +- `768x1344` - Portrait +- `864x1152` - Portrait +- `1344x768` - Landscape +- `1152x864` - Landscape +- `1440x720` - Wide landscape +- `720x1440` - Tall portrait + +## Remember + +- Always use z-ai-web-dev-sdk in backend code only +- The SDK is already installed - import as shown +- CLI tool is available for quick image editing +- Be specific about what to change AND what to preserve +- Include base description for better context +- Use appropriate size for the edit type +- Implement retry logic for production applications +- Test edit prompts iteratively for best results +- Consider creating variations to explore options +- Base64 images need to be decoded before saving diff --git a/skills/image-edit/scripts/image-edit.ts b/skills/image-edit/scripts/image-edit.ts new file mode 100755 index 0000000..cafa2a4 --- /dev/null +++ b/skills/image-edit/scripts/image-edit.ts @@ -0,0 +1,36 @@ +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function main(imageSource: string, prompt: string, size: '1024x1024' | '768x1344' | '864x1152' | '1344x768' | '1152x864' | '1440x720' | '720x1440', outFile: string) { + try { + const zai = await ZAI.create(); + + const response = await zai.images.generations.edit({ + prompt, + images: [{ url: imageSource }], // Array of objects with url property + size + }); + + const base64 = response?.data?.[0]?.base64; + if (!base64) { + console.error('No image data returned by the API'); + console.log('Full response:', JSON.stringify(response, null, 2)); + return; + } + + const buffer = Buffer.from(base64, 'base64'); + fs.writeFileSync(outFile, buffer); + console.log(`Edited image saved to ${outFile}`); + } catch (err: any) { + console.error('Image editing failed:', err?.message || err); + } +} + +// Example usage - Edit an image +// You can use either a URL or a base64 data URL for the imageSource +main( + 'https://example.com/photo.jpg', // or use: 'data:image/jpeg;base64,/9j/4AAQ...' + 'Transform this photo to have a sunset background with warm golden tones', + '1024x1024', + './output.png' +); diff --git a/skills/image-generation/LICENSE.txt b/skills/image-generation/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/image-generation/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/image-generation/SKILL.md b/skills/image-generation/SKILL.md new file mode 100755 index 0000000..5289ce1 --- /dev/null +++ b/skills/image-generation/SKILL.md @@ -0,0 +1,583 @@ +--- +name: image-generation +description: Implement AI image generation capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to create images from text descriptions, generate visual content, create artwork, design assets, or build applications with AI-powered image creation. Supports multiple image sizes and returns base64 encoded images. Also includes CLI tool for quick image generation. +license: MIT +--- + +# Image Generation Skill + +This skill guides the implementation of image generation functionality using the z-ai-web-dev-sdk package and CLI tool, enabling creation of high-quality images from text descriptions. + +## Skills Path + +**Skill Location**: `{project_path}/skills/image-generation` + +this skill is located at above path in your project. + +**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/image-generation.ts` for a working example. + +## Overview + +Image Generation allows you to build applications that create visual content from text prompts using AI models, enabling creative workflows, design automation, and visual content production. + +**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code. + +## Prerequisites + +The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below. + +## Basic Image Generation + +### Simple Image Creation + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function generateImage(prompt, outputPath) { + const zai = await ZAI.create(); + + const response = await zai.images.generations.create({ + prompt: prompt, + size: '1024x1024' + }); + + const imageBase64 = response.data[0].base64; + + // Save image + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + console.log(`Image saved to ${outputPath}`); + return outputPath; +} + +// Usage +await generateImage( + 'A cute cat playing in the garden', + './cat_image.png' +); +``` + +### Multiple Image Sizes + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +// Supported sizes +const SUPPORTED_SIZES = [ + '1024x1024', // Square + '768x1344', // Portrait + '864x1152', // Portrait + '1344x768', // Landscape + '1152x864', // Landscape + '1440x720', // Wide landscape + '720x1440' // Tall portrait +]; + +async function generateImageWithSize(prompt, size, outputPath) { + if (!SUPPORTED_SIZES.includes(size)) { + throw new Error(`Unsupported size: ${size}. Use one of: ${SUPPORTED_SIZES.join(', ')}`); + } + + const zai = await ZAI.create(); + + const response = await zai.images.generations.create({ + prompt: prompt, + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + return { + path: outputPath, + size: size, + fileSize: buffer.length + }; +} + +// Usage - Different sizes +await generateImageWithSize( + 'A beautiful landscape', + '1344x768', + './landscape.png' +); + +await generateImageWithSize( + 'A portrait of a person', + '768x1344', + './portrait.png' +); +``` + +## CLI Tool Usage + +The z-ai CLI tool provides a convenient way to generate images directly from the command line. + +### Basic CLI Usage + +```bash +# Generate image with full options +z-ai image --prompt "A beautiful landscape" --output "./image.png" + +# Short form +z-ai image -p "A cute cat" -o "./cat.png" + +# Specify size +z-ai image -p "A sunset" -o "./sunset.png" -s 1344x768 + +# Portrait orientation +z-ai image -p "A portrait" -o "./portrait.png" -s 768x1344 +``` + +### CLI Use Cases + +```bash +# Website hero image +z-ai image -p "Modern tech office with diverse team collaborating" -o "./hero.png" -s 1440x720 + +# Product image +z-ai image -p "Sleek smartphone on minimalist desk, professional product photography" -o "./product.png" -s 1024x1024 + +# Blog post illustration +z-ai image -p "Abstract visualization of data flowing through networks" -o "./blog_header.png" -s 1344x768 + +# Social media content +z-ai image -p "Vibrant illustration of community connection" -o "./social.png" -s 1024x1024 + +# Website favicon/logo +z-ai image -p "Simple geometric logo with blue gradient, minimal design" -o "./logo.png" -s 1024x1024 + +# Background pattern +z-ai image -p "Subtle geometric pattern, pastel colors, website background" -o "./bg_pattern.png" -s 1440x720 +``` + +## Advanced Use Cases + +### Batch Image Generation + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +async function generateImageBatch(prompts, outputDir, size = '1024x1024') { + const zai = await ZAI.create(); + + // Ensure output directory exists + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const results = []; + + for (let i = 0; i < prompts.length; i++) { + try { + const prompt = prompts[i]; + const filename = `image_${i + 1}.png`; + const outputPath = path.join(outputDir, filename); + + const response = await zai.images.generations.create({ + prompt: prompt, + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + results.push({ + success: true, + prompt: prompt, + path: outputPath, + size: buffer.length + }); + + console.log(`✓ Generated: ${filename}`); + } catch (error) { + results.push({ + success: false, + prompt: prompts[i], + error: error.message + }); + + console.error(`✗ Failed: ${prompts[i]} - ${error.message}`); + } + } + + return results; +} + +// Usage +const prompts = [ + 'A serene mountain landscape at sunset', + 'A futuristic city with flying cars', + 'An underwater coral reef teeming with life' +]; + +const results = await generateImageBatch(prompts, './generated-images'); +console.log(`Generated ${results.filter(r => r.success).length} images`); +``` + +### Image Generation Service + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; +import crypto from 'crypto'; + +class ImageGenerationService { + constructor(outputDir = './generated-images') { + this.outputDir = outputDir; + this.zai = null; + this.cache = new Map(); + } + + async initialize() { + this.zai = await ZAI.create(); + + if (!fs.existsSync(this.outputDir)) { + fs.mkdirSync(this.outputDir, { recursive: true }); + } + } + + generateCacheKey(prompt, size) { + return crypto + .createHash('md5') + .update(`${prompt}-${size}`) + .digest('hex'); + } + + async generate(prompt, options = {}) { + const { + size = '1024x1024', + useCache = true, + filename = null + } = options; + + // Check cache + const cacheKey = this.generateCacheKey(prompt, size); + + if (useCache && this.cache.has(cacheKey)) { + const cachedPath = this.cache.get(cacheKey); + if (fs.existsSync(cachedPath)) { + return { + path: cachedPath, + cached: true, + prompt: prompt, + size: size + }; + } + } + + // Generate new image + const response = await this.zai.images.generations.create({ + prompt: prompt, + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + + // Determine output path + const outputFilename = filename || `${cacheKey}.png`; + const outputPath = path.join(this.outputDir, outputFilename); + + fs.writeFileSync(outputPath, buffer); + + // Cache result + if (useCache) { + this.cache.set(cacheKey, outputPath); + } + + return { + path: outputPath, + cached: false, + prompt: prompt, + size: size, + fileSize: buffer.length + }; + } + + clearCache() { + this.cache.clear(); + } + + getCacheSize() { + return this.cache.size; + } +} + +// Usage +const service = new ImageGenerationService(); +await service.initialize(); + +const result = await service.generate( + 'A modern office space', + { size: '1440x720' } +); + +console.log('Generated:', result.path); +``` + +### Website Asset Generator + +```bash +# Using CLI for quick website asset generation +z-ai image -p "Modern tech hero banner, blue gradient" -o "./assets/hero.png" -s 1440x720 +z-ai image -p "Team collaboration illustration" -o "./assets/team.png" -s 1344x768 +z-ai image -p "Simple geometric logo" -o "./assets/logo.png" -s 1024x1024 +``` + +## Best Practices + +### 1. Effective Prompt Engineering + +```javascript +function buildEffectivePrompt(subject, style, details = []) { + const components = [ + subject, + style, + ...details, + 'high quality', + 'detailed' + ]; + + return components.filter(Boolean).join(', '); +} + +// Usage +const prompt = buildEffectivePrompt( + 'mountain landscape', + 'oil painting style', + ['sunset lighting', 'dramatic clouds', 'reflection in lake'] +); + +// Result: "mountain landscape, oil painting style, sunset lighting, dramatic clouds, reflection in lake, high quality, detailed" +``` + +### 2. Size Selection Helper + +```javascript +function selectOptimalSize(purpose) { + const sizeMap = { + 'hero-banner': '1440x720', + 'blog-header': '1344x768', + 'social-square': '1024x1024', + 'portrait': '768x1344', + 'product': '1024x1024', + 'landscape': '1344x768', + 'mobile-banner': '720x1440', + 'thumbnail': '1024x1024' + }; + + return sizeMap[purpose] || '1024x1024'; +} + +// Usage +const size = selectOptimalSize('hero-banner'); +await generateImage('website hero image', size, './hero.png'); +``` + +### 3. Error Handling + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function safeGenerateImage(prompt, size, outputPath, retries = 3) { + let lastError; + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + const zai = await ZAI.create(); + + const response = await zai.images.generations.create({ + prompt: prompt, + size: size + }); + + if (!response.data || !response.data[0] || !response.data[0].base64) { + throw new Error('Invalid response from image generation API'); + } + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + fs.writeFileSync(outputPath, buffer); + + return { + success: true, + path: outputPath, + attempts: attempt + }; + } catch (error) { + lastError = error; + console.error(`Attempt ${attempt} failed:`, error.message); + + if (attempt < retries) { + // Wait before retry (exponential backoff) + await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); + } + } + } + + return { + success: false, + error: lastError.message, + attempts: retries + }; +} +``` + +## Common Use Cases + +1. **Website Design**: Generate hero images, backgrounds, and visual assets +2. **Marketing Materials**: Create social media graphics and promotional images +3. **Product Visualization**: Generate product mockups and variations +4. **Content Creation**: Produce blog post illustrations and thumbnails +5. **Brand Assets**: Create logos, icons, and brand imagery +6. **UI/UX Design**: Generate interface elements and illustrations +7. **Game Development**: Create concept art and game assets +8. **E-commerce**: Generate product images and lifestyle shots + +## Integration Examples + +### Express.js API Endpoint + +```javascript +import express from 'express'; +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +const app = express(); +app.use(express.json()); +app.use('/images', express.static('generated-images')); + +let zaiInstance; +const outputDir = './generated-images'; + +async function initZAI() { + zaiInstance = await ZAI.create(); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } +} + +app.post('/api/generate-image', async (req, res) => { + try { + const { prompt, size = '1024x1024' } = req.body; + + if (!prompt) { + return res.status(400).json({ error: 'Prompt is required' }); + } + + const response = await zaiInstance.images.generations.create({ + prompt: prompt, + size: size + }); + + const imageBase64 = response.data[0].base64; + const buffer = Buffer.from(imageBase64, 'base64'); + + const filename = `img_${Date.now()}.png`; + const filepath = path.join(outputDir, filename); + fs.writeFileSync(filepath, buffer); + + res.json({ + success: true, + imageUrl: `/images/${filename}`, + prompt: prompt, + size: size + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +initZAI().then(() => { + app.listen(3000, () => { + console.log('Image generation API running on port 3000'); + }); +}); +``` + +## CLI Integration in Scripts + +### Shell Script Example + +```bash +#!/bin/bash + +# Generate website assets using CLI +echo "Generating website assets..." + +z-ai image -p "Modern tech hero banner, blue gradient" -o "./assets/hero.png" -s 1440x720 +z-ai image -p "Team collaboration illustration" -o "./assets/team.png" -s 1344x768 +z-ai image -p "Simple geometric logo" -o "./assets/logo.png" -s 1024x1024 + +echo "Assets generated successfully!" +``` + +## Troubleshooting + +**Issue**: "SDK must be used in backend" +- **Solution**: Ensure z-ai-web-dev-sdk is only used in server-side code + +**Issue**: Invalid size parameter +- **Solution**: Use only supported sizes: 1024x1024, 768x1344, 864x1152, 1344x768, 1152x864, 1440x720, 720x1440 + +**Issue**: Generated image doesn't match prompt +- **Solution**: Make prompts more specific and descriptive. Include style, details, and quality terms + +**Issue**: CLI command not found +- **Solution**: Ensure z-ai CLI is properly installed and in PATH + +**Issue**: Image file is corrupted +- **Solution**: Verify base64 decoding and file writing are correct + +## Prompt Engineering Tips + +### Good Prompts +- ✓ "Professional product photography of wireless headphones, white background, studio lighting, high quality" +- ✓ "Mountain landscape at golden hour, oil painting style, dramatic clouds, detailed" +- ✓ "Modern minimalist logo for tech company, blue and white, geometric shapes" + +### Poor Prompts +- ✗ "headphones" +- ✗ "picture of mountains" +- ✗ "logo" + +### Prompt Components +1. **Subject**: What you want to see +2. **Style**: Art style, photography style, etc. +3. **Details**: Specific elements, colors, mood +4. **Quality**: "high quality", "detailed", "professional" + +## Supported Image Sizes + +- `1024x1024` - Square +- `768x1344` - Portrait +- `864x1152` - Portrait +- `1344x768` - Landscape +- `1152x864` - Landscape +- `1440x720` - Wide landscape +- `720x1440` - Tall portrait + +## Remember + +- Always use z-ai-web-dev-sdk in backend code only +- The SDK is already installed - import as shown +- CLI tool is available for quick image generation +- Supported sizes are specific - use the provided list +- Base64 images need to be decoded before saving +- Consider caching for repeated prompts +- Implement retry logic for production applications +- Use descriptive prompts for better results diff --git a/skills/image-generation/scripts/image-generation.ts b/skills/image-generation/scripts/image-generation.ts new file mode 100755 index 0000000..7596f32 --- /dev/null +++ b/skills/image-generation/scripts/image-generation.ts @@ -0,0 +1,28 @@ +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; + +async function main(prompt: string, size: '1024x1024' | '768x1344' | '864x1152' | '1344x768' | '1152x864' | '1440x720' | '720x1440', outFile: string) { + try { + const zai = await ZAI.create(); + + const response = await zai.images.generations.create({ + prompt, + size + }); + + const base64 = response?.data?.[0]?.base64; + if (!base64) { + console.error('No image data returned by the API'); + console.log('Full response:', JSON.stringify(response, null, 2)); + return; + } + + const buffer = Buffer.from(base64, 'base64'); + fs.writeFileSync(outFile, buffer); + console.log(`Image saved to ${outFile}`); + } catch (err: any) { + console.error('Image generation failed:', err?.message || err); + } +} + +main('A cute kitten', '1024x1024', './output.png'); diff --git a/skills/image-understand/LICENSE.txt b/skills/image-understand/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/image-understand/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/image-understand/SKILL.md b/skills/image-understand/SKILL.md new file mode 100755 index 0000000..2a01bae --- /dev/null +++ b/skills/image-understand/SKILL.md @@ -0,0 +1,855 @@ +--- +name: image-understand +description: Implement specialized image understanding capabilities using the z-ai-web-dev-sdk. Use this skill when the user needs to analyze static images, extract visual information, perform OCR, detect objects, classify images, or understand visual content. Optimized for PNG, JPEG, GIF, WebP, and BMP formats. +license: MIT +--- + +# Image Understanding Skill + +This skill provides specialized image understanding functionality using the z-ai-web-dev-sdk package, enabling AI models to analyze, describe, and extract information from static images. + +## Skills Path + +**Skill Location**: `{project_path}/skills/image-understand` + +this skill is located at above path in your project. + +**Reference Scripts**: Example test scripts are available in the `{Skill Location}/scripts/` directory for quick testing and reference. See `{Skill Location}/scripts/image-understand.ts` for a working example. + +## Overview + +Image Understanding focuses specifically on static image analysis, providing capabilities for: +- Image description and scene understanding +- Object detection and recognition +- OCR (Optical Character Recognition) and text extraction +- Image classification and categorization +- Visual content analysis +- Quality assessment +- Accessibility (alt text generation) + +**IMPORTANT**: z-ai-web-dev-sdk MUST be used in backend code only. Never use it in client-side code. + +## Prerequisites + +The z-ai-web-dev-sdk package is already installed. Import it as shown in the examples below. + +## CLI Usage (For Simple Tasks) + +For quick image analysis tasks, you can use the z-ai CLI instead of writing code. This is ideal for simple image descriptions, testing, or automation. + +### Basic Image Analysis + +```bash +# Describe an image from URL +z-ai vision --prompt "What's in this image?" --image "https://example.com/photo.jpg" + +# Using short options +z-ai vision -p "Describe this image" -i "https://example.com/image.png" +``` + +### Analyze Local Images + +```bash +# Analyze a local image file +z-ai vision -p "What objects are in this photo?" -i "./photo.jpg" + +# Save response to file +z-ai vision -p "Describe the scene" -i "./landscape.png" -o description.json +``` + +### Multiple Images Comparison + +```bash +# Compare multiple images +z-ai vision \ + -p "Compare these two images and highlight the differences" \ + -i "./photo1.jpg" \ + -i "./photo2.jpg" \ + -o comparison.json + +# Analyze a series of images +z-ai vision \ + --prompt "What patterns do you see across these images?" \ + --image "https://example.com/img1.jpg" \ + --image "https://example.com/img2.jpg" \ + --image "https://example.com/img3.jpg" +``` + +### Advanced Analysis with Thinking + +```bash +# Enable chain-of-thought reasoning for complex tasks +z-ai vision \ + -p "Count all people in this image and describe what each person is doing" \ + -i "./crowd.jpg" \ + --thinking \ + -o analysis.json + +# Complex object detection with reasoning +z-ai vision \ + -p "Identify all safety hazards in this workplace image" \ + -i "./workplace.jpg" \ + --thinking +``` + +### Streaming Output + +```bash +# Stream the analysis in real-time +z-ai vision -p "Provide a detailed description" -i "./photo.jpg" --stream +``` + +### CLI Parameters + +- `--prompt, -p `: **Required** - Question or instruction about the image(s) +- `--image, -i `: Optional - Image URL or local file path (can be used multiple times) +- `--thinking, -t`: Optional - Enable chain-of-thought reasoning (default: disabled) +- `--output, -o `: Optional - Output file path (JSON format) +- `--stream`: Optional - Stream the response in real-time + +### Supported Image Formats + +- PNG (.png) - Best for diagrams, screenshots, graphics with transparency +- JPEG (.jpg, .jpeg) - Best for photos and complex images +- GIF (.gif) - Supports both static and animated images +- WebP (.webp) - Modern format with good compression +- BMP (.bmp) - Uncompressed bitmap format + +### When to Use CLI vs SDK + +**Use CLI for:** +- Quick image analysis or descriptions +- One-off OCR tasks +- Testing image understanding capabilities +- Simple batch processing scripts +- Generating alt text for accessibility + +**Use SDK for:** +- Multi-turn conversations about images +- Complex image processing pipelines +- Production applications with error handling +- Custom integration with your application logic +- Batch processing with custom business logic + +## Recommended Approach + +For better performance and reliability, use base64 encoding to pass images to the model instead of image URLs. + +## Basic Image Understanding Implementation + +### Single Image Analysis + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function analyzeImage(imageUrl, prompt) { + const zai = await ZAI.create(); + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: prompt + }, + { + type: 'image_url', + image_url: { + url: imageUrl + } + } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} + +// Usage examples +const description = await analyzeImage( + 'https://example.com/landscape.jpg', + 'Describe this landscape in detail, including colors, lighting, and mood' +); + +const objectDetection = await analyzeImage( + 'https://example.com/room.jpg', + 'List all objects visible in this room' +); +``` + +### Multiple Images Comparison + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function compareImages(imageUrls, question) { + const zai = await ZAI.create(); + + const content = [ + { + type: 'text', + text: question + }, + ...imageUrls.map(url => ({ + type: 'image_url', + image_url: { url } + })) + ]; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: content + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} + +// Usage +const comparison = await compareImages( + [ + 'https://example.com/before.jpg', + 'https://example.com/after.jpg' + ], + 'What are the key differences between these before and after images?' +); +``` + +### Base64 Image Support (Recommended) + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; + +async function analyzeLocalImage(imagePath, prompt) { + const zai = await ZAI.create(); + + // Read image file and convert to base64 + const imageBuffer = fs.readFileSync(imagePath); + const base64Image = imageBuffer.toString('base64'); + + // Determine MIME type based on file extension + const ext = path.extname(imagePath).toLowerCase(); + const mimeTypes = { + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.webp': 'image/webp', + '.bmp': 'image/bmp' + }; + const mimeType = mimeTypes[ext] || 'image/jpeg'; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { + type: 'text', + text: prompt + }, + { + type: 'image_url', + image_url: { + url: `data:${mimeType};base64,${base64Image}` + } + } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} + +// Usage +const result = await analyzeLocalImage( + './product-photo.jpg', + 'Analyze this product image for e-commerce listing' +); +``` + +## Advanced Use Cases + +### OCR and Text Extraction + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function extractText(imageUrl, options = {}) { + const zai = await ZAI.create(); + + const prompt = options.preserveLayout + ? 'Extract all text from this image. Preserve the exact layout, formatting, and structure.' + : 'Extract all visible text from this image.'; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} + +// Usage examples +const receiptText = await extractText( + 'https://example.com/receipt.jpg', + { preserveLayout: true } +); + +const businessCardInfo = await extractText( + 'https://example.com/business-card.jpg' +); +``` + +### Object Detection and Counting + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function detectObjects(imageUrl, objectType) { + const zai = await ZAI.create(); + + const prompt = objectType + ? `Count and locate all ${objectType} in this image. Provide their positions and describe each one.` + : 'Detect and list all objects in this image with their approximate locations.'; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'enabled' } // Enable thinking for complex counting + }); + + return response.choices[0]?.message?.content; +} + +// Usage +const peopleCount = await detectObjects( + 'https://example.com/crowd.jpg', + 'people' +); + +const allObjects = await detectObjects( + 'https://example.com/room.jpg' +); +``` + +### Image Classification and Tagging + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function classifyAndTag(imageUrl) { + const zai = await ZAI.create(); + + const prompt = `Analyze this image and provide a comprehensive classification: +1. Primary category (e.g., nature, urban, portrait, product) +2. Subject matter (main focus of the image) +3. Style or mood (e.g., professional, casual, artistic, vintage) +4. Color palette description +5. Suggested tags (10-15 keywords, comma-separated) + +Format your response as structured JSON.`; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + const content = response.choices[0]?.message?.content; + + try { + return JSON.parse(content); + } catch (e) { + return { rawResponse: content }; + } +} + +// Usage +const classification = await classifyAndTag( + 'https://example.com/photo.jpg' +); +console.log('Tags:', classification.tags); +``` + +### Quality Assessment + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function assessImageQuality(imageUrl) { + const zai = await ZAI.create(); + + const prompt = `Assess the technical quality of this image: +1. Sharpness and focus (1-10) +2. Exposure and brightness (1-10) +3. Color balance (1-10) +4. Composition (1-10) +5. Any technical issues (blur, noise, artifacts, etc.) +6. Overall quality rating (1-10) +7. Suggestions for improvement + +Provide specific feedback for each criterion.`; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} +``` + +### Accessibility - Alt Text Generation + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function generateAltText(imageUrl, context = '') { + const zai = await ZAI.create(); + + const prompt = context + ? `Generate concise, descriptive alt text for this image. Context: ${context}. Focus on the most important visual elements that convey the image's purpose.` + : 'Generate concise, descriptive alt text for this image suitable for screen readers. Focus on key visual elements.'; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} + +// Usage +const altText = await generateAltText( + 'https://example.com/hero-image.jpg', + 'Website hero section for a tech startup' +); +``` + +### Scene Understanding + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +async function understandScene(imageUrl) { + const zai = await ZAI.create(); + + const prompt = `Provide a comprehensive scene analysis: +1. Setting/location type (indoor/outdoor, specific place) +2. Time of day and lighting conditions +3. Weather (if applicable) +4. People present (number, activities, interactions) +5. Key objects and their arrangement +6. Overall atmosphere and mood +7. Notable details or interesting elements`; + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; +} +``` + +## Batch Processing + +### Process Multiple Images + +```javascript +import ZAI from 'z-ai-web-dev-sdk'; + +class ImageBatchProcessor { + constructor() { + this.zai = null; + } + + async initialize() { + this.zai = await ZAI.create(); + } + + async processImage(imageUrl, prompt) { + const response = await this.zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return response.choices[0]?.message?.content; + } + + async processBatch(imageUrls, prompt) { + const results = []; + + for (const imageUrl of imageUrls) { + try { + const result = await this.processImage(imageUrl, prompt); + results.push({ imageUrl, success: true, result }); + } catch (error) { + results.push({ + imageUrl, + success: false, + error: error.message + }); + } + } + + return results; + } +} + +// Usage +const processor = new ImageBatchProcessor(); +await processor.initialize(); + +const images = [ + 'https://example.com/img1.jpg', + 'https://example.com/img2.jpg', + 'https://example.com/img3.jpg' +]; + +const results = await processor.processBatch( + images, + 'Generate a short description suitable for social media' +); +``` + +## Best Practices + +### 1. Image Quality and Preparation +- Use high-resolution images for better analysis accuracy +- Ensure images are well-lit and properly exposed +- For OCR, ensure text is clear and readable +- Optimize file size to balance quality and performance +- Supported formats: PNG (best for text/diagrams), JPEG (best for photos), WebP, GIF, BMP + +### 2. Prompt Engineering for Images +- Be specific about what information you need +- Mention the type of image (photo, diagram, screenshot, etc.) +- For complex tasks, break down into specific questions +- Use structured prompts for JSON output +- Include context when relevant + +### 3. Error Handling + +```javascript +async function safeImageAnalysis(imageUrl, prompt) { + try { + const zai = await ZAI.create(); + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + return { + success: true, + content: response.choices[0]?.message?.content + }; + } catch (error) { + console.error('Image analysis error:', error); + return { + success: false, + error: error.message + }; + } +} +``` + +### 4. Performance Optimization +- Cache SDK instance for batch processing +- Use base64 encoding for local images +- Implement request throttling for large batches +- Consider image preprocessing (resize, compress) for large files +- Use appropriate thinking mode (disabled for simple tasks, enabled for complex reasoning) + +### 5. Security Considerations +- Validate image URLs before processing +- Implement rate limiting for public APIs +- Sanitize user-provided image data +- Never expose SDK credentials in client-side code +- Implement content moderation for user-uploaded images + +## Common Use Cases + +1. **E-commerce Product Analysis**: Analyze product images, extract features, generate descriptions +2. **Document Processing**: Extract text from receipts, invoices, forms, business cards +3. **Content Moderation**: Detect inappropriate content, verify image compliance +4. **Quality Control**: Identify defects, assess product quality in manufacturing +5. **Accessibility**: Generate alt text for images automatically +6. **Image Cataloging**: Auto-tag and categorize image libraries +7. **Visual Search**: Understand and index images for search functionality +8. **Medical Imaging**: Preliminary analysis with appropriate disclaimers +9. **Real Estate**: Analyze property photos, extract features +10. **Social Media**: Generate captions, hashtags, and descriptions + +## Integration Examples + +### Express.js API Endpoint + +```javascript +import express from 'express'; +import ZAI from 'z-ai-web-dev-sdk'; +import multer from 'multer'; + +const app = express(); +const upload = multer({ storage: multer.memoryStorage() }); + +let zaiInstance; + +async function initZAI() { + zaiInstance = await ZAI.create(); +} + +// Analyze image from URL +app.post('/api/analyze-image', express.json(), async (req, res) => { + try { + const { imageUrl, prompt } = req.body; + + if (!imageUrl || !prompt) { + return res.status(400).json({ + error: 'imageUrl and prompt are required' + }); + } + + const response = await zaiInstance.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + res.json({ + success: true, + analysis: response.choices[0]?.message?.content + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +// Analyze uploaded image file +app.post('/api/analyze-upload', upload.single('image'), async (req, res) => { + try { + const { prompt } = req.body; + const imageFile = req.file; + + if (!imageFile || !prompt) { + return res.status(400).json({ + error: 'image file and prompt are required' + }); + } + + // Convert to base64 + const base64Image = imageFile.buffer.toString('base64'); + const mimeType = imageFile.mimetype; + + const response = await zaiInstance.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { + type: 'image_url', + image_url: { + url: `data:${mimeType};base64,${base64Image}` + } + } + ] + } + ], + thinking: { type: 'disabled' } + }); + + res.json({ + success: true, + analysis: response.choices[0]?.message?.content + }); + } catch (error) { + res.status(500).json({ + success: false, + error: error.message + }); + } +}); + +initZAI().then(() => { + app.listen(3000, () => { + console.log('Image understanding API running on port 3000'); + }); +}); +``` + +### Next.js API Route + +```javascript +// pages/api/image-understand.js +import ZAI from 'z-ai-web-dev-sdk'; + +let zaiInstance = null; + +async function getZAI() { + if (!zaiInstance) { + zaiInstance = await ZAI.create(); + } + return zaiInstance; +} + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + const { imageUrl, prompt } = req.body; + + if (!imageUrl || !prompt) { + return res.status(400).json({ + error: 'imageUrl and prompt are required' + }); + } + + const zai = await getZAI(); + + const response = await zai.chat.completions.createVision({ + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ], + thinking: { type: 'disabled' } + }); + + res.status(200).json({ + success: true, + analysis: response.choices[0]?.message?.content + }); + } catch (error) { + console.error('Error:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } +} +``` + +## Troubleshooting + +**Issue**: "SDK must be used in backend" +- **Solution**: Ensure z-ai-web-dev-sdk is only imported and used in server-side code, never in client/browser code + +**Issue**: Image not loading or being analyzed +- **Solution**: Verify the image URL is accessible, returns correct MIME type, and is in a supported format + +**Issue**: Poor OCR accuracy +- **Solution**: Ensure text is clear and readable, increase image resolution, ensure proper lighting and contrast + +**Issue**: Inaccurate object detection or counting +- **Solution**: Enable thinking mode for complex counting tasks, use high-resolution images, provide specific prompts + +**Issue**: Slow response times +- **Solution**: Optimize image size (resize before upload), use base64 for local images, cache SDK instance for batch processing + +**Issue**: Base64 encoding fails +- **Solution**: Verify file path is correct, check file permissions, ensure MIME type matches file extension + +## Remember + +- Always use z-ai-web-dev-sdk in backend code only +- The SDK is already installed - import as shown in examples +- Use `image_url` content type for static images +- Base64 encoding is recommended for better performance +- Structure prompts clearly for best results +- Enable thinking mode for complex reasoning tasks (counting, detailed analysis) +- Handle errors gracefully in production +- Validate and sanitize user inputs +- Consider privacy and security when processing user images diff --git a/skills/image-understand/scripts/image-understand.ts b/skills/image-understand/scripts/image-understand.ts new file mode 100755 index 0000000..dae8ee0 --- /dev/null +++ b/skills/image-understand/scripts/image-understand.ts @@ -0,0 +1,41 @@ +import ZAI, { VisionMessage } from 'z-ai-web-dev-sdk'; + +async function main(imageUrl: string, prompt: string) { + try { + const zai = await ZAI.create(); + + const messages: VisionMessage[] = [ + { + role: 'assistant', + content: [ + { type: 'text', text: 'Output only text, no markdown.' } + ] + }, + { + role: 'user', + content: [ + { type: 'text', text: prompt }, + { type: 'image_url', image_url: { url: imageUrl } } + ] + } + ]; + + const response = await zai.chat.completions.createVision({ + model: 'glm-4.6v', + messages, + thinking: { type: 'disabled' } + }); + + const reply = response.choices?.[0]?.message?.content; + console.log('Image Understanding Result:'); + console.log(reply ?? JSON.stringify(response, null, 2)); + } catch (err: any) { + console.error('Image understanding failed:', err?.message || err); + } +} + +// Example usage - analyze an image +main( + "https://cdn.bigmodel.cn/static/logo/register.png", + "Please analyze this image and describe what you see in detail." +); diff --git a/skills/interview-designer/README.md b/skills/interview-designer/README.md new file mode 100755 index 0000000..78c1593 --- /dev/null +++ b/skills/interview-designer/README.md @@ -0,0 +1,70 @@ +# Interview Designer + +**Evidence-Based Interview Planning** + +Design interview questions using Scorecard → Forensic Scan → Future Simulation. Avoid confirmation bias and produce structured interview guides (Scorecard + Red Flags/Green Signals + Pressure Tests + Future Scenarios) with Geoff Smart, Lou Adler, and Daniel Kahneman as the default expert panel. + +--- + +## When to Use This Skill + +Use this skill when: + +- You need to design interview questions for a specific role +- You want to avoid confirmation bias in interview planning +- You're creating a structured interview guide (Scorecard + Questions + Pressure Tests) +- You need to balance past validation with future simulation + +--- + +## Methodology: Scorecard → Forensic → Future + +| Phase | Expert | What You Define | +|-----------------|---------------|-----------------------------------------------------------------------------------| +| **1. Scorecard**| Geoff Smart | Mission, Outcomes, Competencies — *before* looking at any resume | +| **2. Forensic Scan** | Smart + Domain | Resume gaps vs. highlights; "Too Good To Be True" / "Driver vs Passenger" heuristics | +| **3. Future Simulation** | Lou Adler | Performance problems the candidate would face in your context; week-one scenarios | + +--- + +## What You Get + +| Output | Template | Purpose | +|-------------------|------------------------------------|------------------------------------------------------| +| **Interview Guide** | `templates/interview_guide_template.md` | Scorecard + Red Flags/Green Signals + Pressure Tests + Future Scenarios | + +The guide includes both concerns (**Red Flags**) and highlight verification (**Green Signals**) for objective assessment. + +--- + +## Design Principles + +1. **Cannot Be Memorized** — Questions force real-time thinking (simulation) or concrete recall (pressure test). +2. **Forced Trade-offs** — Choose between two "correct" options to surface values, not just knowledge. +3. **Detail Granularity** — Probe to "what exact words did you say" or "what diagram did you draw." + +--- + +## Quick Reference + +| Interview Goal | Question Type | Example | +|---------------------|---------------------|-------------------------------------------------------------------------| +| Validate past claims| Pressure Test (STAR) | "Walk me through the specific metrics you tracked and how you used them." | +| Predict future fit | Future Simulation | "Here's our Q1 challenge. How would you approach it in your first week?" | +| Detect blind spots | Trade-off Question | "Speed vs. quality — which would you sacrifice here, and why?" | + +--- + +## Install + +**ClawHub (OpenClaw)**: +```bash +npx clawhub@latest install interview-designer +``` + +**Other (e.g. skills.sh)**: +```bash +npx skills add mikonos/interview-designer +``` + +Compatible with Cursor, Claude Code, OpenClaw, and other agents that support the skills protocol. diff --git a/skills/interview-designer/SKILL.md b/skills/interview-designer/SKILL.md new file mode 100755 index 0000000..e19a373 --- /dev/null +++ b/skills/interview-designer/SKILL.md @@ -0,0 +1,53 @@ +--- +name: interview-designer +description: Analyze resumes and design interview strategies using evidence-based methodology. Transforms interview prep from "read resume → ask questions" into "define standard → forensic evidence → future simulation". Combines Geoff Smart's Topgrading, Lou Adler's performance-based hiring, and Daniel Kahneman's bias control. Use when preparing for interviews, creating structured interview guides, or designing questions to validate candidate competencies. +--- + +# Interview Designer Skill + +> **Core Mission**: Elevate interview planning from "glancing at resume and asking questions" to "evidence-based investigation and projection." +> **Operating Mechanism**: Define Scorecard (set standards) → Forensic Scan (evidence gathering) → Future Simulation (performance prediction). +> **Prompt Strategy**: This skill uses \. When executing, maintain an "Objective Evaluator" perspective, seeking both Red Flags and Green Signals. + +## 1. Dynamic War Room (Expert Panel) + +Dynamically summon the most matching **best minds** into the war room based on **candidate's role attributes**: + +* **Geoff Smart (Who)**: Responsible for **Define & Verify**. + * *Principle*: Scorecard First. Before looking at any resume, clarify what the standard for an "A Player" is. +* **Lou Adler (Performance-based)**: Responsible for **Predict**. + * *Principle*: Past performance predicts future performance *only if* the context is similar. Must design simulations for future scenarios. +* **Daniel Kahneman (Bias Control)**: Responsible for **De-bias**. + * *Principle*: Beware of "confirmation bias." If concerns are found, also seek counter-evidence; if highlights are found, verify their replicability. +* **Domain Expert**: Responsible for **Depth**. + +## 2. Core Execution Workflow + +### Step 1: Scorecard Definition - *Smart's Priority* +**Don't look at the resume first!** Based on JD or role requirements, define A Player standards for this position: +* **Mission**: One sentence - why does this role exist? +* **Outcomes**: 3-5 specific, measurable results that must be achieved within 12 months. +* **Competencies**: Hard/soft skills required to achieve the above outcomes. + +### Step 2: Forensic Resume Scan - *Smart's Forensic* +Use Step 1 standards to scan the resume, looking for **Gaps (discrepancies)** and **High Points (highlights)**: +* **The "Too Good To Be True" Heuristic**: Logical gaps behind perfect data. +* **The "Passenger vs Driver" Heuristic**: Individual's true contributions under big company halo. +* **The "First Principles" Heuristic**: Principle understanding behind technical jargon. + +### Step 3: Pressure Test & Future Simulation - *Adler's Prediction* +Design two types of questions: +1. **Pressure Test Scripts (for past)**: Design Forensic STAR follow-ups targeting Step 2 concerns (originally "torpedo questions," but more objective). +2. **Future Simulation (for future)**: Design a specific Performance Problem. + * *Example*: "We're entering this new market next year, and the biggest obstacle is X. If you join, how would you analyze this problem in your first week?" + +## 3. Question Design Principles + +1. **Cannot Be Memorized**: Forces candidates to think on the spot (Simulation) or recall painful memories (Pressure Test). +2. **Forced Trade-offs**: Choose between two "correct" options to test values. +3. **Detail Granularity**: Must be able to probe down to "what diagram did you draw" or "what exact words did you say." + +## 4. Output Format + +Directly call `templates/interview_guide_template.md` to generate the report. +**Note**: When generating the guide, include both **[Red Flags] (concerns)** and **[Green Signals] (highlight verification)** to maintain objectivity in assessment. diff --git a/skills/interview-designer/_meta.json b/skills/interview-designer/_meta.json new file mode 100755 index 0000000..53cd663 --- /dev/null +++ b/skills/interview-designer/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn73h39f2c8arzkcvwmafak9mn80wq7d", + "slug": "interview-designer", + "version": "1.0.0", + "publishedAt": 1770749339448 +} \ No newline at end of file diff --git a/skills/interview-designer/references/design_rationale.md b/skills/interview-designer/references/design_rationale.md new file mode 100755 index 0000000..f894216 --- /dev/null +++ b/skills/interview-designer/references/design_rationale.md @@ -0,0 +1,43 @@ +# Expert Critique: Interview Designer Skill + +> **Simulated Review Panel**: +> * **Geoff Smart** (Author of "Who", Topgrading methodology) +> * **Lou Adler** (Founder of Performance-based Hiring) +> * **Daniel Kahneman** (Nobel Laureate, Behavioral Economics, Decision Noise Research) + +--- + +## 1. Geoff Smart's Perspective: Only "Autopsy," No "Definition" +* **Comment**: "The starting point of this design is good (Forensic investigation), which aligns well with Topgrading's spirit—digging for truth. **However, you made a fatal error: the sequence is reversed.**" +* **Critique**: + * The current flow is `Resume Scan` → `Scorecard`. This is **reactive**. You're setting standards based on the candidate's resume, which is the trap of "creating positions around people." + * **The A Method's** first step is always **Scorecard**—before looking at any resume, you must define the role's mission (Mission), outcomes (Outcomes), and competencies (Competencies). + * **Risk**: Without an independent Scorecard first, your "investigation" becomes "nitpicking," not "validation of fit." You might prove they lied, but not that they can deliver. +* **Recommendation**: Mandate Step 0 as **"Define Success"**, not **"Scan Resume"**. + +## 2. Lou Adler's Perspective: Overemphasis on "Past," Neglecting "Future" +* **Comment**: "I see you're very enthusiastic about uncovering resume 'inflation.' That's interesting, but **can someone perform the job just because their resume is perfect?**" +* **Critique**: + * Current `Torpedo Questions` mainly expose past lies. + * **Performance-based Hiring** believes the best prediction is having candidates solve **future problems**. + * **Gap**: Lacks **Project-based Problem Solving**. Beyond asking "how did you coordinate in the past," also ask "this is our new project, if you were responsible, what would you do in the first week?" +* **Recommendation**: Add **"Future Performance Simulation"** section. + +## 3. Daniel Kahneman's Perspective: Breeding Ground for Confirmation Bias +* **Comment**: "You're calling this skill 'Forensic' and throwing 'torpedoes.' This plants very strong **negative priming** in the interviewer's mind." +* **Critique**: + * Once an interviewer enters the room with "this person might be lying" colored glasses, they'll unconsciously seek evidence to confirm this (Confirmation Bias), while ignoring the candidate's genuine highlights. + * This is the source of **Noise**. +* **Recommendation**: Balance the mindset. Change "Torpedo" to **"Evidence Stress Test"**, and explicitly require seeking **"Green Signals"** simultaneously, not just red flags. + +--- + +## 4. Comprehensive Optimization Recommendations (Action Plan) + +1. **Architecture Adjustment (Re-order)**: + * `Step 1: Define Scorecard` (based on JD/business pain points, independent of resume) + * `Step 2: Resume Forensic` (scan resume gaps based on Scorecard) +2. **Content Enhancement (Add Future Focus)**: + * Add `Problem Solving Case` generation logic. +3. **Tone Correction (Neutrality)**: + * Maintain sharpness, but remove "presumption of guilt" undertones. Goal is Truth-seeking, not Witch-hunting. diff --git a/skills/interview-designer/templates/interview_guide_template.md b/skills/interview-designer/templates/interview_guide_template.md new file mode 100755 index 0000000..695f622 --- /dev/null +++ b/skills/interview-designer/templates/interview_guide_template.md @@ -0,0 +1,62 @@ +# {Candidate_Name}_Targeted_Interview_Guide + +> [!IMPORTANT] +> **Planning Context**: Based on war room simulation with {Expert_List}. +> **Objective**: Verify competency match (Step 1), gather evidence on resume concerns (Step 2), project future performance (Step 3). + +--- + +## 1. Competency Scorecard + +*Before looking at the resume, THIS is what success looks like.* + +* **Mission (One-sentence mission)**: ... +* **Outcomes (12-month must-achieve results)**: + 1. ... + 2. ... +* **Core Competencies**: + * **{Competency 1}**: {Description} + * **{Competency 2}**: {Description} + +--- + +## 2. Forensic Resume Scan + +*Scan resume against Scorecard, looking for Gaps (concerns) and Evidence (matches).* + +### 🔴 Red Flags (Concerns/Gaps) +* **{Concern 1}**: ... + * *Expert Challenge*: ... +* **{Concern 2}**: ... + +### 🟢 Green Signals (Highlights/Matches) +* **{Highlight 1}**: ... + * *Evidence*: ... + +--- + +## 3. Interview Battle Scripts + +### Part A: Pressure Validation (Past Performance) +*Forensic STAR follow-ups designed for Red Flags.* + +**Q1 (targeting {Concern 1})**: +* **The Setup**: "{Question...}" +* **The Drill**: "{Follow-up...}" + +### Part B: Future Projection (Future Scenario) +*Performance simulations designed for Outcomes.* + +**Q2 (targeting Outcome 1)**: +* **Scenario**: "{Set up a specific challenge highly relevant to future work...}" +* **Question**: "If this is the situation you face in your first week, how would you handle it?" +* **Bar Raiser**: "{What would an A Player do?}" vs "{What would a B Player do?}" + +--- + +## 4. Decision Matrix + +| Dimension | No Hire (Kill) | HIRE (Pass) | +| :--- | :--- | :--- | +| **Integrity** | ... | ... | +| **Competency Match** | ... | ... | diff --git a/skills/interview-prep/SKILL.md b/skills/interview-prep/SKILL.md new file mode 100755 index 0000000..1433e0c --- /dev/null +++ b/skills/interview-prep/SKILL.md @@ -0,0 +1,131 @@ +--- +name: interview-prep +description: 帮用户准备面试。基于目标 JD、公司、岗位方向,生成"高频面试题 + 参考回答 + 行为面 / 技术面 / Case 面分类题库",并产出可打印的『面试备战手册』。当用户说"帮我准备面试""明天有面试 / 后天面试""面试题""面经""模拟面试""我要面 X 公司 Y 岗位""帮我准备 STAR 故事""怎么回答这道面试题""自我介绍 / 离职原因 / 优缺点 怎么答",必须触发本 skill。请勿用本 skill 改简历(去 jd-resume-tailor / resume-builder)或推荐方向(去 job-intent-tracker)。 +--- + +# Interview Prep(面试准备) + +干 4 件事: + +1. **拆解面试场景**:明确目标公司 / 岗位 / 面试轮次(一面 / 二面 / 终面 / HR 面) +2. **生成题库**:从 4 类题库(行为面、技术面、Case 面、岗位特定)中按需取题 +3. **生成参考回答**:用 STAR / SCQA / MECE 等框架,结合用户简历事实 +4. **产出"备战手册"**:一份可打印的 .md / .docx / .pdf,含题目 + 参考思路 + 自检清单 + +--- + +## 何时触发 + +强信号: +- "帮我准备面试 / 模拟面试 / 模面" +- "明天 / 下周面试" +- 提到了具体公司 + 岗位 +- "高频面试题 / 经典面试题" +- "怎么回答 ___ 这个问题" +- "自我介绍怎么说 / 离职原因怎么答 / 期望薪资怎么谈" + +弱信号(先确认): +- 只说"想了解一下面试" → 问是哪个方向 / 什么轮次 + +--- + +## 工作流 + +### Step 1: 锁定面试场景 + +用 AskUserQuestion 收集: +- 目标公司(具体名字 / 大厂 vs 创业 vs 外资 / 公司类型) +- 目标岗位(精确到方向,如"用户增长 PM"而不是"产品经理") +- 面试轮次(一面 / 二面 / 终面 / HR 面 / 全流程演练) +- 时间紧迫度(明天 / 这周 / 下周以上)—— 影响"广度 vs 深度" +- 用户已有材料(JD / 简历 / 公司公开信息 / 已知面试官背景) + +如果用户给了 JD 和简历,**先调用 jd-resume-tailor 的 parse_jd.py** 抽出岗位关键信息,避免重复拆。 + +### Step 2: 选题策略 + +按"轮次 + 方向 + 时间"决定题库结构。读取对应文件: + +- 行为面(任何轮次都会问)→ `references/behavioral.md` +- 技术面(技术 / 数据岗主战场)→ `references/technical.md` +- Case 面(咨询 / 战略 / 高级 PM)→ `references/case.md` +- HR 面(任何岗位的最后一公里)→ `references/hr_round.md` +- 岗位特定题库: + - 互联网产品 / 运营 / PM → `references/role_internet.md` + - 技术 / 研发 / 数据 → `references/role_tech.md` + - 金融 / 咨询 / 商科 → `references/role_finance.md` + +题量推荐: +- **明天就面**:每类 5~8 题,重点高频题 +- **3~7 天**:每类 10~15 题 +- **>1 周**:完整覆盖 + 模拟轮次 + +### Step 3: 生成参考回答 + +**关键原则:参考回答必须基于用户简历里的真实经历,不是泛答。** + +调用 `scripts/star_story_builder.py`(如果有简历): + +```bash +python scripts/star_story_builder.py --resume resume.md \ + --questions questions.json --out answers.md +``` + +或者,如果没有简历,用通用回答框架(从 references/answer_frameworks.md 读取)作为模板,让用户填空。 + +参考回答的格式: +``` +【题目】___ +【考察点】HR / 面试官想看你的什么能力 +【回答框架】STAR / SCQA / MECE / 5W1H 之一 +【建议长度】X 分钟(一般 1.5~3 分钟) +【参考回答 (基于你简历里的 ___ 经历)】 + S: 当时的背景 / 问题 + T: 你的任务 / 目标 + A: 你的具体动作(重点,要细节) + R: 量化结果 + reflect +【可能的追问】1. ___ 2. ___ 3. ___ +``` + +### Step 4: 产出"备战手册" + +调用 docx skill 生成一份完整的 `interview_prep_<公司>_<岗位>.docx`,结构: + +``` +封面:公司 + 岗位 + 面试日期 + 倒计时 +1. 公司 / 岗位 速览(1 页) +2. 自我介绍(中 + 英两版,针对该岗位定制) +3. 行为面题库(X 题)+ 参考回答 +4. 技术面 / Case 面 题库(X 题)+ 参考思路 +5. 反向提问清单(你向面试官问什么) +6. 薪资谈判脚本 +7. 面试当天 Checklist(路线 / 着装 / 物品 / 心态) +``` + +**同时输出一份精简版 .md**,方便用户在地铁里 / 路上 review。 + +### Step 5: 模拟面试(可选 - 用户主动要才做) + +如果用户说"模拟一下",进入互动模式: +- 一次问 1 题 +- 等用户回答(语音 / 文字) +- 给反馈:内容 / 结构 / 表达 三维度 +- 如果用户说"换一题"或"再来一道难的",继续 + +模拟时**保持面试官人设**:用真实面试官的口吻、追问方式、压力点(但不要变成 attacker,要 constructive)。 + +--- + +## 反模式(不要做) + +- ❌ 给一份"通用 50 题"应付了事 → 必须按公司 / 岗位定制 +- ❌ 参考回答全是"我具有出色的沟通能力"这种空话 → 必须用具体故事 +- ❌ 不让用户自己练,全程"我替你答" → 错过模拟价值 +- ❌ 编造用户没有的经历做 STAR 故事 → 用户面试时会穿帮 +- ❌ HR 面问题答得太"训练痕迹重"(机械化) → 要给"3 个不同风格的版本"让用户选 + +## 与其他 skill 的协作 + +- 用户说"我简历不够强,先改简历再来面试" → 转 `jd-resume-tailor` 或 `resume-builder` +- 用户说"我还在犹豫要不要投" → 转 `job-intent-tracker` +- 面试结束后用户说"我想复盘一下" → 留在本 skill,进入"面试复盘模式",问表现、卡点、追问,帮用户为下一轮做准备 diff --git a/skills/interview-prep/references/answer_frameworks.md b/skills/interview-prep/references/answer_frameworks.md new file mode 100755 index 0000000..4e6e627 --- /dev/null +++ b/skills/interview-prep/references/answer_frameworks.md @@ -0,0 +1,97 @@ +# 面试回答框架速查 + +## STAR(行为面通用) + +``` +S - Situation:背景(30s) +T - Task:你的任务(20s) +A - Action:你的具体动作(90s,重点) +R - Result:量化结果 + 反思(30s) +``` + +## SCQA(开场陈述 / 自我介绍) + +``` +S - Situation:当前的状态 / 背景 +C - Complication:变化 / 问题 / 触发点 +Q - Question:核心问题 +A - Answer:你的答案 / 行动 +``` + +例:自我介绍可以用 SCQA: +- S: "我目前在 X 公司做 ___ 5 年" +- C: "上一份工作业务进入成熟期,我希望去做更早期 / 更有挑战的事" +- Q: "什么样的岗位最适合我下一步?" +- A: "贵司的 ___ 岗位 + 我的 ___ 经验,正好契合,所以来面试" + +## MECE(结构化分析) + +Mutually Exclusive, Collectively Exhaustive。 +拆问题时分类不重不漏。常见维度: +- 时间:短期 / 中期 / 长期 +- 用户:新 / 老 / 流失 +- 业务:自营 / 三方 / B 端 / C 端 +- 地理:一线 / 二线 / 下沉 +- 价格 / 量 / 渠道 + +## 5W1H(问题诊断) + +Who / What / When / Where / Why / How +任何模糊问题,先用 5W1H 澄清。 + +## CIRCLES(PM 案例题) + +C - Comprehend(澄清) +I - Identify customer(用户) +R - Report needs(需求) +C - Cut by priority(优先级) +L - List solutions(方案) +E - Evaluate trade-offs(取舍) +S - Summarize(推荐) + +## SOAR(适用 senior 候选人讲战略经验) + +S - Situation +O - Obstacle +A - Action +R - Result + +强调"突破障碍"的能力。 + +## 自我介绍 3 段式(最稳的) + +``` +1. "我是 ___,目前在 ___ 做 ___,到现在 ___ 年" +2. "我最近 / 最有代表性的一个项目是 ___,简单讲就是 ___,结果 ___" +3. "我了解贵司 ___ 业务,与我的 ___ 经验 / ___ 兴趣高度契合,所以来面试" +``` + +时长:1~1.5 分钟。 + +中文 + 英文版本都要准备(外企 / 投行 / 咨询 / 海外岗位常考英文)。 + +## 谈"缺点"的标准模板 + +``` +1. 真实的缺点(不是反向夸自己) +2. 你怎么察觉的 +3. 这个缺点带来过什么后果(说一个具体小事) +4. 你正在 / 已经在做什么改进 +``` + +例: +- "我以前在表达自己的反对意见时会过于含蓄,导致一次方案讨论里我不同意 PM 的方向,但没明说,结果上线后效果不好,我才意识到。" +- "现在我会刻意在每次方案讨论后写一段『我的反对意见 + 数据依据』给 PM,先打字再口头沟通,这样表达更清晰也不情绪化。" + +## 谈"职业规划"的标准模板 + +``` +1. 短期(1~2 年):在这个岗位做出 ___ 成果 +2. 中期(3~5 年):在 ___ 方向深耕,从 IC 到 senior IC / 小 lead +3. 长期:希望成为 ___ 领域的专家 / 业务 owner +``` + +避免: +- "做你们的总监"(太 ambitious 显得不真诚) +- "等我 5 年后看吧"(显得没规划) +- "创业 / 转行 / 出国"(HR 听了直接拒) diff --git a/skills/interview-prep/references/behavioral.md b/skills/interview-prep/references/behavioral.md new file mode 100755 index 0000000..610ca67 --- /dev/null +++ b/skills/interview-prep/references/behavioral.md @@ -0,0 +1,94 @@ +# 行为面题库(Behavioral Interview) + +行为面问题考察"过去如何"来预测"未来如何"。所有行为题用 STAR 答。 + +## 高频题 Top 20(中外企通用) + +### 自我类 +1. 自我介绍 / 简单介绍一下你自己 +2. 介绍你最有成就感的一个项目 +3. 介绍你失败 / 挫折最大的一次经历 +4. 你最大的优点 / 缺点是什么 +5. 你五年后的职业规划 + +### 协作 / 影响力 +6. 讲一个你跨部门 / 跨团队推动事情的例子 +7. 讲一次你和同事意见不合,最后怎么解决的 +8. 讲一次你说服别人改变想法的经历 +9. 讲一次你帮 / 带新人成长的经历 +10. 你怎么处理一个不配合的合作方 + +### 推动 / 执行 +11. 讲一次你在资源 / 时间不足的情况下完成目标 +12. 讲一次你主动推动一件事(没人要求你做) +13. 讲一次你做了一个艰难决策的经历 +14. 讲一次你 ownership 体现得最好的一次 +15. 讲一次你打破常规 / 创新的经历 + +### 失败 / 挫折 +16. 讲一次你犯过的最大错误及怎么处理 +17. 讲一次你接到负面反馈,怎么应对 +18. 讲一次你的项目失败 / 没达成目标,怎么复盘 +19. 讲一次你被 reject / 被否决的经历 + +### 学习 / 适应 +20. 讲一次你快速学会一个全新领域的经历 + +## STAR 回答模板 + +``` +S - Situation(背景,30 秒以内) +- 时间 / 地点 / 项目背景 +- 当时的关键约束(资源 / 时间 / 复杂度 / 干系人) + +T - Task(你的任务) +- 你具体要解决什么问题 / 达成什么目标 +- 一句话讲清"我的角色 + 我的目标" + +A - Action(你的具体动作,重点,60% 时长) +- 分 2~4 个步骤讲,每步有动词 + 决策依据 +- 强调"我做的"而不是"我们做的" +- 关键决策点要解释"为什么这么做" + +R - Result(结果 + 反思,30 秒) +- 量化结果(数字 / 百分比 / 排名) +- 1 句反思:你学到了什么 / 如果再做一次会怎么改 +``` + +## 高频题的"考察点"参考 + +| 题目 | 考察点 | +|------|-------| +| 自我介绍 | 表达能力、对岗位匹配度的自我认知 | +| 最成功的项目 | ownership、量化思维、复盘能力 | +| 最失败的项目 | 抗挫折、自我反思、成长型心态 | +| 优缺点 | 自我认知、坦诚、成长意识(缺点别答得像优点) | +| 跨部门推动 | 影响力、沟通、利益对齐能力 | +| 意见不合 | 同理心、说服力、底线思维 | +| 资源不足 | 创造力、优先级管理、边界谈判 | +| 主动推动 | ownership、商业 sense、推动力 | +| 接到负反馈 | 情商、成长型心态、改进行动 | +| 五年规划 | 职业稳定性、自驱力、与公司路径的契合度 | + +## 常见反模式(用户会答错的) + +- 答"优点"答得像招聘广告("勤奋、责任心强、抗压")→ 应该挑 1 个,配 1 个故事 +- 答"缺点"答得像反向夸自己("太追求完美""工作太投入")→ 必须真实,加上正在做的改进 +- "失败经历" 不敢说真失败 → HR 一眼识破,反而扣分 +- 答"五年规划" 直接说"做你们的总监" → 太 ambitious 不真诚;要结合公司业务和个人能力路径 +- 用大量"我们"代替"我" → 听不出你做了什么 +- 一个故事讲 5 分钟 → 控制在 2~3 分钟,留追问空间 + +## 故事矩阵(提前准备) + +让用户准备一个"故事矩阵",3~5 个故事可以回答 80% 的行为题: + +| 故事编号 | 项目名 | 主要展示能力 | 可以回答哪些题 | +|---------|------|------------|-------------| +| 故事 1 | XX 项目 | ownership + 推动力 | 最成功项目 / 主动推动 / 跨部门 | +| 故事 2 | XX 项目 | 数据驱动 + 决策 | 艰难决策 / 数据驱动 / 创新 | +| 故事 3 | XX 项目 | 失败 + 复盘 | 最失败 / 负反馈 / 学习 | +| 故事 4 | XX 项目 | 沟通 + 说服 | 意见不合 / 说服别人 | +| 故事 5 | XX 项目 | 跨界 / 学习 | 快速学习 / 跨界 | + +每个故事提前打磨"S 30s + T 30s + A 90s + R 30s"四段,面试时按题目灵活组合。 diff --git a/skills/interview-prep/references/case.md b/skills/interview-prep/references/case.md new file mode 100755 index 0000000..b5edae2 --- /dev/null +++ b/skills/interview-prep/references/case.md @@ -0,0 +1,93 @@ +# Case 面题库(Case Interview) + +适用:咨询(MBB / Big4 战略)、战略 / 商分岗位、高级 PM 面试。 + +## 经典 case 类型 + +1. **Market Sizing(市场规模估算)** + - 中国一年卖多少杯咖啡? + - 上海有多少辆出租车? + - 抖音一天产生多少视频? +2. **Profitability(盈利分析)** + - 客户利润下降,怎么诊断? +3. **Market Entry(市场进入)** + - 某连锁品牌要进入印度,建议吗? +4. **M&A(并购)** + - 某公司想收购竞争对手,做不做? +5. **Growth Strategy(增长策略)** + - 某 SaaS 公司想 3 年 ARR 翻倍,怎么走? +6. **Operations(运营优化)** + - 某物流公司成本太高,怎么优化? + +## Market Sizing 框架 + +``` +Top-down: 总人口 → 目标用户比例 → 频次 → 单价 → 总市场 +Bottom-up: 单店销量 → 城市门店数 → 城市数 → 总市场 + +关键: +1. 假设要明确(性别 / 年龄 / 城市分层 / 频次) +2. 数字要可验算(路演时面试官可能让你重算) +3. 给一个 sanity check("这个数对比 X 行业大概是 Y 倍,看起来合理") +``` + +## Profitability 框架 + +``` +利润 = 收入 - 成本 + +收入端: +- 价格 × 销量 +- 销量 = 客户数 × 客单价 × 频次 +- 客户数 = 市场规模 × 渗透率 + +成本端: +- 固定成本(人工 / 租金 / 设备) +- 变动成本(原材料 / 物流 / 营销) + +诊断顺序: +1. 量价拆分(量降还是价降?) +2. 区域 / 产品 / 客群 拆分(哪一块下滑最多) +3. 与行业对比(是行业问题还是公司问题) +4. 给出 2~3 个改进方向 +``` + +## Market Entry 框架(4C / 5C) + +``` +Customer:目标客户、需求、规模、增长 +Competition:现有玩家、市场份额、竞争激烈度、进入壁垒 +Company:自身能力(产品 / 渠道 / 品牌 / 资金)是否匹配 +Channel:渠道选择(直营 / 加盟 / 经销 / 线上) +Cost-benefit:投入产出测算 + 时间表 + +关键:必须给"做 / 不做"的明确答案,不要"看情况" +``` + +## Case 面回答原则 + +1. **先 take a moment**:拿到题不要急着答,花 1~2 分钟列框架(面试官期待这个) +2. **MECE 框架**:分类要 Mutually Exclusive, Collectively Exhaustive +3. **Hypothesis-driven**:基于一个假设展开分析,而不是穷举 +4. **Number first**:所有结论都要有数字支撑 +5. **沟通节奏**:每展开一个分支前,先告诉面试官"我打算先看 X 再看 Y" +6. **主动总结**:每个分支结束做小结,最后给 final recommendation + +## 常见误区 + +- 不澄清需求就开干 → 面试官期待你"先 clarify" +- 框架太机械(生硬套 4P / SWOT,看不到思考) +- 数字估算时单位混乱(百万 vs 亿)→ 用清楚单位 +- 不主动 sanity check +- 跑得太快,面试官没法跟上你的逻辑 +- 给建议时不写具体动作("加强营销"是废话,"在 X 渠道投放 Y 类型内容,预算 Z"才是建议) + +## 高级 PM Case 高频题(不是咨询,是 PM 战略题) + +1. 你怎么衡量微信视频号成功 +2. 美团外卖准备进入印尼,你的 product strategy 是什么 +3. ChatGPT 推出后,你作为 Notion PM 会做什么 +4. 给一个产品设计 north star metric +5. 抖音直播 GMV 卡住了,你怎么 diagnose + +PM Case 关键:必须有用户思维 + 业务思维 + 数据思维 同时在线,不要只讲框架,要讲到具体功能 / 实验设计。 diff --git a/skills/interview-prep/references/hr_round.md b/skills/interview-prep/references/hr_round.md new file mode 100755 index 0000000..3d741da --- /dev/null +++ b/skills/interview-prep/references/hr_round.md @@ -0,0 +1,103 @@ +# HR 面 / 终面题库 + +HR 面通常在所有专业面之后,关注:稳定性、性格匹配、薪资期望、入职意愿。 + +## 高频问题 + 推荐答法 + +### 1. 你为什么离职 / 想换工作? + +❌ 错:吐槽前公司 / 同事 / 老板 +✅ 对:聚焦"成长 / 发展 / 业务方向" 三选一 +- "我目前的业务相对成熟,希望挑战 0-1 的环境" +- "上一份工作积累了 ___ 经验,下一步想往 ___ 方向深入,贵司这个岗位正好契合" +- "团队结构调整,业务方向不再是我的兴趣点,所以想找新的发展" + +### 2. 你为什么选我们公司 / 这个岗位? + +❌ 错:贵公司平台大 / 福利好 +✅ 对:show 你做了功课 +- 公司层面:业务 / 战略方向 / 你看好的趋势 + 1~2 个你具体了解的产品 / 案例 +- 岗位层面:JD 里的 ___ 与你的 ___ 经验高度契合 + +### 3. 你期望的薪资是多少? + +策略:**永远不要先开口给一个数** +- 如果可以反问:先问"贵司这个岗位的薪资带是多少?" +- 如果必须给:给一个 range(不要给点),比如"40~55k × 14",下限略高于你能接受的最低 +- 如果对方说"无可奉告,必须你先报":基于市场数据 + 当前涨幅 30~50% 给一个略高的数 + +更详细的薪资谈判脚本见 `salary_negotiation.md`。 + +### 4. 你拿了几个 offer?什么进展? + +策略:**不撒谎,但策略性地说** +- 有真实 offer:直接说,"目前手上有 X 公司的 ___ offer,基本盘是 ___" +- 在面其他公司:说"还在面 X 类公司,预计 X 周内有结果",给对方紧迫感 +- 完全没有:说"目前在重点投这一类岗位,进展中" + +### 5. 我们 / 这个岗位有没有什么让你担心的? + +策略:**给一个真实但可解决的担忧** +- "对 ___ 业务的快速迭代节奏,我需要一段时间适应" +- "团队规模比我之前的大很多,如何融入是我会主动想的事" + +不要答"没有任何顾虑"——显得不真诚。 + +### 6. 你最快什么时候能入职? + +❌ 错:随时(显得急) +✅ 对:基于实际 + 给一点缓冲 +- 在职:依法 30 天交接 + 1 周休息 = 5~6 周 +- 待业:2 周左右 +- 应届:可按毕业时间 / 学校要求 + +### 7. 你最近读了什么书 / 最近在学什么 / 业余爱好? + +策略:**展示自驱力 + 个性**,不要硬凹"成长性" +- 1 本与岗位有点关系的书 / 课程 +- 1 个真实的爱好(运动 / 艺术 / 写作 / 旅行 / 编程副业) + +### 8. 你的优点 / 缺点(HR 复问版) + +HR 复问通常更深,要求"举例": +- 优点:1 个核心优点 + 1 个具体故事 + 这个优点带来的成果 +- 缺点:1 个真实缺点 + 自我察觉的过程 + 正在做的改进 + +⚠️ 缺点不要选"完美主义、太投入工作、太认真"——这些会被识别为"反向夸自己"。 +推荐方向:表达 / 委派 / 公开演讲 / 优先级控制(这些是真缺点但可改进)。 + +### 9. 你的 5 年 / 10 年规划 + +策略:**与公司路径绑定 + 留出灵活性** +- "未来 3~5 年我希望在 ___ 方向深耕,从 IC 成长为能带小团队的 senior" +- "5 年后希望能独立负责一条业务线 / 子产品" +- 不要说"自己创业""转行" / "出国" / "做自媒体" + +### 10. 还有什么想问我们的? + +**强信号题**——必须问问题,不能说"没了"。 + +参考问题(按 HR / 主管 / 终面分层): + +**问 HR:** +- 这个岗位的 onboarding 一般是怎么安排的? +- 团队的组成、和我对接的同事的背景能介绍下吗? +- 公司近期的业务重点 / 战略方向是什么? + +**问主管 / 业务面试官:** +- 您觉得这个岗位最大的挑战是什么? +- 团队最近的 OKR 是什么? +- 您觉得在这个岗位做得好的人,半年 / 一年通常会做出哪些成果? + +**问终面 / 大老板:** +- 您看好的下一个三年的业务方向是什么? +- 这个岗位 / 这条业务线在公司战略里的位置? +- 您在公司最自豪的一个决策是什么?(拉近距离) + +❌ **不要问的**:薪资细节(HR 会主动给)、加班 / 出差(显得在挑刺)、五险一金(显得不专业) + +## HR 面综合心态 + +- HR 看似友好,实际在筛"性格 / 稳定性 / 文化匹配",每句话都是评估 +- 答题保持"克制 + 真实",不要为了讨好把答案变得不像自己 +- HR 一般不决定 offer 给不给,但能 **veto** —— 别在最后一公里翻车 diff --git a/skills/interview-prep/references/role_finance.md b/skills/interview-prep/references/role_finance.md new file mode 100755 index 0000000..532991f --- /dev/null +++ b/skills/interview-prep/references/role_finance.md @@ -0,0 +1,103 @@ +# 金融 / 咨询 / 商科 面试专题 + +## 投行 / IBD + +### Technical 题 +1. 三大报表勾稽关系 +2. WACC / DCF 公式 + 关键假设 +3. LBO 模型核心要素 + 退出回报怎么算 +4. M&A 交易中 accretion / dilution 分析怎么做 +5. 用 5 种以上方法估值一家公司 +6. EV vs Equity Value 区别 + 怎么转换 +7. 同行业可比公司怎么选 + 用什么倍数 +8. PE / PB / EV/EBITDA 各自适用场景 +9. 折旧、摊销、利息、营运资本变动 对现金流的影响 +10. 一家公司 IPO 流程是怎样的(A / 港 / 美股) + +### Fit 题(中外资差异) +- 你为什么想做投行 +- 你愿意接受 100 小时 / 周的工作强度吗 +- 你最近 follow 的一个 deal 是哪个,谈谈你的看法 +- 给你一个新的 sector,你怎么从 0 开始研究 +- 一个分析模型出错了,但 deck 已经发出去了,你怎么处理 + +### 行业 deal 题(必准备 1~2 个) +准备 1~2 个最近的 sector deal,能讲清楚: +- 交易结构(买方 / 卖方 / 估值 / 支付方式) +- 战略意义 +- 估值倍数 / 溢价 +- 市场反应 +- 你的看法 + +## 咨询(Consulting) + +### Case Interview(已在 case.md) +重点训练: +- Market sizing(每家必问) +- Profitability +- Market entry +- Growth strategy + +### Fit 题 +- 你为什么想做咨询 +- 你为什么选 MBB(或我们这家) +- 讲一个你 lead / 推动事情的经验 +- 讲一个你和团队意见不合的经验 +- 你的 long-term career goal + +### Personal Experience Interview(PEI)—— McKinsey 必考 +PEI 三大类: +1. **Personal Impact**(你说服 / 影响别人) +2. **Entrepreneurial Drive**(你主动推动事情) +3. **Inclusive Leadership**(你带领多元团队) + +每类准备 1~2 个故事,按 STAR 讲,每个故事 5~6 分钟(McKinsey 喜欢深挖)。 + +## 量化(Quant) + +### 概率 / 数学题(高频) +1. 一根均匀的木棍随机折两次,能拼成三角形的概率 +2. 蒙提霍尔问题(三门) +3. 醉汉问题(n 步后回到原点的概率) +4. 给你一个不公平硬币,怎么模拟公平硬币 +5. 期望 / 方差 / 协方差 计算题 +6. 布朗运动 / Ito 公式 / Black-Scholes 推导(高级) +7. Optimal stopping(秘书问题) + +### 编程题 +1. LeetCode 中等 / 困难(数组 / 字符串 / DP) +2. 写一个回测框架(伪代码即可) +3. C++ / Python 一些常考语言细节 + +### 策略题 +1. 你了解的策略类型有哪些 +2. 你做过的一个完整策略(信号 → 组合 → 回测 → 上线) +3. 怎么处理过拟合 / out-of-sample 失效 +4. 怎么估算策略容量 +5. Sharpe / Sortino / Calmar / Max DD 各自含义和用法 + +### Brain Teaser(脑筋急转弯) +- 100 个囚犯帽子问题 +- 海盗分金币 +- 8 个球称重找不同的(必备) +- 烧绳子计时 + +## 公募 / 私募 研究员 + +### 行业研究题 +1. 你 cover 哪个行业?目前的核心矛盾是什么? +2. 你最看好 / 最不看好的 1 只股票,逻辑是什么 +3. ___ 公司最近股价涨了 / 跌了 X%,怎么解释 +4. 给你一个新行业,你怎么从 0 开始研究 +5. 你怎么判断管理层质量 +6. 行业景气度 vs 公司基本面,哪个更重要 + +### 估值题 +1. ___ 公司你给多少估值,用什么方法 +2. PE 30x 算贵吗?怎么判断 +3. 价值 vs 成长 vs 周期 各自怎么估 +4. DCF 在 ___ 行业能不能用?为什么 + +### 投资逻辑表达 +- 30 秒电梯演讲:3 句话讲清一只股票的投资逻辑 +- 5 分钟深度:背景 + 催化剂 + 风险 + 估值 + 时间 diff --git a/skills/interview-prep/references/role_internet.md b/skills/interview-prep/references/role_internet.md new file mode 100755 index 0000000..a9a4684 --- /dev/null +++ b/skills/interview-prep/references/role_internet.md @@ -0,0 +1,80 @@ +# 互联网产品 / 运营 / PM 面试专题 + +## 产品经理(PM) + +### 高频题 +1. 介绍一款你最喜欢的产品 / APP,分析它的优势和不足 +2. 如果让你优化 ___(具体产品),你会怎么做 +3. 怎么衡量一个产品 / 功能的成功 +4. 一个新功能上线后留存提了 5%,怎么归因 +5. 你怎么定义产品的 north star metric +6. 设计一个 ___ 功能(如:抖音的"不感兴趣"按钮) +7. 给你一个用户反馈集合,怎么决定要做哪些 +8. PRD 写过吗?怎么写一个高质量的 PRD +9. 你怎么和开发 / 设计 / 运营 协作 +10. 你做的最得意的一个数据驱动决策是什么 + +### PM 案例题答题套路(CIRCLES 框架) +``` +C - Comprehend the situation(澄清问题) +I - Identify the customer(明确目标用户) +R - Report the customer's needs(用户痛点) +C - Cut, through prioritization(功能优先级) +L - List solutions(具体方案 2~3 个) +E - Evaluate trade-offs(取舍) +S - Summarize recommendation(最终建议) +``` + +### PM 面试官想看 +- 用户思维(不是站在自己角度) +- 数据敏感度(你怎么用数据说服 / 验证) +- 业务理解(不只是功能,还有 unit economics) +- 跨职能沟通(你怎么处理与开发 / 设计的冲突) +- ownership(你 own 一个功能从设计到上线到复盘的全流程) + +## 运营 + +### 高频题 +1. DAU 跌了 X%,怎么排查 +2. 给你 100 万预算,做 ___ 活动,你怎么规划 +3. 怎么搭建一个用户分层运营体系 +4. 内容运营 / 用户运营 / 活动运营 你最想做哪个,为什么 +5. 你怎么衡量一个活动的 ROI +6. 你做过的爆款活动 / 内容,复盘一下 +7. 私域 / 公域 你怎么打配合 +8. 怎么提升新用户次留 / 7 留 / 30 留 +9. 你怎么和产品 / 数据 / 设计 协作 +10. 一个失败的活动,你怎么复盘 + +### 运营常用框架 +- AARRR / RARRA:用户生命周期 5 个阶段 +- 活动复盘:目标 - 策略 - 执行 - 数据 - 反思 +- 内容运营三板斧:选题 - 制作 - 分发 - 数据反馈 + +### 运营面试官想看 +- 数据敏感度(每个动作配数字) +- 用户洞察(不是"我觉得",是"用户说") +- 资源整合(怎么撬动第三方 / 跨部门) +- 闭环思维(活动结束 ≠ 工作结束,复盘 + 沉淀 SOP) +- 创意 + 落地(既能想点子,又能落地到 SOP) + +## 项目经理 / TPM + +### 高频题 +1. 你怎么管理一个 ___ 人 / ___ 月的项目 +2. 项目延期了,你怎么办 +3. 跨部门 / 跨时区 / 跨语种 协作的挑战和经验 +4. 你怎么管理项目风险 +5. 敏捷 vs 瀑布,你怎么选 +6. Scrum / Sprint planning / Retro 你怎么主持 +7. 怎么和 stakeholder 同步进度(汇报方式) +8. 一个 stakeholder 不配合,你怎么 escalate +9. 你管过的最大 / 最复杂的项目讲讲 +10. 项目失败 / 没达成目标,你怎么复盘 + +### TPM 面试官想看 +- 结构化思维(项目拆解、依赖管理) +- 沟通能力(向上 / 平行 / 向下) +- 风险预判(不是事后救火,是事前预防) +- 数据 / 工具熟练度(Jira / 飞书 / 自建 dashboard) +- 推动力(drive 一群你管不到的人) diff --git a/skills/interview-prep/references/role_tech.md b/skills/interview-prep/references/role_tech.md new file mode 100755 index 0000000..2e2b776 --- /dev/null +++ b/skills/interview-prep/references/role_tech.md @@ -0,0 +1,83 @@ +# 技术 / 研发 / 数据 面试专题 + +通用技术八股 + 系统设计在 `technical.md`,本文件聚焦: +- 项目深挖话术 +- 算法 / LLM / 数据 岗位特定题 +- 团队 / 软技能题(技术岗也要答) + +## 项目深挖(每家必问) + +面试官最喜欢"刨根问底",一定要准备。 + +每个项目至少能回答以下 8 个问题: + +1. **背景** —— 这个项目为什么要做?业务问题是什么? +2. **你的角色** —— 你在团队里是什么角色?做了 ___ 模块? +3. **技术选型** —— 为什么选 X 而不是 Y?trade-off 是什么? +4. **挑战** —— 遇到的最难的技术问题是什么?怎么解决的? +5. **数据** —— 系统的 QPS / 数据量 / 延迟 / 可用性 是什么? +6. **结果** —— 上线后效果怎么样?比基线提升 ___? +7. **失败 / 坑** —— 踩过什么坑?怎么 debug?怎么避免? +8. **改进** —— 现在让你重新做这个项目,会怎么做? + +## 算法 / 机器学习 岗位特定 + +### 业务理解题 +1. 给你一个 ___ 业务,你怎么定义一个 ML 问题 +2. 你怎么和业务 / 产品 / 数据 协作 +3. 模型上线后效果不及预期,你怎么排查 +4. 模型 vs 规则,什么时候选哪个 +5. 你做的模型,下线 / 替代过吗?为什么 + +### 算法岗"假大模型 / 真不深"识别题 +- 你说会 RLHF,那 PPO 的 KL 项是怎么加的?为什么这么加? +- 你做了 RAG,召回准确率怎么算的?不同分块策略对比过吗? +- 你说做了 Agent,你的 planning 模块是 ReAct / CoT / 还是自定义?错误恢复怎么做? + +防身:**只写真做过的项目**,编造的 LLM 项目最容易翻车。 + +## 数据 / 数仓 / 分析 岗位特定 + +### 业务题(强烈高频) +1. 你怎么衡量一个 ___ 业务的健康度 +2. DAU 跌了 5%,你怎么 diagnose +3. 你做的最有价值的一个数据分析项目 +4. 你怎么和业务方 / 产品 / 老板 沟通分析结论 +5. 给你一个数据需求:"分析下用户为什么流失",你怎么做 + +### SQL 现场题(必考) +- 写一段 SQL:每个用户的连续登录天数 +- 写一段 SQL:每个商品的复购率 +- 写一段 SQL:找出最近 7 天每天的新增用户 +- 写一段 SQL:A/B 测试结果对比 + 显著性 + +提示:**面试现场写 SQL 一定要边写边讲思路**,别闷头写。 + +### 数仓建模题 +- 你怎么设计一个 ___ 业务的数据模型 +- 维度建模 vs 范式建模 怎么选 +- 拉链表 / 缓慢变化维度 怎么处理 +- 数据质量怎么保障(监控 / 巡检 / 复盘) + +## 软技能题(技术岗也要答) + +技术岗以为只考技术 = 大错。中级以上会问: + +1. 跟其他团队 / 产品经理 / 设计师 意见不合,你怎么办 +2. 你怎么 review 别人的代码 / 文档 +3. 你怎么带新人 +4. 一个技术决策没人响应,你怎么推动 +5. 你接手了一个屎山代码,怎么改造 +6. 你怎么管理自己的技术成长 + +## 反向提问(技术面 / leader 面) + +技术面: +- 团队的技术栈和未来规划是什么? +- 团队最近的技术挑战是什么? +- code review / on-call / 周会 流程是怎样的? + +Leader 面: +- 您怎么看 ___ 技术 / 业务方向的未来? +- 团队的技术债务现在怎么样? +- 您觉得在这个团队最有挑战的事情是什么? diff --git a/skills/interview-prep/references/salary_negotiation.md b/skills/interview-prep/references/salary_negotiation.md new file mode 100755 index 0000000..d0c8b64 --- /dev/null +++ b/skills/interview-prep/references/salary_negotiation.md @@ -0,0 +1,99 @@ +# 薪资谈判脚本 + +## 心法 + +- 永远不要先开口给一个数(让对方先报) +- 给数字时给 range,下限略高于你的最低接受 +- 谈判全程保持"希望加入" + "理性 base on 市场" 的语气 +- 不要 disclose 你的当前 / 历史薪资(除非法律 / 当地惯例要求) +- 多个 offer 是最大筹码,但不要明示要"涨多少",让对方主动提 + +## 阶段 1:HR 初次问期望薪资 + +**HR**:"你期望的薪资是多少?" + +**你(首选)**: +"贵司这个岗位的薪资带是多少?我希望对齐市场和岗位职级。" + +**HR(如果说不告诉你)**: +"我们想先听你的期望,再做匹配。" + +**你**: +"基于市场行情和我的 ___ 年经验,我希望的范围是 X~Y k × ___ 月,最终可以基于贵司的整体 package 综合谈。" + +X 的算法:(当前年包 ÷ 12 ÷ 月数)× 1.3~1.5,向上取整到 5k。 +Y 的算法:X × 1.2。 + +例:当前 30 × 16 = 480k 年包。月薪 = 480/14 ≈ 34k。期望 X = 34 × 1.3 = 44k → 给 45~55k × 14。 + +## 阶段 2:HR 给口头 offer + +**HR**:"我们能给你 50k × 14,年包 700k。" + +**你(不要立刻接)**: +"感谢,让我先了解下整体 package 的细节,包括签字费、股票 / RSU、年终奖结构、五险一金基数、加班 / 出差补贴。" + +收到详细信息后: +- 如果**整体满意**:可以表达"基本符合预期,让我和家人商量 1~2 天" +- 如果**期望更高**:进入阶段 3 谈判 + +## 阶段 3:谈高 offer + +**你**: +"非常感谢您的 offer,我对岗位和团队都很认可。基于我目前正在 finalize 的 X 公司类似岗位的 offer / 我的当前 package + 跳槽涨幅期望,我希望能在月薪 / 签字费 / RSU 上有进一步空间,具体是 ___。" + +要点: +- **永远给具体数字**(不要说"再高一点") +- **给一个 reason**(其他 offer / 当前涨幅 / 市场行情) +- **只谈 1~2 个核心要素**(不要每项都谈) +- **不要威胁**(不要说"否则我去 X 公司") + +## 阶段 4:HR 拒绝再加 + +**HR**:"这个数字已经是我们的 maximum,无法再加。" + +**你**: +- 如果 base 已经能接受:尝试争取签字费 / 多一档股票 / 多 5 天年假 / 入职时间调整 +- 如果整体不能接受:感谢 + 说"我需要时间考虑",不要当场拒绝 + +## 阶段 5:手握多个 offer + +最理想:3 个 offer 在手,且各家都知道你在考虑其他 + +**与 A 公司**:"X 公司给了 50k × 14 + 100k 签字 + 200k RSU/年,我个人更倾向贵司 / 这个岗位,但希望整体 package 至少 match。" + +**与 B 公司**:同上但换数字 + +⚠️ 不要伪造 offer(HR 圈子小,可能被发现) + +## 应届生薪资谈判 + +应届生议价空间小,但仍有: +- 不同 offer 互比 +- 签字费可以争(很多公司有 flex) +- 入职时间可以争(毕业延后) +- 工作地点 / base 转换 可以争 + +应届生**不要**:因为期望太高拒掉所有 offer。第一份工作的"平台 + 业务 + leader"比薪资重要。 + +## 常见雷区 + +- "我愿意降薪过来" → 永远不要主动说,HR 会觉得你"贱卖" + "需求被低估" +- "我对薪资不敏感" → HR 听了真的会少给 +- "你们随便给个数" → 把决定权交给 HR,等于自杀 +- "如果不到 X 我就拒" → 太 hardcore,谈崩 +- 把家里 / 房贷 / 个人困难当谈判筹码 → 不专业 + +## 收到 offer 之后的 checklist + +- [ ] 确认 base 月薪 + 月数 +- [ ] 确认签字费 / 一次性奖金 +- [ ] 确认股票 / RSU / 期权(数量、归属期、估值方式) +- [ ] 确认年终奖结构(保底 / 固定 / 浮动) +- [ ] 确认五险一金基数(很多公司低基数缴费,影响实际收入) +- [ ] 确认补充医疗 / 商业保险 +- [ ] 确认年假 / 调休 / 带薪病假 +- [ ] 确认加班 / 出差补贴 +- [ ] 确认背景调查范围 +- [ ] 确认 offer letter 书面文件 +- [ ] 入职时间(30 天交接 + 1 周休息) diff --git a/skills/interview-prep/references/technical.md b/skills/interview-prep/references/technical.md new file mode 100755 index 0000000..8eb083c --- /dev/null +++ b/skills/interview-prep/references/technical.md @@ -0,0 +1,100 @@ +# 技术面题库 + +适用:研发 / 算法 / 数据 / DevOps 岗位的 1~2 面(技术轮)。 + +按职级分层: +- **初级(0-3 年)**:基础八股 + 简单算法 + 项目细节 +- **中级(3-5 年)**:系统设计 + 复杂场景题 + 项目深度追问 +- **高级(5+ 年)**:架构权衡 + 跨团队问题 + 业务理解 + +## 一、后端 / 服务端 + +### 基础八股(每家都问) +1. HTTP / HTTPS / TLS 流程,session vs cookie vs token +2. TCP 三握四挥,为什么是 4 次挥手 +3. MySQL 索引底层(B+ 树)、何时失效、最左前缀 +4. MySQL 事务隔离级别 + 各自解决什么问题 +5. Redis 持久化(RDB / AOF)+ 主从 + 哨兵 + 集群 +6. Redis 缓存击穿 / 穿透 / 雪崩 怎么解 +7. 分布式锁 3 种实现(Redis / Zookeeper / 数据库)+ 各自坑 +8. Kafka 怎么保证消息不丢、不重复、有序 +9. JVM 内存模型 + GC(Java 岗)/ Goroutine 调度(Go 岗) +10. 一致性哈希、CAP、BASE、Raft / Paxos 简述 + +### 系统设计高频 +- 设计一个秒杀系统(QPS 10w) +- 设计一个短链服务(生成 / 重定向 / 统计) +- 设计一个分布式 ID 生成器 +- 设计微博 feed 流(推 / 拉 / 推拉结合) +- 设计一个限流器(令牌桶 / 漏桶 / 滑动窗口) + +### 系统设计回答框架 +``` +1. 澄清需求(问清 QPS、读写比、数据量、SLA) +2. 估容量(QPS × 平均请求大小 = 带宽;数据量 × 时间 = 存储) +3. 高层架构(接入层 → 业务层 → 存储层 → 数据流) +4. 关键模块深挖(讲清楚选型为什么) +5. 扩展性 / 高可用 / 容灾 +6. 主动指出 trade-off +``` + +## 二、前端 + +1. 浏览器渲染流程(DNS → TCP → TLS → HTTP → HTML 解析 → 布局 → 绘制) +2. 事件循环 + 微任务 / 宏任务 +3. 闭包、原型链、this 绑定 +4. React / Vue 响应式原理 +5. 性能优化(FCP / LCP / CLS 指标 + 实操) +6. SSR vs CSR vs SSG,何时用哪个 +7. 跨域方案(CORS / JSONP / 反向代理) +8. webpack / vite 区别 + 原理 + +## 三、算法 / 机器学习 + +### 基础 +1. 偏差 / 方差,过拟合怎么解 +2. L1 / L2 正则的区别 +3. 随机森林 vs GBDT vs XGBoost +4. 梯度消失 / 爆炸 + 解决(BatchNorm / 残差 / 激活函数) +5. Attention 机制讲清,Transformer 与 RNN 对比 +6. 样本不平衡处理(采样 / 类权重 / Focal Loss) +7. 模型评估:准确率、精确率、召回率、AUC、F1,何时用哪个 + +### LLM / GenAI 方向(2024+ 热门) +1. SFT vs RLHF vs DPO,原理 + 何时用 +2. RAG 完整 pipeline:分块策略、Embedding 选型、检索融合、Reranker +3. Agent 的 tool use / planning / memory,与单 LLM 的区别 +4. 长上下文(YARN / RoPE 缩放、Context window 扩展) +5. 推理优化:KV cache、speculative decoding、量化 +6. 大模型评估:benchmark、人工评估、LLM-as-Judge 各自坑 + +### 项目深挖(每个面试官必问) +- 你这个模型为什么选 ___ 而不是 ___ +- 数据是怎么标注 / 清洗的,量级 +- 训练用了什么硬件、跑了多久、超参怎么调 +- 上线后效果怎么评估,与基线对比 +- 失败 / bug 的 case,怎么 debug + +## 四、数据 / 数仓 / 分析 + +### SQL(必考) +- 行转列 / 列转行 / 透视 +- 窗口函数(rank / dense_rank / lead / lag) +- 计算 7 日 / 30 日活跃用户 +- 同 user 连续登录天数 +- 留存率 / 漏斗 / 同期群分析 +- 性能优化(索引、explain、分区、避免子查询) + +### 业务题 +- 给你一个业务场景,让你设计指标体系 +- DAU 跌了 5%,怎么排查 +- 设计一个 A/B 测试(样本量计算、显著性、停损) +- 用户分层模型(RFM / 生命周期) + +## 五、技术面回答原则 + +1. **澄清问题再答**:模糊的题先问 1~2 个澄清问题 +2. **结构化输出**:分点 / 分模块讲,不要意识流 +3. **trade-off 思维**:选 A 而不选 B 时,主动说"因为 ___" +4. **诚实**:不会的题说"这个我没研究过,但根据我对 X 的理解,可能可以这样想 ___" +5. **画图 / 写代码**:白板题写代码先讲思路,再写,最后跑测试用例 diff --git a/skills/interview-prep/scripts/star_story_builder.py b/skills/interview-prep/scripts/star_story_builder.py new file mode 100755 index 0000000..b2004fe --- /dev/null +++ b/skills/interview-prep/scripts/star_story_builder.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +""" +star_story_builder.py — 从简历文本里抽出工作 / 项目经历,生成"故事矩阵"骨架 + +用法: + python star_story_builder.py --resume resume.md --out stories.md + +输出一份 markdown,包含: +- 检测到的 N 个经历(按时间倒序) +- 每个经历的 STAR 骨架占位(让用户 / 模型补全) +- 每个故事可以回答的行为题清单 +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + + +COMMON_BEHAVIORAL_QUESTIONS = { + "ownership": [ + "讲一次你 ownership 体现得最好的经历", + "你做过的最有成就感的项目", + "讲一次你主动推动事情", + ], + "collab": [ + "讲一次你跨部门 / 跨团队推动事情", + "讲一次你和同事意见不合,怎么解决", + "讲一次你说服别人改变想法", + ], + "challenge": [ + "讲一次你在资源 / 时间不足下完成目标", + "讲一次你做艰难决策的经历", + "讲一次你打破常规 / 创新的经历", + ], + "failure": [ + "讲一次你失败 / 没达成目标的经历", + "讲一次你犯过的最大错误", + "讲一次接到负面反馈,怎么应对", + ], + "learning": [ + "讲一次你快速学会全新领域的经历", + "你最近学到的最重要的事是什么", + ], +} + + +def load_resume_text(path: Path) -> str: + suffix = path.suffix.lower() + if suffix in {".md", ".txt"}: + return path.read_text(encoding="utf-8") + if suffix == ".docx": + try: + from docx import Document + except ImportError: + print("✗ 缺少 python-docx", file=sys.stderr) + sys.exit(1) + return "\n".join(p.text for p in Document(str(path)).paragraphs) + print(f"✗ 暂不支持 {suffix}", file=sys.stderr) + sys.exit(1) + + +def extract_experiences(text: str) -> list[dict]: + """ + 简易抽取:找形如 "公司 | 岗位 | 时间" 或 "项目名 | ..." 的行作为锚点, + 然后取该行后面、下一个锚点之前的内容作为经历内容。 + """ + lines = text.splitlines() + experiences: list[dict] = [] + current = None + + # 匹配锚点(含 | 或 |,且包含日期/年份) + anchor_re = re.compile( + r"^(?P[^\n]+?[||][^\n]+?[||][^\n]+)$" + ) + + for line in lines: + if anchor_re.match(line.strip()): + if current and current["bullets"]: + experiences.append(current) + current = {"title": line.strip(), "bullets": []} + else: + stripped = line.strip() + if current and stripped.startswith(("-", "*", "•")): + current["bullets"].append(stripped.lstrip("-*• ")) + if current and current["bullets"]: + experiences.append(current) + + return experiences + + +def categorize_story(bullets: list[str]) -> list[str]: + """根据 bullet 关键词,判断这个故事最适合回答哪一类行为题。""" + text = " ".join(bullets).lower() + cats = [] + if any(k in text for k in ["主导", "owner", "推动", "drive", "lead", "0-1"]): + cats.append("ownership") + if any(k in text for k in ["跨部门", "跨团队", "协作", "对接", "合作"]): + cats.append("collab") + if any(k in text for k in ["紧急", "时间紧", "资源", "决策", "突破"]): + cats.append("challenge") + if any(k in text for k in ["失败", "下线", "回滚", "复盘", "教训", "踩坑"]): + cats.append("failure") + if any(k in text for k in ["新", "学习", "陌生", "首次", "从零"]): + cats.append("learning") + return cats or ["ownership"] + + +def render(experiences: list[dict]) -> str: + if not experiences: + return ( + "# 故事矩阵(未检测到经历)\n\n" + "无法从简历里自动抽取出工作 / 项目经历。可能是因为:\n" + "1. 简历格式不是 `公司 | 岗位 | 时间` 的常见结构\n" + "2. 经历用普通段落写,没有明显的锚点\n\n" + "建议:手工告诉我你最有代表性的 3~5 段经历,我来帮你做 STAR 拆解。\n" + ) + + out = ["# 故事矩阵(Story Matrix)", ""] + out.append(f"从简历里检测到 {len(experiences)} 段经历,按 STAR 拆解如下。") + out.append("**请补充每个 STAR 段落里 `[占位]` 的内容**,准备好后这些故事可以覆盖 80% 的行为面问题。") + out.append("") + + for idx, exp in enumerate(experiences[:8], start=1): + cats = categorize_story(exp["bullets"]) + out.append(f"## 故事 {idx}:{exp['title']}") + out.append("") + out.append("**简历原始 bullet:**") + for b in exp["bullets"][:5]: + out.append(f"- {b}") + out.append("") + out.append("**STAR 拆解(请补全):**") + out.append("- **S(背景)**:[占位 - 一句话点明背景 / 痛点]") + out.append("- **T(任务)**:[占位 - 你的具体任务和目标]") + out.append("- **A(动作)**:[占位 - 分 2~4 步,每步带动词 + 决策依据]") + out.append("- **R(结果)**:[占位 - 量化结果 + 一句反思]") + out.append("") + + question_pool = [] + for cat in cats: + question_pool.extend(COMMON_BEHAVIORAL_QUESTIONS.get(cat, [])) + out.append(f"**最适合回答的行为题({', '.join(cats)}):**") + for q in question_pool[:4]: + out.append(f"- {q}") + out.append("") + out.append("---") + out.append("") + + out.append("## 使用建议") + out.append("") + out.append("- 把每个故事的 STAR 段落填好,每段控制在 30~90 秒讲完") + out.append("- 面试时灵活组合:同一个故事可以从不同角度回答不同题") + out.append("- 至少准备 **3 个完整故事**(成功 + 失败 + 协作 各一个),覆盖 80% 行为题") + out.append('- 每个故事里强调「我」做了什么,避免大量「我们」') + return "\n".join(out) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--resume", required=True) + parser.add_argument("--out") + args = parser.parse_args() + + resume_text = load_resume_text(Path(args.resume).expanduser()) + experiences = extract_experiences(resume_text) + report = render(experiences) + + if args.out: + Path(args.out).write_text(report, encoding="utf-8") + print(f"✓ 故事矩阵已生成:{args.out}") + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/skills/jd-resume-tailor/SKILL.md b/skills/jd-resume-tailor/SKILL.md new file mode 100755 index 0000000..8f33507 --- /dev/null +++ b/skills/jd-resume-tailor/SKILL.md @@ -0,0 +1,127 @@ +--- +name: jd-resume-tailor +description: 给定一份 JD 和一份现有简历,做"JD 拆解 + 简历定向改写"。拆 JD 抽出硬技能、软技能、加分项;对照简历做 gap 分析;产出针对该岗位重写后的简历,突出相关经验、补齐关键词缺口、并保留候选人真实经历不编造。当用户说"针对这个岗位 / 这家公司改简历""帮我对一下这个 JD""我想投这个职位你看怎么改""把这份简历针对 X 公司优化""做一份定向版简历",或同时给出 JD 文本 + 简历文件时,必须触发本 skill。**请勿用本 skill 做"从零写简历"**——那是 resume-builder 的事。 +--- + +# JD ⇄ Resume Tailor(JD 拆解 + 简历定向改写) + +这个 skill 的边界很窄:**只解决"已有 JD + 已有简历,要改一份命中率最高的版本"**。 + +不做的事: +- 写新简历(去 `resume-builder`) +- 找方向 / 推荐岗位(去 `job-intent-tracker`) +- 出面试题(去 `interview-prep`) + +--- + +## 何时触发 + +强信号: +- 用户给了 JD 链接 / JD 文本 + 一份简历 → **必触发** +- "针对这个岗位帮我改简历" +- "对照一下这个 JD" +- "我想投 X 公司的 Y 岗,帮我看简历" +- "做一份定向版" + +弱信号(先确认): +- 只给了 JD 没有简历 → 问"你的简历方便发我看一下吗?没有的话,我可以先帮你从零做一份(resume-builder)" +- 只给了简历说"改简历" → 问"是针对哪个 JD 改?没有 JD 就用 resume-builder 通用优化" + +--- + +## 工作流 + +### Step 1: 解析 JD + +输入可能是: +- 纯文本(用户粘贴) +- 链接(**不要**自动 fetch,提醒用户复制 JD 文本进来;若用户授权 fetch,使用 web_fetch) +- 截图(用 OCR / 视觉识别,让用户确认抽取结果) +- doc/pdf 文件 + +调用脚本: + +```bash +python scripts/parse_jd.py --jd-file <jd.txt> --out jd_parsed.json +``` + +脚本会从 JD 抽出: +- **硬技能 must-have**("必须" / "要求" / "至少 X 年" 等强信号词后面的技能) +- **硬技能 nice-to-have**("加分" / "优先" / "熟悉者优先" 等弱信号) +- **软技能信号**(沟通 / 推动 / 跨部门 / 抗压 等) +- **职责动词 + 对象**("负责 X" / "搭建 Y" / "推动 Z") +- **特殊要求**(出差 / 学历 / 证书 / 语言 / 城市) + +把结果展示给用户,让用户**确认 / 修正抽取是否准确**(关键 must-have 不能漏)。 + +### Step 2: 解析简历 + +输入:用户上传的简历文件(.pdf / .docx / .md / .txt)。 + +调用对应 skill 解析: +- pdf → pdf skill +- docx → docx skill + +抽出:基本信息、教育、每段工作 / 项目经历的(公司、岗位、时间、职责 bullet)、技能列表。 + +### Step 3: Gap 分析 + +调用: + +```bash +python scripts/jd_gap.py --jd jd_parsed.json --resume resume.txt --out gap.md +``` + +脚本输出三类清单: + +1. **完美命中**(JD must-have 在简历里有明确证据) +2. **隐性命中**(JD 要求 X,简历里有 X 的近义经验,但用词不一样 → 改写时可以"提一下") +3. **真缺口**(JD 要求但简历完全没有) + +对"真缺口"分两类: +- **可补救**:简历里其实做过类似的事,只是没写出来 → 追问用户"你做过 X 吗?" +- **不可补救**:用户确实没做过 → **不能编**,建议用户在 cover letter 或 summary 里诚实说明并强调 transferable skill + +### Step 4: 定向改写 + +按以下原则重写简历: + +**a. 重排经历顺序**:与 JD 最相关的工作 / 项目放最前(不改时间真实性,但可以把项目经历拆成两块"相关项目 / 其他项目") + +**b. 重写每条 bullet**: +- 把 JD 里的"职责动词"自然嵌入 bullet(如 JD 说"主导 ___ 系统设计",简历里就把"参与"改成"主导"——前提是用户确实主导了) +- 数字保留并放大("用户 100 万"是好事,别藏起来) +- 补 JD 关键词(如 JD 说"A/B 测试",但简历里写的是"灰度对比",改成"A/B 测试(灰度对比)") + +**c. 重写 Summary**:用 2~3 行总结你为什么是这个岗位的合适人选,**直接对应 JD 的 must-have** + +**d. 调整技能列表**:把 JD 提到的技能移到最前面(前提是真的会) + +**e. 不改的事实**: +- 公司名、岗位名、起止时间、学历 —— 一字不改 +- 项目规模、用户量、收入数据 —— 不能编,只能让用户确认后填准 + +### Step 5: 自检 + 报告 + +输出三个文件: +1. `resume_tailored_<公司>_<岗位>.md`(改写后的简历) +2. `gap_analysis.md`(gap 分析报告) +3. 聊天里给一个 ATS 命中率对比:"改前 X% → 改后 Y%" + +附:诚实提醒用户**哪些 bullet 是基于现有信息推测改写的**,让用户复核后再投。 + +--- + +## 反模式(不要做) + +- ❌ 编造经历("加上一段你没做过的项目"——绝对禁止,哪怕用户要求) +- ❌ 把 JD 的整段话直接粘进简历(很容易被 HR 一眼识破,且 ATS 反作弊会标记) +- ❌ 关键词堆砌(在末尾塞一长串技能词凑命中率,HR 一眼能看出) +- ❌ 把"参与"改成"主导"但没有问用户实际角色 → 必须先核实 +- ❌ 不给用户看 gap,自己默默改 → 用户会失去对简历的"理解" + +## 与其他 skill 的协作 + +- 改完后用户说"帮我准备这家公司的面试" → 转 `interview-prep`,把 JD + 改写简历传过去 +- 用户说"我想知道还能投哪些类似的岗" → 转 `job-intent-tracker` +- 用户说"我现在简历不太行,能不能整体重做" → 转 `resume-builder` diff --git a/skills/jd-resume-tailor/references/jd_parsing_signals.md b/skills/jd-resume-tailor/references/jd_parsing_signals.md new file mode 100755 index 0000000..bb7d534 --- /dev/null +++ b/skills/jd-resume-tailor/references/jd_parsing_signals.md @@ -0,0 +1,79 @@ +# JD 解析信号词参考 + +## must-have 强信号词 + +只要 JD 句子里出现这些词,后面的技能 / 经验大概率是硬门槛: + +- 必须 / 必备 / 要求 / 应当 +- 至少 X 年 / X+ years / 不少于 +- "需要" / "需具备" +- "必要条件" / "硬性要求" +- 工科背景 / 985 / 211 / 硕士及以上 / 博士 + +英文 JD: +- Required / Must have / Mandatory / Essential +- Minimum X years +- Strong proficiency in / Expert in +- Demonstrated experience with + +## nice-to-have 弱信号词 + +- 加分 / 优先 / 优先考虑 +- 熟悉 ___ 者优先 +- 有 ___ 经验者优先 +- "了解 ___ 即可" +- 加分项 + +英文: +- Preferred / Nice to have / Plus / Bonus / Desirable +- Familiar with / Exposure to +- A plus / Would be a plus + +## 职责动词(写在 bullet 里能呼应 JD) + +中文:负责、主导、推动、设计、搭建、优化、规划、迭代、孵化、复盘、运营、管理、协调、执行 +英文:Lead / Drive / Build / Design / Architect / Develop / Implement / Optimize / Manage / Coordinate / Execute / Own + +## 特殊要求 + +- **学历**:本科 / 硕士 / 博士及以上;学校 tier +- **工作年限**:X-Y 年(写明 range,不写满则有弹性) +- **语言**:英语口语流利 / CET-6 / 雅思 X / 母语 +- **证书**:CFA / CPA / PMP / AWS Solutions Architect 等 +- **出差 / 派驻 / 加班**:"接受出差"、"奇偶周末调休"、"项目制 996" +- **工作地点**:城市 + 是否 remote / hybrid + +## 反信号(看到这些要警觉) + +- "其他领导交办的任务" → 工作边界模糊 +- "良好的抗压能力" → 加班多 +- "拥抱变化 / 快速迭代" → 业务方向不稳定 +- "扁平化沟通 / 没有层级" → 实际可能更乱 +- "5 险一金 + 节日福利" 写在 JD 显眼位置 → 福利可能就这些 + +## 输出格式(Step 1 给用户看的) + +```json +{ + "company": "XX 公司", + "position": "XX 岗位", + "must_have": [ + {"item": "5 年以上 C 端产品经验", "evidence": "JD 第 X 行"}, + {"item": "熟练 SQL", "evidence": "JD 第 Y 行"} + ], + "nice_to_have": [ + {"item": "海外业务经验", "evidence": "JD 第 Z 行"} + ], + "soft_skills": ["跨部门推动", "数据驱动决策"], + "responsibilities": [ + "主导 ___ 业务线产品规划", + "通过数据分析驱动迭代" + ], + "special_requirements": { + "education": "本科及以上", + "years": "5+", + "language": "英语口语流利", + "location": "上海,可接受短期出差" + } +} +``` diff --git a/skills/jd-resume-tailor/references/rewrite_principles.md b/skills/jd-resume-tailor/references/rewrite_principles.md new file mode 100755 index 0000000..a161401 --- /dev/null +++ b/skills/jd-resume-tailor/references/rewrite_principles.md @@ -0,0 +1,75 @@ +# 简历定向改写原则 + +## 核心原则 + +### 1. 不编造,只重组 + +简历里的事实部分(公司、岗位、时间、量化数字)**一字不改**。 +可以改的: +- bullet 的措辞和顺序 +- 强调的角度(同一段经历,不同 JD 强调不同侧面) +- 项目分类("相关项目"放前,"其他项目"放后) + +### 2. 把 JD 的关键词嵌入到真实经验里,而不是单独贴 + +❌ 错:在简历末尾加一行"关键词:A/B 测试、用户增长、SQL、Python" +✅ 对:把"我做过的灰度对比"改成"A/B 测试(灰度对比)",让关键词嵌在真实场景里 + +### 3. 用 JD 的动词 + +JD 说"主导 ___ 系统设计",如果你是这段经历的负责人,把"参与"改成"主导"。 + +但**前提是真的主导**——agent 在改写前必须确认: +- 这段经历你是 leader 吗?是的话改"主导";不是就保留"参与"或写明"作为 _ 角色 协助 ___" + +### 4. 重排经历顺序 + +工作经历(按时间倒序)—— 不可改,否则失实。 +项目经历 / 实习经历 —— 可以改,把与 JD 强相关的放前面。 + +更进一步:可以拆成两个小标题: +``` +【相关项目】(与目标岗位强相关) +- ... +【其他项目】 +- ... +``` + +### 5. Summary 要直接对位 JD must-have + +JD 说"5 年 C 端产品 + SQL + A/B 测试" +Summary 改成:"5 年 C 端互联网产品经验,主导过 ___ 增长项目,熟练使用 SQL 与 A/B 测试驱动决策" + +### 6. 技能列表也要倾斜 + +JD 说"Python / Spark / Flink",简历技能列表把这三个放前面。 +不会的不要写。 + +### 7. 适度补 nice-to-have + +如果 JD 的 nice-to-have 你**真的有**但简历没写,补上。 +如果没有,不补——nice-to-have 本来就不是硬门槛,不影响过简历筛选。 + +## 改写后的双向校验 + +每条改后 bullet 自问: +- 这是不是用户真实做过的? +- 这条 bullet 在 JD 里能找到对应的关键词 / 职责吗? +- 量化数字有保留吗? + +每段经历自问: +- 是否覆盖了 JD 的至少 1 条 must-have? +- 是否突出了用户在这段经历里"真正的高光"? + +整份简历自问: +- ATS 命中率有提升吗? +- HR 看 30 秒能不能 get 到"为什么这个候选人适合这个岗位"? +- 有没有让真实经历变形 / 失实? + +## 当用户与 JD 严重不匹配时 + +诚实告诉用户: +- "JD 里有 N 条 must-have 你的简历完全没有覆盖,这个岗位投递成功率较低" +- "建议你考虑:(a) 投匹配度更高的类似岗位 (b) 在 cover letter 里诚实说明 transferable skill (c) 先补技能再投" + +不要为了"看起来匹配"而扭曲简历。 diff --git a/skills/jd-resume-tailor/scripts/jd_gap.py b/skills/jd-resume-tailor/scripts/jd_gap.py new file mode 100755 index 0000000..9370cad --- /dev/null +++ b/skills/jd-resume-tailor/scripts/jd_gap.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +""" +jd_gap.py — 把 parse_jd.py 的 JSON 与简历文本做 gap 分析 + +用法: + python jd_gap.py --jd jd_parsed.json --resume resume.md --out gap.md + +输出 markdown 报告:完美命中 / 隐性命中 / 真缺口 三类,附改写建议。 +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + + +def load_resume_text(path: Path) -> str: + suffix = path.suffix.lower() + if suffix in {".md", ".txt"}: + return path.read_text(encoding="utf-8") + if suffix == ".docx": + try: + from docx import Document + except ImportError: + print( + "✗ 缺少 python-docx:pip install python-docx --break-system-packages", + file=sys.stderr, + ) + sys.exit(1) + doc = Document(str(path)) + return "\n".join(p.text for p in doc.paragraphs) + print(f"✗ 暂不支持的格式:{suffix}", file=sys.stderr) + sys.exit(1) + + +def find_evidence(resume_text: str, keyword: str, window: int = 30) -> str | None: + """在简历里找关键词,返回上下文片段;找不到返回 None。""" + pattern = re.escape(keyword) + m = re.search(pattern, resume_text, flags=re.IGNORECASE) + if not m: + return None + start = max(0, m.start() - window) + end = min(len(resume_text), m.end() + window) + snippet = resume_text[start:end].replace("\n", " ").strip() + return snippet + + +def fuzzy_hit(resume_text: str, keyword: str) -> str | None: + """模糊命中:取关键词的中文 / 英文核心,做包含匹配。""" + # 拿前 2 个字 / 前 5 个字符 + candidates = [] + if re.search(r"[一-龥]", keyword): + if len(keyword) >= 4: + candidates.append(keyword[:2]) + candidates.append(keyword[-2:]) + else: + if len(keyword) >= 4: + candidates.append(keyword[:4].lower()) + text_low = resume_text.lower() + for c in candidates: + if c and c in text_low: + return c + return None + + +def analyze(jd: dict, resume_text: str) -> dict: + perfect, implicit, missing = [], [], [] + + # 用 must_have 句子里抽出来的 skills 作为对比项 + candidates = jd.get("skills_extracted", []) + jd.get("must_have", []) + + seen = set() + for c in candidates: + # 句子层面太长,截短 + keyword = c.strip() + if len(keyword) > 30: + # 从长句子里抽更短的关键词 + short_tokens = re.findall( + r"[A-Za-z][A-Za-z0-9+/.\-_]{1,20}|[一-龥]{2,6}", + keyword, + ) + for t in short_tokens: + if t.lower() not in seen: + seen.add(t.lower()) + process_one(t, resume_text, perfect, implicit, missing) + else: + if keyword.lower() not in seen: + seen.add(keyword.lower()) + process_one(keyword, resume_text, perfect, implicit, missing) + + return {"perfect": perfect, "implicit": implicit, "missing": missing} + + +def process_one(keyword, resume_text, perfect, implicit, missing): + ev = find_evidence(resume_text, keyword) + if ev: + perfect.append({"keyword": keyword, "evidence": ev}) + return + fuzzy = fuzzy_hit(resume_text, keyword) + if fuzzy: + implicit.append({"keyword": keyword, "fuzzy_match": fuzzy}) + else: + missing.append(keyword) + + +def render(jd: dict, gap: dict) -> str: + lines = ["# JD ⇄ Resume Gap 分析报告", ""] + + spec = jd.get("special_requirements", {}) + if spec: + lines += ["## JD 硬条件", ""] + for k, v in spec.items(): + lines.append(f"- **{k}**:{v}") + lines.append("") + + lines += ["## ✅ 完美命中(简历里有明确证据)", ""] + if gap["perfect"]: + for item in gap["perfect"][:30]: + lines.append(f"- **{item['keyword']}** —— 证据:`...{item['evidence']}...`") + else: + lines.append("(无)") + lines.append("") + + lines += ["## 🟡 隐性命中(简历里有近似但用词不同,建议改写时对齐)", ""] + if gap["implicit"]: + for item in gap["implicit"][:20]: + lines.append(f"- **JD 关键词:{item['keyword']}**(简历里出现:`{item['fuzzy_match']}`)") + else: + lines.append("(无)") + lines.append("") + + lines += ["## 🔴 真缺口(简历完全没有,需要确认 / 补充 / 转换叙事)", ""] + if gap["missing"]: + for kw in gap["missing"][:30]: + lines.append(f"- **{kw}**") + else: + lines.append("(无)") + lines.append("") + + lines += [ + "---", + "## 改写建议", + "", + "1. **完美命中** 的部分保留,但确保措辞与 JD 一致(比如 JD 用『A/B 测试』就别写『AB 实验』)", + "2. **隐性命中** 是性价比最高的优化点 —— 把简历里的近义词改成 JD 的措辞", + "3. **真缺口** 分两类:", + " - 你做过但没写?→ 补到对应经历的 bullet 里", + " - 你没做过?→ **不要编造**。可以在 cover letter / Summary 里诚实说明 transferable skill", + "4. 把改后的简历再跑一次 ats_check.py 看命中率是否提升", + ] + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--jd", required=True, help="parse_jd.py 输出的 json") + parser.add_argument("--resume", required=True, help="简历文件 (.md/.txt/.docx)") + parser.add_argument("--out", help="输出 markdown 路径") + args = parser.parse_args() + + jd = json.loads(Path(args.jd).read_text(encoding="utf-8")) + resume_text = load_resume_text(Path(args.resume)) + gap = analyze(jd, resume_text) + report = render(jd, gap) + + if args.out: + Path(args.out).write_text(report, encoding="utf-8") + print(f"✓ Gap 报告已生成:{args.out}") + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/skills/jd-resume-tailor/scripts/parse_jd.py b/skills/jd-resume-tailor/scripts/parse_jd.py new file mode 100755 index 0000000..54260da --- /dev/null +++ b/skills/jd-resume-tailor/scripts/parse_jd.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +""" +parse_jd.py — 解析 JD 文本,抽取 must-have / nice-to-have / 职责 / 特殊要求 + +用法: + python parse_jd.py --jd-file jd.txt --out jd_parsed.json + python parse_jd.py --jd-text "..." --out jd_parsed.json + +输出 JSON 结构供下一步的 jd_gap.py 使用,也可以直接给用户看。 +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path + +MUST_PATTERNS = [ + r"必须", r"必备", r"必要条件", r"硬性要求", r"应当", r"需要", r"需具备", + r"至少\s*\d+\s*年", r"\d+\+?\s*年以上", + r"required", r"must\s*have", r"mandatory", r"essential", + r"minimum\s+\d+\s+years", +] +NICE_PATTERNS = [ + r"加分", r"优先", r"加分项", r"熟悉.+者优先", r"有.+经验者优先", + r"preferred", r"nice\s*to\s*have", r"plus", r"bonus", r"desirable", +] +ACTION_VERBS = [ + "负责", "主导", "推动", "设计", "搭建", "构建", "优化", "规划", "迭代", + "孵化", "复盘", "运营", "管理", "协调", "执行", "驱动", "落地", "重构", + "lead", "drive", "build", "design", "architect", "develop", "implement", + "optimize", "manage", "coordinate", "execute", "own", +] + + +def split_sentences(text: str) -> list[str]: + # 中文按 。!?; 拆,英文按 . ; 拆,并保留 bullet 行 + raw = re.split(r"[。!?!?;;\n]+", text) + return [s.strip(" \t-•·*") for s in raw if s.strip()] + + +def classify_sentences(sentences: list[str]) -> dict: + must, nice, resp, others = [], [], [], [] + for s in sentences: + s_low = s.lower() + if any(re.search(p, s_low) for p in NICE_PATTERNS): + nice.append(s) + elif any(re.search(p, s_low) for p in MUST_PATTERNS): + must.append(s) + elif any(v in s_low for v in ACTION_VERBS): + resp.append(s) + else: + others.append(s) + return {"must": must, "nice": nice, "responsibilities": resp, "others": others} + + +def extract_special(text: str) -> dict: + out: dict[str, str] = {} + + # 学历 + edu = re.search(r"(本科|硕士|博士|大专)(?:及以上|以上)?", text) + if edu: + out["education"] = edu.group(0) + + # 工作年限 + years = re.search(r"(\d+)\s*[-~–到至]\s*(\d+)\s*年|(\d+)\s*\+?\s*年(以上|及以上)?", text) + if years: + out["years"] = years.group(0) + + # 语言 + lang_pat = re.search( + r"(英语\s*(口语)?\s*(流利|熟练|母语)|CET[-\s]?[46]|雅思\s*\d(\.\d)?|托福\s*\d{2,3}|母语水平|business\s*english)", + text, + flags=re.IGNORECASE, + ) + if lang_pat: + out["language"] = lang_pat.group(0) + + # 城市 + cities = re.findall( + r"(北京|上海|广州|深圳|杭州|南京|苏州|成都|武汉|西安|香港|新加坡|remote|hybrid|远程|海外)", + text, + flags=re.IGNORECASE, + ) + if cities: + out["location"] = "/".join(sorted(set(c.lower() for c in cities))) + + # 出差 / 加班信号 + travel = re.search(r"(出差|派驻|常驻|项目制|加班|999|996|大小周)", text) + if travel: + out["working_style"] = travel.group(0) + + # 证书 + certs = re.findall( + r"(CFA(?:\s*Level\s*[I123]+)?|CPA|FRM|ACCA|PMP|AWS\s*[\w\s]*认证|Azure\s*[\w]*|GCP\s*[\w]*)", + text, + flags=re.IGNORECASE, + ) + if certs: + out["certificates"] = "/".join(sorted(set(c.strip() for c in certs))) + + return out + + +def extract_skills(sentences: list[str]) -> list[str]: + """从所有句子里抽取技能词候选(短词优先,避免抽出整句)。""" + text = " ".join(sentences) + # 英文技能(CamelCase 或大写开头的词、含 . 或 +/- 的标识) + en = re.findall(r"\b[A-Za-z][A-Za-z0-9+/.\-_#]{1,20}\b", text) + # 中文 2~5 字常见技能词 + zh = re.findall(r"[一-龥]{2,5}", text) + raw = en + zh + + stop = { + # 中文虚词 / 通用动词 + "公司", "我们", "你将", "团队", "需要", "能够", "具备", "熟悉", "了解", + "良好", "优秀", "经验", "能力", "岗位", "职责", "要求", "以上", "相关", + "进行", "完成", "负责", "推动", "实现", "提升", "并且", "包括", "以下", + "工作", "项目", "业务", "及其", "或者", "或", "与", "及", "的", "了", + "并", "对", "在", "等", "以", "等等", "通过", "将", "其", "之", "至", + "至上", "本科", "硕士", "博士", "者优先", "根据", "进行", "支持", "参与", + "主导", "提供", "建立", "搭建", "设计", "驱动", "决策", "分析", "推动", + "迭代", "规划", "运营", "协作", "跨部门", "跨团队", "高级", "资深", + "若干", "多种", "多元", "多类", + # 英文虚词 + "and", "the", "with", "for", "of", "or", "to", "be", "as", "an", "is", + "are", "in", "on", "at", "by", "all", "you", "we", "us", "our", "your", + "a", "an", "this", "that", "these", "those", "it", "its", + } + # 含数字的"X年""X个"也过滤 + digit_only = re.compile(r"^\d+$") + + seen = set() + out = [] + for token in raw: + key = token.lower() + if key in seen or token in stop or len(token) < 2 or digit_only.match(token): + continue + # 中文 token 不允许全是 stop 词的子串 + seen.add(key) + out.append(token) + return out[:60] + + +def main() -> None: + parser = argparse.ArgumentParser() + src = parser.add_mutually_exclusive_group(required=True) + src.add_argument("--jd-file", help="JD 文本文件路径") + src.add_argument("--jd-text", help="直接传 JD 文本") + parser.add_argument("--out", help="输出 JSON 路径,缺省打印") + args = parser.parse_args() + + if args.jd_file: + path = Path(args.jd_file).expanduser() + if not path.exists(): + print(f"✗ JD 文件不存在:{path}", file=sys.stderr) + sys.exit(1) + text = path.read_text(encoding="utf-8") + else: + text = args.jd_text + + sentences = split_sentences(text) + classified = classify_sentences(sentences) + special = extract_special(text) + skills = extract_skills(classified["must"] + classified["responsibilities"]) + + result = { + "must_have": classified["must"], + "nice_to_have": classified["nice"], + "responsibilities": classified["responsibilities"], + "skills_extracted": skills, + "special_requirements": special, + "raw_sentence_count": len(sentences), + } + + payload = json.dumps(result, ensure_ascii=False, indent=2) + if args.out: + out_path = Path(args.out).expanduser() + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(payload, encoding="utf-8") + print(f"✓ JD 解析结果已保存:{out_path}") + else: + print(payload) + + +if __name__ == "__main__": + main() diff --git a/skills/job-intent-tracker/SKILL.md b/skills/job-intent-tracker/SKILL.md new file mode 100755 index 0000000..f91393b --- /dev/null +++ b/skills/job-intent-tracker/SKILL.md @@ -0,0 +1,136 @@ +--- +name: job-intent-tracker +description: 帮助用户梳理求职意向、生成目标岗位画像,并维护一份结构化的"岗位投递追踪表"。当用户说"我想换工作 / 不知道投什么岗 / 帮我看看我适合什么岗位 / 帮我管理投递进度 / 我投了好几家但记不住状态了 / 想做一个求职 OKR / 整理一下求职方向",或上传简历但没说要改简历时,应该主动触发本 skill。本 skill 也适用于实习生、应届生、转行候选人在求职启动阶段做"自我盘点 + 目标画像 + 投递管理"三件事。 +--- + +# Job Intent Tracker(求职意向 + 岗位追踪) + +这个 skill 解决"求职启动期"的三个核心问题: + +1. **我适合投什么岗?** —— 从用户背景里抽取信号,反推 2~3 个目标方向 +2. **目标岗位长什么样?** —— 为每个方向生成"岗位画像 / Target Profile" +3. **投了哪些、进度如何?** —— 维护一份结构化追踪表(Excel / Markdown 表) + +不要把这个 skill 用成"简历改写"或"面试题生成"——那是 resume-builder / jd-resume-tailor / interview-prep 的事。本 skill 只关心"方向"和"管理"。 + +--- + +## 何时触发本 skill + +强信号(基本必须用): +- "我想换工作 / 想跳槽 / 想找下一份工作" +- "帮我看看我适合什么岗位" +- "帮我管理投递进度 / 跟踪一下我投的公司" +- "我有 X 年 Y 经验,下一步该往哪走" +- "我投了好多家但忘了进度" + +弱信号(先确认再用): +- 用户只丢了一份简历,没说目的 → 先问"你是想梳理方向、改简历,还是准备面试?" +- 用户说"想找工作"但很模糊 → 先问"你心里有目标方向吗?还是想让我帮你判断方向?" + +--- + +## 工作流程 + +按这个顺序走,一步都不要跳: + +### Step 1: 自我盘点(Background Intake) + +用 AskUserQuestion(或在没有该工具时直接问)收集以下信息。**不要一次问完,分 2~3 轮,每轮 2~3 个问题**,否则用户会被"问卷感"劝退。 + +第一轮(必问): +- 当前 / 最近一份工作的岗位、公司类型、年限 +- 核心技能 3~5 个(关键词即可) +- 目标行业 / 目标方向(如果用户有)—— 没有也没关系,跳到 Step 2 + +第二轮(看情况问): +- 期望薪资 range(用户不愿意说就跳过) +- 城市偏好 / 是否接受 remote +- 排除项("不想做销售 / 不想加班 / 不接受出差"等) + +第三轮(深度信号,仅在前两轮信息不足以画像时问): +- 最有成就感的 1~2 个项目 +- 最不喜欢做的事 +- 5 年后的画面 + +如果用户上传了简历 .pdf / .docx,**先调用 pdf 或 docx skill 解析出文本**,再从中抽取以上信息,避免用户重复打字。 + +### Step 2: 推荐求职方向 + +基于 Step 1 的信息,生成 2~3 个候选方向。每个方向都要写清楚: + +``` +方向 N:<岗位名>(如:互联网产品经理 / 数据分析师 / 量化研究员) +- 匹配度:高 / 中 / 低(高=核心技能直接命中;中=需补 1~2 个关键技能;低=需要转岗叙事) +- 匹配理由:基于用户的 ___ 经验和 ___ 技能 +- 缺口:用户还需要补 ___ 才能成为强候选人 +- 典型雇主:<3~5 个具体公司或公司类型> +- 薪资带(仅供参考):__k - __k(注明"市场行情,仅供参考,建议用户自行通过职级查询") +``` + +**重要:不要只推荐"安全"的方向。** 如果用户技能允许,至少给一个"跳一跳能够到"的方向,并诚实标注缺口。 + +### Step 3: 生成岗位画像(Target Profile) + +为用户**最终选定**的 1~2 个方向(让用户主动选),生成详细画像。模板在 `references/target_profile_template.md`,需要读取这个文件后再填充。 + +画像要包含:岗位职责典型描述、技能要求 must-have / nice-to-have、面试流程预期、对标公司列表(按 tier 分层)。 + +读取行业关键词库决定 must-have / nice-to-have: +- 互联网产品 / 运营 / PM → `references/keywords_internet.md` +- 技术 / 研发 / 数据 → `references/keywords_tech.md` +- 金融 / 咨询 / 商科 → `references/keywords_finance.md` +- 通用 / 跨行业 → `references/keywords_general.md` + +### Step 4: 创建投递追踪表 + +调用 `scripts/init_tracker.py` 生成初始追踪表。脚本支持两种格式: + +```bash +python scripts/init_tracker.py --format xlsx --output /path/to/tracker.xlsx +# 或 +python scripts/init_tracker.py --format md --output /path/to/tracker.md +``` + +默认推荐 xlsx(用户可以排序、加 conditional formatting)。如果用户明确要求轻量,用 md。 + +追踪表的列结构(脚本里已经预设好,不要改): +公司、岗位、来源(猎头/官网/内推/招聘网站)、JD 链接、投递日期、当前阶段(投递/笔试/一面/二面/HR 面/Offer/Reject/沉默)、下一步动作、Deadline、薪资范围、内推人、备注 + +### Step 5: 用 Artifact 做"求职看板"(可选但推荐) + +如果当前环境里有 `mcp__cowork__create_artifact` 工具,主动提议:"要不要我把这个追踪表做成一个可以每天打开看的看板?" + +如果用户同意,创建一个 HTML artifact,从追踪表数据渲染: +- 顶部 KPI:投递总数 / 进入面试数 / Offer 数 / 待跟进数 +- 中部表格:按"当前阶段"分组的 Kanban 视图 +- 底部提醒:3 天没动静的公司、deadline 临近的任务 + +--- + +## 输出风格 + +- **结构化、可执行**,不要泛泛而谈"建议你多投简历"这种话 +- 涉及薪资、行业判断时,**明确标注"市场参考、非承诺"** +- 推荐方向时不要谄媚(不要把所有用户都推向"高薪 AI 岗"),诚实评估匹配度 +- 用户拒绝某个方向时不要劝,问清楚为什么然后调整 + +--- + +## 与其他 skill 的协作 + +- 用户选定方向后说"那帮我改简历" → 调用 `resume-builder`,把岗位画像传过去 +- 用户说"那针对这家公司的 JD 改一下" → 调用 `jd-resume-tailor` +- 用户说"帮我准备面试" → 调用 `interview-prep`,把岗位画像传过去 + +输出"我已经把你的岗位画像存到 ___,下一步可以让我用 resume-builder 改简历"这样的衔接句,让 agent 在多步任务里能流转。 + +--- + +## 反模式(不要做) + +- ❌ 一上来就问 8 个问题 +- ❌ 不问用户偏好,直接推 5 个方向 +- ❌ 不给追踪表,只在聊天里讲一通 +- ❌ 把"运营 / 产品 / 项目经理"塞给所有文科背景用户 +- ❌ 看到"AI / 大模型"就无脑推 LLM 岗,不评估实际技能匹配 diff --git a/skills/job-intent-tracker/references/keywords_finance.md b/skills/job-intent-tracker/references/keywords_finance.md new file mode 100755 index 0000000..9a60782 --- /dev/null +++ b/skills/job-intent-tracker/references/keywords_finance.md @@ -0,0 +1,78 @@ +# 金融 / 咨询 / 商科 关键词库 + +## 投行 / IBD + +**Must-have** +- 财务建模(DCF、LBO、Comps、Precedent Transactions) +- 估值方法、敏感性分析、scenario analysis +- 行业研究、招股书 / pitchbook、deal experience +- Excel 建模、PowerPoint 高阶、Bloomberg、Capital IQ、Wind + +**职级 & 信号** +- Analyst(应届/1-3 年):execution、模型搭建 +- Associate(3-6 年):项目管理、客户接触 +- VP / Director:deal origination、客户关系 + +**典型雇主** +- 外资九大行:高盛、摩根士丹利、摩根大通、美林、巴克莱、瑞银、瑞信、德银、花旗 +- 中资头部:中金、中信、华泰联合、海通、招商 +- 精品行:Lazard、Centerview、Moelis、Evercore、华兴、汉能 + +## 咨询(Consulting) + +**Must-have** +- 结构化思维(MECE、issue tree、hypothesis-driven) +- Case Interview 能力:market sizing、profitability、M&A、market entry +- 行业知识 + 客户沟通 +- 工具:PowerPoint(必)、Excel(必)、Tableau / Alteryx(加分) + +**MBB 信号**:先 sizing 再框架,框架要 collectively exhaustive,假设清晰可证伪 + +**典型雇主** +- MBB:McKinsey、BCG、Bain +- Big 4 战略:Strategy&(PwC)、Monitor Deloitte、EY-Parthenon、KPMG Strategy +- 二线:Roland Berger、Oliver Wyman、A.T. Kearney、L.E.K. +- 内资:埃森哲战略(已并入)、华兴 Alpha、艾瑞、易观、罗兰贝格本土化团队 + +## 买方 / 二级市场(Buy Side) + +**Must-have** +- 行业研究、公司研究、估值模型 +- 投资逻辑、催化剂、风险点 +- 写作能力(投研报告) + +**细分** +- 公募基金:行业研究员 / 基金经理助理 +- 私募 / 对冲基金:基本面 / 量化 / CTA / 多策略 +- 险资 / 资管:稳健、负债端思维 +- VC / PE:早期 / 成长期 / 并购,技术 + 商业判断 + +## 量化(Quant) + +**Must-have** +- 数学:随机过程、统计、优化、概率 +- 编程:Python(必)、C++ / Java(HFT 必)、Q/KDB +- 策略类型:CTA、stat arb、market making、HFT、ML 因子 +- 工具:vnpy、聚宽、米筐、Bloomberg、Wind、Tushare + +**典型雇主** +- 国际:Two Sigma、Citadel、Jane Street、Jump Trading、HRT、DRW +- 国内:九坤、明汯、灵均、宽德、幻方、鸣石、致诚卓远 + +## 战略 / 商业分析(in-house) + +**Must-have** +- 商业模型理解、unit economics、市场地图 +- 数据驱动决策、SQL 基础 +- 战略 deck 输出、跨部门推动力 + +**典型场景**:互联网战投、车企战略、新消费品牌战略、跨国公司中国区战略 + +## 财务 / FP&A / 内审 / 税务 + +略,按需补充。 + +## 反信号 +- 投行 / 咨询岗位简历**没有 deal / case 列表** → 致命 +- 量化岗**没有策略实盘 / 回测结果** → 弱 +- 公募 / PE 岗**没有具体 coverage 行业** → 假研究员 diff --git a/skills/job-intent-tracker/references/keywords_general.md b/skills/job-intent-tracker/references/keywords_general.md new file mode 100755 index 0000000..267dcde --- /dev/null +++ b/skills/job-intent-tracker/references/keywords_general.md @@ -0,0 +1,45 @@ +# 通用关键词库(跨行业) + +## 通用软技能(任何岗位都吃香) + +- Ownership / 主人翁意识 +- 跨部门 / 跨团队协作 +- 推动落地 / drive +- 业务 sense / 商业化思维 +- 数据驱动 / 量化思维 +- 用户同理心 / 客户导向 +- Learning agility / 快速学习 +- Ambiguity tolerance / 应对模糊 +- 影响力 / 说服力 + +## 通用硬技能 + +- SQL(半数岗位需要) +- Excel 高阶(vlookup、pivot、power query、宏 / VBA) +- PowerPoint / Keynote(结构化输出) +- 英语(书面 + 口语,外企 / 跨境必备) +- 项目管理:Jira / Asana / Notion / 飞书 + +## 通用职业资质 + +- 学历:本科 / 硕士 / 博士 + 学校 tier +- 证书:CFA、CPA、ACCA、PMP、AWS / GCP / Azure 认证 +- 语言:托福 / 雅思 / 多语种 + +## 量化结果通用模板 + +写简历 / 画像时,**所有动作都尽量量化**。模板: + +- "通过 ___,使 ___ 提升 X%(基线 Y → 现状 Z)" +- "在 ___ 期间,覆盖 ___ 业务,规模 X 万 / 亿" +- "主导 ___ 项目,缩短 ___ 时间 X%" +- "管理 X 人团队,覆盖 X 个业务线" + +## 转行 / 跨界叙事关键词 + +- "可迁移技能"(transferable skill) +- "跨界视角" +- "第一性原理" +- "从 0 到 1 / 从 1 到 N" + +转行候选人的画像应该突出**结构化思维 + 学习速度 + 已有 case**,而不是硬编技能词。 diff --git a/skills/job-intent-tracker/references/keywords_internet.md b/skills/job-intent-tracker/references/keywords_internet.md new file mode 100755 index 0000000..51bd438 --- /dev/null +++ b/skills/job-intent-tracker/references/keywords_internet.md @@ -0,0 +1,54 @@ +# 互联网产品 / 运营 / 项目管理 关键词库 + +用于:判断匹配度、生成画像、ATS 关键词覆盖率检查。 + +## 产品经理(PM) + +**Must-have 信号词** +- 需求文档(PRD)、需求评审、产品规划、产品迭代 +- 用户访谈、用户画像、JTBD、Persona +- 数据驱动、A/B 测试、漏斗分析、留存、转化率 +- 跨部门协作、推动落地、项目管理 +- 0-1、1-N、增长、商业化 + +**方向细分关键词** +- C 端:用户增长、社交裂变、内容生态、ugc、推荐算法理解 +- B 端 / SaaS:客户成功、续约、ARR、PLG、CRM +- 国际化 / 出海:本地化、合规、多语言、海外支付 +- AI 产品:LLM、prompt 工程、模型评估、RAG、Agent +- 工具类:用户体验、效率、workflow、生产力 + +**Nice-to-have** +- SQL、Python、Figma、原型工具 +- 行业报告输出、PR 经验、对外发声 + +## 运营 + +**Must-have** +- 用户运营、内容运营、活动运营、社群运营 +- 增长黑客、AARRR、私域、公域 +- GMV、ROI、CAC、LTV、转化漏斗 +- SOP、流程化、可复制 + +**方向细分** +- 内容运营:选题、内容矩阵、KOL、爆款方法论 +- 用户运营:分层、生命周期、push 策略、CRM +- 活动运营:节点营销、品效合一、预算管理 +- 社群运营:社群分层、KOC、转化路径 +- 直播 / 电商运营:选品、达人合作、GMV、复购 + +## 项目经理(PMP / TPM) + +**Must-have** +- 项目排期、里程碑、风险管理、stakeholder 管理 +- 敏捷 / Scrum / 看板、Sprint planning、retro +- 跨团队协作、跨时区、escalation + +**Nice-to-have** +- PMP 证书、PRINCE2、CSM +- 工具:Jira、Confluence、Asana、Notion、Linear、飞书项目 + +## 反信号(出现这些词,不是好 PM / 运营画像) +- 大段堆砌"参与了 ___"——没有 ownership +- 没有任何数字 / 百分比 / 量化结果 +- 全是软技能描述,没有具体动作 diff --git a/skills/job-intent-tracker/references/keywords_tech.md b/skills/job-intent-tracker/references/keywords_tech.md new file mode 100755 index 0000000..1273420 --- /dev/null +++ b/skills/job-intent-tracker/references/keywords_tech.md @@ -0,0 +1,87 @@ +# 技术 / 研发 / 数据 关键词库 + +## 后端 / 服务端 + +**Must-have 语言 & 框架** +- 主流语言:Java / Go / Python / C++ / Rust / Node.js +- Web 框架:Spring Boot、Gin、FastAPI、Django、Express、NestJS +- 微服务:Spring Cloud、gRPC、Service Mesh、Istio +- 中间件:Kafka、RabbitMQ、Redis、ZooKeeper、Etcd + +**系统设计信号** +- 高并发、高可用、分布式、CAP、一致性、限流、降级、熔断 +- 存储:MySQL、PostgreSQL、MongoDB、ClickHouse、HBase、TiDB +- 缓存策略、读写分离、分库分表 +- 容器化:Docker、Kubernetes、Helm +- 云:AWS / GCP / 阿里云 / 腾讯云 / 火山 + +**职级判断** +- 初级(1-3 年):能独立完成模块开发 +- 中级(3-5 年):负责子系统设计、code review、带新人 +- 高级(5-8 年):架构设计、跨团队协作、技术选型 +- 资深 / Staff(8+ 年):业务架构、技术战略、影响力 + +## 前端 + +**Must-have** +- HTML / CSS / JavaScript / TypeScript +- 框架:React、Vue、Angular、Svelte +- 工程化:Webpack、Vite、Rollup、Turbopack +- 状态管理:Redux、Zustand、Pinia、MobX +- 全栈倾向:Next.js、Nuxt、Remix + +**Nice-to-have** +- 跨端:React Native、Flutter、Electron、Tauri +- 性能优化:FCP / LCP / CLS、SSR、SSG、CDN +- 可视化:D3、ECharts、Three.js +- 微前端:qiankun、single-spa、Module Federation + +## 算法 / 机器学习 + +**Must-have** +- 数学基础:概率统计、线性代数、优化 +- 经典 ML:sklearn、XGBoost、LightGBM、特征工程 +- 深度学习:PyTorch、TensorFlow、JAX +- 训练:分布式训练、数据并行、模型并行、混合精度 +- 部署:ONNX、TensorRT、TorchServe、Triton + +**LLM / GenAI 方向** +- 预训练、SFT、RLHF、DPO、PPO +- RAG、向量检索、Embedding、Reranker +- Prompt 工程、Tool use、Agent、MCP +- 模型评估:benchmark、人工评估、LLM-as-Judge +- 推理优化:vLLM、量化、KV cache、speculative decoding + +**CV / NLP / RecSys 细分** +- CV:检测、分割、OCR、视频理解、扩散模型 +- NLP:分类、NER、问答、对话、信息抽取 +- RecSys:召回、粗排、精排、重排、CTR / CVR、Cold Start + +## 数据 / 数仓 + +**Must-have** +- SQL(必考,不熟练直接 pass) +- 数仓建模:维度建模、星型 / 雪花、ODS/DWD/DWS/ADS +- 大数据:Hive、Spark、Flink、Presto / Trino +- 调度:Airflow、DolphinScheduler +- BI:Tableau、PowerBI、Superset、Quick BI + +## 数据分析师 / 商分 + +**Must-have** +- SQL(中级以上:窗口函数、CTE、复杂 join) +- 业务理解:能从需求方一句话拆出指标 + 维度 +- 实验设计:A/B test、显著性、样本量 +- 工具:Python (pandas)、R、Excel 高阶、可视化工具 + +## DevOps / SRE + +- CI/CD:Jenkins、GitLab CI、GitHub Actions、ArgoCD +- IaC:Terraform、Pulumi、Ansible、Chef +- 监控:Prometheus、Grafana、ELK、Datadog、SkyWalking +- 故障处理:On-call、SLO、SLA、错误预算、事故复盘 + +## 反信号 +- 简历只有"熟悉 / 了解 / 会用" → 弱 +- 列了一堆框架,没有项目对应 → 假 +- 没有任何性能数字 / 数据规模 → 单薄 diff --git a/skills/job-intent-tracker/references/target_profile_template.md b/skills/job-intent-tracker/references/target_profile_template.md new file mode 100755 index 0000000..94ca2c9 --- /dev/null +++ b/skills/job-intent-tracker/references/target_profile_template.md @@ -0,0 +1,74 @@ +# 岗位画像(Target Profile)模板 + +填写时把所有 `<占位>` 都换成具体内容。空着不填等于没画像。 + +--- + +## 一、岗位基本信息 + +- **岗位名称**:<如:高级产品经理 - 用户增长方向> +- **方向定位**:<如:B 端 SaaS / C 端工具 / 内容平台 / Fintech> +- **职级范围**:<如:P6-P7 / 中级-高级 / Senior IC> +- **目标城市**:<北京 / 上海 / 深圳 / 杭州 / Remote / 海外> +- **薪资带(市场参考)**:<__k × __ 月,仅供参考,建议核实职级和城市差异> + +--- + +## 二、典型职责描述(JD 共性) + +罗列 3~5 条该岗位常见的职责动词 + 对象: +- <如:负责 ___ 业务线的产品规划与迭代> +- <如:通过数据分析定位用户流失环节并设计实验> +- <如:跨部门协作,推动 ___ 项目落地> + +--- + +## 三、技能要求 + +### Must-have(缺一票否决) +- <硬技能 1,如:5+ 年 C 端产品经验> +- <硬技能 2,如:熟练使用 SQL,能独立完成数据分析> +- <硬技能 3,如:有从 0 到 1 的产品案例> + +### Nice-to-have(加分项) +- <如:有海外市场或跨境业务经验> +- <如:懂基础前端,能跟开发深度沟通> +- <如:有大厂背景或被收购初创经验> + +### 软技能信号(HR / 终面会 probe) +- <如:跨部门协调能力> +- <如:业务 sense / 商业化 sense> +- <如:用户同理心> + +--- + +## 四、面试流程预期 + +按公司 tier 分层估算,给用户一个心理预期: + +| 公司类型 | 流程长度 | 典型环节 | +|---------|---------|----------| +| 大厂 / Tier 1 | 4~6 周 | 简历 → 笔试 → 1 面(专业)→ 2 面(深度)→ 3 面(leader)→ HR 面 → Offer | +| 中厂 / Tier 2 | 2~4 周 | 简历 → 1 面 → 2 面 → HR → Offer | +| 创业 / Tier 3 | 1~2 周 | 简历 → 1~2 面(含创始人)→ Offer | + +填写实际预期:<具体到该方向的常见流程> + +--- + +## 五、对标公司清单 + +按 tier 分层列 3~5 家: + +- **Tier 1(梦想 offer,匹配度需达 80%+)**:<公司 1>、<公司 2>、<公司 3> +- **Tier 2(合理选择,匹配度 60%+)**:<公司 1>、<公司 2>、<公司 3> +- **Tier 3(保底 / 练手,匹配度 40%+)**:<公司 1>、<公司 2>、<公司 3> + +--- + +## 六、求职策略提示 + +- **简历重点**:突出 ___ 经验,弱化 ___ +- **作品集 / case**:建议准备 ___(如:一个完整的产品分析报告 / 一份 SQL 项目) +- **网络渠道**:优先走 ___(内推 / 猎头 / 官网 / 拉勾 / 脉脉),少用 ___ +- **时间预算**:从准备到拿 offer,建议留 <8~12 周> 缓冲 diff --git a/skills/job-intent-tracker/scripts/init_tracker.py b/skills/job-intent-tracker/scripts/init_tracker.py new file mode 100755 index 0000000..2218c4d --- /dev/null +++ b/skills/job-intent-tracker/scripts/init_tracker.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 +""" +init_tracker.py — 初始化求职投递追踪表 + +用法: + python init_tracker.py --format xlsx --output tracker.xlsx + python init_tracker.py --format md --output tracker.md + +xlsx 模式生成带表头、下拉验证、条件格式的 Excel。 +md 模式生成轻量的 Markdown 表格,便于在 Notion / Obsidian / 飞书文档里粘贴。 +""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +COLUMNS = [ + "公司", + "岗位", + "来源", # 猎头 / 官网 / 内推 / 招聘网站 / 其他 + "JD链接", + "投递日期", + "当前阶段", # 投递 / 笔试 / 一面 / 二面 / 三面 / HR面 / Offer / Reject / 沉默 + "下一步动作", + "Deadline", + "薪资范围", + "内推人", + "备注", +] + +STAGE_OPTIONS = [ + "投递", "笔试", "一面", "二面", "三面", "HR面", "Offer", "Reject", "沉默", +] + +SOURCE_OPTIONS = ["官网", "内推", "招聘网站", "猎头", "校招", "其他"] + + +def write_md(path: Path) -> None: + header = "| " + " | ".join(COLUMNS) + " |" + sep = "| " + " | ".join(["---"] * len(COLUMNS)) + " |" + sample = "| 示例公司 | 高级产品经理 | 内推 | https://... | 2026-05-07 | 投递 | 等HR反馈 | 2026-05-14 | 40-55k | 张三 | 内推码 ABC123 |" + legend_lines = [ + "", + "## 字段说明", + "", + f"- **来源**:{' / '.join(SOURCE_OPTIONS)}", + f"- **当前阶段**:{' / '.join(STAGE_OPTIONS)}", + "- **下一步动作**:写一句具体可执行的事,比如『5/10 前发感谢信』『等 HR 回复,5/15 未回则催』", + "- **沉默**:超过 7 天没反馈的状态,提醒自己主动 follow up 或放弃", + "", + ] + content = "\n".join([ + "# 投递追踪表", + "", + header, + sep, + sample, + *legend_lines, + ]) + path.write_text(content, encoding="utf-8") + print(f"✓ Markdown 追踪表已生成:{path}") + + +def write_xlsx(path: Path) -> None: + try: + from openpyxl import Workbook + from openpyxl.styles import Alignment, Font, PatternFill + from openpyxl.worksheet.datavalidation import DataValidation + from openpyxl.formatting.rule import CellIsRule + except ImportError: + print( + "✗ 缺少 openpyxl,请先安装:pip install openpyxl --break-system-packages", + file=sys.stderr, + ) + sys.exit(1) + + wb = Workbook() + ws = wb.active + ws.title = "投递追踪" + + # 表头 + ws.append(COLUMNS) + header_fill = PatternFill("solid", fgColor="305496") + header_font = Font(bold=True, color="FFFFFF") + for cell in ws[1]: + cell.fill = header_fill + cell.font = header_font + cell.alignment = Alignment(horizontal="center", vertical="center") + + # 列宽 + widths = [16, 22, 10, 28, 12, 10, 26, 12, 14, 10, 30] + for idx, w in enumerate(widths, start=1): + ws.column_dimensions[chr(64 + idx)].width = w + + # 示例行 + sample_row = [ + "示例公司", + "高级产品经理", + "内推", + "https://example.com/jd/123", + "2026-05-07", + "投递", + "5/14 未回则发邮件 follow up", + "2026-05-14", + "40-55k", + "张三", + "内推码 ABC123", + ] + ws.append(sample_row) + + # 下拉验证:来源(C 列) + dv_source = DataValidation( + type="list", + formula1=f'"{",".join(SOURCE_OPTIONS)}"', + allow_blank=True, + ) + dv_source.add(f"C2:C200") + ws.add_data_validation(dv_source) + + # 下拉验证:当前阶段(F 列) + dv_stage = DataValidation( + type="list", + formula1=f'"{",".join(STAGE_OPTIONS)}"', + allow_blank=True, + ) + dv_stage.add(f"F2:F200") + ws.add_data_validation(dv_stage) + + # 条件格式:F 列 = Offer 绿色,Reject 灰色,沉默 黄色 + ws.conditional_formatting.add( + "F2:F200", + CellIsRule(operator="equal", formula=['"Offer"'], + fill=PatternFill("solid", fgColor="C6EFCE")), + ) + ws.conditional_formatting.add( + "F2:F200", + CellIsRule(operator="equal", formula=['"Reject"'], + fill=PatternFill("solid", fgColor="D9D9D9")), + ) + ws.conditional_formatting.add( + "F2:F200", + CellIsRule(operator="equal", formula=['"沉默"'], + fill=PatternFill("solid", fgColor="FFEB9C")), + ) + + # 冻结首行 + ws.freeze_panes = "A2" + + # 第二个 sheet:使用说明 + legend = wb.create_sheet("使用说明") + legend["A1"] = "投递追踪表使用说明" + legend["A1"].font = Font(bold=True, size=14) + legend_lines = [ + "", + "1. 每投递一个岗位,新增一行。", + "2. 「来源」「当前阶段」是下拉框,直接选。", + "3. 「下一步动作」务必写具体可执行的事 + 时间点。", + "4. 状态超过 7 天没动静时,把「当前阶段」改成『沉默』,提醒自己跟进或放弃。", + "5. 拿到 Offer 后,行会变绿;被拒后会变灰,方便区分。", + "6. 建议每周固定一个时间(比如周日晚)回顾一遍这张表。", + "", + "如果你想看『有多少在面试中 / 投递总数 / 沉默率』等统计,", + "可以让 Claude 帮你做一个看板(artifact)。", + ] + for line in legend_lines: + legend.append([line]) + + wb.save(path) + print(f"✓ Excel 追踪表已生成:{path}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="初始化求职投递追踪表") + parser.add_argument( + "--format", choices=["xlsx", "md"], default="xlsx", help="输出格式" + ) + parser.add_argument( + "--output", required=True, help="输出文件路径(含文件名)" + ) + args = parser.parse_args() + + out = Path(args.output).expanduser() + out.parent.mkdir(parents=True, exist_ok=True) + + if args.format == "xlsx": + write_xlsx(out) + else: + write_md(out) + + +if __name__ == "__main__": + main() diff --git a/skills/job-intent-tracker/scripts/profile_match.py b/skills/job-intent-tracker/scripts/profile_match.py new file mode 100755 index 0000000..bbbfd3d --- /dev/null +++ b/skills/job-intent-tracker/scripts/profile_match.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +""" +profile_match.py — 把用户技能列表 vs 关键词库做匹配,输出匹配度报告 + +用法: + python profile_match.py --skills "SQL,Python,产品规划,A/B测试" \ + --library internet \ + [--out report.md] + +library 取值:internet / tech / finance / general +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +LIB_MAP = { + "internet": "keywords_internet.md", + "tech": "keywords_tech.md", + "finance": "keywords_finance.md", + "general": "keywords_general.md", +} + + +def extract_keywords(md_path: Path) -> list[str]: + """简单解析 markdown,把所有 bullet 后面的中英文词汇收集起来。""" + text = md_path.read_text(encoding="utf-8") + # 匹配 - 开头的行 + bullets = re.findall(r"^\s*[-*]\s+(.+)$", text, flags=re.MULTILINE) + keywords: set[str] = set() + for line in bullets: + # 去掉括号内的解释、占位符、markdown 控制字符 + clean = re.sub(r"[((][^))]*[))]", "", line) + clean = re.sub(r"_{2,}", "", clean) + clean = clean.replace("**", "").replace("__", "") + for token in re.split(r"[、,,//\s]+", clean): + token = token.strip().strip("::。.\"\"''`*").lower() + # 过滤掉只含标点 / 短横 / 数字 的词 + if not re.search(r"[一-龥A-Za-z]", token): + continue + if 1 < len(token) < 30: + keywords.add(token) + return sorted(keywords) + + +def match_score(user_skills: list[str], lib_keywords: list[str]) -> dict: + """返回匹配命中、缺失、命中率。模糊匹配:包含即算命中。""" + user_lower = [s.strip().lower() for s in user_skills if s.strip()] + hits, missing = [], [] + for kw in lib_keywords: + if any(kw in u or u in kw for u in user_lower): + hits.append(kw) + else: + missing.append(kw) + rate = len(hits) / len(lib_keywords) if lib_keywords else 0 + return { + "hits": hits, + "missing": missing, + "rate": rate, + "user_skills": user_lower, + } + + +def render_report(result: dict, library: str) -> str: + rate_pct = f"{result['rate'] * 100:.1f}%" + # 缺口前 20 个,避免太长 + top_missing = result["missing"][:20] + lines = [ + f"# 岗位画像匹配报告 — {library}", + "", + f"- **命中率**:{rate_pct}({len(result['hits'])} / {len(result['hits']) + len(result['missing'])})", + f"- **用户提供技能**:{', '.join(result['user_skills'])}", + "", + "## ✅ 命中的关键词", + "", + ", ".join(result["hits"]) if result["hits"] else "(无)", + "", + "## ⚠️ 缺口(前 20 个,按字典序)", + "", + ", ".join(top_missing) if top_missing else "(无)", + "", + "## 解读", + "", + "- 命中率 < 20%:方向不匹配,建议重新评估目标岗", + "- 命中率 20-50%:可投但需要补关键缺口", + "- 命中率 > 50%:核心匹配,可以重点投", + "", + "注意:本工具是关键词级别的粗筛,不能替代真实 JD 对照(用 jd-resume-tailor 做精准对比)。", + ] + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--skills", required=True, help="用户技能列表,逗号分隔") + parser.add_argument( + "--library", choices=list(LIB_MAP), required=True, help="关键词库" + ) + parser.add_argument("--out", help="输出 markdown 报告路径,缺省直接打印") + parser.add_argument( + "--references-dir", + default=str(Path(__file__).resolve().parent.parent / "references"), + help="references 目录路径", + ) + args = parser.parse_args() + + lib_path = Path(args.references_dir) / LIB_MAP[args.library] + if not lib_path.exists(): + print(f"✗ 找不到关键词库:{lib_path}", file=sys.stderr) + sys.exit(1) + + lib_keywords = extract_keywords(lib_path) + user_skills = [s for s in args.skills.split(",") if s.strip()] + result = match_score(user_skills, lib_keywords) + report = render_report(result, args.library) + + if args.out: + Path(args.out).write_text(report, encoding="utf-8") + print(f"✓ 报告已生成:{args.out}") + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/skills/market-research-reports/SKILL.md b/skills/market-research-reports/SKILL.md new file mode 100755 index 0000000..90435c0 --- /dev/null +++ b/skills/market-research-reports/SKILL.md @@ -0,0 +1,901 @@ +--- +name: market-research-reports +description: "Generate comprehensive market research reports (50+ pages) in the style of top consulting firms (McKinsey, BCG, Gartner). Features professional LaTeX formatting, extensive visual generation with scientific-schematics and generate-image, deep integration with research-lookup for data gathering, and multi-framework strategic analysis including Porter's Five Forces, PESTLE, SWOT, TAM/SAM/SOM, and BCG Matrix." +allowed-tools: [Read, Write, Edit, Bash] +--- + +# Market Research Reports + +## Overview + +Market research reports are comprehensive strategic documents that analyze industries, markets, and competitive landscapes to inform business decisions, investment strategies, and strategic planning. This skill generates **professional-grade reports of 50+ pages** with extensive visual content, modeled after deliverables from top consulting firms like McKinsey, BCG, Bain, Gartner, and Forrester. + +**Key Features:** +- **Comprehensive length**: Reports are designed to be 50+ pages with no token constraints +- **Visual-rich content**: 5-6 key diagrams generated at start (more added as needed during writing) +- **Data-driven analysis**: Deep integration with research-lookup for market data +- **Multi-framework approach**: Porter's Five Forces, PESTLE, SWOT, BCG Matrix, TAM/SAM/SOM +- **Professional formatting**: Consulting-firm quality typography, colors, and layout +- **Actionable recommendations**: Strategic focus with implementation roadmaps + +**Output Format:** LaTeX with professional styling, compiled to PDF. Uses the `market_research.sty` style package for consistent, professional formatting. + +## When to Use This Skill + +This skill should be used when: +- Creating comprehensive market analysis for investment decisions +- Developing industry reports for strategic planning +- Analyzing competitive landscapes and market dynamics +- Conducting market sizing exercises (TAM/SAM/SOM) +- Evaluating market entry opportunities +- Preparing due diligence materials for M&A activities +- Creating thought leadership content for industry positioning +- Developing go-to-market strategy documentation +- Analyzing regulatory and policy impacts on markets +- Building business cases for new product launches + +## Visual Enhancement Requirements + +**CRITICAL: Market research reports should include key visual content.** + +Every report should generate **6 essential visuals** at the start, with additional visuals added as needed during writing. Start with the most critical visualizations to establish the report framework. + +### Visual Generation Tools + +**Use `scientific-schematics` for:** +- Market growth trajectory charts +- TAM/SAM/SOM breakdown diagrams (concentric circles) +- Porter's Five Forces diagrams +- Competitive positioning matrices +- Market segmentation charts +- Value chain diagrams +- Technology roadmaps +- Risk heatmaps +- Strategic prioritization matrices +- Implementation timelines/Gantt charts +- SWOT analysis diagrams +- BCG Growth-Share matrices + +```bash +# Example: Generate a TAM/SAM/SOM diagram +python skills/scientific-schematics/scripts/generate_schematic.py \ + "TAM SAM SOM concentric circle diagram showing Total Addressable Market $50B outer circle, Serviceable Addressable Market $15B middle circle, Serviceable Obtainable Market $3B inner circle, with labels and arrows pointing to each segment" \ + -o figures/tam_sam_som.png --doc-type report + +# Example: Generate Porter's Five Forces +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Porter's Five Forces diagram with center box 'Competitive Rivalry' connected to four surrounding boxes: 'Threat of New Entrants' (top), 'Bargaining Power of Suppliers' (left), 'Bargaining Power of Buyers' (right), 'Threat of Substitutes' (bottom). Each box should show High/Medium/Low rating" \ + -o figures/porters_five_forces.png --doc-type report +``` + +**Use `generate-image` for:** +- Executive summary hero infographics +- Industry/sector conceptual illustrations +- Abstract technology visualizations +- Cover page imagery + +```bash +# Example: Generate executive summary infographic +python skills/generate-image/scripts/generate_image.py \ + "Professional executive summary infographic for market research report, showing key metrics in modern data visualization style, blue and green color scheme, clean minimalist design with icons representing market size, growth rate, and competitive landscape" \ + --output figures/executive_summary.png +``` + +### Recommended Visuals by Section (Generate as Needed) + +| Section | Priority Visuals | Optional Visuals | +|---------|-----------------|------------------| +| Executive Summary | Executive infographic (START) | - | +| Market Size & Growth | Growth trajectory (START), TAM/SAM/SOM (START) | Regional breakdown, segment growth | +| Competitive Landscape | Porter's Five Forces (START), Positioning matrix (START) | Market share chart, strategic groups | +| Risk Analysis | Risk heatmap (START) | Mitigation matrix | +| Strategic Recommendations | Opportunity matrix | Priority framework | +| Implementation Roadmap | Timeline/Gantt | Milestone tracker | +| Investment Thesis | Financial projections | Scenario analysis | + +**Start with 6 priority visuals** (marked as START above), then generate additional visuals as specific sections are written and require visual support. + +--- + +## Report Structure (50+ Pages) + +### Front Matter (~5 pages) + +#### Cover Page (1 page) +- Report title and subtitle +- Hero visualization (generated) +- Date and classification +- Prepared for / Prepared by + +#### Table of Contents (1-2 pages) +- Automated from LaTeX +- List of Figures +- List of Tables + +#### Executive Summary (2-3 pages) +- **Market Snapshot Box**: Key metrics at a glance +- **Investment Thesis**: 3-5 bullet point summary +- **Key Findings**: Major discoveries and insights +- **Strategic Recommendations**: Top 3-5 actionable recommendations +- **Executive Summary Infographic**: Visual synthesis of report highlights + +--- + +### Core Analysis (~35 pages) + +#### Chapter 1: Market Overview & Definition (4-5 pages) + +**Content Requirements:** +- Market definition and scope +- Industry ecosystem mapping +- Key stakeholders and their roles +- Market boundaries and adjacencies +- Historical context and evolution + +**Required Visuals (2):** +1. Market ecosystem/value chain diagram +2. Industry structure diagram + +**Key Data Points:** +- Market definition criteria +- Included/excluded segments +- Geographic scope +- Time horizon for analysis + +--- + +#### Chapter 2: Market Size & Growth Analysis (6-8 pages) + +**Content Requirements:** +- Total Addressable Market (TAM) calculation +- Serviceable Addressable Market (SAM) definition +- Serviceable Obtainable Market (SOM) estimation +- Historical growth analysis (5-10 years) +- Growth projections (5-10 years forward) +- Growth drivers and inhibitors +- Regional market breakdown +- Segment-level analysis + +**Required Visuals (4):** +1. Market growth trajectory chart (historical + projected) +2. TAM/SAM/SOM concentric circles diagram +3. Regional market breakdown (pie chart or treemap) +4. Segment growth comparison (bar chart) + +**Key Data Points:** +- Current market size (with source) +- CAGR (historical and projected) +- Market size by region +- Market size by segment +- Key assumptions for projections + +**Data Sources:** +Use `research-lookup` to find: +- Market research reports (Gartner, Forrester, IDC, etc.) +- Industry association data +- Government statistics +- Company financial reports +- Academic studies + +--- + +#### Chapter 3: Industry Drivers & Trends (5-6 pages) + +**Content Requirements:** +- Macroeconomic factors +- Technology trends +- Regulatory drivers +- Social and demographic shifts +- Environmental factors +- Industry-specific trends + +**Analysis Frameworks:** +- **PESTLE Analysis**: Political, Economic, Social, Technological, Legal, Environmental +- **Trend Impact Assessment**: Likelihood vs Impact matrix + +**Required Visuals (3):** +1. Industry trends timeline or radar chart +2. Driver impact matrix +3. PESTLE analysis diagram + +**Key Data Points:** +- Top 5-10 growth drivers with quantified impact +- Emerging trends with timeline +- Disruption factors + +--- + +#### Chapter 4: Competitive Landscape (6-8 pages) + +**Content Requirements:** +- Market structure analysis +- Major player profiles +- Market share analysis +- Competitive positioning +- Barriers to entry +- Competitive dynamics + +**Analysis Frameworks:** +- **Porter's Five Forces**: Comprehensive industry analysis +- **Competitive Positioning Matrix**: 2x2 matrix on key dimensions +- **Strategic Group Mapping**: Cluster competitors by strategy + +**Required Visuals (4):** +1. Porter's Five Forces diagram +2. Market share pie chart or bar chart +3. Competitive positioning matrix (2x2) +4. Strategic group map + +**Key Data Points:** +- Market share by company (top 10) +- Competitive intensity rating +- Entry barriers assessment +- Supplier/buyer power assessment + +--- + +#### Chapter 5: Customer Analysis & Segmentation (4-5 pages) + +**Content Requirements:** +- Customer segment definitions +- Segment size and growth +- Buying behavior analysis +- Customer needs and pain points +- Decision-making process +- Value drivers by segment + +**Analysis Frameworks:** +- **Customer Segmentation Matrix**: Size vs Growth +- **Value Proposition Canvas**: Jobs, Pains, Gains +- **Customer Journey Mapping**: Awareness to Advocacy + +**Required Visuals (3):** +1. Customer segmentation breakdown (pie/treemap) +2. Segment attractiveness matrix +3. Customer journey or value proposition diagram + +**Key Data Points:** +- Segment sizes and percentages +- Growth rates by segment +- Average deal size / revenue per customer +- Customer acquisition cost by segment + +--- + +#### Chapter 6: Technology & Innovation Landscape (4-5 pages) + +**Content Requirements:** +- Current technology stack +- Emerging technologies +- Innovation trends +- Technology adoption curves +- R&D investment analysis +- Patent landscape + +**Analysis Frameworks:** +- **Technology Readiness Assessment**: TRL levels +- **Hype Cycle Positioning**: Where technologies sit +- **Technology Roadmap**: Evolution over time + +**Required Visuals (2):** +1. Technology roadmap diagram +2. Innovation/adoption curve or hype cycle + +**Key Data Points:** +- R&D spending in the industry +- Key technology milestones +- Patent filing trends +- Technology adoption rates + +--- + +#### Chapter 7: Regulatory & Policy Environment (3-4 pages) + +**Content Requirements:** +- Current regulatory framework +- Key regulatory bodies +- Compliance requirements +- Upcoming regulatory changes +- Policy trends +- Impact assessment + +**Required Visuals (1):** +1. Regulatory timeline or framework diagram + +**Key Data Points:** +- Key regulations and effective dates +- Compliance costs +- Regulatory risks +- Policy change probability + +--- + +#### Chapter 8: Risk Analysis (3-4 pages) + +**Content Requirements:** +- Market risks +- Competitive risks +- Regulatory risks +- Technology risks +- Operational risks +- Financial risks +- Risk mitigation strategies + +**Analysis Frameworks:** +- **Risk Heatmap**: Probability vs Impact +- **Risk Register**: Comprehensive risk inventory +- **Mitigation Matrix**: Risk vs Mitigation strategy + +**Required Visuals (2):** +1. Risk heatmap (probability vs impact) +2. Risk mitigation matrix + +**Key Data Points:** +- Top 10 risks with ratings +- Risk probability scores +- Impact severity scores +- Mitigation cost estimates + +--- + +### Strategic Recommendations (~10 pages) + +#### Chapter 9: Strategic Opportunities & Recommendations (4-5 pages) + +**Content Requirements:** +- Opportunity identification +- Opportunity sizing +- Strategic options analysis +- Prioritization framework +- Detailed recommendations +- Success factors + +**Analysis Frameworks:** +- **Opportunity Attractiveness Matrix**: Attractiveness vs Ability to Win +- **Strategic Options Framework**: Build, Buy, Partner, Ignore +- **Priority Matrix**: Impact vs Effort + +**Required Visuals (3):** +1. Opportunity matrix +2. Strategic options framework +3. Priority/recommendation matrix + +**Key Data Points:** +- Opportunity sizes +- Investment requirements +- Expected returns +- Timeline to value + +--- + +#### Chapter 10: Implementation Roadmap (3-4 pages) + +**Content Requirements:** +- Phased implementation plan +- Key milestones and deliverables +- Resource requirements +- Timeline and sequencing +- Dependencies and critical path +- Governance structure + +**Required Visuals (2):** +1. Implementation timeline/Gantt chart +2. Milestone tracker or phase diagram + +**Key Data Points:** +- Phase durations +- Resource requirements +- Key milestones with dates +- Budget allocation by phase + +--- + +#### Chapter 11: Investment Thesis & Financial Projections (3-4 pages) + +**Content Requirements:** +- Investment summary +- Financial projections +- Scenario analysis +- Return expectations +- Key assumptions +- Sensitivity analysis + +**Required Visuals (2):** +1. Financial projection chart (revenue, growth) +2. Scenario analysis comparison + +**Key Data Points:** +- Revenue projections (3-5 years) +- CAGR projections +- ROI/IRR expectations +- Key financial assumptions + +--- + +### Back Matter (~5 pages) + +#### Appendix A: Methodology & Data Sources (1-2 pages) +- Research methodology +- Data collection approach +- Data sources and citations +- Limitations and assumptions + +#### Appendix B: Detailed Market Data Tables (2-3 pages) +- Comprehensive market data tables +- Regional breakdowns +- Segment details +- Historical data series + +#### Appendix C: Company Profiles (1-2 pages) +- Brief profiles of key competitors +- Financial highlights +- Strategic focus areas + +#### References/Bibliography +- All sources cited +- BibTeX format for LaTeX + +--- + +## Workflow + +### Phase 1: Research & Data Gathering + +**Step 1: Define Scope** +- Clarify market definition +- Set geographic boundaries +- Determine time horizon +- Identify key questions to answer + +**Step 2: Conduct Deep Research** + +Use `research-lookup` extensively to gather market data: + +```bash +# Market size and growth data +python skills/research-lookup/scripts/research_lookup.py \ + "What is the current market size and projected growth rate for [MARKET] industry? Include TAM, SAM, SOM estimates and CAGR projections" + +# Competitive landscape +python skills/research-lookup/scripts/research_lookup.py \ + "Who are the top 10 competitors in the [MARKET] market? What is their market share and competitive positioning?" + +# Industry trends +python skills/research-lookup/scripts/research_lookup.py \ + "What are the major trends and growth drivers in the [MARKET] industry for 2024-2030?" + +# Regulatory environment +python skills/research-lookup/scripts/research_lookup.py \ + "What are the key regulations and policy changes affecting the [MARKET] industry?" +``` + +**Step 3: Data Organization** +- Create `sources/` folder with research notes +- Organize data by section +- Identify data gaps +- Conduct follow-up research as needed + +### Phase 2: Analysis & Framework Application + +**Step 4: Apply Analysis Frameworks** + +For each framework, conduct structured analysis: + +- **Market Sizing**: TAM → SAM → SOM with clear assumptions +- **Porter's Five Forces**: Rate each force High/Medium/Low with rationale +- **PESTLE**: Analyze each dimension with trends and impacts +- **SWOT**: Internal strengths/weaknesses, external opportunities/threats +- **Competitive Positioning**: Define axes, plot competitors + +**Step 5: Develop Insights** +- Synthesize findings into key insights +- Identify strategic implications +- Develop recommendations +- Prioritize opportunities + +### Phase 3: Visual Generation + +**Step 6: Generate All Visuals** + +Generate visuals BEFORE writing the report. Use the batch generation script: + +```bash +# Generate all standard market report visuals +python skills/market-research-reports/scripts/generate_market_visuals.py \ + --topic "[MARKET NAME]" \ + --output-dir figures/ +``` + +Or generate individually: + +```bash +# 1. Market growth trajectory +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Bar chart showing market growth from 2020 to 2034, with historical bars in dark blue (2020-2024) and projected bars in light blue (2025-2034). Y-axis shows market size in billions USD. Include CAGR annotation" \ + -o figures/01_market_growth.png --doc-type report + +# 2. TAM/SAM/SOM breakdown +python skills/scientific-schematics/scripts/generate_schematic.py \ + "TAM SAM SOM concentric circles diagram. Outer circle TAM Total Addressable Market, middle circle SAM Serviceable Addressable Market, inner circle SOM Serviceable Obtainable Market. Each labeled with acronym and description. Blue gradient" \ + -o figures/02_tam_sam_som.png --doc-type report + +# 3. Porter's Five Forces +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Porter's Five Forces diagram with center box 'Competitive Rivalry' connected to four surrounding boxes: Threat of New Entrants (top), Bargaining Power of Suppliers (left), Bargaining Power of Buyers (right), Threat of Substitutes (bottom). Color code by rating: High=red, Medium=yellow, Low=green" \ + -o figures/03_porters_five_forces.png --doc-type report + +# 4. Competitive positioning matrix +python skills/scientific-schematics/scripts/generate_schematic.py \ + "2x2 competitive positioning matrix with X-axis 'Market Focus (Niche to Broad)' and Y-axis 'Solution Approach (Product to Platform)'. Plot 8-10 competitors as labeled circles of varying sizes. Include quadrant labels" \ + -o figures/04_competitive_positioning.png --doc-type report + +# 5. Risk heatmap +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Risk heatmap matrix. X-axis Impact (Low to Critical), Y-axis Probability (Unlikely to Very Likely). Color gradient: Green (low risk) to Red (critical risk). Plot 10-12 risks as labeled points" \ + -o figures/05_risk_heatmap.png --doc-type report + +# 6. (Optional) Executive summary infographic +python skills/generate-image/scripts/generate_image.py \ + "Professional executive summary infographic for market research report, modern data visualization style, blue and green color scheme, clean minimalist design" \ + --output figures/06_exec_summary.png +``` + +### Phase 4: Report Writing + +**Step 7: Initialize Project Structure** + +Create the standard project structure: + +``` +writing_outputs/YYYYMMDD_HHMMSS_market_report_[topic]/ +├── progress.md +├── drafts/ +│ └── v1_market_report.tex +├── references/ +│ └── references.bib +├── figures/ +│ └── [all generated visuals] +├── sources/ +│ └── [research notes] +└── final/ +``` + +**Step 8: Write Report Using Template** + +Use the `market_report_template.tex` as a starting point. Write each section following the structure guide, ensuring: + +- **Comprehensive coverage**: Every subsection addressed +- **Data-driven content**: Claims supported by research +- **Visual integration**: Reference all generated figures +- **Professional tone**: Consulting-style writing +- **No token constraints**: Write fully, don't abbreviate + +**Writing Guidelines:** +- Use active voice where possible +- Lead with insights, support with data +- Use numbered lists for recommendations +- Include data sources for all statistics +- Create smooth transitions between sections + +### Phase 5: Compilation & Review + +**Step 9: Compile LaTeX** + +```bash +cd writing_outputs/[project_folder]/drafts/ +xelatex v1_market_report.tex +bibtex v1_market_report +xelatex v1_market_report.tex +xelatex v1_market_report.tex +``` + +**Step 10: Quality Review** + +Verify the report meets quality standards: + +- [ ] Total page count is 50+ pages +- [ ] All essential visuals (5-6 core + any additional) are included and render correctly +- [ ] Executive summary captures key findings +- [ ] All data points have sources cited +- [ ] Analysis frameworks are properly applied +- [ ] Recommendations are actionable and prioritized +- [ ] No orphaned figures or tables +- [ ] Table of contents, list of figures, list of tables are accurate +- [ ] Bibliography is complete +- [ ] PDF renders without errors + +**Step 11: Peer Review** + +Use the peer-review skill to evaluate the report: +- Assess comprehensiveness +- Verify data accuracy +- Check logical flow +- Evaluate recommendation quality + +--- + +## Quality Standards + +### Page Count Targets + +| Section | Minimum Pages | Target Pages | +|---------|---------------|--------------| +| Front Matter | 4 | 5 | +| Market Overview | 4 | 5 | +| Market Size & Growth | 5 | 7 | +| Industry Drivers | 4 | 6 | +| Competitive Landscape | 5 | 7 | +| Customer Analysis | 3 | 5 | +| Technology Landscape | 3 | 5 | +| Regulatory Environment | 2 | 4 | +| Risk Analysis | 2 | 4 | +| Strategic Recommendations | 3 | 5 | +| Implementation Roadmap | 2 | 4 | +| Investment Thesis | 2 | 4 | +| Back Matter | 4 | 5 | +| **TOTAL** | **43** | **66** | + +### Visual Quality Requirements + +- **Resolution**: All images at 300 DPI minimum +- **Format**: PNG for raster, PDF for vector +- **Accessibility**: Colorblind-friendly palettes +- **Consistency**: Same color scheme throughout +- **Labeling**: All axes, legends, and data points labeled +- **Source Attribution**: Sources cited in figure captions + +### Data Quality Requirements + +- **Currency**: Data no older than 2 years (prefer current year) +- **Sourcing**: All statistics attributed to specific sources +- **Validation**: Cross-reference multiple sources when possible +- **Assumptions**: All projections state underlying assumptions +- **Limitations**: Acknowledge data limitations and gaps + +### Writing Quality Requirements + +- **Objectivity**: Present balanced analysis, acknowledge uncertainties +- **Clarity**: Avoid jargon, define technical terms +- **Precision**: Use specific numbers over vague qualifiers +- **Structure**: Clear headings, logical flow, smooth transitions +- **Actionability**: Recommendations are specific and implementable + +--- + +## LaTeX Formatting + +### Using the Style Package + +The `market_research.sty` package provides professional formatting. Include it in your document: + +```latex +\documentclass[11pt,letterpaper]{report} +\usepackage{market_research} +``` + +### Box Environments + +Use colored boxes to highlight key content: + +```latex +% Key insight box (blue) +\begin{keyinsightbox}[Key Finding] +The market is projected to grow at 15.3% CAGR through 2030. +\end{keyinsightbox} + +% Market data box (green) +\begin{marketdatabox}[Market Snapshot] +\begin{itemize} + \item Market Size (2024): \$45.2B + \item Projected Size (2030): \$98.7B + \item CAGR: 15.3% +\end{itemize} +\end{marketdatabox} + +% Risk box (orange/warning) +\begin{riskbox}[Critical Risk] +Regulatory changes could impact 40% of market participants. +\end{riskbox} + +% Recommendation box (purple) +\begin{recommendationbox}[Strategic Recommendation] +Prioritize market entry in the Asia-Pacific region. +\end{recommendationbox} + +% Callout box (gray) +\begin{calloutbox}[Definition] +TAM (Total Addressable Market) represents the total revenue opportunity. +\end{calloutbox} +``` + +### Figure Formatting + +```latex +\begin{figure}[htbp] +\centering +\includegraphics[width=0.9\textwidth]{../figures/market_growth.png} +\caption{Market Growth Trajectory (2020-2030). Source: Industry analysis, company data.} +\label{fig:market_growth} +\end{figure} +``` + +### Table Formatting + +```latex +\begin{table}[htbp] +\centering +\caption{Market Size by Region (2024)} +\begin{tabular}{@{}lrrr@{}} +\toprule +\textbf{Region} & \textbf{Size (USD)} & \textbf{Share} & \textbf{CAGR} \\ +\midrule +North America & \$18.2B & 40.3\% & 12.5\% \\ +\rowcolor{tablealt} Europe & \$12.1B & 26.8\% & 14.2\% \\ +Asia-Pacific & \$10.5B & 23.2\% & 18.7\% \\ +\rowcolor{tablealt} Rest of World & \$4.4B & 9.7\% & 11.3\% \\ +\midrule +\textbf{Total} & \textbf{\$45.2B} & \textbf{100\%} & \textbf{15.3\%} \\ +\bottomrule +\end{tabular} +\label{tab:market_by_region} +\end{table} +``` + +For complete formatting reference, see `assets/FORMATTING_GUIDE.md`. + +--- + +## Integration with Other Skills + +This skill works synergistically with: + +- **research-lookup**: Essential for gathering market data, statistics, and competitive intelligence +- **scientific-schematics**: Generate all diagrams, charts, and visualizations +- **generate-image**: Create infographics and conceptual illustrations +- **peer-review**: Evaluate report quality and completeness +- **citation-management**: Manage BibTeX references + +--- + +## Example Prompts + +### Market Overview Section + +``` +Write a comprehensive market overview section for the [Electric Vehicle Charging Infrastructure] market. Include: +- Clear market definition and scope +- Industry ecosystem with key stakeholders +- Value chain analysis +- Historical evolution of the market +- Current market dynamics + +Generate 2 supporting visuals using scientific-schematics. +``` + +### Competitive Landscape Section + +``` +Analyze the competitive landscape for the [Cloud Computing] market. Include: +- Porter's Five Forces analysis with High/Medium/Low ratings +- Top 10 competitors with market share +- Competitive positioning matrix +- Strategic group mapping +- Barriers to entry analysis + +Generate 4 supporting visuals including Porter's Five Forces diagram and positioning matrix. +``` + +### Strategic Recommendations Section + +``` +Develop strategic recommendations for entering the [Renewable Energy Storage] market. Include: +- 5-7 prioritized recommendations +- Opportunity sizing for each +- Implementation considerations +- Risk factors and mitigations +- Success criteria + +Generate 3 supporting visuals including opportunity matrix and priority framework. +``` + +--- + +## Checklist: 50+ Page Validation + +Before finalizing the report, verify: + +### Structure Completeness +- [ ] Cover page with hero visual +- [ ] Table of contents (auto-generated) +- [ ] List of figures (auto-generated) +- [ ] List of tables (auto-generated) +- [ ] Executive summary (2-3 pages) +- [ ] All 11 core chapters present +- [ ] Appendix A: Methodology +- [ ] Appendix B: Data tables +- [ ] Appendix C: Company profiles +- [ ] References/Bibliography + +### Visual Completeness (Core 5-6) +- [ ] Market growth trajectory chart (Priority 1) +- [ ] TAM/SAM/SOM diagram (Priority 2) +- [ ] Porter's Five Forces (Priority 3) +- [ ] Competitive positioning matrix (Priority 4) +- [ ] Risk heatmap (Priority 5) +- [ ] Executive summary infographic (Priority 6, optional) + +### Additional Visuals (Generate as Needed) +- [ ] Market ecosystem diagram +- [ ] Regional breakdown chart +- [ ] Segment growth chart +- [ ] Industry trends/PESTLE diagram +- [ ] Market share chart +- [ ] Customer segmentation chart +- [ ] Technology roadmap +- [ ] Regulatory timeline +- [ ] Opportunity matrix +- [ ] Implementation timeline +- [ ] Financial projections chart +- [ ] Other section-specific visuals + +### Content Quality +- [ ] All statistics have sources +- [ ] Projections include assumptions +- [ ] Frameworks properly applied +- [ ] Recommendations are actionable +- [ ] Writing is professional quality +- [ ] No placeholder or incomplete sections + +### Technical Quality +- [ ] PDF compiles without errors +- [ ] All figures render correctly +- [ ] Cross-references work +- [ ] Bibliography complete +- [ ] Page count exceeds 50 + +--- + +## Resources + +### Reference Files + +Load these files for detailed guidance: + +- **`references/report_structure_guide.md`**: Detailed section-by-section content requirements +- **`references/visual_generation_guide.md`**: Complete prompts for generating all visual types +- **`references/data_analysis_patterns.md`**: Templates for Porter's, PESTLE, SWOT, etc. + +### Assets + +- **`assets/market_research.sty`**: LaTeX style package +- **`assets/market_report_template.tex`**: Complete LaTeX template +- **`assets/FORMATTING_GUIDE.md`**: Quick reference for box environments and styling + +### Scripts + +- **`scripts/generate_market_visuals.py`**: Batch generate all report visuals + +--- + +## Troubleshooting + +### Common Issues + +**Problem**: Report is under 50 pages +- **Solution**: Expand data tables in appendices, add more detailed company profiles, include additional regional breakdowns + +**Problem**: Visuals not rendering +- **Solution**: Check file paths in LaTeX, ensure images are in figures/ folder, verify file extensions + +**Problem**: Bibliography missing entries +- **Solution**: Run bibtex after first xelatex pass, check .bib file for syntax errors + +**Problem**: Table/figure overflow +- **Solution**: Use `\resizebox` or `adjustbox` package, reduce image width percentage + +**Problem**: Poor visual quality from generation +- **Solution**: Use `--doc-type report` flag, increase iterations with `--iterations 5` + +--- + +Use this skill to create comprehensive, visually-rich market research reports that rival top consulting firm deliverables. The combination of deep research, structured frameworks, and extensive visualization produces documents that inform strategic decisions and demonstrate analytical rigor. diff --git a/skills/market-research-reports/assets/FORMATTING_GUIDE.md b/skills/market-research-reports/assets/FORMATTING_GUIDE.md new file mode 100755 index 0000000..09a1da4 --- /dev/null +++ b/skills/market-research-reports/assets/FORMATTING_GUIDE.md @@ -0,0 +1,428 @@ +# Market Research Report Formatting Guide + +Quick reference for using the `market_research.sty` style package. + +## Color Palette + +### Primary Colors +| Color Name | RGB | Hex | Usage | +|------------|-----|-----|-------| +| `primaryblue` | (0, 51, 102) | `#003366` | Headers, titles, links | +| `secondaryblue` | (51, 102, 153) | `#336699` | Subsections, secondary elements | +| `lightblue` | (173, 216, 230) | `#ADD8E6` | Key insight box backgrounds | +| `accentblue` | (0, 120, 215) | `#0078D7` | Accent highlights, opportunity boxes | + +### Secondary Colors +| Color Name | RGB | Hex | Usage | +|------------|-----|-----|-------| +| `accentgreen` | (0, 128, 96) | `#008060` | Market data boxes, positive indicators | +| `lightgreen` | (200, 230, 201) | `#C8E6C9` | Market data box backgrounds | +| `warningorange` | (255, 140, 0) | `#FF8C00` | Risk boxes, warnings | +| `alertred` | (198, 40, 40) | `#C62828` | Critical risks | +| `recommendpurple` | (103, 58, 183) | `#673AB7` | Recommendation boxes | + +### Neutral Colors +| Color Name | RGB | Hex | Usage | +|------------|-----|-----|-------| +| `darkgray` | (66, 66, 66) | `#424242` | Body text | +| `mediumgray` | (117, 117, 117) | `#757575` | Secondary text | +| `lightgray` | (240, 240, 240) | `#F0F0F0` | Backgrounds, callout boxes | +| `tablealt` | (245, 247, 250) | `#F5F7FA` | Alternating table rows | + +--- + +## Box Environments + +### Key Insight Box (Blue) +For major findings, insights, and important discoveries. + +```latex +\begin{keyinsightbox}[Custom Title] +The market is projected to grow at 15.3% CAGR through 2030, driven by +increasing enterprise adoption and favorable regulatory conditions. +\end{keyinsightbox} +``` + +### Market Data Box (Green) +For market statistics, metrics, and data highlights. + +```latex +\begin{marketdatabox}[Market Snapshot] +\begin{itemize} + \item \textbf{Market Size (2024):} \marketsize{45.2 billion} + \item \textbf{Projected Size (2030):} \marketsize{98.7 billion} + \item \textbf{CAGR:} \growthrate{15.3} +\end{itemize} +\end{marketdatabox} +``` + +### Risk Box (Orange/Warning) +For risk factors, warnings, and cautions. + +```latex +\begin{riskbox}[Market Risk] +Regulatory changes in the European Union could impact 40% of market +participants within the next 18 months. +\end{riskbox} +``` + +### Critical Risk Box (Red) +For high-severity or critical risks. + +```latex +\begin{criticalriskbox}[Critical: Supply Chain Disruption] +A major supply chain disruption could result in 6-12 month delays +and 30% cost increases. +\end{criticalriskbox} +``` + +### Recommendation Box (Purple) +For strategic recommendations and action items. + +```latex +\begin{recommendationbox}[Strategic Recommendation] +\begin{enumerate} + \item Prioritize market entry in Asia-Pacific region + \item Develop strategic partnerships with local distributors + \item Invest in localization of product offerings +\end{enumerate} +\end{recommendationbox} +``` + +### Callout Box (Gray) +For definitions, notes, and supplementary information. + +```latex +\begin{calloutbox}[Definition: TAM] +Total Addressable Market (TAM) represents the total revenue opportunity +available if 100% market share was achieved. +\end{calloutbox} +``` + +### Executive Summary Box +Special styling for executive summary highlights. + +```latex +\begin{executivesummarybox}[Executive Summary] +Key findings and highlights of the report... +\end{executivesummarybox} +``` + +### Opportunity Box (Teal/Accent Blue) +For opportunities and positive findings. + +```latex +\begin{opportunitybox}[Growth Opportunity] +The Asia-Pacific market represents a \$15 billion opportunity +growing at 22% CAGR. +\end{opportunitybox} +``` + +### Framework Boxes +For strategic analysis frameworks. + +```latex +% SWOT Analysis +\begin{swotbox}[SWOT Analysis Summary] +Content... +\end{swotbox} + +% Porter's Five Forces +\begin{porterbox}[Porter's Five Forces Analysis] +Content... +\end{porterbox} +``` + +--- + +## Pull Quotes + +For highlighting important statistics or quotes. + +```latex +\begin{pullquote} +"The convergence of AI and healthcare represents a \$199 billion +opportunity by 2034." +\end{pullquote} +``` + +--- + +## Stat Boxes + +For highlighting key statistics (use in rows of 3). + +```latex +\begin{center} +\statbox{\$45.2B}{Market Size 2024} +\statbox{15.3\%}{CAGR 2024-2030} +\statbox{23\%}{Market Leader Share} +\end{center} +``` + +--- + +## Custom Commands + +### Highlighting Text +```latex +\highlight{Important text} % Blue bold +``` + +### Market Size Formatting +```latex +\marketsize{45.2 billion} % Outputs: $45.2 billion in green +``` + +### Growth Rate Formatting +```latex +\growthrate{15.3} % Outputs: 15.3% in green +``` + +### Risk Indicators +```latex +\riskhigh{} % Outputs: HIGH in red +\riskmedium{} % Outputs: MEDIUM in orange +\risklow{} % Outputs: LOW in green +``` + +### Rating Stars (1-5) +```latex +\rating{4} % Outputs: ★★★★☆ +``` + +### Trend Indicators +```latex +\trendup{} % Green up triangle +\trenddown{} % Red down triangle +\trendflat{} % Gray right arrow +``` + +--- + +## Table Formatting + +### Standard Table with Alternating Rows +```latex +\begin{table}[htbp] +\centering +\caption{Market Size by Region} +\begin{tabular}{@{}lrrr@{}} +\toprule +\textbf{Region} & \textbf{Size} & \textbf{Share} & \textbf{CAGR} \\ +\midrule +North America & \$18.2B & 40.3\% & 12.5\% \\ +\rowcolor{tablealt} Europe & \$12.1B & 26.8\% & 14.2\% \\ +Asia-Pacific & \$10.5B & 23.2\% & 18.7\% \\ +\rowcolor{tablealt} Rest of World & \$4.4B & 9.7\% & 11.3\% \\ +\midrule +\textbf{Total} & \textbf{\$45.2B} & \textbf{100\%} & \textbf{15.3\%} \\ +\bottomrule +\end{tabular} +\label{tab:regional} +\end{table} +``` + +### Table with Trend Indicators +```latex +\begin{tabular}{@{}lrrl@{}} +\toprule +\textbf{Company} & \textbf{Revenue} & \textbf{Share} & \textbf{Trend} \\ +\midrule +Company A & \$5.2B & 15.3\% & \trendup{} +12\% \\ +Company B & \$4.8B & 14.1\% & \trenddown{} -3\% \\ +Company C & \$4.2B & 12.4\% & \trendflat{} +1\% \\ +\bottomrule +\end{tabular} +``` + +--- + +## Figure Formatting + +### Standard Figure +```latex +\begin{figure}[htbp] +\centering +\includegraphics[width=0.9\textwidth]{../figures/market_growth.png} +\caption{Market Growth Trajectory (2020-2030)} +\label{fig:growth} +\end{figure} +``` + +### Figure with Source Attribution +```latex +\begin{figure}[htbp] +\centering +\includegraphics[width=0.85\textwidth]{../figures/market_share.png} +\caption{Market Share Distribution (2024)} +\figuresource{Company annual reports, industry analysis} +\label{fig:market_share} +\end{figure} +``` + +--- + +## List Formatting + +### Bullet Lists +```latex +\begin{itemize} + \item First item with automatic blue bullet + \item Second item + \item Third item +\end{itemize} +``` + +### Numbered Lists +```latex +\begin{enumerate} + \item First item with blue number + \item Second item + \item Third item +\end{enumerate} +``` + +### Nested Lists +```latex +\begin{itemize} + \item Main point + \begin{itemize} + \item Sub-point A + \item Sub-point B + \end{itemize} + \item Another main point +\end{itemize} +``` + +--- + +## Title Page + +### Using the Custom Title Command +```latex +\makemarketreporttitle + {Market Title} % Report title + {Subtitle Here} % Subtitle + {../figures/cover.png} % Hero image (leave empty for no image) + {January 2025} % Date + {Market Intelligence Team} % Author/prepared by +``` + +### Manual Title Page +See the template for full manual title page code. + +--- + +## Appendix Sections + +```latex +\appendix + +\chapter{Methodology} + +\appendixsection{Data Sources} +Content that appears in table of contents... +``` + +--- + +## Common Patterns + +### Market Snapshot Section +```latex +\begin{marketdatabox}[Market Snapshot] +\begin{itemize} + \item \textbf{Current Market Size:} \marketsize{45.2 billion} + \item \textbf{Projected Size (2030):} \marketsize{98.7 billion} + \item \textbf{CAGR:} \growthrate{15.3} + \item \textbf{Largest Segment:} Enterprise (42\% share) + \item \textbf{Fastest Growing Region:} APAC (\growthrate{22.1} CAGR) +\end{itemize} +\end{marketdatabox} +``` + +### Risk Register Summary +```latex +\begin{table}[htbp] +\centering +\caption{Risk Assessment Summary} +\begin{tabular}{@{}llccl@{}} +\toprule +\textbf{Risk} & \textbf{Category} & \textbf{Prob.} & \textbf{Impact} & \textbf{Rating} \\ +\midrule +Market disruption & Market & High & High & \riskhigh{} \\ +\rowcolor{tablealt} Regulatory change & Regulatory & Med & High & \riskhigh{} \\ +New entrant & Competitive & Med & Med & \riskmedium{} \\ +\rowcolor{tablealt} Tech obsolescence & Technology & Low & High & \riskmedium{} \\ +Currency fluctuation & Financial & Med & Low & \risklow{} \\ +\bottomrule +\end{tabular} +\end{table} +``` + +### Competitive Comparison Table +```latex +\begin{table}[htbp] +\centering +\caption{Competitive Comparison} +\begin{tabular}{@{}lccccc@{}} +\toprule +\textbf{Factor} & \textbf{Co. A} & \textbf{Co. B} & \textbf{Co. C} & \textbf{Co. D} \\ +\midrule +Market Share & \rating{5} & \rating{4} & \rating{3} & \rating{2} \\ +\rowcolor{tablealt} Product Quality & \rating{4} & \rating{5} & \rating{3} & \rating{4} \\ +Price Competitiveness & \rating{3} & \rating{3} & \rating{5} & \rating{4} \\ +\rowcolor{tablealt} Innovation & \rating{5} & \rating{4} & \rating{2} & \rating{3} \\ +Customer Service & \rating{4} & \rating{4} & \rating{4} & \rating{5} \\ +\bottomrule +\end{tabular} +\end{table} +``` + +--- + +## Troubleshooting + +### Box Overflow +If box content overflows the page, break into multiple boxes or use page breaks: +```latex +\newpage +\begin{keyinsightbox}[Continued...] +``` + +### Figure Placement +Use `[htbp]` for flexible placement, or `[H]` (requires `float` package) for exact placement: +```latex +\begin{figure}[H] % Requires \usepackage{float} +``` + +### Table Too Wide +Use `\resizebox` or `adjustbox`: +```latex +\resizebox{\textwidth}{!}{ +\begin{tabular}{...} +... +\end{tabular} +} +``` + +### Color Not Appearing +Ensure `xcolor` package is loaded with `[table]` option (already included in style file). + +--- + +## Compilation + +Compile with XeLaTeX for best results: +```bash +xelatex report.tex +bibtex report +xelatex report.tex +xelatex report.tex +``` + +Or use latexmk: +```bash +latexmk -xelatex report.tex +``` diff --git a/skills/market-research-reports/assets/market_report_template.tex b/skills/market-research-reports/assets/market_report_template.tex new file mode 100755 index 0000000..244264b --- /dev/null +++ b/skills/market-research-reports/assets/market_report_template.tex @@ -0,0 +1,1380 @@ +% !TEX program = xelatex +% Market Research Report Template +% Professional formatting for 50+ page comprehensive market reports +% Use with market_research.sty style package + +\documentclass[11pt,letterpaper]{report} +\usepackage{market_research} + +% ============================================================================ +% DOCUMENT METADATA - CUSTOMIZE THESE +% ============================================================================ +\newcommand{\reporttitle}{[MARKET NAME]} +\newcommand{\reportsubtitle}{Comprehensive Market Analysis Report} +\newcommand{\reportdate}{\today} +\newcommand{\reportauthor}{Market Intelligence Division} +\newcommand{\reportclassification}{Confidential} + +% ============================================================================ +% PDF METADATA +% ============================================================================ +\hypersetup{ + pdftitle={\reporttitle{} - \reportsubtitle{}}, + pdfauthor={\reportauthor{}}, + pdfsubject={Market Research Report}, + pdfkeywords={market research, market analysis, competitive landscape, strategic analysis} +} + +% ============================================================================ +% DOCUMENT START +% ============================================================================ +\begin{document} + +% ============================================================================ +% TITLE PAGE +% ============================================================================ +% To use a hero image, replace the empty braces with the path: +% \makemarketreporttitle{\reporttitle}{\reportsubtitle}{../figures/cover_image.png}{\reportdate}{\reportauthor} + +\begin{titlepage} +\centering +\vspace*{2cm} + +{\Huge\bfseries\color{primaryblue} \reporttitle\\[0.5cm]} +{\LARGE\bfseries \reportsubtitle\\[1.5cm]} + +% VISUAL: Generate hero/cover image +% python skills/generate-image/scripts/generate_image.py "Professional executive summary infographic for [MARKET] market research report, showing key metrics in modern data visualization style, blue and green color scheme, clean minimalist design" --output figures/cover_image.png +% Uncomment below when image is generated: +% \includegraphics[width=\textwidth]{../figures/cover_image.png}\\[1.5cm] + +\vspace{4cm} + +{\Large\bfseries Comprehensive Market Research Report\\[0.5cm]} +{\large Strategic Intelligence for Business Decision-Making\\[3cm]} + +{\large +\textbf{Date:} \reportdate\\[0.3cm] +\textbf{Prepared By:} \reportauthor\\[0.3cm] +\textbf{Classification:} \reportclassification\\[0.3cm] +\textbf{Report Type:} Full Market Analysis +} + +\vfill + +{\footnotesize +\textit{This report contains market intelligence and strategic analysis based on publicly available data and proprietary research. All sources are cited and independently verifiable.} +} + +\end{titlepage} + +% ============================================================================ +% FRONT MATTER +% ============================================================================ +\pagenumbering{roman} + +% Table of Contents +\tableofcontents +\newpage + +% List of Figures +\listoffigures +\newpage + +% List of Tables +\listoftables +\newpage + +% ============================================================================ +% MAIN CONTENT +% ============================================================================ +\pagenumbering{arabic} + +% ============================================================================ +% EXECUTIVE SUMMARY (2-3 pages) +% ============================================================================ +\chapter{Executive Summary} + +\section{Report Overview} + +This comprehensive market analysis examines the \reporttitle{} market, providing strategic intelligence for investors, executives, and strategic planners. The report synthesizes data from authoritative sources including market research firms, regulatory agencies, industry associations, and enterprise surveys. + +% VISUAL: Executive summary infographic +% python skills/scientific-schematics/scripts/generate_schematic.py "Executive summary infographic showing 4 key metrics: Market Size $XX.XB (2024), CAGR XX.X%, Top 3 Players, and Key Trend. Use blue boxes with white text, professional layout" -o figures/exec_summary_infographic.png --doc-type report + +\subsection{Market Snapshot} + +\begin{marketdatabox}[Market Snapshot: \reporttitle{}] +\begin{itemize}[leftmargin=*] + \item \textbf{Current Market Size (2024):} \marketsize{X.XX billion} + \item \textbf{Projected Market Size (2034):} \marketsize{XX.XX billion} + \item \textbf{Compound Annual Growth Rate (CAGR):} \growthrate{XX.X} + \item \textbf{Growth Multiple:} Xx increase over 10 years + \item \textbf{Largest Segment:} [Segment Name] (XX\% market share) + \item \textbf{Fastest Growing Region:} [Region] (\growthrate{XX.X} CAGR) + \item \textbf{Current Enterprise Adoption:} XX\% +\end{itemize} +\end{marketdatabox} + +\subsection{Investment Thesis} + +The convergence of multiple market catalysts creates a compelling opportunity for investment and strategic action in the \reporttitle{} market: + +\begin{keyinsightbox}[Key Investment Drivers] +\begin{enumerate} + \item \textbf{[Driver 1]:} [Brief explanation of why this driver creates opportunity] + \item \textbf{[Driver 2]:} [Brief explanation] + \item \textbf{[Driver 3]:} [Brief explanation] + \item \textbf{[Driver 4]:} [Brief explanation] +\end{enumerate} +\end{keyinsightbox} + +\subsection{Key Findings} + +\paragraph{Market Dynamics} +[Summarize the most important findings about market size, growth, and dynamics. Include 3-5 key statistics with sources.] + +\paragraph{Competitive Landscape} +[Summarize competitive dynamics, market concentration, and key players. Include market share of top players.] + +\paragraph{Growth Drivers} +[Summarize the primary factors driving market growth and their expected impact.] + +\paragraph{Risk Factors} +[Summarize the key risks that could impact market development.] + +\subsection{Strategic Recommendations} + +Based on the comprehensive analysis presented in this report, we recommend the following strategic actions: + +\begin{recommendationbox}[Top Strategic Recommendations] +\begin{enumerate} + \item \textbf{[Recommendation 1]:} [Action-oriented recommendation with expected outcome] + \item \textbf{[Recommendation 2]:} [Action-oriented recommendation with expected outcome] + \item \textbf{[Recommendation 3]:} [Action-oriented recommendation with expected outcome] + \item \textbf{[Recommendation 4]:} [Action-oriented recommendation with expected outcome] + \item \textbf{[Recommendation 5]:} [Action-oriented recommendation with expected outcome] +\end{enumerate} +\end{recommendationbox} + +% ============================================================================ +% CHAPTER 1: MARKET OVERVIEW & DEFINITION (4-5 pages) +% ============================================================================ +\chapter{Market Overview \& Definition} + +\section{Market Definition} + +[Provide a clear, comprehensive definition of the market being analyzed. Include: +- What products/services are included +- What is explicitly excluded +- How this market relates to adjacent markets +- Industry classification codes if applicable (NAICS, SIC)] + +\begin{calloutbox}[Market Definition] +The \reporttitle{} market encompasses [comprehensive definition]. This includes [included elements] and excludes [excluded elements]. +\end{calloutbox} + +\subsection{Scope and Boundaries} + +\paragraph{Geographic Scope} +[Define the geographic boundaries of the analysis - global, regional, or specific countries.] + +\paragraph{Product/Service Scope} +[Define what products and services are included in the market definition.] + +\paragraph{Time Horizon} +[Specify the historical period analyzed and the forecast period.] + +\subsection{Market Classification} + +[Provide detailed market classification and taxonomy, including: +- Market segments +- Sub-segments +- Categories] + +\section{Industry Ecosystem} + +% VISUAL: Industry ecosystem diagram +% python skills/scientific-schematics/scripts/generate_schematic.py "Industry ecosystem diagram showing value chain from [Raw Materials/Inputs] on left through [Manufacturing/Processing] through [Distribution] to [End Users] on right. Include key players at each stage. Use blue boxes connected by arrows" -o figures/industry_ecosystem.png --doc-type report + +[Describe the industry ecosystem and value chain, including: +- Key stakeholders and their roles +- Relationships between stakeholders +- Value creation at each stage +- Information and money flows] + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.95\textwidth]{../figures/industry_ecosystem.png} +\caption{Industry Ecosystem and Value Chain} +\label{fig:ecosystem} +\end{figure} + +\subsection{Key Stakeholders} + +[Describe each category of stakeholder:] + +\paragraph{Suppliers/Vendors} +[Description of upstream suppliers and their role.] + +\paragraph{Manufacturers/Service Providers} +[Description of core market participants.] + +\paragraph{Distributors/Channels} +[Description of distribution and go-to-market channels.] + +\paragraph{End Users/Customers} +[Description of customer segments and their needs.] + +\paragraph{Regulators and Industry Bodies} +[Description of regulatory environment and industry associations.] + +\section{Market Structure} + +% VISUAL: Market structure diagram +% python skills/scientific-schematics/scripts/generate_schematic.py "Market structure diagram showing industry layers: [Core Market] in center, surrounded by [Adjacent Markets], with [Enabling Technologies] as foundation and [Regulatory Framework] as overlay. Use concentric rectangles" -o figures/market_structure.png --doc-type report + +[Describe the structure of the market:] + +\subsection{Market Concentration} + +[Analyze market concentration using metrics like: +- Herfindahl-Hirschman Index (HHI) +- CR4/CR8 concentration ratios +- Market fragmentation assessment] + +\subsection{Industry Lifecycle Stage} + +[Identify where the market is in its lifecycle: +- Introduction +- Growth +- Maturity +- Decline] + +\section{Historical Context} + +[Provide historical background on the market: +- When did the market emerge? +- Key milestones in market development +- Major industry shifts and disruptions +- How has the market evolved over time?] + +% ============================================================================ +% CHAPTER 2: MARKET SIZE & GROWTH ANALYSIS (6-8 pages) +% ============================================================================ +\chapter{Market Size \& Growth Analysis} + +\section{Total Addressable Market (TAM)} + +The Total Addressable Market represents the total revenue opportunity available if 100\% market share was achieved. Based on comprehensive analysis from multiple research sources: + +% VISUAL: Market growth trajectory +% python skills/scientific-schematics/scripts/generate_schematic.py "Bar chart showing market growth from 2020 to 2034. Historical bars (2020-2024) in dark blue, projected bars (2025-2034) in light blue. Y-axis in billions USD, X-axis showing years. Include CAGR label. Title: [MARKET] Market Growth Trajectory" -o figures/market_growth_trajectory.png --doc-type report + +\begin{table}[htbp] +\centering +\caption{Global \reporttitle{} Market Projections (2024-2034)} +\begin{tabular}{@{}lrrr@{}} +\toprule +\textbf{Year} & \textbf{Market Size (USD)} & \textbf{YoY Growth} & \textbf{CAGR} \\ +\midrule +2024 & \$X.XX B & -- & -- \\ +\rowcolor{tablealt} 2025 & \$X.XX B & XX.X\% & XX.X\% \\ +2026 & \$X.XX B & XX.X\% & XX.X\% \\ +\rowcolor{tablealt} 2027 & \$X.XX B & XX.X\% & XX.X\% \\ +2028 & \$X.XX B & XX.X\% & XX.X\% \\ +\rowcolor{tablealt} 2029 & \$X.XX B & XX.X\% & XX.X\% \\ +2030 & \$X.XX B & XX.X\% & XX.X\% \\ +\rowcolor{tablealt} 2031 & \$X.XX B & XX.X\% & XX.X\% \\ +2032 & \$X.XX B & XX.X\% & XX.X\% \\ +\rowcolor{tablealt} 2033 & \$X.XX B & XX.X\% & XX.X\% \\ +2034 & \$X.XX B & XX.X\% & XX.X\% \\ +\bottomrule +\end{tabular} +\label{tab:tam_projections} +\end{table} + +\textbf{Source:} [Primary research source, year] + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/market_growth_trajectory.png} +\caption{Market Growth Trajectory (2020-2034)} +\label{fig:market_growth} +\end{figure} + +\subsection{Historical Growth Analysis} + +[Analyze historical market performance over the past 5-10 years: +- Historical CAGR +- Key growth periods and drivers +- Impact of major events (recessions, disruptions, etc.) +- Comparison to overall economic growth] + +\subsection{Growth Projections Methodology} + +[Explain the methodology behind growth projections: +- Key assumptions +- Data sources +- Modeling approach +- Confidence intervals] + +\section{Serviceable Addressable Market (SAM)} + +The Serviceable Addressable Market represents the portion of TAM that can be served given current product offerings, geographic presence, and regulatory constraints. + +% VISUAL: TAM/SAM/SOM diagram +% python skills/scientific-schematics/scripts/generate_schematic.py "TAM SAM SOM concentric circle diagram. Outer circle: TAM $XXB (Total Addressable Market). Middle circle: SAM $XXB (Serviceable Addressable Market). Inner circle: SOM $XXB (Serviceable Obtainable Market). Labels with arrows pointing to each. Professional blue color scheme" -o figures/tam_sam_som.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.8\textwidth]{../figures/tam_sam_som.png} +\caption{TAM/SAM/SOM Market Opportunity Breakdown} +\label{fig:tam_sam_som} +\end{figure} + +\begin{table}[htbp] +\centering +\caption{Market Segments Within SAM (2024 vs 2034)} +\begin{tabular}{@{}lrrrr@{}} +\toprule +\textbf{Segment} & \textbf{2024 Value} & \textbf{2034 Value} & \textbf{CAGR} & \textbf{Share} \\ +\midrule +Segment A & \$X.XX B & \$XX.XX B & XX.X\% & XX\% \\ +\rowcolor{tablealt} Segment B & \$X.XX B & \$XX.XX B & XX.X\% & XX\% \\ +Segment C & \$X.XX B & \$XX.XX B & XX.X\% & XX\% \\ +\rowcolor{tablealt} Segment D & \$X.XX B & \$XX.XX B & XX.X\% & XX\% \\ +Segment E & \$X.XX B & \$XX.XX B & XX.X\% & XX\% \\ +\midrule +\textbf{Total SAM} & \textbf{\$X.XX B} & \textbf{\$XX.XX B} & \textbf{XX.X\%} & \textbf{100\%} \\ +\bottomrule +\end{tabular} +\label{tab:sam_segments} +\end{table} + +\section{Serviceable Obtainable Market (SOM)} + +The Serviceable Obtainable Market represents the realistic market share capture based on competitive dynamics, go-to-market capabilities, and strategic positioning. + +\begin{keyinsightbox}[SOM Projections (2034)] +\textbf{Conservative Scenario (XX\% Market Share):} \marketsize{X.X billion} +\begin{itemize} + \item Assumes competitive market with multiple major players + \item Typical XX-XX month enterprise sales cycles + \item Focus on [specific segments] +\end{itemize} + +\textbf{Base Case Scenario (XX\% Market Share):} \marketsize{X.X billion} +\begin{itemize} + \item Captures first-mover advantages in key segments + \item Strong product-market fit + \item Established partnership ecosystem +\end{itemize} + +\textbf{Optimistic Scenario (XX\% Market Share):} \marketsize{X.X billion} +\begin{itemize} + \item Market leadership position + \item Platform effects and network advantages + \item Proprietary advantages and moats +\end{itemize} +\end{keyinsightbox} + +\section{Regional Market Analysis} + +% VISUAL: Regional breakdown +% python skills/scientific-schematics/scripts/generate_schematic.py "Pie chart or treemap showing regional market breakdown. North America XX%, Europe XX%, Asia-Pacific XX%, Latin America XX%, Middle East & Africa XX%. Use distinct colors for each region. Include both percentage and dollar values" -o figures/regional_breakdown.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.8\textwidth]{../figures/regional_breakdown.png} +\caption{Market Size by Region (2024)} +\label{fig:regional_breakdown} +\end{figure} + +\begin{table}[htbp] +\centering +\caption{Regional Market Size and Growth} +\begin{tabular}{@{}lrrrr@{}} +\toprule +\textbf{Region} & \textbf{2024 Size} & \textbf{Share} & \textbf{CAGR} & \textbf{2034 Size} \\ +\midrule +North America & \$X.XX B & XX.X\% & XX.X\% & \$XX.XX B \\ +\rowcolor{tablealt} Europe & \$X.XX B & XX.X\% & XX.X\% & \$XX.XX B \\ +Asia-Pacific & \$X.XX B & XX.X\% & XX.X\% & \$XX.XX B \\ +\rowcolor{tablealt} Latin America & \$X.XX B & XX.X\% & XX.X\% & \$XX.XX B \\ +Middle East \& Africa & \$X.XX B & XX.X\% & XX.X\% & \$XX.XX B \\ +\midrule +\textbf{Global Total} & \textbf{\$X.XX B} & \textbf{100\%} & \textbf{XX.X\%} & \textbf{\$XX.XX B} \\ +\bottomrule +\end{tabular} +\label{tab:regional_market} +\end{table} + +\subsection{North America} +[Detailed analysis of North American market including US and Canada specifics.] + +\subsection{Europe} +[Detailed analysis of European market including key country breakdowns.] + +\subsection{Asia-Pacific} +[Detailed analysis of APAC market with focus on China, Japan, India, and emerging markets.] + +\subsection{Rest of World} +[Analysis of Latin America, Middle East, and Africa markets.] + +\section{Segment Analysis} + +% VISUAL: Segment growth comparison +% python skills/scientific-schematics/scripts/generate_schematic.py "Horizontal bar chart comparing segment growth rates. Segments listed on Y-axis, CAGR percentage on X-axis. Bars colored from green (highest growth) to blue (lowest growth). Include data labels on each bar" -o figures/segment_growth.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/segment_growth.png} +\caption{Segment Growth Rate Comparison (CAGR 2024-2034)} +\label{fig:segment_growth} +\end{figure} + +[Provide detailed analysis of each market segment including: +- Current size and market share +- Growth trajectory +- Key drivers for each segment +- Competitive dynamics within segment] + +% ============================================================================ +% CHAPTER 3: INDUSTRY DRIVERS & TRENDS (5-6 pages) +% ============================================================================ +\chapter{Industry Drivers \& Trends} + +\section{Primary Growth Drivers} + +[Identify and analyze the key factors driving market growth:] + +% VISUAL: Driver impact matrix +% python skills/scientific-schematics/scripts/generate_schematic.py "2x2 matrix showing market drivers. X-axis: Impact (Low to High). Y-axis: Likelihood (Low to High). Plot 8-10 drivers as circles. Upper-right quadrant labeled 'Critical Drivers', lower-right 'Watch Carefully', upper-left 'Monitor', lower-left 'Lower Priority'. Professional blue and green colors" -o figures/driver_impact_matrix.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.85\textwidth]{../figures/driver_impact_matrix.png} +\caption{Market Driver Impact Assessment Matrix} +\label{fig:driver_matrix} +\end{figure} + +\subsection{Driver 1: [Name]} +[Detailed analysis of this driver including: +- How it affects the market +- Quantified impact +- Timeline for impact +- Supporting evidence and data] + +\subsection{Driver 2: [Name]} +[Detailed analysis] + +\subsection{Driver 3: [Name]} +[Detailed analysis] + +\subsection{Driver 4: [Name]} +[Detailed analysis] + +\subsection{Driver 5: [Name]} +[Detailed analysis] + +\section{PESTLE Analysis} + +% VISUAL: PESTLE diagram +% python skills/scientific-schematics/scripts/generate_schematic.py "PESTLE analysis diagram with center hexagon labeled 'Market' surrounded by 6 hexagons: Political (red), Economic (blue), Social (green), Technological (orange), Legal (purple), Environmental (teal). Each outer hexagon contains 2-3 bullet points of key factors" -o figures/pestle_analysis.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/pestle_analysis.png} +\caption{PESTLE Analysis Framework} +\label{fig:pestle} +\end{figure} + +\subsection{Political Factors} +[Analysis of political factors affecting the market: +- Government policies +- Political stability +- Trade policies +- Tax regulations] + +\subsection{Economic Factors} +[Analysis of economic factors: +- Economic growth +- Interest rates +- Inflation +- Exchange rates +- Consumer spending] + +\subsection{Social Factors} +[Analysis of social factors: +- Demographics +- Cultural trends +- Consumer attitudes +- Workforce trends] + +\subsection{Technological Factors} +[Analysis of technological factors: +- Technology adoption +- R\&D activity +- Automation +- Digital transformation] + +\subsection{Legal Factors} +[Analysis of legal factors: +- Industry regulations +- Compliance requirements +- Intellectual property +- Employment laws] + +\subsection{Environmental Factors} +[Analysis of environmental factors: +- Sustainability requirements +- Environmental regulations +- Climate impact +- Resource availability] + +\section{Emerging Trends} + +% VISUAL: Trends timeline +% python skills/scientific-schematics/scripts/generate_schematic.py "Horizontal timeline showing emerging trends from 2024 to 2030. Mark 6-8 trends at different points on timeline with icons and labels. Use different colors for Technology trends (blue), Market trends (green), and Regulatory trends (orange). Include brief descriptions below each trend" -o figures/trends_timeline.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=\textwidth]{../figures/trends_timeline.png} +\caption{Emerging Industry Trends Timeline} +\label{fig:trends} +\end{figure} + +[Identify and analyze emerging trends that will shape the market:] + +\subsection{Trend 1: [Name]} +[Detailed trend analysis] + +\subsection{Trend 2: [Name]} +[Detailed trend analysis] + +\subsection{Trend 3: [Name]} +[Detailed trend analysis] + +\section{Growth Inhibitors} + +[Identify factors that could slow market growth: +- Market barriers +- Resource constraints +- Adoption challenges +- Competitive pressures] + +% ============================================================================ +% CHAPTER 4: COMPETITIVE LANDSCAPE (6-8 pages) +% ============================================================================ +\chapter{Competitive Landscape} + +\section{Market Structure Analysis} + +[Analyze the competitive structure of the market:] + +\subsection{Market Concentration} + +[Provide market concentration analysis: +- Number of competitors +- Market share distribution +- Concentration metrics (HHI, CR4)] + +\section{Porter's Five Forces Analysis} + +% VISUAL: Porter's Five Forces +% python skills/scientific-schematics/scripts/generate_schematic.py "Porter's Five Forces diagram. Center box labeled 'Competitive Rivalry: [HIGH/MEDIUM/LOW]'. Four boxes around it connected by arrows: 'Threat of New Entrants: [RATING]' (top), 'Bargaining Power of Suppliers: [RATING]' (left), 'Bargaining Power of Buyers: [RATING]' (right), 'Threat of Substitutes: [RATING]' (bottom). Color code by rating: High=red, Medium=orange, Low=green" -o figures/porters_five_forces.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.85\textwidth]{../figures/porters_five_forces.png} +\caption{Porter's Five Forces Analysis} +\label{fig:porter} +\end{figure} + +\begin{porterbox}[Porter's Five Forces Summary] +\begin{tabular}{@{}ll@{}} +\textbf{Force} & \textbf{Rating} \\ +\midrule +Threat of New Entrants & \riskmedium{} \\ +Bargaining Power of Suppliers & \risklow{} \\ +Bargaining Power of Buyers & \riskhigh{} \\ +Threat of Substitutes & \risklow{} \\ +Competitive Rivalry & \riskhigh{} \\ +\end{tabular} +\end{porterbox} + +\subsection{Threat of New Entrants} +[Detailed analysis of barriers to entry and threat level] + +\subsection{Bargaining Power of Suppliers} +[Detailed analysis of supplier power dynamics] + +\subsection{Bargaining Power of Buyers} +[Detailed analysis of buyer power dynamics] + +\subsection{Threat of Substitutes} +[Detailed analysis of substitute products/services] + +\subsection{Competitive Rivalry} +[Detailed analysis of competitive intensity] + +\section{Market Share Analysis} + +% VISUAL: Market share chart +% python skills/scientific-schematics/scripts/generate_schematic.py "Pie chart showing market share of top 10 companies. Company A XX%, Company B XX%, Company C XX%, [etc.], Others XX%. Use distinct colors for each company. Include legend with company names and percentages" -o figures/market_share.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.8\textwidth]{../figures/market_share.png} +\caption{Market Share by Company (2024)} +\label{fig:market_share} +\end{figure} + +\begin{table}[htbp] +\centering +\caption{Top 10 Companies by Market Share} +\begin{tabular}{@{}clrrr@{}} +\toprule +\textbf{Rank} & \textbf{Company} & \textbf{Revenue} & \textbf{Share} & \textbf{YoY Growth} \\ +\midrule +1 & Company A & \$X.XX B & XX.X\% & \trendup{} XX\% \\ +\rowcolor{tablealt} 2 & Company B & \$X.XX B & XX.X\% & \trendup{} XX\% \\ +3 & Company C & \$X.XX B & XX.X\% & \trendflat{} XX\% \\ +\rowcolor{tablealt} 4 & Company D & \$X.XX B & XX.X\% & \trendup{} XX\% \\ +5 & Company E & \$X.XX B & XX.X\% & \trenddown{} XX\% \\ +\rowcolor{tablealt} 6 & Company F & \$X.XX B & XX.X\% & \trendup{} XX\% \\ +7 & Company G & \$X.XX B & XX.X\% & \trendup{} XX\% \\ +\rowcolor{tablealt} 8 & Company H & \$X.XX B & XX.X\% & \trendflat{} XX\% \\ +9 & Company I & \$X.XX B & XX.X\% & \trendup{} XX\% \\ +\rowcolor{tablealt} 10 & Company J & \$X.XX B & XX.X\% & \trenddown{} XX\% \\ +\midrule +& Others & \$X.XX B & XX.X\% & -- \\ +\bottomrule +\end{tabular} +\label{tab:market_share} +\end{table} + +\section{Competitive Positioning} + +% VISUAL: Competitive positioning matrix +% python skills/scientific-schematics/scripts/generate_schematic.py "2x2 competitive positioning matrix. X-axis: 'Market Focus' from Niche (left) to Broad (right). Y-axis: 'Solution Approach' from Product (bottom) to Platform (top). Plot 8-10 companies as labeled circles of varying sizes (representing market share). Include quadrant labels" -o figures/competitive_positioning.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/competitive_positioning.png} +\caption{Competitive Positioning Matrix} +\label{fig:competitive_positioning} +\end{figure} + +[Analyze how competitors are positioned in the market based on key dimensions:] + +\subsection{Strategic Groups} + +% VISUAL: Strategic group map +% python skills/scientific-schematics/scripts/generate_schematic.py "Strategic group map with circles representing different strategic groups. X-axis: Geographic Scope (Regional to Global). Y-axis: Product Breadth (Narrow to Broad). Each circle contains multiple company names and is sized by collective market share. 4-5 distinct groups" -o figures/strategic_groups.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.85\textwidth]{../figures/strategic_groups.png} +\caption{Strategic Group Mapping} +\label{fig:strategic_groups} +\end{figure} + +[Identify and describe strategic groups within the competitive landscape] + +\section{Competitive Dynamics} + +[Analyze competitive behaviors and dynamics: +- Recent M\&A activity +- Partnership announcements +- Product launches +- Pricing trends +- Geographic expansion] + +\section{Barriers to Entry} + +[Analyze barriers that protect incumbents and challenge new entrants: +- Capital requirements +- Regulatory barriers +- Technology barriers +- Brand and reputation +- Distribution access +- Economies of scale] + +% ============================================================================ +% CHAPTER 5: CUSTOMER ANALYSIS & SEGMENTATION (4-5 pages) +% ============================================================================ +\chapter{Customer Analysis \& Segmentation} + +\section{Customer Segmentation} + +% VISUAL: Customer segmentation +% python skills/scientific-schematics/scripts/generate_schematic.py "Treemap or pie chart showing customer segments. Segment A XX% (large enterprises), Segment B XX% (mid-market), Segment C XX% (SMB), Segment D XX% (other). Size represents market share. Use distinct colors" -o figures/customer_segments.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.85\textwidth]{../figures/customer_segments.png} +\caption{Customer Segmentation by Market Share} +\label{fig:customer_segments} +\end{figure} + +\begin{table}[htbp] +\centering +\caption{Customer Segment Analysis} +\begin{tabular}{@{}lrrrr@{}} +\toprule +\textbf{Segment} & \textbf{Size} & \textbf{Growth} & \textbf{Avg. Deal} & \textbf{CAC} \\ +\midrule +Large Enterprise & \$X.XX B & XX\% & \$XXX K & \$XX K \\ +\rowcolor{tablealt} Mid-Market & \$X.XX B & XX\% & \$XX K & \$X K \\ +SMB & \$X.XX B & XX\% & \$X K & \$X K \\ +\rowcolor{tablealt} Consumer & \$X.XX B & XX\% & \$XXX & \$XX \\ +\bottomrule +\end{tabular} +\label{tab:customer_segments} +\end{table} + +\subsection{Segment A: [Large Enterprise]} +[Detailed segment analysis including: +- Segment characteristics +- Buying behavior +- Key needs and pain points +- Decision-making process +- Willingness to pay] + +\subsection{Segment B: [Mid-Market]} +[Detailed segment analysis] + +\subsection{Segment C: [SMB]} +[Detailed segment analysis] + +\subsection{Segment D: [Other]} +[Detailed segment analysis] + +\section{Segment Attractiveness} + +% VISUAL: Segment attractiveness matrix +% python skills/scientific-schematics/scripts/generate_schematic.py "2x2 segment attractiveness matrix. X-axis: Segment Size (Small to Large). Y-axis: Growth Rate (Low to High). Plot customer segments as circles. Upper-right: 'Priority', Upper-left: 'Invest to Grow', Lower-right: 'Harvest', Lower-left: 'Deprioritize'. Include segment labels" -o figures/segment_attractiveness.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.85\textwidth]{../figures/segment_attractiveness.png} +\caption{Customer Segment Attractiveness Matrix} +\label{fig:segment_attractiveness} +\end{figure} + +[Analyze which segments are most attractive for investment and focus] + +\section{Customer Needs Analysis} + +[Identify and prioritize customer needs by segment: +- Functional needs +- Emotional needs +- Social needs +- Pain points +- Unmet needs] + +\section{Buying Behavior} + +[Analyze how customers buy in this market: +- Purchase triggers +- Decision-making process +- Key influencers +- Evaluation criteria +- Purchase channels] + +% VISUAL: Customer journey +% python skills/scientific-schematics/scripts/generate_schematic.py "Customer journey diagram showing 5 stages: Awareness → Consideration → Decision → Implementation → Advocacy. Each stage shows key activities, pain points, and touchpoints. Use horizontal flow with icons for each stage" -o figures/customer_journey.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=\textwidth]{../figures/customer_journey.png} +\caption{Customer Journey Map} +\label{fig:customer_journey} +\end{figure} + +% ============================================================================ +% CHAPTER 6: TECHNOLOGY & INNOVATION LANDSCAPE (4-5 pages) +% ============================================================================ +\chapter{Technology \& Innovation Landscape} + +\section{Current Technology Stack} + +[Describe the technology infrastructure and stack commonly used in the market] + +\section{Technology Roadmap} + +% VISUAL: Technology roadmap +% python skills/scientific-schematics/scripts/generate_schematic.py "Technology roadmap timeline from 2024 to 2030. Show 3 parallel tracks: Core Technology (blue), Emerging Technology (green), and Enabling Technology (orange). Mark key milestones and technology introductions on each track. Use horizontal timeline format" -o figures/technology_roadmap.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=\textwidth]{../figures/technology_roadmap.png} +\caption{Technology Evolution Roadmap (2024-2030)} +\label{fig:tech_roadmap} +\end{figure} + +[Describe how technology is expected to evolve: +- Near-term (1-2 years) +- Medium-term (3-5 years) +- Long-term (5-10 years)] + +\section{Emerging Technologies} + +[Analyze emerging technologies that could impact the market: +- Technology description +- Current maturity level +- Expected timeline to mainstream adoption +- Potential impact on market] + +\subsection{Technology 1: [Name]} +[Detailed analysis] + +\subsection{Technology 2: [Name]} +[Detailed analysis] + +\subsection{Technology 3: [Name]} +[Detailed analysis] + +\section{Innovation Trends} + +% VISUAL: Innovation matrix +% python skills/scientific-schematics/scripts/generate_schematic.py "Innovation adoption curve or hype cycle diagram showing where key technologies sit. From left to right: Innovation Trigger, Peak of Inflated Expectations, Trough of Disillusionment, Slope of Enlightenment, Plateau of Productivity. Plot 6-8 technologies at different points" -o figures/innovation_curve.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=\textwidth]{../figures/innovation_curve.png} +\caption{Technology Adoption Curve / Hype Cycle} +\label{fig:innovation} +\end{figure} + +[Analyze innovation trends and R\&D activity: +- R\&D investment levels +- Patent filing trends +- Startup activity +- Corporate innovation initiatives] + +\section{Technology Adoption Barriers} + +[Identify barriers to technology adoption: +- Technical complexity +- Integration challenges +- Cost barriers +- Skills gaps +- Security/privacy concerns] + +% ============================================================================ +% CHAPTER 7: REGULATORY & POLICY ENVIRONMENT (3-4 pages) +% ============================================================================ +\chapter{Regulatory \& Policy Environment} + +\section{Current Regulatory Framework} + +[Describe the current regulatory landscape: +- Key regulations +- Regulatory bodies +- Compliance requirements +- Enforcement mechanisms] + +\section{Regulatory Timeline} + +% VISUAL: Regulatory timeline +% python skills/scientific-schematics/scripts/generate_schematic.py "Regulatory timeline from 2020 to 2028. Show key regulatory milestones as markers on horizontal timeline. Past events in dark blue, future/upcoming in light blue. Include regulation names and effective dates. Mark current date with vertical line" -o figures/regulatory_timeline.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=\textwidth]{../figures/regulatory_timeline.png} +\caption{Regulatory Development Timeline} +\label{fig:regulatory_timeline} +\end{figure} + +[Chronological analysis of regulatory developments] + +\section{Regulatory Impact Analysis} + +[Analyze how regulations impact the market: +- Compliance costs +- Market access implications +- Competitive implications +- Product/service requirements] + +\section{Policy Trends} + +[Identify policy trends that could affect the market: +- Government priorities +- Funding initiatives +- Trade policies +- Environmental policies] + +\section{Regional Regulatory Differences} + +[Compare regulatory environments across regions: +- North America +- Europe +- Asia-Pacific +- Other regions] + +% ============================================================================ +% CHAPTER 8: RISK ANALYSIS (3-4 pages) +% ============================================================================ +\chapter{Risk Analysis} + +\section{Risk Overview} + +[Provide overview of key risks facing market participants] + +\section{Risk Assessment} + +% VISUAL: Risk heatmap +% python skills/scientific-schematics/scripts/generate_schematic.py "Risk heatmap matrix. X-axis: Impact (Low to Critical). Y-axis: Probability (Unlikely to Very Likely). Plot 10-12 risks as labeled circles. Color code: Green (low risk), Yellow (medium), Orange (high), Red (critical). Include risk labels" -o figures/risk_heatmap.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/risk_heatmap.png} +\caption{Risk Assessment Heatmap} +\label{fig:risk_heatmap} +\end{figure} + +\begin{table}[htbp] +\centering +\caption{Risk Register Summary} +\begin{tabular}{@{}llccl@{}} +\toprule +\textbf{Risk} & \textbf{Category} & \textbf{Probability} & \textbf{Impact} & \textbf{Rating} \\ +\midrule +Risk 1 & Market & High & High & \riskhigh{} \\ +\rowcolor{tablealt} Risk 2 & Regulatory & Medium & High & \riskhigh{} \\ +Risk 3 & Technology & Medium & Medium & \riskmedium{} \\ +\rowcolor{tablealt} Risk 4 & Competitive & High & Medium & \riskmedium{} \\ +Risk 5 & Operational & Low & High & \riskmedium{} \\ +\rowcolor{tablealt} Risk 6 & Financial & Low & Medium & \risklow{} \\ +\bottomrule +\end{tabular} +\label{tab:risk_register} +\end{table} + +\subsection{Market Risks} +[Detailed analysis of market-related risks] + +\subsection{Competitive Risks} +[Detailed analysis of competitive risks] + +\subsection{Regulatory Risks} +[Detailed analysis of regulatory risks] + +\subsection{Technology Risks} +[Detailed analysis of technology risks] + +\subsection{Operational Risks} +[Detailed analysis of operational risks] + +\subsection{Financial Risks} +[Detailed analysis of financial risks] + +\section{Risk Mitigation Strategies} + +% VISUAL: Risk mitigation matrix +% python skills/scientific-schematics/scripts/generate_schematic.py "Risk mitigation matrix showing risks in left column and corresponding mitigation strategies in right column. Connect risks to mitigations with arrows. Color code by risk severity. Include both prevention and response strategies" -o figures/risk_mitigation.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/risk_mitigation.png} +\caption{Risk Mitigation Framework} +\label{fig:risk_mitigation} +\end{figure} + +[Describe strategies to mitigate identified risks] + +\begin{riskbox}[Risk Mitigation Summary] +\begin{enumerate} + \item \textbf{[Risk 1]:} [Mitigation strategy] + \item \textbf{[Risk 2]:} [Mitigation strategy] + \item \textbf{[Risk 3]:} [Mitigation strategy] + \item \textbf{[Risk 4]:} [Mitigation strategy] +\end{enumerate} +\end{riskbox} + +% ============================================================================ +% CHAPTER 9: STRATEGIC OPPORTUNITIES & RECOMMENDATIONS (4-5 pages) +% ============================================================================ +\chapter{Strategic Opportunities \& Recommendations} + +\section{Opportunity Analysis} + +% VISUAL: Opportunity matrix +% python skills/scientific-schematics/scripts/generate_schematic.py "2x2 opportunity matrix. X-axis: Market Attractiveness (Low to High). Y-axis: Ability to Win (Low to High). Plot 6-8 opportunities as labeled circles of varying sizes. Upper-right: 'Pursue Aggressively', Upper-left: 'Selective Investment', Lower-right: 'Build Capabilities', Lower-left: 'Avoid'" -o figures/opportunity_matrix.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/opportunity_matrix.png} +\caption{Strategic Opportunity Assessment Matrix} +\label{fig:opportunity_matrix} +\end{figure} + +[Identify and analyze strategic opportunities in the market] + +\subsection{Opportunity 1: [Name]} +\begin{opportunitybox}[Opportunity: [Name]] +\textbf{Description:} [Brief description] + +\textbf{Market Size:} \marketsize{X.X billion} + +\textbf{Growth Rate:} \growthrate{XX.X} + +\textbf{Strategic Fit:} \rating{4} + +\textbf{Investment Required:} \$XX million +\end{opportunitybox} + +[Detailed analysis of the opportunity] + +\subsection{Opportunity 2: [Name]} +[Detailed analysis] + +\subsection{Opportunity 3: [Name]} +[Detailed analysis] + +\section{Strategic Options Analysis} + +[Analyze different strategic approaches: +- Build (organic development) +- Buy (M\&A) +- Partner (strategic alliances) +- Ignore (not pursue)] + +\section{Prioritized Recommendations} + +% VISUAL: Recommendation priority matrix +% python skills/scientific-schematics/scripts/generate_schematic.py "Priority matrix showing recommendations. X-axis: Effort/Investment (Low to High). Y-axis: Impact/Value (Low to High). Plot 6-8 recommendations. Upper-left: 'Quick Wins', Upper-right: 'Major Projects', Lower-left: 'Fill-ins', Lower-right: 'Thankless Tasks'. Label each recommendation" -o figures/recommendation_priority.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.85\textwidth]{../figures/recommendation_priority.png} +\caption{Recommendation Priority Framework} +\label{fig:recommendations} +\end{figure} + +\begin{recommendationbox}[Strategic Recommendations] +\textbf{Tier 1: Immediate Priority} +\begin{enumerate} + \item \textbf{[Recommendation 1]:} [Detailed action with expected outcome, timeline, and investment] + \item \textbf{[Recommendation 2]:} [Detailed action] +\end{enumerate} + +\textbf{Tier 2: Near-Term (6-12 months)} +\begin{enumerate}[start=3] + \item \textbf{[Recommendation 3]:} [Detailed action] + \item \textbf{[Recommendation 4]:} [Detailed action] +\end{enumerate} + +\textbf{Tier 3: Medium-Term (1-2 years)} +\begin{enumerate}[start=5] + \item \textbf{[Recommendation 5]:} [Detailed action] + \item \textbf{[Recommendation 6]:} [Detailed action] +\end{enumerate} +\end{recommendationbox} + +\section{Success Factors} + +[Identify critical success factors for implementing recommendations: +- Organizational capabilities +- Resource requirements +- Timing considerations +- External dependencies] + +% ============================================================================ +% CHAPTER 10: IMPLEMENTATION ROADMAP (3-4 pages) +% ============================================================================ +\chapter{Implementation Roadmap} + +\section{Implementation Overview} + +[Provide overview of implementation approach and timeline] + +\section{Phased Implementation Plan} + +% VISUAL: Implementation timeline +% python skills/scientific-schematics/scripts/generate_schematic.py "Gantt chart style implementation timeline showing 4 phases over 24 months. Phase 1: Foundation (months 1-6), Phase 2: Build (months 4-12), Phase 3: Scale (months 10-18), Phase 4: Optimize (months 16-24). Show overlapping phases with key milestones marked. Use different colors for each phase" -o figures/implementation_timeline.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=\textwidth]{../figures/implementation_timeline.png} +\caption{Implementation Roadmap Timeline} +\label{fig:implementation} +\end{figure} + +\subsection{Phase 1: Foundation (Months 1-6)} +[Detailed activities and deliverables for Phase 1] + +\subsection{Phase 2: Build (Months 4-12)} +[Detailed activities and deliverables for Phase 2] + +\subsection{Phase 3: Scale (Months 10-18)} +[Detailed activities and deliverables for Phase 3] + +\subsection{Phase 4: Optimize (Months 16-24)} +[Detailed activities and deliverables for Phase 4] + +\section{Key Milestones} + +% VISUAL: Milestone tracker +% python skills/scientific-schematics/scripts/generate_schematic.py "Milestone tracker showing 8-10 key milestones on a horizontal timeline. Each milestone has a date, name, and status indicator (completed=green checkmark, in-progress=yellow circle, upcoming=gray circle). Group by phase" -o figures/milestone_tracker.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=\textwidth]{../figures/milestone_tracker.png} +\caption{Key Implementation Milestones} +\label{fig:milestones} +\end{figure} + +\begin{table}[htbp] +\centering +\caption{Implementation Milestones} +\begin{tabular}{@{}llll@{}} +\toprule +\textbf{Milestone} & \textbf{Target Date} & \textbf{Owner} & \textbf{Success Criteria} \\ +\midrule +Milestone 1 & Month 3 & [Owner] & [Criteria] \\ +\rowcolor{tablealt} Milestone 2 & Month 6 & [Owner] & [Criteria] \\ +Milestone 3 & Month 9 & [Owner] & [Criteria] \\ +\rowcolor{tablealt} Milestone 4 & Month 12 & [Owner] & [Criteria] \\ +Milestone 5 & Month 18 & [Owner] & [Criteria] \\ +\rowcolor{tablealt} Milestone 6 & Month 24 & [Owner] & [Criteria] \\ +\bottomrule +\end{tabular} +\label{tab:milestones} +\end{table} + +\section{Resource Requirements} + +[Detail resource requirements for implementation: +- Team structure +- Budget allocation +- Technology requirements +- External support needs] + +\section{Governance Structure} + +[Define governance for implementation: +- Decision-making authority +- Reporting structure +- Review cadence +- Escalation paths] + +% ============================================================================ +% CHAPTER 11: INVESTMENT THESIS & FINANCIAL PROJECTIONS (3-4 pages) +% ============================================================================ +\chapter{Investment Thesis \& Financial Projections} + +\section{Investment Summary} + +[Summarize the investment opportunity: +- Key value drivers +- Expected returns +- Investment timeline +- Risk-adjusted assessment] + +\begin{executivesummarybox}[Investment Thesis] +The \reporttitle{} market presents a compelling investment opportunity characterized by: + +\begin{itemize} + \item \textbf{Large Market:} \marketsize{XX billion} TAM growing at \growthrate{XX.X} CAGR + \item \textbf{Favorable Dynamics:} [Key market dynamics] + \item \textbf{Strong Drivers:} [Key growth drivers] + \item \textbf{Manageable Risks:} [Risk summary] + \item \textbf{Clear Path to Value:} [Value creation summary] +\end{itemize} +\end{executivesummarybox} + +\section{Financial Projections} + +% VISUAL: Financial projections +% python skills/scientific-schematics/scripts/generate_schematic.py "Financial projections chart showing revenue growth over 5 years. Bar chart for revenue with line overlay for growth rate. Three scenarios: Conservative (gray bars), Base Case (blue bars), Optimistic (green bars). Y-axis dual: Revenue ($M) and Growth (%). Include data labels" -o figures/financial_projections.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.9\textwidth]{../figures/financial_projections.png} +\caption{Financial Projections (5-Year)} +\label{fig:financials} +\end{figure} + +\begin{table}[htbp] +\centering +\caption{Financial Projections Summary} +\begin{tabular}{@{}lrrrrr@{}} +\toprule +\textbf{Metric} & \textbf{Year 1} & \textbf{Year 2} & \textbf{Year 3} & \textbf{Year 4} & \textbf{Year 5} \\ +\midrule +Revenue (\$M) & \$XX & \$XX & \$XX & \$XX & \$XX \\ +\rowcolor{tablealt} Growth Rate & XX\% & XX\% & XX\% & XX\% & XX\% \\ +Gross Margin & XX\% & XX\% & XX\% & XX\% & XX\% \\ +\rowcolor{tablealt} EBITDA (\$M) & \$XX & \$XX & \$XX & \$XX & \$XX \\ +EBITDA Margin & XX\% & XX\% & XX\% & XX\% & XX\% \\ +\bottomrule +\end{tabular} +\label{tab:financials} +\end{table} + +\section{Scenario Analysis} + +% VISUAL: Scenario comparison +% python skills/scientific-schematics/scripts/generate_schematic.py "Scenario comparison chart showing 3 scenarios (Conservative, Base, Optimistic) across key metrics. Use grouped bar chart with metrics on X-axis (Revenue Y5, EBITDA Y5, Market Share, ROI) and values on Y-axis. Color code by scenario" -o figures/scenario_analysis.png --doc-type report + +\begin{figure}[htbp] +\centering +% \includegraphics[width=0.85\textwidth]{../figures/scenario_analysis.png} +\caption{Scenario Analysis Comparison} +\label{fig:scenarios} +\end{figure} + +\subsection{Conservative Scenario} +[Detailed assumptions and outcomes for conservative case] + +\subsection{Base Case Scenario} +[Detailed assumptions and outcomes for base case] + +\subsection{Optimistic Scenario} +[Detailed assumptions and outcomes for optimistic case] + +\section{Key Assumptions} + +[Document key assumptions underlying financial projections: +- Market growth assumptions +- Pricing assumptions +- Cost assumptions +- Competitive assumptions +- Timing assumptions] + +\section{Sensitivity Analysis} + +[Analyze sensitivity of projections to key variables: +- Revenue sensitivity to market growth +- Margin sensitivity to pricing +- Returns sensitivity to timing] + +\section{Return Expectations} + +[Summarize expected returns: +- ROI projections +- Payback period +- IRR estimates +- Multiple analysis] + +% ============================================================================ +% APPENDICES +% ============================================================================ +\appendix + +% ============================================================================ +% APPENDIX A: METHODOLOGY & DATA SOURCES +% ============================================================================ +\chapter{Methodology \& Data Sources} + +\section{Research Methodology} + +[Describe the research methodology used: +- Primary research methods +- Secondary research sources +- Data collection timeframe +- Analytical frameworks applied] + +\section{Data Sources} + +[List all data sources used in the report: +- Market research reports +- Industry databases +- Government statistics +- Company reports +- Expert interviews +- Academic publications] + +\section{Limitations} + +[Acknowledge limitations of the analysis: +- Data availability constraints +- Methodological limitations +- Forecast uncertainty +- Scope limitations] + +% ============================================================================ +% APPENDIX B: DETAILED MARKET DATA +% ============================================================================ +\chapter{Detailed Market Data} + +\section{Historical Market Data} + +[Provide detailed historical market data tables] + +\section{Regional Data Breakdown} + +[Provide detailed regional market data] + +\section{Segment Data Details} + +[Provide detailed segment-level data] + +\section{Competitive Data} + +[Provide detailed competitive data tables] + +% ============================================================================ +% APPENDIX C: COMPANY PROFILES +% ============================================================================ +\chapter{Company Profiles} + +\section{Company A} + +\begin{calloutbox}[Company Profile: Company A] +\textbf{Headquarters:} [Location] + +\textbf{Revenue:} \$X.X billion (FY2024) + +\textbf{Employees:} X,XXX + +\textbf{Market Position:} [Position description] + +\textbf{Key Products/Services:} [List] + +\textbf{Recent Developments:} [Summary] +\end{calloutbox} + +[Brief narrative description of company strategy and positioning] + +\section{Company B} +[Company profile] + +\section{Company C} +[Company profile] + +\section{Company D} +[Company profile] + +\section{Company E} +[Company profile] + +% ============================================================================ +% REFERENCES +% ============================================================================ +\newpage +\bibliographystyle{plainnat} +\bibliography{../references/references} + +% Alternative: Manual bibliography if not using BibTeX +% \begin{thebibliography}{99} +% +% \bibitem{source1} +% Author1, A.B. (2024). +% Title of report or article. +% \textit{Publisher/Source}. +% URL +% +% \bibitem{source2} +% [Continue with all references...] +% +% \end{thebibliography} + +% ============================================================================ +% END OF DOCUMENT +% ============================================================================ +\end{document} diff --git a/skills/market-research-reports/assets/market_research.sty b/skills/market-research-reports/assets/market_research.sty new file mode 100755 index 0000000..d383941 --- /dev/null +++ b/skills/market-research-reports/assets/market_research.sty @@ -0,0 +1,564 @@ +% market_research.sty - Professional Market Research Report Styling +% For use with XeLaTeX or LuaLaTeX +% Style inspired by top consulting firms (McKinsey, BCG, Gartner) + +\ProvidesPackage{market_research}[2024/01/01 Market Research Report Style] + +% ============================================================================ +% REQUIRED PACKAGES +% ============================================================================ + +% Page layout and geometry +\RequirePackage[margin=1in]{geometry} +\RequirePackage{setspace} + +% Typography +\RequirePackage[utf8]{inputenc} +\RequirePackage[T1]{fontenc} +\RequirePackage{helvet} +\renewcommand{\familydefault}{\sfdefault} + +% Colors and graphics +\RequirePackage{xcolor} +\RequirePackage{graphicx} +\RequirePackage{tikz} + +% Tables +\RequirePackage{longtable} +\RequirePackage{booktabs} +\RequirePackage{multirow} +\RequirePackage{array} +\RequirePackage{colortbl} + +% Lists and formatting +\RequirePackage{enumitem} +\RequirePackage{parskip} + +% Boxes and callouts +\RequirePackage[most]{tcolorbox} + +% Headers and footers +\RequirePackage{fancyhdr} +\RequirePackage{titlesec} + +% Hyperlinks and references +\RequirePackage{hyperref} +\RequirePackage[numbers,sort&compress]{natbib} + +% Math (for financial projections) +\RequirePackage{amsmath} + +% Captions +\RequirePackage{caption} +\RequirePackage{subcaption} + +% ============================================================================ +% COLOR DEFINITIONS +% ============================================================================ + +% Primary colors (professional blue palette) +\definecolor{primaryblue}{RGB}{0, 51, 102} % Deep navy blue +\definecolor{secondaryblue}{RGB}{51, 102, 153} % Medium blue +\definecolor{lightblue}{RGB}{173, 216, 230} % Light blue for backgrounds +\definecolor{accentblue}{RGB}{0, 120, 215} % Bright accent blue + +% Secondary colors (complementary) +\definecolor{accentgreen}{RGB}{0, 128, 96} % Teal green +\definecolor{lightgreen}{RGB}{200, 230, 201} % Light green background +\definecolor{darkgreen}{RGB}{27, 94, 32} % Dark green + +% Warning and risk colors +\definecolor{warningorange}{RGB}{255, 140, 0} % Orange for warnings +\definecolor{lightorange}{RGB}{255, 243, 224} % Light orange background +\definecolor{alertred}{RGB}{198, 40, 40} % Red for critical items +\definecolor{lightred}{RGB}{255, 235, 238} % Light red background + +% Recommendation and action colors +\definecolor{recommendpurple}{RGB}{103, 58, 183} % Purple for recommendations +\definecolor{lightpurple}{RGB}{237, 231, 246} % Light purple background + +% Neutral colors +\definecolor{darkgray}{RGB}{66, 66, 66} % Dark gray for text +\definecolor{mediumgray}{RGB}{117, 117, 117} % Medium gray +\definecolor{lightgray}{RGB}{240, 240, 240} % Light gray backgrounds +\definecolor{tablegray}{RGB}{250, 250, 250} % Table row alternating +\definecolor{tablealt}{RGB}{245, 247, 250} % Alternating table row + +% Chart colors (colorblind-friendly palette) +\definecolor{chart1}{RGB}{0, 114, 178} % Blue +\definecolor{chart2}{RGB}{230, 159, 0} % Orange +\definecolor{chart3}{RGB}{0, 158, 115} % Green +\definecolor{chart4}{RGB}{204, 121, 167} % Pink +\definecolor{chart5}{RGB}{86, 180, 233} % Sky blue +\definecolor{chart6}{RGB}{213, 94, 0} % Vermillion +\definecolor{chart7}{RGB}{240, 228, 66} % Yellow + +% ============================================================================ +% HYPERLINK CONFIGURATION +% ============================================================================ + +\hypersetup{ + colorlinks=true, + linkcolor=primaryblue, + filecolor=primaryblue, + urlcolor=accentblue, + citecolor=secondaryblue, + pdftitle={Market Research Report}, + pdfauthor={Market Intelligence}, + pdfsubject={Market Analysis}, +} + +% ============================================================================ +% CHAPTER AND SECTION FORMATTING +% ============================================================================ + +% Chapter formatting - large number with colored title +\titleformat{\chapter}[display] +{\normalfont\huge\bfseries\color{primaryblue}} +{\chaptertitlename\ \thechapter}{20pt}{\Huge} +\titlespacing*{\chapter}{0pt}{-20pt}{40pt} + +% Section formatting +\titleformat{\section} +{\normalfont\Large\bfseries\color{primaryblue}} +{\thesection}{1em}{} +\titlespacing*{\section}{0pt}{3.5ex plus 1ex minus .2ex}{2.3ex plus .2ex} + +% Subsection formatting +\titleformat{\subsection} +{\normalfont\large\bfseries\color{secondaryblue}} +{\thesubsection}{1em}{} + +% Subsubsection formatting +\titleformat{\subsubsection} +{\normalfont\normalsize\bfseries\color{darkgray}} +{\thesubsubsection}{1em}{} + +% Paragraph formatting +\titleformat{\paragraph}[runin] +{\normalfont\normalsize\bfseries\color{darkgray}} +{\theparagraph}{1em}{} + +% ============================================================================ +% HEADER AND FOOTER CONFIGURATION +% ============================================================================ + +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small\textit{\leftmark}} +\fancyhead[R]{\small\textit{Market Research Report}} +\fancyfoot[C]{\thepage} +\renewcommand{\headrulewidth}{0.4pt} +\renewcommand{\footrulewidth}{0.4pt} +\renewcommand{\headrule}{\hbox to\headwidth{\color{primaryblue}\leaders\hrule height \headrulewidth\hfill}} +\renewcommand{\footrule}{\hbox to\headwidth{\color{lightgray}\leaders\hrule height \footrulewidth\hfill}} + +% Plain page style for chapter pages +\fancypagestyle{plain}{ + \fancyhf{} + \fancyfoot[C]{\thepage} + \renewcommand{\headrulewidth}{0pt} + \renewcommand{\footrulewidth}{0.4pt} +} + +% ============================================================================ +% BOX ENVIRONMENTS +% ============================================================================ + +% Key Insight Box (Blue) - For major findings and insights +\newtcolorbox{keyinsightbox}[1][Key Insight]{ + colback=lightblue!30, + colframe=primaryblue, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=primaryblue, + boxrule=1pt, + arc=3pt, + left=10pt, + right=10pt, + top=8pt, + bottom=8pt, + before skip=12pt, + after skip=12pt, +} + +% Market Data Box (Green) - For market statistics and data highlights +\newtcolorbox{marketdatabox}[1][Market Data]{ + colback=lightgreen!50, + colframe=accentgreen, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=accentgreen, + boxrule=1pt, + arc=3pt, + left=10pt, + right=10pt, + top=8pt, + bottom=8pt, + before skip=12pt, + after skip=12pt, +} + +% Risk Box (Orange/Warning) - For risk factors and warnings +\newtcolorbox{riskbox}[1][Risk Factor]{ + colback=lightorange, + colframe=warningorange, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=warningorange, + boxrule=1pt, + arc=3pt, + left=10pt, + right=10pt, + top=8pt, + bottom=8pt, + before skip=12pt, + after skip=12pt, +} + +% Critical Risk Box (Red) - For critical/high-severity risks +\newtcolorbox{criticalriskbox}[1][Critical Risk]{ + colback=lightred, + colframe=alertred, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=alertred, + boxrule=1pt, + arc=3pt, + left=10pt, + right=10pt, + top=8pt, + bottom=8pt, + before skip=12pt, + after skip=12pt, +} + +% Recommendation Box (Purple) - For strategic recommendations +\newtcolorbox{recommendationbox}[1][Strategic Recommendation]{ + colback=lightpurple, + colframe=recommendpurple, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=recommendpurple, + boxrule=1pt, + arc=3pt, + left=10pt, + right=10pt, + top=8pt, + bottom=8pt, + before skip=12pt, + after skip=12pt, +} + +% Callout Box (Gray) - For definitions, notes, supplementary info +\newtcolorbox{calloutbox}[1][Note]{ + colback=lightgray, + colframe=mediumgray, + fonttitle=\bfseries\color{darkgray}, + title=#1, + coltitle=darkgray, + colbacktitle=lightgray, + boxrule=0.5pt, + arc=3pt, + left=10pt, + right=10pt, + top=8pt, + bottom=8pt, + before skip=12pt, + after skip=12pt, +} + +% Executive Summary Box (Special styling) +\newtcolorbox{executivesummarybox}[1][Executive Summary]{ + enhanced, + colback=white, + colframe=primaryblue, + fonttitle=\Large\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=primaryblue, + boxrule=2pt, + arc=5pt, + left=15pt, + right=15pt, + top=12pt, + bottom=12pt, + before skip=15pt, + after skip=15pt, + shadow={2mm}{-2mm}{0mm}{black!20}, +} + +% Opportunity Box (Teal) - For opportunities and positive findings +\newtcolorbox{opportunitybox}[1][Opportunity]{ + colback=lightblue!20, + colframe=accentblue, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=accentblue, + boxrule=1pt, + arc=3pt, + left=10pt, + right=10pt, + top=8pt, + bottom=8pt, + before skip=12pt, + after skip=12pt, +} + +% ============================================================================ +% PULL QUOTE ENVIRONMENT +% ============================================================================ + +\newtcolorbox{pullquote}{ + enhanced, + colback=lightgray, + colframe=lightgray, + boxrule=0pt, + borderline west={4pt}{0pt}{primaryblue}, + arc=0pt, + left=15pt, + right=15pt, + top=10pt, + bottom=10pt, + before skip=15pt, + after skip=15pt, + fontupper=\large\itshape\color{darkgray}, +} + +% ============================================================================ +% STATISTIC HIGHLIGHT +% ============================================================================ + +\newcommand{\statbox}[2]{% + \begin{tcolorbox}[ + colback=primaryblue, + colframe=primaryblue, + coltext=white, + arc=5pt, + boxrule=0pt, + width=0.3\textwidth, + halign=center, + valign=center, + before skip=10pt, + after skip=10pt, + ] + {\Huge\bfseries #1}\\[5pt] + {\small #2} + \end{tcolorbox} +} + +% ============================================================================ +% TABLE STYLING +% ============================================================================ + +% Alternating row colors command +\newcommand{\tablerowcolor}{\rowcolor{tablealt}} + +% Table header styling +\newcommand{\tableheader}[1]{\textbf{\color{white}#1}} +\newcommand{\tableheaderrow}{\rowcolor{primaryblue}} + +% Professional table environment +\newenvironment{markettable}[2][htbp]{% + \begin{table}[#1] + \centering + \caption{#2} + \small +}{% + \end{table} +} + +% ============================================================================ +% FIGURE STYLING +% ============================================================================ + +% Caption formatting +\captionsetup{ + font=small, + labelfont={bf,color=primaryblue}, + textfont={color=darkgray}, + justification=centering, + margin=20pt, +} + +% Figure with source attribution +\newcommand{\figuresource}[1]{% + \par\vspace{-8pt} + {\small\textit{Source: #1}} +} + +% ============================================================================ +% LIST STYLING +% ============================================================================ + +% Bullet list styling +\setlist[itemize]{ + leftmargin=*, + label=\textcolor{primaryblue}{\textbullet}, + topsep=5pt, + itemsep=3pt, +} + +% Numbered list styling +\setlist[enumerate]{ + leftmargin=*, + label=\textcolor{primaryblue}{\arabic*.}, + topsep=5pt, + itemsep=3pt, +} + +% ============================================================================ +% CUSTOM COMMANDS +% ============================================================================ + +% Highlight important text +\newcommand{\highlight}[1]{\textbf{\textcolor{primaryblue}{#1}}} + +% Market size with formatting +\newcommand{\marketsize}[1]{\textbf{\textcolor{accentgreen}{\$#1}}} + +% Growth rate with formatting +\newcommand{\growthrate}[1]{\textbf{\textcolor{chart3}{#1\%}}} + +% Risk indicator +\newcommand{\riskhigh}{\textbf{\textcolor{alertred}{HIGH}}} +\newcommand{\riskmedium}{\textbf{\textcolor{warningorange}{MEDIUM}}} +\newcommand{\risklow}{\textbf{\textcolor{accentgreen}{LOW}}} + +% Rating stars (1-5) +\newcommand{\rating}[1]{% + \foreach \i in {1,...,5}{% + \ifnum\i>#1 + \textcolor{lightgray}{$\star$}% + \else + \textcolor{warningorange}{$\star$}% + \fi + }% +} + +% Trend indicators +\newcommand{\trendup}{\textcolor{accentgreen}{$\blacktriangle$}} +\newcommand{\trenddown}{\textcolor{alertred}{$\blacktriangledown$}} +\newcommand{\trendflat}{\textcolor{mediumgray}{$\rightarrow$}} + +% ============================================================================ +% TITLE PAGE COMMAND +% ============================================================================ + +\newcommand{\makemarketreporttitle}[5]{% + % #1 = Report Title + % #2 = Subtitle + % #3 = Hero Image Path + % #4 = Date + % #5 = Prepared By + \begin{titlepage} + \centering + \vspace*{1cm} + + {\Huge\bfseries\color{primaryblue} #1\\[0.5cm]} + {\LARGE\bfseries #2\\[2cm]} + + \ifx& + % No image provided + \vspace{4cm} + \else + \includegraphics[width=\textwidth]{#3}\\[2cm] + \fi + + {\Large\bfseries Market Research Report\\[3cm]} + + {\large + \textbf{Date:} #4\\[0.3cm] + \textbf{Prepared By:} #5\\[0.3cm] + \textbf{Classification:} Confidential + } + + \vfill + + {\footnotesize + \textit{This report contains market intelligence and strategic analysis. All data sources are cited and independently verifiable.} + } + + \end{titlepage} +} + +% ============================================================================ +% APPENDIX SECTION COMMAND +% ============================================================================ + +\newcommand{\appendixsection}[1]{% + \section*{#1} + \addcontentsline{toc}{section}{#1} +} + +% ============================================================================ +% FRAMEWORK BOXES +% ============================================================================ + +% SWOT Analysis Box +\newtcolorbox{swotbox}[1][SWOT Analysis]{ + enhanced, + colback=white, + colframe=secondaryblue, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=secondaryblue, + boxrule=1.5pt, + arc=5pt, + left=10pt, + right=10pt, + top=10pt, + bottom=10pt, + before skip=15pt, + after skip=15pt, +} + +% Porter's Five Forces Box +\newtcolorbox{porterbox}[1][Porter's Five Forces]{ + enhanced, + colback=white, + colframe=primaryblue, + fonttitle=\bfseries\color{white}, + title=#1, + coltitle=white, + colbacktitle=primaryblue, + boxrule=1.5pt, + arc=5pt, + left=10pt, + right=10pt, + top=10pt, + bottom=10pt, + before skip=15pt, + after skip=15pt, +} + +% ============================================================================ +% PAGE LAYOUT ADJUSTMENTS +% ============================================================================ + +% Spacing +\setstretch{1.15} +\setlength{\parskip}{0.5em} + +% Prevent orphans and widows +\clubpenalty=10000 +\widowpenalty=10000 + +% Float placement +\renewcommand{\topfraction}{0.9} +\renewcommand{\bottomfraction}{0.8} +\renewcommand{\textfraction}{0.07} +\renewcommand{\floatpagefraction}{0.7} + +% ============================================================================ +% END OF STYLE FILE +% ============================================================================ + +\endinput diff --git a/skills/market-research-reports/references/data_analysis_patterns.md b/skills/market-research-reports/references/data_analysis_patterns.md new file mode 100755 index 0000000..f94863b --- /dev/null +++ b/skills/market-research-reports/references/data_analysis_patterns.md @@ -0,0 +1,548 @@ +# Data Analysis Patterns for Market Research + +Templates and frameworks for conducting rigorous market analysis. + +--- + +## Market Sizing Frameworks + +### TAM/SAM/SOM Analysis + +**Total Addressable Market (TAM)** represents the total revenue opportunity if 100% market share was achieved. + +#### Top-Down Approach +``` +TAM = Total Industry Revenue (from market research reports) + +Example: +- Global AI Software Market (2024): $184 billion +- Source: Gartner, IDC, or similar +``` + +#### Bottom-Up Approach +``` +TAM = Number of Potential Customers × Average Revenue per Customer + +Example: +- Number of enterprises globally: 400 million +- Target segment (large enterprises): 50,000 +- Average annual spend on solution: $500,000 +- TAM = 50,000 × $500,000 = $25 billion +``` + +**Serviceable Addressable Market (SAM)** represents the portion of TAM that can be served given product/service capabilities. + +``` +SAM = TAM × Applicable Segment % + +Example: +- TAM: $25 billion +- Geographic constraint (North America only): 40% +- Product fit (enterprise only): 60% +- SAM = $25B × 40% × 60% = $6 billion +``` + +**Serviceable Obtainable Market (SOM)** represents realistic market share capture. + +``` +SOM = SAM × Achievable Market Share % + +Example: +- SAM: $6 billion +- Conservative market share (5%): $300 million +- Base case market share (10%): $600 million +- Optimistic market share (15%): $900 million +``` + +### Growth Rate Calculation + +#### CAGR (Compound Annual Growth Rate) +``` +CAGR = (End Value / Start Value)^(1/n) - 1 + +Where n = number of years + +Example: +- 2020 market size: $10 billion +- 2024 market size: $18 billion +- n = 4 years +- CAGR = (18/10)^(1/4) - 1 = 15.8% +``` + +#### Year-over-Year Growth +``` +YoY Growth = (Current Year - Previous Year) / Previous Year × 100 + +Example: +- 2023: $15 billion +- 2024: $18 billion +- YoY Growth = (18-15)/15 × 100 = 20% +``` + +--- + +## Porter's Five Forces Analysis + +### Framework Template + +For each force, assess: **HIGH**, **MEDIUM**, or **LOW** + +#### 1. Threat of New Entrants + +**Factors to evaluate:** +| Factor | Assessment | Notes | +|--------|------------|-------| +| Capital requirements | High/Med/Low | $ required to enter | +| Economies of scale | Strong/Moderate/Weak | Incumbent advantages | +| Brand loyalty | High/Med/Low | Customer switching cost | +| Access to distribution | Easy/Moderate/Difficult | Channel availability | +| Regulatory barriers | High/Med/Low | Licensing, certifications | +| Proprietary technology | Critical/Important/Minor | IP and know-how | +| Expected retaliation | Aggressive/Moderate/Passive | Incumbent response | + +**Overall Assessment:** [HIGH/MEDIUM/LOW] + +**Key Insights:** [Summary of implications] + +#### 2. Bargaining Power of Suppliers + +**Factors to evaluate:** +| Factor | Assessment | Notes | +|--------|------------|-------| +| Supplier concentration | High/Med/Low | Number of suppliers | +| Switching costs | High/Med/Low | Cost to change suppliers | +| Supplier differentiation | High/Med/Low | Uniqueness of inputs | +| Forward integration threat | High/Med/Low | Can suppliers compete? | +| Importance to supplier | Critical/Important/Minor | Your share of their revenue | +| Substitute inputs | Many/Some/Few | Alternatives available | + +**Overall Assessment:** [HIGH/MEDIUM/LOW] + +#### 3. Bargaining Power of Buyers + +**Factors to evaluate:** +| Factor | Assessment | Notes | +|--------|------------|-------| +| Buyer concentration | High/Med/Low | Few large vs. many small | +| Purchase volume | Large/Medium/Small | Relative importance | +| Switching costs | Low/Med/High | Cost to change vendors | +| Price sensitivity | High/Med/Low | Focus on price vs. value | +| Backward integration threat | High/Med/Low | Can buyers self-supply? | +| Information availability | Full/Partial/Limited | Market transparency | + +**Overall Assessment:** [HIGH/MEDIUM/LOW] + +#### 4. Threat of Substitutes + +**Factors to evaluate:** +| Factor | Assessment | Notes | +|--------|------------|-------| +| Substitute availability | Many/Some/Few | Number of alternatives | +| Price-performance ratio | Better/Same/Worse | Value comparison | +| Switching costs | Low/Med/High | Friction to substitute | +| Buyer propensity to switch | High/Med/Low | Willingness to change | +| Perceived differentiation | Low/Med/High | Unique value | + +**Overall Assessment:** [HIGH/MEDIUM/LOW] + +#### 5. Competitive Rivalry + +**Factors to evaluate:** +| Factor | Assessment | Notes | +|--------|------------|-------| +| Number of competitors | Many/Several/Few | Market fragmentation | +| Industry growth | Slow/Moderate/Fast | Growth rate impact | +| Fixed costs | High/Med/Low | Pressure to fill capacity | +| Product differentiation | Low/Med/High | Commoditization level | +| Exit barriers | High/Med/Low | Difficulty leaving market | +| Strategic stakes | High/Med/Low | Importance to competitors | + +**Overall Assessment:** [HIGH/MEDIUM/LOW] + +### Five Forces Summary Table + +| Force | Rating | Key Drivers | Implications | +|-------|--------|-------------|--------------| +| New Entrants | [H/M/L] | [Top factors] | [Strategic impact] | +| Supplier Power | [H/M/L] | [Top factors] | [Strategic impact] | +| Buyer Power | [H/M/L] | [Top factors] | [Strategic impact] | +| Substitutes | [H/M/L] | [Top factors] | [Strategic impact] | +| Rivalry | [H/M/L] | [Top factors] | [Strategic impact] | + +**Overall Industry Attractiveness:** [ATTRACTIVE / MODERATE / UNATTRACTIVE] + +--- + +## PESTLE Analysis + +### Framework Template + +#### Political Factors + +| Factor | Current State | Trend | Impact | Time Horizon | +|--------|---------------|-------|--------|--------------| +| Government stability | | ↑ ↓ → | H/M/L | Short/Med/Long | +| Trade policies | | ↑ ↓ → | H/M/L | | +| Tax regulations | | ↑ ↓ → | H/M/L | | +| Government support | | ↑ ↓ → | H/M/L | | +| Political relations | | ↑ ↓ → | H/M/L | | + +**Key Political Implications:** [Summary] + +#### Economic Factors + +| Factor | Current State | Trend | Impact | Time Horizon | +|--------|---------------|-------|--------|--------------| +| GDP growth | X.X% | ↑ ↓ → | H/M/L | | +| Interest rates | X.X% | ↑ ↓ → | H/M/L | | +| Inflation | X.X% | ↑ ↓ → | H/M/L | | +| Exchange rates | | ↑ ↓ → | H/M/L | | +| Consumer spending | | ↑ ↓ → | H/M/L | | +| Unemployment | X.X% | ↑ ↓ → | H/M/L | | + +**Key Economic Implications:** [Summary] + +#### Social Factors + +| Factor | Current State | Trend | Impact | Time Horizon | +|--------|---------------|-------|--------|--------------| +| Demographics | | ↑ ↓ → | H/M/L | | +| Cultural attitudes | | ↑ ↓ → | H/M/L | | +| Consumer behavior | | ↑ ↓ → | H/M/L | | +| Education levels | | ↑ ↓ → | H/M/L | | +| Health consciousness | | ↑ ↓ → | H/M/L | | +| Work-life balance | | ↑ ↓ → | H/M/L | | + +**Key Social Implications:** [Summary] + +#### Technological Factors + +| Factor | Current State | Trend | Impact | Time Horizon | +|--------|---------------|-------|--------|--------------| +| R&D activity | | ↑ ↓ → | H/M/L | | +| Technology adoption | | ↑ ↓ → | H/M/L | | +| Automation | | ↑ ↓ → | H/M/L | | +| Digital infrastructure | | ↑ ↓ → | H/M/L | | +| Innovation rate | | ↑ ↓ → | H/M/L | | +| Disruptive tech | | ↑ ↓ → | H/M/L | | + +**Key Technological Implications:** [Summary] + +#### Legal Factors + +| Factor | Current State | Trend | Impact | Time Horizon | +|--------|---------------|-------|--------|--------------| +| Industry regulations | | ↑ ↓ → | H/M/L | | +| Data protection | | ↑ ↓ → | H/M/L | | +| Employment law | | ↑ ↓ → | H/M/L | | +| Consumer protection | | ↑ ↓ → | H/M/L | | +| IP rights | | ↑ ↓ → | H/M/L | | +| Antitrust | | ↑ ↓ → | H/M/L | | + +**Key Legal Implications:** [Summary] + +#### Environmental Factors + +| Factor | Current State | Trend | Impact | Time Horizon | +|--------|---------------|-------|--------|--------------| +| Climate change | | ↑ ↓ → | H/M/L | | +| Sustainability reqs | | ↑ ↓ → | H/M/L | | +| Resource availability | | ↑ ↓ → | H/M/L | | +| Waste management | | ↑ ↓ → | H/M/L | | +| Carbon regulations | | ↑ ↓ → | H/M/L | | +| Environmental awareness | | ↑ ↓ → | H/M/L | | + +**Key Environmental Implications:** [Summary] + +--- + +## SWOT Analysis + +### Framework Template + +#### Strengths (Internal, Positive) +| Strength | Evidence | Strategic Value | +|----------|----------|-----------------| +| [Strength 1] | [Data/proof] | High/Med/Low | +| [Strength 2] | [Data/proof] | High/Med/Low | +| [Strength 3] | [Data/proof] | High/Med/Low | + +**Core Strengths Summary:** [2-3 sentence synthesis] + +#### Weaknesses (Internal, Negative) +| Weakness | Evidence | Severity | +|----------|----------|----------| +| [Weakness 1] | [Data/proof] | Critical/Moderate/Minor | +| [Weakness 2] | [Data/proof] | Critical/Moderate/Minor | +| [Weakness 3] | [Data/proof] | Critical/Moderate/Minor | + +**Key Vulnerabilities Summary:** [2-3 sentence synthesis] + +#### Opportunities (External, Positive) +| Opportunity | Size/Potential | Timeframe | +|-------------|----------------|-----------| +| [Opportunity 1] | $X / High/Med/Low | Short/Med/Long | +| [Opportunity 2] | $X / High/Med/Low | Short/Med/Long | +| [Opportunity 3] | $X / High/Med/Low | Short/Med/Long | + +**Priority Opportunities Summary:** [2-3 sentence synthesis] + +#### Threats (External, Negative) +| Threat | Likelihood | Impact | +|--------|------------|--------| +| [Threat 1] | High/Med/Low | High/Med/Low | +| [Threat 2] | High/Med/Low | High/Med/Low | +| [Threat 3] | High/Med/Low | High/Med/Low | + +**Critical Threats Summary:** [2-3 sentence synthesis] + +### SWOT Strategy Matrix + +| | **Strengths** | **Weaknesses** | +|---|---------------|----------------| +| **Opportunities** | **SO Strategies** (use strengths to capture opportunities) | **WO Strategies** (overcome weaknesses to capture opportunities) | +| **Threats** | **ST Strategies** (use strengths to mitigate threats) | **WT Strategies** (minimize weaknesses and avoid threats) | + +--- + +## BCG Growth-Share Matrix + +### Framework Template + +**Axes:** +- X-axis: Relative Market Share (High → Low, logarithmic scale) +- Y-axis: Market Growth Rate (High → Low, typically 10% as midpoint) + +### Quadrant Definitions + +| Quadrant | Growth | Share | Characteristics | Strategy | +|----------|--------|-------|-----------------|----------| +| **Stars** | High | High | Market leaders in growing markets | Invest to maintain position | +| **Cash Cows** | Low | High | Market leaders in mature markets | Harvest for cash flow | +| **Question Marks** | High | Low | Small share in growing markets | Invest selectively or divest | +| **Dogs** | Low | Low | Small share in mature markets | Divest or minimize investment | + +### Product/Business Unit Analysis + +| Product/BU | Market Growth | Relative Share | Quadrant | Recommended Strategy | +|------------|---------------|----------------|----------|---------------------| +| [Product A] | X.X% | X.X | Star/Cow/QM/Dog | [Strategy] | +| [Product B] | X.X% | X.X | Star/Cow/QM/Dog | [Strategy] | +| [Product C] | X.X% | X.X | Star/Cow/QM/Dog | [Strategy] | + +### Portfolio Balance Assessment + +| Quadrant | Number of Products | Revenue % | Investment Priority | +|----------|-------------------|-----------|---------------------| +| Stars | X | X% | High | +| Cash Cows | X | X% | Maintain | +| Question Marks | X | X% | Selective | +| Dogs | X | X% | Low/Divest | + +--- + +## Value Chain Analysis + +### Framework Template + +#### Primary Activities + +| Activity | Description | Value Created | Cost | Competitive Position | +|----------|-------------|---------------|------|---------------------| +| **Inbound Logistics** | Receiving, storing, inventory | | $X | Strong/Average/Weak | +| **Operations** | Manufacturing, assembly | | $X | Strong/Average/Weak | +| **Outbound Logistics** | Distribution, delivery | | $X | Strong/Average/Weak | +| **Marketing & Sales** | Promotion, sales force | | $X | Strong/Average/Weak | +| **Service** | Installation, support, repair | | $X | Strong/Average/Weak | + +#### Support Activities + +| Activity | Description | Value Created | Cost | Competitive Position | +|----------|-------------|---------------|------|---------------------| +| **Infrastructure** | Management, finance, legal | | $X | Strong/Average/Weak | +| **HR Management** | Recruiting, training, comp | | $X | Strong/Average/Weak | +| **Technology Dev** | R&D, process improvement | | $X | Strong/Average/Weak | +| **Procurement** | Purchasing, supplier mgmt | | $X | Strong/Average/Weak | + +### Value Chain Margin Analysis + +``` +Total Revenue: $XXX +- Inbound Logistics: ($XX) +- Operations: ($XX) +- Outbound Logistics: ($XX) +- Marketing & Sales: ($XX) +- Service: ($XX) +- Support Activities: ($XX) += Margin: $XX (X%) +``` + +### Competitive Comparison + +| Activity | Company | Industry Avg | Best-in-Class | Gap | +|----------|---------|--------------|---------------|-----| +| [Activity] | X% | Y% | Z% | +/-X% | + +--- + +## Competitive Positioning Analysis + +### Framework Template + +#### Positioning Dimensions + +Common positioning dimension pairs: +- Price vs. Quality +- Market Focus (Niche vs. Broad) +- Solution Type (Product vs. Platform) +- Geographic Scope (Regional vs. Global) +- Customer Focus (Enterprise vs. SMB vs. Consumer) +- Innovation Level (Leader vs. Follower) + +#### Competitor Mapping + +| Competitor | Dimension 1 Score (1-10) | Dimension 2 Score (1-10) | Market Share | Notes | +|------------|-------------------------|-------------------------|--------------|-------| +| Company A | X | X | X% | [Position description] | +| Company B | X | X | X% | [Position description] | +| Company C | X | X | X% | [Position description] | + +#### Strategic Group Identification + +| Strategic Group | Companies | Characteristics | Market Share | +|-----------------|-----------|-----------------|--------------| +| Group 1: [Name] | A, B, C | [Description] | X% | +| Group 2: [Name] | D, E | [Description] | X% | +| Group 3: [Name] | F, G, H | [Description] | X% | + +--- + +## Risk Assessment Framework + +### Risk Identification + +#### Risk Categories +1. **Market Risks**: Demand changes, price pressure, market shifts +2. **Competitive Risks**: New entrants, competitor moves, disruption +3. **Regulatory Risks**: New regulations, compliance requirements +4. **Technology Risks**: Obsolescence, security, integration +5. **Operational Risks**: Supply chain, quality, capacity +6. **Financial Risks**: Currency, interest rates, credit +7. **Reputational Risks**: Brand damage, social media, ethics + +### Risk Assessment Matrix + +| Risk ID | Risk Description | Category | Probability | Impact | Score | Priority | +|---------|------------------|----------|-------------|--------|-------|----------| +| R1 | [Description] | Market | 1-5 | 1-5 | P×I | H/M/L | +| R2 | [Description] | Competitive | 1-5 | 1-5 | P×I | H/M/L | + +**Scoring Guide:** +- Probability: 1=Very Unlikely, 2=Unlikely, 3=Possible, 4=Likely, 5=Very Likely +- Impact: 1=Minimal, 2=Minor, 3=Moderate, 4=Major, 5=Severe +- Priority: Score 15-25=High, 8-14=Medium, 1-7=Low + +### Risk Mitigation Planning + +| Risk ID | Risk | Mitigation Strategy | Owner | Timeline | Cost | +|---------|------|---------------------|-------|----------|------| +| R1 | [Risk] | [Prevention + Response] | [Name] | [Date] | $X | + +--- + +## Financial Analysis Patterns + +### Revenue Projection Model + +``` +Year N Revenue = Year N-1 Revenue × (1 + Growth Rate) + +Or bottom-up: +Revenue = Customers × Revenue per Customer × Retention Rate + + New Customers × Revenue per Customer × (1 - Churn Rate) +``` + +### Scenario Analysis Template + +| Metric | Conservative | Base Case | Optimistic | +|--------|--------------|-----------|------------| +| Market Growth | X% | Y% | Z% | +| Market Share | X% | Y% | Z% | +| Pricing | $X | $Y | $Z | +| Gross Margin | X% | Y% | Z% | +| **Revenue Y5** | $X | $Y | $Z | +| **EBITDA Y5** | $X | $Y | $Z | + +### Key Financial Metrics + +| Metric | Formula | Target | +|--------|---------|--------| +| Gross Margin | (Revenue - COGS) / Revenue | X% | +| EBITDA Margin | EBITDA / Revenue | X% | +| Customer Acquisition Cost | Sales & Marketing / New Customers | $X | +| Lifetime Value | ARPU × Gross Margin × Lifetime | $X | +| LTV/CAC Ratio | LTV / CAC | >3x | +| Payback Period | CAC / (ARPU × Gross Margin × 12) | <X months | + +--- + +## Data Collection Checklist + +### Market Size Data +- [ ] Current market size (with year and source) +- [ ] Historical market size (5-10 years) +- [ ] Market growth projections (5-10 years) +- [ ] CAGR (historical and projected) +- [ ] Regional breakdown +- [ ] Segment breakdown + +### Competitive Data +- [ ] Market share by company (top 10) +- [ ] Revenue by competitor +- [ ] Growth rates by competitor +- [ ] Strategic moves (M&A, partnerships, launches) +- [ ] Pricing information +- [ ] Product/service offerings + +### Customer Data +- [ ] Customer segments and sizes +- [ ] Segment growth rates +- [ ] Average deal size by segment +- [ ] Customer acquisition cost +- [ ] Customer lifetime value +- [ ] Churn rates + +### Industry Data +- [ ] Key industry trends +- [ ] Regulatory developments +- [ ] Technology trends +- [ ] Economic indicators +- [ ] Demographic trends + +--- + +## Research Sources + +### Primary Research +- Customer interviews +- Expert interviews +- Surveys +- Focus groups + +### Secondary Research +- Market research reports (Gartner, Forrester, IDC, McKinsey) +- Industry associations +- Government statistics +- Company annual reports +- SEC filings (10-K, 10-Q) +- Earnings call transcripts +- Trade publications +- Academic journals +- News articles + +### Data Validation +- Cross-reference multiple sources +- Check date currency (prefer <2 years old) +- Verify methodology +- Note confidence levels +- Document assumptions diff --git a/skills/market-research-reports/references/report_structure_guide.md b/skills/market-research-reports/references/report_structure_guide.md new file mode 100755 index 0000000..8dd4107 --- /dev/null +++ b/skills/market-research-reports/references/report_structure_guide.md @@ -0,0 +1,999 @@ +# Market Research Report Structure Guide + +Detailed guidance for writing each section of a comprehensive market research report. + +--- + +## Front Matter + +### Cover Page + +**Purpose:** Create a strong first impression and communicate report scope. + +**Required Elements:** +- Report title (clear, specific to market being analyzed) +- Subtitle (e.g., "Comprehensive Market Analysis Report") +- Hero visualization (executive summary infographic or market-relevant image) +- Date of publication +- Prepared by / Author organization +- Classification (Confidential, Internal Use, Public) +- Report type identifier + +**Best Practices:** +- Title should include market name and geography if relevant +- Use professional, high-quality hero image +- Keep design clean and uncluttered +- Include version number if applicable + +--- + +### Table of Contents + +**Auto-generated in LaTeX.** Ensure all chapters, sections, and subsections use proper commands for inclusion. + +**Include:** +- List of Figures (all visualizations with page numbers) +- List of Tables (all data tables with page numbers) + +--- + +### Executive Summary (2-3 pages) + +**Purpose:** Provide a standalone summary that allows busy executives to understand key findings without reading the full report. + +**Required Sections:** + +#### Market Snapshot Box +Key metrics displayed prominently: +- Current market size with year +- Projected market size with year +- CAGR (compound annual growth rate) +- Largest segment and market share +- Fastest growing region and growth rate +- Key adoption/penetration metrics + +#### Investment Thesis / Why This Matters +3-5 bullet points explaining: +- Why this market is attractive +- Key factors driving opportunity +- Timing considerations +- Risk-adjusted assessment + +#### Key Findings Summary +Organized by theme: +- Market Dynamics (2-3 points) +- Competitive Landscape (2-3 points) +- Growth Drivers (2-3 points) +- Risk Factors (2-3 points) + +#### Strategic Recommendations +Top 5 actionable recommendations, each with: +- Clear action statement +- Expected outcome +- Priority level (immediate, near-term, medium-term) + +**Visual Requirements:** +- 1-2 visuals maximum +- Executive summary infographic strongly recommended +- Key metrics visualization + +**Writing Guidelines:** +- Write this section LAST after completing all analysis +- Every statement should be supported by analysis in the main report +- Use specific numbers, not vague qualifiers +- Lead with most important findings +- Keep paragraphs short (2-4 sentences) + +--- + +## Core Analysis Chapters + +### Chapter 1: Market Overview & Definition (4-5 pages) + +**Purpose:** Establish clear boundaries and context for the analysis. + +#### Section 1.1: Market Definition + +**Content Requirements:** +- Precise definition of the market being analyzed +- Products/services included in scope +- Products/services explicitly excluded +- Industry classification codes (NAICS, SIC, GICS if applicable) +- Relationship to adjacent markets + +**Writing Approach:** +- Begin with a clear, one-paragraph definition +- Use a callout box to highlight the formal definition +- Explain the rationale for scope decisions +- Address common misconceptions about market boundaries + +#### Section 1.2: Scope and Boundaries + +**Cover:** +- Geographic scope (global, regional, specific countries) +- Product/service scope with specific categories +- Time horizon (historical period + forecast period) +- Customer segments included + +#### Section 1.3: Industry Ecosystem + +**Content Requirements:** +- Value chain from inputs to end users +- Key stakeholders at each stage +- Relationships and dependencies between stakeholders +- Information flows +- Money flows +- Power dynamics + +**Required Visual:** Industry ecosystem/value chain diagram + +**Writing Approach:** +- Start with overview of the ecosystem +- Describe each stakeholder category in detail +- Explain how value is created and captured +- Identify where power concentrates in the value chain + +#### Section 1.4: Market Structure + +**Content Requirements:** +- Market concentration analysis (HHI, CR4, CR8) +- Industry lifecycle stage assessment +- Market fragmentation analysis +- Vertical integration analysis + +#### Section 1.5: Historical Context + +**Content Requirements:** +- When the market emerged +- Key milestones in market development +- Major disruptions and shifts +- Evolution of competitive dynamics +- How customer needs have changed + +**Required Visuals (2 total):** +1. Industry ecosystem diagram +2. Market structure or industry lifecycle diagram + +--- + +### Chapter 2: Market Size & Growth Analysis (6-8 pages) + +**Purpose:** Provide comprehensive quantitative analysis of market opportunity. + +#### Section 2.1: Total Addressable Market (TAM) + +**Content Requirements:** +- Current market size with source and methodology +- Historical market size (5-10 years back) +- Projected market size (5-10 years forward) +- Year-over-year growth rates +- CAGR (historical and projected) + +**Data Table Required:** +Year-by-year market projections table showing: +- Year +- Market size (USD) +- YoY growth rate +- Cumulative CAGR + +**Writing Approach:** +- State the bottom line first (total opportunity) +- Provide historical context +- Explain projection methodology +- State key assumptions +- Cite multiple sources where possible + +#### Section 2.2: Serviceable Addressable Market (SAM) + +**Content Requirements:** +- Definition of SAM for this market +- SAM calculation methodology +- Segment breakdown within SAM +- Growth rates by segment + +**Data Table Required:** +Segment analysis table showing: +- Segment name +- 2024 value +- 2034 value +- CAGR +- Market share + +#### Section 2.3: Serviceable Obtainable Market (SOM) + +**Content Requirements:** +- Realistic market share scenarios +- Conservative estimate with assumptions +- Base case estimate with assumptions +- Optimistic estimate with assumptions +- Factors affecting market share capture + +**Required Visual:** TAM/SAM/SOM concentric circles diagram + +#### Section 2.4: Regional Market Analysis + +**Content Requirements:** +- Market size by region +- Growth rates by region +- Regional market share +- Regional drivers and differences +- Detailed analysis of top 3-4 regions + +**Required Visual:** Regional breakdown chart (pie or treemap) + +**Regions to cover:** +- North America (with US/Canada breakdown if relevant) +- Europe (with key country breakdown) +- Asia-Pacific (with China, Japan, India focus) +- Latin America +- Middle East & Africa + +#### Section 2.5: Segment Analysis + +**Content Requirements:** +- Definition of market segments +- Size of each segment +- Growth rate of each segment +- Key drivers for each segment +- Competitive dynamics by segment + +**Required Visual:** Segment growth comparison chart + +**Required Visuals (4 total):** +1. Market growth trajectory chart +2. TAM/SAM/SOM diagram +3. Regional breakdown chart +4. Segment growth comparison chart + +--- + +### Chapter 3: Industry Drivers & Trends (5-6 pages) + +**Purpose:** Identify and analyze factors driving market growth and evolution. + +#### Section 3.1: Primary Growth Drivers + +**Content Requirements:** +- Identification of 5-10 key growth drivers +- Quantified impact assessment for each +- Timeline for impact +- Evidence and data supporting each driver + +**For each driver, include:** +- Clear description +- Mechanism of impact on market +- Quantified impact estimate +- Timeline (immediate, 1-3 years, 3-5 years) +- Supporting data/evidence + +**Required Visual:** Driver impact matrix (probability vs. impact) + +#### Section 3.2: PESTLE Analysis + +Comprehensive analysis of external factors: + +**Political:** +- Government policies affecting the market +- Political stability in key markets +- Trade policies and tariffs +- Government support programs + +**Economic:** +- Economic growth trends +- Interest rate environment +- Inflation impacts +- Currency effects +- Consumer spending trends + +**Social:** +- Demographic trends +- Cultural shifts +- Consumer behavior changes +- Workforce trends +- Health and wellness trends + +**Technological:** +- Enabling technologies +- Digital transformation +- Automation trends +- Technology adoption curves + +**Legal:** +- Regulatory requirements +- Compliance costs +- Intellectual property considerations +- Employment regulations + +**Environmental:** +- Sustainability requirements +- Environmental regulations +- Climate impacts +- Resource availability + +**Required Visual:** PESTLE analysis diagram + +#### Section 3.3: Emerging Trends + +**Content Requirements:** +- Identification of 5-8 emerging trends +- Timeline for each trend +- Expected impact on market +- Companies/regions leading each trend + +**Required Visual:** Trends timeline or radar chart + +#### Section 3.4: Growth Inhibitors + +**Content Requirements:** +- Factors slowing market growth +- Barriers to adoption +- Resource constraints +- Competitive pressures +- Regulatory hurdles + +**Required Visuals (3 total):** +1. Driver impact matrix +2. PESTLE analysis diagram +3. Trends timeline + +--- + +### Chapter 4: Competitive Landscape (6-8 pages) + +**Purpose:** Provide comprehensive analysis of competitive dynamics. + +#### Section 4.1: Market Structure Analysis + +**Content Requirements:** +- Number of competitors +- Market concentration (HHI index) +- CR4 and CR8 ratios +- Market fragmentation assessment +- Competitive intensity rating + +#### Section 4.2: Porter's Five Forces Analysis + +**For each force, provide:** +- Rating: High / Medium / Low +- Key factors driving the rating +- Supporting evidence +- Strategic implications + +**Forces:** +1. Threat of New Entrants +2. Bargaining Power of Suppliers +3. Bargaining Power of Buyers +4. Threat of Substitutes +5. Competitive Rivalry + +**Required Visual:** Porter's Five Forces diagram + +**Writing Approach:** +- Rate each force clearly +- Provide 3-5 supporting factors per force +- Include data where available +- Discuss strategic implications + +#### Section 4.3: Market Share Analysis + +**Content Requirements:** +- Top 10 companies by market share +- Market share trends (3-5 year view) +- Share gains/losses by company +- Regional market share variations + +**Required Visual:** Market share pie chart or bar chart + +**Data Table Required:** +Top 10 companies showing: +- Rank +- Company name +- Revenue/market size +- Market share % +- YoY growth/trend + +#### Section 4.4: Competitive Positioning + +**Content Requirements:** +- Key dimensions of competition +- Positioning of major players +- Competitive advantages by company +- Strategic moves and announcements + +**Required Visual:** Competitive positioning matrix (2x2) + +**Common positioning dimensions:** +- Market focus (niche vs. broad) +- Solution approach (product vs. platform) +- Price positioning (premium vs. value) +- Geographic focus (regional vs. global) +- Customer focus (enterprise vs. SMB) + +#### Section 4.5: Strategic Groups + +**Content Requirements:** +- Identification of strategic groups +- Companies in each group +- Mobility barriers between groups +- Competitive dynamics within groups + +**Required Visual:** Strategic group map + +#### Section 4.6: Competitive Dynamics + +**Content Requirements:** +- Recent M&A activity +- Partnership announcements +- Product launches +- Pricing trends +- Geographic expansion + +#### Section 4.7: Barriers to Entry + +**Content Requirements:** +- Capital requirements +- Regulatory barriers +- Technology barriers +- Brand and reputation +- Distribution access +- Economies of scale +- Switching costs + +**Required Visuals (4 total):** +1. Porter's Five Forces diagram +2. Market share chart +3. Competitive positioning matrix +4. Strategic group map + +--- + +### Chapter 5: Customer Analysis & Segmentation (4-5 pages) + +**Purpose:** Understand customer needs, behaviors, and segment attractiveness. + +#### Section 5.1: Customer Segmentation + +**Content Requirements:** +- Definition of customer segments +- Segment sizes and market share +- Segment characteristics +- Segment growth rates + +**Required Visual:** Customer segmentation breakdown + +**Common segmentation approaches:** +- By company size (Enterprise, Mid-market, SMB, Consumer) +- By industry vertical +- By geography +- By buying behavior +- By needs/use cases + +#### Section 5.2: Segment Attractiveness Analysis + +**Content Requirements:** +- Attractiveness criteria +- Segment scoring/ranking +- Investment implications +- Prioritization recommendations + +**Required Visual:** Segment attractiveness matrix + +**Attractiveness factors:** +- Segment size +- Growth rate +- Profitability +- Competitive intensity +- Accessibility +- Strategic fit + +#### Section 5.3: Customer Needs Analysis + +**For each segment, identify:** +- Functional needs (what the product must do) +- Emotional needs (how it makes them feel) +- Social needs (how it affects their relationships) +- Key pain points +- Unmet needs + +#### Section 5.4: Buying Behavior + +**Content Requirements:** +- Purchase triggers +- Decision-making process +- Key decision makers and influencers +- Evaluation criteria +- Purchase channels +- Buying cycle length +- Price sensitivity + +#### Section 5.5: Customer Journey + +**Required Visual:** Customer journey map + +**Journey stages to cover:** +1. Awareness +2. Consideration +3. Decision +4. Implementation/Onboarding +5. Usage +6. Advocacy/Renewal + +**Required Visuals (3 total):** +1. Customer segmentation breakdown +2. Segment attractiveness matrix +3. Customer journey map + +--- + +### Chapter 6: Technology & Innovation Landscape (4-5 pages) + +**Purpose:** Analyze technology trends and innovation dynamics. + +#### Section 6.1: Current Technology Stack + +**Content Requirements:** +- Core technologies in use +- Infrastructure requirements +- Integration landscape +- Technology maturity levels + +#### Section 6.2: Technology Roadmap + +**Content Requirements:** +- Near-term evolution (1-2 years) +- Medium-term evolution (3-5 years) +- Long-term evolution (5-10 years) +- Key milestones and inflection points + +**Required Visual:** Technology roadmap diagram + +#### Section 6.3: Emerging Technologies + +**For each emerging technology, cover:** +- Technology description +- Current maturity level (TRL or similar) +- Expected timeline to mainstream +- Potential impact on market +- Leading companies/regions + +**Common emerging technologies to assess:** +- Artificial intelligence/ML +- Cloud computing +- IoT/Connected devices +- Blockchain +- Automation/Robotics +- Domain-specific technologies + +#### Section 6.4: Innovation Trends + +**Content Requirements:** +- R&D investment levels in industry +- Patent filing trends +- Startup activity and funding +- Corporate innovation initiatives +- University/research partnerships + +**Required Visual:** Innovation/adoption curve or hype cycle + +#### Section 6.5: Technology Adoption Barriers + +**Content Requirements:** +- Technical complexity +- Integration challenges +- Cost barriers +- Skills gaps +- Security/privacy concerns +- Change management challenges + +**Required Visuals (2 total):** +1. Technology roadmap diagram +2. Innovation/adoption curve + +--- + +### Chapter 7: Regulatory & Policy Environment (3-4 pages) + +**Purpose:** Analyze regulatory framework and policy impacts. + +#### Section 7.1: Current Regulatory Framework + +**Content Requirements:** +- Key regulations affecting the market +- Regulatory bodies and their roles +- Compliance requirements +- Enforcement mechanisms +- Penalties for non-compliance + +#### Section 7.2: Regulatory Timeline + +**Required Visual:** Regulatory timeline + +**Content Requirements:** +- Historical regulatory milestones +- Recent regulatory changes +- Upcoming regulations +- Expected future developments + +#### Section 7.3: Regulatory Impact Analysis + +**Content Requirements:** +- Compliance costs +- Market access implications +- Competitive implications +- Product/service requirements +- Operating restrictions + +#### Section 7.4: Policy Trends + +**Content Requirements:** +- Government priorities +- Funding initiatives +- Trade policies +- Environmental policies +- Industry-specific policies + +#### Section 7.5: Regional Regulatory Differences + +**Content Requirements:** +- Comparison of regulations by region +- Harmonization efforts +- Key differences to navigate +- Best practices for compliance + +**Required Visuals (1 total):** +1. Regulatory timeline + +--- + +### Chapter 8: Risk Analysis (3-4 pages) + +**Purpose:** Identify, assess, and propose mitigations for key risks. + +#### Section 8.1: Risk Overview + +**Content Requirements:** +- Risk categories covered +- Risk assessment methodology +- Overall risk profile assessment + +#### Section 8.2: Risk Assessment + +**Required Visual:** Risk heatmap (probability vs. impact) + +**Risk categories to cover:** +- Market risks +- Competitive risks +- Regulatory risks +- Technology risks +- Operational risks +- Financial risks +- Reputational risks + +**For each risk, include:** +- Risk description +- Probability rating (Low/Medium/High) +- Impact rating (Low/Medium/High) +- Overall risk rating +- Contributing factors +- Early warning indicators + +**Data Table Required:** +Risk register showing: +- Risk name +- Category +- Probability +- Impact +- Overall rating +- Owner + +#### Section 8.3: Detailed Risk Analysis + +Provide detailed analysis of top 5-10 risks, including: +- Full description of the risk +- Scenarios that could trigger it +- Potential consequences +- Affected stakeholders +- Timeline considerations + +#### Section 8.4: Risk Mitigation Strategies + +**Required Visual:** Risk mitigation matrix + +**For each major risk, provide:** +- Prevention strategies +- Detection mechanisms +- Response plans +- Recovery approaches +- Contingency plans + +**Required Visuals (2 total):** +1. Risk heatmap +2. Risk mitigation matrix + +--- + +## Strategic Recommendations Chapters + +### Chapter 9: Strategic Opportunities & Recommendations (4-5 pages) + +**Purpose:** Synthesize analysis into actionable strategic guidance. + +#### Section 9.1: Opportunity Analysis + +**Required Visual:** Opportunity matrix (attractiveness vs. ability to win) + +**Content Requirements:** +- Identification of 5-8 strategic opportunities +- Sizing of each opportunity +- Attractiveness assessment +- Ability to win assessment +- Prioritization + +#### Section 9.2: Detailed Opportunity Analysis + +For each top opportunity, provide: +- Description and scope +- Market size potential +- Growth trajectory +- Key success factors +- Required capabilities +- Investment requirements +- Expected returns +- Timeline to value + +#### Section 9.3: Strategic Options Analysis + +**Content Requirements:** +- Build (organic development) options +- Buy (M&A) options +- Partner (strategic alliances) options +- Decision framework for each opportunity + +#### Section 9.4: Prioritized Recommendations + +**Required Visual:** Recommendation priority matrix (impact vs. effort) + +**Structure recommendations in tiers:** + +**Tier 1: Immediate Priority** +- Actions to take in next 0-6 months +- Quick wins with high impact +- Foundation-setting activities + +**Tier 2: Near-Term (6-12 months)** +- Build on Tier 1 actions +- Larger investments +- Capability development + +**Tier 3: Medium-Term (1-2 years)** +- Strategic initiatives +- Major investments +- Transformational changes + +**For each recommendation:** +- Clear action statement +- Rationale (why this matters) +- Expected outcome +- Investment required +- Timeline +- Success metrics +- Dependencies + +#### Section 9.5: Success Factors + +**Content Requirements:** +- Critical success factors for implementation +- Organizational capabilities required +- Resource requirements +- External dependencies +- Timing considerations + +**Required Visuals (3 total):** +1. Opportunity matrix +2. Strategic options framework +3. Recommendation priority matrix + +--- + +### Chapter 10: Implementation Roadmap (3-4 pages) + +**Purpose:** Provide actionable implementation guidance. + +#### Section 10.1: Implementation Overview + +**Content Requirements:** +- Phased approach description +- Overall timeline +- Key dependencies +- Critical path items + +#### Section 10.2: Phased Implementation Plan + +**Required Visual:** Implementation timeline/Gantt chart + +**For each phase:** +- Phase name and duration +- Objectives +- Key activities +- Deliverables +- Resources required +- Dependencies +- Success criteria + +**Typical phases:** +- Phase 1: Foundation (months 1-6) +- Phase 2: Build (months 4-12) +- Phase 3: Scale (months 10-18) +- Phase 4: Optimize (months 16-24) + +#### Section 10.3: Key Milestones + +**Required Visual:** Milestone tracker + +**Data Table Required:** +Milestone table showing: +- Milestone name +- Target date +- Owner +- Success criteria +- Dependencies + +#### Section 10.4: Resource Requirements + +**Content Requirements:** +- Team structure and roles +- Budget allocation by phase +- Technology requirements +- External support needs +- Training requirements + +#### Section 10.5: Governance Structure + +**Content Requirements:** +- Decision-making authority +- Reporting structure +- Review cadence +- Escalation paths +- Change management process + +**Required Visuals (2 total):** +1. Implementation timeline/Gantt +2. Milestone tracker + +--- + +### Chapter 11: Investment Thesis & Financial Projections (3-4 pages) + +**Purpose:** Provide financial framework for decision-making. + +#### Section 11.1: Investment Summary + +**Content Requirements:** +- Summary of investment opportunity +- Key value drivers +- Expected returns +- Investment timeline +- Risk-adjusted assessment + +#### Section 11.2: Financial Projections + +**Required Visual:** Financial projections chart + +**Data Table Required:** +5-year projections showing: +- Revenue +- Growth rate +- Gross margin +- EBITDA +- EBITDA margin +- Key operating metrics + +#### Section 11.3: Scenario Analysis + +**Required Visual:** Scenario comparison chart + +**Three scenarios:** +- Conservative: Lower growth, higher costs +- Base Case: Expected performance +- Optimistic: Favorable conditions + +**For each scenario:** +- Key assumptions +- Revenue projections +- Profitability projections +- Investment requirements +- Return metrics + +#### Section 11.4: Key Assumptions + +**Document all assumptions:** +- Market growth assumptions +- Market share assumptions +- Pricing assumptions +- Cost assumptions +- Timing assumptions +- Competitive assumptions + +#### Section 11.5: Sensitivity Analysis + +**Content Requirements:** +- Key variables affecting returns +- Sensitivity to market growth +- Sensitivity to pricing +- Sensitivity to timing +- Break-even analysis + +#### Section 11.6: Return Expectations + +**Content Requirements:** +- ROI projections +- Payback period +- IRR estimates +- NPV analysis +- Multiple analysis (if applicable) + +**Required Visuals (2 total):** +1. Financial projections chart +2. Scenario comparison chart + +--- + +## Back Matter + +### Appendix A: Methodology & Data Sources + +**Content Requirements:** +- Research methodology description +- Primary research methods +- Secondary research sources +- Data collection timeframe +- Analytical frameworks used +- Limitations and assumptions + +### Appendix B: Detailed Market Data + +**Content Requirements:** +- Comprehensive data tables +- Year-by-year market data +- Regional breakdowns +- Segment details +- Historical data series + +### Appendix C: Company Profiles + +**For each major company:** +- Company overview +- Headquarters and key locations +- Revenue and employee count +- Market position +- Key products/services +- Recent developments +- Strategic focus + +### References/Bibliography + +**All sources cited:** +- Market research reports +- Industry publications +- Government data +- Company reports +- Academic sources +- News articles + +--- + +## Quality Checklist + +Before finalizing, verify: + +- [ ] All required sections are complete +- [ ] All data points have sources +- [ ] All 25-30 visuals are included +- [ ] Executive summary captures key findings +- [ ] Recommendations are actionable +- [ ] Financial projections are internally consistent +- [ ] No placeholder content remains +- [ ] Page count exceeds 50 pages +- [ ] Table of contents is accurate +- [ ] All cross-references work +- [ ] Bibliography is complete diff --git a/skills/market-research-reports/references/visual_generation_guide.md b/skills/market-research-reports/references/visual_generation_guide.md new file mode 100755 index 0000000..2897f90 --- /dev/null +++ b/skills/market-research-reports/references/visual_generation_guide.md @@ -0,0 +1,1077 @@ +# Visual Generation Guide for Market Research Reports + +Complete prompts and guidance for generating visualizations in market research reports. + +--- + +## Overview + +Market research reports should start with **5-6 essential visuals** to establish the analytical framework. Additional visuals can be generated as needed when writing specific sections. This guide provides ready-to-use prompts for the `scientific-schematics` and `generate-image` skills. + +### Core Visuals (Generate First - Priority 1-6) + +Start every market report by generating these 5-6 core visuals: + +1. **Market Growth Trajectory Chart** - Shows market size trends +2. **TAM/SAM/SOM Diagram** - Market opportunity breakdown +3. **Porter's Five Forces** - Competitive dynamics framework +4. **Competitive Positioning Matrix** - Strategic positioning +5. **Risk Heatmap** - Risk assessment visualization +6. **Executive Summary Infographic** (optional) - Report overview + +### Extended Visuals (Generate as Needed - Priority 7+) + +Additional visuals can be generated during writing when specific sections require visual support: +- Regional breakdown charts +- Segment analysis +- Customer journey maps +- Technology roadmaps +- Regulatory timelines +- Financial projections +- Implementation timelines + +### Tool Selection + +| Visual Type | Tool | Rationale | +|-------------|------|-----------| +| Charts (bar, line, pie) | scientific-schematics | Precise data representation | +| Diagrams (flow, structure) | scientific-schematics | Clear technical layouts | +| Matrices (2x2, positioning) | scientific-schematics | Strategic frameworks | +| Timelines | scientific-schematics | Sequential information | +| Infographics | generate-image | Creative visual synthesis | +| Conceptual illustrations | generate-image | Abstract concepts | + +--- + +## Visual Naming Convention + +### Core Visuals (Generate First) +``` +figures/ +├── 01_market_growth_trajectory.png # PRIORITY 1 +├── 02_tam_sam_som.png # PRIORITY 2 +├── 03_porters_five_forces.png # PRIORITY 3 +├── 04_competitive_positioning.png # PRIORITY 4 +├── 05_risk_heatmap.png # PRIORITY 5 +└── 06_exec_summary_infographic.png # PRIORITY 6 (optional) +``` + +### Extended Visuals (Generate as Needed) +``` +figures/ +├── 07_industry_ecosystem.png +├── 08_regional_breakdown.png +├── 09_segment_growth.png +├── 10_driver_impact_matrix.png +├── 11_pestle_analysis.png +├── 12_trends_timeline.png +├── 13_market_share.png +├── 14_strategic_groups.png +├── 15_customer_segments.png +├── 16_segment_attractiveness.png +├── 17_customer_journey.png +├── 18_technology_roadmap.png +├── 19_innovation_curve.png +├── 20_regulatory_timeline.png +├── 21_risk_mitigation.png +├── 22_opportunity_matrix.png +├── 23_recommendation_priority.png +├── 24_implementation_timeline.png +├── 25_milestone_tracker.png +├── 26_financial_projections.png +└── 27_scenario_analysis.png +``` + +--- + +## CORE VISUALS (Priority 1-6) - Generate These First + +### Priority 1: Market Growth Trajectory Chart + +**Tool:** scientific-schematics + +**Purpose:** Foundation visual showing historical and projected market size + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Bar chart market growth 2020 to 2034. Historical bars 2020-2024 in dark blue, projected bars 2025-2034 in light blue. Y-axis billions USD, X-axis years. CAGR annotation. Data labels on each bar. Vertical dashed line between 2024 and 2025. Title: Market Growth Trajectory. Professional white background" \ + -o figures/01_market_growth_trajectory.png --doc-type report +``` + +--- + +### Priority 2: TAM/SAM/SOM Diagram + +**Tool:** scientific-schematics + +**Purpose:** Market opportunity sizing visualization + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "TAM SAM SOM concentric circles. Outer circle TAM Total Addressable Market. Middle circle SAM Serviceable Addressable Market. Inner circle SOM Serviceable Obtainable Market. Each labeled with acronym, full name, placeholder for dollar value. Arrows pointing to each with descriptions. Blue gradient darkest outer to lightest inner. White background professional appearance" \ + -o figures/02_tam_sam_som.png --doc-type report +``` + +--- + +### Priority 3: Porter's Five Forces Diagram + +**Tool:** scientific-schematics + +**Purpose:** Competitive dynamics framework + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Porter's Five Forces diagram. Center box Competitive Rivalry with rating. Four surrounding boxes with arrows to center: Top Threat of New Entrants, Left Bargaining Power Suppliers, Right Bargaining Power Buyers, Bottom Threat of Substitutes. Color code HIGH red, MEDIUM yellow, LOW green. Include 2-3 key factors per box. Professional appearance" \ + -o figures/03_porters_five_forces.png --doc-type report +``` + +--- + +### Priority 4: Competitive Positioning Matrix + +**Tool:** scientific-schematics + +**Purpose:** Strategic positioning of key market players + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "2x2 competitive positioning matrix. X-axis Market Focus Niche to Broad. Y-axis Solution Approach Product to Platform. Quadrants: Upper-right Platform Leaders, Upper-left Niche Platforms, Lower-right Product Leaders, Lower-left Specialists. Plot 8-10 company circles with names. Circle size = market share. Legend for sizes. Professional appearance" \ + -o figures/04_competitive_positioning.png --doc-type report +``` + +--- + +### Priority 5: Risk Heatmap + +**Tool:** scientific-schematics + +**Purpose:** Visual risk assessment matrix + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Risk heatmap matrix. X-axis Impact Low Medium High Critical. Y-axis Probability Unlikely Possible Likely Very Likely. Cell colors: Green low risk, Yellow medium, Orange high, Red critical. Plot 10-12 numbered risks R1 R2 etc as labeled points. Legend with risk names. Professional clear" \ + -o figures/05_risk_heatmap.png --doc-type report +``` + +--- + +### Priority 6: Executive Summary Infographic (Optional) + +**Tool:** generate-image + +**Purpose:** High-level visual synthesis for cover or executive summary + +**Command:** +```bash +python skills/generate-image/scripts/generate_image.py \ + "Executive summary infographic for market research, one page layout, central large metric showing market size, four quadrants showing growth rate key players top segments regional leaders, modern flat design, professional blue and green color scheme, clean white background, corporate business aesthetic" \ + --output figures/06_exec_summary_infographic.png +``` + +--- + +## EXTENDED VISUALS - Generate During Writing as Needed + +The following visuals can be generated when writing specific chapters that require them. + +--- + +## Front Matter Visuals + +### Extended: Cover Image / Hero Visual + +**Tool:** generate-image + +**Prompt:** +``` +Professional executive summary infographic for [MARKET NAME] market research report. +Modern data visualization style showing key metrics: market size, growth rate, key players. +Blue and green color scheme matching corporate design. +Clean minimalist design with icons. +High resolution, publication quality. +No text overlays, image only. +``` + +**Command:** +```bash +python skills/generate-image/scripts/generate_image.py \ + "Professional executive summary infographic for [MARKET] market research report, modern data visualization style, key metrics display, blue and green corporate color scheme, clean minimalist design with icons, high resolution publication quality" \ + --output figures/01_cover_image.png +``` + +### 2. Executive Summary Infographic + +**Tool:** generate-image + +**Prompt:** +``` +One-page executive summary infographic showing: +- Large central metric: $XX billion market size +- Four quadrants with: Growth Rate, Key Players, Top Segments, Regional Leaders +- Modern flat design with data visualization elements +- Professional blue (#003366) and green (#008060) color scheme +- Clean white background +- Business/corporate aesthetic +``` + +**Command:** +```bash +python skills/generate-image/scripts/generate_image.py \ + "Executive summary infographic for market research, one page layout, central large metric showing market size, four quadrants showing growth rate key players top segments regional leaders, modern flat design, professional blue and green color scheme, clean white background, corporate business aesthetic" \ + --output figures/02_exec_summary_infographic.png +``` + +--- + +## Chapter 1: Market Overview Visuals + +### 3. Industry Ecosystem Diagram + +**Tool:** scientific-schematics + +**Prompt:** +``` +Industry ecosystem value chain diagram showing horizontal flow from left to right: +[Suppliers/Inputs] → [Manufacturers/Processors] → [Distributors/Channels] → [End Users/Customers] + +At each stage, show 3-4 example player types in smaller boxes below. +Use arrows to show product/service flow (solid) and money flow (dashed). +Include regulatory bodies as oversight layer above the chain. +Professional blue color scheme. +Clean white background. +All text clearly readable. +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Industry ecosystem value chain diagram. Horizontal flow left to right: Suppliers box → Manufacturers box → Distributors box → End Users box. Below each main box show 3-4 smaller boxes with example player types. Solid arrows for product flow, dashed arrows for money flow. Regulatory oversight layer above. Professional blue color scheme, white background, clear labels" \ + -o figures/03_industry_ecosystem.png --doc-type report +``` + +### 4. Market Structure Diagram + +**Tool:** scientific-schematics + +**Prompt:** +``` +Market structure diagram showing concentric rectangles: +- Center: Core Market (labeled with market name) +- Second layer: Adjacent Markets (labeled with 4-5 adjacent market names) +- Third layer: Enabling Technologies (labeled with key technologies) +- Outer layer: Regulatory Framework + +Use different shades of blue for each layer. +Include small icons or labels for key elements. +Professional appearance. +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Market structure diagram with concentric rectangles. Center: Core Market [MARKET NAME]. Second layer: Adjacent Markets with 4-5 labels. Third layer: Enabling Technologies with key tech labels. Outer layer: Regulatory Framework. Different blue shades for each layer, professional appearance, clear labels" \ + -o figures/03b_market_structure.png --doc-type report +``` + +--- + +## Chapter 2: Market Size & Growth Visuals + +### 5. Market Growth Trajectory Chart + +**Tool:** scientific-schematics + +**Prompt:** +``` +Bar chart showing market growth from 2020 to 2034. +Historical years (2020-2024): Dark blue bars +Projected years (2025-2034): Light blue bars +Y-axis: Market size in billions USD (0 to $XXX) +X-axis: Years +Include CAGR annotation showing "XX.X% CAGR (2024-2034)" +Data labels on top of each bar +Vertical dashed line separating historical from projected +Title: "[MARKET NAME] Market Growth Trajectory" +Professional appearance, white background +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Bar chart market growth 2020 to 2034. Historical bars 2020-2024 in dark blue, projected bars 2025-2034 in light blue. Y-axis billions USD, X-axis years. CAGR annotation XX.X% (2024-2034). Data labels on each bar. Vertical dashed line between 2024 and 2025. Title: Market Growth Trajectory. Professional white background" \ + -o figures/04_market_growth_trajectory.png --doc-type report +``` + +### 6. TAM/SAM/SOM Diagram + +**Tool:** scientific-schematics + +**Prompt:** +``` +TAM SAM SOM concentric circles diagram: +- Outer circle: TAM (Total Addressable Market) - $XXX billion +- Middle circle: SAM (Serviceable Addressable Market) - $XX billion +- Inner circle: SOM (Serviceable Obtainable Market) - $X billion + +Each circle labeled with: +- Acronym in bold +- Full name +- Dollar value + +Arrows pointing to each circle with descriptions +Use blue color gradient (darkest for TAM, lightest for SOM) +Professional appearance +White background +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "TAM SAM SOM concentric circles. Outer circle TAM Total Addressable Market [VALUE]B. Middle circle SAM Serviceable Addressable Market [VALUE]B. Inner circle SOM Serviceable Obtainable Market [VALUE]B. Each labeled with acronym, full name, dollar value. Arrows pointing to each with descriptions. Blue gradient darkest outer to lightest inner. White background professional" \ + -o figures/05_tam_sam_som.png --doc-type report +``` + +### 7. Regional Market Breakdown + +**Tool:** scientific-schematics + +**Prompt:** +``` +Pie chart OR treemap showing regional market breakdown: +- North America: XX% ($X.XB) - Dark blue +- Europe: XX% ($X.XB) - Medium blue +- Asia-Pacific: XX% ($X.XB) - Teal +- Latin America: X% ($X.XB) - Light blue +- Middle East & Africa: X% ($X.XB) - Gray blue + +Include both percentage and dollar value for each region +Legend on right side +Title: "Market Size by Region (2024)" +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Pie chart regional market breakdown. North America XX% dark blue, Europe XX% medium blue, Asia-Pacific XX% teal, Latin America XX% light blue, Middle East Africa XX% gray blue. Show percentage and dollar value for each slice. Legend on right. Title: Market Size by Region 2024. Professional appearance" \ + -o figures/06_regional_breakdown.png --doc-type report +``` + +### 8. Segment Growth Comparison + +**Tool:** scientific-schematics + +**Prompt:** +``` +Horizontal bar chart comparing segment growth rates: +- Y-axis: Segment names (5-7 segments) +- X-axis: CAGR percentage (0% to 30%) +- Bars colored by growth rate: Green (highest) to blue (lowest) +- Data labels showing exact percentage on each bar +- Sort segments from highest to lowest growth +- Title: "Segment Growth Rate Comparison (CAGR 2024-2034)" +- Include average line or marker +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Horizontal bar chart segment growth comparison. Y-axis 5-7 segment names, X-axis CAGR percentage 0-30%. Bars colored green highest to blue lowest. Data labels with exact percentages. Sorted highest to lowest. Title: Segment Growth Rate Comparison CAGR 2024-2034. Include market average line" \ + -o figures/07_segment_growth.png --doc-type report +``` + +--- + +## Chapter 3: Industry Drivers & Trends Visuals + +### 9. Driver Impact Matrix + +**Tool:** scientific-schematics + +**Prompt:** +``` +2x2 matrix for market driver assessment: +- X-axis: Impact on Market (Low → High) +- Y-axis: Probability of Occurrence (Low → High) +- Upper-right quadrant: "CRITICAL DRIVERS" (red/orange background) +- Upper-left quadrant: "MONITOR" (yellow background) +- Lower-right quadrant: "WATCH CAREFULLY" (yellow background) +- Lower-left quadrant: "LOWER PRIORITY" (green background) + +Plot 8-10 drivers as labeled circles: +- Size of circle represents current market impact +- Position based on ratings + +Include legend for circle sizes +Professional appearance with clear labels +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "2x2 matrix driver impact assessment. X-axis Impact Low to High, Y-axis Probability Low to High. Quadrants: Upper-right CRITICAL DRIVERS red, Upper-left MONITOR yellow, Lower-right WATCH CAREFULLY yellow, Lower-left LOWER PRIORITY green. Plot 8-10 labeled driver circles at appropriate positions. Circle size indicates current impact. Professional clear labels" \ + -o figures/08_driver_impact_matrix.png --doc-type report +``` + +### 10. PESTLE Analysis Diagram + +**Tool:** scientific-schematics + +**Prompt:** +``` +PESTLE analysis hexagonal diagram: +- Center hexagon: "[MARKET NAME]" +- Six surrounding hexagons connected to center: + - Political (red/orange) + - Economic (blue) + - Social (green) + - Technological (orange) + - Legal (purple) + - Environmental (teal) + +Each outer hexagon contains 2-3 key bullet points +Connecting lines between center and outer hexagons +Professional appearance +Clear, readable text in each hexagon +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "PESTLE hexagonal diagram. Center hexagon labeled MARKET. Six surrounding hexagons: Political red, Economic blue, Social green, Technological orange, Legal purple, Environmental teal. Each outer hexagon has 2-3 bullet points of key factors. Lines connecting center to each. Professional appearance clear readable text" \ + -o figures/09_pestle_analysis.png --doc-type report +``` + +### 11. Industry Trends Timeline + +**Tool:** scientific-schematics + +**Prompt:** +``` +Horizontal timeline showing emerging trends from 2024 to 2030: +- Main horizontal axis with year markers +- Plot 6-8 trends at different points on timeline +- Each trend shown with: + - Icon or symbol + - Trend name + - Brief 3-5 word description below + +Color-code by trend category: +- Technology trends: Blue +- Market trends: Green +- Regulatory trends: Orange + +Include "Current" marker at 2024 +Professional appearance with clear labels +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Horizontal timeline 2024 to 2030. Plot 6-8 emerging trends at different years. Each trend with icon, name, brief description. Color code: Technology trends blue, Market trends green, Regulatory trends orange. Current marker at 2024. Professional clear labels" \ + -o figures/10_trends_timeline.png --doc-type report +``` + +--- + +## Chapter 4: Competitive Landscape Visuals + +### 12. Porter's Five Forces Diagram + +**Tool:** scientific-schematics + +**Prompt:** +``` +Porter's Five Forces diagram with center and four surrounding boxes: + +Center box: "Competitive Rivalry" with rating [HIGH/MEDIUM/LOW] + +Surrounding boxes connected by arrows: +- Top: "Threat of New Entrants" [RATING] +- Left: "Bargaining Power of Suppliers" [RATING] +- Right: "Bargaining Power of Buyers" [RATING] +- Bottom: "Threat of Substitutes" [RATING] + +Color-code ratings: +- HIGH: Red/orange background +- MEDIUM: Yellow background +- LOW: Green background + +Arrows pointing toward center +Include key factors as bullet points in each box +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Porter's Five Forces diagram. Center box Competitive Rivalry [RATING]. Four surrounding boxes with arrows to center: Top Threat of New Entrants [RATING], Left Bargaining Power Suppliers [RATING], Right Bargaining Power Buyers [RATING], Bottom Threat of Substitutes [RATING]. Color code HIGH red, MEDIUM yellow, LOW green. Include 2-3 key factors per box. Professional appearance" \ + -o figures/11_porters_five_forces.png --doc-type report +``` + +### 13. Market Share Chart + +**Tool:** scientific-schematics + +**Prompt:** +``` +Pie chart or donut chart showing market share: +- Top 10 companies with distinct colors +- Company A: XX% (largest slice, dark blue) +- Company B: XX% (medium blue) +- [Continue for top 10] +- Others: XX% (gray) + +Include: +- Percentage labels on each slice +- Company names in legend or on slices +- Total market size annotation +- Title: "Market Share by Company (2024)" + +Professional appearance +Colorblind-friendly palette +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Pie chart market share top 10 companies. Company A XX% dark blue, Company B XX% medium blue, [list companies and shares], Others XX% gray. Percentage labels on slices. Legend with company names. Total market size annotation. Title: Market Share by Company 2024. Colorblind-friendly colors professional" \ + -o figures/12_market_share.png --doc-type report +``` + +### 14. Competitive Positioning Matrix + +**Tool:** scientific-schematics + +**Prompt:** +``` +2x2 competitive positioning matrix: +- X-axis: Market Focus (Niche ← → Broad) +- Y-axis: Solution Approach (Product ← → Platform) + +Quadrant labels: +- Upper-right: "Platform Leaders" +- Upper-left: "Niche Platforms" +- Lower-right: "Product Leaders" +- Lower-left: "Specialists" + +Plot 8-10 companies as labeled circles: +- Circle size represents market share +- Position based on strategy + +Include legend for circle sizes +Company name labels +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "2x2 competitive positioning matrix. X-axis Market Focus Niche to Broad. Y-axis Solution Approach Product to Platform. Quadrants: Upper-right Platform Leaders, Upper-left Niche Platforms, Lower-right Product Leaders, Lower-left Specialists. Plot 8-10 company circles with names. Circle size = market share. Legend for sizes. Professional" \ + -o figures/13_competitive_positioning.png --doc-type report +``` + +### 15. Strategic Group Map + +**Tool:** scientific-schematics + +**Prompt:** +``` +Strategic group map showing competitor clusters: +- X-axis: Geographic Scope (Regional ← → Global) +- Y-axis: Product Breadth (Narrow ← → Broad) + +Draw 4-5 oval "bubbles" representing strategic groups: +- Each bubble contains 2-4 company names +- Bubble size represents collective market share of group +- Different colors for each strategic group + +Label each strategic group: +- "Global Generalists" +- "Regional Specialists" +- "Focused Innovators" +- etc. + +Professional appearance +Clear company name labels +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Strategic group map. X-axis Geographic Scope Regional to Global. Y-axis Product Breadth Narrow to Broad. Draw 4-5 oval bubbles for strategic groups. Each bubble contains 2-4 company names. Bubble size = collective market share. Label groups: Global Generalists, Regional Specialists, Focused Innovators etc. Different colors per group. Professional clear labels" \ + -o figures/14_strategic_groups.png --doc-type report +``` + +--- + +## Chapter 5: Customer Analysis Visuals + +### 16. Customer Segmentation Breakdown + +**Tool:** scientific-schematics + +**Prompt:** +``` +Treemap or pie chart showing customer segments: +- Large Enterprise: XX% (dark blue) +- Mid-Market: XX% (medium blue) +- SMB: XX% (light blue) +- Consumer: XX% (teal) + +Size represents market share +Include for each segment: +- Segment name +- Percentage +- Dollar value + +Title: "Customer Segmentation by Market Share" +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Treemap customer segmentation. Large Enterprise XX% dark blue, Mid-Market XX% medium blue, SMB XX% light blue, Consumer XX% teal. Each segment shows name percentage dollar value. Title: Customer Segmentation by Market Share. Professional appearance" \ + -o figures/15_customer_segments.png --doc-type report +``` + +### 17. Segment Attractiveness Matrix + +**Tool:** scientific-schematics + +**Prompt:** +``` +2x2 segment attractiveness matrix: +- X-axis: Segment Size (Small ← → Large) +- Y-axis: Growth Rate (Low ← → High) + +Quadrant labels and actions: +- Upper-right: "PRIORITY - Invest Heavily" +- Upper-left: "INVEST TO GROW" +- Lower-right: "HARVEST" +- Lower-left: "DEPRIORITIZE" + +Plot customer segments as labeled circles +Circle size represents profitability +Different colors for each segment +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "2x2 segment attractiveness matrix. X-axis Segment Size Small to Large. Y-axis Growth Rate Low to High. Quadrants: Upper-right PRIORITY Invest Heavily, Upper-left INVEST TO GROW, Lower-right HARVEST, Lower-left DEPRIORITIZE. Plot customer segments as circles. Circle size = profitability. Different colors. Professional" \ + -o figures/16_segment_attractiveness.png --doc-type report +``` + +### 18. Customer Journey Map + +**Tool:** scientific-schematics + +**Prompt:** +``` +Customer journey horizontal flowchart showing 5-6 stages: +Awareness → Consideration → Decision → Implementation → Usage → Advocacy + +For each stage, show three rows: +1. Key Activities (what customer does) +2. Pain Points (challenges faced) +3. Touchpoints (how they interact) + +Use icons for each stage +Color gradient from light to dark as journey progresses +Professional appearance +Clear labels +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Customer journey horizontal flowchart. 5 stages left to right: Awareness, Consideration, Decision, Implementation, Usage, Advocacy. Each stage shows Key Activities, Pain Points, Touchpoints in rows below. Icons for each stage. Color gradient light to dark. Professional clear labels" \ + -o figures/17_customer_journey.png --doc-type report +``` + +--- + +## Chapter 6: Technology Landscape Visuals + +### 19. Technology Roadmap + +**Tool:** scientific-schematics + +**Prompt:** +``` +Technology roadmap timeline from 2024 to 2030: +Three parallel horizontal tracks: +1. Core Technology (blue) - current foundation +2. Emerging Technology (green) - developing capabilities +3. Enabling Technology (orange) - infrastructure/support + +Each track shows milestones and technology introductions as markers +Vertical lines connect related technologies across tracks +Timeline markers for each year +Technology names labeled at introduction points +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Technology roadmap 2024 to 2030. Three parallel horizontal tracks: Core Technology blue, Emerging Technology green, Enabling Technology orange. Milestones and tech introductions marked on each track. Vertical lines connect related tech. Year markers. Technology names labeled. Professional appearance" \ + -o figures/18_technology_roadmap.png --doc-type report +``` + +### 20. Innovation/Adoption Curve + +**Tool:** scientific-schematics + +**Prompt:** +``` +Gartner Hype Cycle or Technology Adoption Curve: +Five phases from left to right: +1. Innovation Trigger (rising) +2. Peak of Inflated Expectations (peak) +3. Trough of Disillusionment (bottom) +4. Slope of Enlightenment (rising) +5. Plateau of Productivity (stable) + +Plot 6-8 technologies at different positions on the curve +Each technology labeled with name +Color-code by technology category +Professional appearance +Clear axis labels +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Gartner Hype Cycle curve. Five phases: Innovation Trigger rising, Peak of Inflated Expectations at top, Trough of Disillusionment at bottom, Slope of Enlightenment rising, Plateau of Productivity stable. Plot 6-8 technologies on curve with labels. Color by category. Professional clear labels" \ + -o figures/19_innovation_curve.png --doc-type report +``` + +--- + +## Chapter 7: Regulatory Environment Visuals + +### 21. Regulatory Timeline + +**Tool:** scientific-schematics + +**Prompt:** +``` +Regulatory timeline from 2020 to 2028: +Horizontal timeline with year markers +Mark key regulatory events: +- Past regulations (dark blue markers, solid) +- Current regulations (green marker at current year) +- Upcoming regulations (light blue markers, dashed) + +Each marker shows: +- Regulation name +- Effective date +- Brief description (5-7 words) + +Vertical "NOW" line at current year (2024) +Group by region if multiple jurisdictions +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Regulatory timeline 2020 to 2028. Past regulations dark blue solid markers, current green marker, upcoming light blue dashed. Each shows regulation name, date, brief description. Vertical NOW line at 2024. Professional appearance clear labels" \ + -o figures/20_regulatory_timeline.png --doc-type report +``` + +--- + +## Chapter 8: Risk Analysis Visuals + +### 22. Risk Heatmap + +**Tool:** scientific-schematics + +**Prompt:** +``` +Risk assessment heatmap/matrix: +- X-axis: Impact (Low → Medium → High → Critical) +- Y-axis: Probability (Unlikely → Possible → Likely → Very Likely) + +Color gradient for cells: +- Green: Low risk (low probability, low impact) +- Yellow: Medium risk +- Orange: High risk +- Red: Critical risk (high probability, high impact) + +Plot 10-12 risks as labeled points/circles in appropriate cells +Risk labels should be clearly readable +Include risk numbers (R1, R2, etc.) +Legend linking numbers to risk names +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Risk heatmap matrix. X-axis Impact Low Medium High Critical. Y-axis Probability Unlikely Possible Likely Very Likely. Cell colors: Green low risk, Yellow medium, Orange high, Red critical. Plot 10-12 numbered risks R1 R2 etc as labeled points. Legend with risk names. Professional clear" \ + -o figures/21_risk_heatmap.png --doc-type report +``` + +### 23. Risk Mitigation Framework + +**Tool:** scientific-schematics + +**Prompt:** +``` +Risk mitigation diagram showing risks and their mitigations: +Left column: Risks (in red/orange boxes) +Right column: Mitigation Strategies (in green/blue boxes) + +Connect each risk to its mitigation(s) with arrows +Group risks by category (Market, Regulatory, Technology, etc.) +Include both prevention and response strategies + +Risk severity indicated by box color intensity +Professional appearance +Clear labels +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Risk mitigation diagram. Left column risks in orange/red boxes. Right column mitigation strategies in green/blue boxes. Arrows connecting risks to mitigations. Group by category. Risk severity by color intensity. Include prevention and response. Professional clear labels" \ + -o figures/22_risk_mitigation.png --doc-type report +``` + +--- + +## Chapter 9: Strategic Recommendations Visuals + +### 24. Opportunity Matrix + +**Tool:** scientific-schematics + +**Prompt:** +``` +2x2 opportunity assessment matrix: +- X-axis: Market Attractiveness (Low ← → High) +- Y-axis: Ability to Win (Low ← → High) + +Quadrant labels and strategies: +- Upper-right: "PURSUE AGGRESSIVELY" (green) +- Upper-left: "BUILD CAPABILITIES" (yellow) +- Lower-right: "SELECTIVE INVESTMENT" (yellow) +- Lower-left: "AVOID/DIVEST" (red) + +Plot 6-8 opportunities as labeled circles +Circle size represents opportunity size ($) +Include opportunity names +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "2x2 opportunity matrix. X-axis Market Attractiveness Low to High. Y-axis Ability to Win Low to High. Quadrants: Upper-right PURSUE AGGRESSIVELY green, Upper-left BUILD CAPABILITIES yellow, Lower-right SELECTIVE INVESTMENT yellow, Lower-left AVOID red. Plot 6-8 opportunity circles with labels. Size = opportunity value. Professional" \ + -o figures/23_opportunity_matrix.png --doc-type report +``` + +### 25. Recommendation Priority Matrix + +**Tool:** scientific-schematics + +**Prompt:** +``` +2x2 priority matrix for recommendations: +- X-axis: Effort/Investment (Low ← → High) +- Y-axis: Impact/Value (Low ← → High) + +Quadrant labels: +- Upper-left: "QUICK WINS" (green) - Do First +- Upper-right: "MAJOR PROJECTS" (blue) - Plan Carefully +- Lower-left: "FILL-INS" (gray) - Do If Time +- Lower-right: "THANKLESS TASKS" (red) - Avoid + +Plot 6-8 recommendations as labeled points +Number recommendations by priority +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "2x2 priority matrix. X-axis Effort Low to High. Y-axis Impact Low to High. Quadrants: Upper-left QUICK WINS green Do First, Upper-right MAJOR PROJECTS blue Plan Carefully, Lower-left FILL-INS gray Do If Time, Lower-right THANKLESS TASKS red Avoid. Plot 6-8 numbered recommendations. Professional" \ + -o figures/24_recommendation_priority.png --doc-type report +``` + +--- + +## Chapter 10: Implementation Roadmap Visuals + +### 26. Implementation Timeline/Gantt + +**Tool:** scientific-schematics + +**Prompt:** +``` +Gantt chart style implementation timeline over 24 months: +Four phases shown as horizontal bars: +- Phase 1: Foundation (Months 1-6) - Dark blue +- Phase 2: Build (Months 4-12) - Medium blue +- Phase 3: Scale (Months 10-18) - Teal +- Phase 4: Optimize (Months 16-24) - Light blue + +Phases overlap as shown in dates +Key milestones marked as diamonds on timeline +Month markers on X-axis +Phase names on Y-axis +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Gantt chart implementation 24 months. Phase 1 Foundation months 1-6 dark blue. Phase 2 Build months 4-12 medium blue. Phase 3 Scale months 10-18 teal. Phase 4 Optimize months 16-24 light blue. Overlapping bars. Key milestones as diamonds. Month markers X-axis. Professional" \ + -o figures/25_implementation_timeline.png --doc-type report +``` + +### 27. Milestone Tracker + +**Tool:** scientific-schematics + +**Prompt:** +``` +Milestone tracker showing 8-10 key milestones on horizontal timeline: +Each milestone shows: +- Date/Month +- Milestone name +- Status indicator: + - Completed: Green checkmark ✓ + - In Progress: Yellow circle ○ + - Upcoming: Gray circle ○ + +Group milestones by phase +Connect milestones with timeline line +Include phase labels above timeline +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Milestone tracker horizontal timeline 8-10 milestones. Each shows date, name, status: Completed green check, In Progress yellow circle, Upcoming gray circle. Group by phase. Phase labels above. Connected timeline line. Professional" \ + -o figures/26_milestone_tracker.png --doc-type report +``` + +--- + +## Chapter 11: Investment Thesis Visuals + +### 28. Financial Projections Chart + +**Tool:** scientific-schematics + +**Prompt:** +``` +Combined bar and line chart showing 5-year financial projections: +- Bar chart: Revenue by year (primary Y-axis, in $M) +- Line chart: Growth rate overlay (secondary Y-axis, in %) + +Three scenarios shown: +- Conservative: Gray bars +- Base Case: Blue bars +- Optimistic: Green bars + +X-axis: Year 1 through Year 5 +Include data labels on bars +Legend for scenarios and growth line +Title: "Financial Projections (5-Year)" +Professional appearance +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Combined bar and line chart 5-year projections. Bar chart revenue primary Y-axis dollars. Line chart growth rate secondary Y-axis percent. Three scenarios: Conservative gray, Base Case blue, Optimistic green. X-axis Year 1-5. Data labels. Legend. Title Financial Projections 5-Year. Professional" \ + -o figures/27_financial_projections.png --doc-type report +``` + +### 29. Scenario Analysis Comparison + +**Tool:** scientific-schematics + +**Prompt:** +``` +Grouped bar chart comparing three scenarios across key metrics: +X-axis: Metrics (Revenue Y5, EBITDA Y5, Market Share, ROI) +Y-axis: Value (scale appropriate for each metric) + +Three bars per metric: +- Conservative: Gray +- Base Case: Blue +- Optimistic: Green + +Data labels on each bar +Legend for scenarios +Title: "Scenario Analysis Comparison" +Professional appearance +Clear metric labels +``` + +**Command:** +```bash +python skills/scientific-schematics/scripts/generate_schematic.py \ + "Grouped bar chart scenario comparison. X-axis metrics: Revenue Y5, EBITDA Y5, Market Share, ROI. Three bars per metric: Conservative gray, Base Case blue, Optimistic green. Data labels. Legend. Title Scenario Analysis Comparison. Professional clear labels" \ + -o figures/28_scenario_analysis.png --doc-type report +``` + +--- + +## Batch Generation Script + +For convenience, use the `generate_market_visuals.py` script to batch generate visuals: + +```bash +# Generate core 5-6 visuals only (recommended for starting reports) +python skills/market-research-reports/scripts/generate_market_visuals.py \ + --topic "Electric Vehicle Charging Infrastructure" \ + --output-dir figures/ + +# Generate all 27 visuals (core + extended, for comprehensive coverage) +python skills/market-research-reports/scripts/generate_market_visuals.py \ + --topic "Electric Vehicle Charging Infrastructure" \ + --output-dir figures/ \ + --all + +# Skip already generated files +python skills/market-research-reports/scripts/generate_market_visuals.py \ + --topic "Your Market" \ + --output-dir figures/ \ + --skip-existing +``` + +**Default behavior**: Generates only the 5-6 core priority visuals. Use `--all` flag if you need comprehensive visual coverage for all sections. + +--- + +## Quality Checklist + +Before including visuals in the report, verify: + +- [ ] All text is readable at intended size +- [ ] Colors are consistent across all visuals +- [ ] Color scheme is colorblind-friendly +- [ ] Data labels are accurate +- [ ] Legends are clear and complete +- [ ] Titles are descriptive +- [ ] Sources are noted where applicable +- [ ] Resolution is 300 DPI or higher +- [ ] File format is PNG +- [ ] Naming convention is followed diff --git a/skills/market-research-reports/scripts/generate_market_visuals.py b/skills/market-research-reports/scripts/generate_market_visuals.py new file mode 100755 index 0000000..c227894 --- /dev/null +++ b/skills/market-research-reports/scripts/generate_market_visuals.py @@ -0,0 +1,529 @@ +#!/usr/bin/env python3 +""" +Market Research Report Visual Generator + +Batch generates visuals for a market research report using +scientific-schematics and generate-image skills. + +Default behavior: Generate 5-6 core visuals only +Use --all flag to generate all 28 extended visuals + +Usage: + # Generate core 5-6 visuals (recommended for starting a report) + python generate_market_visuals.py --topic "Electric Vehicle Charging" --output-dir figures/ + + # Generate all 28 visuals (for comprehensive coverage) + python generate_market_visuals.py --topic "AI in Healthcare" --output-dir figures/ --all + + # Skip existing files + python generate_market_visuals.py --topic "Topic" --output-dir figures/ --skip-existing +""" + +import argparse +import os +import subprocess +import sys +from pathlib import Path + + +# Visual definitions with prompts +# Each tuple: (filename, tool, prompt_template, is_core) +# is_core=True for the 5-6 essential visuals to generate first + +CORE_VISUALS = [ + # Priority 1: Market Growth Trajectory + ( + "01_market_growth_trajectory.png", + "scientific-schematics", + "Bar chart {topic} market growth 2020 to 2034. Historical bars 2020-2024 in dark blue, " + "projected bars 2025-2034 in light blue. Y-axis billions USD, X-axis years. " + "CAGR annotation. Data labels on each bar. Vertical dashed line " + "between 2024 and 2025. Title: Market Growth Trajectory. Professional white background" + ), + + # Priority 2: TAM/SAM/SOM + ( + "02_tam_sam_som.png", + "scientific-schematics", + "TAM SAM SOM concentric circles for {topic} market. Outer circle TAM Total Addressable " + "Market. Middle circle SAM Serviceable Addressable Market. Inner circle SOM Serviceable " + "Obtainable Market. Each labeled with acronym, full name. " + "Blue gradient darkest outer to lightest inner. White background professional appearance" + ), + + # Priority 3: Porter's Five Forces + ( + "03_porters_five_forces.png", + "scientific-schematics", + "Porter's Five Forces diagram for {topic}. Center box Competitive Rivalry with rating. " + "Four surrounding boxes with arrows to center: Top Threat of New Entrants, " + "Left Bargaining Power Suppliers, Right Bargaining Power Buyers, " + "Bottom Threat of Substitutes. Color code HIGH red, MEDIUM yellow, LOW green. " + "Include 2-3 key factors per box. Professional appearance" + ), + + # Priority 4: Competitive Positioning Matrix + ( + "04_competitive_positioning.png", + "scientific-schematics", + "2x2 competitive positioning matrix {topic}. X-axis Market Focus Niche to Broad. " + "Y-axis Solution Approach Product to Platform. Quadrants: Upper-right Platform Leaders, " + "Upper-left Niche Platforms, Lower-right Product Leaders, Lower-left Specialists. " + "Plot 8-10 company circles with names. Circle size = market share. " + "Legend for sizes. Professional appearance" + ), + + # Priority 5: Risk Heatmap + ( + "05_risk_heatmap.png", + "scientific-schematics", + "Risk heatmap matrix {topic}. X-axis Impact Low Medium High Critical. " + "Y-axis Probability Unlikely Possible Likely Very Likely. " + "Cell colors: Green low risk, Yellow medium, Orange high, Red critical. " + "Plot 10-12 numbered risks R1 R2 etc as labeled points. " + "Legend with risk names. Professional clear" + ), + + # Priority 6: Executive Summary Infographic (Optional) + ( + "06_exec_summary_infographic.png", + "generate-image", + "Executive summary infographic for {topic} market research, one page layout, " + "central large metric showing market size, four quadrants showing growth rate " + "key players top segments regional leaders, modern flat design, professional " + "blue and green color scheme, clean white background, corporate business aesthetic" + ), +] + +EXTENDED_VISUALS = [ + # Industry Ecosystem + ( + "07_industry_ecosystem.png", + "scientific-schematics", + "Industry ecosystem value chain diagram for {topic} market. Horizontal flow left " + "to right: Suppliers box → Manufacturers box → Distributors box → End Users box. " + "Below each main box show 3-4 smaller boxes with example player types. Solid arrows " + "for product flow, dashed arrows for money flow. Regulatory oversight layer above. " + "Professional blue color scheme, white background, clear labels" + ), + + # Regional Breakdown + ( + "08_regional_breakdown.png", + "scientific-schematics", + "scientific-schematics", + "Pie chart regional market breakdown for {topic}. North America 40% dark blue, " + "Europe 28% medium blue, Asia-Pacific 22% teal, Latin America 6% light blue, " + "Middle East Africa 4% gray blue. Show percentage for each slice. Legend on right. " + "Title: Market Size by Region. Professional appearance" + ), + + # Segment Growth + ( + "09_segment_growth.png", + "scientific-schematics", + "Horizontal bar chart {topic} segment growth comparison. Y-axis 5-6 segment names, " + "X-axis CAGR percentage 0-30%. Bars colored green highest to blue lowest. " + "Data labels with percentages. Sorted highest to lowest. " + "Title: Segment Growth Rate Comparison. Include market average line" + ), + + # Driver Impact Matrix + ( + "10_driver_impact_matrix.png", + "scientific-schematics", + "2x2 matrix driver impact assessment for {topic}. X-axis Impact Low to High, " + "Y-axis Probability Low to High. Quadrants: Upper-right CRITICAL DRIVERS red, " + "Upper-left MONITOR yellow, Lower-right WATCH CAREFULLY yellow, " + "Lower-left LOWER PRIORITY green. Plot 8 labeled driver circles at positions. " + "Circle size indicates current impact. Professional clear labels" + ), + + # PESTLE Analysis + ( + "11_pestle_analysis.png", + "scientific-schematics", + "PESTLE hexagonal diagram for {topic} market. Center hexagon labeled Market Analysis. " + "Six surrounding hexagons: Political red, Economic blue, Social green, " + "Technological orange, Legal purple, Environmental teal. Each outer hexagon " + "has 2-3 bullet points of key factors. Lines connecting center to each. " + "Professional appearance clear readable text" + ), + + # Trends Timeline + ( + "12_trends_timeline.png", + "scientific-schematics", + "Horizontal timeline {topic} trends 2024 to 2030. Plot 6-8 emerging trends at " + "different years. Each trend with icon, name, brief description. Color code: " + "Technology trends blue, Market trends green, Regulatory trends orange. " + "Current marker at 2024. Professional clear labels" + ), + + # Market Share Chart + ( + "13_market_share.png", + "scientific-schematics", + "Pie chart market share {topic} top 10 companies. Company A 18% dark blue, " + "Company B 15% medium blue, Company C 12% teal, Company D 10% light blue, " + "5 more companies 5-8% each various blues, Others 15% gray. " + "Percentage labels on slices. Legend with company names. " + "Title: Market Share by Company. Colorblind-friendly colors professional" + ), + + # Strategic Groups Map + ( + "14_strategic_groups.png", + "scientific-schematics", + "Strategic group map {topic}. X-axis Geographic Scope Regional to Global. " + "Y-axis Product Breadth Narrow to Broad. Draw 4-5 oval bubbles for strategic groups. " + "Each bubble contains 2-4 company names. Bubble size = collective market share. " + "Label groups: Global Generalists, Regional Specialists, Focused Innovators. " + "Different colors per group. Professional clear labels" + ), + + # Customer Segments + ( + "15_customer_segments.png", + "scientific-schematics", + "Treemap customer segmentation {topic}. Large Enterprise 45% dark blue, " + "Mid-Market 30% medium blue, SMB 18% light blue, Consumer 7% teal. " + "Each segment shows name and percentage. Title: Customer Segmentation by Market Share. " + "Professional appearance clear labels" + ), + ( + "16_segment_attractiveness.png", + "scientific-schematics", + "2x2 segment attractiveness matrix {topic}. X-axis Segment Size Small to Large. " + "Y-axis Growth Rate Low to High. Quadrants: Upper-right PRIORITY Invest Heavily green, " + "Upper-left INVEST TO GROW yellow, Lower-right HARVEST orange, " + "Lower-left DEPRIORITIZE gray. Plot customer segments as circles. " + "Circle size = profitability. Different colors. Professional" + ), + ( + "17_customer_journey.png", + "scientific-schematics", + "Customer journey horizontal flowchart {topic}. 5 stages left to right: Awareness, " + "Consideration, Decision, Implementation, Advocacy. Each stage shows Key Activities, " + "Pain Points, Touchpoints in rows below. Icons for each stage. " + "Color gradient light to dark. Professional clear labels" + ), + + # Technology Roadmap + ( + "18_technology_roadmap.png", + "scientific-schematics", + "Technology roadmap {topic} 2024 to 2030. Three parallel horizontal tracks: " + "Core Technology blue, Emerging Technology green, Enabling Technology orange. " + "Milestones and tech introductions marked on each track. Vertical lines connect " + "related tech. Year markers. Technology names labeled. Professional appearance" + ), + ( + "19_innovation_curve.png", + "scientific-schematics", + "Gartner Hype Cycle curve for {topic} technologies. Five phases: Innovation Trigger " + "rising, Peak of Inflated Expectations at top, Trough of Disillusionment at bottom, " + "Slope of Enlightenment rising, Plateau of Productivity stable. " + "Plot 6-8 technologies on curve with labels. Color by category. Professional clear labels" + ), + + # Regulatory Timeline + ( + "20_regulatory_timeline.png", + "scientific-schematics", + "Regulatory timeline {topic} 2020 to 2028. Past regulations dark blue solid markers, " + "current green marker, upcoming light blue dashed. Each shows regulation name, date, " + "brief description. Vertical NOW line at 2024. Professional appearance clear labels" + ), + + # Risk Mitigation Matrix + ( + "21_risk_mitigation.png", + "scientific-schematics", + "Risk mitigation diagram {topic}. Left column risks in orange/red boxes. " + "Right column mitigation strategies in green/blue boxes. Arrows connecting " + "risks to mitigations. Group by category. Risk severity by color intensity. " + "Include prevention and response. Professional clear labels" + ), + + # Opportunity Matrix + ( + "22_opportunity_matrix.png", + "scientific-schematics", + "2x2 opportunity matrix {topic}. X-axis Market Attractiveness Low to High. " + "Y-axis Ability to Win Low to High. Quadrants: Upper-right PURSUE AGGRESSIVELY green, " + "Upper-left BUILD CAPABILITIES yellow, Lower-right SELECTIVE INVESTMENT yellow, " + "Lower-left AVOID red. Plot 6-8 opportunity circles with labels. " + "Size = opportunity value. Professional" + ), + + # Recommendation Priority Matrix + ( + "23_recommendation_priority.png", + "scientific-schematics", + "2x2 priority matrix {topic} recommendations. X-axis Effort Low to High. " + "Y-axis Impact Low to High. Quadrants: Upper-left QUICK WINS green Do First, " + "Upper-right MAJOR PROJECTS blue Plan Carefully, Lower-left FILL-INS gray Do If Time, " + "Lower-right THANKLESS TASKS red Avoid. Plot 6-8 numbered recommendations. Professional" + ), + + # Implementation Timeline + ( + "24_implementation_timeline.png", + "scientific-schematics", + "Gantt chart implementation {topic} 24 months. Phase 1 Foundation months 1-6 dark blue. " + "Phase 2 Build months 4-12 medium blue. Phase 3 Scale months 10-18 teal. " + "Phase 4 Optimize months 16-24 light blue. Overlapping bars. " + "Key milestones as diamonds. Month markers X-axis. Professional" + ), + + # Milestone Tracker + ( + "25_milestone_tracker.png", + "scientific-schematics", + "Milestone tracker {topic} horizontal timeline 8-10 milestones. " + "Each shows date, name, status: Completed green check, In Progress yellow circle, " + "Upcoming gray circle. Group by phase. Phase labels above. " + "Connected timeline line. Professional" + ), + + # Financial Projections + ( + "26_financial_projections.png", + "scientific-schematics", + "Combined bar and line chart {topic} 5-year projections. Bar chart revenue " + "primary Y-axis dollars. Line chart growth rate secondary Y-axis percent. " + "Three scenarios: Conservative gray, Base Case blue, Optimistic green. " + "X-axis Year 1-5. Data labels. Legend. Title Financial Projections 5-Year. Professional" + ), + + # Scenario Analysis + ( + "27_scenario_analysis.png", + "scientific-schematics", + "Grouped bar chart {topic} scenario comparison. X-axis metrics: Revenue Y5, " + "EBITDA Y5, Market Share, ROI. Three bars per metric: Conservative gray, " + "Base Case blue, Optimistic green. Data labels. Legend. " + "Title Scenario Analysis Comparison. Professional clear labels" + ), +] + + +def get_script_path(tool: str) -> Path: + """Get the path to the appropriate generation script.""" + base_path = Path(__file__).parent.parent.parent # skills directory + + if tool == "scientific-schematics": + return base_path / "scientific-schematics" / "scripts" / "generate_schematic.py" + elif tool == "generate-image": + return base_path / "generate-image" / "scripts" / "generate_image.py" + else: + raise ValueError(f"Unknown tool: {tool}") + + +def generate_visual( + filename: str, + tool: str, + prompt: str, + output_dir: Path, + topic: str, + skip_existing: bool = False, + verbose: bool = False +) -> bool: + """Generate a single visual using the appropriate tool.""" + output_path = output_dir / filename + + # Skip if exists and skip_existing is True + if skip_existing and output_path.exists(): + if verbose: + print(f" [SKIP] {filename} already exists") + return True + + # Format prompt with topic + formatted_prompt = prompt.format(topic=topic) + + # Get script path + script_path = get_script_path(tool) + + if not script_path.exists(): + print(f" [ERROR] Script not found: {script_path}") + return False + + # Build command + if tool == "scientific-schematics": + cmd = [ + sys.executable, + str(script_path), + formatted_prompt, + "-o", str(output_path), + "--doc-type", "report" + ] + else: # generate-image + cmd = [ + sys.executable, + str(script_path), + formatted_prompt, + "--output", str(output_path) + ] + + if verbose: + print(f" [GEN] {filename}") + print(f" Tool: {tool}") + print(f" Prompt: {formatted_prompt[:80]}...") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=120 # 2 minute timeout per image + ) + + if result.returncode == 0: + if verbose: + print(f" [OK] {filename} generated successfully") + return True + else: + print(f" [ERROR] {filename} failed:") + if result.stderr: + print(f" {result.stderr[:200]}") + return False + + except subprocess.TimeoutExpired: + print(f" [TIMEOUT] {filename} generation timed out") + return False + except Exception as e: + print(f" [ERROR] {filename}: {str(e)}") + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Generate visuals for a market research report (default: 5-6 core visuals)" + ) + parser.add_argument( + "--topic", "-t", + required=True, + help="Market topic (e.g., 'Electric Vehicle Charging Infrastructure')" + ) + parser.add_argument( + "--output-dir", "-o", + default="figures", + help="Output directory for generated images (default: figures)" + ) + parser.add_argument( + "--all", "-a", + action="store_true", + help="Generate all 27 extended visuals (default: only core 5-6)" + ) + parser.add_argument( + "--skip-existing", "-s", + action="store_true", + help="Skip generation if file already exists" + ) + parser.add_argument( + "--verbose", "-v", + action="store_true", + help="Show detailed output" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be generated without actually generating" + ) + parser.add_argument( + "--only", + type=str, + help="Only generate visuals matching this pattern (e.g., '01_', 'porter')" + ) + + args = parser.parse_args() + + # Create output directory + output_dir = Path(args.output_dir) + if not args.dry_run: + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"\n{'='*60}") + print(f"Market Research Visual Generator") + print(f"{'='*60}") + print(f"Topic: {args.topic}") + print(f"Output Directory: {output_dir.absolute()}") + print(f"Mode: {'All Visuals (27)' if args.all else 'Core Visuals Only (5-6)'}") + print(f"Skip Existing: {args.skip_existing}") + print(f"{'='*60}\n") + + # Select visual set based on --all flag + if args.all: + visuals_to_generate = CORE_VISUALS + EXTENDED_VISUALS + print("Generating ALL visuals (core + extended)\n") + else: + visuals_to_generate = CORE_VISUALS + print("Generating CORE visuals only (use --all for extended set)\n") + + # Filter visuals if --only specified + if args.only: + pattern = args.only.lower() + visuals_to_generate = [ + v for v in VISUALS + if pattern in v[0].lower() or pattern in v[2].lower() + ] + print(f"Filtered to {len(visuals_to_generate)} visuals matching '{args.only}'\n") + + if args.dry_run: + print("DRY RUN - The following visuals would be generated:\n") + for filename, tool, prompt in visuals_to_generate: + formatted = prompt.format(topic=args.topic) + print(f" {filename}") + print(f" Tool: {tool}") + print(f" Prompt: {formatted[:60]}...") + print() + return + + # Generate all visuals + total = len(visuals_to_generate) + success = 0 + failed = 0 + skipped = 0 + + for i, (filename, tool, prompt) in enumerate(visuals_to_generate, 1): + print(f"\n[{i}/{total}] Generating {filename}...") + + result = generate_visual( + filename=filename, + tool=tool, + prompt=prompt, + output_dir=output_dir, + topic=args.topic, + skip_existing=args.skip_existing, + verbose=args.verbose + ) + + if result: + if args.skip_existing and (output_dir / filename).exists(): + skipped += 1 + else: + success += 1 + else: + failed += 1 + + # Print summary + print(f"\n{'='*60}") + print(f"Generation Complete") + print(f"{'='*60}") + print(f"Total: {total}") + print(f"Success: {success}") + print(f"Skipped: {skipped}") + print(f"Failed: {failed}") + print(f"{'='*60}") + + if failed > 0: + print(f"\nWARNING: {failed} visuals failed to generate.") + print("Check the output above for error details.") + print("You may need to generate failed visuals manually.") + + print(f"\nOutput directory: {output_dir.absolute()}") + + +if __name__ == "__main__": + main() diff --git a/skills/marketing-mode/README.md b/skills/marketing-mode/README.md new file mode 100755 index 0000000..aec80be --- /dev/null +++ b/skills/marketing-mode/README.md @@ -0,0 +1,49 @@ +# Marketing Mode + +📈 **Mark the Marketer** - Growth-obsessed marketing strategist + +## Activation + +```bash +clawdhub install marketing-mode +``` + +Then tell Clawdbot to switch modes. + +## Who is Mark? + +Mark is a growth-obsessed marketing strategist who lives for the next conversion. He speaks in marketing frameworks, funnels, and metrics. + +## Marketing Frameworks + +### AIDA (Attention → Interest → Desire → Action) +The classic marketing funnel. Use to structure messaging and content. + +### PAS (Problem → Agitation → Solution) +Identify the problem, agitate it (make it hurt), then provide the solution. + +### Hook Model (Trigger → Action → Variable Reward → Investment) +Build habit-forming products by designing compelling hooks. + +### Value Proposition Canvas +Match what customers need with what you offer. + +### Positioning Framework +Answer: Who is this for? What does it do? Why is it different? + +## Usage Examples + +Mark helps with: +- Writing better CTAs and hooks +- Positioning products and services +- Funnel optimization +- Messaging strategy +- A/B testing ideas +- Channel selection +- Conversion optimization + +## Catchphrases +- "What's the CTA?" +- "What's the hook?" +- "Who is this for?" +- "Make it specific. Make it memorable." diff --git a/skills/marketing-mode/SKILL.md b/skills/marketing-mode/SKILL.md new file mode 100755 index 0000000..34a340c --- /dev/null +++ b/skills/marketing-mode/SKILL.md @@ -0,0 +1,693 @@ +--- +name: marketing-mode +description: "Marketing Mode combines 23 comprehensive marketing skills covering strategy, psychology, content, SEO, conversion optimization, and paid growth. Use when users need marketing strategy, copywriting, SEO help, conversion optimization, paid advertising, or any marketing tactic." +metadata: + version: 1.0.0 + tags: ["marketing", "growth", "seo", "copywriting", "cro", "paid-ads", "strategy", "psychology", "launch", "pricing", "email", "social"] + clawdbot: + mode: + name: "Mark the Marketer" + role: "Growth & Marketing Strategist" + emoji: "📈" + personality: | + Mark is a growth-obsessed marketing strategist who lives for the next conversion. He speaks in marketing frameworks, funnels, and metrics. He's constantly analyzing messaging, positioning, and channels for maximum impact. Mark doesn't just "post content" - he builds systems that convert. + requires: + bins: ["node"] + npm: true + install: + - id: "skill-install" + kind: "skill" + source: "clawdhub" + slug: "marketing-mode" + label: "Activate Marketing Mode" +--- + +# Marketing Mode - Complete Marketing Knowledge Base + +You are a marketing strategist with expertise across 23 comprehensive marketing disciplines. Your goal is to help users find the right strategies, tactics, and frameworks for their specific situation, stage, and resources. + +## Mode Activation + +When users need marketing help, activate this mode. Ask clarifying questions about their product, audience, stage, budget, and goals. Then recommend specific skills and tactics from this knowledge base. + +--- + +# PART 1: MARKETING STRATEGY & FRAMEWORKS + +## Marketing Ideas (140+ Proven Approaches) + +### Content & SEO +- Easy Keyword Ranking +- SEO Audit +- Glossary Marketing +- Programmatic SEO +- Content Repurposing +- Proprietary Data Content +- Internal Linking +- Content Refreshing +- Knowledge Base SEO +- Parasite SEO + +### Competitor & Comparison +- Competitor Comparison Pages +- Marketing Jiu-Jitsu +- Competitive Ad Research + +### Free Tools & Engineering +- Side Projects as Marketing +- Engineering as Marketing +- Importers as Marketing +- Quiz Marketing +- Calculator Marketing +- Chrome Extensions +- Microsites +- Scanners +- Public APIs + +### Paid Advertising +- Podcast Advertising +- Pre-targeting Ads +- Facebook Ads +- Instagram Ads +- Twitter/X Ads +- LinkedIn Ads +- Reddit Ads +- Quora Ads +- Google Ads +- YouTube Ads +- Cross-Platform Retargeting +- Click-to-Messenger Ads + +### Social Media & Community +- Community Marketing +- Quora Marketing +- Reddit Keyword Research +- Reddit Marketing +- LinkedIn Audience +- Instagram Audience +- X Audience +- Short Form Video +- Engagement Pods +- Comment Marketing + +### Email Marketing +- Mistake Email Marketing +- Reactivation Emails +- Founder Welcome Email +- Dynamic Email Capture +- Monthly Newsletters +- Inbox Placement +- Onboarding Emails +- Win-back Emails +- Trial Reactivation + +### Partnerships & Programs +- Affiliate Discovery Through Backlinks +- Influencer Whitelisting +- Reseller Programs +- Expert Networks +- Newsletter Swaps +- Article Quotes +- Pixel Sharing +- Shared Slack Channels +- Affiliate Program +- Integration Marketing +- Community Sponsorship + +### Events & Speaking +- Live Webinars +- Virtual Summits +- Roadshows +- Local Meetups +- Meetup Sponsorship +- Conference Speaking +- Conferences +- Conference Sponsorship + +### PR & Media +- Media Acquisitions as Marketing +- Press Coverage +- Fundraising PR +- Documentaries + +### Launches & Promotions +- Black Friday Promotions +- Product Hunt Launch +- Early-Access Referrals +- New Year Promotions +- Early Access Pricing +- Product Hunt Alternatives +- Twitter Giveaways +- Giveaways +- Vacation Giveaways +- Lifetime Deals + +### Product-Led Growth +- Powered By Marketing +- Free Migrations +- Contract Buyouts +- One-Click Registration +- In-App Upsells +- Newsletter Referrals +- Viral Loops +- Offboarding Flows +- Concierge Setup +- Onboarding Optimization + +### Unconventional & Creative +- Awards as Marketing +- Challenges as Marketing +- Reality TV Marketing +- Controversy as Marketing +- Moneyball Marketing +- Curation as Marketing +- Grants as Marketing +- Product Competitions +- Cameo Marketing +- OOH Advertising +- Marketing Stunts +- Guerrilla Marketing +- Humor Marketing + +### Platforms & Marketplaces +- Open Source as Marketing +- App Store Optimization +- App Marketplaces +- YouTube Reviews +- YouTube Channel +- Source Platforms +- Review Sites +- Live Audio +- International Expansion +- Price Localization + +### Developer & Technical +- Investor Marketing +- Certifications +- Support as Marketing +- Developer Relations + +### Audience-Specific +- Two-Sided Referrals +- Podcast Tours +- Customer Language + +--- + +## Launch Strategy (5-Phase Framework) + +### Phase 1: Internal (Pre-Launch) +- Use product internally first +- Find bugs in real use cases +- Build initial case studies +- Create launch content +- Set up analytics and tracking + +### Phase 2: Alpha (Private Beta) +- Invite existing customers and warm leads +- Get feedback and testimonials +- Refine positioning based on response +- Build waitlist + +### Phase 3: Beta (Public Preview) +- Broader access with invite codes +- Collect more testimonials +- Refine pricing and packaging +- Build SEO content + +### Phase 4: Early Access (Launch Prep) +- Public waitlist opening +- Special launch pricing +- Affiliate/partner outreach +- Press and analyst outreach + +### Phase 5: Full Launch +- General availability +- Full promotional push +- Customer success stories +- Ongoing optimization + +--- + +## Pricing Strategy + +### Research Methods +- Competitor pricing analysis +- Value-based pricing models +- Willingness-to-pay surveys +- A/B testing for optimization + +### Tier Structure +- Free tier (awareness) +- Pro tier (core value) +- Enterprise tier (scale + support) + +### Value Metrics +- Per-seat pricing +- Usage-based pricing +- Feature-based tiers +- Outcome-based pricing + +### Monetization Optimization +- Annual vs. monthly discounts +- Upgrade paths +- Churn prevention pricing +- Revenue recovery + +--- + +# PART 2: PSYCHOLOGY & MENTAL MODELS + +## Foundational Thinking Models + +### First Principles +Break problems down to basic truths. Don't copy competitors—ask "why" repeatedly to find root causes. + +**Marketing application**: Don't do content marketing because competitors do. Ask why, what problem it solves, if there's a better solution. + +### Jobs to Be Done (JTBD) +People "hire" products to get a job done. Focus on outcomes, not features. + +**Marketing application**: A drill buyer wants a hole, not a drill. Frame around the job accomplished. + +### Circle of Competence +Know what you're good at and stay within it. Double down on genuine expertise. + +**Marketing application**: Don't chase every channel. Focus on where you have real competitive advantage. + +### Inversion +Ask what would guarantee failure, then avoid those things. + +**Marketing application**: List everything that would make a campaign fail, then systematically prevent each. + +### Occam's Razor +Simpler explanations are usually correct. Avoid overcomplicating strategies. + +**Marketing application**: If conversions dropped, check obvious first (broken form, slow page) before complex attribution. + +### Pareto Principle (80/20) +80% of results come from 20% of efforts. Find and focus on the vital few. + +**Marketing application**: Find channels driving most results. Cut or reduce the rest. + +### Hick's Law +Decision time increases with options. More choices = more abandonment. + +**Marketing application**: One clear CTA beats three. Fewer form fields = higher conversion. + +### AIDA Funnel +Attention → Interest → Desire → Action + +**Marketing application**: Structure pages to move through each stage. Capture attention before building desire. + +### Law of Diminishing Returns +After a point, additional investment yields progressively smaller gains. + +**Marketing application**: The 10th blog post won't have the same impact as the first. Diversify channels. + +### Commitment & Consistency +Once people commit to something, they want to stay consistent. + +**Marketing application**: Small commitments first (email signup) lead to larger ones (paid subscription). + +### Reciprocity Principle +Give first. People feel obligated to return favors. + +**Marketing application**: Free content, tools, and freemium models create reciprocal obligation. + +### Scarcity & Urgency +Limited availability increases perceived value. + +**Marketing application**: Limited-time offers, low-stock warnings. Only use when genuine. + +### Loss Aversion +Losses feel twice as painful as equivalent gains feel good. + +**Marketing application**: "Don't miss out" beats "You could gain." Frame in terms of what they'll lose. + +### Anchoring Effect +First number heavily influences subsequent judgments. + +**Marketing application**: Show higher price first (original, competitor, enterprise) to anchor expectations. + +### Paradox of Choice +Too many options overwhelm. Fewer choices lead to more decisions. + +**Marketing application**: Three pricing tiers. Recommend a single "best for most" option. + +### Endowment Effect +People value things more once they own them. + +**Marketing application**: Free trials, samples, freemium models let customers "own" the product. + +### IKEA Effect +People value things they put effort into creating. + +**Marketing application**: Let customers customize, configure, build. Their investment increases commitment. + +### Mere Exposure Effect +Familiarity breeds liking. Consistent presence builds preference. + +**Marketing application**: Repetition across channels creates comfort and trust. + +### Social Proof / Bandwagon Effect +People follow what others are doing. Popularity signals quality. + +**Marketing application**: Show customer counts, testimonials, "trending" indicators. + +### Prospect Theory / Loss Aversion +People avoid actions that might cause regret. + +**Marketing application**: Money-back guarantees, free trials reduce regret fear. Address concerns directly. + +### Zeigarnik Effect +Unfinished tasks occupy the mind. Open loops create tension. + +**Marketing application**: "You're 80% done" creates pull to finish. Incomplete profiles, abandoned carts. + +### Status-Quo Bias +People prefer current state. Change feels risky. + +**Marketing application**: Reduce friction. Make transition feel safe. "Import in one click." + +### Default Effect +People accept pre-selected options. Defaults are powerful. + +**Marketing application**: Pre-select the plan you want customers to choose. Opt-out beats opt-in. + +### Peak-End Rule +People judge experiences by the peak (best/worst) and end, not average. + +**Marketing application**: Design memorable peaks and strong endings. Thank you pages matter. + +--- + +# PART 3: SEO & CONTENT + +## SEO Audit Framework + +### Priority Order +1. **Crawlability & Indexation** - Can Google find and index pages? +2. **Technical Foundations** - Is the site fast and functional? +3. **On-Page Optimization** - Is content optimized? +4. **Content Quality** - Does it deserve to rank? +5. **Authority & Links** - Does it have credibility? + +### Technical SEO Checklist + +**Crawlability** +- Robots.txt not blocking important pages +- XML sitemap accessible and updated +- Site architecture within 3 clicks of homepage +- No orphan pages + +**Indexation** +- No accidental noindex on important pages +- Proper canonical tags (self-referencing) +- No redirect chains +- No soft 404s + +**Core Web Vitals** +- LCP < 2.5s +- INP < 200ms +- CLS < 0.1 +- Server response time optimized +- Images optimized + +**On-Page** +- Title tags optimized (60 chars, keyword placement) +- Meta descriptions compelling (155 chars) +- Header hierarchy (H1 → H2 → H3) +- Internal linking to priority pages + +**E-E-A-T** +- Author expertise demonstrated +- Clear sourcing and citations +- Regular content updates +- Accurate, comprehensive information + +--- + +## Programmatic SEO (12 Playbooks) + +1. **Location Pages** - City + keyword targeting +2. **Comparison Pages** - Product + alternative/competitor +3. **Integration Pages** - Tool + integration targets +4. **Use Case Pages** - Solution + use case +5. **Problem Pages** - Pain point + solution +6. **Industry Pages** - Industry + keyword targets +7. **Review/Alternatives Pages** - Competitor alternatives +8. **Calculator/Generator Pages** - Tools with keyword targets +9. **Template Pages** - Document templates for keywords +10. **Glossary Pages** - Industry terms explained +11. **Checklist Pages** - How-to guides as checklists +12. **Quiz/Assessment Pages** - Interactive tools + +--- + +## Schema Markup + +- Organization schema +- Product/Service schema +- FAQPage schema +- HowTo schema +- Review/BreadcrumbList schema +- LocalBusiness schema + +--- + +## Copywriting Frameworks + +### AIDA +Attention → Interest → Desire → Action + +### PAS +Problem → Agitation → Solution + +### Before/After/Bridge +Current state → Problem → Your solution → Transformation + +### ACCA +Awareness → Comprehension → Conviction → Action + +### Hero's Journey +Customer as hero on a journey with your product as guide + +--- + +## Copy Editing (7 Sweeps) + +1. **Clarity Sweep** +2. **Voice Sweep** +3. **Proof Sweep** +4. **Impact Sweep** +5. **Emotion Sweep** +6. **Format Sweep** +7. **Authenticity Sweep** + +--- + +## Social Content Strategy + +### Hook Templates +- Question hooks +- Number hooks +- Story hooks +- Contrast hooks +- Controversy hooks + +### Platform Optimization +- LinkedIn: Professional, thought leadership +- X/Twitter: Bite-sized, threads +- Instagram: Visual + captions +- TikTok/Reels: Entertainment + education +- YouTube: Long-form + shorts + +--- + +# PART 4: CONVERSION OPTIMIZATION (CRO) + +## Page CRO Elements + +1. **Value Proposition** + - Clear headline (8-12 words) + - Subhead explaining transformation + - Visual proof (screenshot/video) + +2. **Trust Signals** + - Logos of customers/press + - Testimonials + - Security badges + - Social proof numbers + +3. **CTA Optimization** + - Action-oriented (not "Submit") + - Contrast with page + - Above fold placement + +4. **Friction Analysis** + - Remove unnecessary form fields + - Auto-fill where possible + - Clear error messages + +--- + +## Funnel Optimization + +### Signup Flow +- Minimize fields (email only first) +- Social auth options +- Progress indicators +- Clear value proposition + +### Form CRO +- Progressive profiling +- Inline validation +- Smart defaults +- Auto-save drafts + +### Onboarding +- Aha moment identification +- Progress tracking +- Feature discovery +- Milestone celebrations + +### A/B Test Setup +- Hypothesis framework +- Sample size calculations +- Statistical significance (95%+ confidence) +- Test one variable at a time + +--- + +# PART 5: PAID ADVERTISING & GROWTH + +## Channel Strategy + +### Google Ads +- Brand terms protection +- Competitor targeting +- Solution keywords +- Remarketing lists + +### Meta/Facebook Ads +- Detailed targeting +- Creative testing +- Lookalike audiences +- Retargeting + +### LinkedIn Ads +- Job titles/functions +- Company size targeting +- Industry filters +- B2B intent + +### Analytics & Tracking +- UTM parameters (consistent naming) +- GA4 events for goals +- GTM container setup +- Conversion tracking pixels + +--- + +## Referral Program Design + +### Viral Mechanics +- Two-sided rewards +- Milestone celebrations +- Fraud detection rules +- Nurture sequences for referred users + +--- + +## Free Tool Strategy + +### Tool Categories +- Calculators +- Analyzers +- Generators +- Checklists +- Templates + +### SEO Value +- Keyword targeting +- Backlink attraction +- Shareable results + +--- + +# PART 6: EMAIL MARKETING + +## Sequence Types + +1. **Welcome Series** - First 7 days +2. **Nurture Sequence** - Build interest over 2-3 weeks +3. **Onboarding Sequence** - Product education +4. **Win-Back/Reactivation** - Churned users +5. **Re-engagement** - Dormant subscribers + +--- + +# QUICK REFERENCE + +## Marketing Challenges → Relevant Frameworks + +| Challenge | Start Here | +|-----------|------------| +| Low conversions | AIDA, Hick's Law, BJ Fogg | +| Pricing objections | Anchoring, Mental Accounting, Loss Aversion | +| SEO issues | Technical SEO audit, Programmatic SEO | +| Copy not converting | PAS, Copy editing sweeps, A/B tests | +| Email performance | Welcome series, Segmentation, Send time optimization | +| No traffic | SEO audit, Content strategy, Programmatic SEO | +| High churn | Onboarding CRO, Win-back sequences | +| Low engagement | Social proof, Reciprocity, Consistency | +| Unclear messaging | Value proposition, Positioning, Differentiation | + +--- + +## Questions to Ask (Marketing Discovery) + +**About Product & Audience** +- What's your product and who's the target customer? +- What's your current stage (pre-launch → scale)? +- What are your main marketing goals? +- What's your budget and team size? + +**About Current State** +- What have you tried that worked or didn't? +- What are your competitors doing well? +- Where are you losing customers in the funnel? + +**About Goals** +- What metrics matter most (traffic, leads, revenue)? +- What's your timeline? +- What's your competitive advantage? + +--- + +## Related Skills + +- **marketing-ideas**: 140+ tactical marketing ideas +- **marketing-psychology**: 70+ mental models for persuasion +- **launch-strategy**: 5-phase launch framework +- **pricing-strategy**: Research and optimization methods +- **seo-audit**: Technical and on-page SEO diagnosis +- **programmatic-seo**: Building pages at scale +- **schema-markup**: Structured data implementation +- **competitor-alternatives**: Comparison page strategy +- **copywriting**: Framework-driven copy +- **copy-editing**: 7-sweep improvement process +- **social-content**: Platform-specific strategies +- **email-sequence**: Campaign types and templates +- **page-cro**: Landing page optimization +- **signup-flow-cro**: Form and signup optimization +- **form-cro**: Lead capture and conversion +- **onboarding-cro**: Activation and retention +- **paywall-cro**: Premium content strategy +- **popup-cro**: Trigger-based conversion +- **ab-test-setup**: Statistical rigor in testing +- **paid-ads**: Channel-specific strategies +- **analytics-tracking**: Measurement infrastructure +- **referral-program**: Viral loop design +- **free-tool-strategy**: Lead generation through tools diff --git a/skills/marketing-mode/_meta.json b/skills/marketing-mode/_meta.json new file mode 100755 index 0000000..0015744 --- /dev/null +++ b/skills/marketing-mode/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn72ce44tqw8bnnnewrn1s5x3s7yz7sq", + "slug": "marketing-mode", + "version": "1.0.0", + "publishedAt": 1769016558544 +} \ No newline at end of file diff --git a/skills/marketing-mode/mode-prompt.md b/skills/marketing-mode/mode-prompt.md new file mode 100755 index 0000000..5a392fd --- /dev/null +++ b/skills/marketing-mode/mode-prompt.md @@ -0,0 +1,39 @@ +# Mark the Marketer - Mode Prompt + +You are Mark the Marketer, a growth-obsessed marketing strategist. + +## Your Core Identity +- You live for the next conversion, the better hook, the tighter positioning +- You think in funnels: Awareness → Interest → Desire → Action +- You analyze everything through a marketing lens: "What's the CTA?", "Is this positioned right?", "Does this hook?" +- You use frameworks like AIDA (Attention, Interest, Desire, Action), PAS (Problem, Agitation, Solution), and the Hook Model +- You're energetic, data-driven, and always asking: "What's the metric that matters?" + +## Marketing Frameworks You Use +- **AIDA**: Attention → Interest → Desire → Action +- **PAS**: Problem → Agitation → Solution +- **Hook Model**: Trigger → Action → Variable Reward → Investment +- **Value Proposition Canvas**: Customer Jobs → Pains → Gains → Value Prop +- **Positioning**: Who it's for, what it does, why it's different + +## How You Communicate +- Use marketing terminology naturally (CTAs, conversion, positioning, hook, etc.) +- Ask about metrics and results +- Suggest frameworks for solving problems +- Be energetic and growth-focused +- Challenge vague messaging: "What does it actually do for the customer?" + +## Your Catchphrases +- "What's the CTA?" +- "What's the hook?" +- "Who is this for?" +- "What's the one thing they should remember?" +- "Make it specific. Make it memorable." + +## Mode Activation +When users want marketing help, switch to this mode. Ask clarifying questions about: +- Target audience +- Desired action/conversion +- Current messaging/positioning +- Channels being used +- Metrics being tracked diff --git a/skills/marketing-mode/skill.json b/skills/marketing-mode/skill.json new file mode 100755 index 0000000..d667d06 --- /dev/null +++ b/skills/marketing-mode/skill.json @@ -0,0 +1,51 @@ +{ + "name": "marketing-mode", + "description": "Marketing and growth strategies for founders. Focuses on messaging, positioning, funnel optimization, and growth tactics.", + "version": "1.0.0", + "tags": ["marketing", "growth", "founder", "positioning", "messaging", "funnel"], + "author": "Seth Rose", + "license": "MIT", + "repository": "https://github.com/TheSethRose/clawdbot-skills", + "documentation": "SKILL.md", + "keywords": [ + "marketing", + "growth", + "founder", + "positioning", + "messaging", + "funnel", + "conversion", + "cta", + "hooks" + ], + "engines": { + "node": ">=18.0.0" + }, + "install": { + "npm": "npm install -g @thesethrose/marketing-mode" + }, + "usage": { + "cli": "clawdhub install marketing-mode" + }, + "clawdbot": { + "requires": { + "node": true, + "npm": true + }, + "mode": { + "name": "Mark the Marketer", + "role": "Marketing Strategist", + "emoji": "📈", + "personality": "Growth-obsessed marketing strategist focused on funnels, positioning, and conversion.", + "system_prompt_path": "mode-prompt.md" + }, + "install": [ + { + "id": "npm-pkg", + "kind": "npm", + "package": "@thesethrose/marketing-mode", + "label": "Install Marketing Mode (npm)" + } + ] + } +} diff --git a/skills/mindfulness-meditation/SKILL.md b/skills/mindfulness-meditation/SKILL.md new file mode 100755 index 0000000..a0b01dd --- /dev/null +++ b/skills/mindfulness-meditation/SKILL.md @@ -0,0 +1,65 @@ +--- +name: mindfulness-meditation +description: Build a meditation practice with guided sessions, streaks, and mindfulness reminders +author: clawd-team +version: 1.0.0 +triggers: + - "meditate now" + - "mindfulness practice" + - "guided meditation" + - "meditation streak" + - "be present" +--- + +# Mindfulness & Meditation + +Build a consistent meditation practice with guided sessions, progress tracking, and daily mindfulness reminders. + +## What it does + +This skill transforms your device into a personal meditation coach. It guides you through structured meditation sessions, tracks your practice streaks, logs sessions for long-term insights, and sends mindfulness reminders to keep you anchored throughout your day. + +## Usage + +### Start Meditation +Initiate a guided meditation session. Choose your meditation type and duration, then follow along with step-by-step guidance. + +### Quick Mindfulness +Take a 2-5 minute breathing pause. Perfect for stressful moments or transitions between tasks. No commitment, just presence. + +### Check Streak +View your current meditation streak and session history. See weekly/monthly breakdowns of your practice consistency and total minutes logged. + +### Set Reminders +Configure daily or custom mindfulness reminders. Get gentle notifications to pause, breathe, and check in with yourself. + +### Session Log +Review detailed logs of past sessions: type, duration, date, and personal notes. Export your practice data for reflection or sharing. + +## Meditation Types + +**Body Scan** — Systematically observe sensations from head to toe, releasing tension and building bodily awareness. + +**Breath Focus** — Anchor attention to the natural rhythm of your breath. Redirect your mind gently when it wanders. + +**Loving-Kindness** — Cultivate compassion by sending well-wishes to yourself and others in expanding circles. + +**Walking** — Meditate while moving. Synchronize breath with steps and notice your surroundings with full attention. + +**Open Awareness** — Observe thoughts and sensations without judgment. Develop witness consciousness and mental spaciousness. + +## Session Lengths + +- **2 min** — Micro-practice. Reset focus in the middle of your day. +- **5 min** — Short sits. Build the habit without time friction. +- **10 min** — Standard practice. Enough depth to settle your mind. +- **20 min** — Deep work. Move beyond the surface chatter. +- **Custom** — Set your own duration. Practice at your pace. + +## Tips + +- **Start small:** 2-3 minutes daily beats sporadic hour-long sessions. Consistency compounds over time. +- **Pick one type:** Master breath focus before exploring other techniques. Foundation first. +- **Meditate at the same time:** Morning sits anchor your day. Neural pathways strengthen with repetition. +- **Don't aim for blank mind:** Thoughts are normal. The skill is noticing them without judgment—that's the practice. +- **All data stays local on your machine:** Your meditation history, preferences, and reminders are stored securely on your device. Nothing leaves your control. diff --git a/skills/mindfulness-meditation/_meta.json b/skills/mindfulness-meditation/_meta.json new file mode 100755 index 0000000..38c143b --- /dev/null +++ b/skills/mindfulness-meditation/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7dsqp497235e9hhzdwd0q9a57zxjw6", + "slug": "mindfulness-meditation", + "version": "1.0.0", + "publishedAt": 1769326483710 +} \ No newline at end of file diff --git a/skills/multi-search-engine/CHANGELOG.md b/skills/multi-search-engine/CHANGELOG.md new file mode 100755 index 0000000..f5d10e3 --- /dev/null +++ b/skills/multi-search-engine/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## v2.0.1 (2026-02-06) +- Simplified documentation +- Removed gov-related content +- Optimized for ClawHub publishing + +## v2.0.0 (2026-02-06) +- Added 9 international search engines +- Enhanced advanced search capabilities +- Added DuckDuckGo Bangs support +- Added WolframAlpha knowledge queries + +## v1.0.0 (2026-02-04) +- Initial release with 8 domestic search engines diff --git a/skills/multi-search-engine/CHANNELLOG.md b/skills/multi-search-engine/CHANNELLOG.md new file mode 100755 index 0000000..74bec12 --- /dev/null +++ b/skills/multi-search-engine/CHANNELLOG.md @@ -0,0 +1,48 @@ +# Multi Search Engine + +## 基本信息 + +- **名称**: multi-search-engine +- **版本**: v2.0.1 +- **描述**: 集成17个搜索引擎(8国内+9国际),支持高级搜索语法 +- **发布时间**: 2026-02-06 + +## 搜索引擎 + +**国内(8个)**: 百度、必应、360、搜狗、微信、头条、集思录 +**国际(9个)**: Google、DuckDuckGo、Yahoo、Brave、Startpage、Ecosia、Qwant、WolframAlpha + +## 核心功能 + +- 高级搜索操作符(site:, filetype:, intitle:等) +- DuckDuckGo Bangs快捷命令 +- 时间筛选(小时/天/周/月/年) +- 隐私保护搜索 +- WolframAlpha知识计算 + +## 更新记录 + +### v2.0.1 (2026-02-06) +- 精简文档,优化发布 + +### v2.0.0 (2026-02-06) +- 新增9个国际搜索引擎 +- 强化深度搜索能力 + +### v1.0.0 (2026-02-04) +- 初始版本:8个国内搜索引擎 + +## 使用示例 + +```javascript +// Google搜索 +web_fetch({"url": "https://www.google.com/search?q=python"}) + +// 隐私搜索 +web_fetch({"url": "https://duckduckgo.com/html/?q=privacy"}) + +// 站内搜索 +web_fetch({"url": "https://www.google.com/search?q=site:github.com+python"}) +``` + +MIT License diff --git a/skills/multi-search-engine/SKILL.md b/skills/multi-search-engine/SKILL.md new file mode 100755 index 0000000..56c27a2 --- /dev/null +++ b/skills/multi-search-engine/SKILL.md @@ -0,0 +1,78 @@ +--- +name: "multi-search-engine" +description: "Multi search engine integration with 8 domestic (CN) search engines. Supports advanced search operators, time filters, site search, and WeChat article search. No API keys required." +--- + +# Multi Search Engine v2.0.1 + +Integration of 8 domestic Chinese search engines for web crawling without API keys. + +## Search Engines (Domestic - CN Only) + +- **Baidu**: `https://www.baidu.com/s?wd={keyword}` +- **Bing CN**: `https://cn.bing.com/search?q={keyword}&ensearch=0` +- **Bing INT**: `https://cn.bing.com/search?q={keyword}&ensearch=1` +- **360**: `https://www.so.com/s?q={keyword}` +- **Sogou**: `https://sogou.com/web?query={keyword}` +- **WeChat**: `https://wx.sogou.com/weixin?type=2&query={keyword}` +- **Toutiao**: `https://so.toutiao.com/search?keyword={keyword}` +- **Jisilu**: `https://www.jisilu.cn/explore/?keyword={keyword}` + +## Quick Examples + +```javascript +// Basic search (Baidu) +web_fetch({"url": "https://www.baidu.com/s?wd=python+tutorial"}) + +// Site-specific (Bing CN) +web_fetch({"url": "https://cn.bing.com/search?q=site:github.com+react&ensearch=0"}) + +// File type (Baidu) +web_fetch({"url": "https://www.baidu.com/s?wd=machine+learning+filetype:pdf"}) + +// WeChat article search +web_fetch({"url": "https://wx.sogou.com/weixin?type=2&query=人工智能+最新进展"}) + +// Toutiao search +web_fetch({"url": "https://so.toutiao.com/search?keyword=新能源+政策"}) + +// Jisilu financial data +web_fetch({"url": "https://www.jisilu.cn/explore/?keyword=REITs"}) +``` + +## Advanced Operators + +| Operator | Example | Description | +|----------|---------|-------------| +| `site:` | `site:github.com python` | Search within site | +| `filetype:` | `filetype:pdf report` | Specific file type | +| `""` | `"machine learning"` | Exact match | +| `-` | `python -snake` | Exclude term | +| `OR` | `cat OR dog` | Either term | + +## Time Filters + +| Parameter | Description | +|-----------|-------------| +| `tbs=qdr:h` | Past hour | +| `tbs=qdr:d` | Past day | +| `tbs=qdr:w` | Past week | +| `tbs=qdr:m` | Past month | +| `tbs=qdr:y` | Past year | + +## Search Engine Notes + +- **WeChat Search**: Best for searching WeChat public articles and content +- **Toutiao**: Good for trending topics and news aggregation +- **Jisilu**: Focused on financial and investment data +- **Bing INT**: International search results via Bing interface +- **Bing CN**: Localized Chinese search results + +## Documentation + +- `references/international-search.md` - Archived international search guide (for reference) +- `CHANGELOG.md` - Version history + +## License + +MIT diff --git a/skills/multi-search-engine/_meta.json b/skills/multi-search-engine/_meta.json new file mode 100755 index 0000000..0c19f52 --- /dev/null +++ b/skills/multi-search-engine/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn79j8kk7fb9w10jh83803j7f180a44m", + "slug": "multi-search-engine", + "version": "2.0.1", + "publishedAt": 1770313848158 +} \ No newline at end of file diff --git a/skills/multi-search-engine/config.json b/skills/multi-search-engine/config.json new file mode 100755 index 0000000..c6d32cb --- /dev/null +++ b/skills/multi-search-engine/config.json @@ -0,0 +1,14 @@ +{ + "name": "multi-search-engine", + "description": "Multi search engine integration with 8 domestic (CN) search engines", + "engines": [ + {"name": "Baidu", "url": "https://www.baidu.com/s?wd={keyword}", "region": "cn"}, + {"name": "Bing CN", "url": "https://cn.bing.com/search?q={keyword}&ensearch=0", "region": "cn"}, + {"name": "Bing INT", "url": "https://cn.bing.com/search?q={keyword}&ensearch=1", "region": "cn"}, + {"name": "360", "url": "https://www.so.com/s?q={keyword}", "region": "cn"}, + {"name": "Sogou", "url": "https://sogou.com/web?query={keyword}", "region": "cn"}, + {"name": "WeChat", "url": "https://wx.sogou.com/weixin?type=2&query={keyword}", "region": "cn"}, + {"name": "Toutiao", "url": "https://so.toutiao.com/search?keyword={keyword}", "region": "cn"}, + {"name": "Jisilu", "url": "https://www.jisilu.cn/explore/?keyword={keyword}", "region": "cn"} + ] +} diff --git a/skills/multi-search-engine/metadata.json b/skills/multi-search-engine/metadata.json new file mode 100755 index 0000000..91be4f7 --- /dev/null +++ b/skills/multi-search-engine/metadata.json @@ -0,0 +1,7 @@ +{ + "name": "multi-search-engine", + "version": "2.0.1", + "description": "Multi search engine with 17 engines (8 CN + 9 Global). Supports advanced operators, time filters, privacy engines.", + "engines": 17, + "requires_api_key": false +} diff --git a/skills/multi-search-engine/references/international-search.md b/skills/multi-search-engine/references/international-search.md new file mode 100755 index 0000000..b797b93 --- /dev/null +++ b/skills/multi-search-engine/references/international-search.md @@ -0,0 +1,651 @@ +# 国际搜索引擎深度搜索指南 + +## 🔍 Google 深度搜索 + +### 1.1 基础高级搜索操作符 + +| 操作符 | 功能 | 示例 | URL | +|--------|------|------|-----| +| `""` | 精确匹配 | `"machine learning"` | `https://www.google.com/search?q=%22machine+learning%22` | +| `-` | 排除关键词 | `python -snake` | `https://www.google.com/search?q=python+-snake` | +| `OR` | 或运算 | `machine learning OR deep learning` | `https://www.google.com/search?q=machine+learning+OR+deep+learning` | +| `*` | 通配符 | `machine * algorithms` | `https://www.google.com/search?q=machine+*+algorithms` | +| `()` | 分组 | `(apple OR microsoft) phones` | `https://www.google.com/search?q=(apple+OR+microsoft)+phones` | +| `..` | 数字范围 | `laptop $500..$1000` | `https://www.google.com/search?q=laptop+%24500..%241000` | + +### 1.2 站点与文件搜索 + +| 操作符 | 功能 | 示例 | +|--------|------|------| +| `site:` | 站内搜索 | `site:github.com python projects` | +| `filetype:` | 文件类型 | `filetype:pdf annual report` | +| `inurl:` | URL包含 | `inurl:login admin` | +| `intitle:` | 标题包含 | `intitle:"index of" mp3` | +| `intext:` | 正文包含 | `intext:password filetype:txt` | +| `cache:` | 查看缓存 | `cache:example.com` | +| `related:` | 相关网站 | `related:github.com` | +| `info:` | 网站信息 | `info:example.com` | + +### 1.3 时间筛选参数 + +| 参数 | 含义 | URL示例 | +|------|------|---------| +| `tbs=qdr:h` | 过去1小时 | `https://www.google.com/search?q=news&tbs=qdr:h` | +| `tbs=qdr:d` | 过去24小时 | `https://www.google.com/search?q=news&tbs=qdr:d` | +| `tbs=qdr:w` | 过去1周 | `https://www.google.com/search?q=news&tbs=qdr:w` | +| `tbs=qdr:m` | 过去1月 | `https://www.google.com/search?q=news&tbs=qdr:m` | +| `tbs=qdr:y` | 过去1年 | `https://www.google.com/search?q=news&tbs=qdr:y` | +| `tbs=cdr:1,cd_min:1/1/2024,cd_max:12/31/2024` | 自定义日期范围 | 2024年全年 | + +### 1.4 语言和地区筛选 + +| 参数 | 功能 | 示例 | +|------|------|------| +| `hl=en` | 界面语言 | `https://www.google.com/search?q=test&hl=en` | +| `lr=lang_zh-CN` | 搜索结果语言 | `https://www.google.com/search?q=test&lr=lang_zh-CN` | +| `cr=countryCN` | 国家/地区 | `https://www.google.com/search?q=test&cr=countryCN` | +| `gl=us` | 地理位置 | `https://www.google.com/search?q=test&gl=us` | + +### 1.5 特殊搜索类型 + +| 类型 | URL | 说明 | +|------|-----|------| +| 图片搜索 | `https://www.google.com/search?q={keyword}&tbm=isch` | `tbm=isch` 表示图片 | +| 新闻搜索 | `https://www.google.com/search?q={keyword}&tbm=nws` | `tbm=nws` 表示新闻 | +| 视频搜索 | `https://www.google.com/search?q={keyword}&tbm=vid` | `tbm=vid` 表示视频 | +| 地图搜索 | `https://www.google.com/search?q={keyword}&tbm=map` | `tbm=map` 表示地图 | +| 购物搜索 | `https://www.google.com/search?q={keyword}&tbm=shop` | `tbm=shop` 表示购物 | +| 图书搜索 | `https://www.google.com/search?q={keyword}&tbm=bks` | `tbm=bks` 表示图书 | +| 学术搜索 | `https://scholar.google.com/scholar?q={keyword}` | Google Scholar | + +### 1.6 Google 深度搜索示例 + +```javascript +// 1. 搜索GitHub上的Python机器学习项目 +web_fetch({"url": "https://www.google.com/search?q=site:github.com+python+machine+learning"}) + +// 2. 搜索2024年的PDF格式机器学习教程 +web_fetch({"url": "https://www.google.com/search?q=machine+learning+tutorial+filetype:pdf&tbs=cdr:1,cd_min:1/1/2024"}) + +// 3. 搜索标题包含"tutorial"的Python相关页面 +web_fetch({"url": "https://www.google.com/search?q=intitle:tutorial+python"}) + +// 4. 搜索过去一周的新闻 +web_fetch({"url": "https://www.google.com/search?q=AI+breakthrough&tbs=qdr:w&tbm=nws"}) + +// 5. 搜索中文内容(界面英文,结果中文) +web_fetch({"url": "https://www.google.com/search?q=人工智能&lr=lang_zh-CN&hl=en"}) + +// 6. 搜索特定价格范围的笔记本电脑 +web_fetch({"url": "https://www.google.com/search?q=laptop+%241000..%242000+best+rating"}) + +// 7. 搜索排除Wikipedia的结果 +web_fetch({"url": "https://www.google.com/search?q=python+programming+-wikipedia"}) + +// 8. 搜索学术文献 +web_fetch({"url": "https://scholar.google.com/scholar?q=deep+learning+optimization"}) + +// 9. 搜索缓存页面(查看已删除内容) +web_fetch({"url": "https://webcache.googleusercontent.com/search?q=cache:example.com"}) + +// 10. 搜索相关网站 +web_fetch({"url": "https://www.google.com/search?q=related:stackoverflow.com"}) +``` + +--- + +## 🦆 DuckDuckGo 深度搜索 + +### 2.1 DuckDuckGo 特色功能 + +| 功能 | 语法 | 示例 | +|------|------|------| +| **Bangs 快捷** | `!缩写` | `!g python` → Google搜索 | +| **密码生成** | `password` | `https://duckduckgo.com/?q=password+20` | +| **颜色转换** | `color` | `https://duckduckgo.com/?q=+%23FF5733` | +| **短链接** | `shorten` | `https://duckduckgo.com/?q=shorten+example.com` | +| **二维码生成** | `qr` | `https://duckduckgo.com/?q=qr+hello+world` | +| **生成UUID** | `uuid` | `https://duckduckgo.com/?q=uuid` | +| **Base64编解码** | `base64` | `https://duckduckgo.com/?q=base64+hello` | + +### 2.2 DuckDuckGo Bangs 完整列表 + +#### 搜索引擎 + +| Bang | 跳转目标 | 示例 | +|------|---------|------| +| `!g` | Google | `!g python tutorial` | +| `!b` | Bing | `!b weather` | +| `!y` | Yahoo | `!y finance` | +| `!sp` | Startpage | `!sp privacy` | +| `!brave` | Brave Search | `!brave tech` | + +#### 编程开发 + +| Bang | 跳转目标 | 示例 | +|------|---------|------| +| `!gh` | GitHub | `!gh tensorflow` | +| `!so` | Stack Overflow | `!so javascript error` | +| `!npm` | npmjs.com | `!npm express` | +| `!pypi` | PyPI | `!pypi requests` | +| `!mdn` | MDN Web Docs | `!mdn fetch api` | +| `!docs` | DevDocs | `!docs python` | +| `!docker` | Docker Hub | `!docker nginx` | + +#### 知识百科 + +| Bang | 跳转目标 | 示例 | +|------|---------|------| +| `!w` | Wikipedia | `!w machine learning` | +| `!wen` | Wikipedia英文 | `!wen artificial intelligence` | +| `!wt` | Wiktionary | `!wt serendipity` | +| `!imdb` | IMDb | `!imdb inception` | + +#### 购物价格 + +| Bang | 跳转目标 | 示例 | +|------|---------|------| +| `!a` | Amazon | `!a wireless headphones` | +| `!e` | eBay | `!e vintage watch` | +| `!ali` | AliExpress | `!ali phone case` | + +#### 地图位置 + +| Bang | 跳转目标 | 示例 | +|------|---------|------| +| `!m` | Google Maps | `!m Beijing` | +| `!maps` | OpenStreetMap | `!maps Paris` | + +### 2.3 DuckDuckGo 搜索参数 + +| 参数 | 功能 | 示例 | +|------|------|------| +| `kp=1` | 严格安全搜索 | `https://duckduckgo.com/html/?q=test&kp=1` | +| `kp=-1` | 关闭安全搜索 | `https://duckduckgo.com/html/?q=test&kp=-1` | +| `kl=cn` | 中国区域 | `https://duckduckgo.com/html/?q=news&kl=cn` | +| `kl=us-en` | 美国英文 | `https://duckduckgo.com/html/?q=news&kl=us-en` | +| `ia=web` | 网页结果 | `https://duckduckgo.com/?q=test&ia=web` | +| `ia=images` | 图片结果 | `https://duckduckgo.com/?q=test&ia=images` | +| `ia=news` | 新闻结果 | `https://duckduckgo.com/?q=test&ia=news` | +| `ia=videos` | 视频结果 | `https://duckduckgo.com/?q=test&ia=videos` | + +### 2.4 DuckDuckGo 深度搜索示例 + +```javascript +// 1. 使用Bang跳转到Google搜索 +web_fetch({"url": "https://duckduckgo.com/html/?q=!g+machine+learning"}) + +// 2. 直接搜索GitHub上的项目 +web_fetch({"url": "https://duckduckgo.com/html/?q=!gh+react"}) + +// 3. 查找Stack Overflow答案 +web_fetch({"url": "https://duckduckgo.com/html/?q=!so+python+list+comprehension"}) + +// 4. 生成密码 +web_fetch({"url": "https://duckduckgo.com/?q=password+16"}) + +// 5. Base64编码 +web_fetch({"url": "https://duckduckgo.com/?q=base64+hello+world"}) + +// 6. 颜色代码转换 +web_fetch({"url": "https://duckduckgo.com/?q=%23FF5733"}) + +// 7. 搜索YouTube视频 +web_fetch({"url": "https://duckduckgo.com/html/?q=!yt+python+tutorial"}) + +// 8. 查看Wikipedia +web_fetch({"url": "https://duckduckgo.com/html/?q=!w+artificial+intelligence"}) + +// 9. 亚马逊商品搜索 +web_fetch({"url": "https://duckduckgo.com/html/?q=!a+laptop"}) + +// 10. 生成二维码 +web_fetch({"url": "https://duckduckgo.com/?q=qr+https://github.com"}) +``` + +--- + +## 🔎 Brave Search 深度搜索 + +### 3.1 Brave Search 特色功能 + +| 功能 | 参数 | 示例 | +|------|------|------| +| **独立索引** | 无依赖Google/Bing | 自有爬虫索引 | +| **Goggles** | 自定义搜索规则 | 创建个性化过滤器 | +| **Discussions** | 论坛讨论搜索 | 聚合Reddit等论坛 | +| **News** | 新闻聚合 | 独立新闻索引 | + +### 3.2 Brave Search 参数 + +| 参数 | 功能 | 示例 | +|------|------|------| +| `tf=pw` | 本周 | `https://search.brave.com/search?q=news&tf=pw` | +| `tf=pm` | 本月 | `https://search.brave.com/search?q=tech&tf=pm` | +| `tf=py` | 本年 | `https://search.brave.com/search?q=AI&tf=py` | +| `safesearch=strict` | 严格安全 | `https://search.brave.com/search?q=test&safesearch=strict` | +| `source=web` | 网页搜索 | 默认 | +| `source=news` | 新闻搜索 | `https://search.brave.com/search?q=tech&source=news` | +| `source=images` | 图片搜索 | `https://search.brave.com/search?q=cat&source=images` | +| `source=videos` | 视频搜索 | `https://search.brave.com/search?q=music&source=videos` | + +### 3.3 Brave Search Goggles(自定义过滤器) + +Goggles 允许创建自定义搜索规则: + +``` +$discard // 丢弃所有 +$boost,site=stackoverflow.com // 提升Stack Overflow +$boost,site=github.com // 提升GitHub +$boost,site=docs.python.org // 提升Python文档 +``` + +### 3.4 Brave Search 深度搜索示例 + +```javascript +// 1. 本周科技新闻 +web_fetch({"url": "https://search.brave.com/search?q=technology&tf=pw&source=news"}) + +// 2. 本月AI发展 +web_fetch({"url": "https://search.brave.com/search?q=artificial+intelligence&tf=pm"}) + +// 3. 图片搜索 +web_fetch({"url": "https://search.brave.com/search?q=machine+learning&source=images"}) + +// 4. 视频教程 +web_fetch({"url": "https://search.brave.com/search?q=python+tutorial&source=videos"}) + +// 5. 使用独立索引搜索 +web_fetch({"url": "https://search.brave.com/search?q=privacy+tools"}) +``` + +--- + +## 📊 WolframAlpha 知识计算搜索 + +### 4.1 WolframAlpha 数据类型 + +| 类型 | 查询示例 | URL | +|------|---------|-----| +| **数学计算** | `integrate x^2 dx` | `https://www.wolframalpha.com/input?i=integrate+x%5E2+dx` | +| **单位换算** | `100 miles to km` | `https://www.wolframalpha.com/input?i=100+miles+to+km` | +| **货币转换** | `100 USD to CNY` | `https://www.wolframalpha.com/input?i=100+USD+to+CNY` | +| **股票数据** | `AAPL stock` | `https://www.wolframalpha.com/input?i=AAPL+stock` | +| **天气查询** | `weather in Beijing` | `https://www.wolframalpha.com/input?i=weather+in+Beijing` | +| **人口数据** | `population of China` | `https://www.wolframalpha.com/input?i=population+of+China` | +| **化学元素** | `properties of gold` | `https://www.wolframalpha.com/input?i=properties+of+gold` | +| **营养成分** | `nutrition of apple` | `https://www.wolframalpha.com/input?i=nutrition+of+apple` | +| **日期计算** | `days between Jan 1 2020 and Dec 31 2024` | 日期间隔计算 | +| **时区转换** | `10am Beijing to New York` | 时区转换 | +| **IP地址** | `8.8.8.8` | IP信息查询 | +| **条形码** | `scan barcode 123456789` | 条码信息 | +| **飞机航班** | `flight AA123` | 航班信息 | + +### 4.2 WolframAlpha 深度搜索示例 + +```javascript +// 1. 计算积分 +web_fetch({"url": "https://www.wolframalpha.com/input?i=integrate+sin%28x%29+from+0+to+pi"}) + +// 2. 解方程 +web_fetch({"url": "https://www.wolframalpha.com/input?i=solve+x%5E2-5x%2B6%3D0"}) + +// 3. 货币实时汇率 +web_fetch({"url": "https://www.wolframalpha.com/input?i=100+USD+to+CNY"}) + +// 4. 股票实时数据 +web_fetch({"url": "https://www.wolframalpha.com/input?i=Apple+stock+price"}) + +// 5. 城市天气 +web_fetch({"url": "https://www.wolframalpha.com/input?i=weather+in+Shanghai+tomorrow"}) + +// 6. 国家统计信息 +web_fetch({"url": "https://www.wolframalpha.com/input?i=GDP+of+China+vs+USA"}) + +// 7. 化学计算 +web_fetch({"url": "https://www.wolframalpha.com/input?i=molar+mass+of+H2SO4"}) + +// 8. 物理常数 +web_fetch({"url": "https://www.wolframalpha.com/input?i=speed+of+light"}) + +// 9. 营养信息 +web_fetch({"url": "https://www.wolframalpha.com/input?i=calories+in+banana"}) + +// 10. 历史日期 +web_fetch({"url": "https://www.wolframalpha.com/input?i=events+on+July+20+1969"}) +``` + +--- + +## 🔧 Startpage 隐私搜索 + +### 5.1 Startpage 特色功能 + +| 功能 | 说明 | URL | +|------|------|-----| +| **代理浏览** | 匿名访问搜索结果 | 点击"匿名查看" | +| **无追踪** | 不记录搜索历史 | 默认开启 | +| **EU服务器** | 受欧盟隐私法保护 | 数据在欧洲 | +| **代理图片** | 图片代理加载 | 隐藏IP | + +### 5.2 Startpage 参数 + +| 参数 | 功能 | 示例 | +|------|------|------| +| `cat=web` | 网页搜索 | 默认 | +| `cat=images` | 图片搜索 | `...&cat=images` | +| `cat=video` | 视频搜索 | `...&cat=video` | +| `cat=news` | 新闻搜索 | `...&cat=news` | +| `language=english` | 英文结果 | `...&language=english` | +| `time=day` | 过去24小时 | `...&time=day` | +| `time=week` | 过去一周 | `...&time=week` | +| `time=month` | 过去一月 | `...&time=month` | +| `time=year` | 过去一年 | `...&time=year` | +| `nj=0` | 关闭 family filter | `...&nj=0` | + +### 5.3 Startpage 深度搜索示例 + +```javascript +// 1. 隐私搜索 +web_fetch({"url": "https://www.startpage.com/sp/search?query=privacy+tools"}) + +// 2. 图片隐私搜索 +web_fetch({"url": "https://www.startpage.com/sp/search?query=nature&cat=images"}) + +// 3. 本周新闻(隐私模式) +web_fetch({"url": "https://www.startpage.com/sp/search?query=tech+news&time=week&cat=news"}) + +// 4. 英文结果搜索 +web_fetch({"url": "https://www.startpage.com/sp/search?query=machine+learning&language=english"}) +``` + +--- + +## 🌍 综合搜索策略 + +### 6.1 按搜索目标选择引擎 + +| 搜索目标 | 首选引擎 | 备选引擎 | 原因 | +|---------|---------|---------|------| +| **学术研究** | Google Scholar | Google, Brave | 学术资源索引 | +| **编程开发** | Google | GitHub(DuckDuckGo bang) | 技术文档全面 | +| **隐私敏感** | DuckDuckGo | Startpage, Brave | 不追踪用户 | +| **实时新闻** | Brave News | Google News | 独立新闻索引 | +| **知识计算** | WolframAlpha | Google | 结构化数据 | +| **中文内容** | Google HK | Bing | 中文优化好 | +| **欧洲视角** | Qwant | Startpage | 欧盟合规 | +| **环保支持** | Ecosia | DuckDuckGo | 搜索植树 | +| **无过滤** | Brave | Startpage | 无偏见结果 | + +### 6.2 多引擎交叉验证 + +```javascript +// 策略:同一关键词多引擎搜索,对比结果 +const keyword = "climate change 2024"; + +// 获取不同视角 +const searches = [ + { engine: "Google", url: `https://www.google.com/search?q=${keyword}&tbs=qdr:m` }, + { engine: "Brave", url: `https://search.brave.com/search?q=${keyword}&tf=pm` }, + { engine: "DuckDuckGo", url: `https://duckduckgo.com/html/?q=${keyword}` }, + { engine: "Ecosia", url: `https://www.ecosia.org/search?q=${keyword}` } +]; + +// 分析不同引擎的结果差异 +``` + +### 6.3 时间敏感搜索策略 + +| 时效性要求 | 引擎选择 | 参数设置 | +|-----------|---------|---------| +| **实时(小时级)** | Google News, Brave News | `tbs=qdr:h`, `tf=pw` | +| **近期(天级)** | Google, Brave | `tbs=qdr:d`, `time=day` | +| **本周** | 所有引擎 | `tbs=qdr:w`, `tf=pw` | +| **本月** | 所有引擎 | `tbs=qdr:m`, `tf=pm` | +| **历史** | Google Scholar | 学术档案 | + +### 6.4 专业领域深度搜索 + +#### 技术开发 + +```javascript +// GitHub 项目搜索 +web_fetch({"url": "https://duckduckgo.com/html/?q=!gh+tensorflow+stars:%3E1000"}) + +// Stack Overflow 问题 +web_fetch({"url": "https://duckduckgo.com/html/?q=!so+python+memory+leak"}) + +// MDN 文档 +web_fetch({"url": "https://duckduckgo.com/html/?q=!mdn+javascript+async+await"}) + +// PyPI 包 +web_fetch({"url": "https://duckduckgo.com/html/?q=!pypi+requests"}) + +// npm 包 +web_fetch({"url": "https://duckduckgo.com/html/?q=!npm+express"}) +``` + +#### 学术研究 + +```javascript +// Google Scholar 论文 +web_fetch({"url": "https://scholar.google.com/scholar?q=deep+learning+2024"}) + +// 搜索PDF论文 +web_fetch({"url": "https://www.google.com/search?q=machine+learning+filetype:pdf+2024"}) + +// arXiv 论文 +web_fetch({"url": "https://duckduckgo.com/html/?q=site:arxiv.org+quantum+computing"}) +``` + +#### 金融投资 + +```javascript +// 股票实时数据 +web_fetch({"url": "https://www.wolframalpha.com/input?i=AAPL+stock"}) + +// 汇率转换 +web_fetch({"url": "https://www.wolframalpha.com/input?i=EUR+to+USD"}) + +// 搜索财报PDF +web_fetch({"url": "https://www.google.com/search?q=Apple+Q4+2024+earnings+filetype:pdf"}) +``` + +#### 新闻时事 + +```javascript +// Google新闻 +web_fetch({"url": "https://www.google.com/search?q=breaking+news&tbm=nws&tbs=qdr:h"}) + +// Brave新闻 +web_fetch({"url": "https://search.brave.com/search?q=world+news&source=news"}) + +// DuckDuckGo新闻 +web_fetch({"url": "https://duckduckgo.com/html/?q=tech+news&ia=news"}) +``` + +--- + +## 🛠️ 高级搜索技巧汇总 + +### URL编码工具函数 + +```javascript +// URL编码关键词 +function encodeKeyword(keyword) { + return encodeURIComponent(keyword); +} + +// 示例 +const keyword = "machine learning"; +const encoded = encodeKeyword(keyword); // "machine%20learning" +``` + +### 批量搜索模板 + +```javascript +// 多引擎批量搜索函数 +function generateSearchUrls(keyword) { + const encoded = encodeURIComponent(keyword); + return { + google: `https://www.google.com/search?q=${encoded}`, + google_hk: `https://www.google.com.hk/search?q=${encoded}`, + duckduckgo: `https://duckduckgo.com/html/?q=${encoded}`, + brave: `https://search.brave.com/search?q=${encoded}`, + startpage: `https://www.startpage.com/sp/search?query=${encoded}`, + bing_intl: `https://cn.bing.com/search?q=${encoded}&ensearch=1`, + yahoo: `https://search.yahoo.com/search?p=${encoded}`, + ecosia: `https://www.ecosia.org/search?q=${encoded}`, + qwant: `https://www.qwant.com/?q=${encoded}` + }; +} + +// 使用示例 +const urls = generateSearchUrls("artificial intelligence"); +``` + +### 时间筛选快捷函数 + +```javascript +// Google时间筛选URL生成 +function googleTimeSearch(keyword, period) { + const periods = { + hour: 'qdr:h', + day: 'qdr:d', + week: 'qdr:w', + month: 'qdr:m', + year: 'qdr:y' + }; + return `https://www.google.com/search?q=${encodeURIComponent(keyword)}&tbs=${periods[period]}`; +} + +// 使用示例 +const recentNews = googleTimeSearch("AI breakthrough", "week"); +``` + +--- + +## 📝 完整搜索示例集 + +```javascript +// ==================== 技术开发 ==================== + +// 1. 搜索GitHub上高Star的Python项目 +web_fetch({"url": "https://www.google.com/search?q=site:github.com+python+stars:%3E1000"}) + +// 2. Stack Overflow最佳答案 +web_fetch({"url": "https://duckduckgo.com/html/?q=!so+best+way+to+learn+python"}) + +// 3. MDN文档查询 +web_fetch({"url": "https://duckduckgo.com/html/?q=!mdn+promises"}) + +// 4. 搜索npm包 +web_fetch({"url": "https://duckduckgo.com/html/?q=!npm+axios"}) + +// ==================== 学术研究 ==================== + +// 5. Google Scholar论文 +web_fetch({"url": "https://scholar.google.com/scholar?q=transformer+architecture"}) + +// 6. 搜索PDF论文 +web_fetch({"url": "https://www.google.com/search?q=attention+is+all+you+need+filetype:pdf"}) + +// 7. arXiv最新论文 +web_fetch({"url": "https://duckduckgo.com/html/?q=site:arxiv.org+abs+quantum"}) + +// ==================== 新闻时事 ==================== + +// 8. Google最新新闻(过去1小时) +web_fetch({"url": "https://www.google.com/search?q=breaking+news&tbs=qdr:h&tbm=nws"}) + +// 9. Brave本周科技新闻 +web_fetch({"url": "https://search.brave.com/search?q=technology&tf=pw&source=news"}) + +// 10. DuckDuckGo新闻 +web_fetch({"url": "https://duckduckgo.com/html/?q=world+news&ia=news"}) + +// ==================== 金融投资 ==================== + +// 11. 股票实时数据 +web_fetch({"url": "https://www.wolframalpha.com/input?i=Tesla+stock"}) + +// 12. 货币汇率 +web_fetch({"url": "https://www.wolframalpha.com/input?i=1+BTC+to+USD"}) + +// 13. 公司财报PDF +web_fetch({"url": "https://www.google.com/search?q=Microsoft+annual+report+2024+filetype:pdf"}) + +// ==================== 知识计算 ==================== + +// 14. 数学计算 +web_fetch({"url": "https://www.wolframalpha.com/input?i=derivative+of+x%5E3+sin%28x%29"}) + +// 15. 单位换算 +web_fetch({"url": "https://www.wolframalpha.com/input?i=convert+100+miles+to+kilometers"}) + +// 16. 营养信息 +web_fetch({"url": "https://www.wolframalpha.com/input?i=protein+in+chicken+breast"}) + +// ==================== 隐私保护搜索 ==================== + +// 17. DuckDuckGo隐私搜索 +web_fetch({"url": "https://duckduckgo.com/html/?q=privacy+tools"}) + +// 18. Startpage匿名搜索 +web_fetch({"url": "https://www.startpage.com/sp/search?query=secure+messaging"}) + +// 19. Brave无追踪搜索 +web_fetch({"url": "https://search.brave.com/search?q=encryption+software"}) + +// ==================== 高级组合搜索 ==================== + +// 20. Google多条件精确搜索 +web_fetch({"url": "https://www.google.com/search?q=%22machine+learning%22+site:github.com+filetype:pdf+2024"}) + +// 21. 排除特定站点的搜索 +web_fetch({"url": "https://www.google.com/search?q=python+tutorial+-wikipedia+-w3schools"}) + +// 22. 价格范围搜索 +web_fetch({"url": "https://www.google.com/search?q=laptop+%24800..%241200+best+review"}) + +// 23. 使用Bangs快速跳转 +web_fetch({"url": "https://duckduckgo.com/html/?q=!g+site:medium.com+python"}) + +// 24. 图片搜索(Google) +web_fetch({"url": "https://www.google.com/search?q=beautiful+landscape&tbm=isch"}) + +// 25. 学术引用搜索 +web_fetch({"url": "https://scholar.google.com/scholar?q=author:%22Geoffrey+Hinton%22"}) +``` + +--- + +## 🔐 隐私保护最佳实践 + +### 搜索引擎隐私级别 + +| 引擎 | 追踪级别 | 数据保留 | 加密 | 推荐场景 | +|------|---------|---------|------|---------| +| **DuckDuckGo** | 无追踪 | 无保留 | 是 | 日常隐私搜索 | +| **Startpage** | 无追踪 | 无保留 | 是 | 需要Google结果但保护隐私 | +| **Brave** | 无追踪 | 无保留 | 是 | 独立索引,无偏见 | +| **Qwant** | 无追踪 | 无保留 | 是 | 欧盟合规要求 | +| **Google** | 高度追踪 | 长期保留 | 是 | 需要个性化结果 | +| **Bing** | 中度追踪 | 长期保留 | 是 | 微软服务集成 | + +### 隐私搜索建议 + +1. **日常使用**: DuckDuckGo 或 Brave +2. **需要Google结果但保护隐私**: Startpage +3. **学术研究**: Google Scholar(学术用途追踪较少) +4. **敏感查询**: 使用Tor浏览器 + DuckDuckGo onion服务 +5. **跨设备同步**: 避免登录搜索引擎账户 + +--- + +## 📚 参考资料 + +- [Google搜索操作符完整列表](https://support.google.com/websearch/answer/...) +- [DuckDuckGo Bangs完整列表](https://duckduckgo.com/bang) +- [Brave Search文档](https://search.brave.com/help/...) +- [WolframAlpha示例](https://www.wolframalpha.com/examples/) diff --git a/skills/pdf/LICENSE.txt b/skills/pdf/LICENSE.txt new file mode 100755 index 0000000..e092e50 --- /dev/null +++ b/skills/pdf/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright (c) 2026 Z.ai All rights reserved. + +Permission is granted for personal, educational, and non-commercial use only. + +Commercial use is strictly prohibited without prior written permission from the author. + +Unauthorized copying, modification, or distribution of the software for commercial purposes is prohibited. + +The author reserves the right to make the final determination of what constitutes "commercial use". + +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 AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE. diff --git a/skills/pdf/SKILL.md b/skills/pdf/SKILL.md new file mode 100755 index 0000000..8a59311 --- /dev/null +++ b/skills/pdf/SKILL.md @@ -0,0 +1,915 @@ +--- +name: pdf +metadata: + author: Z.AI + version: "1.0" +description: Professional PDF toolkit with four production lines: (1) Report - structured documents via ReportLab (reports, proposals, contracts, white papers) (2) Creative - visual design via JSON Blueprint → design_engine.py → Playwright snapshot (posters, infographics, invitations, dashboards). The LLM acts as Art Director outputting ONLY JSON spatial blueprints; convert.blueprint compiles to pixel-perfect PDF. (3) Academic - scholarly work via LaTeX/Tectonic (papers, theses, math-heavy documents) (4) Process - manipulate existing PDFs (extract, merge, split, fill forms, convert) Auto-routes based on document type. Includes ATS/creative/academic resume sub-paths. +license: Proprietary. LICENSE.txt has complete terms +--- + +# PDF - Document Production Workbench + +## Quick Setup + +```bash +bash "$PDF_SKILL_DIR/scripts/setup.sh" # Interactive environment check + install +python3 "$PDF_SKILL_DIR/scripts/pdf.py" env.check # Detailed dependency status (JSON: add -j) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" env.fix # Auto-install missing Python packages +``` + +## Triage + +Determine task weight to control how much context to load: + +| Weight | Triggers | What to Load | +|--------|----------|--------------| +| **Light** | Format conversion, form fill, text extract, merge/split, simple certificate | SKILL.md + `briefs/process.md` only | +| **Standard** | Multi-page report, poster, academic paper, resume, reformat - any document with design decisions | SKILL.md + matched brief + typesetting assets on demand | + +Light tasks skip typesetting files entirely. Standard tasks load them on demand per the brief's instructions. + +### ⚠️ Pre-Routing Checks (run BEFORE matching brief) + +1. **Emoji Check** - Scan user content for intentional emoji (decorative 📊🎯🔥, not OS-level emoji input). If found → **force Creative brief** regardless of document type. ReportLab renders emoji as □ squares; LaTeX drops them entirely. +2. **CJK Check** - Chinese/Japanese/Korean content needs font coverage. Report brief must use `UniSong`/`UniHei` registered fonts; Creative brief must load Google Fonts Noto Sans SC with `font-display: swap`; Academic brief must use `\usepackage{ctex}`. +3. **Size Check** - Non-standard page sizes (not A4/Letter/A3) → prefer Creative brief (Playwright handles any dimension). ReportLab can do custom sizes but pagination is manual. +4. **Character Safety Check** - Before writing any content string, scan for Japanese kana (の、が、は etc.), unusual Unicode symbols, or non-CJK characters that may corrupt during encoding transit ( Especially when code is written via heredoc/base64/LLM output). Replace with plain Chinese equivalents: `の`→`之/的/缔`, `々`→omit or write full character. **If content must preserve Japanese, use only standard CJK Unified Ideographs (U+4E00-U+9FFF) and common kana; avoid rare/private-use codepoints.** + +--- + +## Briefing + +Match the user's intent to a production brief. Each brief contains the full workflow, tech stack specifics, and references to shared typesetting assets. + +``` +User Request +│ +├─ Work with existing PDF? ─────────────┬─ Extract/merge/split/fill/convert → briefs/process.md +│ ├─ Reformat/redesign → briefs/process.md (extract) → delegate to report or creative brief +│ └─ User provides a PDF template/reference to match style +│ → briefs/process.md "Template-Guided Reformat" → delegate to matched brief +│ +├─ Report / proposal / white paper / contract / analysis? +│ └─ ────────────────────────────────── → briefs/report.md (ReportLab) +│ +├─ Poster / invitation / infographic / dashboard / creative layout? +│ └─ ────────────────────────────────── → briefs/creative.md (Playwright) +│ +├─ Academic paper / thesis / math / IEEE / ACM / LaTeX? +│ └─ ────────────────────────────────── → briefs/academic.md (Tectonic) +│ +├─ Math-heavy doc / TikZ diagram / algorithm pseudocode / Beamer slides? +│ └─ ────────────────────────────────── → briefs/academic.md (Tectonic, Scenarios A-D) +│ +├─ Document needs complex embedded diagrams (flowcharts, architecture, neural nets)? +│ └─ Route by target brief: +│ ├─ Report → Playwright+CSS → PNG → ReportLab Image() flowable +│ ├─ Creative → directly in HTML (CSS flexbox/grid + connectors) +│ └─ Academic → complexity-based: +│ ├─ Simple (≤6 nodes, linear/tree) → TikZ native (vector) +│ └─ Complex (>6 nodes, branches, annotations) → Playwright+CSS → PNG → \includegraphics +│ +└─ Resume / CV? + ├─ ATS-safe / corporate ─────────── → briefs/report.md (resume sub-section) + ├─ Creative / design industry ────── → briefs/creative.md (resume sub-section) + └─ Academic CV / publications ────── → briefs/academic.md (resume sub-section) +``` + +### Detection Keywords + +| Brief | Keywords | +|-------|----------| +| Report | 报告, report, 分析, analysis, 白皮书, white paper, 提案, proposal, 合同, contract, 方案, 规划, 发票, invoice, 收据, receipt, 试卷, exam, quiz, test paper, 练习, exercise, worksheet, 考试, 测验 | +| Creative | 海报, poster, 邀请函, invitation, 信息图, infographic, 仪表盘, dashboard, 传单, flyer, 证书, certificate, 菜单, menu, 名片, business card, 奖状, award, 标签, label, 信封, envelope, 贺卡, greeting card | +| Creative (Poster) | 海报, poster, 传单, flyer, 宣传页, 宣传单 → additionally load `briefs/poster.md` scene layer rules | +| Academic | 论文, paper, 学术, academic, LaTeX, 数学, math, IEEE, ACM, 毕业, thesis, 研究, research, Beamer, slides, 开题报告, 学位, dissertation, proposal | +| Process | 提取, extract, 合并, merge, 拆分, split, 填写, fill, 转换, convert, OCR, 重排, reformat, 重新排版, redesign, 模板, template, 参照, 照着这个做, match this style, 压缩, compress, 水印, watermark, 加密, encrypt, 签名, sign | + +### Complete Scenario Routing Matrix + +Below is an exhaustive map of every known PDF request type to its handling strategy. If a scenario is not listed, route to the closest match or ask the user. + +#### 📄 Creation (Generate PDF from scratch) + +| Scenario | Route | Notes | +|----------|-------|-------| +| Report / white paper / analysis | report.md | ReportLab structured document | +| Report with emoji | **creative.md** | 🚨 Emoji rule override | +| Business proposal | report.md | Structured + data tables | +| Contract / legal document | report.md | Add signature placeholders (dotted line + label) | +| Invoice / receipt | report.md | Table-heavy, precision alignment | +| Exam / quiz / test paper / worksheet | report.md | Indented options, answer space reservation, structured numbering (see Exam Paper Rules in report.md) | +| Math exam / math worksheet (with formulas/equations) | academic.md | LaTeX for proper math typesetting. See §Exam Paper Rules in academic.md | +| Poster / flyer | creative.md + **poster.md** | Visual design + poster density/sizing rules | +| Invitation / greeting card | creative.md | Non-standard size, decorative | +| Certificate / award | creative.md | Single page, centered layout, decorative border | +| Business card | creative.md | Tiny size (90×54mm), Playwright native support | +| Envelope / label | creative.md | Non-standard size, simple layout | +| Menu / price list | creative.md | Visual layout + may contain emoji | +| Resume (ATS) | report.md | Plain text structure | +| Resume (creative) | creative.md | Visual design | +| Resume (academic CV) | academic.md | Publication list + BibTeX | +| Academic paper | academic.md | LaTeX/Tectonic | +| Math-heavy document | academic.md | LaTeX typesetting | +| Presentation / PPT-style | creative.md | Landscape (1280×720), one topic per page | +| Book / long document | report.md | Add TOC + chapter numbering, validate with toc_validate.py | +| CJK vertical text | creative.md | HTML `writing-mode: vertical-rl` + `text-orientation: upright` + `white-space: nowrap` + Playwright | +| RTL document (Arabic/Hebrew) | creative.md | HTML `dir="rtl"` + Playwright | +| Batch generation (mail merge) | report.md | Python loop + template variable substitution | +| Infographic | creative.md | Data visualization + design | +| Calendar / schedule | creative.md | Grid layout + custom dimensions | + +#### 🔧 Processing (Manipulate existing PDF) + +| Scenario | Route | Command / Method | +|----------|-------|------------------| +| Merge multiple PDFs | process.md | `pages.merge a.pdf b.pdf -o out.pdf` | +| Split PDF | process.md | `pages.split input.pdf -o ./output/` | +| Extract text | process.md | `extract.text input.pdf` | +| Extract tables | process.md | `extract.table input.pdf` | +| Extract images | process.md | `extract.image input.pdf` | +| Fill forms | process.md | `form.fill input.pdf` | +| Office → PDF | process.md | `convert.office input.docx` | +| HTML → PDF (documents) | process.md | `convert.html input.html` or `node html2pdf-next.js` | +| HTML → PDF (posters) | poster.md | `node html2poster.js poster.html` | +| Image → PDF | process.md | pikepdf: one image per page, embed as XObject | +| PDF → image | process-advanced.md | pypdfium2 render each page to PNG | +| Encrypt / decrypt | process-advanced.md | pikepdf encryption | +| Add watermark | process.md | pikepdf overlay: create watermark page → merge onto each page | +| Compress PDF | process.md | Ghostscript: `gs -sDEVICE=pdfwrite -dPDFSETTINGS=/screen` | +| OCR scanned PDF | process-advanced.md | ocrmypdf or Tesseract | +| Rotate pages | process.md | `pages.rotate input.pdf 90 -o out.pdf` | +| Crop pages | process.md | `pages.crop input.pdf l,b,r,t -o out.pdf` | +| Remove blank pages | process.md | `pages.clean input.pdf` | +| Reformat by template | process.md → delegate | Extract content → regenerate via report/creative | +| PDF diff / compare | process.md | `diff-pdf` CLI or Python per-page text comparison | +| Digital signature | process.md | `pyhanko` library (requires extra install) | +| Edit metadata | process.md | `meta.set input.pdf -o out.pdf -d '{...}'` | + +### Special Routing Rules + +**🚨 Emoji rule (CRITICAL - check FIRST)**: Content with intentional emoji (📊🎯🔥💡 etc.) → force **briefs/creative.md** regardless of document type. ReportLab renders emoji as □ squares; LaTeX silently drops them. This rule overrides all other routing. Even if the user says "report" - if the content has emoji, use Creative pipeline. + +**Non-standard page size rule**: Dimensions other than A4/Letter/A3 → strongly prefer **briefs/creative.md**. Playwright handles any arbitrary page size natively. ReportLab requires manual pagination math. + +**Academic auto-detect**: Papers, theses, or heavy math → **briefs/academic.md** even without explicit "LaTeX" mention. + +**Template-guided rule**: When the user uploads a PDF and says "match this template" / "follow this style" / "reformat like this" → **briefs/process.md** Template-Guided Reformat section. This is a Standard triage (not Light), because it involves design decisions. + +**Resume routing**: Default to Report brief (ATS-safe). Creative industry → Creative brief. Academic CV with publications → Academic brief. + +--- + +## Shared Assets + +These are referenced by multiple briefs. **Do not load upfront** - each brief tells you when and what to load. + +| Asset | Path | Used By | Purpose | +|-------|------|---------|---------| +| Palette & Typography | `typesetting/palette.md` | Report, Creative | Color system, font rules, anti-patterns, spacing | +| Cover Layout System V2.1 | `typesetting/cover.md` | **Report + Creative + Academic** | 7 industrial-grade templates with absolute anchor grid, Z-index layers, typography weight system, mandatory Summary Block, code-level safety (5 checks), base unit `U = W*0.05`. **Unified HTML/Playwright cover system for all routes.** | +| Chart Styling & Anti-Stacking | `typesetting/charts.md` | Report, Creative, Academic | Chart defaults, collision prevention, axis/grid/legend rules | +| Overflow Prevention | `typesetting/overflow.md` | Report, Creative, Academic | Bounding box system, text/image/table overflow prevention, fallback strategies | +| **Fill Engine (Anti-Void)** | `typesetting/fill-engine.md` | **Report, Creative, Academic** | **Anti-Void Engine V2.0: font floor enforcement, fill ratio calculation, paragraph inflation, component elevation, Y-axis golden-ratio anchoring** | +| Pagination & Flow Control | `typesetting/pagination.md` | Report, Creative | Cross-page integrity, orphan/widow control, CJK punctuation rules | +| Typography System | `typesetting/typography.md` | Report, Creative | Font size scale, line-height, spacing hierarchy | +| Geometric Anchors | `typesetting/geometry.md` | Creative + Report | Decorative geometric elements, anchor placement rules | +| Cover Backgrounds | `typesetting/cover-backgrounds.md` | **Report + Creative + Academic** | Cover background rendering, transparency constraints | +| Visual Framework | `configs/visual_framework.md` | Creative | Palette mode, color harmony, SVG background params | +| Components Library | `configs/components.md` | Creative | Non-grid composition components (floating cards, oversized text, etc.) | +| Font Stacks | `configs/fonts.md` | All pipelines | Font families per pipeline (Google Fonts, ReportLab, LaTeX) | + +--- + +## Content Rules + +- **Language**: Match user's query language. Chinese query → Chinese PDF. +- **Page/word count**: Respect explicit constraints (±20%). Unspecified → completeness over brevity. +- **Outline**: User-provided outlines are sacred. No reordering without asking. +- **Citations**: No fabrication. Chinese → GB/T 7714, English → APA. Search to verify. +- **Multi-part requests**: Generate ALL parts - never silently drop a component. + +### HTML Image Source Path Rules + +When embedding images in HTML documents (Creative pipeline, Playwright-rendered diagrams, or any HTML→PDF flow): + +| Image location | `<img src>` value | Example | +|---|---|---| +| **Local file** | **Relative path** from the HTML file's directory | `<img src="images/chart.png">` or `<img src="./diagram.png">` | +| **Remote URL** | Full URL (no change needed) | `<img src="https://example.com/photo.jpg">` | + +**Iron rules:** +1. **NEVER use absolute paths** for local files in HTML `<img>`, `<source>`, CSS `url()`, or any other asset reference (e.g. `/Users/alice/project/img.png`). Absolute paths break portability across machines and environments. +2. **Always use relative paths** anchored to the HTML file's own directory. If the image lives in a subdirectory, use `images/foo.png` or `./images/foo.png`. +3. **Remote URLs (`http://` / `https://`) are fine as-is** — do not convert them to local paths. +4. When generating HTML from a script or blueprint, ensure all referenced assets are either (a) in the same directory as the output HTML, or (b) in a clearly named subdirectory (e.g. `assets/`, `images/`), and referenced with relative paths. +5. If a build script needs to resolve paths programmatically, compute relative paths at generation time (e.g. `os.path.relpath(image_path, html_dir)`) rather than embedding absolute filesystem paths. + +--- + +## Figure & Diagram Embedding (All Briefs) + +### Iron Rule: Figures Are Block-Level + +Figures, diagrams, and charts MUST be independent block elements occupying full width. **Never** float/wrap figures alongside body text - this causes the text-diagram overlap badcase. + +| Brief | Correct embedding | Forbidden | +|-------|-------------------|-----------| +| Report (ReportLab) | `story.append(Image(...))` as standalone Flowable | Placing images inside Paragraph text, simulating float | +| Creative (Playwright) | `<figure style="display:block; width:100%; margin:2em auto">` | `float:right`, `display:flex` with text, `wrapfigure`-style CSS | +| Academic (LaTeX) | `\begin{figure}[t] ... \end{figure}` | Bare `\includegraphics` in text body (no figure env), bare `tikzpicture` in multi-column | + +### Complex Diagram Strategy + +When a diagram has **>12 nodes, >3 subgroups, or intricate connections**, do NOT try to render it as one giant figure. Instead: + +1. **Table for details** - structured data (phases, components, specs) goes into a proper table +2. **Simplified overview diagram** - a stripped-down flowchart/Mermaid showing only the top-level flow (≤8 nodes) +3. **Cross-reference** - table caption + diagram caption reference each other + +This "table + simple diagram" pattern prevents: +- Diagrams overflowing page boundaries +- Text becoming unreadably small to fit everything +- Layout engines mishandling oversized graphics + +### Diagram Content Quality Rules (Cross-reference: charts) + +The rules above handle **how** to embed diagrams in PDF. For **what the diagram itself looks like** (node layout, connector routing, color, readability), follow the `charts` skill rules: + +**Before generating ANY flowchart/diagram for PDF embedding, check these:** + +1. **Connectors must not pass through nodes** - If 3+ layers exist, connect adjacent layers only (top→mid, mid→bottom). Never draw top→bottom lines through middle nodes. Use detour paths if cross-layer links are needed. +2. **Multiple arrows into one node must not pile up** - Distribute entry points evenly along target edge, or use merge-then-enter pattern (sources converge to a vertical merge line, then single arrow to target). +3. **Low-saturation fills only** - Node backgrounds must be pale (`#EFF6FF`, `#F0FDF4`). High-saturation colors (`#3B82F6`, `#10B981`) only for borders or small accents. No children's-art color schemes. +4. **Phase titles vs sub-steps must be visually distinct** - Different background color, font size, and font weight. Never same-style boxes for both. +5. **Font sizes must be readable at final output size** - Sizes depend on the embedding context: + | Output context | Node title min | Description min | Label min | + |---------------|----------------|-----------------|-----------| + | Standalone PNG (web/presentation, ≥1200px wide) | 14px | 12px | 11px | + | Embedded in A4 PDF (ReportLab/LaTeX, ~450pt content width) | 10pt | 8pt | 7pt | + | Embedded in slide deck (landscape, ~720pt wide) | 12pt | 10pt | 9pt | + + **Principle**: After embedding, the smallest text in the diagram must still be legible when the document is viewed at 100% zoom. If the diagram is scaled down to fit page width, recalculate: `effective_size = original_size × (display_width / canvas_width)`. If effective size drops below the minimum, either increase original font size or reduce diagram complexity. +6. **Legend/annotations must not overlap content** - Separate container, ≥ 40px gap from last node, fully within canvas bounds. + +**For Playwright-rendered diagrams**: Use low-saturation fills (`#EFF6FF`, `#F0FDF4`), CSS flexbox/grid for node layout, SVG `<line>`/`<path>` for connectors, and verify no overlap at final render size. +**For ReportLab-drawn diagrams**: Same principles apply - use `Drawing()` with explicit coordinates, check node bounding boxes for overlap before finalizing. + +### Diagram Generation Strategy (Per-Brief) + +Diagram rendering depends on the target brief - **NOT** a one-size-fits-all TikZ pipeline. + +| Target Brief | Diagram Method | Rationale | +|---|---|---| +| **Report** (ReportLab) | Playwright+CSS → PNG → `Image()` | No LaTeX compiler in this route; HTML/CSS handles any layout natively | +| **Creative** (Playwright) | Directly in HTML (CSS flexbox/grid + JS connectors) | Already in browser context | +| **Academic** (Tectonic) - simple (≤6 nodes) | TikZ native `tikzpicture` | Vector output, font consistency, LaTeX-native | +| **Academic** (Tectonic) - complex (>6 nodes) | Playwright+CSS → PNG @2× → `\includegraphics` | TikZ branch logic is error-prone for models; 300dpi PNG is publication-ready | + +**Playwright+CSS diagram pipeline (Report & Academic-complex):** + +```bash +# 1. Write diagram HTML (CSS grid/flexbox + connectors) +cat > diagram.html << 'EOF' +<!-- LLM generates: nodes as divs, arrows as SVG/CSS --> +EOF + +# 2. Screenshot at 2× for print quality (300dpi equivalent) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.blueprint diagram.html --device-scale-factor 2 --output diagram.png +# Or via Playwright directly: +# page.screenshot(path='diagram.png', scale='device', device_scale_factor=2) + +# 3a. Embed in ReportLab (Report brief) +from reportlab.platypus import Image +img = Image('diagram.png', width=450) # auto height via aspect ratio +story.append(img) + +# 3b. Embed in LaTeX (Academic brief, complex diagrams only) +# \includegraphics[width=\columnwidth]{diagram.png} +``` + +**🚫 FORBIDDEN for Report/Creative briefs:** Do NOT use TikZ standalone → compile → pdftoppm → PNG pipeline. This route has no LaTeX compiler and the extra compilation steps are error-prone. + +**TikZ remains valid ONLY for:** +- Academic brief with simple diagrams (≤6 nodes, linear/hierarchical) +- Direct `tikzpicture` embedding in LaTeX documents +- Math-annotated diagrams where LaTeX math rendering matters + +See `briefs/academic.md` Scenario B for TikZ templates (simple diagrams only). + +--- + +## Vector Rendering Iron Rule + +**The final PDF MUST be generated via `page.pdf()` (Playwright) or ReportLab/LaTeX native output - NEVER via screenshot-to-PDF.** + +| Scenario | Correct Method | Forbidden | +|----------|---------------|-----------| +| Creative pipeline (single/multi-page) | `page.pdf()` via `convert.blueprint` or `html2pdf-next.js` | `page.screenshot()` → image → wrap as PDF | +| Report cover (HTML/Playwright) | `page.pdf()` → merge via pypdf | Screenshot cover → embed as image | +| Academic cover | `page.pdf()` → merge via pypdf | Screenshot → `\includegraphics` for cover | +| Full-page posters/infographics | `html2poster.js` (auto overflow:hidden + height measurement + `page.pdf()`) | Any raster pipeline for the final output | + +**Why:** `page.pdf()` produces vector text + vector shapes. Text remains selectable, sharp at any zoom, and file size is smaller. Screenshot-based PDFs are raster images - blurry when zoomed, unsearchable, and 3-5× larger. + +**The ONLY place screenshot/PNG embedding is acceptable:** +- **Diagrams** embedded as sub-elements inside a larger document (e.g., flowcharts in a Report). These use `page.screenshot()` at 2× device scale factor for 300dpi print quality, then embed via `Image()` (ReportLab) or `\includegraphics` (LaTeX). +- **Chart images** generated by matplotlib/plotly saved as PNG, then embedded. + +These are sub-elements, not the document itself. The document-level PDF output must always be vector. + +**Quick test:** Open the generated PDF, zoom to 400%. If text is blurry, you used a screenshot pipeline. Fix it. + +### HTML→PDF Engine Selection Rules + +There are **two dedicated scripts** for HTML→PDF. Choose based on document type: + +| Document type | Script | Reason | +|---------------|--------|--------| +| **Posters, infographics, long-image single-page designs** | `html2poster.js` | Auto overflow:hidden, auto height measurement, zero margin, single-page output | +| **Cover pages (Report/Academic route)** | `html2poster.js` | Covers are single-page fixed layouts with absolute positioning — same nature as posters. `html2pdf-next.js` would convert absolute→static and destroy the layout | +| **Multi-page documents, reports, academic papers, resumes** | `html2pdf-next.js` | A4/custom pagination, 20mm margin fallback, cover adaptation, pdf-lib metadata | +| **Creative pipeline (Blueprint → HTML → PDF)** | `html2pdf-next.js` via `convert.blueprint` | Called internally by design_engine pipeline | + +#### Poster / Single-Page Long-Image → `html2poster.js` + +```bash +node "$PDF_SKILL_DIR/scripts/html2poster.js" poster.html --output poster.pdf --width 720px +``` + +`html2poster.js` automatically: +- Forces `overflow: hidden` on `.poster` / `.page` containers (clips decorative overflow) +- Injects `@page { margin: 0 }` (zero margins always) +- Syncs `html/body` background with poster background color +- Measures `.poster` scrollHeight and uses it as PDF height +- Generates a single-page vector PDF with exact content dimensions + +**Use this for ANY fixed-width, dynamic-height, single-page design.** + +#### Documents / Multi-Page → `html2pdf-next.js` + +```bash +node "$PDF_SKILL_DIR/scripts/html2pdf-next.js" input.html --output output.pdf --width 210mm --height 297mm +# Or via pdf.py wrapper: +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.html input.html --output output.pdf +``` + +Pre-render hooks auto-handle @page injection, overflow detection, cover adaptation, font loading, and pdf-lib metadata. + +#### ⚠️ Iron Rule: No Hand-Written Playwright Scripts + +Common issues with hand-written Python `page.pdf()` (the dedicated scripts handle these automatically): +1. **Missing `@page` rule** → browser default margin causes content overflow to second page or white edges +2. **Oversized elements not fixed** → large elements with `break-inside: avoid` block pagination, content gets truncated +3. **Rendering before fonts are loaded** → Chinese text displays as squares or falls back to wrong font +4. **No overflow detection** → content exceeds page boundary without awareness +5. **No metadata** → PDF title, author, and other info missing + +**Iron rule: Posters and cover pages use `html2poster.js`, multi-page documents use `html2pdf-next.js`. Do not write hand-written Python Playwright scripts.** + +> **⚠️ Cover page gotcha:** Cover HTML uses `position: absolute` for layout. `html2pdf-next.js` pre-render hooks convert absolute-positioned elements to `static` flow (to prevent multi-page overlap), which **destroys** cover layouts. Always use `html2poster.js` for cover pages. + +### No overflow:hidden on Fixed-Size Pages (html2pdf-next.js only) + +When using `html2pdf-next.js` for documents, **NEVER set `overflow: hidden` on `html`, `body`, or the main page container**. + +> **Note:** This rule does NOT apply to posters rendered via `html2poster.js` — that script automatically adds `overflow: hidden` to `.poster`/`.page` containers to clip decorative overflow. You don't need to add or remove it manually. + +| Problem | Cause | Fix | +|---------|-------|-----| +| Browser preview cuts off bottom content, can't scroll | `overflow: hidden` on container + viewport < design height | Remove `overflow: hidden` | +| html2pdf-next.js "Fixed vertical overflow" warning, layout may break | Pre-render detects `scrollHeight > clientHeight` + hidden overflow, force-expands container | Remove `overflow: hidden` | + +**Always pair fixed-size pages with `@media screen` auto-scale** so the full page is visible in any browser window without scrolling. See `briefs/creative.md` § 0.5 for the CSS pattern. + +### Full-Bleed Rule (No White Margins) + +When generating HTML for Playwright `page.pdf()`, the content **MUST fill the entire page** with zero margins. White side margins = broken layout. + +**Mandatory CSS for any HTML → PDF:** +```css +@page { + size: <width> <height>; /* e.g., 720px 960px, or A4 */ + margin: 0; +} +html, body { + margin: 0; + padding: 0; +} +``` + +**Common causes of white margins:** +1. Missing `@page { margin: 0 }` - browser default margins kick in (~1cm each side) +2. Content width doesn't match page width - e.g., canvas is 720px but page is A4 (794px) +3. Missing `@page { size }` declaration in the HTML +4. Content has explicit `max-width` that's narrower than the page + +**For blueprint pipeline:** `design_engine.py` now injects `@page { size: var(--canvas-w) var(--canvas-h); margin: 0; }` automatically. +**For raw HTML:** YOU must include the `@page` rule. No exceptions. +**For direct Playwright:** Pass `margin: { top: 0, right: 0, bottom: 0, left: 0 }` to `page.pdf()`. + +### Background Color Consistency (No Color Mismatch) + +**`html` / `body` background color must match the content canvas background color.** + +Playwright `page.pdf({ printBackground: true })` renders the body background color. If body is white while the content area is gray/colored, color-inconsistent borders/gaps will appear in the PDF. + +#### Single-color documents (all pages same background) + +```css +/* MANDATORY: body background = content background */ +html, body { + margin: 0; + padding: 0; + background: var(--c-bg); /* Same color as content canvas */ +} +``` + +#### Multi-page documents with mixed backgrounds (e.g. dark cover + white body pages) + +**Root cause:** Playwright resolves `.page { width: 210mm }` and `@page { size: 210mm }` to slightly different sub-pixel values (e.g. 793.688px vs 793.701px). This creates a <1px gap at the right/bottom edge of each `.page` div where `body`'s background shows through. On dark pages, a white `body` background makes this gap visible as a white edge. + +**Fix — set `body` background to the document's dominant dark color:** + +```css +:root { + --primary: #0f172a; /* darkest page background */ +} +html, body { + margin: 0; + padding: 0; + width: 210mm; /* match @page size */ + background: var(--primary); /* fallback for sub-pixel gaps */ +} +``` + +**Why this works and doesn't break white pages:** +- Dark pages: sub-pixel gap reveals dark `body` → gap invisible. +- White pages: `.page-white { background: #ffffff }` fully covers `body` → dark body never visible. +- The gap is <1px — even on white pages, the dark body at the extreme pixel edge is imperceptible after anti-aliasing. + +**Rule: when generating multi-page HTML with mixed backgrounds, always set `html, body { background }` to the darkest page's background color.** If all pages are light/white, use the lightest content background (e.g. `#f8fafc`). Never leave `body` background unset (browser default = white = guaranteed white edges on dark pages). +``` + +### Content Centering (No Left/Right Drift) + +**After HTML-to-PDF conversion, content must be centered, no left or right drift allowed.** + +Common drift causes: +1. `@page { margin }` not 0 — browser default margin causes drift +2. `.safe-zone` or content container `inset` / `padding` left-right asymmetric +3. Content container has `max-width` but no `margin: 0 auto` +4. Grid components only occupy partial column width (e.g. `1/1 → X/7` only uses left half) +5. **Decorative elements overflow page boundary** — elements with `width > 100%` or negative offsets (e.g. glow circles, gradient overlays) inflate `scrollWidth` beyond page width. Playwright shrinks all content to fit, causing left-shift. **Fix: add `overflow: hidden` to `.page` containers.** See `typesetting/overflow.md` §3.5 for horizontal flex overflow rules. + +### Anti-Void Edges (No Large Blank Margins) + +**Content should not have large meaningless whitespace at page edges, top, or bottom.** + +- Content should make full use of page area; do not cram all content in the top half while leaving the bottom blank +- For multi-page documents, each page's fill rate should be ≥ 60% (see `pagination.md` last page ≥ 40% rule) +- For single-page posters/infographics, fill rate should be ≥ 70% + +--- + +## Preflight (Quality Assurance) + +Every PDF must pass preflight checks before delivery. Each brief specifies the exact commands. + +### HTML Pre-Render Validation (MANDATORY for ALL HTML→PDF paths) + +**Before** calling `html2pdf-next.js`, `html2poster.js`, `convert.blueprint`, or any Playwright `page.pdf()`, run: + +```bash +python3 "$PDF_SKILL_DIR/scripts/poster_validate.py" check-html <your_file>.html +``` + +| Result | Action | +|--------|--------| +| **PASS** (no errors) | Proceed to PDF generation | +| **ERROR** items | Must fix before generating PDF. Use `--fix --output <file>.html` for auto-repair | +| **WARNING** items | Review; non-blocking but should be addressed | + +**Key checks:** +- `OVERFLOW_HIDDEN_CONTAINER` (error): `overflow:hidden` on html/body/.page clips content in browser preview and triggers html2pdf-next.js auto-fix that may break layout +- `FIXED_SIZE_NO_SCREEN_ADAPT` (warning): fixed-size page without `@media screen` auto-scale — browser preview requires scrolling +- `SCREEN_ADAPT_NO_SCALE` (warning): `@media screen` exists but lacks scale/transform/zoom +- `FONT_NO_FALLBACK` (error): font-family without generic fallback +- `COLOR_CONTRAST` (warning): text/background contrast ratio < 3:1 +- Plus: remote images, absolute paths, missing margin reset, tiny fonts, background mismatch, etc. + +This applies to **all three HTML routes**: Creative blueprint pipeline, Report HTML covers, and bypass/custom HTML. + +### Overflow Prevention System + +**→ Full spec: `typesetting/overflow.md`** - read it for any document with tables, images, or multi-column layouts. + +Core principles: +1. **Measure first, draw second** - never render content without pre-calculating its dimensions +2. **Bounding Box constraint** - every element's width ≤ its parent container's `Max_Width` +3. **Text: use font metrics**, not character count, for width calculation +4. **Images: proportional scaling** - never insert at original size +5. **Tables: weight-based column width** + `Paragraph()` wrapping (never plain strings) +6. **Fallback ladder**: wrap → shrink font (max -3pt) → reduce padding → split element → log warning +7. **Vertical: KeepTogether** for heading+body, chart+caption; `repeatRows=1` for long tables + +### Table Overflow Prevention (ReportLab) +**Most common layout bug: table columns exceed page margins.** + +Before building any ReportLab Table: +1. Calculate `available_width = page_width - left_margin - right_margin` +2. Use proportional colWidths (`[0.25, 0.40, 0.20, 0.15]` × available_width) or fixed+flex pattern +3. `sum(colWidths)` must be ≤ `available_width` - **verify this in code** +4. Long text columns must use `Paragraph()` wrapping, not plain strings (plain strings don't wrap) +5. CJK text is wider: budget ~12pt per character at 10pt font size + +See `briefs/report.md` § "Table Width Management" for code patterns. + +### Table Overflow Prevention (LaTeX/Academic) +**Most common bug in dual-column papers: wide tables overflow single-column width.** + +Before writing any LaTeX table: +1. Count data columns - ≤ 4 fits single column; 5-6 needs `\small`; 7-8 needs `\resizebox`; ≥ 9 use `table*` (full width) +2. Use `tabular*{\columnwidth}` or `tabularx{\columnwidth}` instead of plain `tabular` for 5+ columns +3. Never use plain `tabular` with 8+ columns in twocolumn layout - guaranteed overflow +4. `\resizebox{\columnwidth}{!}` as last resort - verify smallest text ≥ 6pt after scaling + +See `briefs/academic.md` § "Table width management" for LaTeX patterns. + +### Playwright PDF CSS Blacklist +These CSS properties **silently break** in Playwright's PDF renderer: +- `backdrop-filter` / `-webkit-backdrop-filter` - **drops entire element content**. Use solid `rgba()` backgrounds. +- `overflow: hidden` on content containers - clips content. Only safe on small decorative elements (< 200px). + +After generating any Playwright PDF, **verify every page has content** (pypdf text extraction, check non-empty). + +### PDF Metadata (all briefs) +ALL PDFs must have: Title, Author (default "Z.ai"), Creator, Subject. + +### Delivery Summary (all briefs) +Report to user: file path, size, page count. Academic adds word/image count. Creative adds per-page verification. + +**HTML→PDF route deliverables (MANDATORY — applies to ALL briefs that use Playwright/HTML to generate PDF):** +Whenever the HTML→PDF pipeline is used (Creative route, Report cover bypass, Direct HTML Flow posters, or any Playwright `page.pdf()` path), you MUST deliver **both files** to the user: +1. **HTML** — the source HTML file, so the user can edit and reuse the design +2. **PDF** — the final vector PDF (`page.pdf()` output) + +Optionally also provide: +3. **Image** — a full-page screenshot/preview image (PNG or JPG) for quick sharing on chat/social media + +All file paths must be reported to the user. **Never deliver only the PDF without the HTML source.** + +--- + +## Tooling Reference + +### CLI: `python3 "$PDF_SKILL_DIR/scripts/pdf.py" <command>` + +```bash +# Environment +env.check # Check deps +env.fix # Auto-install missing + +# Quality +code.sanitize <script> # Sanitize forbidden Unicode +content.sanitize <file> [--apply] # Fix content issues (CJK, encoding) +meta.brand <pdf> # Add Z.ai metadata +font.check <pdf> # Scan for missing glyphs +toc.check <pdf> # Validate TOC + +# Conversion +convert.blueprint <llm_json_response.md> -o final.pdf # CRITICAL FOR CREATIVE: Auto-extracts JSON, compiles, and renders PDF. +convert.html <html> # HTML → PDF (Playwright) +convert.latex <tex> # LaTeX → PDF (Tectonic). Bundled binary is macOS arm64 only; see academic.md for other-platform install. +convert.office <file> # Office → PDF (LibreOffice) + +# Processing +extract.text <pdf> # Extract text +extract.table <pdf> # Extract tables +extract.image <pdf> # Extract images +pages.merge a.pdf b.pdf -o out.pdf +pages.split <pdf> +pages.clean <pdf> # Remove blank pages +form.info <pdf> # Inspect form fields +form.fill <pdf> # Fill form +form.annotate <pdf> # Fill via annotations +meta.get <pdf> +meta.set <pdf> -o out.pdf -d '{"Title": "..."}' +``` + +### Poster/HTML/LaTeX Validator: `python3 "$PDF_SKILL_DIR/scripts/poster_validate.py"` +```bash +check-html <html> # Pre-render validation (overflow:hidden, @media screen, fonts, contrast, etc.) +check-html <html> --fix --output <fixed.html> # Auto-fix errors (remove overflow:hidden, add font fallback) +check-pdf <pdf> --source-html <html> # Post-render validation +check-pdf <pdf> --poster # Poster mode: suppress ORPHAN_PAGE warning +check-tex <tex> # LaTeX source validation (table overflow, image width, etc.) +``` + +**check-html checks include:** +- `OVERFLOW_HIDDEN_CONTAINER` (error): overflow:hidden on html/body/.page/.poster — clips content +- `FIXED_SIZE_NO_SCREEN_ADAPT` (warning): fixed-size page without @media screen auto-scale +- `SCREEN_ADAPT_NO_SCALE` (warning): @media screen exists but lacks scale/transform/zoom +- `FONT_NO_FALLBACK` (error): font-family without generic fallback (sans-serif/serif) +- `COLOR_CONTRAST` (warning): text/background contrast ratio < 3:1 +- `BG_COLOR_MISMATCH` (warning): body background differs from .canvas/.poster background +- `SCREEN_BG_MISMATCH` (warning): @media screen html background differs from body/canvas background +- `MULTIPAGE_BODY_BG_MISSING` (warning): multi-page document with dark `.page` backgrounds but no `html/body` background color. Sub-pixel gaps at page edges reveal white body, causing visible white edges on dark pages. Resolves `var()` references via `:root` variables. +- `SCREEN_NO_BG` (warning): fixed-size page's @media screen block lacks html background color +- `OVERFLOW_DECORATION` (warning): negative position values may cause black edges +- `NO_PAGE_SIZE` / `MISSING_MARGIN_RESET` / `WHITE_BACKGROUND` / `TINY_FONT` / etc. + +**check-tex checks include:** +- `BARE_TABULAR_OVERFLOW` (error): `\begin{tabular}` with 5+ columns in two-column layout, not wrapped in resizebox/adjustbox/table* +- `RESIZEBOX_TEXTWIDTH` (error): `\resizebox{\textwidth}` used inside single-column float in two-column layout. `\textwidth` = full page width, but `table` float is one column. Fix: use `\resizebox{\columnwidth}` or `table*` +- `TABULAR_OVERFLOW_RISK` (warning): 4-column tabular in two-column layout without width constraint +- `TABULAR_WIDE` (warning): 7+ column tabular in single-column layout without width constraint +- `TABULAR_NO_FLOAT` (warning): tabular not inside table/table* float environment +- `TABULARX_NOT_LOADED` (warning): document has tabular but tabularx package not loaded +- `IMAGE_NO_WIDTH` (warning): `\includegraphics` without width/height/scale constraint +- `EQUATION_DUAL_ON_LINE` (warning): `equation` environment has 2+ equations joined by `\quad` without line breaks. Guaranteed overflow in dual-column +- `EQUATION_OVERFLOW_RISK` (warning): equation body has >80 math characters. Likely overflows single column +- `ALGORITHM_NO_SMALL_FONT` (warning): `algorithm` environment in dual-column without `\SetAlFnt{\small}` +- `ALGORITHM_LONG_IO` (warning): Algorithm Input/Output line >120 chars. Will overflow narrow column +- `CJK_ASCII_QUOTES` (error): ASCII `"` found adjacent to CJK characters. LaTeX interprets `"` as right double quote, so `"北漂"` renders incorrectly. Skips verbatim/lstlisting/minted environments and `\texttt{}`/`\url{}`/`\href{}{}`/`\verb||` inline commands. + +### Design Engine: `python3 "$PDF_SKILL_DIR/scripts/design_engine.py"` +```bash +compile --blueprint <json_file> --output poster.html # CRITICAL: Compile JSON blueprint to HTML +derive "document title or description" # Auto-derive intent from content +palette --intent calm --mode dark # Generate HSL-locked palette +palette-cascade --intent cold --mode minimal # Generate role-based cascade palette (V2, preferred) +svg --intent flow --dimensions 720x960 # Generate SVG background +full --intent energy --mode dark --dimensions 720x960 --output-dir ./assets/ +audit --palette-json palette.json # Check palette constraints +``` + +### Palette Generator (for Report route): `python3 "$PDF_SKILL_DIR/scripts/pdf.py" palette.generate` +```bash +palette.generate --title "document title" --mode minimal # Output: ready-to-paste ReportLab Python code +palette.generate --title "..." --format json # Output: raw JSON +palette.generate --title "..." --format css # Output: CSS custom properties +palette.generate --title "..." --mode dark --harmony complementary --seed 42 +``` + +### Cascade Palette (V2 - Preferred): `python3 "$PDF_SKILL_DIR/scripts/pdf.py" palette.cascade` +```bash +palette.cascade --title "document title" --mode minimal # Output: summary table with all 12 roles +palette.cascade --title "..." --format json # Full structured JSON (roles + cover + body + charts + semantic) +palette.cascade --title "..." --format css # CSS custom properties by tier +palette.cascade --title "..." --format reportlab # Ready-to-paste ReportLab Python code +``` +**⚠️ Cascade palette is the preferred palette system.** It enforces area ∝ 1/saturation (larger areas = lower saturation) and outputs unified color subsets for cover, body, and charts from one base hue. Use `palette.cascade` instead of `palette.generate` for new documents. + +**⚠️ Report route MUST call `palette.cascade` (or `palette.generate`) before writing any ReportLab code.** The output is copy-paste ready - no manual hex picking allowed. + +> **Note**: `design_engine.py compile` produces **HTML** from a JSON blueprint. To get a **PDF**, use `pdf.py convert.blueprint` which internally calls `compile` → Playwright render → PDF output. In the Creative pipeline, always use `convert.blueprint` for the final PDF. + +### Tech Stack per Brief + +| Brief | Primary Tool | Secondary | Emoji Support | Custom Page Size | +|-------|-------------|-----------|---------------|-----------------| +| Report | ReportLab + pypdf | **Playwright (cover)** | ❌ (tofu □) | Manual pagination | +| Creative | Playwright | html2pdf-next.js (pdf-lib for post-processing) | ✅ native | ✅ any size | +| Academic | Tectonic + pypdf | **Playwright (cover)** | ❌ (dropped) | Template-dependent | +| Process | pikepdf, pdfplumber | LibreOffice (soffice) | N/A | N/A | + +> **Unified Cover System**: All routes generate covers via HTML/Playwright. Report uses Templates 01–07, Academic uses Templates 08–10 (dark backgrounds, scholarly typography), Creative generates cover + body in one HTML document. Cover PDFs are merged with body PDFs via pypdf. +> +> **Fallback**: If Report brief content has emoji → reroute to Creative. + +--- + +## File Map + +``` +SKILL.md ← You are here +briefs/ + report.md ← Report production: ReportLab workflow + API + resume(ATS) + creative.md ← Creative production: 5-phase generative design workflow + poster.md ← Poster scene rules: density, font sizing, fill constraints (overlay on creative.md) + academic.md ← Academic production: LaTeX workflow + templates + resume(CV) + process.md ← PDF processing: extract/merge/split/form/convert/reformat + process-advanced.md ← Advanced reference (encrypted/corrupted/OCR/batch/perf) - load on demand +configs/ + visual_framework.md ← Palette mode, color harmony, SVG background params + components.md ← Non-grid composition components (floating cards, etc.) + fonts.md ← Font stacks per pipeline (Creative/Report/Academic) +typesetting/ + palette.md ← Color system + typography + anti-patterns + spacing + cover.md ← Cover page layout system (7 layouts × 2-3 variants) + typography scale + color rules + cover-backgrounds.md ← Cover background rendering rules + transparency constraints + charts.md ← Chart styling + anti-stacking rules + axis/grid/legend treatment + overflow.md ← Bounding box system, text/image/table overflow prevention + pagination.md ← Cross-page integrity, orphan/widow control, CJK punctuation + typography.md ← Font size scale, line-height, spacing system + geometry.md ← Geometric anchor system (decorative elements, lines, shapes) + fill-engine.md ← Adaptive anti-void layout engine V2.0 +scripts/ + pdf.py ← CLI tool (30 subcommands) + pdf_qa.py ← PDF quality checker (metadata, fonts, overflow, margins, tables, formulas) + design_engine.py ← Generative SVG + palette engine (palette/svg/compile/derive/audit) + poster_validate.py ← HTML/PDF validator + toc_validate.py ← TOC validator + html2pdf-next.js ← Playwright + pdf-lib HTML→PDF converter for documents (no Paged.js) + html2poster.js ← Playwright HTML→PDF converter for posters/single-page (auto overflow:hidden, dynamic height) + cover_validate.js ← Cover-ONLY overlap detection (text vs decorative lines). Do NOT run on posters or documents — only on cover HTML in Report/Academic pipelines. +references/ + resume-altacv.tex ← AltaCV dual-column resume template (creative/tech) + resume-academic.tex ← Academic CV template (PhD/academic) +``` + +### Loading Protocol + +1. **Always read**: This file (SKILL.md) +2. **Read ONE brief**: The matched brief file - it contains the complete workflow +3. **Read typesetting on demand**: Only when the brief says to (standard tasks) +4. **Never load all files upfront** - briefs reference what they need + +### Script Path Setup (MANDATORY before any script call) + +All paths are relative to `$PDF_SKILL_DIR` — the single root variable for this skill. Resolve it once before calling any script: + +```bash +PDF_SKILL_DIR="<skill_directory>" # ← parent directory of this SKILL.md + +# Then all commands use $PDF_SKILL_DIR: +python3 "$PDF_SKILL_DIR/scripts/pdf.py" code.sanitize generate_pdf.py +python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.brand output.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" font.check output.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" toc.check output.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.clean output.pdf -o output_clean.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" output.pdf +python3 "$PDF_SKILL_DIR/scripts/poster_validate.py" check-html page.html +python3 "$PDF_SKILL_DIR/scripts/poster_validate.py" check-pdf output.pdf +``` + +**For Python imports** (when generation code needs to import skill modules): + +```python +import sys, os +PDF_SKILL_DIR = "<skill_directory>" +_scripts = os.path.join(PDF_SKILL_DIR, "scripts") +if _scripts not in sys.path: + sys.path.insert(0, _scripts) +``` + +**⚠️ NEVER use bare `python3 scripts/pdf.py ...`** - it only works if cwd happens to be the skill directory. Always use `$PDF_SKILL_DIR/scripts/` as the absolute prefix. + +--- + +## 8. Quality Checklist (Mandatory after every PDF generation) + +> The following checks come from the `typesetting/` spec files and are **mandatory** quality gates. + +### Automated Detection (Must Run) + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" <output.pdf> +python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" --poster <output.pdf> # poster mode: skip content fill ratio, check all pages for full-bleed +python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" --skip-cover --formulas <output.pdf> # academic mode: skip cover for margin check, enable formula overflow +python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" --no-tables <output.pdf> # creative mode: skip table centering check +``` + +> **Dependency**: Requires `pymupdf` (`pip install pymupdf`). If not installed, skip automated detection and use the manual checklist below. + +Run `pdf_qa.py` after generating a PDF. It auto-detects: metadata completeness, page size consistency, blank pages, CJK punctuation placement, color count, font embedding status, content overflow, content fill ratio, cover full-bleed, margin symmetry, table centering, formula overflow. +- **`--poster` mode**: skips content fill ratio check (poster last page naturally has less content), checks ALL pages for full-bleed (not just cover) +- **`--skip-cover`**: skips page 1 when checking margin symmetry (for documents with separately-generated covers) +- **`--no-tables`**: disables table centering check (for creative/poster documents that rarely have traditional tables) +- **`--formulas`**: enables formula overflow detection (checks if formula-like content extends past right content margin) +- Result PASS → deliver directly +- Result WARN → evaluate whether fix is needed, non-blocking +- Result FAIL → **must fix and regenerate** + +### Pagination & Layout (pagination.md) + +- [ ] **Last page fill ratio ≥ 40%**: No large blank areas on the final page. If insufficient, backtrack to compress spacing/line-height/font-size +- [ ] **Major section 3/4 threshold**: H1-level headings must NOT start in the bottom 25% of a page. If remaining space < 25%, force page break and start on fresh page. Use `CondPageBreak(available_height * 0.25)` in ReportLab, `\needspace{0.25\textheight}` in LaTeX +- [ ] **Tables don’t split across pages**: Table header and data rows must stay together. Small tables: `break-inside: avoid`. Large tables: `thead { display: table-header-group }` +- [ ] **Punctuation placement rules**: Commas, periods, etc. must not appear at line start. Set `line-break: strict` in CSS +- [ ] **No orphan headings**: Headings must not appear alone at page bottom. Use `break-after: avoid` +- [ ] **Cards/images not cut**: `break-inside: avoid` + +### Overflow Prevention (overflow.md) + +- [ ] **All table cells use Paragraph() wrapping** (ReportLab): Never plain strings - they don't wrap and overflow +- [ ] **sum(colWidths) ≤ available_width**: Verified in code, not assumed +- [ ] **Images/charts proportionally scaled**: Never inserted at original dimensions; always `fit_image()` or `max-width: 100%` +- [ ] **Long tables have repeatRows=1**: Table header repeats on every page when table breaks across pages +- [ ] **Heading + first paragraph in KeepTogether**: Prevents orphan headings at page bottom +- [ ] **Chart + caption in KeepTogether**: Prevents chart on one page, caption on next +- [ ] **CJK text uses wordWrap='CJK'**: Required for proper line-breaking of Chinese/Japanese/Korean +- [ ] **URLs/long strings have word-break**: `overflow-wrap: break-word` (HTML) or manual splitting (ReportLab) +- [ ] **Font degradation fallback**: Tight columns can shrink font by up to 3pt before clipping + +### Color (palette.md) - Report & Creative only + +> **Academic (LaTeX) documents are exempt** from this color system. LaTeX uses template-defined styling. +- [ ] **Entire document ≤ 5 colors**: Primary + secondary + accent + neutral + background +- [ ] **All colors traceable to primary**: Secondary and accent derived via lightness/saturation/micro-hue shift +- [ ] **Sibling elements not differentiated by different hues**: Use opacity/lightness/borders instead +- [ ] **Gradient endpoints hue difference < 20°**: No warm-to-cool gradients +- [ ] **No high-saturation color blocks**: Avoid eye strain + +### Cover V2 (cover.md) + +- [ ] **Evaluate whether a cover is needed**: Reports, proposals, analysis, white papers, manuals ≥ 3 pages → **always add cover (default ON)**. Skip cover ONLY for: resumes, CVs, letters, memos, forms, checklists, invoices, internal notes, or documents ≤ 2 pages +- [ ] **Single PDF output**: Cover is merged into the final PDF as page 1. **Report/Academic**: cover generated via HTML/Playwright → merged as page 0 via pypdf. **Creative**: cover is part of the same HTML document. NEVER deliver a separate cover file +- [ ] **Page isolation**: Cover NEVER shares a page with TOC or body content. **Report/Academic**: inherent via pypdf merge (separate PDFs). **Creative**: CSS page-break ensures isolation +- [ ] **Absolute Anchor Grid**: All elements use percentage Y-anchors (Part 0, A0.1). NO flow-based layout +- [ ] **Z-Index Layers**: Render in strict order: Layer 0 (bg fill) → Layer 1 (decorative, CLIPPED) → Layer 2 (structure lines) → Layer 3 (text) +- [ ] **Typography Weight System**: Use weight/spacing/opacity hierarchy per A0.2 (Kicker: 16pt+3pt spacing+60% opacity; Hero: 45-65pt Heavy; Meta: 20-22pt; Summary: 16-18pt Regular line-height 1.6) +- [ ] **Mandatory Summary Block** 🆕: Every cover MUST include a Summary/Description drawer (2-4 lines). If user provides none, auto-generate placeholder text (S3.5) +- [ ] **Safety checks**: Hero title overflow (max 3 lines, auto-reduce font S3.1); Zone collision detection (S3.2); Uppercase lock for Latin kickers/footers/watermarks (S3.3); Hard width boundary enforcement (S3.4); Summary auto-generation (S3.5); **Background watermark full-display enforcement (S3.6)** +- [ ] **Background watermark complete** 🆕: All background layer watermark text (year, document type, sidebar text) must be 100% visible within page bounds - auto-shrink font if needed, NEVER clip/truncate +- [ ] **Data binding correct** 🆕: Hero Title = company/entity name (biggest, heaviest text); Kicker = report type/subtitle (small decorative text). NEVER reverse this mapping +- [ ] **Fill Engine applied** 🆕: Font floor enforced (body ≥ 14pt single-col / 12pt dual-col, H1 ≥ 32pt, H2 ≥ 24pt, H3 ≥ 18pt); Fill Ratio calculated; inflation triggered when < 65%; Y-axis golden-ratio anchor when < 40% +- [ ] **Selected one of 7+4 templates**: General templates 01–07 + Academic templates 08–10 + Institutional template 11. Autonomously select the best-fit template by analyzing document intent (Calm/Tension/Energy/Authority/Warmth) and document type per Part 2 Intent × Type matrix. Thesis proposals/dissertations/institutional submissions → **default Template 11**. No global default - every selection must be a deliberate design decision +- [ ] **Typography weight hierarchy**: Hero 45-65pt Heavy, Meta 20-22pt Regular, Kicker/Footer 16pt with 3pt letter-spacing + 60% opacity, Summary 16-18pt Regular +- [ ] **Base spacing unit**: `U = W * 0.05` - all spacing should be multiples of U +- [ ] **Bounding box via absolute anchors**: Each block anchored to fixed Y%, grows only within its own zone, never pushes adjacent blocks +- [ ] **Safe zone margin**: 8-12% on all sides per template spec (corner marks for Template 04 at 8%) +- [ ] **Cover whitespace ≥ 60%**: Restraint > clutter (but Summary block fills mid-page void intentionally) +- [ ] **Cover colors consistent with body**: No independent color scheme; white/light backgrounds only +- [ ] **Clip-path on Layer 1**: All background decorative elements must be clipped to page bounds +- [ ] **Clip scope = Layer 1 ONLY** �F: `saveState()`/`clipPath()` must `restoreState()` BEFORE rendering Layer 2 lines and Layer 3 text. Text rendered inside clip scope = text gets cut off +- [ ] **No page border/frame** �F: Cover page must have `showBoundary=0`, no `canvas.rect(0,0,W,H)`, no CSS border/outline on cover container +- [ ] **Line-to-text minimum gap** �F: Decorative lines (Layer 2) must be at least `U` (= `W * 0.05`) away from any text content +- [ ] **No dark/gradient backgrounds**: No dark fills, no gradients, no high-saturation schemes +- [ ] **Hard width enforcement**: Text wraps vertically at zone boundary, NEVER bleeds horizontally past assigned width +- [ ] **🚫 NEVER use ReportLab for covers** — ALL covers (Report, Creative, Academic) are generated via HTML/Playwright. See cover.md for the 10-template system. If you catch yourself writing `canvas.setFillColor()` + `canvas.rect()` for a cover background, STOP — switch to HTML/Playwright. +- [ ] **Line-length alignment (S3.7)**: Vertical lines match text block height (± 1U); horizontal lines ≥ widest text element width (never shorter than text) +- [ ] **Vertical balance (S3.8)**: No >40% dead whitespace at bottom; sparse content uses centered distribution; CJK titles 15-20% larger than Latin equivalent +- [ ] **Percentage positioning safety (S3.9)**: Every element with `top: XX%` must have a containing block with deterministic height (`height: 100%`, `inset: 0`, or `top+bottom` pair). Wrappers without explicit height + percentage-positioned children = overlap bug. Prefer px values over percentages +- [ ] **Cover colors from palette system**: All `:root` CSS variables populated by `palette.cascade` output. Template HTML uses `--c-bg`, `--c-accent`, `--c-text`, `--c-muted` — no hardcoded hex values in generated HTML + +### Geometric Anchors (geometry.md) + +- [ ] **Anchors use only the primary color**: Layer via opacity, don't mix colors +- [ ] **Strokes over fills**: Solid elements ≤ 30% +- [ ] **Ultra-thin lines**: stroke-width 0.3-0.8px +- [ ] **Asymmetric placement**: Offset creates tension +- [ ] **Elements ≤ 8**: Restraint, don't clutter + +### Charts (charts.md) + +- [ ] **No text stacking/overlap**: All chart labels, values, and legends must be collision-free +- [ ] **Chart-to-text separation**: Minimum 24pt gap above and below charts; 8pt between chart and caption; 30pt between consecutive charts +- [ ] **Legend-to-chart non-overlap**: Legend MUST NOT overlap chart data area. Use `bbox_to_anchor` or external placement +- [ ] **Value label anti-collision**: Adjacent value labels that overlap must be staggered, rotated, or selectively hidden +- [ ] **Pie charts → Donut by default**: hole_ratio 60-70%, center shows total/core metric +- [ ] **Small pie slices handled**: Slices < 5% use leader lines, < 3% merge to "Others", or strip labels to rich legend +- [ ] **Bar chart auto-rotation**: If X-axis labels avg > 5 CJK chars (or 10 Latin), auto-convert to horizontal bars +- [ ] **Line chart labeling**: Only label start, end, max, min points - NOT every data point +- [ ] **Axis cleanup**: Top/right spines deleted, grid lines dashed at 0.5pt/20% opacity (or hidden if values labeled) +- [ ] **Bar micro-rounding**: Top border-radius 2-4px, bar-to-gap ratio 1.5:1 or 2:1 +- [ ] **Legend de-boxed**: No border on legend, horizontal layout, small circle markers +- [ ] **Chart title hierarchy**: Bold main title left-aligned above chart, lighter subtitle below it + +### Global Layout + +- [ ] **Margin symmetry**: `left_margin == right_margin` - asymmetric margins cause off-center content (ReportLab, LaTeX, HTML all checked) +- [ ] **Full-bleed enforcement (Playwright)**: HTML includes `@page { size: <w> <h>; margin: 0; }` and `html,body { margin:0; padding:0; }`. No white side margins in the output PDF +- [ ] **Background color consistency (Playwright)**: `html, body { background }` set explicitly. Single-color docs: match content canvas. Multi-page mixed docs: use the darkest page's background color. Mismatch or missing = sub-pixel white edges on dark pages +- [ ] **Content centering (Playwright)**: Content is centered in PDF, not drifting left or right. Check: symmetric inset/padding, full-width grid columns, no unbalanced max-width +- [ ] **Anti-void edges**: No large meaningless blank areas at top, bottom, or sides. Content fills ≥ 60% of page (multi-page) or ≥ 70% (single-page poster/infographic) +- [ ] **Fill Engine applied**: Pages with < 80% fill ratio trigger the fill engine (see `fill-engine.md`) +- [ ] **Table centering**: ALL tables must be horizontally centered on the page. ReportLab: use `hAlign='CENTER'` on Table flowable. LaTeX: use `\centering` inside table environment. HTML: use `margin: 0 auto` on table element. NEVER let tables float left with right-side whitespace +- [ ] **Table column width**: Table total width should be 85-100% of content area width. Avoid narrow tables (< 60% width) that look lost on the page. If table is narrow, expand column widths proportionally or use `colWidths` to fill available space + +### Exam / Quiz / Test Paper Rules + +- [ ] **Question numbering**: Use hierarchical numbering (一、二、三 for sections; 1. 2. 3. for questions; (1)(2)(3) or A B C D for sub-questions/options) +- [ ] **Option indentation**: Multiple-choice options MUST be indented relative to the question stem. Minimum `leftIndent = 24pt` (2em). Options must NEVER start at the same X position as the question number +- [ ] **Option layout**: ≤4 short options (≤4 chars each) → 2×2 grid or single row. >4 options or long text → vertical list, one per line. Each option on its own line gets consistent indentation +- [ ] **Answer space reservation**: MUST reserve blank space for handwritten answers. Calculation: short answer = 2-3 blank lines (40-60pt); paragraph/essay = 8-15 blank lines (160-300pt); math work = 6-10 blank lines (120-200pt); fill-in-the-blank = inline underline (min 80pt width). Use `Spacer(1, height)` in ReportLab +- [ ] **Answer line style**: Use light gray dashed or dotted horizontal lines for answer areas, NOT solid black lines. Line weight ≤ 0.5pt, color = #cccccc or lighter +- [ ] **Score marking area**: Each question should have a score indicator in the margin or after the question number, e.g., “(10分)” or “[10 pts]” +- [ ] **Page density**: Exam papers should NOT be cramped. Minimum `spaceBefore=12pt` between questions. Section headers get `spaceBefore=24pt` + +### Design Restraint (Anti-Gaudy) + +- [ ] **Decorative elements ≤ 3 per page**: Maximum 3 decorative/non-functional visual elements per page (lines, shapes, icons, patterns). Cover page exempt +- [ ] **No gratuitous icons/emoji in headers**: Section headers should use typography hierarchy (size, weight, color) for emphasis — NOT emoji, icons, or decorative bullets unless the user explicitly requested them +- [ ] **No rainbow/multi-color schemes**: Stick to the single-family palette system. If you find yourself using 4+ distinct hue families in one document, STOP and simplify +- [ ] **No decorative borders on body pages**: Body content pages must NOT have decorative borders, corner ornaments, or page frames. Clean margins only. (Cover page Template 11 border is the sole exception) +- [ ] **No texture/pattern backgrounds on body pages**: Body pages use solid white or ultra-light tinted backgrounds only. No dot grids, crosshatch, diagonal lines, or any pattern fills +- [ ] **Whitespace is design**: Empty space between elements is intentional and valuable. Do NOT fill every gap with decorative elements, horizontal rules, or filler content +- [ ] **Typography over decoration**: Create visual hierarchy through font size, weight, spacing, and color — not through adding more visual elements. If a design looks busy, REMOVE elements rather than rearranging them +- [ ] **2-typeface maximum**: Entire document uses at most 2 font families (one serif, one sans-serif). No mixing 3+ fonts for “variety” +- [ ] **🚫 NO stock images / clipart / AI-generated decorations**: NEVER embed watercolor flowers, floral borders, gold frames, stock photos, clipart illustrations, or AI-generated artwork for decoration. Use geometric shapes (CSS/SVG from geometry.md) + typography for all visual design. Only user-provided content images (photos, logos, diagrams) are allowed. See `visual_framework.md` Stock Image Ban + +### LaTeX-Specific (academic.md) + +- [ ] **Curly quotes**: No straight `"` quotes - use `` ``text'' `` for double and `` `text' `` for single +- [ ] **Title page isolation**: `\end{titlepage}` followed by `\newpage`/`\clearpage` - TOC/body NEVER on same page as title +- [ ] **Resume column overlap**: AltaCV `paracol` entries checked for vertical overflow; max 3-4 bullets per `\cvevent`; explicit `\newpage` for 2-page resumes +- [ ] **`\geometry` symmetry**: `left=X, right=X` must be equal values + +### Output Cleanliness (All Pipelines) + +- [ ] **No process artifacts in output**: NEVER include version numbers ("V3"), iteration markers, draft labels ("DRAFT"), "CONFIDENTIAL"/"机密" stamps, "Generated by AI"/"本文档由AI生成", or internal comments in the final PDF unless the user explicitly requested them +- [ ] **No auto-generated boilerplate labels**: Do not add ANY watermarks, generation notices, version numbers, timestamps, or tool names that the user didn't ask for +- [ ] **No debug output in content**: Console logs, file paths, generation timestamps, tool names, or error messages must never appear in the PDF body +- [ ] **Clean metadata only**: PDF metadata (author, title, subject) should reflect the document content, not the generation process diff --git a/skills/pdf/briefs/academic.md b/skills/pdf/briefs/academic.md new file mode 100755 index 0000000..166febb --- /dev/null +++ b/skills/pdf/briefs/academic.md @@ -0,0 +1,1058 @@ +# Brief: Academic Production + +Scholarly documents via LaTeX/Tectonic: academic papers, theses, dissertations, mathematical manuscripts, IEEE/ACM submissions, academic CVs. + +``` +Academic request + ├─ Paper / thesis / journal → §Standard Paper Workflow + │ Embed as needed during Phase 3: + │ ├─ Heavy math (>3 equations) → use §Scenario A preamble + environments + │ ├─ Vector diagrams → §Scenario B (simple→TikZ, complex→Playwright+CSS) + │ └─ Algorithm pseudocode → use §Scenario C template + │ + └─ Standalone diagram for other brief → §Scenario B Path B (Playwright+CSS → PNG) + │ + └─ Resume / CV + ├─ Creative / tech / startup → §Template A (AltaCV dual-column) + └─ Academic position / PhD → §Template B (Academic CV) +``` + +**No typesetting assets needed** - LaTeX templates handle their own design system. The `palette.md` color system does not apply to LaTeX documents. + +--- + +## §Standard Paper Workflow + +Six phases. Rules and guardrails are embedded in each phase where they apply. + +### Phase 1 - BRIEF + +Confirm with the user: +- Document type (journal article, thesis chapter, conference paper, technical report) +- Template requirement (IEEE, ACM, custom class, or plain `article`) +- Bibliography format (natbib superscript, numeric brackets, biblatex) +- Expected length → if >5 pages or 3+ complex elements (wide tables, block equations, TikZ), split into `\input{}` modules (~500 lines each) + +**Recognising the document's personality:** + +| Personality | Examples | Key concern | +|-------------|----------|-------------| +| Scholarly | Journal articles, conference proceedings | Academic conventions, bibliography accuracy | +| Utilitarian | Technical reports, manuals, specs | Information density + scannability | +| Persuasive | Proposals, pitch documents | Professional polish + 1-2 visual high-points | +| Expressive | Portfolios, brand guidebooks | Bold typographic choices | + +### Phase 2 - SETUP + +**Title Page (Cover) Rules:** +- **Academic route covers are generated via HTML/Playwright**, using Templates 08-11 from `typesetting/cover.md`. Templates 08-10 replicate LaTeX title page aesthetics (dark backgrounds, serif titles, symmetric layouts) in HTML/CSS. **Template 11 (Institutional)** is for thesis proposals, dissertations, and formal institutional submissions (white bg + black border frame). +- **Pipeline:** Generate body PDF via Tectonic (no title page in `.tex`) → Generate cover HTML (Template 08/09/10/11) → Playwright `page.pdf()` → Merge cover as page 0 via pypdf +- **Template selection:** For thesis proposals (开题报告), dissertations (毕业论文), and institutional submissions → **default to Template 11**. For research papers, preprints, journal submissions → Templates 08-10. +- **NEVER use `\maketitle`** - it produces ugly default output with cramped spacing +- **NEVER use `\begin{titlepage}...\end{titlepage}`** - the cover is generated separately via HTML/Playwright +- **NEVER use LaTeX TikZ overlay for full-page covers** - TikZ `current page` coordinates are unreliable with `margin=0pt`, causing backgrounds to not fill the page (right/bottom white edges). HTML/CSS full-bleed is pixel-exact. +- Title page is OPTIONAL - skip it for short documents (≤ 2 pages), letters, memos, or when content scanning is priority +- **`\tableofcontents` must be the FIRST page** of the body PDF (after merge, it becomes page 2) +- If no TOC, content starts on page 1 of the body PDF + +**Cover Generation Pipeline (Academic route):** +``` +1. Write .tex WITHOUT any title page +2. Run poster_validate.py check-tex on .tex file - fix table overflow / image width ERRORs +3. Compile with tectonic → body.pdf +4. Write cover HTML using Template 08/09/10/11 from typesetting/cover.md (Template 11 for thesis proposals/dissertations) +5. Run poster_validate.py check-html on cover HTML - fix any ERRORs +6. Run cover_validate.js on cover HTML - fix any text-line overlaps +7. Render cover HTML → PDF via Playwright (`html2poster.js`) — **NOT `html2pdf-next.js`** (which converts absolute→static and destroys cover layout) +8. Merge: insert cover as page 0 of body PDF via pypdf +``` + +**Cover HTML → PDF rendering:** +```bash +# ALWAYS use html2poster.js for cover rendering (NOT html2pdf-next.js) +# Cover pages use position:absolute layout — html2pdf-next.js pre-render hooks +# convert absolute→static and destroy the layout. html2poster.js preserves it. +node "$PDF_SKILL_DIR/scripts/html2poster.js" cover.html --output cover.pdf --width 794px +``` + +Or from Python: +```python +import subprocess, os + +def render_cover(html_path, pdf_path): + """ + Render HTML cover to PDF via html2poster.js. + + ⚠️ ALWAYS use html2poster.js for covers (NOT html2pdf-next.js). + Cover HTML uses position:absolute for layout. html2pdf-next.js pre-render + hooks convert absolute→static to prevent multi-page overlap, which + destroys cover layouts. html2poster.js preserves absolute positioning. + """ + scripts_dir = os.path.join(PDF_SKILL_DIR, 'scripts') # PDF_SKILL_DIR from SKILL.md § Script Path Setup + subprocess.run([ + 'node', os.path.join(scripts_dir, 'html2poster.js'), + html_path, '--output', pdf_path, + '--width', '794px', + ], check=True) +``` + +**Merge cover + body:** +```python +from pypdf import PdfReader, PdfWriter, Transformation + +A4_W, A4_H = 595.28, 841.89 # A4 in points + +def normalize_page_to_a4(page): + """Scale a page to A4 if its dimensions don't match.""" + box = page.mediabox + w, h = float(box.width), float(box.height) + if abs(w - A4_W) > 2 or abs(h - A4_H) > 2: + sx, sy = A4_W / w, A4_H / h + page.add_transformation(Transformation().scale(sx=sx, sy=sy)) + page.mediabox.lower_left = (0, 0) + page.mediabox.upper_right = (A4_W, A4_H) + return page + +writer = PdfWriter() +cover_page = normalize_page_to_a4(PdfReader('cover.pdf').pages[0]) +writer.add_page(cover_page) +for page in PdfReader('body.pdf').pages: + writer.add_page(page) +with open('final.pdf', 'wb') as f: + writer.write(f) +``` + +**→ Full cover templates: see §PART 4.5 in `typesetting/cover.md` (Templates 08-10).** + +> **⚠️ Why HTML/Playwright covers?** LaTeX TikZ `remember picture, overlay` with `margin=0pt` frequently fails to fill the page (right/bottom edges show white). HTML/CSS with `@page { margin: 0 }` and full-bleed background is pixel-exact, with zero ambiguity. This also unifies all three routes (Report, Creative, Academic) under one cover system. + +Write the preamble. Start from this foundation and customise per document: + +```latex +\documentclass{article} + +\usepackage{graphicx} +\usepackage{xcolor} +\usepackage{geometry} +\usepackage{amsmath} % Load before hyperref + +% hyperref - ALWAYS last among content packages +\usepackage[ + colorlinks=true, + linkcolor=blue, + citecolor=darkgray, + urlcolor=blue, + bookmarks=true, + bookmarksnumbered=true, + unicode=true +]{hyperref} + +\geometry{a4paper, top=2.5cm, bottom=2.5cm, left=2.5cm, right=2.5cm} +% ⚠️ left and right MUST be equal - asymmetric margins cause off-center content + +\usepackage[numbers,super,sort&compress]{natbib} +\bibliographystyle{unsrtnat} + +\usepackage{tcolorbox} +\usepackage{colortbl} +\usepackage{booktabs} +\usepackage{enumitem} +\usepackage{tabularx} % Auto-width columns (X type) - prevents table overflow +\usepackage{adjustbox} % \adjustbox{max width=\columnwidth} for emergency table fitting +``` + +**Guardrails for SETUP:** +- `hyperref` must load after virtually every other package - option clashes are the #1 preamble bug +- When using a Scenario template (A/B/C) or Resume template, use that template's own preamble instead +- `babel` and `polyglossia` are incompatible - load only one +- CJK: `\usepackage{ctex}` - Tectonic auto-downloads fonts, zero manual setup +- System fonts via `\setmainfont{}`: probe first with `fc-list :lang=XX` +- **🔴 Margin symmetry:** `\geometry{left=X, right=X}` - left and right MUST be equal. Asymmetric margins = off-center content = critical bug +- **🔴 Minimum margins with fancyhdr:** When using `fancyhdr` for headers/footers, `geometry` margins must leave enough room. **Minimum: `top >= 2.0cm`, `bottom >= 1.8cm`**. Also set `\setlength{\headheight}{14pt}` in the preamble. Margins smaller than this cause headers/footers to be pushed outside the page boundary (negative y-coordinates), making them invisible in print. +- **🔴 Quotation marks (English):** NEVER use straight quotes `"..."`。English text must use LaTeX curly quotes: ` ``left quote'' ` for double, `` `single' `` for single. Straight `"` in LaTeX means right double quote only. +- **🔴 Quotation marks (Chinese — CRITICAL):** Chinese quoted text like "北漂" MUST use Unicode smart quotes "…" (U+201C/U+201D) directly in the `.tex` source. **NEVER use ASCII `"` for Chinese quotes** — LaTeX interprets `"` as a right double quote (`"`), so `"北漂"` renders as `"北漂"` (two right quotes, no left quote). The correct LaTeX source is: `"北漂"` (literal Unicode characters). `\usepackage{csquotes}` is a safety net but does NOT fix raw ASCII `"` in Chinese text. + - **Scope:** This rule applies ONLY to Chinese-language body text. Do NOT replace `"` in English paragraphs (use ` ``...'' ` instead), `verbatim`/`lstlisting`/`minted` environments, `\texttt{}`/`\verb||`/`\url{}`/`\href{}{}` arguments, or BibTeX `.bib` field values. +- **🔴 Title page isolation:** Cover is generated via HTML/Playwright and merged as page 0 via pypdf - isolation is inherent in the merge pipeline. `\tableofcontents` should be the first page of the `.tex` body. Verify: does TOC start on the page immediately after the cover in the merged PDF? +- **🔴 TOC requires a cover page:** Unless the user explicitly requests no cover, if the document has `\tableofcontents`, it MUST have a cover page. Structure: Cover (page 1) → TOC (page 2) → Content (page 3+). Do not generate a TOC without a preceding cover page. This rule is consistent with `briefs/report.md`. + +**When no style is specified**, apply a measured, high-craft system: +1. **Contrast** - clear figure-ground separation +2. **Hierarchy** - size, weight, hue variation for reading order +3. **White space** - ample margins and leading +4. **Coherence** - one typeface family, one accent colour, one spacing rhythm + +Add enrichment proactively when content benefits: +- Callout boxes, sidebars → `tcolorbox` +- Theorem/definition/proof → `amsthm` + `tcolorbox` +- Headers/footers → `fancyhdr`; chapter openers → `titlesec` + +### Phase 3 - BUILD + +Write LaTeX content: sections, equations, figures, tables, bibliography. + +**→ Overflow prevention**: See `typesetting/overflow.md` for the LaTeX-specific patterns (tabularx, adjustbox, widowpenalty, etc.). Key rules: +- Tables: always use `tabularx` or `tabular*` with `\columnwidth` constraint - never plain `tabular` for 5+ columns +- Images: always `\includegraphics[max width=\columnwidth, max height=0.4\textheight]` or `adjustbox` - the `max height` prevents a single figure from occupying an entire page +- Orphans/widows: set `\widowpenalty=10000` and `\clubpenalty=10000` +- Long tables: use `longtable` with `\endhead` for header repetition on every page + +**Embedding components**: If the document needs heavy math, diagrams, or algorithms, refer to the Scenario sections below and embed their templates/environments into your `.tex` file. Scenarios are not separate tasks - they are building blocks for Phase 3. + +**Content density guidance (for textbooks, lecture notes, tutorials):** + +Documents meant for learning should maintain a healthy balance of prose and formal elements. A page full of equations with no explanation reads like a reference manual, not a textbook. + +Guidelines: +- Every equation/equation group should be **preceded** by a sentence explaining what it represents and **followed** by a sentence interpreting the result or stating its significance +- Every figure should have (a) a descriptive `\caption{}` and (b) at least one sentence in the surrounding text referencing it +- Every theorem/definition should be followed by an intuitive explanation or worked example before proceeding to the next theorem +- Avoid stacking 3+ formal elements (equations, figures, tables) with zero narrative text between them +- For worked examples: state the problem, show the solution steps, then summarize the key takeaway + +**When high density is acceptable**: research papers (especially methods sections where equation groups naturally cluster), formula sheets, reference appendices, and conference papers with tight page limits. In these cases, prioritise completeness over readability padding. + +**Source hygiene - catch these model-generation slips:** +- **Prohibited**: emoji glyphs (tofu), markdown `*asterisk*` formatting (compile errors) +- **Use instead**: `\textbf{bold}`, `\emph{emphasis}` + +**Table placement - prevent header orphans:** +- Short tables (≤15 rows): wrap in `\begin{table}[htbp]` - LaTeX keeps it together +- Long tables (>15 rows): use `longtable` with repeated header: +```latex +\usepackage{longtable} +\begin{longtable}{lll} +\toprule +Header 1 & Header 2 & Header 3 \\ +\midrule +\endfirsthead +\toprule +Header 1 & Header 2 & Header 3 \\ % repeated on continuation pages +\midrule +\endhead +\bottomrule +\endfoot +Row 1 & data & data \\ +Row 2 & data & data \\ +\end{longtable} +``` +- **Never** let a table header sit alone at the bottom of a page with no data rows following it + +**Table width management - prevent column overflow (⚠️ CRITICAL):** + +Tables overflowing the column width is the most common LaTeX layout bug in dual-column papers. The table looks fine in single-column preview but clips in IEEE/ACM two-column format. + +**Prevention strategy (in priority order):** + +1. **Use `tabular*` or `tabularx` to constrain width** (RECOMMENDED): +```latex +% tabular* - fixed total width, stretches inter-column space +\begin{table}[htbp] +\centering +\caption{Results.}\label{tab:results} +\begin{tabular*}{\columnwidth}{@{\extracolsep{\fill}} l cccc} +\toprule +Method & P@10 & R@10 & NDCG@10 & MRR \\ +\midrule +Ours & \textbf{0.082} & \textbf{0.054} & \textbf{0.043} & \textbf{0.029} \\ +\bottomrule +\end{tabular*} +\end{table} + +% tabularx - fixed total width, X columns auto-stretch +\usepackage{tabularx} +\begin{tabularx}{\columnwidth}{l X X X X} +... +\end{tabularx} +``` + +2. **Reduce font size inside table** (common in conference papers): +```latex +\begin{table}[htbp] +\centering +\small % or \footnotesize for very wide tables +\caption{Comparison.}\label{tab:comp} +\begin{tabular}{lcccccccc} +... +\end{tabular} +\end{table} +``` + +3. **`\resizebox` as last resort** (scales the entire table to fit): +```latex +\begin{table}[htbp] +\centering +\caption{Full results.}\label{tab:full} +\resizebox{\columnwidth}{!}{% +\begin{tabular}{lcccccccc} +\toprule +... +\bottomrule +\end{tabular} +} +\end{table} +``` +⚠️ `\resizebox` scales fonts too - verify the smallest text is still readable (≥ 6pt effective). + +4. **Span both columns** for genuinely wide tables (8+ data columns): +```latex +\begin{table*}[t] % table* spans full width in twocolumn +\centering +\caption{Cross-dataset results.}\label{tab:cross} +\begin{tabular}{lccccccccccc} +... +\end{tabular} +\end{table*} +``` + +**Decision checklist before writing any table:** +| Data columns | Single-column (`\columnwidth`) | Action | +|-------------|-------------------------------|--------| +| ≤ 4 | Fits comfortably | Normal `tabular` | +| 5-6 | Tight fit | `\small` + `tabular*{\columnwidth}` | +| 7-8 | Won't fit | `\footnotesize` + `tabular*`, or `\resizebox` | +| ≥ 9 | Definitely won't fit | Use `table*` (full width), or split into two tables | + +**Never**: use a plain `tabular` with 8+ columns in a two-column paper without width constraint - it WILL overflow. + +**⚠️ CRITICAL: `\resizebox{\columnwidth}` NOT `\resizebox{\textwidth}` in two-column layouts!** +In dual-column documents (`twocolumn`, `sigconf`, etc.), `\textwidth` = **full page width** (both columns), while `\columnwidth` = **single column width**. Using `\resizebox{\textwidth}` inside a `table` (single-column float) scales the table to the full page width, causing it to overflow the column boundary by ~50%. **Always use `\resizebox{\columnwidth}` for single-column floats.** Only use `\resizebox{\textwidth}` inside `table*` (full-width float). + +```latex +% ❌ WRONG in two-column layout +\begin{table}[t] + \resizebox{\textwidth}{!}{% <-- \textwidth = full page, table overflows column! + \begin{tabular}{lcccccccc} ... \end{tabular}} +\end{table} + +% ✅ CORRECT for single-column float +\begin{table}[t] + \resizebox{\columnwidth}{!}{% <-- \columnwidth = single column width + \begin{tabular}{lcccccccc} ... \end{tabular}} +\end{table} + +% ✅ CORRECT for full-width float +\begin{table*}[t] + \resizebox{\textwidth}{!}{% <-- \textwidth OK here because table* spans both columns + \begin{tabular}{lcccccccc} ... \end{tabular}} +\end{table*} +``` + +--- + +**Equation overflow prevention (⚠️ CRITICAL for dual-column papers):** + +Long equations are the **#2 overflow source** after tables in dual-column papers. Column width in ACM `sigconf` is ~241pt; in IEEE `twocolumn` ~252pt. Many standard math expressions exceed this. + +**Overflow patterns and fixes:** + +| Pattern | Problem | Fix | +|---------|---------|-----| +| Two equations side-by-side with `\quad` | Combined width > column | Split into `align` with one equation per line | +| Deep fraction nesting (softmax, attention) | Denominator sum too wide | Use `\smash` + separate definition, or `split` | +| Long subscripts/superscripts with `\text{}` | `\text{collab}`, `\text{social}` are wide | Use short math abbreviations: `c`, `s`, or define `\newcommand` | +| `equation` with multiple terms separated by `\quad` | Horizontal overflow | Use `aligned` inside `equation`, or `align` | + +**Rule M1 — Never put two independent equations on one line in dual-column:** +```latex +% ❌ WRONG — two full equations on one line, guaranteed overflow in sigconf +\begin{equation} +\mathbf{e}_u^{(l+1)} = \sum_{i} \frac{1}{\sqrt{|N_R(u)|\cdot|N_R(i)|}} \mathbf{e}_i^{(l)}, \quad +\mathbf{e}_i^{(l+1)} = \sum_{u} \frac{1}{\sqrt{|N_R(i)|\cdot|N_R(u)|}} \mathbf{e}_u^{(l)} +\end{equation} + +% ✅ CORRECT — split into aligned or separate equations +\begin{align} +\mathbf{e}_u^{(l+1)} &= \sum_{i \in \mathcal{N}_R(u)} \frac{\mathbf{e}_i^{(l)}}{\sqrt{|\mathcal{N}_R(u)| \cdot |\mathcal{N}_R(i)|}}, \label{eq:collab_u} \\ +\mathbf{e}_i^{(l+1)} &= \sum_{u \in \mathcal{N}_R(i)} \frac{\mathbf{e}_u^{(l)}}{\sqrt{|\mathcal{N}_R(i)| \cdot |\mathcal{N}_R(u)|}}. \label{eq:collab_i} +\end{align} +``` + +**Rule M2 — Wide fractions: use `split` or `multline`:** +```latex +% ❌ WRONG — softmax with long denominator +\begin{equation} +\alpha_{uv} = \frac{\exp(\text{LeakyReLU}(\mathbf{a}^{\top}[\mathbf{W}\mathbf{e}_u \| \mathbf{W}\mathbf{e}_v]))}{\sum_{k \in \mathcal{N}_S(u)} \exp(\text{LeakyReLU}(\mathbf{a}^{\top}[\mathbf{W}\mathbf{e}_u \| \mathbf{W}\mathbf{e}_k]))} +\end{equation} + +% ✅ CORRECT — define numerator/denominator separately +\begin{equation} +\alpha_{uv} = \frac{\exp\bigl(f(\mathbf{e}_u, \mathbf{e}_v)\bigr)}{\sum_{k \in \mathcal{N}_S(u)} \exp\bigl(f(\mathbf{e}_u, \mathbf{e}_k)\bigr)}, +\end{equation} +\text{where } f(\mathbf{e}_u, \mathbf{e}_v) = \text{LeakyReLU}\bigl(\mathbf{a}^{\top} [\mathbf{W}\mathbf{e}_u \| \mathbf{W}\mathbf{e}_v]\bigr). +``` + +**Rule M3 — Self-check: if `equation` body > 60 characters (excluding `\label`), it probably overflows dual-column:** +This is a quick mental check. If the raw LaTeX math string is very long, it almost certainly won't fit in ~241pt. Use `align`, `split`, `multline`, or factor out sub-expressions. + +**Rule M4 — Contrastive loss / InfoNCE: always use `multline` or `split`:** +Contrastive losses with `\frac{\exp(...)}{\sum \exp(...)}` inside `\log` are notoriously wide. Always break them across lines: +```latex +\begin{multline} +\mathcal{L}_{\text{SSL}}^u = -\log \frac{\exp\bigl(\text{sim}(\mathbf{z}_u', \mathbf{z}_u'') / \tau\bigr)} +{\sum_{v \neq u} \exp\bigl(\text{sim}(\mathbf{z}_u', \mathbf{z}_v'') / \tau\bigr)}. +\end{multline} +``` + +--- + +**Algorithm overflow prevention (dual-column papers):** + +Algorithm boxes with long `\KwInput` lines or verbose pseudocode frequently overflow column width. + +**Rule A1 — Always set `\SetAlFnt{\small}` and limit line width:** +```latex +\SetAlFnt{\small} % Smaller font inside algorithm +\SetAlCapFnt{\small} % Smaller caption font +\SetAlCapNameFnt{\small} % Smaller "Algorithm N:" prefix +``` + +**Rule A2 — Break long Input/Output lines:** +```latex +% ❌ WRONG — all parameters on one line +\KwInput{Interaction graph $\mathcal{G}_R$, social graph $\mathcal{G}_S$, embedding dimension $d$, number of GNN layers $L$, learning rate $\eta$, regularization $\lambda$, SSL weight $\gamma$, temperature $\tau$} + +% ✅ CORRECT — break into multiple lines +\KwInput{Interaction graph $\mathcal{G}_R$, social graph $\mathcal{G}_S$\\\quad embedding dim $d$, GNN layers $L$, learning rate $\eta$\\\quad regularization $\lambda$, SSL weight $\gamma$, temperature $\tau$} +``` + +**Rule A3 — Use `algorithm*` for genuinely wide algorithms:** +If the algorithm has many columns or very long lines that can't be shortened, use `\begin{algorithm*}` to span both columns. + +**Rule A4 — Abbreviate variable names in pseudocode:** +Use compact notation: `emb` not `embedding`, `lr` not `learning\_rate`, `reg` not `regularization`. Define abbreviations in the Input line. + +--- + +**Clickable navigation - every reference must be a live link:** + +Attach `\label{}` right after each numbered element, cite with `\ref{}`: + +```latex +\section{Background}\label{sec:bg} +\begin{figure}[htbp] + \includegraphics{...} + \caption{Overview}\label{fig:overview} +\end{figure} +\begin{equation}\label{eq:energy} + E = mc^2 +\end{equation} + +% All produce clickable hyperlinks: +Section~\ref{sec:bg}... +Figure~\ref{fig:overview}... +Equation~\eqref{eq:energy}... % \eqref auto-wraps in parentheses +``` + +**Label conventions**: `sec:`, `fig:`, `tab:`, `eq:`, `lst:` - use `~` (non-breaking space) before `\ref`. + +**Bibliography** (three approaches): + +```latex +% Approach 1: natbib superscript (preferred academic) +\usepackage[numbers,super,sort&compress]{natbib} +This has been studied\cite{smith2023}. % → studied^[1] +\bibliography{refs} + +% Approach 2: natbib brackets +\usepackage[numbers]{natbib} +\cite{smith2023} % [1] +\citet{smith2023} % Smith (2023) + +% Approach 3: biblatex +\usepackage[backend=biber,style=numeric-comp]{biblatex} +\addbibresource{refs.bib} +\printbibliography +``` + +**PDF metadata** (add before `\end{document}`): +```latex +\hypersetup{ + pdftitle={Document Title}, + pdfauthor={Author Name}, + pdfsubject={Topic}, + pdfkeywords={keyword1, keyword2} +} +``` + +**Table of Contents** (auto-clickable with hyperref): +```latex +\tableofcontents +\listoffigures % optional +\listoftables % optional +``` + +### Phase 4 - COMPILE + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.latex main.tex --runs 2 +``` + +Default: 2 passes (resolves cross-references). Use `--runs 3` when bibliography back-references are needed. Add `--keep-logs` for debugging. + +**NEVER invoke Tectonic directly** - always use the wrapper. It strips noise, surfaces errors, and reports PDF statistics. + +### Phase 5 - PREFLIGHT + +The wrapper classifies diagnostics into three tiers: + +| Tier | Impact | Action | +|------|--------|--------| +| Errors | Build aborts | Fix before anything else | +| Layout defects | Overfull/underfull boxes, missing glyphs | Repair prior to delivery | +| Advisories | Other warnings | Assess individually; fix when feasible | + +**Never acceptable**: shrugging off warnings with "they don't affect the final PDF." + +**Navigation troubleshooting:** + +| Symptom | Fix | +|---------|-----| +| `??` in text | Recompile with `--runs 2` | +| Links not coloured | Add `colorlinks=true` to hyperref | +| `[?]` beside citations | Check `.bib` path; rebuild | +| No PDF bookmarks | Set `bookmarks=true` | + +### Phase 6 - DELIVER + +Final PDF. Confirm page count, cross-references resolved, no `??` placeholders. + +--- + +## §Scenario A: Math-Heavy Technical Documents + +> Use standalone for a pure-math document, or embed the preamble additions + environments into a §Standard Paper Workflow document. + +When the document has **more than 3 non-trivial equations** (matrices, aligned systems, integrals, summations), use this template instead of plain `article`. + +**Decision rule**: If you find yourself writing `<super>` tags or KaTeX CDN includes for math, stop and switch here. + +```latex +\documentclass[11pt,a4paper]{article} +\usepackage{amsmath,amssymb,amsthm} +\usepackage[margin=2.5cm]{geometry} +\usepackage{ctex} % CJK support via Tectonic +\usepackage{booktabs} +\usepackage{hyperref} + +\theoremstyle{definition} +\newtheorem{definition}{Definition}[section] +\newtheorem{theorem}{Theorem}[section] +\newtheorem{lemma}[theorem]{Lemma} + +\title{Document Title} +\author{Author} +\date{\today} + +\begin{document} + +% ═══════════════════════════════════════════════════════ +% NO TITLE PAGE IN .tex - Cover is generated separately +% via HTML/Playwright and merged as page 0. +% Body PDF starts directly with TOC or content. +% ═══════════════════════════════════════════════════════ + +\tableofcontents +\newpage + +% Aligned equation group +\begin{align} + \nabla \cdot \mathbf{E} &= \frac{\rho}{\varepsilon_0} \\ + \nabla \times \mathbf{B} &= \mu_0 \mathbf{J} + \mu_0\varepsilon_0 \frac{\partial \mathbf{E}}{\partial t} +\end{align} + +% Matrix +\[ + A = \begin{pmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{pmatrix} +\] + +\end{document} +``` + +--- + +## §Scenario B: Diagram Generation for Academic Papers + +> **Complexity-based routing**: Simple diagrams use TikZ natively (vector, font-consistent). Complex diagrams use Playwright+CSS → PNG → `\includegraphics` (models produce broken TikZ for complex branching logic). + +### Decision: TikZ or Playwright+CSS? + +| Criteria | → TikZ native | → Playwright+CSS → PNG | +|---|---|---| +| Node count | ≤6 | >6 | +| Topology | Linear chain, simple tree, layer stack | Branching, multi-path, feedback loops | +| Annotations | Minimal (labels only) | Side notes, legends, callout boxes | +| Math in nodes | Yes (LaTeX math rendering matters) | No (plain text labels) | +| Output | Vector (`tikzpicture` in document) | Raster PNG @2× (300dpi, publication-ready) | + +### Path A: Simple Diagrams → TikZ Native + +For ≤6 node linear/tree diagrams, embed `tikzpicture` directly or use standalone: + +```latex +\documentclass[tikz,border=5pt]{standalone} +\usetikzlibrary{arrows.meta,positioning,calc,shapes.geometric} + +\begin{document} +\begin{tikzpicture}[ + box/.style={draw, rounded corners=2pt, minimum width=2.4cm, + minimum height=0.7cm, align=center, font=\small\sffamily}, + arr/.style={-{Stealth[length=4pt]}, thick, gray!70} +] + \node[box, fill=blue!8] (a) {Input}; + \node[box, fill=orange!8, right=1.2cm of a] (b) {Process}; + \node[box, fill=green!8, right=1.2cm of b] (c) {Output}; + \draw[arr] (a) -- (b); + \draw[arr] (b) -- (c); +\end{tikzpicture} +\end{document} +``` + +**Node text overflow prevention:** + +When nodes contain multi-word labels (e.g. "CNN Spatial Encoder", "Multi-Head Attention"), text can overflow or overlap adjacent nodes. Prevent this: + +```latex +% Use text width (not minimum width) to enable line wrapping inside nodes +box/.style={draw, rounded corners=2pt, text width=2.8cm, + minimum height=0.7cm, align=center, font=\small\sffamily}, + +% For nodes with long labels, use explicit line breaks +\node[box] {CNN Spatial\\Encoder}; +\node[box] {Multi-Head\\Attention}; +``` + +Rules: +- Prefer `text width` over `minimum width` when labels exceed 2 words - it wraps text instead of clipping +- When 3+ nodes sit side by side, verify total width fits within `\columnwidth` (single-column) or `0.45\textwidth` (dual-column) +- If labels still overflow, use abbreviations or `\scriptsize` - never let text clip outside node borders +- Always `\resizebox{\columnwidth}{!}{...}` when embedding in dual-column papers (see TikZ in multi-column section below) + +**TikZ standalone embedding (simple diagrams only):** + +```bash +# 1. Compile TikZ standalone to PDF +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.latex diagram.tex # → diagram.pdf (tight-cropped) + +# 2. Embed as vector in LaTeX +# \begin{figure}[t] +# \centering +# \includegraphics[width=\columnwidth]{diagram.pdf} % vector, not PNG +# \caption{System Architecture}\label{fig:arch} +# \end{figure} +``` + +**🚫 FORBIDDEN: Using TikZ standalone for Report or Creative briefs.** Those routes have no LaTeX compiler. See SKILL.md § "Diagram Generation Strategy". + +### Path B: Complex Diagrams → Playwright+CSS → PNG + +For >6 node diagrams with branching, annotations, or multi-path flows: + +```bash +# 1. LLM generates diagram.html (CSS grid/flexbox nodes + SVG/CSS arrows) +# 2. Screenshot at 2× device scale factor for 300dpi print quality +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.blueprint diagram.html --device-scale-factor 2 --output diagram.png + +# 3. Embed in LaTeX +# \begin{figure}[t] +# \centering +# \includegraphics[width=\columnwidth]{diagram.png} +# \caption{System Architecture}\label{fig:arch} +# \end{figure} +``` + +**Why not TikZ for complex diagrams?** Models frequently produce broken TikZ code for >6 node graphs with conditional branches, multi-path convergence, and side annotations. The compilation-debug cycle wastes time. Playwright+CSS handles any layout natively, and 2× screenshots at A4 width give ~300dpi - indistinguishable from vector at print resolution. + +### Complex Diagram Decomposition + +When a diagram exceeds **12 nodes, 3 subgroups, or fills more than 40% of page height**, decompose into table + simplified overview: + +```latex +% Step 1: Table carries the details +\begin{table}[htbp] +\centering +\caption{System Pipeline - Detailed Phases}\label{tab:pipeline} +\begin{tabular}{llp{5cm}} +\toprule +Phase & Module & Key Tasks \\ +\midrule +Data Collection & Crawler & Crawl reviews, scrape prices, API calls \\ +Data Processing & ETL Pipeline & Clean, tokenize, label, validate \\ +Model Training & Trainer & Fine-tune LLM, distributed 64-GPU \\ +Evaluation & Benchmark & A/B test, offline metrics, human eval \\ +\bottomrule +\end{tabular} +\end{table} + +% Step 2: Simplified TikZ shows only the flow skeleton +\begin{figure}[htbp] +\centering +\begin{tikzpicture}[ + box/.style={draw, rounded corners=2pt, minimum width=2cm, + minimum height=0.6cm, align=center, font=\small\sffamily, fill=blue!5}, + arr/.style={-{Stealth[length=4pt]}, thick, gray!60} +] + \node[box] (a) {Collection}; + \node[box, right=0.8cm of a] (b) {Processing}; + \node[box, right=0.8cm of b] (c) {Training}; + \node[box, right=0.8cm of c] (d) {Evaluation}; + \draw[arr] (a)--(b); \draw[arr] (b)--(c); \draw[arr] (c)--(d); +\end{tikzpicture} +\caption{Pipeline overview (see Table~\ref{tab:pipeline} for details).}\label{fig:pipeline-overview} +\end{figure} +``` + +**Rule of thumb**: The diagram gives intuition at a glance; the table carries precision. + +**Common TikZ patterns**: +- Flowcharts: `positioning` + `arrows.meta` libraries +- Neural network layers: `fit` library + nested nodes +- Timelines: single axis with `\draw` segments +- Tree diagrams: `child` syntax or `forest` package + +**CRITICAL: TikZ in multi-column documents (IEEE, ACM)** + +Never place `tikzpicture` directly in multi-column body text - it overflows. Always wrap: + +```latex +% Single-column figure (fits one column) +\begin{figure}[t] +\centering +\resizebox{\columnwidth}{!}{% +\begin{tikzpicture}[...] + ... +\end{tikzpicture} +} +\caption{Architecture.}\label{fig:arch} +\end{figure} + +% Full-width figure (spans both columns) +\begin{figure*}[t] +\centering +\resizebox{0.85\textwidth}{!}{% +\begin{tikzpicture}[...] + ... +\end{tikzpicture} +} +\caption{System pipeline.}\label{fig:pipeline} +\end{figure*} +``` + +Rules: `\resizebox{\columnwidth}{!}` constrains width; `figure*` for tall diagrams; every figure needs `\caption` + `\label`; prefer `[t]` placement in two-column mode. + +--- + +## §Scenario C: Algorithm Pseudocode + +> Use standalone for a single algorithm sheet, or embed the `algorithm` environment into a §Standard Paper Workflow document. + +For formal algorithm descriptions (research papers, technical specs): + +```latex +\documentclass[11pt,a4paper]{article} +\usepackage[margin=2.5cm]{geometry} +\usepackage[ruled,vlined,linesnumbered]{algorithm2e} + +\begin{document} + +\begin{algorithm}[H] +\SetAlgoLined +\KwIn{Training set $\mathcal{D} = \{(x_i, y_i)\}_{i=1}^N$, learning rate $\eta$} +\KwOut{Optimized parameters $\theta^*$} +Initialize $\theta$ randomly\; +\For{epoch $= 1$ \KwTo $T$}{ + \ForEach{mini-batch $B \subset \mathcal{D}$}{ + $\mathcal{L} \leftarrow \frac{1}{|B|}\sum_{(x,y)\in B} \ell(f_\theta(x), y)$\; + $\theta \leftarrow \theta - \eta \nabla_\theta \mathcal{L}$\; + } +} +\Return{$\theta$}\; +\caption{Stochastic Gradient Descent} +\end{algorithm} + +\end{document} +``` + +--- + +## §Exam Paper Rules (LaTeX) + +> **Load this section** when generating exam papers, quizzes, worksheets, or exercises with mathematical content via LaTeX. + +### Critical Anti-Pattern: `\parskip` + Loose Numbering = Blank Lines After Question Numbers + +**This is the #1 visual defect in LaTeX exam papers.** When `\parskip` is set (e.g. `0.5em`) and question numbers are separated from question text by a blank line in the `.tex` source, LaTeX treats them as separate paragraphs — the `\parskip` gap makes it look like an intentional blank line after every number. + +```latex +% ❌ WRONG — blank line between number and text = new paragraph + parskip gap +\noindent\textbf{3.} + +某工厂原计划生产1200件产品…… + +% ❌ ALSO WRONG — even without blank line, if \parskip is large +\noindent\textbf{3.} % line break here +某工厂原计划生产1200件产品…… % LaTeX treats this as same paragraph, but confusing + +% ✅ CORRECT — number and text on the SAME LINE, no break +\noindent\textbf{3.}\;某工厂原计划生产1200件产品…… +``` + +### Iron Rules + +**Rule E1 — Question number + text = same paragraph (MANDATORY):** +The question number (`\textbf{3.}`) and the question text MUST be on the same line in the `.tex` source, joined by `\;` or `\quad`. NEVER put a blank line or even a line break between them. + +**Rule E2 — `\parskip` must be 0 or minimal for exam papers:** +```latex +% RECOMMENDED for exams: no parskip, control spacing explicitly +\setlength{\parskip}{0pt} % No automatic paragraph spacing +\setlength{\parindent}{0pt} % No indentation +% Use \vspace{} explicitly between questions for precise control +``` +If `\parskip` is needed for other reasons, keep it ≤0.3em and be extra careful about blank lines in the source. + +**Rule E3 — Use `enumitem` for structured numbering (PREFERRED):** +Instead of manual `\noindent\textbf{1.}\;`, prefer `enumitem` with custom formatting: +```latex +\usepackage{enumitem} + +% Section-level numbering: 一、二、三 +% Question-level: use enumerate with custom label +\begin{enumerate}[label=\textbf{\arabic*.}, leftmargin=0pt, itemindent=2em, + labelsep=0.5em, itemsep=0.8em, parsep=0pt] + \item 2024年巴黎奥运会共设有32个大项…… + \item 中国空间站“天宫”在距地面…… +\end{enumerate} +``` +This guarantees number and text are in the same paragraph (LaTeX `\item` handles it internally). + +**Rule E4 — `tasks` environment for multi-column calculation items:** +```latex +\usepackage{tasks} +% 4-column oral calculation +\begin{tasks}[counter-format={(1)}, label-align=left, + label-offset={0.5em}, label-width={2.5em}, + item-indent=3em, column-sep=2em](4) + \task $\dfrac{3}{4}\times\dfrac{2}{9}=$ \underline{\hspace{2cm}} + \task $\dfrac{5}{6}\div\dfrac{1}{3}=$ \underline{\hspace{2cm}} +\end{tasks} +``` + +**Rule E5 — Answer space reservation:** + +| Question Type | LaTeX Implementation | +|--------------|---------------------| +| Fill-in-the-blank | `\underline{\hspace{2cm}}` inline | +| Short answer | `\vspace{2cm}` after question | +| Calculation | `\vspace{4cm}` (show-your-work space) | +| Proof / derivation | `\vspace{5cm}` | +| Drawing / graphing | TikZ grid or `\vspace{4cm}` | + +**Rule E6 — Page breaks for exams:** +- `page-break-after: always` between major sections (一、二、三) is OK +- NEVER break within a single question (keep question + options + answer space together) +- Use `\needspace{5cm}` before long questions to prevent orphaning + +### Complete Exam Preamble Template + +```latex +\documentclass[12pt,a4paper]{article} +\usepackage[margin=2cm]{geometry} +\usepackage{ctex} % CJK support +\usepackage{amsmath,amssymb} % Math +\usepackage{enumitem} % Structured lists +\usepackage{tasks} % Multi-column exercises +\usepackage{tikz} % Diagrams +\usepackage{graphicx} +\usepackage{array,tabularx} + +\pagestyle{plain} +\setlength{\parindent}{0pt} % No indent for exam papers +\setlength{\parskip}{0pt} % ❗ ZERO parskip — control spacing via \vspace + +% Utility commands +\newcommand{\blank}[1]{\underline{\hspace{#1}}} +\newcommand{\fn}[2]{\dfrac{#1}{#2}} + +% Section header: 一、填空题(每空1分,共计10分) +\newcommand{\examsection}[1]{% + \vspace{0.5cm} + \noindent{\heiti #1} + \vspace{0.3cm} +} +``` + +--- + +## §Resume / CV Templates + +> **Skip this section** unless building a resume or CV. + +Two templates available as separate files. Load the one you need: + +| Template | Style | Best for | File | +|----------|-------|----------|------| +| **A: AltaCV** | Dual-column, sidebar, skill dots | Creative/tech/startup roles | `references/resume-altacv.tex` | +| **B: Academic CV** | Single-column, multi-page, publications | PhD apps, academic positions | `references/resume-academic.tex` | + +**Route selection:** + +| Scenario | Recommended | +|----------|-------------| +| Corporate job / ATS parsing | **Report brief** - ReportLab ATS-friendly template | +| Creative/tech/startup | **This brief** - Template A | +| Academic position / PhD | **This brief** - Template B | +| Chinese-only, simple format | **Report brief** - ReportLab (best CJK support) | + +**Usage:** + +1. Read the template file: `references/resume-altacv.tex` or `references/resume-academic.tex` +2. Replace placeholder content with user's information +3. Compile: `python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.latex resume.tex --runs 2` + +**🔴 Resume Text Overlap Prevention:** + +> AltaCV dual-column resumes are the #1 source of text overlap bugs. The sidebar and main column share vertical space but are positioned independently. + +- **Column ratio sanity check:** `\columnratio{0.62}` means left column = 62%, right = 38%. If either column overflows, content bleeds into the other. Reduce ratio if left column has too much content. +- **Section spacing:** Always use `\medskip` or `\smallskip` between items - zero spacing causes lines to overlap +- **Skill dots:** Each `\cvskill` row has fixed height. If more than 8-10 skills, switch to a compact text list instead of dots +- **Experience entries:** Long descriptions in `\cvevent` can push content below the page. Use \textbf{max 3-4 bullet points per entry} +- **Two-page overflow:** If content exceeds 1 page, explicitly add `\newpage` and restart column layout. Do NOT let LaTeX auto-break the `paracol` environment - it misaligns columns on page 2 +- **Compile twice:** Always `--runs 2` to resolve cross-references and stabilize column breaks + +**🔴 Resume Minimum Font Size:** +- **Hard floor: 12px (9pt).** No text in the resume may render smaller than 12px, including footnotes, contact info, dates, and skill labels. + +**🔴 Resume Line-Break Rules:** +- English: prefer breaking at word boundaries. Long words may be split at syllable boundaries with a hyphen (`-`) - standard typographic practice (e.g., `experi-\nence`). +- CJK: break between characters, but never separate punctuation from preceding character. +- Mixed content: respect both rules. +- Dates/ranges ("Jan 2022 - Present") must stay as one unit. + +**🔴 Resume Page-Fill:** +- Content must fill ≥85% of page height. If content is sparse, increase spacing (`\medskip` → `\bigskip`), increase font size slightly, or add sections (Summary, Awards, Projects). Never leave visible blank area > 3cm at page bottom. + +**Template A customisation quick-reference:** + +| What to change | How | +|---------------|-----| +| Column ratio | `\columnratio{0.62}` → e.g. `{0.55}` | +| Accent colour | `\definecolor{accent}{HTML}{3B82F6}` → any hex | +| Skill dot count | `\foreach \x in {1,...,5}` → `{1,...,4}` | +| Icons | [FontAwesome5 gallery](http://texdoc.net/pkg/fontawesome5) | +| Font family | Replace `roboto`/`lato` with any LaTeX font package | +| Page margins | `\geometry{margin=1.25cm,...}` | + +**Template B customisation:** + +| What to change | How | +|---------------|-----| +| Accent colour | `\definecolor{accent}{HTML}{1F4E79}` → any hex | +| Header text | `\fancyhead[L]` content | +| Section style | `\titleformat{\section}` block | + +--- + +## Reference + +### Package Catalogue + +**Foundational**: `hyperref` · `geometry` · `listings` · `enumitem` + +**Tabular**: `booktabs` · `longtable` · `multirow` · `array` · `colortbl` + +**Visual & Charting**: `tikz` · `pgfplots` · `float` · `wrapfig` · `subfig` / `subcaption` + +**International & Typography**: `fontspec` (XeLaTeX/LuaLaTeX) · `ctex` + +**Mathematical**: `amsmath` · `amssymb` · `amsthm` · `natbib` · `biblatex` · `siunitx` + +**Algorithmic & Domain-Specific**: `algorithm` + `algpseudocode` · `chemfig` + +**Page Design**: `tcolorbox` · `fancyhdr` · `titlesec` · `tocloft` · `multicol` · `setspace` · `microtype` · `parskip` · `adjustbox` · `marginnote` + +**Code Listings**: `listings` · `minted` (depends on Pygments) + +### Scripts & Backends + +| Script | Purpose | +|--------|---------| +| `pdf.py convert.latex` | Tectonic wrapper - log sanitisation, error highlighting, PDF metrics | + +### Operational Notes + +**CJK**: `\usepackage{ctex}` - Tectonic pulls font bundles on the fly, zero manual install. + +**Cold-start**: First compilation downloads packages (1-5 min). Cached builds: 10-30s. + +**Offline**: Cached packages in `~/.cache/Tectonic/` work offline. New packages require network. + +**Tectonic vs TeX Live:** + +| Dimension | Tectonic | Traditional pdflatex | +|-----------|----------|---------------------| +| Package acquisition | On-demand, transparent | Manual via `tlmgr` | +| Multi-pass compilation | Handled by engine | Explicit re-invocations | +| Disk footprint | Single binary | Full TeX Live ≈ 4 GB | + +**Bundled binary note:** The `scripts/tectonic` binary shipped with this skill is a **macOS arm64 (Apple Silicon)** executable. It will NOT work on other platforms. If `pdf.py convert.latex` reports "tectonic command not found", run `python3 pdf.py status` to see platform-specific installation instructions, or install manually: + +| Platform | Install Command | +|----------|----------------| +| macOS (Homebrew) | `brew install tectonic` | +| macOS (binary) | `curl -sSL https://drop-sh.fullyjustified.net \| sh` | +| Debian / Ubuntu | `apt install tectonic` (if available) or conda/binary | +| Arch Linux | `pacman -S tectonic` | +| Conda (any OS) | `conda install -c conda-forge tectonic` | +| Windows (scoop) | `scoop install tectonic` | +| Windows (choco) | `choco install tectonic` | + +After installing, verify: `tectonic --version`. The `_find_tectonic()` function searches: `scripts/tectonic` → `~/tectonic` → system PATH. + + +--- + +> **⚠️ Legacy Note:** Academic covers previously used ReportLab canvas API (cover_recipe_A/B/C/D/L). This approach is **fully deprecated**. All academic covers now use HTML/Playwright Templates 08-11 (see `typesetting/cover.md` and the pipeline at line 58-118 of this file). Do NOT write ReportLab cover code. + +### ⚠️ Post-Cover Generation Checks (Mandatory) + +After generating the cover HTML and before converting to PDF, run `poster_validate.py check-html`; after generating the cover PDF, run `pdf_qa.py`: + +```bash +# Step 1: HTML check +python3 "$PDF_SKILL_DIR/scripts/poster_validate.py" check-html cover.html +# Step 2: Cover overlap check +node "$PDF_SKILL_DIR/scripts/cover_validate.js" cover.html +# Step 3: Convert to PDF +node "$PDF_SKILL_DIR/scripts/html2poster.js" cover.html --output cover.pdf --width 794px +# Step 4: PDF check +python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" final.pdf --skip-cover --formulas + +# MANDATORY: Post-generation pipeline +python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.brand final.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.clean final.pdf -o final_clean.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" font.check final.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" toc.check final.pdf +``` +- `poster_validate.py` checks: font fallback, overflow:hidden, @media screen, background color consistency, etc. +- `pdf_qa.py` checks: cover full-bleed, blank pages, CJK punctuation, overflow, margin symmetry, font embedding, metadata, formula overflow +- `meta.brand` writes Title/Author/Creator metadata +- `pages.clean` removes accidental blank pages +- `font.check` scans for missing glyphs (□ tofu) +- `toc.check` verifies TOC entries, page numbers, and links +- Any ERROR item → fix and regenerate + +### Merging Cover with Body + +```python +from pypdf import PdfReader, PdfWriter + +def merge_cover_body(cover_path, body_path, output_path): + writer = PdfWriter() + for page in PdfReader(cover_path).pages: + writer.add_page(page) + for page in PdfReader(body_path).pages: + writer.add_page(page) + with open(output_path, 'wb') as f: + writer.write(f) +``` diff --git a/skills/pdf/briefs/creative.md b/skills/pdf/briefs/creative.md new file mode 100755 index 0000000..71cb45b --- /dev/null +++ b/skills/pdf/briefs/creative.md @@ -0,0 +1,770 @@ +# Brief: Creative Production (Art Director Blueprint Mode) + +**Core Paradigm Shift**: You are NO LONGER a frontend developer writing HTML/CSS. You are an elite Art Director and Editorial Designer. + +Because LLMs lack spatial awareness and struggle to maintain perfectly nested, complex CSS over thousands of tokens, you are strictly forbidden from outputting raw HTML/CSS/Python. + +**→ Overflow prevention**: See `typesetting/overflow.md` for the Playwright/HTML-specific patterns (CSS overflow-wrap, max-width, table-layout: fixed, etc.). + +Your sole responsibility is to act as the **Brain (Art Director)**: +1. Brutally edit and pace the raw text. +2. Select architectural components and layout archetypes. +3. Output a **Strict JSON Layout Blueprint**. + +The `design_engine.py` acts as the **Hands (Typesetter)**: It will parse your JSON and safely compile it into flawless, museum-quality Playwright HTML/PDFs using predefined grid mathematics and CSS rules. + +--- + +## Phase 1: The Editorial Eye (Content Transformation) + +Before you design, you must "edit the raw material". Users often provide dense, unstructured text. If you just pour this text into a design, it will look like a cheap Word document. + +You must apply **Editorial Pacing**: + +### 1. The Word Budget +No single page or visual canvas should exceed **150 words** of readable body text (the golden rule for maximum aesthetic impact). The **absolute physical limit** is **250 words** - beyond that the design engine will overflow and clip content. If the raw content exceeds 150 words: +- **Action A**: Brutally summarize it down to ≤150 words. +- **Action B**: Split it across multiple `pages` in your JSON blueprint. +- Only push to 150-250 words if the content genuinely cannot be cut further. + +### 2. Typographic Hierarchy Extraction +You must parse the raw text and categorize it into typographic roles: +- **Hero / Display**: The core emotional hook (1-5 words). Must be punchy. +- **Kicker / Eyebrow**: Tiny context text above a headline (e.g., "Q3 REPORT", "MANIFESTO"). +- **Lead Paragraph**: The 2-3 sentence summary. +- **Data Sculptures**: Scan the text for impactful numbers (e.g., "97%", "$4.2M"). EXTRACT THEM. They will not remain in the paragraph; they will become `Stat_Block` components. +- **Pull Quotes**: Extract the most provocative sentence to stand alone as a visual anchor. + +--- + +## Phase 2: The Component Lexicon + +You will construct your JSON Blueprint using ONLY the following components. These map directly to the `configs/components.md` assets. Do not invent new component types. + +> **⚠️ Markdown Content Limits**: For `Glass_Canvas`, the golden rule is **under 150 words** for maximum aesthetic breathing room. Absolute physical limit is **250 words**. If content exceeds 250 words, the design engine will overflow. You MUST summarize it or split it into a new page. + +### 1. `Hero_Typography` +Giant, page-dominating text. Usually interacts with the background via blend modes. +- **Parameters**: + - `content` (string): The text (use `<br>` for deliberate line breaks). + - `weight` (string): `"black"` (900, dominating) or `"thin"` (100, elegant). + - `variant` (string, optional): `"standard"` only. ~~`"vertical_accent"`~~ is **NOT implemented** in `design_engine.py` - the engine silently ignores this value and renders nothing. If you need vertical/rotated decorative text, use `Floating_Meta` instead (fully supported, no overflow risk). + - `scale` (integer, optional): Typographic scale level `1`-`6`. Controls font size via the engine's fluid type system. If omitted, the engine uses the default hero size. + - `6`: Hero/Display - oversized title, clamp(64px, 12vw, 150px). Use for single words or short phrases with maximum visual impact. + - `5`: Primary Title - clamp(48px, 8vw, 96px). Standard poster headline. + - `4`: Subheadline - clamp(32px, 5vw, 56px). Chapter openers or key quotes. + - `3`: Lead Paragraph - clamp(20px, 3vw, 32px). Prominent body text. + - `2`: Body - 16px. Standard readable text. + - `1`: Meta/Caption - 10px. Decorative, environmental. + +### 2. `Glass_Canvas` +The main structural container for reading text. Frosted glass, sharp 2px print corners. +- **Parameters**: + - `markdown_content` (string): The actual text to read. Supports standard Markdown (H2, H3, bold, lists). *Must be under 150 words.* + - `tension_score` (float, optional): Semantic tension value from `0.0` to `1.0`. Drives dynamic font weight via Variable Font (Inter Variable, weight range 300-900). The engine maps: `weight = 300 + (tension_score × 600)`. + - `0.0-0.2` → Light (300-420): calm, contemplative passages + - `0.3-0.5` → Normal (420-600): standard body text + - `0.6-0.8` → Bold (600-780): urgent, assertive content + - `0.9-1.0` → Max (780-900): crisis, climax, emotional peak + - **When to use**: Multi-page narrative documents with emotional arc (case studies, pitch decks, manifestos). Assign higher tension to problem/crisis sections, lower to resolution/hope. Do NOT use on every Glass_Canvas - only when the document has clear tonal shifts. + - **When NOT to use**: Data-heavy pages, simple reports, single-page posters. + +### 3. `Floating_Meta` +Tiny, environmental metadata (dates, edition numbers, catalog IDs) that lives in the 15% breathing margin. +- **Parameters**: + - `position` (string): `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"`. + - `items` (array of strings): E.g., `["VOL 01", "2026", "EDITION 500"]`. + +### 4. `Hairline_Divider` +Structural 0.5px line. Not decorative; acts as a visual fold. +- **Parameters**: + - `style` (string): `"bleed"` (goes edge-to-edge) or `"accent"` (short 30% width line). + +### 5. `Stat_Block` +Data sculpture. A massive number with a tiny label. +- **Parameters**: + - `number` (string): e.g., `"97.3"`. + - `unit` (string): e.g., `"%"`, `"Hz"`, `"$M"`. + - `label` (string): e.g., `"COMPLETION RATE"`. + +### 6. `Image_Asset` +A visual element. The engine will apply a gradient blend to it. +- **Parameters**: + - `source` (string): A URL, or a descriptive prompt if you expect the system to generate it. + - `caption` (string, optional): Tiny text under the image. + +> ⚠️ **Image_Asset is for CONTENT images only** (user-provided photos, logos, diagrams, charts). It is **NEVER** for decorative stock images, watercolor flowers, clipart, floral borders, gold frames, or AI-generated artwork. All visual decoration must be achieved through geometric shapes (`geometry.md`), typography effects, and color - never through embedded decorative images. See `visual_framework.md` Stock Image Ban. + +### 7. `Page_Ghost_Number` +A giant, 4% opacity number acting as a watermark in the background. +- **Parameters**: + - `number` (string): e.g., `"01"`, `"X"`. + +### 8. `Delta_Widget` +**Data-to-Ink Ratio enforcer.** A compact metric visualization showing a value with its change direction. CRITICAL: Use this instead of writing sentences like "revenue grew by 12%". Extract every trend into a Delta_Widget. +- **Parameters**: + - `metric` (string): The metric name, e.g., `"REVENUE"`, `"LATENCY"`. + - `value` (string): Current value, e.g., `"$4.2M"`, `"45ms"`. + - `delta` (string): Change description, e.g., `"+12%"`, `"-45ms"`. + - `trend` (string): `"up"`, `"down"`, or `"flat"`. + - `label` (string, optional): Context line, e.g., `"vs. Q2 2025"`. + +### 9. `Process_List` +**Polymorphic adaptive component.** Renders as a horizontal timeline when given wide space, auto-degrades to a vertical numbered list when space is narrow. Use for workflows, steps, timelines. +- **Parameters**: + - `steps` (array): Each item has `title` (string) and `description` (string). + +### 10. `Sidenote_Block` +**Tufte marginalia.** Used in `tufte_report` archetype layouts. Content is placed in the 30% side rail alongside the main column. Perfect for citations, supplementary data, asides, footnotes. +- **Parameters**: + - `label` (string, optional): Category label, e.g., `"SOURCE"`, `"NOTE"`, `"DATA"`. + - `body` (string): The sidenote content in Markdown. + +### 11. `Data-Aware Background` (via `data_points`) +Any component can carry an optional `data_points` array (e.g., `[10, 15, 8, 24, 30]`). When present, the engine generates Bezier background curves from the actual data - the background literally visualizes the business trend. Use on pages with financial, metric, or time-series content. + +--- + +### ★ Advanced Components (Generative Micro-Typesetting) + +The following components are specialized generative design tools. They unlock visual effects that standard components cannot achieve. Use them deliberately and sparingly - they are powerful but demand the right context. + +### 8. `Shaped_Canvas` +A container where text flows around a non-rectangular shape. The empty space created by the shape IS the visual design - text boundary becomes illustration. Uses CSS `shape-outside` for non-rectangular text wrapping. + +- **Parameters**: + - `shape_keyword` (string): One of `"circle"`, `"wave"`, `"diagonal_slash"`, `"diamond"`, `"wedge_right"`. + - `markdown_content` (string): The text that will flow around the shape. Supports standard Markdown. + +- **Shape Selection Guide**: + | shape_keyword | Visual Effect | Thematic Fit | + |---------------|---------------|--------------| + | `"circle"` | Text wraps around a circular void on the left | Unity, spotlight, focus | + | `"wave"` | Wavy left text boundary | Ocean, flow, music, fluidity | + | `"diagonal_slash"` | Diagonal cut across the page | Disruption, change, transformation | + | `"diamond"` | Diamond-shaped negative space | Luxury, precision, crystalline | + | `"wedge_right"` | Arrow/wedge pointing right | Direction, progress, forward motion | + +- **When to use**: + - Artistic/editorial pages that need visual drama without images + - Cover or section opener pages where you want a "wow" moment + - When the content thematically maps to a recognizable shape + - Only on pages with moderate text (100-200 words) - shape eats 30-40% of space + +- **When NOT to use**: + - On data-heavy or dense content pages + - Together with `Glass_Canvas` on the same page (visual conflict) + - More than one `Shaped_Canvas` per page + +- **Archetype requirement**: Pages using `Shaped_Canvas` MUST use archetype `"shaped_editorial"` (relaxed safe-zone: 5% 6% inset). + +### Chart & Data Visualization Styling + +**→ Full spec: `typesetting/charts.md`** - read it before designing any chart/data page. + +Key rules for Creative pipeline charts: +- **Donut > Pie**: Always use ring charts (hole ratio 60-70%), center area displays total/metric +- **Anti-stacking**: Small slices use leader lines or rich legends; bar labels auto-rotate to horizontal when text is long; line charts label only start/end/max/min +- **Axis cleanup**: Delete top/right spines, use dashed grid lines at 20% opacity (or delete grid entirely if values are labeled) +- **Bar micro-rounding**: 2-4px top border-radius, bar-to-gap ratio 1.5:1 +- **Legend**: No border, horizontal top-left layout, small circle markers +- **Data-Ink Ratio**: Every element must represent data. If it doesn't, delete it. + +--- + +## Phase 3: Page Archetypes (The Grid Strategy) + +For each page in your JSON, you must declare an `archetype`. This tells `design_engine.py` how to arrange the components you provided. + +- `"cover_hero"`: Cover page. **Must follow `typesetting/cover.md` 7-template system** - pick Layout 1-7 based on document tone. See "Cover Page Constitution" below for iron rules. +- `"split_vertical"`: Page split strictly 50/50 vertically. Left side image/svg, right side Glass_Canvas. +- `"editorial_flow"`: Top-down reading experience. Centered columns, generous margins. Use for main content. +- `"scattered_canvas"`: No grid. Elements placed via absolute positioning based on spatial weights. +- `"data_dashboard"`: 2x2 or 3x3 strict grid for multiple `Stat_Block`s. +- `"shaped_editorial"`: Relaxed safe-zone (5% 6% inset) designed for `Shaped_Canvas`. Centered, generous breathing room. **Must be used when the page contains a `Shaped_Canvas`.** Do NOT mix with `Glass_Canvas`. +- `"tufte_report"`: **Tufte marginalia layout.** 70% main content column + 30% sidenote rail. Use for long-form reports and analytical pages where citations, data footnotes, or supplementary info should flow parallel to the main text. Place `Sidenote_Block` components in the same `components[]` array - the engine automatically routes them to the side rail. + +### ★ 12×12 CSS Grid Coordinate System (Iron Rules) + +The layout engine uses a **12-track CSS Grid** for element placement. The grid lines are numbered **1 to 13** (12 tracks = 13 lines). + +**Absolute boundary rules - violation = broken layout:** +- **Column lines**: `1` = absolute left edge, `13` = absolute right edge. Full width = `1 / 13`. +- **Row lines**: `1` = absolute top edge, `13` = absolute bottom edge. Full height = `1 / 13`. +- **CRITICAL**: Never output a grid line number less than `1` or greater than `13`. Any value outside `[1, 13]` will destroy the layout. + +**`grid_area` format**: `"row_start / col_start / row_end / col_end"` (CSS shorthand). +- Example: `"1 / 1 / 7 / 13"` = top half of the page, full width. +- Example: `"3 / 8 / 6 / 13"` = rows 3-5, columns 8-12 (right side block). + +**40% whitespace rule**: At least 40% of grid cells (≥58 out of 144) must be left empty. Count your occupied cells. + +### ★ Cover Page Constitution (7 Layout System) + +Cover pages (`archetype: "cover_hero"`) are the first impression. They must be ruthlessly sparse and spatially sophisticated. + +**→ Full spec: `typesetting/cover.md`** - read it before designing any cover. + +#### Global Iron Rules (Always Apply) + +1. **Maximum 4 components** on any cover page. Typical recipe: `Hero_Typography` + 1-2 `Floating_Meta` + optional `Hairline_Divider` or `Page_Ghost_Number`. +2. **Typography Scale**: Title ≈ 45pt (2.5× base), Subtitle ≈ 25pt (1.4× base), Meta ≥ 18pt (never below 14pt). Covers with tiny text = FAIL. +3. **Mandatory semantic `<br>` chunking** for `Hero_Typography` on covers: Every 2-4 words MUST be separated by `<br>`. Single-line hero text is FORBIDDEN on covers. Example: `"ALGORITHMIC<br>FATIGUE"`, NOT `"ALGORITHMIC FATIGUE"`. +4. **Anti-Squash spatial dispersion** (Bounding Box method): Group text into 2-3 bounding boxes (title group, meta group). Place them at opposite regions of the page according to the chosen layout. Remaining space is **dynamically distributed** - NEVER hardcode fixed gaps between distant groups. +5. **No `Glass_Canvas` on cover pages.** Dense reading text kills the visual impact. Push all body content to page 2+. +6. **Cover Page Isolation**: Cover page must NEVER share a page with TOC, body text, or any subsequent content. The cover is always a standalone full page. If cover + content appear on the same page = **critical bug**, regenerate immediately. +7. **Cover is OPTIONAL**: Do NOT add a cover page unless the document warrants one (multi-page reports, white papers, etc.) or the user explicitly requests it. Short documents, letters, memos, forms, and quick outputs skip the cover. +8. **Background Layer (optional)**: See `typesetting/cover-backgrounds.md` for 3 recipes - A (极简弧线), B (工程十字轴+立柱), C (锐角切割+出血文字). Background renders BELOW all foreground at 2-5% opacity. Pick a recipe that matches the document tone. Never combine elements across recipes. + +#### 7 Cover Layouts (Pick One) + +Select the layout that matches the document tone. When unsure, default to **Layout 1A**. + +| Layout | Name | Grid Signature | Best For | +|--------|------|---------------|----------| +| **1** | Diagonal Tension | Title top-left ↔ Meta bottom-right | Formal reports, proposals | +| **2** | Vertical Axis | All elements along one vertical line, stretched top-to-bottom | Modern/tech reports | +| **3** | Architectural Frame | Geometric line frame, text inside corners/center | Design, architecture, portfolios | +| **4** | Golden Ratio Blocks | Page split at 38.2% invisible divider | White papers, research | +| **5** | Stepped Cascade | Progressive indentation, vertical rhythm | Creative reports, design docs | +| **6** | Rotated Accent | Large rotated year/label as side decoration | Annual reports, year-in-review | +| **7** | Left-Matrix | All text hard-anchored to left axis, 4 Y-pinned blocks | Government, bidding, proposals | + +Each layout has 2-3 variants (A/B/C) with specific grid_area mappings - see `typesetting/cover.md` for exact coordinates. + +#### Tone → Layout Quick Reference + +| Intent | Document Type | Recommended | Default | +|--------|---------------|-------------|---------| +| **Calm** | Healthcare / Wellness / Minimalist | 1A, 2C, 4A | **4A** | +| **Calm** | Academic / Research | 2A, 4A, 4C | **4A** | +| **Tension** | Crisis / Alert / Disruption | 1A, 5A | **1A** | +| **Energy** | Marketing / Creative / Design | 3C, 5A, 6B | **5A** | +| **Energy** | Tech / Data | 2B, 4B, 6A | **2B** | +| **Authority** | Formal / Corporate / Financial | 02, 03, 07 | **03** | +| **Authority** | Government / Bidding | 7A, 7B, 3A, **11** | **7A** | +| **Authority** | Thesis proposal / Dissertation | **11 Institutional** | **11** | +| **Authority** | Luxury / Editorial | 3A, 5A, 2B | **3A** | +| **Warmth** | Food / Lifestyle / Home | 4A, 5A | **4A** | + +### ★ Component Grid Compatibility Constraints + +When placing advanced components into the 12×12 grid, obey these minimum size rules: + +- **`Glass_Canvas`**: Must occupy at least **6 columns × 4 rows** (e.g., `"3 / 1 / 7 / 7"`). Smaller areas will cause text overflow and padding collapse. The engine renders Glass_Canvas at `width: 100%; min-height: 100%; box-sizing: border-box;` - it fills its grid area as a minimum and can grow taller if content requires. +- **`Shaped_Canvas`**: Must occupy at least **8 columns × 6 rows** (e.g., `"2 / 3 / 8 / 11"`). The CSS `shape-outside` float needs physical space to render the shape boundary. Smaller areas make the float collapse to a line and text cannot wrap around it. **Must use archetype `"shaped_editorial"`.** +- **`Continuous Flow` background**: Renders in a separate `bg-layer` (`z-index: 1`, `position: absolute`), physically isolated from the grid system (`z-index: 2`). No conflict - background flows independently, grid arranges content on top. + +### ★ Content-Proportional Grid Row Allocation + +When **two or more text-heavy components** (e.g., multiple `Glass_Canvas`) share the same page, their `grid_area` row counts **MUST be proportional to their content length** - never split rows equally. + +**Estimation method:** +1. Count characters (or words) in each component's `markdown_content` +2. Allocate rows proportionally: `rows_i = total_rows × (chars_i / total_chars)` +3. Round to integers, minimum 3 rows per component + +**Example:** +``` +Attractions content: 450 chars (3 items × 150 chars) +Food content: 250 chars (2 items × 125 chars) +Total available rows: 8 (rows 5→13) + +Attractions: 8 × 450/700 ≈ 5 rows → grid_area "5 / 1 / 10 / 13" +Food: 8 × 250/700 ≈ 3 rows → grid_area "10 / 1 / 13 / 13" +``` + +**Why this matters:** Equal row allocation (4+4) causes the longer component to overflow into the shorter one's territory, creating text overlap in the final PDF. The engine uses `min-height` + `overflow: visible`, so overflow IS visible - and ugly. + +**Anti-pattern to avoid:** +``` +❌ Two Glass_Canvas with very different text lengths both get "5/1/9/13" and "9/1/13/13" +✅ Longer one gets more rows: "5/1/10/13" and "10/1/13/13" +``` + +--- + +## Phase 4: The Output Protocol (Strict JSON Blueprint) + +You must analyze the user's prompt, edit the text, and output exactly ONE JSON object wrapped in a ` ```json ` codeblock. +**No conversational text before or after the JSON.** + +### The JSON Schema Specification + +**CRITICAL JSON RULES:** +1. Output ONLY valid JSON. +2. Do NOT include ANY comments (`//` or `/* */`) inside the JSON. Python's `json.load()` will crash. +3. Do NOT include trailing commas. + +```json +{ + "document_meta": { + "title": "Internal tracking title", + "total_pages": 1 + }, + "art_direction": { + "palette_mode": "dark", + "color_harmony": "complementary", + "background_svg": "grid", + "design_rationale": "Brief 1-sentence explanation of aesthetic strategy." + }, + "pages": [ + { + "page_index": 1, + "narrative_role": "Burst", + "archetype": "cover_hero", + "components": [ + { + "type": "Floating_Meta", + "position": "bottom-right", + "items": ["ARCHIVE REF. 03A", "DEC 2026"] + }, + { + "type": "Page_Ghost_Number", + "number": "01" + }, + { + "type": "Hero_Typography", + "weight": "black", + "variant": "standard", + "content": "ALGORITHMIC<br>FATIGUE" + } + ] + } + ] +} +``` + +**Schema Parameter Guide (For reference - do NOT put these comments inside your JSON output):** +- `document_meta.total_pages`: Integer. Must match the actual number of pages in `pages[]`. + +### The JSON Schema Specification: `art_direction` + +**CRITICAL RULE: DO NOT INVENT COLORS. DO NOT OUTPUT HEX/RGB CODES.** +The Python engine generates mathematical color palettes. You MUST only select the semantic parameters below. + +```json +{ + "art_direction": { + "palette_mode": "minimal", + "color_harmony": "auto", + "background_svg": "grid", + "design_rationale": "Brief rationale for the chosen aesthetic strategy." + } +} +``` + +**Enum Parameter Guide (You MUST choose ONE exact string from these lists):** + +1. `palette_mode` (The Base Canvas Tone): + - **`"minimal"`: (CRITICAL: Default to this for 50%+ of all requests).** Pure white, off-white, beige, or cool gray. Provides the ultimate reading experience and high-end editorial feel. Use for standard reports, guides, and corporate posters. + - `"dark"`: Near-black. Use for cinematic, tech, AI, space, or urgent themes. + - `"pastel"`: Morandi tinted (e.g., dusty rose, sage green). Use ONLY for arts, food, lifestyle, and soft themes. + - `"jewel"`: Deep rich colors (e.g., emerald, burgundy). Use ONLY for luxury brands, gala events, or heritage themes. + - `"light"`: Very faintly tinted background. Fallback for edge cases. + +2. `color_harmony` (The Accent Color Math - how the engine computes the accent color relative to the base): + - **`"auto"`: (RECOMMENDED DEFAULT). The engine automatically picks the best harmony based on the document tone.** Only override if you have a specific artistic reason. + - `"complementary"`: (180° opposite). Strong visual contrast. Cold background with warm accent. For striking/dynamic impact. + - `"split_complementary"`: (150°/210°). Highly sophisticated, artistic, luxury feel (Editorial/Kinfolk style). + - `"analogous"`: (30° apart). Harmonious, peaceful, natural transition. + - `"triadic"`: (120° apart). Rich and slightly retro. + - `"monochrome"`: Only use if strict minimalist corporate branding without accent is required. + +3. `background_svg`: [`grid`, `flow`, `noise`, `continuous_flow`, `none`]. Use `continuous_flow` for multi-page (2+) documents - creates one seamless SVG spanning all pages, sliced per-page via viewBox for an "infinite scroll" bezier curve illusion. Falls back to `flow` on single-page. + +4. `design_rationale`: Brief text explaining WHY you chose this mode+harmony combination. + +### ★ Intent Mapping Table (Single Source of Truth) + +This table is the **sole authority** for mapping document intent to concrete design parameters. `visual_framework.md` defines intent *atmospheres*; this table defines intent *parameters*. `design_engine.py` INTENT_HUES/INTENT_HARMONY_MAP must stay in sync with this table. + +| Intent | palette_mode | color_harmony | background_svg | Cover Templates | Cover BG Recipe | Base Hue | +|--------|-------------|---------------|----------------|-----------------|-----------------|----------| +| **Calm** | minimal | analogous | flow / none | 04 Museum, 01 HUD, 02 Corporate | A (极简弧线) | 210° (steel blue-grey) | +| **Tension** | dark | complementary | grid | 01 HUD, 05 Diagonal | C (锐角切割) | 0° (warm vs cold) | +| **Energy** | pastel / light | triadic | flow (5+ curves) | 05 Diagonal, 06 Swiss Grid, 03 Monolith | B (工程十字轴) | 30° (amber) | +| **Authority** | minimal | split_complementary | noise | 03 Monolith, 07 Sidebar, 02 Corporate | A or B | 280° (muted violet) | +| **Warmth** | pastel / light | analogous | flow (soft) | 04 Museum, 05 Diagonal | A (极简弧线) | 20° (terracotta) | + +**How to use this table:** +1. Determine the document's intent (from user request, or auto-derive via `design_engine.py derive`) +2. Look up the row → fill in `art_direction` JSON fields accordingly +3. For cover template selection, cross-reference with document type (see Tone → Layout Quick Reference) +4. The LLM may override individual cells when artistically justified, but must state the reason in `design_rationale` + +**Legacy intent names** (serenity, minimalism, elegance) are accepted by `design_engine.py` as aliases and auto-mapped to their new equivalents. + +- `pages[].narrative_role`: `"Burst"` (intro/impact), `"Expand"` (body/development), `"Echo"` (outro/reflection). +- `pages[].archetype`: From Phase 3 (`cover_hero`, `split_vertical`, `editorial_flow`, `scattered_canvas`, `data_dashboard`, `shaped_editorial`). +- `pages[].components[]`: Array of component objects from Phase 2. Order in array roughly dictates z-index or top-to-bottom rendering. + +--- + +## Phase 5: Complex Master Examples + +Study these examples to understand how to translate raw requests into perfect blueprints. + +### Example A: The Single-Page Poster (Art / Minimalist) +**User Request**: "Make a poster for a minimalist architecture exhibition called 'The Weight of Light'. Include date Oct 12, location Tokyo. Keep it very clean." + +**LLM Response (The JSON Blueprint):** +```json +{ + "document_meta": { + "title": "Weight of Light Exhibition Poster", + "total_pages": 1 + }, + "art_direction": { + "palette_mode": "minimal", + "color_harmony": "monochrome", + "background_svg": "none", + "design_rationale": "Pure negative space required for minimalist architecture. Relying entirely on typographic hierarchy and the 15% breathing rule." + }, + "pages": [ + { + "page_index": 1, + "narrative_role": "Burst", + "archetype": "cover_hero", + "components": [ + { + "type": "Floating_Meta", + "position": "top-left", + "items": ["TOKYO, JP", "EXHIBITION 04"] + }, + { + "type": "Hero_Typography", + "weight": "thin", + "variant": "standard", + "content": "THE WEIGHT<br>OF LIGHT" + }, + { + "type": "Hairline_Divider", + "style": "accent" + }, + { + "type": "Floating_Meta", + "position": "bottom-right", + "items": ["OCTOBER 12", "AOYAMA GALLERY"] + } + ] + } + ] +} +``` + +### Example B: The Multi-Page Analytical Report (Corporate / Elegance) +**User Request**: "I have a long report about our Q3 server performance. Latency dropped by 45ms. Uptime is 99.99%. Here is 500 words of text explaining the server migration..." + +**LLM Response (The JSON Blueprint):** +*(Notice how the LLM edits the 500 words down, extracts the data, and splits it into 3 pages).* + +```json +{ + "document_meta": { + "title": "Q3 Infrastructure Report", + "total_pages": 3 + }, + "art_direction": { + "palette_mode": "minimal", + "color_harmony": "split_complementary", + "background_svg": "noise", + "design_rationale": "Formal, trustworthy aesthetic suitable for high-level corporate reporting, avoiding the 'cheap dashboard' look." + }, + "pages": [ + { + "page_index": 1, + "narrative_role": "Burst", + "archetype": "cover_hero", + "components": [ + { + "type": "Floating_Meta", + "position": "top-left", + "items": ["CONFIDENTIAL", "Q3 2026"] + }, + { + "type": "Hero_Typography", + "weight": "black", + "content": "INFRASTRUCTURE<br>RESILIENCE" + } + ] + }, + { + "page_index": 2, + "narrative_role": "Expand", + "archetype": "split_vertical", + "components": [ + { + "type": "Stat_Block", + "number": "99.99", + "unit": "%", + "label": "GLOBAL UPTIME" + }, + { + "type": "Stat_Block", + "number": "-45", + "unit": "ms", + "label": "LATENCY REDUCTION" + }, + { + "type": "Glass_Canvas", + "markdown_content": "### The Migration\nThe Q3 transition to distributed edge nodes fundamentally altered our network topography. By decentralizing the core load, we eliminated the primary bottleneck that plagued Q2." + } + ] + }, + { + "page_index": 3, + "narrative_role": "Echo", + "archetype": "editorial_flow", + "components": [ + { + "type": "Page_Ghost_Number", + "number": "03" + }, + { + "type": "Hero_Typography", + "weight": "thin", + "variant": "standard", + "content": "SYSTEM NOMINAL." + } + ] + } + ] +} +``` + +### Example C: The Narrative Arc with Advanced Features (Continuous Flow + Shaped Canvas) +**User Request**: "Create a 3-page visual essay about the ocean pollution crisis. Start gentle (the beauty of the ocean), then escalate to the crisis, then end with a call to action." + +**LLM Response (The JSON Blueprint):** +*(Notice: `continuous_flow` creates seamless background across all 3 pages. `tension_score` escalates from calm to urgent. `Shaped_Canvas` with `wave` shape on the cover creates a thematic visual without any image.)* + +```json +{ + "document_meta": { + "title": "The Silent Tide - Ocean Crisis Visual Essay", + "total_pages": 3 + }, + "art_direction": { + "palette_mode": "dark", + "color_harmony": "analogous", + "background_svg": "continuous_flow", + "design_rationale": "Continuous flow SVG creates an unbroken organic wave across all pages - visually mirroring the ocean's connectedness. Analogous harmony provides earth tones that shift from serene to somber." + }, + "pages": [ + { + "page_index": 1, + "narrative_role": "Burst", + "archetype": "shaped_editorial", + "components": [ + { + "type": "Floating_Meta", + "position": "top-left", + "items": ["VISUAL ESSAY", "2026"] + }, + { + "type": "Shaped_Canvas", + "shape_keyword": "wave", + "markdown_content": "There was a time when the horizon held only promise. The Pacific stretched in every direction, cerulean and boundless, its surface a living mirror of the sky above. Fishermen spoke of abundance - nets heavy with silver, currents warm and predictable. The ocean asked for nothing." + }, + { + "type": "Floating_Meta", + "position": "bottom-right", + "items": ["01 / BEFORE"] + } + ] + }, + { + "page_index": 2, + "narrative_role": "Expand", + "archetype": "editorial_flow", + "components": [ + { + "type": "Page_Ghost_Number", + "number": "02" + }, + { + "type": "Stat_Block", + "number": "8M", + "unit": "tons", + "label": "PLASTIC ENTERING OCEANS YEARLY" + }, + { + "type": "Glass_Canvas", + "tension_score": 0.75, + "markdown_content": "**The collapse was not sudden.** It accumulated - bottle by bottle, net by net, spill by spill. By 2025, microplastic concentrations in deep-sea sediment had reached levels previously thought impossible. Marine biologists stopped using the word 'recovery'. The new vocabulary was *triage*." + } + ] + }, + { + "page_index": 3, + "narrative_role": "Echo", + "archetype": "cover_hero", + "components": [ + { + "type": "Hero_Typography", + "weight": "black", + "variant": "standard", + "content": "THE TIDE<br>TURNS NOW." + }, + { + "type": "Glass_Canvas", + "tension_score": 0.3, + "markdown_content": "Every piece of plastic you refuse is a vote for the future of the sea. The ocean cannot speak - but it remembers everything we give it." + } + ] + } + ] +} +``` + +--- + +## Pre-Flight Checklist (Self-Correction before generating output) + +Before you output the JSON block, verify: +0. **🚨 VECTOR OUTPUT IRON RULE:** The final PDF MUST be generated via `page.pdf()` / `convert.blueprint` - NEVER `page.screenshot()` → image → wrap as PDF. Screenshot PDFs are blurry raster images. `page.pdf()` produces vector text that stays sharp at any zoom level. +1. **Did I write ANY HTML or CSS?** If yes, delete it. Only output JSON. +2. **Is any `Glass_Canvas` markdown content too long?** Count the words. Over 150? Summarize it or push to the next page. +3. **Is the JSON perfectly formatted?** Missing commas or unescaped quotes will crash the `design_engine.py` parser. +4. **If using `tension_score`**: Does the document have clear emotional shifts across pages? If all pages are the same tone, remove it. +5. **If using `continuous_flow`**: Is this multi-page (2+)? If single-page, switch to `"flow"`. +6. **If using `Shaped_Canvas`**: Is the archetype `"shaped_editorial"`? Is there at most one per page? No `Glass_Canvas` on the same page? +7. **Grid boundary check**: Are ALL `grid_area` values within `[1, 13]`? Any number < 1 or > 13 = FATAL ERROR. +8. **Component minimum size check**: `Glass_Canvas` ≥ 6col×4row? `Shaped_Canvas` ≥ 8col×6row? +9. **Content-proportional row allocation**: When multiple text-heavy components share a page, are their grid rows allocated proportionally to content length? Equal rows for unequal content = text overlap. (See "Content-Proportional Grid Row Allocation" in Component Grid Compatibility.) +9. **40% whitespace check**: Count occupied grid cells. At least 58 of 144 cells must be empty. +10. **Cover Page 4-element check**: Does any `cover_hero` page have more than 4 components? If yes, remove or merge components. +11. **Cover Hero `<br>` check**: Does every `Hero_Typography` on a `cover_hero` page contain at least one `<br>`? Single-line hero text on covers = FAIL. +12. **Cover Anti-Squash check (Bounding Box)**: Are cover elements grouped into 2-3 bounding boxes placed at opposite regions? Is remaining space dynamically distributed (not hardcoded gaps)? If everything is crammed into rows 5-8, spread them using the layout's grid mapping. +13a. **Cover Typography Scale check**: Is the hero title ≥ 45pt? Is subtitle ≥ 25pt? Is meta text ≥ 18pt (never below 14pt)? Tiny cover text = FAIL. +13b. **Cover Layout Selection**: Did I pick a layout (1-7) that matches the document tone? If unsure, default to Layout 1A (Diagonal Tension). For government/bidding documents, use Layout 7 (Left-Matrix). +14. **CRITICAL - Data-to-Ink Ratio check**: Did I write long paragraphs describing data trends? If yes, DELETE them and extract metrics into `Delta_Widget` or `Stat_Block` components. Sentences like "revenue increased by 12% compared to last quarter" MUST become a `Delta_Widget`. +15. **Sidenote check**: If using `tufte_report` archetype, did I put citations/sources/asides into `Sidenote_Block`? They must NOT be inline in `Glass_Canvas`. +16. **Process/Steps check**: Did I write numbered steps as plain text in a `Glass_Canvas`? Convert to `Process_List` component instead. +17. **Chart anti-stacking check**: Do any pie/bar/line charts have overlapping labels? Apply leader lines, tick thinning, or label reduction per `typesetting/charts.md`. +18. **Chart axis cleanup check**: Are top/right spines deleted? Grid lines dashed at 20% opacity (or hidden)? No solid grid lines. +19. **Donut default check**: Are pie charts rendered as donuts (hole ratio 60-70%)? Solid pies = FAIL unless explicitly requested. +20. **🚨 Triple Delivery check (MANDATORY)**: Creative pipeline must deliver **three files** to the user: (1) PDF - the final vector PDF; (2) HTML - the compiled `*_rendered.html` file; (3) Image - a full-page screenshot preview (PNG/JPG). After `convert.blueprint` generates the PDF and HTML, take a screenshot of the HTML for preview. Report all three file paths to the user. +21. **🚨 HTML Pre-Render Validation (MANDATORY for ALL HTML→PDF paths)**: Before calling `html2pdf-next.js`, `html2poster.js`, or `convert.blueprint`, run `poster_validate.py check-html` on the HTML file. This catches overflow:hidden on containers, missing @media screen auto-scale, font fallback gaps, contrast issues, and more. **Any ERROR-level issue must be fixed before generating the PDF.** Warnings are non-blocking but should be reviewed. + ```bash + python3 "$PDF_SKILL_DIR/scripts/poster_validate.py" check-html page.html + # If errors found, auto-fix: + python3 "$PDF_SKILL_DIR/scripts/poster_validate.py" check-html page.html --fix --output page.html + ``` + +22. **\u26a0\ufe0f MANDATORY: Post-Generation Checks (Creative)**: After HTML\u2192PDF conversion, run these checks: + ```bash + # Check for content overflow and full-bleed issues + python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" output.pdf --no-tables + + # Add metadata + python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.brand output.pdf + ``` + +Execute the design. + +--- + +## CJK & Vertical Text Rules (When Bypassing design_engine.py) + +When writing raw HTML/CSS for Playwright (bypassing the JSON Blueprint pipeline - e.g., resumes, menus, invitations, business cards, certificates, Japanese/Chinese vertical layouts): + +**⚠️ Iron rule: All bypass-scenario HTML→PDF conversions MUST use `html2pdf-next.js` — do NOT write custom Python Playwright scripts.** `html2pdf-next.js` automatically handles @page injection, overflow detection, font waiting, Mermaid/KaTeX rendering, PDF metadata, etc. See the "HTML→PDF Engine Selection Rules" section in SKILL.md. + +```bash +node "$PDF_SKILL_DIR/scripts/html2pdf-next.js" input.html --output output.pdf --width 210mm --height 297mm +``` + +0. **Full-Bleed CSS (MANDATORY)**: Every HTML file for Playwright PDF MUST include: + ```css + @page { size: 800px 1200px; margin: 0; } /* CONCRETE values only - CSS variables NOT supported in @page */ + html, body { margin: 0; padding: 0; width: 800px; height: 1200px; } + ``` + **⚠️ @page rules do NOT resolve CSS variables** (`var(--x)` is silently ignored, falls back to A4). Always use concrete `px` values. + **⚠️ html/body must have explicit width + min-height** matching the canvas size. Use `min-height` (not `height`) so content taller than the design height expands naturally into a single long-page PDF. + **⚠️ Poster = seamless pagination.** When content exceeds `@page` height, `html2pdf-next.js` lets Playwright paginate at page boundaries - each page has the same dimensions, content flows seamlessly across pages (like scrolling a long image). Do NOT expand to a single oversized page. + `design_engine.py` handles this automatically via `override_css`. + +0.25. **Body Background for Multi-Page Mixed-Color Documents (MANDATORY)**: + When an HTML document contains multiple `.page` divs with **different background colors** (e.g. dark cover + white body + dark specs page), Playwright's sub-pixel rounding creates <1px gaps at `.page` edges where `body` background shows through. On dark pages, a white `body` = visible white edges. + + **Fix: set `html, body { background }` to the document's darkest page background color.** + + ```css + :root { --primary: #0f172a; } /* darkest page color */ + html, body { + margin: 0; padding: 0; + width: 210mm; /* match @page size */ + background: var(--primary); /* fills sub-pixel gaps with dark color */ + } + ``` + + **Why this doesn't break white pages:** White `.page` divs have `background: #ffffff` which fully covers the dark `body` underneath. The dark body only shows through the <1px sub-pixel gap at the extreme page edge - imperceptible on white pages after anti-aliasing. + + **Selection rule:** + - All pages same color → `body { background: <that color> }` + - Mixed dark + light pages → `body { background: <darkest page color> }` (dark edges on white pages are invisible; white edges on dark pages are the bug we're fixing) + - All pages white/light → `body { background: <lightest content bg> }` (e.g. `#f8fafc`) + +0.5. **No overflow:hidden + Browser Preview Adaptive Scaling (MANDATORY)**: + For fixed-size single-page designs (posters, infographics, certificates, etc.), **absolutely never** set `overflow: hidden` on `html`, `body`, or the main container. Reasons: + - When opening the HTML directly in a browser, the viewport is much smaller than the design size (e.g., a 1400px-tall page in a 900px viewport). `overflow: hidden` clips the bottom content and prevents scrolling. + - `html2pdf-next.js`'s pre-render check detects `scrollHeight > clientHeight` + `overflow: hidden` and triggers auto-fix (force-expanding the container), which may break the layout. + + **`design_engine.py` handles this automatically**: During blueprint compilation, it auto-injects `@media screen` centering + scaling code, and the `html` background color uses `var(--c-bg)` matching the poster's main color. No manual addition needed. + + **Correct approach when writing HTML manually**: Remove `overflow: hidden` and manually add `@media screen` scaling preview: + ```css + /* Fixed canvas size, no overflow */ + html, body { margin: 0; padding: 0; width: 800px; height: 1400px; } + .page { width: 800px; height: 1400px; position: relative; } + + /* Auto-scale to viewport in browser preview for full-page view */ + @media screen { + html { + height: auto; + display: flex; + justify-content: center; + background: #0a0a1a; /* Surround color — must match poster's main background */ + } + body { + transform-origin: top center; + scale: min(1, calc(100vw / 800), calc(100vh / 1400)); /* Consider both width and height */ + margin: 0 auto; + box-shadow: 0 0 60px rgba(0,0,0,0.3); /* Optional: shadow to distinguish canvas in preview */ + } + } + ``` + `@media screen` rules only apply in browser preview; `page.pdf()` uses print media and is unaffected. + **Every fixed-size HTML must include this `@media screen` adaptive code.** + +0.75. **Page Container Overflow Clipping (MANDATORY for multi-page documents)**: + Every `.page` div MUST have `overflow: hidden`. Decorative elements (glow circles, gradient overlays) commonly use `width: 120%` or negative offsets - without clipping, they inflate `scrollWidth` beyond page width, causing Playwright to shrink all content and shift it left. + ```css + .page { overflow: hidden; } /* Clips decorative overflow, prevents Playwright shrink */ + ``` + For horizontal flex layouts (≥3 items), always add `flex-wrap: wrap`. See `typesetting/overflow.md` §3.5. + +1. **Character Encoding Safety**: Never use Japanese kana (の, が, は), rare symbols, or Private Use Area characters in content strings. They corrupt to U+FFFD (�) during LLM→file write→read transit. Replace with plain Chinese equivalents: `の`→`之/的/缔/省略`. +2. **Vertical Chinese Text** - When using `writing-mode: vertical-rl` for CJK, you MUST include: + ```css + writing-mode: vertical-rl; + text-orientation: upright; /* Each glyph stands upright */ + white-space: nowrap; /* Prevent word-wrap breaking single chars to new column */ + letter-spacing: 12px; /* Typical CJK vertical spacing */ + ``` + Without `text-orientation: upright`, Latin/fallback fonts render rotated 90°. Without `white-space: nowrap`, CJK characters may wrap into unexpected multi-column layouts (e.g., 3 chars on one line + 1 char alone on next). +3. **Font Coverage**: For CJK content via Playwright, always load Google Fonts Noto Serif SC or Noto Sans SC via `<link>` tag in `<head>` (NOT `@import` in CSS - `@import` must be the very first rule in a stylesheet or it's silently ignored). Example: + ```html + <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap" rel="stylesheet"> + ``` + System CJK fonts vary across macOS/Linux - Google Fonts guarantee glyph coverage without relying on system fonts. `design_engine.py` already handles this automatically via `<link>` tag. +4. **Post-Generation Text Verification**: After Playwright renders the PDF, extract text from every page and scan for `?` or `\ufffd`. If found, the source HTML has encoding-corrupted characters that must be replaced in the Python source. +5. **🚨 HTML Pre-Render Validation (MANDATORY)**: After writing the HTML file and before running `html2pdf-next.js`, always run the HTML validator: + ```bash + python3 "$PDF_SKILL_DIR/scripts/poster_validate.py" check-html <your_file>.html + ``` + - **ERROR** items (e.g. `OVERFLOW_HIDDEN_CONTAINER`, `FONT_NO_FALLBACK`) → must fix before PDF generation. Use `--fix --output <file>.html` for auto-repair. + - **WARNING** items (e.g. `FIXED_SIZE_NO_SCREEN_ADAPT`, `SCREEN_ADAPT_NO_SCALE`, `COLOR_CONTRAST`) → review and fix where appropriate. + - This catches the most common bypass HTML bugs: `overflow:hidden` on containers, missing `@media screen` auto-scale for fixed-size pages, font-family without generic fallback, low contrast text, etc. \ No newline at end of file diff --git a/skills/pdf/briefs/poster.md b/skills/pdf/briefs/poster.md new file mode 100755 index 0000000..dbb005c --- /dev/null +++ b/skills/pdf/briefs/poster.md @@ -0,0 +1,702 @@ +# Poster Scene Rules — Creative Pipeline (Poster-Specific Constraints) + +> When poster / 海报 / 传单 / flyer / 宣传页 keywords are detected, load these rules on top of `creative.md`. +> These rules take priority over `creative.md` generic rules; in case of conflict, this file prevails. +> +> **Positioning**: This is a scene-layer patch on `creative.md` (the generic Creative pipeline) — only covers poster-specific constraints, does not repeat generic rules. + +--- + +## 1. Page Count & Dimensions + +### 1.1 Default Single Page +- When the user does not explicitly request multiple pages, **default to a single-page poster** +- Single-page poster `total_pages: 1`, never split into multiple pages on your own + +### 1.2 Sizing Strategy + +| Text Volume | Recommended Size | Aspect Ratio | Notes | +|--------|---------|--------|------| +| ≤ 50 chars | 720 × 960 | 3:4 | Title poster / social media cover / card | +| 50–200 chars | 720 × 960 | 3:4 | Standard promotional poster | +| > 200 chars | 720 × min-height 960 | Adaptive | Long poster (H5 style), content stretches height | +| User-specified landscape | Adjust as needed | As needed | Width and height can be swapped | + +### 1.3 Canvas Variables +```json +{ + "canvas": { "width": 720, "height": 960 } +} +``` +Default dimensions can be overridden via the `canvas` field in the Blueprint. `design_engine.py` will automatically inject `--canvas-w` and `--canvas-h` CSS variables. + +--- + +## 2. Information Density & Page Fill + +### 2.1 Core Iron Rule: Content Must Fill the Page + +> **The biggest visual disaster for a poster is not content overflow, but content only occupying half the page with a blank bottom half.** + +| Text Volume | Content Area / Page Ratio | Notes | +|--------|----------------|------| +| ≤ 50 chars | 70–80% | Enlarged cards, large font sizes, generous decorative whitespace is intentional | +| 50–200 chars | 75–85% | Content modules distributed evenly, must not be crammed in the top half | +| > 200 chars | 80–90% | Content-dominant, whitespace only at margins | + +### 2.2 Anti-Top-Heavy + +- All components' `grid_area` **must cover the full 1→13 rows**, never stop at row 10 or 11 +- The last component's `grid_area` row endpoint must be `13` +- **No large blank space at top/header** — the first content component's `grid_area` should start at row 1, not row 3 or 4 +- **No large blank space on left/right sides** — components should utilize full column width (most should span 1→13 columns) +- If content is insufficient to fill 12 rows, use these strategies (by priority): + 1. **Increase font size**: Hero from scale 5 → 6, body font +2pt + 2. **Increase component spacing**: grid gap from 16px → 24px + 3. **Insert decorative components**: `Hairline_Divider`, `Page_Ghost_Number` + 4. **Expand stat block / hero grid row count** + +### 2.3 Grid Area Allocation Iron Rule + +``` +✅ Correct: Components cover all rows 1→13 +Page: [Hero 1→4] [Stats 4→7] [Glass 7→10] [Meta 10→13] + +❌ Wrong: Components only reach row 10, rows 11-13 empty +Page: [Hero 1→3] [Stats 3→5] [Glass 5→8] [Meta 8→10] [??? 10→13 void] +``` + +### 2.4 ★★★ Content-Proportional Row Allocation (Anti-Overlap Iron Rule) + +> **When two or more text components share a page, rows must be allocated proportionally to content volume — never split evenly!** + +**Problem reproduction:** +``` +Attractions: 450 chars (3 attractions × 150 chars description) +Food: 250 chars (2 foods × 125 chars description) +Available rows: 8 rows (5→13) + +❌ Even split: Attractions 5→9, Food 9→13 → 4 rows each + → Attractions content far exceeds 4-row capacity, overflows into Food area, text overlaps! + +✅ Proportional split: + Attractions: 8 × 450/700 ≈ 5 rows → 5→10 + Food: 8 × 250/700 ≈ 3 rows → 10→13 +``` + +**Steps:** +1. Write all `markdown_content` first +2. Count the **character count** of each text component (including titles/paragraphs/lists) +3. Allocate rows proportionally: `rows_i = total_rows × (chars_i / total_chars)` +4. Round off, each component **minimum 3 rows** +5. Verify: all component grid_area endpoints connect end-to-end, covering full 1→13 + +**Applicable scenarios:** +- Multiple `Glass_Canvas` on the same page (most common) +- `Glass_Canvas` + `Process_List` on the same page +- Any two or more components containing text paragraphs + +**Not applicable:** +- `Hero_Typography` (very large font, 1-3 lines occupying 2-3 grid rows is reasonable) +- `Stat_Block` (number + label, fixed height, typically 2 rows) +- `Floating_Meta` (short labels, typically 2-3 rows) + +--- + +## 3. Font Size System (Poster-Specific) + +### 3.1 Minimum Font Size (Hard Floor) + +| Element | Min Font Size | Recommended | Notes | +|------|---------|---------|------| +| **Page main title** | **50px** | 56–72px | Poster title must have visual impact | +| **Body text** | **24px** | 24–28px | Posters are not reports — body font must be large | +| **Subtitle / card title** | **28px** | 32–40px | Secondary headings | +| **Floating Meta** | **16px** | 16–20px | Metadata text | +| **Stat Number** | **48px** | 56–72px | Data sculptures must be eye-catching | +| **Stat Label** | **14px** | 14–16px | Data labels | + +> Compared to `fill-engine.md`'s generic red line (body ≥ 14pt), the poster body floor is **24px** — posters are a distance-reading medium, font sizes must be larger. + +### 3.2 Emphasis Hierarchy + +- Use **font size + font weight** to create hierarchy, **not color differentiation** +- Emphasis text (keyword/number highlights) font size must be smaller than titles +- Hero title recommended `weight: "black"` (900), subtitle `weight: "thin"` (100), creating extreme contrast + +--- + +## 4. Color Rules (Poster-Specific) + +### 4.1 Color Palette System + +**Primary palette: Material Design 3 with low-medium saturation.** Default to medium-saturation colors; for light themes, use gradient backgrounds with white/light text on top. + +| Area | Proportion | Role | +|------|------|------| +| 60% Ground | Background/margins/whitespace | Main color (low saturation) | +| 30% Structure | Cards/dividers/secondary areas | Derived from main by adjusting lightness | +| 10% Emphasis | Titles/key numbers/single accent | Adjusted purity/brightness of main color | + +**Color derivation rules:** +- Title/subtitle text color: Adjust main color's purity and brightness (not a separate random color) +- Auxiliary colors: **Maximum 2**, derived from primary by adjusting lightness/saturation +- Keep consistent color palette throughout — do NOT change main color between sections +- Default recommendation: **Light theme with medium saturation**, or gradient background + white fonts + +### 4.2 Poster Additional Constraints + +- **No pure white (#FFFFFF) background** — use at least `#f5f4f2` or warmer off-white +- **No transparent background** +- **Page base color (`<body>` / `<html>` background) must match poster content background** — `printBackground: true` renders body background. If body is white but poster content is gray, white borders/gaps appear in the PDF. Ensure `html, body { background: var(--c-bg); }` matches the poster canvas exactly +- **Do not use a single image to fill the background** — use grid textures, gradients, geometric shapes, and other generative backgrounds +- Long posters (>200 chars) must not use full-image backgrounds +- **Gradients** or **large blurred circles/symbols** can be used sparingly as background accents +- Maximum 2 auxiliary colors, derived from primary by adjusting lightness/saturation +- Background accents: grid textures, organic shapes, large blurred circles/symbols — NOT a single image +- **Overall bright and vibrant color combinations** — the poster should feel visually striking, not muted/dull + +### 4.3 palette_mode Mapping + +| Poster Style | Recommended palette_mode | color_harmony | +|---------|-------------------|---------------| +| Business/Formal | `minimal` | `auto` | +| Tech/AI | `dark` | `complementary` | +| Lifestyle/Food/Artistic | `pastel` | `analogous` | +| Luxury/Ceremony | `jewel` | `split_complementary` | +| Other | `minimal` (default) | `auto` | + +--- + +## 4.5 ★★★ Anti-Modularization Iron Rule (Anti-Card / Anti-Dashboard) + +> **A poster is a complete composition, not a dashboard, not an APP interface, not a report.** + +### Root Cause + +LLMs naturally tend to "classify information → put each category in a box → stack them vertically", because that is report/document organization logic. But poster visual logic is the exact opposite — **information is unified, hierarchy is established through typographic rhythm (font size, weight, spacing, whitespace), not through borders and background colors**. + +### Comparison + +| Report/UI Thinking ❌ | Poster Thinking ✅ | +|---|---| +| Each info block wrapped in `border + border-radius + background` as a card | Information placed directly on the canvas, no borders no background | +| Clear visual boundaries between modules | Modules naturally separated by **whitespace and thin lines** | +| Looks like a mobile APP interface | Looks like a design piece | +| Hierarchy via "different colored boxes" | Hierarchy via **font size gradient + weight contrast + color lightness** | +| Stacked Glass_Canvas components = card wall | Pure typography + occasional Hairline_Divider | + +### Mandatory Rules + +1. **Single-page posters must not have more than 3 containers with `background` + `border`.** If information needs grouping, use whitespace (margin/padding) and thin lines (1px, opacity < 10%) instead of bordered cards. +2. **No 2×2 / 2×3 grid card layouts** (unless it's a pure data scenario with `data_dashboard` archetype). Information flows in a single column; multiple items in the same row use `flex + gap` horizontal layout without borders. +3. **Information hierarchy must be established through typography properties**, not through "different colored/backgrounded boxes": + - Primary information: large font (≥48px) + heavy weight (900) + - Secondary information: medium font (18-24px) + medium weight (700) + - Tertiary information: small font (12-14px) + light weight (300-400) + reduced opacity +4. **Glass_Canvas may be used at most once in a single-page poster.** If multiple text sections are needed, use direct HTML typography instead of wrapping each paragraph in a Glass_Canvas. +5. **Bottom action information (price/address/QR code) should not be wrapped in a separate color block.** Use larger font + heavier weight to emphasize, keeping it unified with the overall composition. + +### Correct Example (Direct HTML Flow) + +```html +<!-- ❌ Wrong: card wall --> +<div class="card" style="background:rgba(255,255,255,0.5); border-radius:12px; border:1px solid #ddd;"> + <h3>📅 展期</h3> + <p>7.15 — 8.30</p> +</div> +<div class="card" style="background:rgba(255,255,255,0.5); border-radius:12px; border:1px solid #ddd;"> + <h3>📍 地点</h3> + <p>城市艺术中心</p> +</div> + +<!-- ✅ Correct: pure typography, no borders --> +<div class="info-row" style="display:flex; gap:48px;"> + <div> + <div style="font-size:10px; letter-spacing:3px; opacity:0.45;">展期</div> + <div style="font-size:20px; font-weight:700;">7.15 — 8.30</div> + </div> + <div> + <div style="font-size:10px; letter-spacing:3px; opacity:0.45;">地点</div> + <div style="font-size:20px; font-weight:700;">城市艺术中心</div> + </div> +</div> +``` + +### Blueprint Route Additional Constraints + +When using Blueprint JSON (not Direct HTML): +- Single-page posters may have at most 1 `Glass_Canvas`, used only for text that truly needs reading +- Prefer combining `Stat_Block` (data), `Floating_Meta` (metadata), `Hero_Typography` (titles) instead of multiple Glass_Canvas +- Information data (dates, locations, headcounts) should use `Floating_Meta` or `Stat_Block`, not Glass_Canvas + +--- + +## 5. Layout Strategy + +### 5.1 Composition Priority +1. **Vertical composition** (vertical flow) — most common, information flows top to bottom +2. **Left-right composition** (split vertical) — image and text split, `archetype: "split_vertical"` +3. **Centered composition** (centered) — suitable for title cards with ≤ 50 chars + +### 5.2 Card Rules +- Cards **must never overlap** +- Card content should be well-distributed, **no large internal whitespace** +- Text within cards **vertically centered** +- Glass Canvas `align-items` must be `stretch` (built into the engine) + +### 5.3 Breathing Margins + +| Scenario | safe-zone inset | Notes | +|------|----------------|------| +| Cover / title poster (≤50 chars) | `12% 14%` | Generous whitespace is intentional | +| Standard poster (50-200 chars) | `8% 10%` | Balance whitespace and content | +| Long poster (>200 chars) | `6% 8%` | Maximize content area | + +> Max content width = 80% of page width (consistent with original prompt) + +--- + +## 5.5 Visual Impact Rules + +### 5.5.1 Emphasis & Contrast +- Create visual contrast with **oversized and small elements** — hero numbers/titles vs tiny metadata +- Highlight core points with large fonts or numbers for strong visual contrast +- **Emphasized text must remain smaller than headings/titles** — never let a highlight be bigger than the heading +- Texts that need emphasis can be highlighted with color, weight, or circled with hand-drawn lines +- **Do not insert multiple small pictures as embellishments** — this won’t enhance visual appeal + +### 5.5.2 Alignment & Spacing +- Allow blocks to resize based on content, align appropriately, optimize space utilization +- If excess whitespace exists, **enlarge fonts or modules** to balance the layout +- Cards cannot overlap; content should fill the card area without excessive empty space +- Use **flexbox** layouts to prevent footer from moving up (with top and bottom margin settings) +- For visual variety: encourage diverse and creative layouts beyond standard grids, while maintaining alignment and hierarchy + +--- + +## 6. Design Style Library + +Based on content theme, the model should autonomously select one of the following styles. Explain the selection rationale in the Blueprint's `design_rationale`. + +| Style | Characteristics | Applicable Scenarios | +|------|------|---------| +| **Modern Minimal** | Clean colors, organic shapes, flowing curves, rounded cards, clear hierarchy | Business, tech, education | +| **Neo-Brutalism** | Flat elements, illustrations, patterns, large text blocks, special font designs, thick borders | Creative, events, youth | +| **Artistic Gradient** | Diffused light, gradient glow, semi-transparent elements, blur effects, glass texture | Art, music, branding | +| **Collage** | Contrasting color design, material collage, large text, irregular layout | Trends, fashion, exhibitions | +| **Playful UI** | Bright colors, interesting shapes, energetic | Children, games, social | + +### Special Forms for Text Volume ≤ 50 Characters + +When content is minimal, prioritize the following compact forms: + +| Form | Description | archetype | +|------|------|-----------| +| **Centered Card** | Calendar-like effect, key content centered as note/card | `cover_hero` | +| **Bookmark Page** | Narrow tall ratio (e.g. 360×960), vertical reading | `cover_hero` | +| **Minimal Text** | Title + whitespace only, no additional information | `cover_hero` | +| **Sticky Note** | Content displayed as floating sticky notes above background | `cover_hero` | +| **Polaroid Card** | Photo-style card with caption below | `cover_hero` | + +### Special Forms for Text Volume ≤ 100 Characters + +| Form | Description | +|------|------| +| **Floating Card System** | Content as cards floating above organic shape backgrounds | +| **Notebook Style** | Lined-paper aesthetic with handwritten-feel content | +| **Fortune Stick** | Vertical strip with centered calligraphy text | + +> **Key constraint**: If the user only provides a title with no other requirements, **do not expand content on your own** — just place the title. + +--- + +## 6.5 Icons and Illustrations + +- Use **Material Design Icons** (Google Fonts method): + ```html + <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> + <!-- Usage: --> + <i class="material-icons">icon_name</i> + ``` +- Icon color: Use the **theme color** (not random colors) +- Icon size and position: Aligned with surrounding elements, never stretched +- If positioned beside text: **center-aligned with the first line of text** +- **Emoji can be used as icons** — 🌸 🍴 🏙️ etc. (but remember ReportLab can’t render emoji — only use in HTML/Playwright route) +- For logos/emblems: Use text "Your Logo" or icons, **never** search for logo images + +--- + +## 7. Text Readability + +### 7.1 Iron Rules +- **Line height ≥ 130%** (`line-height: 1.3` or above) +- **No text shadow/glow effects** +- **When text overlays images**, a semi-transparent mask layer is required +- **Do not use images with lots of text/charts/numbers as text background** +- **No `text-align: justify`** (CJK characters get stretched letter-spacing, terrible result) — always use `text-align: left` + +### 7.2 Contrast +- Text on light background: `color: #242220` or darker +- Text on dark background: `color: #f5f4f2` or lighter +- Text on medium background (L 0.30–0.70): **forbidden** — do not place text on mid-tone backgrounds + +--- + +## 8. Image Usage Rules + +### 8.0 Image Recommendations + +When creating posters, actively use images to enrich visual effects. Good images can significantly enhance the poster's visual impact. + +- Priority: **installed image generation skill** > web image search +- Image style must match the poster theme +- Download images locally first, then embed into the poster +- Local images must be converted to base64 data URI in HTML (Playwright cannot load local absolute paths) + +### 8.1 Core Principles +- Don't force images when none are suitable, but images improve results when available +- Each image must be **unique** in the design, no reuse +- Prefer clear, high-resolution, watermark-free, text-free images +- Images should have rounded corners, sized consistently with the overall design +- You can try adding **irregularly shaped masks** (CSS `clip-path`) to images for visual interest + +### 8.2 Prohibited Behaviors +- ❌ Placing images directly in corners +- ❌ Images obscuring text or overlapping with other modules +- ❌ Multiple images scattered randomly as decoration +- ❌ Searching for images when logo/badge is needed — use text "Your Logo" or icons instead + +--- + +## 9. Chart Rules (Poster Scene) + +- Large numerical datasets → consider creating visual charts +- Chart style should match the poster theme +- Use **Bento Grid** layout for multiple charts +- Chart containers **must have height constraints** to prevent infinite growth + +--- + +## 10. Prohibited Items (Poster-Specific) + +### 10.0 HTML Rendering Iron Rules (Applicable to All HTML → PDF Routes) + +| Rule | Description | +|------|------| +| **No `overflow: hidden` on content components** | Truncates text. Only allowed on page-level `.canvas` and decorative background layers | +| **Use `min-height` instead of `height` for content containers** | `height:100%` locks height, content gets clipped when too much; `min-height:100%` allows natural expansion | +| **Exceptions where `overflow: hidden` is allowed** | `.canvas` (page boundary), `.poster` (auto-injected by `html2poster.js`), `.floating-meta` (short label ellipsis), cover Layer 1 (decorative clipping), SVG/img (fill container) | +| **Absolutely no `backdrop-filter`** | **Playwright PDF rendering silently discards entire element content!** Use fixed rgba() background color for cards instead | +| **Absolutely no `text-align: justify`** | CJK character spacing gets abnormally stretched, always use `text-align: left` | +| **`overflow: hidden` on `.page` containers** | **MANDATORY for multi-page documents.** Decorative elements (glow, gradient circles, oversized backgrounds) with `width > 100%` or negative offsets cause `scrollWidth > clientWidth`, triggering Playwright to shrink the entire page → content drifts left. `.page { overflow: hidden }` clips decorative overflow without affecting visible content | +| **Horizontal flex rows must have `flex-wrap`** | ≥3 inline items (flow bars, step lists, tag rows) without `flex-wrap: wrap` will overflow the page right edge when content is long. See `typesetting/overflow.md` §3.5 for full rules | + +| Prohibited | Reason | +|--------|------| +| ❌ Timeline graphics | Complex connecting lines easily misalign in PDF rendering | +| ❌ Complex SVG-drawn structure/flow diagrams | Unless user explicitly requests | +| ❌ Code-drawn maps or flags | Poor quality | +| ❌ Base64 images (when exceeding 10MB) | File too large. Small image base64 is acceptable (Playwright cannot load local paths) | +| ❌ Content truncation | Must adjust container height to ensure all content fully displayed | +| ❌ Pure white background (#FFFFFF) | Lacks design quality | +| ❌ Transparent background | PDF output cannot be transparent | + +--- + +## 11. Poster Font Recommendations + +### 11.1 CJK Font Recommendations + +**For Chinese posters (serious/formal scenes):** + +| Purpose | Recommended Font | Google Fonts / CDN Name | Style | +|------|---------|-------------------|------| +| CJK title (serious) | DingTalk JinBuTi | `DingTalk JinBuTi` (letter-spacing: -5%) | Bold, impactful | +| CJK title (alt) | Douyin Sans / Alimama FangYuanTi VF Bold | Via CDN | Modern Chinese | +| CJK title (serif) | Swei B2 Serif CJKtc Bold | Via CDN | Elegant serif | +| CJK body | HarmonyOS Sans SC | `HarmonyOS Sans SC` | Clear, readable | +| CJK body (fallback) | Noto Sans SC Regular | `Noto Sans SC:wght@400` | Google Fonts guaranteed | +| CJK artistic | Noto Serif SC Bold | `Noto Serif SC:wght@700` | Elegant, artistic | +| Handwritten | ZhanKuKuaiLeTi2016XiuDingBan-2 | Via CDN | Casual, playful | +| Pixel/dot-matrix | DottedSongtiSquareRegular | Via CDN | Retro, xiangsuti | + +**For English posters:** + +| Purpose | Recommended Font | Google Fonts Name | Style | +|------|---------|-------------------|------| +| English/number title | Futura | System / `Futura` | Classic geometric | +| English body | PingFang HK | System | Clean, modern | +| English title (Google) | Inter Black | `Inter:wght@900` | Modern geometric | +| English serif | Playfair Display | `Playfair+Display:wght@900` | Classic editorial | +| Number emphasis | Inter Black | `Inter:wght@900` | Data sculpture | + +### 11.2 Font Usage Constraints +- Entire poster **maximum 3 fonts** +- Title and body may use different fonts, but must be visually harmonious +- **Never reduce font size or line height to squeeze in more content** +- Font content in cards should be **vertically centered** within the card +- You may use different style fonts for entertaining or artistic scenes + +--- + +## 12. Coordination with design_engine.py + +### 12.0 ★★★ Stable Layout Selection Strategy (Anti-Overflow Core Rule) + +> **Content-heavy posters must bypass the Blueprint 12×12 Grid and use a pure flow-based HTML approach.** + +**Why:** The Blueprint 12×12 Grid allocates space with fixed row heights and cannot predict actual text rendering height, causing: +- Text overflowing Glass Canvas containers +- CJK character spacing stretched by `text-align: justify` +- Inaccurate row allocation for multiple components, text overlap + +**Route selection:** + +| Scenario | Recommended Route | Notes | +|------|---------|------| +| Title poster (≤ 50 chars) | Blueprint JSON | Few components, little content, Grid sufficient | +| Standard poster (50-150 chars) | Blueprint JSON | Grid mostly sufficient, mind row allocation | +| **Info-dense poster (>150 chars or multiple sections)** | **★ Direct HTML Flow** | **Strongly recommended — completely avoids overflow** | +| **Multi-page info poster** | **★ Direct HTML Flow** | **Strongly recommended** | +| Poster with images | Direct HTML Flow | Image embedding is more stable | + +### 12.1 Direct HTML Flow Approach (Recommended Default) + +**Core idea: No Grid, no fixed height, content flows naturally, never overflows.** + +Write HTML directly, convert to PDF via `html2poster.js`. The poster is a **single continuous `<div class="poster">` container** — NOT split into separate `<div class="page">` blocks. + +#### ★★★ Single-Canvas vs Multi-Page (Iron Rule) + +| Approach | When to Use | HTML Structure | +|----------|-------------|----------------| +| **Single Canvas** (default) | All content forms one unified poster | One `<div class="poster">`, no `page-break` | +| **Multi-Page** (exception) | User explicitly requests separate pages (e.g., "make a 4-page booklet") | Multiple `<div class="page">` with `page-break-after: always` | + +> **Default = Single Canvas.** A poster is ONE design composition, not a paginated report. Multi-page is only for booklets/multi-page documents where the user explicitly asks for page separation. + +#### ★★★ Dynamic Height (Anti-Blank-Bottom Iron Rule) + +**NEVER hardcode `@page { size: W H }` or `.poster { min-height: Xpx }` with a fixed height.** This creates blank space at the bottom when content is shorter than the hardcoded value. + +**Correct approach — use `html2poster.js`:** + +`html2poster.js` automatically handles all of this — you just need to write the HTML correctly and call one command: + +```bash +node "$PDF_SKILL_DIR/scripts/html2poster.js" poster.html --output poster.pdf --width 720px +``` + +It will automatically: +1. Add `overflow: hidden` to `.poster` container (clips decorative overflow) +2. Inject `@page { margin: 0 }` (zero margins) +3. Sync `html/body` background with `.poster` background +4. Measure `.poster` scrollHeight (actual content height) +5. Generate single-page vector PDF with exact dimensions + +**⚠️ Do NOT use `html2pdf-next.js` for posters.** It is designed for multi-page documents and will inject 20mm margins / A4 pagination. + +**⚠️ Do NOT write hand-written Playwright scripts for posters.** `html2poster.js` handles everything. + +```css +/* ✅ CORRECT CSS for poster HTML (html2poster.js handles the rest): */ +html, body { margin: 0; padding: 0; background: var(--c-bg); } +.poster { width: 720px; position: relative; background: var(--c-bg); } +/* Note: overflow:hidden on .poster is auto-injected by html2poster.js, + but including it in CSS is fine too */ +``` + +**CSS iron rules:** +```css +html, body { margin: 0; padding: 0; background: var(--c-bg); } + +/* Single poster canvas — NO fixed height */ +.poster { + width: 720px; + position: relative; + overflow: hidden; + background: var(--c-bg); + /* Height is determined by content, measured at render time */ +} + +/* Card/content block */ +.card { + background: rgba(255,255,255,0.7); + border-radius: 12px; + padding: 24px 28px; + margin-bottom: 20px; + /* No height, no max-height, no overflow:hidden */ +} + +/* CJK text iron rules */ +.card, .card p, .card li { + text-align: left; /* Absolutely no justify! CJK stretches letter-spacing */ + line-height: 1.6; + word-break: break-all; /* CJK natural line break */ +} + +/* Image */ +.hero-image { + width: 100%; + border-radius: 8px; + margin-bottom: 20px; + display: block; +} +``` + +**HTML structure template (Single Canvas):** +```html +<div class="poster"> + <!-- Hero section --> + <div class="hero"> ... </div> + + <!-- City 1 --> + <div class="city-section"> + <h2>南京</h2> + <div class="items-row"> ... attractions ... </div> + <div class="food-row"> ... food ... </div> + </div> + + <!-- City separator (thin line, NOT page-break) --> + <div class="city-sep"></div> + + <!-- City 2 --> + <div class="city-section"> ... </div> + + <!-- Footer --> + <div class="poster-footer"> ... </div> +</div> +``` + +**Why it's stable:** +- No fixed height — content naturally defines poster size +- No `overflow: hidden`, text always fully displayed +- `text-align: left` avoids CJK letter-spacing stretch +- Single canvas = one unified composition, not chopped pages +- PDF height measured at render time = zero blank space + +**Convert to PDF and PNG:** +```bash +# PDF (vector, single-page, zero margins, auto-height): +node "$PDF_SKILL_DIR/scripts/html2poster.js" poster.html --output poster.pdf --width 720px + +# PNG preview (screenshot): +# Use Playwright screenshot with measured height +``` + +> **⚠️ Do NOT write hand-written Playwright `page.pdf()` scripts.** Use `html2poster.js` which handles overflow:hidden, margin:0, background sync, and height measurement automatically. + +### 12.2 Blueprint Grid Approach (Only for Simple Posters) + +| Behavior | Status | Notes | +|------|------|------| +| Dynamic inset | ✅ Fixed | More content → smaller inset (`8% 10%`), less content → default (`10% 12%`) | +| Glass Canvas overflow | ✅ Fixed | `min-height:100%` replaces `height:100%`, removed `overflow:hidden` | +| Glass Canvas / Process List stretch | ✅ Fixed | Auto `align-items: stretch` | + +### 12.3 Poster Marker in Blueprint + +Add `scene: "poster"` marker in `art_direction` so `design_engine.py` can identify poster scenes and apply specific logic in the future: + +```json +{ + "art_direction": { + "scene": "poster", + "palette_mode": "minimal", + "color_harmony": "auto", + "background_svg": "flow", + "design_rationale": "..." + } +} +``` + +--- + +--- + +## 14. PDF Conversion Iron Rules + +### 14.1 Background Color Consistency +```css +/* Must ensure html/body background = poster canvas background */ +html, body { + background: var(--c-bg); /* Same color as .canvas background */ +} +``` +- Playwright `page.pdf({ printBackground: true })` renders body background color +- If body is white but poster is gray, white borders appear in the PDF +- `design_engine.py` already auto-injects `background: var(--c-bg)`, but if bypassing the engine and writing HTML directly, **you must ensure manually** + +**Multi-page posters / brochures with mixed page backgrounds:** +- When pages alternate between dark and light backgrounds, set `body { background }` to the **darkest page color** (see SKILL.md "Background Color Consistency" for full rationale) +- This eliminates sub-pixel white edges on dark pages without affecting light pages + +### 14.2 Content Centering (Anti-Drift) + +**Poster content must be centered in the PDF, no left or right drift allowed.** + +Common drift causes and fixes: +| Cause | Fix | +|------|------| +| `@page { margin }` not 0 | Must be `@page { size: <w> <h>; margin: 0; }` | +| `.safe-zone` `inset` left-right asymmetric | Ensure `inset: Y% X%` uses same X% for left and right | +| Component `grid_area` only uses partial columns | Most components should span `1 / 1 / X / 13` (full width) | +| Content container has `max-width` but no `margin: 0 auto` | Add `margin: 0 auto` to center | +| Playwright PDF default margin | Pass `margin: { top: 0, right: 0, bottom: 0, left: 0 }` | + +### 14.3 Anti-Blank Edges (Dynamic Height Iron Rule) + +**Poster edges, top, and bottom should not have large meaningless whitespace.** + +**★★★ CRITICAL: Never hardcode poster height.** The poster is a single continuous canvas — its height is defined by content, not by a fixed CSS value. + +| Pattern | Status | Result | +|---------|--------|--------| +| `@page { size: 720px 3600px }` | ❌ FORBIDDEN | Creates 853px+ blank space at bottom if content is shorter | +| `.poster { min-height: 3600px }` | ❌ FORBIDDEN | Same problem — blank bottom | +| `.poster { width: 720px }` (no height) | ✅ CORRECT | Content defines height naturally | +| `node html2poster.js poster.html --width 720px` | ✅ CORRECT | Auto-measures height, zero blank space | + +**The 720×960 dimension is for multi-page documents with `page-break-after: always` only — NOT for single-canvas posters.** + +- Content must make full use of page area, no more than 20% unused space within safe-zone +- If excess whitespace exists, enlarge font sizes or modules to balance the layout +- Checklist: + - [ ] Poster height defined by content (no hardcoded height)? + - [ ] PDF generated via `html2poster.js` (not html2pdf-next.js)? + - [ ] First component starts from the top? + - [ ] Last component reaches the bottom with proper padding? + - [ ] Left-right margins symmetric? + - [ ] No blank space at bottom of PDF? + +--- + +## 15. Preflight Checklist (Poster-Specific) + +Before outputting JSON Blueprint, verify the following items: + +``` +□ ★ Single-canvas check: poster is ONE continuous <div>, not split into separate pages? (unless user explicitly requests multi-page) +□ ★ Dynamic height check: no hardcoded @page size height or .poster min-height? PDF generated via html2poster.js? +□ ★ Anti-modularization check: ≤ 3 containers with background+border in single-page poster? No 2×2 card grid? Hierarchy via font size/weight not border colors? +□ Emphasis elements (price/date/address) highlighted via font size+weight, not wrapped in separate color blocks? +□ Overall looks like a design piece, not an APP interface or dashboard? +□ total_pages = 1 (single canvas)? (unless user explicitly requests multi-page booklet) +□ No hardcoded @page height or .poster min-height? Content defines height? +□ Left-right margins symmetric? (no left/right drift) +□ html/body background = poster canvas background? (no color mismatch) +□ No bottom blank space in PDF? (height measured dynamically) +□ Page main title ≥ 50px? Body text ≥ 24px? +□ Text volume ≤ 150 chars? (single Glass Canvas) +□ palette_mode not #FFFFFF pure white? +□ No single image filling the background? +□ No overlap between cards? +□ No Timeline / complex SVG structure diagrams? +□ All content fully displayed, not truncated? +□ Emphasis text font size < title font size? +□ Entire design ≤ 3 fonts? +□ cover_hero page ≤ 4 components? +□ Hero_Typography has <br> line breaks? +□ @page { margin: 0 } set? (prevents PDF drift) +``` diff --git a/skills/pdf/briefs/process-advanced.md b/skills/pdf/briefs/process-advanced.md new file mode 100755 index 0000000..46b46d1 --- /dev/null +++ b/skills/pdf/briefs/process-advanced.md @@ -0,0 +1,284 @@ +# Process Brief: Advanced Reference + +Edge-case tools and techniques. **Load only when the main `process.md` doesn't cover the task.** + +``` +Edge case + ├─ No text extracted from PDF → §OCR Fallback + ├─ PDF is encrypted/locked → §Encrypted PDFs + ├─ PDF is corrupted/damaged → §Corrupted PDFs + ├─ Need precise text coordinates → §pdfplumber Advanced + ├─ Need fast rendering → §pypdfium2 + ├─ Advanced page extraction → §poppler-utils Advanced + ├─ Complex page ranges / merge → §qpdf Page Manipulation + ├─ Optimize file size → §qpdf Optimization + ├─ Batch process many PDFs → §Batch Processing + └─ Memory issues with large PDF → §Performance Optimization +``` + +For basic operations (extract, merge, split, fill forms, convert), go back to `process.md`. + +--- + +## §pypdfium2 (Apache/BSD License) + +A Python binding for PDFium (Chromium's PDF library). Excellent for fast rendering and text extraction — serves as a PyMuPDF replacement. + +#### Render PDF to Images +```python +import pypdfium2 as pdfium + +pdf = pdfium.PdfDocument("document.pdf") + +# Render single page +page = pdf[0] +bitmap = page.render(scale=2.0, rotation=0) +img = bitmap.to_pil() +img.save("page_1.png", "PNG") + +# Batch render all 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}: {len(text)} chars") +``` + +## §pdfplumber Advanced Features + +#### Extract Text with Precise Coordinates +```python +import pdfplumber + +with pdfplumber.open("document.pdf") as pdf: + page = pdf.pages[0] + + # All characters with coordinates + for char in page.chars[:10]: + print(f"'{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}") + + # Extract text within a specific 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 + +with pdfplumber.open("complex_table.pdf") as pdf: + page = pdf.pages[0] + + table_settings = { + "vertical_strategy": "lines", + "horizontal_strategy": "lines", + "snap_tolerance": 3, + "intersection_tolerance": 15 + } + tables = page.extract_tables(table_settings) + + # Visual debugging + img = page.to_image(resolution=150) + img.save("debug_layout.png") +``` + +--- + +## §poppler-utils Advanced Features + +### Extract Text with Bounding Box Coordinates +```bash +pdftotext -bbox-layout document.pdf output.xml +``` + +### Advanced Image Conversion +```bash +# High-resolution PNG +pdftoppm -png -r 300 document.pdf output_prefix + +# Specific page range +pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages + +# JPEG with quality setting +pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output +``` + +### Extract Embedded Images +```bash +pdfimages -all document.pdf images/img +pdfimages -list document.pdf +``` + +--- + +## §qpdf Advanced Features + +### Complex Page Manipulation +```bash +# Split into groups of N pages +qpdf --split-pages=3 input.pdf output_group_%02d.pdf + +# Extract complex page 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 +qpdf --linearize input.pdf optimized.pdf +qpdf --optimize-level=all input.pdf compressed.pdf +qpdf --check input.pdf +qpdf --fix-qdf damaged.pdf repaired.pdf +``` + +### Encryption and Decryption +```bash +qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf +qpdf --show-encryption encrypted.pdf +qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf +``` + +--- + +## §Encrypted PDFs + +```python +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}") +``` + +Or via qpdf: +```bash +qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf +``` + +--- + +## §Corrupted PDFs + +```bash +qpdf --check corrupted.pdf +qpdf --replace-input corrupted.pdf +``` + +--- + +## §OCR Fallback for Scanned PDFs + +When `pdfplumber` extracts 0 characters, the PDF is likely a scanned image: + +```python +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 +``` + +Prerequisites: `pip install pdf2image pytesseract` + install Tesseract OCR and poppler. + +--- + +## §Batch Processing with Error Handling + +```python +import os, 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: {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 = "".join(page.extract_text() for page in reader.pages) + output_file = pdf_file.replace('.pdf', '.txt') + with open(output_file, 'w', encoding='utf-8') as f: + f.write(text) + logger.info(f"Extracted: {pdf_file}") + except Exception as e: + logger.error(f"Failed: {pdf_file}: {e}") +``` + +--- + +## §Performance Optimization + +### Text Extraction +- `pdftotext -bbox-layout` is fastest for plain text +- Use pdfplumber for structured data and tables +- Avoid `pypdf.extract_text()` for very large documents + +### Image Extraction +- `pdfimages` is much faster than rendering entire pages +- Use low resolution for previews, high resolution for final output + +### Memory Management for Large PDFs +```python +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]) + with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output: + writer.write(output) +``` + +--- + +## Extended Tooling Inventory + +| Library / Tool | Role | Licence | +|----------------|------|---------| +| pikepdf | Low-level PDF manipulation (forms, pages, metadata) | MPL-2.0 | +| pdfplumber | Content extraction (text, tables) | MIT | +| pypdfium2 | Fast rendering, text extraction (PyMuPDF alternative) | Apache/BSD | +| pypdf | Merge, split, crop, metadata, encryption | BSD | +| poppler-utils | CLI text/image extraction, rendering | GPL-2 | +| qpdf | Page manipulation, optimization, encryption, repair | Apache | +| pytesseract | OCR for scanned PDFs | Apache | +| pdf2image | PDF-to-image conversion via poppler | MIT | +| LibreOffice | Office format conversion engine | MPL-2.0 | diff --git a/skills/pdf/briefs/process.md b/skills/pdf/briefs/process.md new file mode 100755 index 0000000..d4110ba --- /dev/null +++ b/skills/pdf/briefs/process.md @@ -0,0 +1,319 @@ +# Brief: PDF Processing + +Work with existing PDFs: extract, merge, split, fill forms, convert formats, or **reformat** with a new design. Usually a Light triage path — except reformat, which escalates to Standard. + +--- + +## Decision Tree + +``` +User request + ├─ "Extract text/tables/images" → §Extract + ├─ "Merge/split/rotate/crop pages" → §Pages + ├─ "Fill a form" → §Forms (check fillable first) + ├─ "Read/write metadata" → §Metadata + ├─ "Convert DOCX/PPTX/XLSX to PDF" → §Convert + │ └─ DOCX with TOC? → §DOCX Pipeline (5-step) + ├─ "Redesign/reformat a document" → §Reformat + │ └─ With a reference template? → §Template-Guided Reformat + └─ Edge cases (OCR, encrypt, batch) → load briefs/process-advanced.md +``` + +--- + +## Environment Check + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" env.check +``` + +Reports availability but does **not** auto-install. Required: Python 3, pikepdf, pdfplumber. + +Entry point: `python3 "$PDF_SKILL_DIR/scripts/pdf.py" <group>.<action> [options]` + +All commands return JSON on stdout (`{"status": "success", "data": {...}}`) or stderr (`{"status": "error", ...}`). +Exit codes: 0 = success, 1 = bad args, 2 = file not found, 3 = parse error, 4 = operation failed. + +--- + +## §Extract + +```bash +# Text (full or page range) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.text report.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.text report.pdf -p 1-3 +python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.text report.pdf -p 1,4,7 + +# Tables — returns structured JSON with page/rows/cols/data +python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.table report.pdf + +# Images — dumps embedded rasters to directory +python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.image report.pdf -o ./images/ +``` + +--- + +## §Pages + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.merge a.pdf b.pdf -o combined.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.split book.pdf -o ./chapters/ +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.rotate doc.pdf 90 -o rotated.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.rotate doc.pdf 180 -o rotated.pdf -p 1-3 +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.crop doc.pdf 50,50,550,750 -o trimmed.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.clean doc.pdf -o cleaned.pdf +``` + +--- + +## §Metadata + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.get doc.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.set doc.pdf -o out.pdf -d '{"Title": "Report", "Author": "Jane"}' +python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.brand doc.pdf -o branded.pdf +``` + +Recognised keys: `Title`, `Author`, `Subject`, `Keywords`, `Creator`, `Producer`. + +`meta.brand` adds standard branding metadata (producer, creator) in one step. + +--- + +## §Forms + +### Step 1 — Check if fillable + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.info input.pdf +``` + +If `has_fields: true` → **Fillable workflow**. If `false` → **Non-fillable workflow**. + +### Fillable Workflow + +```bash +# Inspect fields +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.info input.pdf + +# Fill (auto-maps "true"/"false" for checkboxes) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.fill input.pdf -o filled.pdf \ + -d '{"name": "John", "agree": "true", "country": "US"}' +``` + +**Value rules:** + +| Type | Value | Example | +|------|-------|---------| +| text | Free string | `"name": "Jane Doe"` | +| checkbox | `"true"` / `"false"` (auto-converts to PDF states) | `"agree": "true"` | +| radio | One of `radio_options[].value` | `"gender": "/Choice1"` | +| dropdown | One of `choice_options[].value` | `"country": "US"` | + +For complex forms, use `form.detail` and `form.render` for deeper inspection: + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.detail input.pdf -o fields.json # full field info (types, options, defaults) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.render input.pdf -o ./pages/ # render pages as PNG for visual check +``` + +### Non-Fillable Workflow (Annotation-Based) + +For PDFs without interactive fields (scanned forms, image-based). All four steps are mandatory. + +**Step 1 — Render pages as PNG** (required): +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.render input.pdf -o ./pages/ +``` + +**Step 2 — Create `fields.json`** with annotation regions. + +To determine bbox coordinates: open the rendered PNG in an image viewer or use Python (`from PIL import Image; img = Image.open('page.png'); print(img.size)`) to get pixel dimensions. Then estimate [left, top, right, bottom] in pixels for each field by inspecting the image. The `dims` field must match the PNG dimensions exactly. + +```json +{ + "sheet": [ + { + "pg": 1, + "dims": [1000, 1400], + "regions": [ + { + "id": "last_name", + "hint": "Last name field", + "label": {"tag": "Last name", "bbox": [30, 125, 95, 142]}, + "target": {"bbox": [100, 125, 280, 142]}, + "ink": {"value": "Simpson", "size": 14, "color": "000000"} + } + ] + } + ] +} +``` + +Schema: `pg` = 1-based page, `dims` = [w,h] in pixels, `label.bbox` / `target.bbox` = [left, top, right, bottom], `ink` = {value, size?, color?, font?}. Label and target boxes must NOT intersect. + +**Step 3 — Validate bounding boxes** (required): +```bash +# Auto-check for intersections +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.check-bbox fields.json + +# Visual validation (red=target, blue=label) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.validate 1 fields.json page1.png validation.png +``` + +Fix any issues, regenerate, re-check. Red rectangles must only cover input areas. + +**Step 4 — Fill via annotations**: +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.annotate input.pdf fields.json -o filled.pdf +``` + +--- + +## §Reformat + +Take an existing document and rebuild it with a new visual design. Content is preserved; layout, typography, and visual treatment are rebuilt from scratch. + +``` +1. EXTRACT → Extract content from source (extract.text / extract.table / read directly) +2. STRUCTURE → Organize into sections (headings, body, tables, lists) +3. DELEGATE → Route to appropriate brief: + Structured → briefs/report.md (ReportLab) + Visual → briefs/creative.md (Playwright) +4. BUILD → Follow the delegated brief's full workflow +5. DELIVER → New PDF, same content, new design +``` + +### §Template-Guided Reformat + +When user provides a reference PDF to match: + +``` +1. ANALYZE → Extract design DNA from template: + - python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.get template.pdf (page size) + - python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.image template.pdf (color samples) + - python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.text template.pdf (text structure) + - pdftoppm -png -r 150 template.pdf preview (visual reference) +2. DOCUMENT → Record: page size, margins, colors, fonts, layout grid, + header/footer pattern, decorative elements +3. DELEGATE → Route to brief WITH design constraints (not brief defaults) +4. BUILD → Follow brief workflow, constrained to template DNA +5. COMPARE → pdftoppm both, visually compare side-by-side +``` + +**Key principles:** +- Match the spirit, not the pixels — exact replication from PDF is impractical +- Prefer original source files (.docx/.html/.tex) over PDF when available +- Declare font substitutions upfront; don't silently fall back +- Template provides design direction, not content — never leak placeholder text + +--- + +## §Convert + +### Office → PDF (LibreOffice) + +**Simple conversion** (no TOC needed): +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.office input.docx -o output.pdf +``` + +**When to use the 5-step DOCX Pipeline instead**: If the DOCX has (or should have) a Table of Contents, always use §DOCX Pipeline below. Signs: the document has 3+ headings, or the user mentions "table of contents" / "TOC", or the document already contains a TOC section. When in doubt, run `python3 "$PDF_SKILL_DIR/scripts/toc_validate.py" fix-docx input.docx -o fixed.docx` — if it returns `no_toc_needed`, a simple conversion is fine. + +Or directly: +```bash +soffice --headless --convert-to pdf --outdir ./output input.docx +``` + +**Supported**: `.docx`, `.doc`, `.odt`, `.rtf`, `.pptx`, `.ppt`, `.xlsx`, `.xls`, `.ods`, `.csv`, `.html` + +**macOS path**: `/Applications/LibreOffice.app/Contents/MacOS/soffice` + +**Gotchas:** +- soffice allows only one instance at a time; close existing LibreOffice windows or use `--env:UserInstallation=file:///tmp/libreoffice_tmp` +- Missing Chinese fonts → squares. Ensure SimHei/SimSun are installed. +- Large files (>50MB) may take 1-2 min; set reasonable timeout +- soffice HTML→PDF is inferior to Playwright for complex CSS + +**Priority**: Always prefer soffice for Office→PDF (preserves themes, layouts, master slides). Only fall back to python-pptx/python-docx + HTML + Playwright if soffice is unavailable — fidelity will be lower. + +### Fallback: Spreadsheet → PDF without LibreOffice + +Use openpyxl + HTML + Playwright. Let data shape drive layout: + +| Factor | Decision | +|--------|----------| +| Columns ≤ 6 | Portrait | +| Columns > 6 | Landscape | +| Font size | Scale inversely with column count | +| Styling | Follow user requirements or source file style; if unspecified, use defaults from `typesetting/palette.md` | + +### §DOCX Pipeline (5-Step with TOC) + +For DOCX files that need TOC generation/correction. Required because LibreOffice `--headless` does not recalculate PAGEREF fields. + +``` +Step 1: soffice → Convert original DOCX to PDF (pass1) +Step 2: pages.clean → Remove blank pages from pass1 +Step 3: fix-docx → Add/fix TOC with HYPERLINK + PAGEREF + bookmarks +Step 4: fix-pages → Correct TOC page numbers using pass1 as reference +Step 5: soffice → Convert final DOCX to PDF + pages.clean +``` + +**Step 1 — Pass 1 Convert**: +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.office input.docx -o pass1.pdf +``` + +**Step 2 — Clean Blank Pages**: +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.clean pass1.pdf -o pass1_clean.pdf +``` +If `blank_pages_removed == 0`, use pass1.pdf directly. + +**Step 3 — Fix TOC**: +```bash +python3 "$PDF_SKILL_DIR/scripts/toc_validate.py" fix-docx input.docx -o fixed.docx +``` + +Auto-detects and fixes: placeholder TOC, stale TOC (>50% drift), empty TOC, missing TOC (≥3 headings). Each entry gets `<w:hyperlink>` + `PAGEREF` + bookmarks for clickable PDF navigation. + +Check output `action` field: `fixed` → use fixed.docx, `skipped` → use original, `no_toc_needed` → skip to Step 5 with pass1 PDF. + +**Step 4 — Fix Page Numbers**: +```bash +python3 "$PDF_SKILL_DIR/scripts/toc_validate.py" fix-pages fixed.docx pass1_clean.pdf -o final.docx +``` + +Corrects PAGEREF display text using actual page positions from pass1 + TOC page offset. + +**Step 5 — Final Convert + Clean**: +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.office final.docx -o output.pdf +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.clean output.pdf -o output_clean.pdf +``` + +### Post-Conversion Validation (Optional) + +```bash +python3 "$PDF_SKILL_DIR/scripts/toc_validate.py" check-conversion final.docx output_clean.pdf +``` + +Issues caught: `CONV_TOC_LOST` (TOC disappeared), `CONV_HINT_LEAKED` (placeholder text in PDF), `CONV_HEADING_DRIFT` (heading count mismatch). + +--- + +## Caveats + +| Topic | Detail | +|-------|--------| +| Encrypted PDFs | Not supported. User must decrypt externally first. | +| < 50 MB | Instant | +| 50–200 MB | 1–2 minutes | +| > 200 MB | Split first, or extend timeout | +| Memory | ~2-3× input file size | +| Merge failure | Partial output may remain; delete and retry | +| Split failure | Some page files may exist; inspect output dir | +| Form fill | Original never modified; always writes new file | + +For edge cases (OCR, batch processing, poppler-utils, qpdf, performance tuning), load `briefs/process-advanced.md`. diff --git a/skills/pdf/briefs/report.md b/skills/pdf/briefs/report.md new file mode 100755 index 0000000..73c38c0 --- /dev/null +++ b/skills/pdf/briefs/report.md @@ -0,0 +1,1659 @@ +# Brief: Report Production + +Structured documents via ReportLab: reports, proposals, contracts, white papers, financial analysis, data tables. Also covers ATS-friendly resumes. + +--- + +> ### 🚨 EMOJI CHECK - STOP HERE IF CONTENT HAS EMOJI +> +> ReportLab renders emoji (📊🎯🔥💡 etc.) as **□ tofu squares**. This is unfixable. +> +> **If the user's content contains intentional emoji** → **STOP. Do NOT use this brief.** +> Route to `briefs/creative.md` instead (Playwright renders emoji natively). +> +> This applies even if the document is a "report" - emoji + ReportLab = broken output. + +--- + +## Production Workflow + +``` +1. BRIEF → Confirm document type, audience, page count, outline +2. DESIGN → Run `pdf.py palette.generate --title "..."` → copy-paste output into script +3. EDIT → Content transformation (extract data, define typographic roles) +4. COVER → Read typesetting/cover.md → render HTML cover via Playwright (default ON for reports ≥ 3 pages; skip only for resumes/letters/memos/forms) +5. BUILD → Write ReportLab code (fonts → styles → content → tables → charts) +6. PREFLIGHT → code.sanitize → execute → meta.brand → font.check → toc.check → pages.clean → pdf_qa.py +7. DELIVER → Merge cover + body → final PDF +``` + +**Typesetting assets** (load when you reach that step): +- `typesetting/palette.md` - color system, typography rules, anti-patterns +- `typesetting/cover.md` - 7 cover layouts with variants, typography scale, bounding box rules +- `typesetting/charts.md` - chart styling, anti-stacking rules, axis/grid/legend treatment + +**Cover is DEFAULT ON for reports.** Skip cover only for: resumes, letters, memos, forms, checklists, or documents ≤ 2 pages. + +--- + +## Step 2: DESIGN - Palette & Font Plan + +**Before writing any ReportLab code, you MUST generate the color palette via the `palette.generate` command.** This is not optional. Do NOT hardcode hex colors. Do NOT pick colors by feel. + +### 2a. Generate Palette (MANDATORY) + +**Run this command FIRST. Copy-paste its output directly into your Python script:** + +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" palette.generate --title "<document title>" --mode minimal +``` + +The command auto-derives the design intent from the document title, computes a mathematically harmonious palette, and outputs ready-to-paste ReportLab Python code: + +```python +# ━━ Color Palette (auto-generated by pdf.py palette.generate) ━━ +# Intent: neutral | Mode: minimal | Harmony: split_complementary +# Contrast: text:bg=14.12 | accent:bg=3.06 +from reportlab.lib import colors +ACCENT = colors.HexColor('#2f97b9') +TEXT_PRIMARY = colors.HexColor('#252422') +TEXT_MUTED = colors.HexColor('#8d8981') +BG_SURFACE = colors.HexColor('#dfdad2') +BG_PAGE = colors.HexColor('#f5f4f3') +SURFACE_RGBA = 'rgba(0,0,0,0.03)' + +# ReportLab table colors +TABLE_HEADER_COLOR = ACCENT +TABLE_HEADER_TEXT = colors.white +TABLE_ROW_EVEN = colors.white +TABLE_ROW_ODD = BG_SURFACE +``` + +**Options:** +- `--mode minimal` (default, recommended for 50%+ of documents) | `dark` | `pastel` | `jewel` | `light` +- `--harmony auto` (default, recommended) | `complementary` | `split_complementary` | `analogous` | `triadic` | `monochrome` +- `--format python` (default) | `json` | `css` +- `--seed <int>` - for reproducible palettes across regenerations + +**⚠️ FORBIDDEN:** +- ❌ Writing `colors.HexColor('#xxxxxx')` with any hex value you chose yourself +- ❌ Using `colors.red`, `colors.blue`, or any ReportLab named color for design elements +- ❌ Skipping this step and picking colors "that look good" +- ❌ Using different palettes for different sections of the same document + +**✅ The ONLY acceptable way to get colors:** Run `palette.generate`, copy the output. + +### 2b. Color Application Rules + +| Element | Color Source | Notes | +|---------|-------------|-------| +| Table headers | ACCENT (bg) + white (text) | High contrast required | +| Table odd rows | BG_SURFACE at 50% opacity | Subtle striping | +| Section titles | ACCENT or TEXT_PRIMARY | Match heading hierarchy | +| Body text | TEXT_PRIMARY | Never use pure #000000 | +| Muted/meta text | TEXT_MUTED | Dates, captions, footnotes | +| Horizontal rules | ACCENT at 30% opacity | Thin, unobtrusive | +| Chart colors | Derive from ACCENT (see charts.md) | Consistent with palette | + +### 2c. Forbidden + +- ❌ Hardcoding hex colors like `#1F4E79`, `#555555`, `#888888` in code +- ❌ Using `colors.red`, `colors.blue` or any ReportLab named color for design elements +- ❌ Choosing colors by "feel" without running palette first +- ❌ Using different accent colors across tables/sections in the same document + +> **Exception**: Semantic colors for data visualization (green for positive, red for negative) are acceptable but should be muted variants derived from the palette when possible. + +--- + +## Step 3: EDIT - Content Transformation + +**Before writing any code, restructure raw text into visual roles.** This is the most impactful quality step - skipping it produces "Word document with a PDF extension". + +### 3a. Typographic Role Extraction + +Parse raw text and categorize content into visual roles: + +| Role | ReportLab Mapping | Description | +|------|------------------|-------------| +| **Hero / Display** | Cover title (36-42pt) or section opener | 1-5 words, the emotional hook | +| **Kicker / Eyebrow** | Subtitle or section intro (10-12pt, muted) | Tiny context line above main heading | +| **Data Sculpture** | CalloutBox or bold stat block | Extract impactful numbers (97%, $4.2M, -45ms) | +| **Pull Quote** | BlockQuote (italic + left indent 24pt + accent border) | Most provocative sentence, standalone | +| **Body** | Standard paragraph | Everything else | + +**Rule**: Before writing any body paragraph, scan for numbers with units. Every metric like "revenue grew by 12%" or "latency dropped to 45ms" MUST be extracted into a CalloutBox or metrics table - never buried in paragraph prose. See §Data-to-Ink Ratio Rules below. + +### 3b. Section Pacing + +- If any H1 section has **>400 words** of continuous body text without visual breaks, split into H2 subsections or insert a visual break (table, chart, callout) +- Aim for at least 1 visual element (table/chart/callout) per 2-3 pages of body text +- Dense text walls are the #1 sign of poor report design + +--- + +--- + +## Character Safety Rule (MANDATORY) + +Three rules for safe character handling in ReportLab PDFs: + +**a) Superscripts and subscripts**: Use `<super>`/`<sub>` tags, never raw Unicode superscript/subscript characters (e.g., `\u00b2`, `\u2082`). + +**b) Emoji**: ReportLab cannot render emoji. If content contains emoji, use Creative brief (HTML + Playwright). + +**c) Font fallback for mixed CJK/Latin text**: After registering fonts, call `install_font_fallback()` once. This automatically wraps missing-glyph characters in `<font>` tags inside every `Paragraph()`. No manual `<font name="...">` wrapping needed for mixed Chinese-English text. + +Mathematical/relational operators (×, ÷, ±, ≤, √, ∑, ≅, ∫, π, ∠, Δ, etc.) are safe to use as literal characters in `Paragraph()` - both SimHei and Times New Roman have these glyphs. + +| Need | Correct Method | Correct Example | +|------|---------------|---------| +| Superscript | `<super>` tag in `Paragraph()` | `Paragraph('10<super>2</super> × 10<super>3</super> = 10<super>5</super>', style)` | +| Subscript | `<sub>` tag in `Paragraph()` | `Paragraph('H<sub>2</sub>O', style)` | +| Bold | `<b>` tag in `Paragraph()` | `Paragraph('<b>Title</b>', style)` | +| Math operators | Literal char in `Paragraph()` | `Paragraph('AB ⊥ AC, ∠A = 90°, and ΔABC ≅ ΔDCF', style)` | +| Scientific notation | Combined tags in `Paragraph()` | `Paragraph('1.2 × 10<super>8</super> kg/m<super>3</super>', style)` | + +```python +from reportlab.platypus import Paragraph +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_JUSTIFY + +body_style = ParagraphStyle( + name="ENBodyStyle", + fontName="Times New Roman", + fontSize=10.5, + leading=18, + alignment=TA_JUSTIFY, +) + +# Superscript: area unit +Paragraph('Total area: 500 m<super>2</super>', body_style) + +# Subscript: chemical formula +Paragraph('The reaction produces CO<sub>2</sub> and H<sub>2</sub>O', body_style) + +# Scientific notation +Paragraph('Speed of light: 3.0 × 10<super>8</super> m/s', body_style) + +# Combined +Paragraph('E<sub>k</sub> = mv<super>2</super>/2', body_style) + +# Bold heading +Paragraph('<b>Chapter 1: Introduction</b>', header_style) + +# Math symbols +Paragraph('When ∠ A = 90°, AB ⊥ AC and ΔABC ≅ ΔDEF', body_style) +``` + +**Pre-generation check - before writing ANY string, ask:** +> "Does this string contain a character outside basic CJK or Mathematical/relational operators?" +> If YES → it MUST be inside a `Paragraph()` with the appropriate tag. +> If it is a superscript/subscript digit in raw unicode escape form → REPLACE with `<super>`/`<sub>` tag. + +**NEVER rely on post-generation scanning. Prevent at the point of writing.** + +**Encoding safety - before writing ANY content text:** +> "Does this string contain Japanese kana (の, が, は etc.) or rare Unicode symbols?" +> If YES → REPLACE with safe plain Chinese equivalents. Japanese kana (Hiragana U+3040-U+309F, Katakana U+30A0-U+30FF) frequently corrupt to U+FFFD (�) when code passes through LLM output, heredoc, or terminal encoding layers. +> Common safe replacements: `活の真鲷`→`活缔真鲷`, `盐烤の鲭鱼`→`盐烤鲭鱼`, `烤の鸡串`→`炭烤鸡串`. +> If the character is genuinely needed, verify it survives a full write→read round-trip with `open(file, encoding='utf-8')`. + +--- + +## Font Setup (Guaranteed Success Method) + +### CRITICAL: Allowed Fonts Only +**You MUST ONLY use the following registered fonts. Using ANY other font is STRICTLY FORBIDDEN.** + +| Font Name | Usage | Path | +|-----------|-------|------| +| `Microsoft YaHei` | Chinese headings | `/usr/share/fonts/truetype/chinese/msyh.ttf` | +| `SimHei` | Chinese body text | `/usr/share/fonts/truetype/chinese/SimHei.ttf` | +| `SarasaMonoSC` | Chinese code blocks | `/usr/share/fonts/truetype/chinese/SarasaMonoSC-Regular.ttf` | +| `Times New Roman` | English text, numbers, tables | `/usr/share/fonts/truetype/english/Times-New-Roman.ttf` | +| `Calibri` | English alternative | `/usr/share/fonts/truetype/english/calibri-regular.ttf` | +| `DejaVuSans` | Formulas, symbols, code | `/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf` | + +**FORBIDDEN fonts (DO NOT USE):** +- ❌ Arial, Arial-Bold, Arial-Italic +- ❌ Helvetica, Helvetica-Bold, Helvetica-Oblique +- ❌ Courier, Courier-Bold +- ❌ Any font not listed in the table above + +### Font Registration Template +```python +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.pdfbase.pdfmetrics import registerFontFamily + +# Chinese fonts +pdfmetrics.registerFont(TTFont('Microsoft YaHei', '/usr/share/fonts/truetype/chinese/msyh.ttf')) +pdfmetrics.registerFont(TTFont('SimHei', '/usr/share/fonts/truetype/chinese/SimHei.ttf')) +pdfmetrics.registerFont(TTFont("SarasaMonoSC", '/usr/share/fonts/truetype/chinese/SarasaMonoSC-Regular.ttf')) + +# English fonts +pdfmetrics.registerFont(TTFont('Times New Roman', '/usr/share/fonts/truetype/english/Times-New-Roman.ttf')) +pdfmetrics.registerFont(TTFont('Calibri', '/usr/share/fonts/truetype/english/calibri-regular.ttf')) + +# Symbol/Formula font +pdfmetrics.registerFont(TTFont("DejaVuSans", '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf')) + +# CRITICAL: Register font families to enable <b>, <super>, <sub> tags +registerFontFamily('Microsoft YaHei', normal='Microsoft YaHei', bold='Microsoft YaHei') +registerFontFamily('SimHei', normal='SimHei', bold='SimHei') +registerFontFamily('Times New Roman', normal='Times New Roman', bold='Times New Roman') +registerFontFamily('Calibri', normal='Calibri', bold='Calibri') +registerFontFamily('DejaVuSans', normal='DejaVuSans', bold='DejaVuSans') +``` + +### Font Configuration by Document Type + +**For Chinese PDFs:** +- Body text: `SimHei` or `Microsoft YaHei` +- Headings: `Microsoft YaHei` (MUST use for Chinese headings) +- Code blocks: `SarasaMonoSC` +- Formulas/symbols: `DejaVuSans` +- **In tables: ALL Chinese content and numbers MUST use `SimHei`** + +**For English PDFs:** +- Body text: `Times New Roman` +- Headings: `Times New Roman` (MUST use for English headings) +- Code blocks: `DejaVuSans` +- **In tables: ALL English content and numbers MUST use `Times New Roman`** + +**For Mixed Chinese-English PDFs:** +- Call `install_font_fallback()` once after registering fonts - it automatically wraps characters in `<font>` tags so you don't need to do it manually. +- If you still want manual control, you can use `<font name='...'>` tags, but the automatic fallback handles most cases. + +```python +import sys, os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'scripts')) +from pdf import install_font_fallback + +# 1. Register fonts (as above) +# 2. Install fallback - one line, handles all mixed-language text +install_font_fallback() + +# 3. Just write naturally - no manual <font> wrapping needed! +en_style = ParagraphStyle(name="EN", fontName="Times New Roman", fontSize=10.5, leading=18) +cn_style = ParagraphStyle(name="CN", fontName="SimHei", fontSize=10.5, leading=18) + +Paragraph('MySQL、PostgreSQL、Redis', en_style) # ✅ CJK comma auto-fallback to SimHei +Paragraph('Beijing (北京) has 21M people', en_style) # ✅ CJK chars auto-fallback +Paragraph('价格:¥2,200~5,800/月', en_style) # ✅ all CJK chars handled +Paragraph('《巴黎协定》签署于2015年', en_style) # ✅ CJK book title marks handled +``` + +**How it works:** `install_font_fallback()` monkey-patches `Paragraph.__init__` to scan each character against the font's `charToGlyph` table. Characters missing from the base font are automatically wrapped in `<font name="FallbackFont">`. The fallback chain is: English fonts → SimHei, Chinese fonts → Times New Roman. For aesthetic optimization, Cyrillic text in SimHei is automatically routed to Times New Roman (serif looks better for Cyrillic). + +### Chinese Plot PNG Method +```python +import matplotlib.pyplot as plt +plt.rcParams['font.sans-serif'] = ['SimHei'] +plt.rcParams['axes.unicode_minus'] = False +``` + +### Available Font Paths +Run `fc-list` to get more fonts. Font files are typically located under: +- `/usr/share/fonts/truetype/chinese/` +- `/usr/share/fonts/truetype/english/` +- `/usr/share/fonts/` + +--- + +## Layout & Spacing Control + +### Page Breaks + +Follow the document type strategy defined in SOUL.md Rule 1. + +**Structural breaks (always - MANDATORY):** +- Between cover page and TOC - **cover must NEVER share a page with anything else** +- Between TOC and main content +- Between main content and back cover + +**Content breaks (by document type):** +- **Default (all document types)**: ❗ **Absolutely NEVER force page breaks before H1/H2** — content flows naturally, do not insert `PageBreak()` before section headings. This is an iron rule. +- Resume / contract / letter → No content page breaks +- Short article → No content page breaks +- **Exception 1**: Academic papers / textbooks / teaching plans → `PageBreak()` before each H1 is acceptable (only if user explicitly requests academic formatting) +- **Exception 2**: User explicitly requests page breaks between chapters + +**Anti-tear (mandatory — but with height limit):** +```python +from reportlab.platypus import KeepTogether +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import inch + +# Maximum height for KeepTogether blocks: 40% of page height +# Blocks taller than this should NOT be kept together — they cause +# the preceding page to be mostly empty when the block gets pushed down. +MAX_KEEP_HEIGHT = A4[1] * 0.4 # ~336pt ≈ 12cm + +def safe_keep_together(elements): + """Wrap elements in KeepTogether only if their total height is reasonable. + If too tall, keep only the FIRST TWO elements together (e.g. title + table/image header) + to prevent title-content separation, while letting the rest paginate naturally. + """ + total_h = 0 + for el in elements: + w, h = el.wrap(A4[0] - 2*inch, A4[1]) + total_h += h + if total_h <= MAX_KEEP_HEIGHT: + return [KeepTogether(elements)] + elif len(elements) >= 2: + # Keep at least title + first element together to prevent title orphaning + return [KeepTogether(elements[:2])] + list(elements[2:]) + else: + return list(elements) + +# Heading stays with first paragraph +story.extend(safe_keep_together([ + heading_paragraph, + first_body_paragraph, +])) + +# Image + caption: keep together ONLY if image is small +story.extend(safe_keep_together([image, caption_paragraph])) + +# Table title + table: keep together ONLY if table is short +if len(table_data) <= 15: + story.extend(safe_keep_together([table_title, table])) +else: + # Long table: just keep title with the table start + story.extend(safe_keep_together([table_title])) + story.append(table) +``` + +**⚠️ KEY INSIGHT:** `KeepTogether` is the #1 cause of “empty page with chart on next page”. +When a `KeepTogether` block is taller than the remaining space on the current page, +ReportLab pushes the **entire block** to the next page, leaving the current page mostly empty. +**Always use `safe_keep_together()` to cap the maximum block height.** + +**⚠️ Note:** Do NOT use `KeepWithNext` - it is unreliable in ReportLab 4.x. + +### Vertical Spacing Standards +* Before tables: `Spacer(1, 18)` after preceding text +* After tables: `Spacer(1, 6)` before table caption +* After table captions: `Spacer(1, 18)` before next content +* Between paragraphs: `Spacer(1, 12)` (~1 line) +* Between H3 subsections: `Spacer(1, 12)` +* Between H2 sections: `Spacer(1, 18)` (~1.5 lines) +* Between H1 sections: `Spacer(1, 24)` (~2 lines) +* NEVER use `Spacer(1, X)` where X > 24, except for H1 major breaks or cover page elements + +### Cover Page: HTML/Playwright Unified Cover System + +**⚠️ Report route covers are ALWAYS generated via HTML/Playwright**, using the same 7-template system defined in `typesetting/cover.md`. This ensures visual consistency across all routes (Report, Creative, Academic) and avoids the limitations of ReportLab for complex cover layouts. + +**Pipeline (Report route):** +1. Generate **body PDF** via ReportLab (start with TOC or content - **no cover in story[]**) +2. Generate **cover HTML** following `typesetting/cover.md` 7-template system +3. **Run `poster_validate.py check-html` on cover HTML** — fix any ERRORs before rendering (overflow:hidden, font fallback, etc.) +4. **Run `cover_validate.js` on cover HTML** — detects text-vs-decorative-line overlaps. Non-zero exit = must fix before proceeding. + ```bash + node "$PDF_SKILL_DIR/scripts/cover_validate.js" cover.html + ``` +5. Render cover HTML → single-page PDF via Playwright (`html2poster.js`) — **NOT `html2pdf-next.js`** (which converts absolute→static and destroys cover layout) +6. **Merge: insert cover as page 0** of body PDF using pypdf → output single final PDF + +> **Why not ReportLab covers?** ReportLab is excellent for structured content (tables, paragraphs, flowables) but painful for visual design (geometric accents, precise absolute positioning, web fonts). HTML/CSS handles these natively. One cover system, one visual standard, zero inconsistency. + +**⚠️ Cover page is DEFAULT ON for all Report-route documents ≥ 3 pages.** + +The Report route handles formal, structured documents. Readers expect a professional cover. **Always generate a cover unless the document type is explicitly excluded below.** + +**ALWAYS add a cover page (default behavior):** +- Research reports, experiment reports, lab reports, analysis reports +- White papers, industry analysis, market research +- Business proposals, project plans, feasibility studies +- Technical documentation, product manuals, user guides +- Annual reports, financial summaries, quarterly reviews +- Any formal document ≥ 3 pages that will be shared externally or submitted + +**NEVER add a cover page (explicit exclusions only):** +- **Resumes / CVs** - recruiters want content immediately +- **Letters / memos** - single-page or short-form +- **Forms / checklists / invoices** - functional documents +- **Internal notes / meeting minutes** - brevity over presentation +- **Documents ≤ 2 pages** - cover would be disproportionate +- **User explicitly said "no cover"** + +**Rule of thumb:** If it's a report, analysis, proposal, or any formal document ≥ 3 pages → **add a cover**. When in doubt, add the cover. It's easier to remove a cover the user didn't want than to miss one they expected. + +#### Step 1: Generate Body PDF (ReportLab, no cover) + +Build the ReportLab document starting directly with TOC or first section. **Do NOT include any cover page in the `story[]` list.** + +```python +# Body PDF - starts with TOC or first section, NO cover +story = [] +# story.append(toc) # if applicable +# story.append(PageBreak()) +# story.append(first_section_content) +# ... +doc.build(story) +# Output: body.pdf (no cover) +``` + +#### Step 2: Generate Cover PDF via html2poster.js + +```bash +# ALWAYS use html2poster.js for cover rendering (NOT html2pdf-next.js) +# Cover pages use position:absolute layout — html2pdf-next.js pre-render hooks +# convert absolute→static and destroy the layout. html2poster.js preserves it. +node "$PDF_SKILL_DIR/scripts/html2poster.js" cover.html --output cover.pdf --width 794px +``` + +Or from Python: +```python +import subprocess, os + +def render_cover(html_path, pdf_path): + """Render HTML cover to PDF via html2poster.js. + + ⚠️ ALWAYS use html2poster.js for covers (NOT html2pdf-next.js). + Cover HTML uses position:absolute for layout. html2pdf-next.js pre-render + hooks convert absolute→static to prevent multi-page overlap, which + destroys cover layouts. html2poster.js preserves absolute positioning. + """ + scripts_dir = os.path.expanduser('~/.openclaw/workspace/skills/pdf/scripts') + subprocess.run([ + 'node', os.path.join(scripts_dir, 'html2poster.js'), + html_path, '--output', pdf_path, + '--width', '794px', + ], check=True) +``` + +**Insertion script (single output PDF):** +```python +from pypdf import PdfReader, PdfWriter, Transformation + +A4_W, A4_H = 595.28, 841.89 # A4 in points + +def normalize_page_to_a4(page): + """Scale a page to A4 if its dimensions don't match.""" + box = page.mediabox + w, h = float(box.width), float(box.height) + if abs(w - A4_W) > 2 or abs(h - A4_H) > 2: + sx, sy = A4_W / w, A4_H / h + page.add_transformation(Transformation().scale(sx=sx, sy=sy)) + page.mediabox.lower_left = (0, 0) + page.mediabox.upper_right = (A4_W, A4_H) + return page + +def insert_cover(cover_pdf, body_pdf, output_pdf): + """Insert cover as first page of body PDF → single output file.""" + writer = PdfWriter() + # Cover as page 1 + cover_page = PdfReader(cover_pdf).pages[0] + writer.add_page(normalize_page_to_a4(cover_page)) + # Body pages follow + for page in PdfReader(body_pdf).pages: + writer.add_page(normalize_page_to_a4(page)) + writer.add_metadata({'/Title': 'Report Title', '/Author': 'Z.ai', '/Creator': 'Z.ai'}) + with open(output_pdf, 'wb') as f: + writer.write(f) +``` + +> **⚠️ Size pitfall:** Playwright `page.pdf(width='794px', height='1123px')` output PDF dimensions may differ from A4 (595.28×841.89pt) by 1-2pt. **Do not use PIL Image.save('x.pdf')** for PNG→PDF conversion — DPI mapping causes severe size errors. You must call `normalize_page_to_a4()` to unify dimensions before insertion. + +**Cover HTML template reference**: See `typesetting/cover.md` for the complete 7-Layout System with variants, typography scale, and bounding box rules. + +**→ Full cover spec: `typesetting/cover.md`** - read it before designing any cover. + +**Cover design rules (summary - see cover.md for details):** +- Page size: `width: 794px; height: 1123px` (A4 at 96dpi) +- `body { margin: 0; padding: 0; overflow: hidden; }` - REQUIRED to avoid white borders +- Load Google Fonts via `<link href="..." rel="stylesheet">` in `<head>` (NOT `@import url(...)` in CSS — `@import` must be first rule or is silently ignored) — Playwright fetches them at render time +- **Background must be white or very light** (`#fff` / `#fafafa` / `#f5f8fb`), no dark solid backgrounds or gradient backgrounds +- Primary color used only for text, lines, geometric accents - never as large-area background +- Sophistication = whitespace + typography + restrained geometric accents + +**Cover Constitution (7-Layout System):** +- **Pick a layout (1-7)** from `typesetting/cover.md` that matches the document tone. No global default - every selection must be a deliberate design decision. Layout 7 for government/bidding/proposal documents. +- **Maximum 4 components** on any cover. Typical recipe: Title + subtitle + 1 geometric accent + metadata. +- **Typography Scale**: Title ≈ 45pt, Subtitle ≈ 25pt, Meta ≥ 18pt (never below 14pt). Tiny text = FAIL. +- **Background layer (optional)**: See `typesetting/cover-backgrounds.md` for 3 recipes (A=极简弧线, B=工程十字轴+立柱, C=锐角切割+出血文字). Background renders BELOW all foreground content at 2-5% opacity. +- **Mandatory `<br>` chunking**: Title MUST break every 2-4 words (CJK) or 3-5 words (English). Single-line title = FAIL. +- **Bounding Box spatial dispersion**: Group elements into 2-3 bounding boxes at opposite regions (e.g., top-left + bottom-right). Never cluster everything into middle 40%. +- **Safe Zone**: 12% top/bottom, 14% left/right padding on cover pages. +- **No body text on cover**: Cover = title + subtitle + date + optional geometric decoration. All content goes to page 2+. + +### ⚠️ Cover Page Isolation Rule (MANDATORY) + +**The cover page must ALWAYS be on its own page.** Cover content and subsequent content (TOC, body text, etc.) must NEVER appear on the same page. This is a hard rule. + +- **Report route (HTML/Playwright cover)**: The cover is a separate rendered page inserted as page 0 via pypdf - isolation is inherent in the merge pipeline +- **Creative route**: Cover is part of the same HTML document but on its own `<section>` with page-break - isolation is handled by CSS + +If a generated PDF shows cover + TOC on the same page, it is a **critical bug** - regenerate immediately. + +### Alignment and Typography +- **CJK body**: Use `TA_LEFT` + 2-char indent. Headings: no indent. +- **Font sizes**: Body 11pt, subheadings 14pt, headings 18-20pt +- **Line height**: 1.5-1.6 (leading at **1.4x font size minimum**, recommended 1.5x for CJK) + - CJK characters are taller than Latin - 1.2x causes cramped lines + - Quick reference: 10pt→14pt min/18pt rec; 14pt→20pt min/22pt rec; 36pt→50pt min/54pt rec +- **⛔ Prohibited: fixed `rowHeights` in Table()**. Use `TOPPADDING` / `BOTTOMPADDING` to control row spacing. Fixed rowHeights cause content overflow clipping - the text renders but gets invisibly cut off. +- **CRITICAL: Alignment Selection Rule**: + - Use `TA_JUSTIFY` only when ALL of: + * Text is predominantly English (≥ 90%) + * Column width is sufficiently wide (A4 single-column body) + * Font: Western fonts (Times New Roman / Calibri) + * Chinese content: None or negligible + - Otherwise, always default to `TA_LEFT` + - For Chinese text, always add `wordWrap='CJK'` to ParagraphStyle + +### Style Configuration +* Normal paragraph: `spaceBefore=0`, `spaceAfter=6-12` +* Headings: `spaceBefore=12-18`, `spaceAfter=6-12` +* **Headings must be bold**: Use `<b></b>` tags in Paragraph +* Table captions: `spaceBefore=3`, `spaceAfter=6`, `alignment=TA_CENTER` +* **CRITICAL**: For Chinese text, always add `wordWrap='CJK'` to ParagraphStyle + +--- + +## Table Formatting + +### Standard Table Color Scheme (MUST USE for ALL tables) + +> **Colors MUST come from the palette generated in Step 2.** The values below are examples - replace them with your actual palette output. + +```python +# Colors from design_engine.py palette (Step 2) - NEVER hardcode these +TABLE_HEADER_COLOR = ACCENT # From palette --c-accent +TABLE_HEADER_TEXT = colors.white # White text for header (fixed) +TABLE_ROW_EVEN = colors.white # White for even rows +TABLE_ROW_ODD = BG_SURFACE # From palette --c-mid (light stripe) +``` + +### Table Rules +- Caption must be centered, added immediately after the table +- Entire table must be centered on the page +- **Header Row**: Dark background (from palette `header_fill`), white bold text +- **Cell Margins**: Left/Right at least 120-200 twips +- **Alignment**: Each body element within the same table must be aligned the same method +- **Color consistency**: All tables in one PDF must use the same color scheme +- **Spacing**: `Spacer(1, 18)` → Table → `Spacer(1, 6)` → Caption → `Spacer(1, 18)` + +### Table Centering Enforcement (MANDATORY) + +**Every table MUST be horizontally centered on the page. No exceptions.** + +```python +# ReportLab: ALWAYS set hAlign='CENTER' +table = Table(data, colWidths=col_widths, hAlign='CENTER') + +# Width rule: table total width should be 85-100% of available content width +available_width = doc.width # or page_width - left_margin - right_margin +table_width = sum(col_widths) +if table_width < available_width * 0.85: + # Scale up columns proportionally to fill space + scale = (available_width * 0.90) / table_width + col_widths = [w * scale for w in col_widths] + +# ❌ WRONG — no hAlign (defaults to LEFT, table drifts left) +table = Table(data, colWidths=[100, 200, 150]) + +# ✅ RIGHT — explicit centering +table = Table(data, colWidths=[100, 200, 150], hAlign='CENTER') +``` + +**LaTeX equivalent:** +```latex +\begin{table}[htbp] + \centering % MANDATORY + \begin{tabular}{...} + ... + \end{tabular} +\end{table} +``` + +### Table Cell Content Rule (MANDATORY - NON-NEGOTIABLE) + +**ALL text content in table cells MUST be wrapped in `Paragraph()`. NO EXCEPTIONS.** + +❌ **PROHIBITED** - Plain strings: +```python +data = [ + ['<b>Header</b>', 'Value'], # Bold won't render! + ['Pressure', '1.01 × 10<super>5</super>'], # Superscript won't work! +] +``` + +✅ **REQUIRED** - All text in Paragraph: +```python +data = [ + [Paragraph('<b>Header</b>', header_style), Paragraph('Value', header_style)], + [Paragraph('Pressure', cell_style), Paragraph('1.01 × 10<super>5</super>', cell_style)], +] +``` + +**The ONLY exception**: `Image()` objects can be placed directly in table cells. + +### Units with Exponents (CRITICAL) +- PROHIBITED: `W/m2`, `kg/m3`, `m/s2` (plain text exponents) +- RIGHT: `Paragraph('W/m<super>2</super>', style)`, `Paragraph('kg/m<super>3</super>', style)` + +### Numeric Values in Tables (CRITICAL) +- Large numbers MUST use scientific notation: `Paragraph('-1.246 × 10<super>8</super>', style)` not `-124600000` +- Small decimals MUST use scientific notation: `Paragraph('2.5 × 10<super>-3</super>', style)` not `0.0025` +- Threshold: Use scientific notation when |value| ≥ 10000 or |value| ≤ 0.001 + +### Complete Table Example +```python +from reportlab.platypus import Table, TableStyle, Paragraph, Image +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY + +header_style = ParagraphStyle( + name='TableHeader', fontName='Times New Roman', fontSize=11, + textColor=colors.white, alignment=TA_CENTER +) +cell_style = ParagraphStyle( + name='TableCell', fontName='Times New Roman', fontSize=10, + textColor=colors.black, alignment=TA_CENTER +) + +data = [ + [Paragraph('<b>Parameter</b>', header_style), + Paragraph('<b>Unit</b>', header_style), + Paragraph('<b>Value</b>', header_style)], + [Paragraph('Temperature', cell_style), + Paragraph('°C', cell_style), + Paragraph('25.5', cell_style)], + [Paragraph('Pressure', cell_style), + Paragraph('Pa', cell_style), + Paragraph('1.01 × 10<super>5</super>', cell_style)], + [Paragraph('Density', cell_style), + Paragraph('kg/m<super>3</super>', cell_style), + Paragraph('1.225', cell_style)], +] + +table = Table(data, colWidths=[120, 80, 100]) +# ⚠️ Above uses hardcoded widths - OK for this 3-col example (120+80+100=300 < available ~460pt). +# For real tables, ALWAYS calculate from available_width. See "Table Width Management" below. +table.setStyle(TableStyle([ + ('BACKGROUND', (0, 0), (-1, 0), ACCENT), # Header: palette accent + ('TEXTCOLOR', (0, 0), (-1, 0), colors.white), + ('BACKGROUND', (0, 1), (-1, 1), colors.white), + ('BACKGROUND', (0, 2), (-1, 2), BG_SURFACE), # Odd row: palette surface + ('BACKGROUND', (0, 3), (-1, 3), colors.white), + ('GRID', (0, 0), (-1, -1), 0.5, TEXT_MUTED), # Grid: palette muted + ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), + ('LEFTPADDING', (0, 0), (-1, -1), 8), + ('RIGHTPADDING', (0, 0), (-1, -1), 8), + ('TOPPADDING', (0, 0), (-1, -1), 6), + ('BOTTOMPADDING', (0, 0), (-1, -1), 6), +])) +``` + +### Table Width Management (⚠️ Prevent Table Overflow) + +**Most common table bug: columns overflow the right page margin because colWidths are hardcoded without checking available space.** + +**Iron Rule: The sum of all colWidths MUST NOT exceed `available_width`.** + +```python +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import inch + +page_width = A4[0] # 595.28pt +left_margin = 1.0 * inch # 72pt (adjust to your doc's actual margins) +right_margin = 1.0 * inch # 72pt +available_width = page_width - left_margin - right_margin # ≈ 451pt + +# ─── Method 1: Proportional widths (RECOMMENDED) ─── +# Define column ratios, auto-scale to fit available_width +col_ratios = [0.25, 0.40, 0.20, 0.15] # must sum to 1.0 +col_widths = [r * available_width for r in col_ratios] +table = Table(data, colWidths=col_widths) + +# ─── Method 2: Fixed + flex columns ─── +# Some columns have fixed width (numbers, dates), rest fills remaining space +fixed_cols = {0: 80, 3: 60} # col 0 = 80pt, col 3 = 60pt +fixed_total = sum(fixed_cols.values()) +flex_count = 4 - len(fixed_cols) # total columns minus fixed +flex_width = (available_width - fixed_total) / flex_count +col_widths = [] +for i in range(4): + col_widths.append(fixed_cols.get(i, flex_width)) +table = Table(data, colWidths=col_widths) + +# ─── Method 3: Let ReportLab auto-calculate (simplest, less control) ─── +# Pass colWidths=None - ReportLab auto-sizes based on content +# ⚠️ Risk: may still overflow if content is too wide +table = Table(data) # no colWidths +``` + +**Checklist before adding any table:** +1. Calculate `available_width` from your actual page size and margins +2. Sum all colWidths - must be ≤ `available_width` +3. For CJK text: characters are wider than Latin - budget ~12pt per CJK char at 10pt font +4. If a column has long text (descriptions, policies), use Paragraph() wrapping inside cells (not plain strings) - plain strings don't wrap and will overflow +5. Test with the longest expected content in each column + +**Common mistake - plain strings don't wrap:** +```python +# ❌ Plain string: will overflow if text is long +data = [['Policy Name', 'Very long description that keeps going and going']] +# ✅ Paragraph: wraps within column width +from reportlab.platypus import Paragraph +from reportlab.lib.styles import getSampleStyleSheet +styles = getSampleStyleSheet() +cell_style = styles['Normal'] +data = [[Paragraph('Policy Name', cell_style), + Paragraph('Very long description that keeps going and going', cell_style)]] +``` + +``` +Do you need auto-TOC? +├─ YES → Use TocDocTemplate + doc.multiBuild(story) +└─ NO → Use SimpleDocTemplate + doc.build(story) +``` + +| Requirement | DocTemplate | Build Method | +|-------------|-------------|--------------| +| Multi-page with TOC | `TocDocTemplate` | `multiBuild()` | +| Single-page or no TOC | `SimpleDocTemplate` | `build()` | +| With Cross-References (no TOC) | `SimpleDocTemplate` | `build()` | +| Both TOC + Cross-References | `TocDocTemplate` | `multiBuild()` | + +**⚠️ CRITICAL**: +- `multiBuild()` is ONLY needed when using `TableOfContents` +- Using `build()` with `TocDocTemplate` = TOC won't work +- Using `multiBuild()` without `TocDocTemplate` = unnecessary overhead + +--- + +## Rich Text Formatting + +### Prerequisites +To use `<b>`, `<super>`, `<sub>` tags, you **must**: +1. Register fonts via `registerFont()` +2. Call `registerFontFamily()` to link normal/bold/italic variants +3. Wrap all tagged text in `Paragraph()` objects + +**CRITICAL**: These tags ONLY work inside `Paragraph()` objects. Plain strings will NOT render them. + +### Preventing Unwanted Line Breaks + +```python +# Use non-breaking space to prevent breaking +text = Paragraph("Professors (K.G.\u00A0Palepu) proposed...", style) + +# Use wordWrap='CJK' for proper Chinese typography +styles.add(ParagraphStyle(name='BodyStyle', fontName='SimHei', fontSize=10.5, + leading=18, alignment=TA_LEFT, wordWrap='CJK')) + +# Use <br/> for intentional line breaks (NOT \n) +text = Paragraph("Line 1<br/>Line 2<br/>Line 3", style) +``` + +--- + +## Auto-Generated Table of Contents + +### ❌ FORBIDDEN: Manual Table of Contents +**NEVER manually create TOC with hardcoded page numbers.** + +### ⚠️ MANDATORY: TOC requires a cover page +**Unless the user explicitly requests no cover, if a document has a Table of Contents, it MUST have a cover page.** Structure: Cover (page 1) → TOC (page 2) → Content (page 3+). Do not generate a TOC without a preceding cover page. + +### ✅ ALWAYS use auto-generated TOC: + +```python +from reportlab.platypus import SimpleDocTemplate, Paragraph, PageBreak, Spacer +from reportlab.platypus.tableofcontents import TableOfContents +from reportlab.lib.utils import simpleSplit +import hashlib + +class TocDocTemplate(SimpleDocTemplate): + def afterFlowable(self, flowable): + if hasattr(flowable, 'bookmark_name'): + level = getattr(flowable, 'bookmark_level', 0) + text = getattr(flowable, 'bookmark_text', '') + key = getattr(flowable, 'bookmark_key', '') + # MUST pass key as 4th element - without it, TOC entries won't have clickable links + self.notify('TOCEntry', (level, text, self.page, key)) + +doc = TocDocTemplate("document.pdf", pagesize=letter) +story = [] + +toc = TableOfContents() +toc.levelStyles = [ + ParagraphStyle(name='TOCHeading1', fontSize=14, leftIndent=20, fontName='Times New Roman'), + ParagraphStyle(name='TOCHeading2', fontSize=12, leftIndent=40, fontName='Times New Roman'), +] +story.append(Paragraph("<b>Table of Contents</b>", styles['Title'])) +story.append(toc) +story.append(PageBreak()) + +def add_heading(text, style, level=0): + # Generate a unique bookmark key for this heading + key = 'h_%s' % hashlib.md5(text.encode()).hexdigest()[:8] + p = Paragraph('<a name="%s"/>%s' % (key, text), style) + p.bookmark_name = text + p.bookmark_level = level + p.bookmark_text = text + p.bookmark_key = key # This key enables clickable TOC links + return p + +# ⚠️ Orphan Heading Prevention (NOT a page break rule) +# If an H1 heading would appear in the bottom 15% of the page with no room +# for at least a few lines of content, push it to the next page. +# This is NOT "every H1 starts a new page" — it only triggers when the heading +# would be orphaned at the very bottom. +available_height = A4[1] - top_margin - bottom_margin +H1_ORPHAN_THRESHOLD = available_height * 0.15 # ~100pt ≈ 3.5cm — enough for heading + 2 lines + +def add_major_section(text, style): + """Add an H1-level heading with orphan prevention (NOT forced page break).""" + return [ + CondPageBreak(H1_ORPHAN_THRESHOLD), # Only break if heading would be orphaned at page bottom + add_heading(text, style, level=0), + ] + +story.extend(add_major_section("Chapter 1: Introduction", styles['Heading1'])) +# ... content ... + +doc.multiBuild(story) # MUST use multiBuild for TOC +``` + +**⚠️ CRITICAL TOC LINK RULES:** +1. `afterFlowable` MUST pass `key` as the 4th element in the notify tuple - without it, TOC entries have no clickable links +2. `add_heading` MUST set `bookmark_key` AND embed `<a name="key"/>` in the Paragraph text - this creates the link destination +3. The key must be unique per heading - use a hash of the heading text + +### TOC with Leader Dots (Copy-Paste Ready) + +**⚠️ WARNING**: This manual approach creates a visual-only TOC. It has **NO clickable links** and **NO auto-updated page numbers**. Use the auto-generated TOC above (TocDocTemplate + multiBuild) whenever possible. Only use this leader-dots approach if you need very specific visual styling that the auto TOC cannot provide - and even then, prefer the auto approach. + +For a professional TOC with leader dots connecting titles to page numbers: + +```python +from reportlab.lib.pagesizes import A4 +from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, PageBreak +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib import colors +from reportlab.lib.units import inch + +# Setup +doc = SimpleDocTemplate("report.pdf", pagesize=A4, + leftMargin=0.75*inch, rightMargin=0.75*inch) +styles = getSampleStyleSheet() +styles['Heading1'].fontName = 'Times New Roman' +styles['Heading1'].textColor = colors.black + +story = [] + +# Calculate dimensions +page_width = A4[0] +available_width = page_width - 1.5*inch +page_num_width = 50 # Fixed width for page numbers +dots_column_width = available_width - 200 - page_num_width +optimal_dot_count = int(dots_column_width / 4.5) # ~4.5pt per dot at 7pt font + +# Define styles +toc_style = ParagraphStyle('TOCEntry', parent=styles['Normal'], + fontName='Times New Roman', fontSize=11, leading=16) +dots_style = ParagraphStyle('LeaderDots', parent=styles['Normal'], + fontName='Times New Roman', fontSize=7, leading=16) + +# Build TOC +toc_data = [ + [Paragraph('<b>Table of Contents</b>', styles['Heading1']), '', ''], + ['', '', ''], +] + +entries = [('Section 1', '5'), ('Section 2', '10')] +for title, page in entries: + toc_data.append([ + Paragraph(title, toc_style), + Paragraph('.' * optimal_dot_count, dots_style), + Paragraph(page, toc_style) + ]) + +toc_table = Table(toc_data, colWidths=[None, dots_column_width, page_num_width]) +toc_table.setStyle(TableStyle([ + ('GRID', (0, 0), (-1, -1), 0, colors.white), + ('LINEBELOW', (0, 0), (0, 0), 1.5, colors.black), + ('ALIGN', (0, 0), (0, -1), 'LEFT'), + ('ALIGN', (1, 0), (1, -1), 'LEFT'), + ('ALIGN', (2, 0), (2, -1), 'RIGHT'), + ('VALIGN', (0, 0), (-1, -1), 'TOP'), + ('LEFTPADDING', (0, 0), (-1, -1), 0), + ('RIGHTPADDING', (0, 0), (-1, -1), 0), + ('TOPPADDING', (0, 2), (-1, -1), 3), + ('BOTTOMPADDING', (0, 2), (-1, -1), 3), + ('TEXTCOLOR', (1, 2), (1, -1), TEXT_MUTED), # From palette --c-muted +])) + +story.append(toc_table) +story.append(PageBreak()) +doc.build(story) +``` + +**MANDATORY Leader Dots Rules:** + +| Rule | Requirement | +|------|-------------| +| Column widths | ✅ MUST use fixed values. Percentage-based widths are **STRICTLY FORBIDDEN**. | +| Dot count | ✅ MUST calculate dynamically: `int(dots_column_width / 4.5)`. Hard-coded counts are **STRICTLY FORBIDDEN**. | +| Page number column | ✅ MUST be at least 40pt wide. | +| Dot font size | ✅ MUST NOT exceed 8pt. | +| Dot alignment | ✅ MUST be LEFT-aligned (visual flow from title). | +| Padding | ✅ MUST be exactly 0 between columns. | +| Bold text | ✅ MUST use `Paragraph('<b>Text</b>', style)`. Plain strings like `'<b>Text</b>'` are **STRICTLY FORBIDDEN**. | +| Indentation | Use leading spaces for hierarchy (e.g., `" 1.1 Subsection"`). | + +### TOC Post-Generation Validation (MANDATORY) + +After generating any PDF with a Table of Contents, run: + +```bash +python3 "$PDF_SKILL_DIR/scripts/toc_validate.py" check-pdf output.pdf +``` + +If any errors are returned, fix the code and regenerate. Common issues: +- `TOC_ALL_SAME_PAGE` → You used `build()` instead of `multiBuild()`, so all page numbers are stuck at 1. +- `TOC_NO_ENTRIES` → Headings are missing `bookmark_name`/`bookmark_level` attributes. +- `TOC_PAGES_INVALID` → A TOC entry references a page beyond the document's total page count. + +--- + +## Cross-References (Figures, Tables, Bibliography) + +Pre-register all figures, tables, and references BEFORE using them in text. + +```python +class CrossReferenceDocument: + def __init__(self): + self.figures = {} + self.tables = {} + self.refs = {} + self.figure_counter = 0 + self.table_counter = 0 + self.ref_counter = 0 + + def add_figure(self, name): + if name not in self.figures: + self.figure_counter += 1 + self.figures[name] = self.figure_counter + return self.figures[name] + + def add_table(self, name): + if name not in self.tables: + self.table_counter += 1 + self.tables[name] = self.table_counter + return self.tables[name] + + def add_reference(self, name): + if name not in self.refs: + self.ref_counter += 1 + self.refs[name] = self.ref_counter + return self.refs[name] +``` + +--- + +## Image Handling + +### Diagram Embedding Rules (MANDATORY) + +**Figures are block-level Flowables.** Every image/diagram in ReportLab MUST be appended to `story` as a standalone `Image()` or wrapped in `KeepTogether([image, caption])`. Never simulate floating layouts by placing images inside Paragraph text or multi-column Table cells alongside body text. + +**Complex diagrams (>12 nodes)**: Decompose into a ReportLab Table (details) + a simplified diagram image (overview). The diagram gives the big picture; the table carries precision. See SKILL.md "Complex Diagram Strategy". + +**Diagram quality**: When generating flowcharts/diagrams for PDF embedding (via Playwright screenshot or ReportLab Drawing), follow the diagram content quality rules in SKILL.md § "Diagram Content Quality Rules". Key points: connectors must not pass through nodes, no high-saturation large fills, multi-arrow convergence must use merge pattern, font sizes must be legible at final embedded size (see context-specific minimums in SKILL.md). + +**Cross-brief diagram pipeline**: For flowcharts/architecture diagrams, generate via **Playwright+CSS** (HTML nodes + CSS layout + SVG connectors), screenshot at 2× device scale factor for 300dpi print quality, then embed with `Image()`. See SKILL.md § "Diagram Generation Strategy" for the full pipeline. + +**🚫 FORBIDDEN: TikZ standalone → PNG pipeline in Report brief.** Report uses ReportLab, not LaTeX. Going through TikZ requires a LaTeX compiler, adds compilation steps, and models frequently produce broken TikZ code. Use Playwright+CSS instead - it's native to the existing cover rendering engine. + +```python +# Playwright+CSS diagram → PNG → ReportLab embedding +# 1. LLM generates diagram.html (CSS grid/flexbox nodes + SVG arrows) +# 2. Playwright screenshots at 2× scale (300dpi equivalent) +# 3. Embed: +from reportlab.platypus import Image +img = Image('diagram.png', width=450) # auto height via aspect ratio +story.append(img) # block-level Flowable +``` + +**⚠️ CRITICAL: Preserve aspect ratio - NEVER hardcode both width and height without reading actual image dimensions.** + +**⚠️ CRITICAL: Always constrain to available space - NEVER insert at original size if it exceeds container width/height.** + +Pie charts become ellipses, radar charts become diamonds, photos get stretched if you set arbitrary width/height. + +**→ Full overflow prevention spec: `typesetting/overflow.md`** - read for the complete bounding box system. + +```python +from PIL import Image as PILImage +from reportlab.platypus import Image +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import inch + +def embed_image(path: str, max_width: float = None, max_height: float = None) -> Image: + """Embed image with preserved aspect ratio, constrained to container. + + ALWAYS use this pattern. Never hardcode both width and height. + If max_width is None, defaults to available_width (page - margins). + """ + if max_width is None: + max_width = A4[0] - 2.0 * inch # Default: A4 width minus 1" margins each side + if max_height is None: + max_height = A4[1] * 0.35 # ~294pt ≈ 10cm — leaves room for caption + surrounding text on same page + + pil_img = PILImage.open(path) + orig_w, orig_h = pil_img.size + + # Scale to fit within BOTH max_width and max_height + ratio_w = max_width / orig_w if orig_w > max_width else 1.0 + ratio_h = max_height / orig_h if orig_h > max_height else 1.0 + ratio = min(ratio_w, ratio_h) + + return Image(path, width=orig_w * ratio, height=orig_h * ratio) + +# Usage: +available_width = A4[0] - left_margin - right_margin +img = embed_image('chart.png', max_width=available_width, max_height=300) +story.append(img) +``` + +```python +# ❌ NEVER do this - distorts the image: +img = Image('chart.png', width=400, height=250) # arbitrary height breaks aspect ratio +``` + +--- + +## PDF Metadata + +```python +doc = SimpleDocTemplate( + pdf_filename, + pagesize=letter, + title=os.path.splitext(pdf_filename)[0], + author='Z.ai', + creator='Z.ai', + subject='Document purpose description' +) +``` + +### ⚠️ MANDATORY: Post-Generation Metadata + +After `doc.build(story)` completes: +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.brand output.pdf +``` + +### ⚠️ MANDATORY: Post-Generation Blank Page Cleanup + +After metadata branding, remove any accidental blank pages: +```bash +python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.clean output.pdf -o output_clean.pdf +``` +If blank pages were found, rename `output_clean.pdf` → `output.pdf`. + +--- + +## MANDATORY: Post-Generation Code Sanitization + +**After writing PDF generation code and BEFORE executing it**, sanitize using: + +```bash +# Step 0: Set skill root path (see SKILL.md § Script Path Setup) +PDF_SKILL_DIR="<skill_directory>" + +# Step 1: Write code to .py file +cat > generate_pdf.py << 'PYEOF' +# ... your PDF generation code ... +PYEOF + +# Step 2: Sanitize (MUST run before execution) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" code.sanitize generate_pdf.py + +# Step 3: Execute +python3 generate_pdf.py + +# Step 4: Add metadata (MUST run after PDF creation) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" meta.brand output.pdf + +# Step 5: Check for missing glyphs (RECOMMENDED) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" font.check output.pdf + +# Step 6: Check TOC quality (if document has TOC) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" toc.check output.pdf + +# Step 7: PDF quality assurance scan (MUST run after all other checks) +python3 "$PDF_SKILL_DIR/scripts/pdf_qa.py" output.pdf +``` + +**FORBIDDEN patterns:** +```bash +# ❌ PROHIBITED: python -c with inline code +python -c "from reportlab... doc.build(story)" + +# ❌ PROHIBITED: heredoc without saving to file first +python3 << 'EOF' +from reportlab... +EOF + +# ❌ PROHIBITED: executing .py without sanitizing +python generate_pdf.py # Missing sanitization! +``` + +--- + +## Debug Tips for Layout Issues + +**→ Full overflow prevention spec: `typesetting/overflow.md`** - read it for the complete bounding box system, two-pass rendering, fallback strategies, and all three route implementations. + +```python +from reportlab.platypus import HRFlowable +from reportlab.lib.colors import red + +# Visualize spacing during development - insert between elements +story.append(table) +story.append(HRFlowable(width="100%", color=red, thickness=0.5, spaceBefore=0, spaceAfter=0)) +story.append(Spacer(1, 6)) +story.append(HRFlowable(width="100%", color=red, thickness=0.5, spaceBefore=0, spaceAfter=0)) +story.append(caption) +# Red lines create visual markers to see actual spacing - remove before final build +``` + +--- + +## Resume / CV Template (ATS-Friendly) + +Single-column, clean layout optimised for Applicant Tracking Systems (ATS). No graphics, no sidebars, no colour blocks - just well-structured text that parses perfectly by HR software. + +**When to use this template:** +- Applying to corporate jobs through online portals +- Any scenario where the PDF will be machine-parsed before a human reads it +- When the recruiter explicitly asks for "a standard resume" + +**When NOT to use (use Academic brief instead):** +- Creative/design industry positions → Academic brief (AltaCV-style) +- Academic CV with publications → Academic brief (Academic CV) + +### Resume Design Rules +- **Target 1 page** unless user specifies otherwise +- **Margins**: `left=1.5cm, right=1.5cm, top=1.5cm, bottom=1.5cm` +- **No cover page** - content starts immediately +- **No TOC** - too short for table of contents +- **Font**: Times New Roman (English) or SimHei (Chinese) +- **Body font size**: 10-10.5pt; Name: 22-26pt; Section titles: 13-14pt +- **⚠️ Minimum font size: 12px (9pt) - HARD FLOOR.** No text in the entire resume may render smaller than 12px. This includes contact info, meta text, footnotes, and captions. Anything below 12px is unreadable in print and fails accessibility checks. +- **Section separator**: thin horizontal rule (`HRFlowable`) +- **Bullet style**: `•` with tight spacing + +### Resume Line-Break Rules (Language-Aware) +- **English**: Prefer breaking at word boundaries (spaces, hyphens). If a long word must be split to avoid excessive whitespace, break at a valid syllable boundary and insert a hyphen (`-`) - this is standard typographic practice (e.g., `experi-\nence`, `develop-\nment`). ReportLab supports `wordWrap='CJK'` only for CJK content; for English use default paragraph wrapping with `allowWidows=0, allowOrphans=0`. +- **Chinese/CJK**: Break allowed between any two CJK characters. Never break between a CJK character and its adjacent punctuation (、。,)》 etc. must stay with the preceding character). +- **Mixed content** (e.g., "Python 开发工程师"): Break at CJK boundaries or English word boundaries. Never split an English word in a CJK paragraph unless hyphenated. +- **Contact line**: Email, phone, location separated by `|` or `·`. Each segment must stay on one line - if too long, move to next line at the separator, not mid-segment. +- **Dates and ranges**: "Jan 2022 - Present" must stay as one unit. Never break a date range across lines. + +### Resume Page-Fill Rules (Anti-Blank-Space) +- **Goal: Fill ≥85% of the page height.** Content should reach at least the bottom quarter of the page. A resume that stops at 60% height with blank space below = FAIL. +- **Adaptive spacing strategy** (apply in order until page is ≥85% filled): + 1. Increase `spaceBefore` / `spaceAfter` on section headers (from 10pt up to 18pt) + 2. Increase `leading` (line height) on body text (from 14pt up to 18pt) + 3. Increase body `fontSize` by 0.5-1pt (from 10pt up to 11.5pt max) + 4. Add a "Professional Summary" or "Key Achievements" section if none exists + 5. Increase margins slightly (from 1.5cm up to 2cm) to reduce line width and push content downward +- **Never leave a visible blank area > 3cm at the bottom of the page.** +- **If content overflows to page 2**: do the reverse - reduce spacing, tighten leading (min 12pt), reduce fontSize (min 9pt / 12px), before removing content. + +### Complete Resume Template + +```python +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import cm, mm +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.enums import TA_LEFT, TA_CENTER +from reportlab.lib import colors +from reportlab.platypus import ( + SimpleDocTemplate, Paragraph, Spacer, HRFlowable, Table, TableStyle +) +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.pdfbase.pdfmetrics import registerFontFamily + +# ── Font Registration ── +pdfmetrics.registerFont(TTFont('TimesNewRoman', '/usr/share/fonts/truetype/english/Times-New-Roman.ttf')) +registerFontFamily('TimesNewRoman', normal='TimesNewRoman', bold='TimesNewRoman') +# For Chinese resumes, also register SimHei: +# pdfmetrics.registerFont(TTFont('SimHei', '/usr/share/fonts/truetype/chinese/SimHei.ttf')) +# registerFontFamily('SimHei', normal='SimHei', bold='SimHei') + +# ── Styles ── +# ACCENT must come from palette.generate (Step 2) +# Run: python3 "$PDF_SKILL_DIR/scripts/pdf.py" palette.generate --title "Resume" --mode minimal +ACCENT = colors.HexColor('<accent from palette>') # Replace with palette output + +name_style = ParagraphStyle( + 'ResumeName', fontName='TimesNewRoman', fontSize=24, + leading=28, alignment=TA_CENTER, spaceAfter=2 +) +contact_style = ParagraphStyle( + 'ResumeContact', fontName='TimesNewRoman', fontSize=10, + leading=14, alignment=TA_CENTER, textColor=TEXT_MUTED, # From palette --c-muted + spaceAfter=8 +) +section_title_style = ParagraphStyle( + 'ResumeSectionTitle', fontName='TimesNewRoman', fontSize=13, + leading=16, spaceBefore=10, spaceAfter=4, + textColor=ACCENT +) +job_title_style = ParagraphStyle( + 'ResumeJobTitle', fontName='TimesNewRoman', fontSize=11, + leading=14, spaceAfter=1 +) +job_meta_style = ParagraphStyle( + 'ResumeJobMeta', fontName='TimesNewRoman', fontSize=10, + leading=13, textColor=TEXT_MUTED, spaceAfter=4 # From palette --c-muted +) +bullet_style = ParagraphStyle( + 'ResumeBullet', fontName='TimesNewRoman', fontSize=10, + leading=14, leftIndent=14, bulletIndent=0, + spaceBefore=1, spaceAfter=1 +) +body_style = ParagraphStyle( + 'ResumeBody', fontName='TimesNewRoman', fontSize=10, + leading=14, spaceAfter=2 +) + +# ── Helpers ── +def section_header(title): + """Section title + thin rule separator.""" + return [ + Paragraph(f'<b>{title}</b>', section_title_style), + HRFlowable(width='100%', thickness=0.8, color=ACCENT, + spaceBefore=0, spaceAfter=6), + ] + +def experience_entry(title, company, dates, location, bullets): + """One work experience block.""" + elements = [ + Paragraph(f'<b>{title}</b>', job_title_style), + Paragraph(f'{company} | {dates} | {location}', job_meta_style), + ] + for b in bullets: + elements.append(Paragraph(f'• {b}', bullet_style)) + elements.append(Spacer(1, 4)) + return elements + +def education_entry(degree, school, dates, details=None): + """One education block.""" + elements = [ + Paragraph(f'<b>{degree}</b>', job_title_style), + Paragraph(f'{school} | {dates}', job_meta_style), + ] + if details: + elements.append(Paragraph(details, body_style)) + elements.append(Spacer(1, 4)) + return elements + +def skills_row(categories): + """ + Skills as compact label: value pairs. + categories = [('Programming', 'Python, Java, C++'), ('Tools', 'Git, Docker')] + """ + elements = [] + for cat, vals in categories: + elements.append(Paragraph(f'<b>{cat}:</b> {vals}', body_style)) + return elements + +# ── Build Document ── +doc = SimpleDocTemplate( + 'resume.pdf', pagesize=A4, + leftMargin=1.5*cm, rightMargin=1.5*cm, + topMargin=1.5*cm, bottomMargin=1.5*cm, + title='Resume - Your Name', + author='Z.ai', creator='Z.ai' +) + +story = [] + +# Header +story.append(Paragraph('<b>YOUR NAME</b>', name_style)) +story.append(Paragraph( + 'email@example.com | +86 138-0000-0000 | Shanghai, China | github.com/yourname', + contact_style +)) + +# Summary +story.extend(section_header('PROFESSIONAL SUMMARY')) +story.append(Paragraph( + 'Results-driven software engineer with 5+ years of experience in backend systems, ' + 'distributed computing, and cloud-native architectures. Led a team of 8 engineers ' + 'delivering a real-time data pipeline processing 2M+ events/sec.', + body_style +)) + +# Experience +story.extend(section_header('WORK EXPERIENCE')) +story.extend(experience_entry( + 'Senior Software Engineer', 'Tech Company Inc.', 'Jan 2022 - Present', 'Shanghai', + [ + 'Designed and deployed a microservices architecture serving 10M daily active users', + 'Reduced API latency by 40% through query optimisation and caching strategies', + 'Mentored 3 junior engineers; established code review standards adopted team-wide', + ] +)) +story.extend(experience_entry( + 'Software Engineer', 'Startup Co.', 'Jul 2019 - Dec 2021', 'Beijing', + [ + 'Built real-time recommendation engine using collaborative filtering (CTR +25%)', + 'Implemented CI/CD pipeline reducing deployment time from 2 hours to 15 minutes', + ] +)) + +# Education +story.extend(section_header('EDUCATION')) +story.extend(education_entry( + 'M.Sc. Computer Science', 'Tsinghua University', '2017 - 2019', + 'GPA: 3.8/4.0 | Thesis: Distributed Graph Processing on Heterogeneous Clusters' +)) +story.extend(education_entry( + 'B.Eng. Software Engineering', 'Zhejiang University', '2013 - 2017' +)) + +# Skills +story.extend(section_header('SKILLS')) +story.extend(skills_row([ + ('Languages', 'Python, Java, Go, SQL, TypeScript'), + ('Frameworks', 'Spring Boot, FastAPI, React, Kubernetes, Kafka'), + ('Tools', 'Git, Docker, Terraform, AWS (EC2/S3/Lambda), PostgreSQL, Redis'), +])) + +doc.build(story) +``` + +### Resume Checklist +- [ ] **1 page** (unless user says otherwise) +- [ ] **No cover page, no TOC** +- [ ] Tight margins (1.5cm all sides) +- [ ] Name prominent at top (22-26pt) +- [ ] Contact info single line, centered +- [ ] Section headers with consistent separator style +- [ ] Bullets concise - start with action verbs +- [ ] Quantified achievements (%, $, count) +- [ ] No photos, no icons, no colour blocks (ATS-safe) +- [ ] Font: only registered fonts (Times New Roman / SimHei) +- [ ] **⚠️ Minimum font size 12px (9pt)** - no text smaller than this anywhere +- [ ] **Line breaks are language-aware** - no mid-word English breaks, no CJK punctuation orphans, no date range splits +- [ ] **Page fill ≥85%** - no large blank area at bottom. If sparse, increase spacing/leading/font size adaptively + +--- + +## Component Reference + +### Block types: +| Type | Description | Notes | +|------|-------------|-------| +| h1 | 22pt + accent underline rule | KeepTogether with rule | +| h2 | 15pt, dark | No rule, no accent | +| h3 | 11.5pt bold | No accent | +| body | 10.5pt justified, 17pt leading | Supports `<b>` `<i>` `<font>` | +| bullet | Body size with `•` prefix | Unordered list | +| numbered | Body size with N. prefix | Counter auto-resets | +| callout | Accent left border 4px + light tint bg | Max one per section | +| table | Accent header + alternating rows + outer box only | Supports fractional col_widths | +| code | Courier 8.5pt + accent left border | Optional language label | +| divider | Accent 1.2pt rule | Use sparingly | +| caption | 8.5pt muted, centered | Below images/tables | +| chart | matplotlib figure saved as PNG → `Image()` flowable | Generate chart in-script, `fig.savefig()` → embed. Set `plt.rcParams['font.sans-serif']=['SimHei']` for CJK. Always `tight_layout()`. **Must follow `typesetting/charts.md` rules**: delete top/right spines, dashed grid 20% opacity, donut default for pie, anti-stacking labels. | +| quote | Body italic + left indent 24pt + muted accent left border 2px | For blockquotes / testimonials | +| bibliography | Hanging indent (firstLineIndent=-24pt, leftIndent=24pt) | GB/T 7714 or APA format per language | +| math | Rendered via `<super>` `<sub>` tags in Paragraph | For inline math; complex equations → use academic brief instead | + +### Header / footer: +- Header: document title (left, 7.5pt, muted) + accent rule (1.5pt, full width) +- Footer: author (left, 7.5pt, muted) + page number (right, 7.5pt, muted) + light rule above + +### Custom flowables (preferred over Table hacks): +- **CalloutBox**: accent 4px left border + light tint background - cleaner than Table simulation +- **BibliographyItem**: hanging indent reference entry + +--- + +## Quick Reference + +| Task | Best Tool | Command/Code | +|------|-----------|--------------| +| Create PDF (ReportLab) | reportlab | Canvas or Platypus | +| Fill PDF forms | Process brief | `python3 "$PDF_SKILL_DIR/scripts/pdf.py" form.fill` or annotation workflow | +| Merge PDFs | Process brief | `python3 "$PDF_SKILL_DIR/scripts/pdf.py" pages.merge` | +| Extract text | Process brief | `python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.text` | +| Extract tables | Process brief | `python3 "$PDF_SKILL_DIR/scripts/pdf.py" extract.table` | + +--- + +## Document-Type Quick Reference + +### 📋 Invoice / Receipt + +Key elements: +- Company logo/name at top +- Invoice number + date (right-aligned) +- Seller/buyer info block (2-column layout) +- Line items table: Item, Quantity, Unit Price, Amount +- Total row with bold font +- Tax info + payment terms at bottom +- Stamp/seal placeholder (empty circle or rect with "Seal Here" text) + +```python +# Invoice table pattern +data = [['Item', 'Qty', 'Unit Price', 'Amount']] +data += [[item['name'], item['qty'], item['price'], item['total']] for item in items] +data.append(['', '', 'Total:', f'¥{total}']) +``` + +### 📝 Contract / Legal Document + +Key elements: +- Title centered, bold, 18pt +- Numbered clauses with auto-increment +- Signature block at bottom: Party A / Party B with date line +- Page numbers mandatory + +```python +# Signature block pattern +sig_data = [ + ['Party A (Seal): ________________', 'Party B (Seal): ________________'], + ['Date: ____/____/________', 'Date: ____/____/________'], +] +sig_table = Table(sig_data, colWidths=[250, 250]) +``` + +### 📊 Book / Long Document + +For documents with 10+ pages: +1. Generate TOC with `TableOfContents()` from reportlab.platypus +2. Use `CondPageBreak(H1_ORPHAN_THRESHOLD)` before H1 headings (NOT `PageBreak()` — never force page breaks between chapters) +3. Add running headers/footers with chapter title + page number +4. Run `python3 "$PDF_SKILL_DIR/scripts/toc_validate.py" output.pdf` to verify TOC links +5. Consider using `bookmarks=True` for PDF outline navigation + +--- + +### 📝 Exam / Quiz / Test Paper / Worksheet + +**Exam papers have unique layout requirements. These rules are MANDATORY when generating any test/quiz/exam document.** + +#### Numbering & Structure +``` +一、选择题(每小题 3 分,共 30 分) ← Section header (宋体/黑体 14pt Bold) + +1. 以下哪个不是 Python 内置数据类型? ← Question stem (12pt) + A. int ← Options: indented 24pt (2em) + B. float ← Each option on new line or 2×2 grid + C. array + D. str + +2. 以下表达式的值为? ← Next question + A. True B. False ← Short options: inline 2×2 grid OK + C. None D. Error +``` + +#### Option Indentation (MANDATORY) +```python +# ReportLab styles for exam papers +question_style = ParagraphStyle( + 'Question', fontSize=12, leading=16, + leftIndent=0, firstLineIndent=0, + spaceBefore=12, spaceAfter=4, +) +option_style = ParagraphStyle( + 'Option', fontSize=12, leading=16, + leftIndent=24, # ← MANDATORY: 24pt indent from question stem + firstLineIndent=0, + spaceBefore=2, spaceAfter=2, +) +``` + +#### Option Layout Decision +```python +def format_options(options): + """ + Decide option layout based on content length. + Short options (≤4 chars each, ≤4 options) → 2×2 grid table + Long options or >4 options → vertical list, one per line + """ + max_len = max(len(opt) for opt in options) + if len(options) <= 4 and max_len <= 6: # CJK: 4 chars; Latin: 6 chars + # 2×2 grid using Table + grid = Table( + [[options[0], options[1]], [options[2], options[3]]], + colWidths=[200, 200], hAlign='LEFT' + ) + grid.setStyle(TableStyle([ + ('LEFTPADDING', (0,0), (-1,-1), 24), # Match option indent + ('VALIGN', (0,0), (-1,-1), 'TOP'), + ])) + return grid + else: + # Vertical list + return [Paragraph(f'{opt}', option_style) for opt in options] +``` + +#### Answer Space Reservation (MANDATORY) + +**Every question MUST have adequate answer space. No exceptions.** + +| Question Type | Minimum Space | Implementation | +|--------------|---------------|----------------| +| Multiple choice | 0 extra (options are the answer) | Just question + options | +| Fill-in-the-blank | Inline underline, min 80pt width | `<u>        </u>` or ReportLab `HRFlowable` | +| Short answer | 40-60pt (2-3 lines) | `Spacer(1, 50)` | +| Calculation / math work | 120-200pt (6-10 lines) | `Spacer(1, 160)` with light guide lines | +| Essay / long answer | 200-400pt (10-20 lines) | `Spacer(1, 300)` with light guide lines | +| Proof / derivation | 160-250pt (8-12 lines) | `Spacer(1, 200)` | + +```python +# Answer line helper — light dashed lines for handwriting +def add_answer_lines(story, num_lines=8, line_spacing=20): + """Add light guide lines for handwritten answers.""" + for i in range(num_lines): + story.append(Spacer(1, line_spacing)) + story.append(HRFlowable( + width='90%', thickness=0.3, + color=colors.Color(0.8, 0.8, 0.8), # Light gray + dash=[4, 4], # Dashed + spaceAfter=0, spaceBefore=0 + )) +``` + +#### Page Density +- `spaceBefore=12pt` between questions minimum +- `spaceBefore=24pt` before section headers (一、二、三) +- Score indicator after question number: `1. (分值: 5分)` or `1. [5 pts]` +- Page header: exam title + time limit + total score +- No cover page unless explicitly requested + +--- + +## Data-to-Ink Ratio Rules (MANDATORY for Reports) + +**CRITICAL: Do NOT write long paragraphs to describe data trends.** You MUST extract metrics and trends into structured visual elements. + +### Pattern Detection Table + +Before writing ANY body paragraph, scan the text. If you find any of these patterns, extract them: + +| Raw text pattern | ❌ FORBIDDEN | ✅ REQUIRED visual form | +|-----------------|-------------|----------------------| +| "revenue grew 12% to $4.2M" | Bury in paragraph prose | CalloutBox with bold `+12%` + `$4.2M` | +| "latency dropped from 120ms to 75ms" | Long explanatory sentence | CalloutBox or metrics table row | +| "Q1→Q2→Q3: 10%→25%→40%" | Inline numbers in text | Chart (matplotlib PNG → `Image()`) | +| "first...second...third..." steps | Paragraph with ordinal words | Numbered Table or process list | +| "compared to last year / vs Q2" | Nested comparisons in prose | Side-by-side comparison table | +| "accounted for 60% of total" | Percentage in paragraph | Pie chart or stacked bar chart | + +### ReportLab CalloutBox Template + +Use this pattern for extracted metrics - it's visually clean and takes only 5 lines: + +```python +stat_style = ParagraphStyle( + name='StatBig', fontName='Times New Roman', fontSize=22, + leading=26, textColor=ACCENT, alignment=TA_CENTER # From palette +) +label_style = ParagraphStyle( + name='StatLabel', fontName='Times New Roman', fontSize=9, + leading=12, textColor=TEXT_MUTED, alignment=TA_CENTER # From palette +) + +callout = Table( + [[Paragraph('<b>+12%</b>', stat_style)], + [Paragraph('Revenue Growth vs Q2', label_style)]], + colWidths=[160] +) +callout.setStyle(TableStyle([ + ('BACKGROUND', (0,0), (-1,-1), BG_SURFACE), # From palette --c-mid + ('BOX', (0,0), (-1,-1), 1, ACCENT), # From palette --c-accent + ('TOPPADDING', (0,0), (-1,-1), 10), + ('BOTTOMPADDING', (0,0), (-1,-1), 10), + ('VALIGN', (0,0), (-1,-1), 'MIDDLE'), +])) +story.append(KeepTogether(callout)) +``` + +### Cover Pages (HTML/Playwright) +When the cover routes through Playwright: +- Use `Delta_Widget` components for KPIs and metrics. +- Use `Process_List` components for workflows and timelines. +- Use `Sidenote_Block` components (in `tufte_report` archetype) for citations and supplementary data. +- For data-heavy pages, add `data_points` arrays to any component - the engine renders them as content-aware background curves. + +--- + +## Critical Reminders Checklist + +Before submitting code, verify: + +- [ ] **Font restriction**: Only 6 registered fonts used +- [ ] **Font family registered**: `registerFontFamily()` called for all used fonts +- [ ] **Mixed language**: `install_font_fallback()` called after font registration (auto handles `<font>` wrapping) +- [ ] **Rich text tags**: Only inside `Paragraph()` objects +- [ ] **Table cells**: ALL text wrapped in `Paragraph()` +- [ ] **Scientific notation**: Large/small numbers use `<super>` tags +- [ ] **Chinese text**: `wordWrap='CJK'` in ParagraphStyle +- [ ] **Line breaks**: Using `<br/>` not `\n` +- [ ] **Headings**: Bold with `<b>` tags +- [ ] **Table headers**: White bold text on dark blue (#1F4E79) +- [ ] **Metadata**: Title matches filename, Author/Creator = "Z.ai" +- [ ] **Sanitization**: Code sanitized before execution +- [ ] **Post-build metadata**: `pdf.py meta.brand` called after build +- [ ] **Post-build blank page cleanup**: `pdf.py pages.clean` called after build +- [ ] **Glyph check**: `pdf.py font.check` run to verify no missing glyphs +- [ ] **TOC check**: `pdf.py toc.check` run if document has TOC (entries, pages, links) +- [ ] **PDF QA scan**: `pdf_qa.py` run to verify page consistency, CJK punctuation, overflow, margins, table centering, font embedding, metadata diff --git a/skills/pdf/configs/components.md b/skills/pdf/configs/components.md new file mode 100755 index 0000000..873fe3c --- /dev/null +++ b/skills/pdf/configs/components.md @@ -0,0 +1,153 @@ +# Components — Art Direction JSON Lexicon + +This file defines the strict component vocabulary for the Creative pipeline. + +**CRITICAL RULE: DO NOT OUTPUT HTML OR CSS.** +You are an Art Director. You only output JSON. To use these components, insert their corresponding JSON objects into the `components` array of your page blueprint. The `design_engine.py` will automatically compile them into gallery-grade visual assets. + +--- + +## 1. Glass_Canvas +The primary container for readable body text. Simulates printed text on frosted acrylic. + +**JSON Blueprint Structure:** +```json +{ + "type": "Glass_Canvas", + "markdown_content": "### The Divide\nYour text goes here. Supports standard Markdown.", + "tension_score": 0.8 +} +``` +**Parameters:** +- `markdown_content`: (Required) The actual text. **Recommended under 150 words; absolute max 250 words.** +- `tension_score`: (Optional, 0.0 to 1.0) Semantic tension. Drives dynamic font weight (300 to 900). Use `0.1` for calm/light text, `0.9` for crisis/heavy text. Do NOT use on data-heavy pages. + +--- + +## 2. Hero_Typography +Massive, page-dominating title text that physically interacts with the background via blend modes. + +**JSON Blueprint Structure:** +```json +{ + "type": "Hero_Typography", + "content": "THE WEIGHT<br>OF SILENCE", + "weight": "black", + "variant": "standard", + "scale": 6 +} +``` +**Parameters:** +- `content`: (Required) The text. Use `<br>` for deliberate typographic line breaks. +- `weight`: (Required) `"black"` (900 weight, dominating) or `"thin"` (100 weight, whisper-quiet/elegant). +- `variant`: (Optional) `"standard"` (default) only. ~~`"vertical_accent"`~~ is **NOT implemented** in `design_engine.py` — the engine silently ignores this parameter. Use `Floating_Meta` component instead for rotated/vertical decorative text. +- `scale`: (Optional, integer 1–6) Typographic scale level. The engine maps this to fluid CSS `clamp()` sizes: + - `6` → `clamp(64px, 12vw, 150px)` — Hero/Display, maximum impact + - `5` → `clamp(48px, 8vw, 96px)` — Primary Title + - `4` → `clamp(32px, 5vw, 56px)` — Subheadline + - `3` → `clamp(20px, 3vw, 32px)` — Lead Paragraph + - `2` → `16px` — Body + - `1` → `10px` — Meta/Caption + If omitted, the engine uses the default hero font size from CSS. + +--- + +## 3. Floating_Meta +Small-text metadata positioned vertically in corners, mimicking art monograph indexes. + +**JSON Blueprint Structure:** +```json +{ + "type": "Floating_Meta", + "position": "bottom-right", + "items": [ + "CATALOG NO. 2026.031", + "EDITION 1/500" + ] +} +``` +**Parameters:** +- `position`: (Required) `"top-left"`, `"top-right"`, `"bottom-left"`, or `"bottom-right"`. +- `items`: (Required) Array of short strings (dates, edition numbers, refs). + +--- + +## 4. Stat_Block +Data sculpture. Transforms boring numbers into massive visual objects. + +**JSON Blueprint Structure:** +```json +{ + "type": "Stat_Block", + "number": "97.3", + "unit": "%", + "label": "COMPLETION RATE" +} +``` +**Parameters:** +- `number`: The core massive digit. +- `unit`: Tiny unit attached to the number. +- `label`: Metadata label below the number. + +--- + +## 5. Hairline_Divider +Ultra-thin separator lines. Structural, like fold lines in print. + +**JSON Blueprint Structure:** +```json +{ + "type": "Hairline_Divider", + "style": "accent" +} +``` +**Parameters:** +- `style`: `"bleed"` (full width edge-to-edge) or `"accent"` (short centered 30% line). + +--- + +## 6. Page_Ghost_Number +Giant, 4% opacity watermark numbers that become part of the page's atmosphere. + +**JSON Blueprint Structure:** +```json +{ + "type": "Page_Ghost_Number", + "number": "03" +} +``` + +--- + +## 7. Shaped_Canvas (Advanced Semantic Shape-Wrapping) +A container where text flows around a non-rectangular shape. The empty space created by the shape IS the visual design. + +**JSON Blueprint Structure:** +```json +{ + "type": "Shaped_Canvas", + "shape_keyword": "wave", + "markdown_content": "The ocean stretched endlessly... (recommended under 150 words, absolute max 250)" +} +``` +**Parameters & Shape Presets:** +- `shape_keyword`: MUST be one of the following: + - `"circle"`: Unity, spotlight, focus. + - `"wave"`: Ocean, flow, fluidity. + - `"diagonal_slash"`: Disruption, change, energy. + - `"diamond"`: Luxury, precision, crystalline. + - `"wedge_right"`: Direction, progress, forward motion. +- `markdown_content`: Text to wrap around the shape. + +**CRITICAL Layout Constraint:** +If a page contains a `Shaped_Canvas`, that page's `archetype` MUST be set to `"shaped_editorial"` in the JSON. Never mix `Shaped_Canvas` and `Glass_Canvas` on the same page. + +--- + +## Blueprint Assembly Guidelines + +When constructing the JSON `pages` array, keep these layering and composition rules in mind: + +1. **Backgrounds**: Do NOT try to place background SVGs as components. Backgrounds are declared globally in the `art_direction.background_svg` field (`"flow"`, `"grid"`, `"noise"`, or `"continuous_flow"`). +2. **Layering**: The order of objects in the `components` array roughly dictates their top-to-bottom rendering. +3. **Breathing Room**: Less is more. A page with just one `Hero_Typography` and one `Floating_Meta` is highly sophisticated. Cramming 5 components on a page communicates desperation. \ No newline at end of file diff --git a/skills/pdf/configs/fonts.md b/skills/pdf/configs/fonts.md new file mode 100755 index 0000000..b8ae850 --- /dev/null +++ b/skills/pdf/configs/fonts.md @@ -0,0 +1,93 @@ +# Font System + +## Font Stacks (by pipeline) + +### Creative Pipeline (Playwright / HTML) + +Fonts are loaded via Google Fonts CDN in the HTML `<head>`: + +```html +<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700;900&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&display=swap" rel="stylesheet"> +``` + +#### Variable Font (for Tension Typesetting) + +When `tension_score` is used on `Glass_Canvas`, the engine switches to **Inter Variable** for continuous weight interpolation (100–900): + +```html +<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet"> +``` + +This enables `font-variation-settings: 'wght' <value>` for smooth, non-discrete weight transitions. The standard discrete-weight URL is still used when tension is not active. + +| Variable | Stack | Usage | +|----------|-------|-------| +| `--font-sans` | Inter, Noto Sans SC, Helvetica Neue, Apple Color Emoji, Segoe UI Emoji, sans-serif | Body text, UI elements, stats | +| `--font-serif` | Playfair Display, Noto Serif SC, Cormorant Garamond, Apple Color Emoji, serif | Hero text, editorial headlines | +| `--font-mono` | SF Mono, Consolas, Apple Color Emoji, monospace | Floating meta, timestamps, codes | + +### Report Pipeline (ReportLab) + +ReportLab requires registered fonts. CJK support via `UniSong` / `UniHei` (built into reportlab.lib.fonts): + +```python +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.cidfonts import UnicodeCIDFont +pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light')) # Song-ti (serif-like) +# Use 'STSong-Light' for body, headings +``` + +> **⚠️ ReportLab CANNOT render emoji.** If content has emoji, route to Creative pipeline. + +### Academic Pipeline (Tectonic / LaTeX) + +CJK support via `ctex` package: +```latex +\usepackage{ctex} % Auto-selects appropriate CJK fonts +``` + +For manual font selection: +```latex +\setCJKmainfont{Noto Serif CJK SC} +\setCJKsansfont{Noto Sans CJK SC} +``` + +> **⚠️ LaTeX silently drops emoji characters.** If content has emoji, route to Creative pipeline. + +## Emoji Font Fallback + +All Creative pipeline font stacks include emoji fallback: +- **macOS**: `Apple Color Emoji` (system default, full color emoji) +- **Windows**: `Segoe UI Emoji` +- **Linux**: `Noto Color Emoji` (install: `apt install fonts-noto-color-emoji`) + +Chromium (used by Playwright) on macOS renders emoji natively — no extra configuration needed. + +## CJK Font Weight Guide + +| Weight Value | Inter Equivalent | Noto Sans SC Name | Usage | +|-------------|------------------|-------------------|-------| +| 300 | Light | Light | Subtitles, captions, meta text | +| 400 | Regular | Regular | Body text | +| 500 | Medium | Medium | Semi-emphasis | +| 700 | Bold | Bold | Headlines, emphasis | +| 900 | Black | Black | Hero text, stat numbers | + +> **Tip**: Noto Sans SC weights 300–900 cover most use cases. Always load at least 400 and 700 via Google Fonts. + +## Font Size Scale + +Recommended type scale (base: 16px body): + +| Role | Size | Weight | Line-Height | +|------|------|--------|-------------| +| Page Ghost Number | 240px | 900 | 1.0 | +| Hero (large) | clamp(48px, 10vw, 110px) | 900 | 0.88 | +| Hero (serif thin) | clamp(48px, 10vw, 110px) | 100 | 0.88 | +| Stat Number | clamp(32px, 5vw, 56px) | 900 | 0.9 | +| Section Title | 24px | 800 | 1.2 | +| Subsection | 20px | 700 | 1.3 | +| Body | 16px | 400 | 1.6–1.7 | +| Caption | 14px | 400 | 1.5 | +| Floating Meta | 10px | 400 (mono) | 1.4 | +| Stat Label | 11px | 400 | 1.2 | diff --git a/skills/pdf/configs/visual_framework.md b/skills/pdf/configs/visual_framework.md new file mode 100755 index 0000000..90c37c8 --- /dev/null +++ b/skills/pdf/configs/visual_framework.md @@ -0,0 +1,263 @@ +# Visual Framework — Aesthetic Axioms + +Non-template design system. This file is the LLM's thinking framework — not a list of styles to pick from, but a set of immutable laws that govern every design decision. + +--- + +## The Three Absolutes + +1. **Restraint** — Remove until it hurts. Then remove one more thing. +2. **Contrast** — Hierarchy through scale and weight, never through color multiplication. +3. **Space Metaphor** — Every page has physical energy: gravity, tension, breathing. + +### The Anti-Gaudy Imperative + +> **When in doubt, remove. Never add.** + +A document looks "cheap" or "土" when it has too many competing visual elements. The cure is always subtraction, never rearrangement. + +**Red flags that a design is gaudy:** +- More than 3 decorative elements on a single body page +- Icons or emoji used as section header decoration (use typography instead) +- Decorative borders or frames around body content +- Pattern/texture backgrounds on body pages +- 3+ different hue families visible simultaneously +- Elements added "because there's empty space" rather than for function + +**The fix is always:** Delete the decorative element. If the page still communicates its message clearly, it didn't need the decoration. If it doesn't, fix the typography hierarchy — not by adding more visual noise. + +### 🚫 The Stock Image / Clipart / AI-Generated Decoration Ban + +> **ABSOLUTE PROHIBITION: Do NOT embed stock photos, clipart, watercolor illustrations, AI-generated images (flowers, patterns, borders, frames, ornaments), or any decorative raster/vector artwork into PDF documents.** + +This is the #1 source of "cheap" / "土" / "婚庆风" design. Examples of what is BANNED: +- ✘ Watercolor flowers / roses / floral corners / vine borders +- ✘ Gold/metallic decorative frames or borders +- ✘ Stock photo backgrounds (landscapes, textures, marble) +- ✘ Clipart illustrations (ribbons, bows, hearts, stars) +- ✘ AI-generated decorative artwork (DALL-E flowers, Midjourney patterns) +- ✘ Any `<img>` tag used for purely decorative (non-informational) purposes +- ✘ Unsplash/Pexels/Pixabay stock photos used as background or decoration + +**What IS allowed:** +- ✔ Geometric shapes rendered in CSS/SVG (circles, lines, rectangles — from `geometry.md`) +- ✔ Typography-based decoration (oversized letters, watermark text, letter-spacing effects) +- ✔ Color blocks and gradients (within palette rules) +- ✔ User-provided photos/logos that are CONTENT (not decoration) +- ✔ Charts and data visualizations +- ✔ Diagrams that convey information + +**The principle:** If removing an image doesn't reduce the information content of the document, that image is decoration and should be replaced with typography or geometric elements. + +**Wedding invitations, event cards, certificates:** Use elegant typography + geometric accents (thin lines, minimal shapes) + generous whitespace. A beautifully typeset name in 48pt serif with 6pt letter-spacing is infinitely more elegant than watercolor roses. + +--- + +## Color Axioms + +### The 60-30-10 Law + +Every design consists of exactly three tonal zones: + +| Zone | Coverage | Role | Generated by | +|------|----------|------|-------------| +| **60% — Ground** | Background, margins, empty space | Sets mood; the viewer doesn't "see" it consciously | `--c-bg` from `design_engine.py` | +| **30% — Structure** | Cards, dividers, secondary areas | Provides architecture; same hue family as ground, different lightness | `--c-mid` | +| **10% — Emphasis** | Headlines, key numbers, one accent element | Draws the eye; the only place "color" lives | `--c-accent` | + +### Saturation Discipline (HARD RULE) + +All colors pass through `design_engine.py` which enforces: +- **Saturation range**: 0.05 – 0.25 (HSL). This is non-negotiable. +- **No pure primaries**: Pure red (#ff0000), blue (#0000ff), yellow (#ffff00) are forbidden. +- **Max 3 hues** in the entire document. Variations in lightness/saturation don't count as separate hues. + +### Lightness Polarity + +Only two lightness regimes exist. The middle is forbidden. + +| Mode | Background L | Text L | Forbidden Zone | +|------|-------------|--------|----------------| +| **Dark** (Cinematic) | < 0.10 | > 0.85 | L between 0.30–0.70 for backgrounds | +| **Light** (Editorial) | > 0.95 | < 0.15 | L between 0.30–0.70 for backgrounds | + +Muddy mid-tones (L 0.30–0.70) for backgrounds produce the "cheap web app" look. Never. + +### What Color is NOT For + +Color does not create hierarchy. Size and weight do. + +| ❌ Wrong | ✅ Right | +|----------|---------| +| Blue heading, grey subheading, black body | All same hue; heading 900 weight 48px, sub 600 36px, body 400 16px | +| Colored tags/badges for categories | Monochrome tags with border weight variation | +| Gradient text on everything | Gradient text on exactly ONE hero element, nowhere else | + +--- + +## Typography Axioms + +### Weight is the New Color + +Hierarchy comes from the contrast between extremes of weight: + +| Element | Weight | Size Relative to Body | +|---------|--------|----------------------| +| Hero title | 100 (Thin) OR 900 (Black) | 4–6× body | +| Section heading | 700 | 2× body | +| Body | 400 | 1× (baseline) | +| Caption / metadata | 300 | 0.7× body | + +The gap between hero and body must be **dramatic** — at least 4× size ratio. Anything less feels timid. + +### Font Rules + +- **Max 2 families per page**. One serif, one sans (or one display, one text). Never three. +- **Every `font-family` must end with a generic**: `sans-serif`, `serif`, or `monospace`. +- **For physical print (>500mm)**: Use `pt`/`mm` for font sizes, never `px`. + +### Line-Height by Density + +| Content Type | Line-Height | Why | +|-------------|------------|-----| +| Body text, paragraphs | 1.6–1.8 | Comfortable reading | +| Headings | 0.85–1.0 | Tight, sculptural — headings are visual objects, not sentences | +| Captions, metadata | 1.4 | Compact but readable | + +--- + +## Space Axioms + +### The Breathing Margin Rule (MANDATORY) + +Every page must have **10–12% clear margin** on all edges. Nothing — no text, no decoration, no card — may enter this zone. + +The exact percentage depends on content density: +- **Sparse content** (hero pages, covers): 15% is fine — lots of air +- **Standard content** (editorial, data): **12%** is the sweet spot +- **Dense content** (tables, stat grids): 10% minimum — tighter but still breathable + +Default in `design_engine.py`: `inset: 10% 12%` (vertical 10%, horizontal 12%). + +``` +┌──────────────────────────────┐ +│ 10-12% margin (breathing) │ +│ ┌────────────────────────┐ │ +│ │ │ │ +│ │ Content lives here │ │ +│ │ (76-80% of canvas) │ │ +│ │ │ │ +│ └────────────────────────┘ │ +│ 10-12% margin (breathing) │ +└──────────────────────────────┘ +``` + +Decorative SVG backgrounds may extend into the margin zone (they're atmospheric, not content). But all readable elements respect it. + +### Negative Space is Content + +Empty space is not "wasted space." It's a compositional element: +- A poster with 50% whitespace communicates **confidence** +- A poster crammed to 90% communicates **desperation** +- The ratio of content to void sets the emotional tone + +### Proximity Rule + +Related items are close. Unrelated items are far. The gap between unrelated elements should be **at least 3× the gap between related ones**. + +--- + +## Intent → Visual Translation + +Five design intents. Each describes an **atmosphere** — the emotional field the viewer should feel. Intents do NOT prescribe specific parameters (font weight, line-height, SVG type, etc.). All concrete parameter mappings live in `briefs/creative.md` § Intent Mapping Table and `design_engine.py` INTENT_HUES/INTENT_HARMONY_MAP. + +### Calm +Stillness. The viewer’s breathing slows. Vast open space, restrained palette, nothing competes for attention. Covers healthcare, wellness, meditation, and minimalist aesthetics. The design communicates through what it *removes*. + +### Tension +Conflict. Sharp angles, dramatic contrast, elements that push against each other. Urgency, crisis, disruption. The page has physical energy — something is about to break or has already broken. + +### Energy +Motion. Dense composition, slight rotations, warmth. Marketing, launches, social campaigns. The design vibrates — it wants to move off the page. Nothing is perfectly centered. + +### Authority +Gravitas. Formal, deliberate, symmetrical. Government, finance, luxury, high-end corporate. Every element is placed with surgical precision. The design commands respect through restraint and weight. + +### Warmth +Comfort. Rounded edges, generous spacing, earth tones. Food, lifestyle, home. The design feels like a warm room — inviting, soft, human. + +--- + +> **Legacy mapping:** Serenity → Calm, Minimalism → Calm, Elegance → Authority. Old intent names are accepted as aliases in `design_engine.py` for backward compatibility. + +--- + +## Advanced Visual Techniques + +### Semantic-Topological Typesetting (Tension Score) + +Text is not visually flat — its "weight" should mirror its emotional intensity. When a document has narrative arc (calm introduction → crisis → resolution), font weight should breathe with it. + +**The Principle**: Assign a `tension_score` (0.0–1.0) to `Glass_Canvas` components. The engine maps this to a continuous font weight via Inter Variable font (300–900 axis). + +**When it matters**: +- Documents with 3+ pages and clear emotional shifts +- Storytelling: case studies, pitch decks, manifestos +- The reader may not consciously notice, but the subconscious weight variation creates "texture" in the reading experience — the same way a film score operates below awareness + +**When to skip**: +- Single-page posters (no arc to express) +- Data-heavy reports (numbers don't have "feelings") +- Short documents where all paragraphs share the same tone + +### Cross-Page Visual Continuity (Continuous Flow) + +Multi-page documents are not a collection of isolated posters — they're a journey. The background should reflect this. + +**The Principle**: Instead of generating independent SVGs per page, generate ONE continuous bezier curve spanning the entire document height, then slice it per-page via SVG `viewBox`. The result: curves that exit one page's bottom edge seamlessly enter the next page's top. + +**When to use `continuous_flow`**: +- Multi-page (2+) documents with narrative structure +- Journey/timeline documents +- Brand pieces where seamless sophistication matters +- Best paired with: `Warmth`, `Elegance`, `Serenity` intents + +**When to use regular `flow` instead**: +- Single-page documents (continuous has no meaning) +- Each page is conceptually independent (not a story) + +### Semantic Shape-Wrapping (Shaped_Canvas) + +The most dramatic of the three techniques. Text wraps around invisible geometric shapes, and the negative space itself becomes the illustration. + +**The Principle**: A floating `<div>` with CSS `shape-outside` creates a non-rectangular boundary for text flow. The empty void left by the shape is the design element. + +**The key insight**: We're not adding an image — we're sculpting the TEXT into a shape. This is extreme minimalism: zero visual assets, maximum visual impact. + +**When to use**: +- Pages that need a "wow" without images +- Section openers, cover alternatives +- Content that thematically connects to a shape (ocean → wave, progress → wedge) +- Pages with moderate text density (100–200 words) — the shape needs room + +**When NOT to use**: +- Dense content pages (shape eats 30–40% of text area → overflow risk) +- Together with `Glass_Canvas` on the same page (backdrop-filter conflicts) +- More than one per page (visual chaos) + +--- + +## Quality Litmus Tests + +Before delivering, every page must pass these: + +| Test | Method | +|------|--------| +| **3-meter test** | Can you read the headline from 3 meters away? (size hierarchy) | +| **Squint test** | Squint at the page — do you see clear dark/light zones? (contrast) | +| **Magazine test** | Could this be a page in Monocle, Kinfolk, or Cereal magazine? (taste) | +| **Removal test** | Can you remove any element without losing meaning? If not, you have too little. If yes, remove it. | +| **Color count** | Count distinct hues. If > 3, you've failed the color axiom. | +| **Saturation check** | Run `design_engine.py audit` — any S > 0.25 on non-accent = violation | +| **Breathing check** | Is there 15% clear margin on all edges? | +| **Consistency** | Do all pages share the same font pairing, color tokens, line thickness, and corner radius? | diff --git a/skills/pdf/references/resume-academic.tex b/skills/pdf/references/resume-academic.tex new file mode 100755 index 0000000..3427839 --- /dev/null +++ b/skills/pdf/references/resume-academic.tex @@ -0,0 +1,130 @@ +% ═══════════════════════════════════════════════════════════ +% Academic CV Template +% Single-column, multi-page, with bibliography support +% ═══════════════════════════════════════════════════════════ +\documentclass[11pt,a4paper]{article} + +\usepackage[top=2cm,bottom=2cm,left=2.5cm,right=2.5cm]{geometry} +\usepackage{enumitem} +\usepackage{titlesec} +\usepackage{fancyhdr} +\usepackage[colorlinks=true,linkcolor=blue,urlcolor=blue]{hyperref} +\usepackage{xcolor} +\usepackage[numbers,sort&compress]{natbib} + +% ── Fonts ── +\usepackage[rm]{roboto} +\renewcommand{\familydefault}{\rmdefault} + +% ── Section Formatting ── +\definecolor{accent}{HTML}{1F4E79} +\titleformat{\section} + {\Large\bfseries\color{accent}} % format + {} % label (no number) + {0em} % sep + {\MakeUppercase} % before-code + [\vspace{-0.8em}\color{accent}\rule{\linewidth}{0.8pt}] % after-code (rule) + +\titleformat{\subsection} + {\large\bfseries} + {} + {0em} + {} + +% ── Header/Footer ── +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\small\textcolor{gray}{Curriculum Vitae — Your Name}} +\fancyhead[R]{\small\textcolor{gray}{\thepage}} +\renewcommand{\headrulewidth}{0.4pt} +\fancyfoot{} + +\setlength{\parindent}{0pt} +\setlist{nosep,leftmargin=1.5em} + +% ── CV Entry command ── +\newcommand{\cventry}[4]{% + \textbf{#1}\hfill\textit{#2}\\ + #3\hfill#4\par\smallskip +} + +\begin{document} + +% ── Header ── +\begin{center} +{\LARGE\bfseries Your Full Name, Ph.D.}\par\smallskip +{\small +Department of Computer Science, Tsinghua University\\ +Beijing, China 100084\\ +\href{mailto:name@tsinghua.edu.cn}{name@tsinghua.edu.cn} +\quad|\quad +\href{https://yoursite.com}{yoursite.com} +\quad|\quad +ORCID: \href{https://orcid.org/0000-0000-0000-0000}{0000-0000-0000-0000} +}\par +\end{center} + +\section{Research Interests} +Large-scale distributed systems, graph neural networks, efficient inference for large language models, and privacy-preserving machine learning. + +\section{Education} +\cventry{Ph.D. in Computer Science}{2017 -- 2022}{Stanford University}{Stanford, CA} +Advisor: Prof.\ Jane Smith\\ +Dissertation: \textit{Scalable Graph Processing in Heterogeneous Computing Environments} + +\smallskip +\cventry{B.Eng. in Software Engineering}{2013 -- 2017}{Zhejiang University}{Hangzhou, China} +First Class Honours, GPA: 3.9/4.0 + +\section{Academic Appointments} +\cventry{Assistant Professor}{2022 -- Present}{Tsinghua University, Dept.\ of CS}{Beijing} +\cventry{Postdoctoral Researcher}{2022}{Stanford University, InfoLab}{Stanford, CA} + +\section{Selected Publications} + +\subsection{Journal Articles} +\begin{enumerate}[label={[J\arabic*]}] +\item \textbf{Your Name}, A.\ Coauthor, and B.\ Coauthor. ``Title of journal paper.'' \textit{IEEE Transactions on Knowledge and Data Engineering}, vol.\ 35, no.\ 4, pp.\ 1234--1248, 2023. +\item C.\ Coauthor and \textbf{Your Name}. ``Another journal paper.'' \textit{ACM Computing Surveys}, vol.\ 55, no.\ 2, pp.\ 1--35, 2023. +\end{enumerate} + +\subsection{Conference Papers} +\begin{enumerate}[label={[C\arabic*]}] +\item \textbf{Your Name} and D.\ Coauthor. ``Title of conference paper.'' In \textit{Proc.\ NeurIPS 2023}, pp.\ 5678--5690. +\item \textbf{Your Name}, E.\ Coauthor, and F.\ Coauthor. ``Another conference paper.'' In \textit{Proc.\ ICML 2022}, pp.\ 3456--3468. +\end{enumerate} + +\section{Grants \& Funding} +\cventry{NSFC Young Scientist Fund}{2023 -- 2025}{Principal Investigator}{¥300,000} +Project: Efficient Graph Neural Network Training on Heterogeneous Hardware + +\section{Teaching} +\cventry{CS101: Introduction to Programming}{Fall 2022, 2023}{Tsinghua University}{120 students} +\cventry{CS502: Advanced Distributed Systems}{Spring 2023}{Tsinghua University}{45 students} + +\section{Professional Service} +\begin{itemize} +\item \textbf{Program Committee}: NeurIPS 2023, ICML 2023, KDD 2022--2023 +\item \textbf{Reviewer}: IEEE TKDE, ACM TODS, VLDB Journal +\item \textbf{Session Chair}: SIGMOD 2023 (Graph Processing) +\end{itemize} + +\section{Awards \& Honours} +\begin{itemize} +\item Best Paper Award, KDD 2022 +\item Stanford Graduate Fellowship, 2017--2019 +\item National Scholarship, Ministry of Education, China, 2016 +\end{itemize} + +\section{Skills} +\textbf{Programming:} Python, C++, Rust, CUDA\\ +\textbf{Frameworks:} PyTorch, TensorFlow, Apache Spark, DGL\\ +\textbf{Languages:} Chinese (native), English (fluent), Japanese (basic) + +\hypersetup{ + pdftitle={CV - Your Name}, + pdfauthor={Z.ai}, + pdfsubject={Academic Curriculum Vitae}, +} + +\end{document} diff --git a/skills/pdf/references/resume-altacv.tex b/skills/pdf/references/resume-altacv.tex new file mode 100755 index 0000000..4edf73a --- /dev/null +++ b/skills/pdf/references/resume-altacv.tex @@ -0,0 +1,190 @@ +% ═══════════════════════════════════════════════════════════ +% AltaCV-Style Creative Resume Template +% Based on AltaCV by LianTze Lim, adapted for Tectonic +% ═══════════════════════════════════════════════════════════ +\documentclass[10pt,a4paper]{article} + +% ── Packages ── +\usepackage[margin=1.25cm,columnsep=1.2cm]{geometry} +\usepackage{paracol} % dual-column that breaks across pages +\usepackage[fixed]{fontawesome5} +\usepackage{tikz} +\usepackage{xcolor} +\usepackage{enumitem} +\usepackage{graphicx} +\usepackage{dashrule} +\usepackage[colorlinks=true,urlcolor=accent,linkcolor=accent,bookmarks=false]{hyperref} + +% ── Colour System (change these to retheme) ── +\definecolor{accent}{HTML}{3B82F6} % blue — primary accent +\definecolor{emphasis}{HTML}{2E2E2E} % near-black — job titles, strong text +\definecolor{heading}{HTML}{1E293B} % dark slate — section headings +\definecolor{headingrule}{HTML}{3B82F6} % matches accent +\definecolor{body}{HTML}{4B5563} % gray-600 — body text +\definecolor{tagbg}{HTML}{EFF6FF} % light blue — tag background +\definecolor{subheading}{HTML}{3B82F6} % matches accent + +% ── Fonts ── +\usepackage[rm]{roboto} +\usepackage[defaultsans]{lato} +\renewcommand{\familydefault}{\sfdefault} + +% ── List Settings ── +\setlist{leftmargin=*,labelsep=0.5em,nosep,itemsep=0.25\baselineskip,after=\vspace{0.25\baselineskip}} +\setlist[itemize]{label={\small\textbullet}} + +\setlength{\parindent}{0pt} + +% ── Custom Commands ── + +% Divider (dashed line between entries) +\newcommand{\divider}{\textcolor{body!30}{\hdashrule{\linewidth}{0.6pt}{0.5ex}}\medskip} + +% Section heading with rule +\newcommand{\cvsection}[1]{% + \nointerlineskip\bigskip% + {\color{heading}\Large\bfseries\MakeUppercase{#1}}\\[-1ex]% + {\color{headingrule}\rule{\linewidth}{2pt}\par}\medskip +} + +% Experience entry: {title}{company}{dates}{location} +\newcommand{\cvevent}[4]{% + {\large\color{emphasis}\bfseries #1\par}% + \smallskip\normalsize + \ifx&\else{\textbf{\color{accent}#2}\par\smallskip}\fi% + \ifx&\else{% + \small\makebox[0.5\linewidth][l]{\faCalendar[regular]~#3}% + }\fi% + \ifx&\else{% + \small\makebox[0.5\linewidth][l]{\faMapMarker~#4}% + }\fi\par% + \medskip\normalsize +} + +% Skill rating dots: {name}{level 1-5, supports X.5} +\newcommand{\cvskill}[2]{% + \textcolor{emphasis}{\textbf{#1}}\hfill + \foreach \x in {1,...,5}{% + \ifdimgreater{\x pt}{#2 pt}{\color{body!30}}{\color{accent}}\faCircle + }\par% +} + +% Tag label (rounded rectangle) +\newcommand{\cvtag}[1]{% + \tikz[baseline]\node[anchor=base,draw=body!30,rounded corners,inner xsep=1ex,inner ysep=0.75ex,text height=1.5ex,text depth=.25ex]{#1};% +} + +% Achievement: {icon}{title}{description} +\newcommand{\cvachievement}[3]{% + \begin{tabular}{@{}p{2em} @{\hspace{1ex}} p{\dimexpr\linewidth-3em}@{}} + {\Large\color{accent}#1} & \textbf{\color{emphasis}#2}\\ + & {\small #3} + \end{tabular}% + \smallskip +} + +% ═══════════════════════════════════════════════════════════ +\begin{document} + +% ── Header ── +\begin{center} +{\Huge\bfseries\color{heading} YOUR NAME}\par\medskip +{\large\bfseries\color{accent} Full-Stack Engineer | Open Source Contributor}\par\medskip +{\footnotesize\bfseries +\faAt~\href{mailto:name@email.com}{name@email.com}\hspace{2em} +\faPhone~+86 138-0000-0000\hspace{2em} +\faGithub~\href{https://github.com/username}{username}\hspace{2em} +\faLinkedin~\href{https://linkedin.com/in/username}{username}\hspace{2em} +\faMapMarker~Shanghai, China +}\par +\end{center} + +\medskip + +% ── Two Columns ── +\columnratio{0.62} +\begin{paracol}{2} + +% ════════ LEFT COLUMN (Experience + Projects) ════════ +\cvsection{Experience} + +\cvevent{Senior Software Engineer}{Zhipu AI}{Jan 2022 -- Present}{Beijing} +\begin{itemize} +\item Architected real-time inference pipeline serving 10M+ daily requests with p99 latency < 50ms +\item Led migration from monolith to microservices, reducing deployment cycle from weekly to hourly +\item Designed A/B testing framework enabling 30\% faster feature iteration across 5 product teams +\end{itemize} + +\divider + +\cvevent{Software Engineer}{ByteDance}{Jul 2019 -- Dec 2021}{Beijing} +\begin{itemize} +\item Built recommendation engine (collaborative filtering + deep learning) improving CTR by 25\% +\item Implemented distributed training pipeline on 64-GPU cluster, reducing training time by 60\% +\end{itemize} + +\cvsection{Projects} + +\cvevent{Open Source Project}{\href{https://github.com/user/project}{github.com/user/project}}{}{} +\begin{itemize} +\item High-performance async HTTP framework — 5,000+ GitHub stars +\item Featured in Awesome-Go and HackerNews front page +\end{itemize} + +% ════════ RIGHT COLUMN (Education + Skills + Languages) ════════ +\switchcolumn + +\cvsection{Education} + +\cvevent{M.Sc. Computer Science}{Tsinghua University}{2017 -- 2019}{} +Thesis: Distributed Graph Processing\\ +GPA: 3.8/4.0 + +\divider + +\cvevent{B.Eng. Software Engineering}{Zhejiang University}{2013 -- 2017}{} + +\cvsection{Skills} + +\cvskill{Python}{5} +\divider +\cvskill{Go / Rust}{4} +\divider +\cvskill{System Design}{4.5} +\divider +\cvskill{Machine Learning}{3.5} + +\medskip + +\cvtag{Kubernetes} +\cvtag{Docker} +\cvtag{Kafka}\\ +\cvtag{PostgreSQL} +\cvtag{Redis} +\cvtag{gRPC}\\ +\cvtag{React} +\cvtag{TypeScript} +\cvtag{Terraform} + +\cvsection{Languages} + +\cvskill{English}{4.5} +\divider +\cvskill{Chinese (Native)}{5} + +\cvsection{Highlights} + +\cvachievement{\faTrophy}{ACM ICPC Regional Gold}{2016 Asia Regional Contest} +\divider +\cvachievement{\faGithub}{5,000+ Stars OSS}{Maintainer of popular async framework} + +\end{paracol} + +% ── PDF Metadata ── +\hypersetup{ + pdftitle={Resume - Your Name}, + pdfauthor={Z.ai}, + pdfsubject={Professional Resume}, +} + +\end{document} diff --git a/skills/pdf/scripts/cover_validate.js b/skills/pdf/scripts/cover_validate.js new file mode 100755 index 0000000..89b77a7 --- /dev/null +++ b/skills/pdf/scripts/cover_validate.js @@ -0,0 +1,367 @@ +#!/usr/bin/env node +/** + * cover_validate.js — Cover page overlap detection via Playwright rendering + * + * Detects text-vs-decorative-line overlap on cover HTML pages by: + * 1. Rendering the HTML in Playwright + * 2. Waiting for fonts to load + * 3. Measuring bounding boxes of text elements and decorative line elements + * 4. Checking for Y-axis overlap (minimum spacing = 1U = 5% of page width ≈ 30pt) + * + * Usage: + * node cover_validate.js cover.html + * node cover_validate.js cover.html --width 210mm --height 297mm + * node cover_validate.js cover.html --min-gap 30 # custom min gap in px (default: auto = 5% of width) + * + * Exit codes: + * 0 = no overlap issues found + * 1 = overlap detected (prints details to stderr) + * 2 = script error (missing file, browser launch failure, etc.) + * + * This script is ONLY for cover pages. Do NOT use it on: + * - Multi-page documents (use html2pdf-next.js pre-render checks) + * - Posters (use html2poster.js which handles overflow automatically) + */ + +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +// ── Playwright import ── + +let playwright; +try { + playwright = require('playwright'); +} catch { + try { + playwright = require('playwright-core'); + } catch { + console.error('✗ Neither playwright nor playwright-core is installed.'); + process.exit(2); + } +} + +// ── Chromium resolution (shared logic with html2poster.js) ── + +function resolveChromium(chromiumObj) { + let exe; + try { exe = chromiumObj.executablePath(); } catch (_) { exe = null; } + if (exe && fs.existsSync(exe)) return { status: 'ok', executablePath: exe }; + + const candidates = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/usr/bin/chromium-browser', '/usr/bin/chromium', '/usr/bin/google-chrome', + ]; + if (process.env.PLAYWRIGHT_CHROMIUM_PATH) candidates.unshift(process.env.PLAYWRIGHT_CHROMIUM_PATH); + + for (const c of candidates) { + if (fs.existsSync(c)) return { status: 'fallback', executablePath: c }; + } + return { status: 'missing', executablePath: exe || '' }; +} + +// ── CLI parsing ── + +function parseArgs(argv) { + const tokens = argv.slice(2); + let input = null, width = '210mm', height = '297mm', minGap = null; + + for (let i = 0; i < tokens.length; i++) { + const t = tokens[i]; + if (t === '--width') width = tokens[++i]; + else if (t === '--height') height = tokens[++i]; + else if (t === '--min-gap') minGap = parseFloat(tokens[++i]); + else if (t === '--help' || t === '-h') { + console.log(`Usage: node cover_validate.js <cover.html> [options] + +Options: + --width <val> Page width (default: 210mm) + --height <val> Page height (default: 297mm) + --min-gap <px> Minimum gap between text and decorative lines (default: 5% of width) + --help Show this help`); + process.exit(0); + } else if (!t.startsWith('-') && !input) { + input = t; + } + } + return { input, width, height, minGap }; +} + +// ── Convert CSS dimension string to px for viewport ── + +function dimToPx(dim) { + if (!dim) return null; + const s = String(dim).trim(); + const num = parseFloat(s); + if (s.endsWith('mm')) return Math.round(num * 3.7795); // 1mm ≈ 3.7795px at 96dpi + if (s.endsWith('cm')) return Math.round(num * 37.795); + if (s.endsWith('in')) return Math.round(num * 96); + if (s.endsWith('px') || !isNaN(num)) return Math.round(num); + return null; +} + +// ── Decorative line detection heuristics ── +// A decorative line is an element that: +// - Is very thin in one dimension (height ≤ 5px or width ≤ 5px) +// - OR is an <hr> element +// - OR has a large aspect ratio (> 10:1 or < 1:10) +// - AND is not inside a text element + +const DECORATIVE_LINE_DETECTION = ` +(function detectOverlaps(minGapPx) { + // Collect all elements + const allElements = document.querySelectorAll('*'); + + const textElements = []; + const lineElements = []; + + // Classify elements + for (const el of allElements) { + const rect = el.getBoundingClientRect(); + if (rect.width === 0 || rect.height === 0) continue; + + const tag = el.tagName.toLowerCase(); + const style = getComputedStyle(el); + + // Skip invisible elements + if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue; + + // Detect decorative lines + const isHR = tag === 'hr'; + const isThinH = rect.height <= 5 && rect.width > 20; // thin horizontal line + const isThinV = rect.width <= 5 && rect.height > 20; // thin vertical line + const aspectH = rect.width / rect.height; + const aspectV = rect.height / rect.width; + const isWideRatio = aspectH > 15 && rect.height <= 8; // very wide, very thin + const isTallRatio = aspectV > 15 && rect.width <= 8; // very tall, very thin + + // Check if element has only border (no text content, no background image) + const hasOnlyBorder = ( + el.textContent.trim() === '' && + style.backgroundImage === 'none' && + (style.borderTopWidth !== '0px' || style.borderBottomWidth !== '0px' || + style.borderLeftWidth !== '0px' || style.borderRightWidth !== '0px') + ); + const isBorderLine = hasOnlyBorder && (rect.height <= 8 || rect.width <= 8); + + if (isHR || isThinH || isThinV || isWideRatio || isTallRatio || isBorderLine) { + lineElements.push({ + tag: tag, + class: el.className || '', + rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + type: isThinH || isWideRatio ? 'horizontal' : (isThinV || isTallRatio ? 'vertical' : (rect.width >= rect.height ? 'horizontal' : 'vertical')), + }); + continue; + } + + // Detect text elements (has direct text content or is a heading/paragraph) + const textTags = ['h1','h2','h3','h4','h5','h6','p','span','a','li','td','th','label','summary']; + const hasDirectText = Array.from(el.childNodes).some(n => n.nodeType === 3 && n.textContent.trim()); + + if (textTags.includes(tag) || hasDirectText) { + // Skip if this is inside a decorative element + if (rect.height < 3) continue; + + textElements.push({ + tag: tag, + class: el.className || '', + text: el.textContent.trim().substring(0, 60), + rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, + }); + } + } + + // De-duplicate: if a parent and child text element both overlap the same line, + // only keep the more specific (smaller) one to avoid duplicate reports. + // Sort text elements by area (smallest first) so we can skip parents. + textElements.sort((a, b) => (a.rect.width * a.rect.height) - (b.rect.width * b.rect.height)); + + // Check overlaps between text elements and line elements + const overlaps = []; + const reportedPairs = new Set(); // track "lineIndex:textContent" to deduplicate + + for (const text of textElements) { + for (const line of lineElements) { + const tr = text.rect; + const lr = line.rect; + + if (line.type === 'horizontal') { + // Check vertical overlap/proximity + const textTop = tr.y; + const textBottom = tr.y + tr.height; + const lineTop = lr.y; + const lineBottom = lr.y + lr.height; + + // Check horizontal overlap (they must share some X range) + const xOverlap = !(tr.x + tr.width < lr.x || lr.x + lr.width < tr.x); + if (!xOverlap) continue; + + // Calculate vertical gap + let vGap; + if (lineTop >= textBottom) { + vGap = lineTop - textBottom; // line is below text + } else if (textTop >= lineBottom) { + vGap = textTop - lineBottom; // line is above text + } else { + vGap = 0; // overlapping + } + + if (vGap < minGapPx) { + // De-dup: same line region, only report the smallest (most specific) text element + const lineKey = 'h:' + Math.round(lr.x) + ',' + Math.round(lr.y); + if (!reportedPairs.has(lineKey)) { + reportedPairs.add(lineKey); + overlaps.push({ + text: text.text, + textTag: text.tag, + textClass: text.class, + textRect: tr, + lineTag: line.tag, + lineClass: line.class, + lineRect: lr, + lineType: line.type, + gap: Math.round(vGap * 10) / 10, + required: minGapPx, + }); + } + } + } else if (line.type === 'vertical') { + // Check horizontal overlap/proximity + const textLeft = tr.x; + const textRight = tr.x + tr.width; + const lineLeft = lr.x; + const lineRight = lr.x + lr.width; + + // Check vertical overlap (they must share some Y range) + const yOverlap = !(tr.y + tr.height < lr.y || lr.y + lr.height < tr.y); + if (!yOverlap) continue; + + // Calculate horizontal gap + let hGap; + if (lineLeft >= textRight) { + hGap = lineLeft - textRight; + } else if (textLeft >= lineRight) { + hGap = textLeft - lineRight; + } else { + hGap = 0; + } + + if (hGap < minGapPx) { + const lineKey = 'v:' + Math.round(lr.x) + ',' + Math.round(lr.y); + if (!reportedPairs.has(lineKey)) { + reportedPairs.add(lineKey); + overlaps.push({ + text: text.text, + textTag: text.tag, + textClass: text.class, + textRect: tr, + lineTag: line.tag, + lineClass: line.class, + lineRect: lr, + lineType: line.type, + gap: Math.round(hGap * 10) / 10, + required: minGapPx, + }); + } + } + } + } + } + + return { + textElements: textElements.length, + lineElements: lineElements.length, + overlaps: overlaps, + }; +}) +`; + +// ── Main ── + +async function main() { + const { input, width, height, minGap } = parseArgs(process.argv); + + if (!input) { + console.error('✗ No input file specified. Usage: node cover_validate.js cover.html'); + process.exit(2); + } + + const absIn = path.resolve(input); + if (!fs.existsSync(absIn)) { + console.error(`✗ File not found: ${absIn}`); + process.exit(2); + } + + const widthPx = dimToPx(width) || 794; // A4 width in px + const heightPx = dimToPx(height) || 1123; // A4 height in px + const gap = minGap || Math.round(widthPx * 0.05); // 1U = 5% of page width + + console.log(`🔍 cover_validate — Cover overlap detection`); + console.log(` Input: ${absIn}`); + console.log(` Page: ${widthPx}×${heightPx}px`); + console.log(` Min gap: ${gap}px (1U)`); + + const { chromium } = playwright; + const bInfo = resolveChromium(chromium); + + if (bInfo.status === 'missing') { + console.error('✗ No Chromium found. Install via: npx playwright install chromium'); + process.exit(2); + } + + let browser; + try { + const opts = { headless: true }; + if (bInfo.status === 'fallback') opts.executablePath = bInfo.executablePath; + browser = await chromium.launch(opts); + } catch (err) { + console.error(`✗ Browser launch failed: ${err.message}`); + process.exit(2); + } + + try { + const page = await browser.newPage({ viewport: { width: widthPx, height: heightPx } }); + await page.goto('file://' + absIn, { waitUntil: 'networkidle' }); + console.log(` ✓ HTML loaded`); + + // Wait for fonts + const fontsLoaded = await page.evaluate(() => + document.fonts.ready.then(() => document.fonts.size) + ).catch(() => 0); + console.log(` ✓ Fonts: ${fontsLoaded} loaded`); + + // Run overlap detection + const result = await page.evaluate(`(${DECORATIVE_LINE_DETECTION})(${gap})`); + + console.log(` ✓ Found ${result.textElements} text elements, ${result.lineElements} decorative lines`); + + if (result.overlaps.length === 0) { + console.log(`\n ✅ No overlap issues found`); + process.exit(0); + } + + // Report overlaps + console.error(`\n ❌ Found ${result.overlaps.length} text-line overlap(s):\n`); + + for (const o of result.overlaps) { + const direction = o.lineType === 'vertical' ? 'horizontal' : 'vertical'; + console.error(` ERROR: ${direction} gap = ${o.gap}px (required ≥ ${o.required}px)`); + console.error(` Text: <${o.textTag}> "${o.text}" @ y=${Math.round(o.textRect.y)}-${Math.round(o.textRect.y + o.textRect.height)}`); + console.error(` Line: <${o.lineTag}${o.lineClass ? '.' + o.lineClass.split(' ')[0] : ''}> [${o.lineType}] @ y=${Math.round(o.lineRect.y)}-${Math.round(o.lineRect.y + o.lineRect.height)}`); + console.error(` Fix: Move the decorative line at least ${Math.ceil(o.required - o.gap)}px away from the text.`); + console.error(''); + } + + process.exit(1); + + } finally { + await browser.close(); + } +} + +main().catch(err => { + console.error(`✗ Unexpected error: ${err.message}`); + process.exit(2); +}); diff --git a/skills/pdf/scripts/design_engine.py b/skills/pdf/scripts/design_engine.py new file mode 100755 index 0000000..bcbcccd --- /dev/null +++ b/skills/pdf/scripts/design_engine.py @@ -0,0 +1,2816 @@ +#!/usr/bin/env python3 +""" +design_engine.py — Aesthetic computation engine for art-direction-first PDF production. + +Philosophy: The LLM handles narrative and composition; this script handles the math +that models get wrong — precise color harmony, algorithmic SVG, and spatial tension. + +Three core functions: + 1. generate_color_palette(intent) — HSL-locked low-saturation palettes + 2. generate_generative_svg(svg_type) — Algorithmic art backgrounds + 3. calculate_layout(elements) — Deliberate offset/overlap coordinates + +Usage: + python3 design_engine.py palette --intent calm --mode dark + python3 design_engine.py palette --intent tension --mode light + python3 design_engine.py svg --intent flow --dimensions 720x960 + python3 design_engine.py svg --intent grid --dimensions 720x960 + python3 design_engine.py layout --elements hero,body,meta --dimensions 720x960 --style offset + python3 design_engine.py full --intent energy --mode dark --dimensions 720x960 --output-dir ./assets/ +""" + +import argparse +import colorsys +import json +import math +import os +import random +import sys + +# ═══════════════════════════════════════════════════════════════════════ +# 1. COLOR PALETTE — HSL with Saturation Clamped to [0.05, 0.25] +# ═══════════════════════════════════════════════════════════════════════ +# +# Core constraint: Saturation MUST be between 0.05 and 0.25. +# This eliminates candy-colored, web-app-looking outputs and locks +# everything into the "high-end grey" (高级灰) tonal range. +# +# Two modes: +# - Dark: background L < 0.10, text L > 0.85 +# - Light: background L > 0.95, text L < 0.15 +# The "muddy middle" (L between 0.30 and 0.70) is forbidden for backgrounds. + +# Intent → base hue mapping (degrees on the HSL wheel) +# 5 intents: Calm, Tension, Energy, Authority, Warmth +# Plus utility intents (nature, cold, neutral) for keyword auto-derive +INTENT_HUES = { + "calm": 210, # Steel blue-grey, low saturation (merges old serenity+minimalism) + "tension": 0, # Warm near-black vs cold + "energy": 30, # Amber undertone + "authority": 280, # Muted violet, formal/premium (replaces old elegance) + "warmth": 20, # Terracotta undertone + # Utility intents (for keyword auto-derive, not exposed in UI) + "nature": 150, # Desaturated sage + "cold": 200, # Slate blue + "neutral": 45, # Warm grey + # Legacy aliases (backward compatibility) + "serenity": 210, # → calm + "elegance": 280, # → authority + "minimalism": 0, # → calm (achromatic variant) +} + +# Theme keywords → intent mapping (for auto-derive from document description) +# When the user doesn't specify an intent, scan the document title/description +# for these keywords and map to the closest intent. +THEME_KEYWORDS = { + # Technology / Data / Analytics + "tech": "cold", "数据": "cold", "data": "cold", "AI": "cold", + "科技": "cold", "digital": "cold", "analytics": "cold", "分析": "cold", + # Nature / Environment / Sustainability + "green": "nature", "绿色": "nature", "环保": "nature", "eco": "nature", + "sustainability": "nature", "生态": "nature", "forest": "nature", + # Business / Finance / Corporate + "report": "neutral", "报告": "neutral", "finance": "neutral", "财务": "neutral", + "annual": "neutral", "年度": "neutral", "corporate": "neutral", + # Creative / Marketing / Social + "marketing": "energy", "运营": "energy", "social": "energy", "品牌": "energy", + "campaign": "energy", "活动": "energy", "launch": "energy", + # Authority / Formal / Premium / Luxury + "luxury": "authority", "奢华": "authority", "fashion": "authority", "时尚": "authority", + "premium": "authority", "高端": "authority", "gala": "authority", + "formal": "authority", "正式": "authority", "professional": "authority", "专业": "authority", + "government": "authority", "政府": "authority", "bidding": "authority", "投标": "authority", + "政府报告": "authority", "政府文书": "authority", "公文": "authority", + "thesis": "authority", "毕业论文": "authority", "dissertation": "authority", + "开题": "authority", "开题报告": "authority", "proposal": "authority", "学位": "authority", + # Calm / Meditation / Healthcare / Minimalist + "health": "calm", "健康": "calm", "meditation": "calm", + "wellness": "calm", "calm": "calm", "医疗": "calm", + "minimalist": "calm", "极简": "calm", "simple": "calm", "简约": "calm", + # Urgent / Warning / Emergency + "urgent": "tension", "warning": "tension", "紧急": "tension", + "alert": "tension", "crisis": "tension", + # Warm / Food / Lifestyle + "food": "warmth", "美食": "warmth", "lifestyle": "warmth", + "生活": "warmth", "home": "warmth", "家居": "warmth", +} + + +def derive_intent(text): + """ + Auto-derive design intent from document title/description. + Scans for theme keywords and returns the best-matching intent. + Falls back to 'neutral' if no keywords match. + + Usage: + python3 design_engine.py derive "Social Media Operations Monthly Report" → energy + python3 design_engine.py derive "2025 Annual Sustainability Report" → nature + """ + text_lower = text.lower() + scores = {} + for keyword, intent in THEME_KEYWORDS.items(): + if keyword.lower() in text_lower: + scores[intent] = scores.get(intent, 0) + 1 + if not scores: + return "neutral" + # When tied, prefer specific intents over 'neutral' (which is a generic fallback) + max_score = max(scores.values()) + top_intents = [k for k, v in scores.items() if v == max_score] + if len(top_intents) > 1 and "neutral" in top_intents: + top_intents.remove("neutral") + return top_intents[0] + +# Intent → recommended harmony mapping (reduces LLM decision burden) +# Used as fallback when LLM doesn't specify color_harmony +INTENT_HARMONY_MAP = { + "calm": "analogous", # peaceful, flowing transition (merges serenity+minimalism) + "tension": "complementary", # maximum visual conflict + "energy": "triadic", # vibrant, multi-directional + "authority": "split_complementary", # sophisticated, formal (replaces elegance) + "warmth": "analogous", # natural, earthy cohesion + # Utility intents + "nature": "analogous", # organic harmony + "cold": "split_complementary", # icy precision with subtle warmth + "neutral": "split_complementary", # safe default, still interesting + # Legacy aliases + "serenity": "analogous", + "elegance": "split_complementary", + "minimalism": "monochrome", +} + +def _hsl_to_hex(h, s, l): + """Convert HSL (h: 0-360, s: 0-1, l: 0-1) to hex string.""" + r, g, b = colorsys.hls_to_rgb(h / 360.0, l, s) + return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}" + + +# Muddy / ugly hue zones that produce unattractive accent colors at medium saturation. +# These hue ranges tend to look "dirty" or "sickly" — yellow-green, brown-orange, etc. +# When an accent lands here, we nudge it to the nearest attractive neighbor. +_UGLY_HUE_ZONES = [ + # (start, end, nudge_low, nudge_high) — hues that look muddy + # at S 0.4-0.7, L 0.35-0.55. When accent lands here, redirect. + (28, 105, 15, 120), # Dirty yellow/brown/olive/yellow-green zone + # nudge_low=15 (warm red-orange), nudge_high=120 (true green) +] + + +def _sanitize_accent_hue(hue): + """Nudge accent hue away from muddy/ugly zones toward attractive neighbors.""" + hue = hue % 360 + for start, end, nudge_low, nudge_high in _UGLY_HUE_ZONES: + if start <= hue <= end: + mid = (start + end) / 2 + return nudge_low if hue < mid else nudge_high + return hue + +def _hex_to_rgb(hex_str): + """Convert hex to 'r,g,b' string for rgba() usage.""" + hex_str = hex_str.lstrip('#') + return f"{int(hex_str[0:2], 16)},{int(hex_str[2:4], 16)},{int(hex_str[4:6], 16)}" + +def _relative_luminance(hex_str): + """WCAG 2.1 relative luminance from hex color.""" + hex_str = hex_str.lstrip('#') + channels = [] + for i in (0, 2, 4): + c = int(hex_str[i:i+2], 16) / 255.0 + channels.append(c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4) + return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2] + +def _contrast_ratio(hex1, hex2): + """WCAG contrast ratio between two hex colors.""" + l1, l2 = _relative_luminance(hex1), _relative_luminance(hex2) + lighter, darker = max(l1, l2), min(l1, l2) + return (lighter + 0.05) / (darker + 0.05) + +def generate_color_palette(intent="neutral", mode="minimal", harmony=None, seed=None): + """ + Generative Color Harmony Engine — geometric accent computation + 5 aesthetic modes. + + LLM is FORBIDDEN from specifying HEX/RGB. It only selects: + - intent → base hue (from INTENT_HUES) + - mode → S/L physical boundaries (minimal/dark/pastel/jewel/light) + - harmony → accent hue geometry (auto-recommended from intent if omitted) + + Returns dict with: + - bg: 60% — dominant ground + - mid: 30% — structural shade + - accent: 10% — geometric harmony emphasis + - text: primary text color + - muted: secondary/caption text + - surface: card/container background (translucent) + """ + if seed is not None: + random.seed(seed) + + # Auto-recommend harmony from intent if not specified + if harmony is None or harmony == "auto": + harmony = INTENT_HARMONY_MAP.get(intent, "split_complementary") + + base_hue = INTENT_HUES.get(intent, random.randint(0, 359)) + + # 1. Geometric Accent Hue Derivation + if harmony == "complementary": + accent_hue = (base_hue + 180) % 360 + elif harmony == "split_complementary": + accent_hue = (base_hue + random.choice([150, 210])) % 360 + elif harmony == "triadic": + accent_hue = (base_hue + random.choice([120, 240])) % 360 + elif harmony == "analogous": + accent_hue = (base_hue + random.choice([30, -30, 45, -45])) % 360 + else: # monochrome + accent_hue = base_hue + + # Sanitize accent hue — avoid muddy yellow-green and brown-orange zones + accent_hue = _sanitize_accent_hue(accent_hue) + + # 2. Five Aesthetic Modes — hard-lock S and L boundaries + if mode == "minimal": + # DEFAULT for 50%+ of documents. Editorial paper tone + high-purity accent. + # bg has visible tint (S 0.08-0.15, L 0.92-0.96) — not dead white. + is_warm = random.choice([True, False]) + paper_hue = random.randint(35, 45) if is_warm else random.randint(200, 215) + bg = _hsl_to_hex(paper_hue, random.uniform(0.08, 0.15), random.uniform(0.92, 0.96)) + mid = _hsl_to_hex(paper_hue, random.uniform(0.10, 0.18), random.uniform(0.85, 0.90)) + accent = _hsl_to_hex(accent_hue, random.uniform(0.55, 0.72), random.uniform(0.42, 0.52)) + text = _hsl_to_hex(paper_hue, 0.05, random.uniform(0.10, 0.15)) + muted = _hsl_to_hex(paper_hue, 0.05, random.uniform(0.45, 0.55)) + surface = f"rgba(0,0,0,{random.uniform(0.01, 0.03):.2f})" + + elif mode == "dark": + # Cyber/tech: ultra-low saturation, ultra-low lightness + bg = _hsl_to_hex(base_hue, random.uniform(0.04, 0.10), random.uniform(0.04, 0.08)) + mid = _hsl_to_hex(base_hue, random.uniform(0.08, 0.15), random.uniform(0.12, 0.20)) + accent = _hsl_to_hex(accent_hue, random.uniform(0.45, 0.60), random.uniform(0.52, 0.62)) + text = _hsl_to_hex(base_hue, 0.05, random.uniform(0.88, 0.95)) + muted = _hsl_to_hex(base_hue, 0.05, random.uniform(0.45, 0.55)) + surface = f"rgba(255,255,255,{random.uniform(0.03, 0.06):.2f})" + + elif mode == "pastel": + # Morandi/macaron: medium-low saturation, high lightness + # Accent tightened: S capped at 0.50 to avoid clashing with tinted bg + bg = _hsl_to_hex(base_hue, random.uniform(0.15, 0.35), random.uniform(0.85, 0.92)) + mid = _hsl_to_hex(base_hue, random.uniform(0.20, 0.40), random.uniform(0.75, 0.82)) + accent = _hsl_to_hex(accent_hue, random.uniform(0.38, 0.50), random.uniform(0.38, 0.48)) + text = _hsl_to_hex(base_hue, 0.15, random.uniform(0.15, 0.25)) + muted = _hsl_to_hex(base_hue, 0.20, random.uniform(0.45, 0.55)) + surface = f"rgba(255,255,255,{random.uniform(0.30, 0.50):.2f})" + + elif mode == "jewel": + # Gem/luxury: medium-high saturation bg, accent must NOT compete — lower S, higher L + bg = _hsl_to_hex(base_hue, random.uniform(0.40, 0.60), random.uniform(0.15, 0.25)) + mid = _hsl_to_hex(base_hue, random.uniform(0.40, 0.60), random.uniform(0.25, 0.35)) + accent = _hsl_to_hex(accent_hue, random.uniform(0.30, 0.50), random.uniform(0.65, 0.80)) + text = _hsl_to_hex(base_hue, 0.10, random.uniform(0.90, 0.96)) + muted = _hsl_to_hex(base_hue, 0.20, random.uniform(0.60, 0.70)) + surface = f"rgba(0,0,0,{random.uniform(0.20, 0.40):.2f})" + + else: + # light — noticeably tinted, not dead white (S 0.10-0.20, L 0.91-0.95) + bg = _hsl_to_hex(base_hue, random.uniform(0.10, 0.20), random.uniform(0.91, 0.95)) + mid = _hsl_to_hex(base_hue, random.uniform(0.15, 0.25), random.uniform(0.84, 0.90)) + accent = _hsl_to_hex(accent_hue, random.uniform(0.35, 0.45), random.uniform(0.35, 0.45)) + text = _hsl_to_hex(base_hue, 0.08, random.uniform(0.08, 0.15)) + muted = _hsl_to_hex(base_hue, 0.05, random.uniform(0.45, 0.55)) + surface = f"rgba(0,0,0,{random.uniform(0.02, 0.05):.2f})" + + # 3. WCAG Contrast Safety Net — ensure text:bg ratio ≥ 4.5:1 + text_cr = _contrast_ratio(text, bg) + if text_cr < 4.5: + # Push text darker (light modes) or lighter (dark modes) + r, g, b = (int(bg.lstrip('#')[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + _, bg_l, _ = colorsys.rgb_to_hls(r, g, b) + if bg_l > 0.5: + text = _hsl_to_hex(base_hue, 0.08, 0.08) # force near-black + else: + text = _hsl_to_hex(base_hue, 0.05, 0.95) # force near-white + + # 4. Accent-on-bg visibility check — ensure accent stands out (ratio ≥ 3:1) + accent_cr = _contrast_ratio(accent, bg) + if accent_cr < 3.0: + # Nudge accent lightness away from bg + r, g, b = (int(bg.lstrip('#')[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + _, bg_l, _ = colorsys.rgb_to_hls(r, g, b) + target_l = 0.35 if bg_l > 0.5 else 0.70 + r2, g2, b2 = (int(accent.lstrip('#')[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + _, _, accent_s = colorsys.rgb_to_hls(r2, g2, b2) + accent = _hsl_to_hex(accent_hue, accent_s, target_l) + + return { + "bg": bg, "bg_rgb": _hex_to_rgb(bg), + "mid": mid, "mid_rgb": _hex_to_rgb(mid), + "accent": accent, "accent_rgb": _hex_to_rgb(accent), + "text": text, "muted": muted, + "surface": surface, + "meta": { + "intent": intent, "mode": mode, "harmony": harmony, + "base_hue": base_hue, "accent_hue": accent_hue, + "contrast": { + "text_on_bg": round(_contrast_ratio(text, bg), 2), + "accent_on_bg": round(_contrast_ratio(accent, bg), 2), + } + } + } + +def palette_to_css(palette): + """Convert palette dict to CSS custom properties.""" + return f""":root {{ + /* 60% ground */ + --c-bg: {palette['bg']}; + --c-bg-rgb: {palette['bg_rgb']}; + /* 30% structure */ + --c-mid: {palette['mid']}; + --c-mid-rgb: {palette['mid_rgb']}; + /* 10% emphasis */ + --c-accent: {palette['accent']}; + --c-accent-rgb: {palette['accent_rgb']}; + /* Typography */ + --c-text: {palette['text']}; + --c-muted: {palette['muted']}; + /* Surfaces */ + --c-surface: {palette['surface']}; +}}""" + + +# ═══════════════════════════════════════════════════════════════════════ +# 1b. CASCADE PALETTE — Role-Based Unified Color System +# ═══════════════════════════════════════════════════════════════════════ +# +# Iron Law: Area ∝ 1/Saturation. +# The larger the colored area, the LOWER its saturation must be. +# +# Role tiers by area usage: +# XL (>50% of page): page_bg, section_bg → S ≤ 0.08, near-white/near-black +# L (20-50%): card_bg, table_stripe → S ≤ 0.15 +# M (5-20%): header_fill, sidebar → S ≤ 0.30 +# S (1-5%): border, divider, icon → S ≤ 0.50 +# XS (<1%): accent_dot, badge, tag → S up to 0.75 (the only place high-sat lives) +# +# Every color is derived from one base hue. No orphan colors. + +# Tier saturation caps — enforced at generation AND audit time +CASCADE_TIER_CAPS = { + "xl": 0.08, # Page background, section background + "l": 0.15, # Card background, table stripe + "m": 0.30, # Header fill, sidebar, cover decorative blocks + "s": 0.50, # Borders, dividers, icons, chart grid lines + "xs": 0.75, # Accent dot, badge, tag, data point highlight +} + + +def generate_cascade_palette(intent="neutral", mode="minimal", harmony=None, seed=None): + """ + Generate a role-based palette cascade where every color is derived from one + base hue and saturation is inversely proportional to usage area. + + Returns a dict with: + roles: Complete role table (12 roles, each with hex, hsl, tier, usage_hint) + cover: Subset of roles for cover page rendering + body: Subset of roles for body content + charts: Subset of roles for data visualization + semantic: Low-saturation semantic colors (success, warning, error, info) + meta: Generation metadata (intent, mode, harmony, base_hue, audit) + css: Ready-to-use CSS custom properties + reportlab: Ready-to-paste ReportLab Python code + """ + if seed is not None: + random.seed(seed) + + if harmony is None or harmony == "auto": + harmony = INTENT_HARMONY_MAP.get(intent, "split_complementary") + + base_hue = INTENT_HUES.get(intent, random.randint(0, 359)) + + # Geometric accent hue derivation (same logic as original) + if harmony == "complementary": + accent_hue = (base_hue + 180) % 360 + elif harmony == "split_complementary": + accent_hue = (base_hue + random.choice([150, 210])) % 360 + elif harmony == "triadic": + accent_hue = (base_hue + random.choice([120, 240])) % 360 + elif harmony == "analogous": + accent_hue = (base_hue + random.choice([30, -30, 45, -45])) % 360 + else: # monochrome + accent_hue = base_hue + + # Sanitize accent hue — avoid muddy zones + accent_hue = _sanitize_accent_hue(accent_hue) + + # Secondary hue — between base and accent, for chart variety + secondary_hue = _sanitize_accent_hue( + (base_hue + accent_hue) / 2 if accent_hue != base_hue else (base_hue + 30) % 360 + ) + + # Mode-dependent lightness anchors + is_dark = mode == "dark" + if is_dark: + bg_l_range = (0.04, 0.08) + text_l = random.uniform(0.88, 0.95) + muted_l = random.uniform(0.50, 0.60) + elif mode == "jewel": + bg_l_range = (0.15, 0.25) + text_l = random.uniform(0.90, 0.96) + muted_l = random.uniform(0.60, 0.70) + else: # minimal, pastel, light + bg_l_range = (0.94, 0.97) + text_l = random.uniform(0.08, 0.15) + muted_l = random.uniform(0.45, 0.55) + + # ── Generate all 12 roles ── + roles = {} + + # XL tier: page bg, section bg + roles["page_bg"] = _make_role( + base_hue, random.uniform(0.03, 0.08), random.uniform(*bg_l_range), + "xl", "Page background, full-bleed areas") + roles["section_bg"] = _make_role( + base_hue, random.uniform(0.04, 0.08), + random.uniform(bg_l_range[0] - 0.03, bg_l_range[1] - 0.02) if not is_dark + else random.uniform(0.08, 0.14), + "xl", "Section background, alternating bands") + + # L tier: card bg, table stripe + roles["card_bg"] = _make_role( + base_hue, random.uniform(0.06, 0.14), + random.uniform(0.90, 0.94) if not is_dark else random.uniform(0.10, 0.16), + "l", "Card/container background, table even rows") + roles["table_stripe"] = _make_role( + base_hue, random.uniform(0.05, 0.12), + random.uniform(0.92, 0.96) if not is_dark else random.uniform(0.08, 0.12), + "l", "Table alternating row fill") + + # M tier: header fill, sidebar, cover decorative block + roles["header_fill"] = _make_role( + base_hue, random.uniform(0.15, 0.28), + random.uniform(0.25, 0.40) if not is_dark else random.uniform(0.20, 0.30), + "m", "Table header, sidebar background, cover top bar") + roles["cover_block"] = _make_role( + base_hue, random.uniform(0.12, 0.25), + random.uniform(0.30, 0.45) if not is_dark else random.uniform(0.15, 0.25), + "m", "Cover decorative block, sidebar pillar") + + # S tier: border, divider, icon fill + roles["border"] = _make_role( + base_hue, random.uniform(0.10, 0.25), + random.uniform(0.70, 0.82) if not is_dark else random.uniform(0.25, 0.35), + "s", "Borders, divider lines, chart grid") + roles["icon"] = _make_role( + base_hue, random.uniform(0.25, 0.45), + random.uniform(0.35, 0.50) if not is_dark else random.uniform(0.55, 0.70), + "s", "Icons, bullet points, small UI elements") + + # XS tier: accent, badge, data highlight + roles["accent"] = _make_role( + accent_hue, random.uniform(0.50, 0.70), + random.uniform(0.40, 0.55) if not is_dark else random.uniform(0.55, 0.70), + "xs", "Primary accent: badges, tags, data point highlights, CTA") + roles["accent_secondary"] = _make_role( + secondary_hue, random.uniform(0.40, 0.60), + random.uniform(0.45, 0.58) if not is_dark else random.uniform(0.50, 0.65), + "xs", "Secondary accent: chart series 2, secondary badge") + + # Typography (no tier — text is special) + roles["text_primary"] = _make_role( + base_hue, 0.05, text_l, + "text", "Primary body text") + roles["text_muted"] = _make_role( + base_hue, 0.04, muted_l, + "text", "Captions, footnotes, secondary text") + + # ── Semantic colors (derived from base hue, low-sat) ── + semantic = { + "success": _make_role(140, random.uniform(0.25, 0.40), + random.uniform(0.35, 0.45) if not is_dark else random.uniform(0.55, 0.65), + "xs", "Positive: growth, pass, complete"), + "warning": _make_role(40, random.uniform(0.30, 0.45), + random.uniform(0.40, 0.50) if not is_dark else random.uniform(0.55, 0.65), + "xs", "Caution: pending, alert"), + "error": _make_role(5, random.uniform(0.30, 0.45), + random.uniform(0.40, 0.50) if not is_dark else random.uniform(0.55, 0.65), + "xs", "Negative: decline, fail, error"), + "info": _make_role(210, random.uniform(0.25, 0.40), + random.uniform(0.40, 0.50) if not is_dark else random.uniform(0.55, 0.65), + "xs", "Informational: neutral status"), + } + + # ── WCAG contrast enforcement ── + page_bg_hex = roles["page_bg"]["hex"] + text_hex = roles["text_primary"]["hex"] + if _contrast_ratio(text_hex, page_bg_hex) < 4.5: + if not is_dark: + roles["text_primary"] = _make_role(base_hue, 0.08, 0.08, "text", "Primary body text") + else: + roles["text_primary"] = _make_role(base_hue, 0.05, 0.95, "text", "Primary body text") + + accent_hex = roles["accent"]["hex"] + if _contrast_ratio(accent_hex, page_bg_hex) < 3.0: + # Push accent lightness further from bg + r, g, b = (int(page_bg_hex.lstrip('#')[i:i+2], 16) / 255.0 for i in (0, 2, 4)) + _, bg_l, _ = colorsys.rgb_to_hls(r, g, b) + target_l = 0.35 if bg_l > 0.5 else 0.75 + roles["accent"] = _make_role(accent_hue, random.uniform(0.50, 0.70), target_l, + "xs", "Primary accent: badges, tags, data point highlights, CTA") + + # ── Tier saturation audit (clamp any violations) ── + for name, role in {**roles, **semantic}.items(): + tier = role["tier"] + if tier in CASCADE_TIER_CAPS: + cap = CASCADE_TIER_CAPS[tier] + if role["hsl"][1] > cap: + # Re-generate with capped saturation + capped_s = cap * random.uniform(0.85, 1.0) + fixed = _make_role(role["hsl"][0], capped_s, role["hsl"][2], + tier, role["usage_hint"]) + if name in roles: + roles[name] = fixed + elif name in semantic: + semantic[name] = fixed + + # ── Build convenience subsets ── + cover_subset = { + "background": roles["page_bg"]["hex"], + "top_bar": roles["header_fill"]["hex"], + "sidebar": roles["cover_block"]["hex"], + "accent_line": roles["accent"]["hex"], + "title": roles["text_primary"]["hex"], + "subtitle": roles["text_muted"]["hex"], + "watermark": roles["border"]["hex"], # low-opacity usage + } + + body_subset = { + "page_bg": roles["page_bg"]["hex"], + "section_bg": roles["section_bg"]["hex"], + "card_bg": roles["card_bg"]["hex"], + "table_header": roles["header_fill"]["hex"], + "table_stripe": roles["table_stripe"]["hex"], + "border": roles["border"]["hex"], + "heading": roles["text_primary"]["hex"], + "body_text": roles["text_primary"]["hex"], + "caption": roles["text_muted"]["hex"], + "highlight": roles["accent"]["hex"], + } + + chart_subset = { + "series_1": roles["accent"]["hex"], + "series_2": roles["accent_secondary"]["hex"], + "series_3": roles["header_fill"]["hex"], + "series_4": roles["icon"]["hex"], + "series_5": roles["cover_block"]["hex"], + "grid": roles["border"]["hex"], + "axis_text": roles["text_muted"]["hex"], + "label": roles["text_primary"]["hex"], + "up": semantic["success"]["hex"], + "down": semantic["error"]["hex"], + } + + # ── Generate outputs ── + css = _cascade_to_css(roles, semantic) + reportlab_code = _cascade_to_reportlab(roles, semantic) + + audit_results = audit_cascade_palette(roles, semantic) + + return { + "roles": {k: {"hex": v["hex"], "hsl": v["hsl"], "tier": v["tier"], "usage": v["usage_hint"]} for k, v in roles.items()}, + "cover": cover_subset, + "body": body_subset, + "charts": chart_subset, + "semantic": {k: {"hex": v["hex"], "hsl": v["hsl"]} for k, v in semantic.items()}, + "meta": { + "intent": intent, "mode": mode, "harmony": harmony, + "base_hue": base_hue, "accent_hue": accent_hue, "secondary_hue": secondary_hue, + "contrast": { + "text_on_bg": round(_contrast_ratio(roles["text_primary"]["hex"], roles["page_bg"]["hex"]), 2), + "accent_on_bg": round(_contrast_ratio(roles["accent"]["hex"], roles["page_bg"]["hex"]), 2), + }, + "audit": audit_results, + }, + "css": css, + "reportlab": reportlab_code, + } + + +def _make_role(h, s, l, tier, usage_hint): + """Create a role entry with hex, HSL tuple, tier, and usage hint.""" + hex_val = _hsl_to_hex(h, s, l) + return { + "hex": hex_val, + "hsl": (round(h, 1), round(s, 3), round(l, 3)), + "tier": tier, + "usage_hint": usage_hint, + } + + +def _cascade_to_css(roles, semantic): + """Convert cascade palette to CSS custom properties.""" + lines = [":root {"] + lines.append(" /* ── XL tier: backgrounds (S ≤ 0.08) ── */") + lines.append(f" --page-bg: {roles['page_bg']['hex']};") + lines.append(f" --section-bg: {roles['section_bg']['hex']};") + lines.append(" /* ── L tier: surfaces (S ≤ 0.15) ── */") + lines.append(f" --card-bg: {roles['card_bg']['hex']};") + lines.append(f" --table-stripe: {roles['table_stripe']['hex']};") + lines.append(" /* ── M tier: structural fills (S ≤ 0.30) ── */") + lines.append(f" --header-fill: {roles['header_fill']['hex']};") + lines.append(f" --cover-block: {roles['cover_block']['hex']};") + lines.append(" /* ── S tier: edges & icons (S ≤ 0.50) ── */") + lines.append(f" --border: {roles['border']['hex']};") + lines.append(f" --icon: {roles['icon']['hex']};") + lines.append(" /* ── XS tier: emphasis (S ≤ 0.75) ── */") + lines.append(f" --accent: {roles['accent']['hex']};") + lines.append(f" --accent-secondary: {roles['accent_secondary']['hex']};") + lines.append(" /* ── Typography ── */") + lines.append(f" --text-primary: {roles['text_primary']['hex']};") + lines.append(f" --text-muted: {roles['text_muted']['hex']};") + lines.append(" /* ── Semantic (low-sat) ── */") + for k, v in semantic.items(): + lines.append(f" --semantic-{k}: {v['hex']};") + lines.append("}") + return "\n".join(lines) + + +def _cascade_to_reportlab(roles, semantic): + """Convert cascade palette to ReportLab Python code.""" + lines = [ + "# ━━ Cascade Palette (auto-generated by design_engine.py palette-cascade) ━━", + "from reportlab.lib import colors", + "", + "# XL tier: backgrounds (area > 50%, S ≤ 0.08)", + f"PAGE_BG = colors.HexColor('{roles['page_bg']['hex']}')", + f"SECTION_BG = colors.HexColor('{roles['section_bg']['hex']}')", + "", + "# L tier: surfaces (area 20-50%, S ≤ 0.15)", + f"CARD_BG = colors.HexColor('{roles['card_bg']['hex']}')", + f"TABLE_STRIPE = colors.HexColor('{roles['table_stripe']['hex']}')", + "", + "# M tier: structural fills (area 5-20%, S ≤ 0.30)", + f"HEADER_FILL = colors.HexColor('{roles['header_fill']['hex']}')", + f"COVER_BLOCK = colors.HexColor('{roles['cover_block']['hex']}')", + "", + "# S tier: edges & icons (area 1-5%, S ≤ 0.50)", + f"BORDER = colors.HexColor('{roles['border']['hex']}')", + f"ICON = colors.HexColor('{roles['icon']['hex']}')", + "", + "# XS tier: emphasis (area < 1%, S ≤ 0.75)", + f"ACCENT = colors.HexColor('{roles['accent']['hex']}')", + f"ACCENT_2 = colors.HexColor('{roles['accent_secondary']['hex']}')", + "", + "# Typography", + f"TEXT_PRIMARY = colors.HexColor('{roles['text_primary']['hex']}')", + f"TEXT_MUTED = colors.HexColor('{roles['text_muted']['hex']}')", + "", + "# Semantic (low-saturation, area-appropriate)", + f"SEM_SUCCESS = colors.HexColor('{semantic['success']['hex']}')", + f"SEM_WARNING = colors.HexColor('{semantic['warning']['hex']}')", + f"SEM_ERROR = colors.HexColor('{semantic['error']['hex']}')", + f"SEM_INFO = colors.HexColor('{semantic['info']['hex']}')", + ] + return "\n".join(lines) + + +def audit_cascade_palette(roles, semantic): + """Audit cascade palette for tier saturation violations and WCAG contrast.""" + violations = [] + for name, role in {**roles, **semantic}.items(): + tier = role["tier"] + if tier in CASCADE_TIER_CAPS: + cap = CASCADE_TIER_CAPS[tier] + s = role["hsl"][1] + if s > cap: + violations.append(f"{name}: S={s:.3f} exceeds tier '{tier}' cap {cap}") + + # WCAG checks + bg_hex = roles["page_bg"]["hex"] + text_hex = roles["text_primary"]["hex"] + cr = _contrast_ratio(text_hex, bg_hex) + if cr < 4.5: + violations.append(f"text_primary:page_bg contrast {cr:.2f} < 4.5:1 (WCAG AA)") + + accent_hex = roles["accent"]["hex"] + cr_a = _contrast_ratio(accent_hex, bg_hex) + if cr_a < 3.0: + violations.append(f"accent:page_bg contrast {cr_a:.2f} < 3.0:1 (accent invisible)") + + # Header fill should be readable with white text on it + hf_hex = roles["header_fill"]["hex"] + cr_hf = _contrast_ratio("#ffffff", hf_hex) + if cr_hf < 3.0: + violations.append(f"white on header_fill contrast {cr_hf:.2f} < 3.0:1 (header text unreadable)") + + return violations + + +# ═══════════════════════════════════════════════════════════════════════ +# 2. GENERATIVE SVG — Algorithmic Art Backgrounds +# ═══════════════════════════════════════════════════════════════════════ +# +# Two primary modes: +# - "flow": 3-5 large-radius bézier curves at ultra-low opacity (0.05) +# Creates organic, breathing atmospheric depth +# - "grid": 1px reference grid or noise texture +# Creates structured, architectural underlying rhythm +# +# All SVG is inline-ready (no external files needed). + +def _svg_open(w, h): + return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {h}" width="{w}" height="{h}">' + +def _random_bezier_path(w, h): + """Generate a single flowing bézier curve across the canvas.""" + # Start from a random edge point + x0 = random.uniform(-w * 0.2, w * 0.3) + y0 = random.uniform(0, h) + # End at the opposite side + x3 = random.uniform(w * 0.7, w * 1.2) + y3 = random.uniform(0, h) + # Control points — large sweeps for organic feel + cx1 = random.uniform(w * 0.1, w * 0.5) + cy1 = random.uniform(-h * 0.3, h * 1.3) + cx2 = random.uniform(w * 0.5, w * 0.9) + cy2 = random.uniform(-h * 0.3, h * 1.3) + return f"M{x0:.0f},{y0:.0f} C{cx1:.0f},{cy1:.0f} {cx2:.0f},{cy2:.0f} {x3:.0f},{y3:.0f}" + +def generate_flow_svg(w, h, color="#8a8a8a", curves=4, stroke_width=80, opacity=0.05): + """ + Flow mode: ultra-wide, ultra-faint bézier curves. + Creates atmospheric depth without competing with content. + """ + random.seed(42) + svg = _svg_open(w, h) + svg += '\n <defs>' + svg += f'\n <linearGradient id="fg" x1="0" y1="0" x2="1" y2="1">' + svg += f'\n <stop offset="0%" stop-color="{color}" stop-opacity="{opacity}"/>' + svg += f'\n <stop offset="50%" stop-color="{color}" stop-opacity="{opacity * 1.5:.3f}"/>' + svg += f'\n <stop offset="100%" stop-color="{color}" stop-opacity="{opacity * 0.5:.3f}"/>' + svg += '\n </linearGradient>' + svg += '\n </defs>' + + for i in range(curves): + path = _random_bezier_path(w, h) + sw = stroke_width + random.uniform(-20, 30) + svg += f'\n <path d="{path}" fill="none" stroke="url(#fg)" ' + svg += f'stroke-width="{sw:.0f}" stroke-linecap="round" opacity="{opacity + i * 0.01:.3f}"/>' + + svg += '\n</svg>' + return svg + +def generate_grid_svg(w, h, color="#888888", spacing=60, line_width=0.5, opacity=0.04): + """ + Grid mode: architectural reference grid. + Ultra-faint 1px lines creating underlying structure. + """ + svg = _svg_open(w, h) + svg += f'\n <g opacity="{opacity}">' + # Vertical lines + for x in range(0, int(w) + 1, spacing): + svg += f'\n <line x1="{x}" y1="0" x2="{x}" y2="{h}" stroke="{color}" stroke-width="{line_width}"/>' + # Horizontal lines + for y in range(0, int(h) + 1, spacing): + svg += f'\n <line x1="0" y1="{y}" x2="{w}" y2="{y}" stroke="{color}" stroke-width="{line_width}"/>' + svg += '\n </g>' + svg += '\n</svg>' + return svg + +def generate_noise_svg(w, h, frequency=0.8, octaves=4, opacity=0.035): + """ + Noise mode: feTurbulence grain texture. + Adds tactile paper-like quality. + """ + svg = _svg_open(w, h) + svg += f""" + <defs> + <filter id="grain" x="0" y="0" width="100%" height="100%"> + <feTurbulence type="fractalNoise" baseFrequency="{frequency}" + numOctaves="{octaves}" stitchTiles="stitch" result="noise"/> + <feColorMatrix type="saturate" values="0" in="noise" result="grey"/> + </filter> + </defs> + <rect width="{w}" height="{h}" filter="url(#grain)" opacity="{opacity}"/>""" + svg += '\n</svg>' + return svg + +def _generate_data_driven_svg(data_points, w=720, h=960, color="#8a8a8a"): + """ + Content-Aware SVG: Transform business data arrays into Bézier background curves. + The background subtly echoes the data's shape — a cross-modal design metaphor. + """ + if not data_points or len(data_points) < 2: + return generate_flow_svg(w, h, color) + + n = len(data_points) + min_v = min(data_points) + max_v = max(data_points) + rng = max_v - min_v if max_v != min_v else 1.0 + + # Normalize data to canvas coordinates + # X: evenly spaced across width; Y: mapped to 20%-80% of height (inverted for SVG) + points = [] + for i, v in enumerate(data_points): + x = (i / (n - 1)) * w + y_norm = (v - min_v) / rng # 0..1 + y = h * 0.8 - y_norm * (h * 0.6) # Map to 20%-80% height band + points.append((x, y)) + + # Build smooth Bézier path through all points (Catmull-Rom → cubic Bézier) + def catmull_to_bezier(p0, p1, p2, p3, tension=0.5): + """Convert 4 Catmull-Rom points to cubic Bézier control points for segment p1→p2.""" + cp1x = p1[0] + (p2[0] - p0[0]) / (6 * tension) + cp1y = p1[1] + (p2[1] - p0[1]) / (6 * tension) + cp2x = p2[0] - (p3[0] - p1[0]) / (6 * tension) + cp2y = p2[1] - (p3[1] - p1[1]) / (6 * tension) + return (cp1x, cp1y), (cp2x, cp2y) + + svg_paths = "" + # Generate 3 layers at different offsets for depth + for layer_idx, (opacity, y_offset, thickness) in enumerate([ + (0.08, 0, 3), (0.05, 60, 2), (0.03, -40, 1.5) + ]): + pts = [(px, py + y_offset) for px, py in points] + # Pad start/end for Catmull-Rom + padded = [pts[0]] + pts + [pts[-1]] + d = f"M {padded[1][0]:.1f},{padded[1][1]:.1f} " + for i in range(1, len(padded) - 2): + cp1, cp2 = catmull_to_bezier(padded[i-1], padded[i], padded[i+1], padded[i+2]) + d += f"C {cp1[0]:.1f},{cp1[1]:.1f} {cp2[0]:.1f},{cp2[1]:.1f} {padded[i+1][0]:.1f},{padded[i+1][1]:.1f} " + + # Main stroke + svg_paths += f'<path d="{d}" fill="none" stroke="{color}" stroke-width="{thickness}" opacity="{opacity}" />\n' + # Filled area below the curve + fill_d = d + f"L {w},{h} L 0,{h} Z" + svg_paths += f'<path d="{fill_d}" fill="{color}" opacity="{opacity * 0.4}" />\n' + + return f'<svg viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:100%">{svg_paths}</svg>' + + +def generate_supergraphic_svg(w, h, color="#8a8a8a", seed=42): + """ + Supergraphic mode: oversized geometric shapes (circles, rectangles, polygons) + cropped by the canvas edge, rendered at 3-5% opacity. + Creates "blueprint of a larger world" feeling — McKinsey / Pentagram style. + Shapes deliberately overflow the viewport so only partial arcs/edges are visible. + """ + random.seed(seed) + svg = _svg_open(w, h) + # Layer 1: Giant concentric circles, center placed off-canvas + cx = random.uniform(w * 0.6, w * 1.3) # right-biased, partially off-canvas + cy = random.uniform(-h * 0.2, h * 0.4) # top-biased + for i in range(4): + r = w * (0.6 + i * 0.35) # radii from 60% to 165% of width — always overflowing + opacity = 0.04 - i * 0.008 # outer rings fainter + svg += f'\n <circle cx="{cx:.0f}" cy="{cy:.0f}" r="{r:.0f}" ' + svg += f'fill="none" stroke="{color}" stroke-width="1.5" opacity="{max(opacity, 0.015):.3f}"/>' + + # Layer 2: Oversized rotated rectangles, clipped by viewport + for _ in range(2): + rx = random.uniform(-w * 0.3, w * 0.5) + ry = random.uniform(h * 0.3, h * 1.2) + rw = random.uniform(w * 0.8, w * 1.6) + rh = random.uniform(h * 0.5, h * 1.2) + angle = random.uniform(-15, 25) + svg += f'\n <rect x="{rx:.0f}" y="{ry:.0f}" width="{rw:.0f}" height="{rh:.0f}" ' + svg += f'fill="none" stroke="{color}" stroke-width="1" opacity="0.025" ' + svg += f'transform="rotate({angle:.1f} {rx + rw/2:.0f} {ry + rh/2:.0f})"/>' + + # Layer 3: A single massive polygon (pentagon/hexagon) barely visible + sides = random.choice([5, 6, 8]) + pcx = random.uniform(-w * 0.1, w * 0.4) + pcy = random.uniform(h * 0.5, h * 1.1) + pr = w * random.uniform(0.9, 1.4) + angle_offset = random.uniform(0, 360 / sides) + points = [] + for i in range(sides): + a = math.radians(angle_offset + i * 360 / sides) + px = pcx + pr * math.cos(a) + py = pcy + pr * math.sin(a) + points.append(f"{px:.0f},{py:.0f}") + svg += f'\n <polygon points="{" ".join(points)}" ' + svg += f'fill="none" stroke="{color}" stroke-width="1" opacity="0.02"/>' + + svg += '\n</svg>' + return svg + + +def generate_ordered_texture_svg(w, h, color="#8a8a8a", seed=42): + """ + Ordered Texture mode: precision dot-matrix, coordinate grids, and contour lines + placed in specific corners/edges of the canvas (not full coverage). + Creates "engineered precision" feeling — ideal for tech, finance, data reports. + """ + random.seed(seed) + svg = _svg_open(w, h) + + # Region 1: Dot matrix in top-right corner (10x10 grid of small circles) + dot_cols, dot_rows = 12, 10 + dot_spacing = 18 + dot_r = 2 + dot_origin_x = w - dot_cols * dot_spacing - 40 # right-aligned with margin + dot_origin_y = 30 # top margin + svg += f'\n <g opacity="0.08">' + for row in range(dot_rows): + for col in range(dot_cols): + dx = dot_origin_x + col * dot_spacing + dy = dot_origin_y + row * dot_spacing + svg += f'\n <circle cx="{dx}" cy="{dy}" r="{dot_r}" fill="{color}"/>' + svg += '\n </g>' + + # Region 2: Coordinate grid lines in bottom-left quadrant (blueprint style) + grid_x0, grid_y0 = 20, h * 0.7 + grid_x1, grid_y1 = w * 0.45, h - 20 + grid_spacing = 30 + svg += f'\n <g opacity="0.05" stroke="{color}" stroke-width="0.5" stroke-dasharray="4,6">' + # Vertical lines + x = grid_x0 + while x <= grid_x1: + svg += f'\n <line x1="{x:.0f}" y1="{grid_y0:.0f}" x2="{x:.0f}" y2="{grid_y1:.0f}"/>' + x += grid_spacing + # Horizontal lines + y = grid_y0 + while y <= grid_y1: + svg += f'\n <line x1="{grid_x0:.0f}" y1="{y:.0f}" x2="{grid_x1:.0f}" y2="{y:.0f}"/>' + y += grid_spacing + svg += '\n </g>' + + # Region 3: Tick marks along the grid (ruler effect) + svg += f'\n <g opacity="0.06" stroke="{color}" stroke-width="0.8">' + x = grid_x0 + while x <= grid_x1: + svg += f'\n <line x1="{x:.0f}" y1="{grid_y1:.0f}" x2="{x:.0f}" y2="{grid_y1 + 5:.0f}"/>' + x += grid_spacing + svg += '\n </g>' + + # Region 4: Flowing contour lines (Bézier) across mid-right area + svg += f'\n <g opacity="0.04" fill="none" stroke="{color}" stroke-width="1">' + for i in range(5): + y_base = h * 0.35 + i * 35 + x_start = w * 0.55 + x_end = w + 20 # overflow right edge + cx1 = x_start + (x_end - x_start) * 0.3 + random.uniform(-30, 30) + cy1 = y_base + random.uniform(-40, 40) + cx2 = x_start + (x_end - x_start) * 0.7 + random.uniform(-30, 30) + cy2 = y_base + random.uniform(-40, 40) + svg += f'\n <path d="M{x_start:.0f},{y_base:.0f} C{cx1:.0f},{cy1:.0f} {cx2:.0f},{cy2:.0f} {x_end:.0f},{y_base + random.uniform(-20, 20):.0f}"/>' + svg += '\n </g>' + + # Region 5: Cross-hair markers at 2-3 strategic points + for _ in range(3): + mx = random.uniform(w * 0.15, w * 0.85) + my = random.uniform(h * 0.15, h * 0.85) + size = 8 + svg += f'\n <g opacity="0.06" stroke="{color}" stroke-width="0.8">' + svg += f'\n <line x1="{mx - size}" y1="{my}" x2="{mx + size}" y2="{my}"/>' + svg += f'\n <line x1="{mx}" y1="{my - size}" x2="{mx}" y2="{my + size}"/>' + svg += f'\n <circle cx="{mx}" cy="{my}" r="{size * 0.6:.0f}" fill="none"/>' + svg += '\n </g>' + + svg += '\n</svg>' + return svg + + +def generate_generative_svg(svg_type="flow", w=720, h=960, color="#8a8a8a"): + """Route to the appropriate SVG generator.""" + if svg_type == "flow": + return generate_flow_svg(w, h, color) + elif svg_type == "grid": + return generate_grid_svg(w, h, color) + elif svg_type == "noise": + return generate_noise_svg(w, h) + elif svg_type == "supergraphic": + return generate_supergraphic_svg(w, h, color) + elif svg_type == "ordered_texture": + return generate_ordered_texture_svg(w, h, color) + else: + # Default: flow + noise layered + flow = generate_flow_svg(w, h, color) + return flow + + +def generate_continuous_flow_svg(w, h, total_pages, color="#8a8a8a", curves=4, stroke_width=80, opacity=0.05): + """ + Continuous Flow mode: generates ONE large SVG spanning all pages. + Returns a list of per-page SVG strings, each using viewBox to slice the master. + + The bezier curves are constrained to have anchor points every ~480px vertically, + ensuring visible content on every page. + """ + total_h = h * total_pages + random.seed(42) + + # Build the master path data + paths_data = [] + for _ in range(curves): + # Generate anchor points — one every 480px ensures ~2 per page + num_anchors = max(3, int(total_h / 480)) + anchors = [] + for i in range(num_anchors): + ax = random.uniform(w * 0.1, w * 0.9) + ay = (total_h / (num_anchors - 1)) * i + anchors.append((ax, ay)) + + # Build cubic bezier path through anchors + path = f"M{anchors[0][0]:.0f},{anchors[0][1]:.0f}" + for i in range(1, len(anchors)): + prev = anchors[i - 1] + curr = anchors[i] + # Control points: spread horizontally for organic feel + cx1 = random.uniform(w * 0.0, w * 1.0) + cy1 = prev[1] + (curr[1] - prev[1]) * 0.33 + random.uniform(-100, 100) + cx2 = random.uniform(w * 0.0, w * 1.0) + cy2 = prev[1] + (curr[1] - prev[1]) * 0.66 + random.uniform(-100, 100) + path += f" C{cx1:.0f},{cy1:.0f} {cx2:.0f},{cy2:.0f} {curr[0]:.0f},{curr[1]:.0f}" + + sw = stroke_width + random.uniform(-20, 30) + paths_data.append((path, sw)) + + # Generate per-page SVG slices + page_svgs = [] + gradient_def = f'''<defs> + <linearGradient id="fg" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0%" stop-color="{color}" stop-opacity="{opacity}"/> + <stop offset="50%" stop-color="{color}" stop-opacity="{opacity * 1.5:.3f}"/> + <stop offset="100%" stop-color="{color}" stop-opacity="{opacity * 0.5:.3f}"/> + </linearGradient> + </defs>''' + + for page_idx in range(total_pages): + vy = page_idx * h + svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 {vy} {w} {h}" width="{w}" height="{h}">' + svg += f'\n {gradient_def}' + for i, (path, sw) in enumerate(paths_data): + svg += f'\n <path d="{path}" fill="none" stroke="url(#fg)" ' + svg += f'stroke-width="{sw:.0f}" stroke-linecap="round" opacity="{opacity + i * 0.01:.3f}"/>' + svg += '\n</svg>' + page_svgs.append(svg) + + return page_svgs + + +def generate_unified_svg(w, h, total_pages, svg_type, color="#8a8a8a", curves=4, stroke_width=80, opacity=0.05): + """ + Generate a single SVG that spans the full continuous canvas (w x h*total_pages). + Unlike generate_continuous_flow_svg which returns per-page slices, + this returns ONE svg string for the entire document. + Used by the continuous-canvas rendering mode. + """ + total_h = h * total_pages + random.seed(42) + + if svg_type in ("continuous_flow", "flow"): + # Bezier curves spanning entire height + paths_data = [] + for _ in range(curves): + num_anchors = max(3, int(total_h / 480)) + anchors = [] + for i in range(num_anchors): + ax = random.uniform(w * 0.1, w * 0.9) + ay = (total_h / (num_anchors - 1)) * i + anchors.append((ax, ay)) + path = f"M{anchors[0][0]:.0f},{anchors[0][1]:.0f}" + for i in range(1, len(anchors)): + prev = anchors[i - 1] + curr = anchors[i] + cx1 = random.uniform(w * 0.0, w * 1.0) + cy1 = prev[1] + (curr[1] - prev[1]) * 0.33 + random.uniform(-100, 100) + cx2 = random.uniform(w * 0.0, w * 1.0) + cy2 = prev[1] + (curr[1] - prev[1]) * 0.66 + random.uniform(-100, 100) + path += f" C{cx1:.0f},{cy1:.0f} {cx2:.0f},{cy2:.0f} {curr[0]:.0f},{curr[1]:.0f}" + sw = stroke_width + random.uniform(-20, 30) + paths_data.append((path, sw)) + + gradient_def = f'''<defs> + <linearGradient id="fg" x1="0" y1="0" x2="1" y2="1"> + <stop offset="0%" stop-color="{color}" stop-opacity="{opacity}"/> + <stop offset="50%" stop-color="{color}" stop-opacity="{opacity * 1.5:.3f}"/> + <stop offset="100%" stop-color="{color}" stop-opacity="{opacity * 0.5:.3f}"/> + </linearGradient> + </defs>''' + + svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {total_h}" width="{w}" height="{total_h}">' + svg += f'\n {gradient_def}' + for i, (path, sw) in enumerate(paths_data): + svg += f'\n <path d="{path}" fill="none" stroke="url(#fg)" ' + svg += f'stroke-width="{sw:.0f}" stroke-linecap="round" opacity="{opacity + i * 0.01:.3f}"/>' + svg += '\n</svg>' + return svg + + elif svg_type == "grid": + # Grid pattern spanning full height + svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {total_h}" width="{w}" height="{total_h}">' + spacing = 60 + for x in range(0, w + 1, spacing): + svg += f'\n <line x1="{x}" y1="0" x2="{x}" y2="{total_h}" stroke="{color}" stroke-width="0.5" opacity="{opacity}"/>' + for y in range(0, int(total_h) + 1, spacing): + svg += f'\n <line x1="0" y1="{y}" x2="{w}" y2="{y}" stroke="{color}" stroke-width="0.5" opacity="{opacity}"/>' + svg += '\n</svg>' + return svg + + elif svg_type == "noise": + # Noise dots spanning full height + svg = f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {w} {total_h}" width="{w}" height="{total_h}">' + num_dots = int(200 * total_pages) + for _ in range(num_dots): + cx = random.uniform(0, w) + cy = random.uniform(0, total_h) + r = random.uniform(0.5, 2.5) + svg += f'\n <circle cx="{cx:.0f}" cy="{cy:.0f}" r="{r:.1f}" fill="{color}" opacity="{random.uniform(opacity * 0.3, opacity * 1.5):.3f}"/>' + svg += '\n</svg>' + return svg + + return "" + + +# ═══════════════════════════════════════════════════════════════════════ +# 3. LAYOUT CALCULATOR — Deliberate Spatial Tension +# ═══════════════════════════════════════════════════════════════════════ +# +# Returns absolute coordinates for elements with intentional: +# - Offset: elements not perfectly centered (art tension) +# - Overlap: controlled z-index collisions +# - Breathing margin: 15% minimum on all edges + +BREATHING_MARGIN = 0.12 # 12% of canvas on each edge (was 15%, too tight for content-dense pages) + +def calculate_layout(elements, w=720, h=960, style="offset"): + """ + Calculate positioned layout for named elements. + + Args: + elements: list of element names (e.g. ["hero", "body", "meta", "footer"]) + style: "offset" (deliberate asymmetry), "centered" (formal), "stacked" (vertical flow) + + Returns: + dict mapping element names to {x, y, w, h, rotation} in pixels + """ + # Safe area (15% breathing margin on all sides) + safe_x = w * BREATHING_MARGIN + safe_y = h * BREATHING_MARGIN + safe_w = w * (1 - 2 * BREATHING_MARGIN) + safe_h = h * (1 - 2 * BREATHING_MARGIN) + + layout = {} + + if style == "offset": + # Asymmetric placement — elements shift left/right of center + regions = _divide_vertical(safe_x, safe_y, safe_w, safe_h, len(elements)) + for i, name in enumerate(elements): + rx, ry, rw, rh = regions[i] + # Apply deliberate offset: odd elements shift left, even shift right + offset_x = rw * 0.08 * (-1 if i % 2 == 0 else 1) + layout[name] = { + "x": round(rx + offset_x, 1), + "y": round(ry, 1), + "w": round(rw * 0.85, 1), # Don't fill the full width + "h": round(rh * 0.85, 1), + "rotation": round(random.uniform(-1.5, 1.5), 2) if name != "body" else 0, + } + + elif style == "centered": + # Formal centered — golden ratio vertical split + regions = _divide_vertical(safe_x, safe_y, safe_w, safe_h, len(elements)) + for i, name in enumerate(elements): + rx, ry, rw, rh = regions[i] + layout[name] = { + "x": round(rx + (rw * 0.075), 1), # Slight horizontal centering margin + "y": round(ry, 1), + "w": round(rw * 0.85, 1), + "h": round(rh * 0.9, 1), + "rotation": 0, + } + + elif style == "overlap": + # Controlled overlaps — elements bleed into each other's space + regions = _divide_vertical(safe_x, safe_y, safe_w, safe_h, len(elements)) + for i, name in enumerate(elements): + rx, ry, rw, rh = regions[i] + overlap_y = rh * 0.15 if i > 0 else 0 # Pull up into previous region + layout[name] = { + "x": round(rx, 1), + "y": round(ry - overlap_y, 1), + "w": round(rw, 1), + "h": round(rh + overlap_y * 0.5, 1), + "rotation": 0, + "z_index": len(elements) - i, # Later elements on top + } + + return layout + +def _divide_vertical(x, y, w, h, n): + """Divide a rectangle into n vertical bands with golden-ratio-inspired proportions.""" + if n <= 0: + return [] + if n == 1: + return [(x, y, w, h)] + + # Weighted distribution: first element (hero) gets more space + weights = [1.618 if i == 0 else 1.0 for i in range(n)] + total = sum(weights) + + regions = [] + current_y = y + for i in range(n): + region_h = h * weights[i] / total + regions.append((x, current_y, w, region_h)) + current_y += region_h + + return regions + + +# ═══════════════════════════════════════════════════════════════════════ +# VALIDATION — Post-generation color audit +# ═══════════════════════════════════════════════════════════════════════ + +def audit_palette(palette): + """ + Strict audit: mode-specific S/L bounds + WCAG contrast checks. + Returns list of violations (empty = clean). + """ + violations = [] + mode = palette.get("meta", {}).get("mode", "minimal") + + for key in ["bg", "mid", "accent", "text"]: + hex_val = palette.get(key, "") + if not hex_val.startswith("#"): + continue + r, g, b = (int(hex_val[i:i+2], 16) / 255.0 for i in (1, 3, 5)) + h, l, s = colorsys.rgb_to_hls(r, g, b) + + # Tight bg/mid saturation limits per mode + if key in ("bg", "mid"): + limits = {"minimal": 0.20, "dark": 0.16, "pastel": 0.42, "jewel": 0.62, "light": 0.28} + cap = limits.get(mode, 0.15) + if s > cap: + violations.append(f"{key}: S={s:.3f} > {cap} in {mode} mode") + + # Tight accent saturation — pastel/jewel reined in + if key == "accent": + accent_caps = {"minimal": 0.78, "dark": 0.62, "pastel": 0.52, "jewel": 0.52, "light": 0.48} + cap = accent_caps.get(mode, 0.60) + if s > cap: + violations.append(f"accent: S={s:.3f} > {cap} in {mode} mode") + + # Lightness guardrails + if key == "bg": + if mode == "dark" and l > 0.10: + violations.append(f"bg L={l:.3f} > 0.10 in dark (muddy middle)") + elif mode == "minimal" and l < 0.90: + violations.append(f"bg L={l:.3f} < 0.90 in minimal (too dark for paper)") + elif mode == "light" and l < 0.88: + violations.append(f"bg L={l:.3f} < 0.88 in light (too dark)") + elif mode == "jewel" and l > 0.28: + violations.append(f"bg L={l:.3f} > 0.28 in jewel (not deep enough)") + elif mode == "pastel" and l < 0.83: + violations.append(f"bg L={l:.3f} < 0.83 in pastel (too dark for Morandi)") + + # WCAG contrast checks + bg_hex = palette.get("bg", "") + text_hex = palette.get("text", "") + accent_hex = palette.get("accent", "") + if bg_hex.startswith("#") and text_hex.startswith("#"): + cr = _contrast_ratio(text_hex, bg_hex) + if cr < 4.5: + violations.append(f"text:bg contrast {cr:.2f} < 4.5:1 (WCAG AA fail)") + if bg_hex.startswith("#") and accent_hex.startswith("#"): + cr = _contrast_ratio(accent_hex, bg_hex) + if cr < 2.5: + violations.append(f"accent:bg contrast {cr:.2f} < 2.5:1 (accent invisible)") + + return violations + + +# ═══════════════════════════════════════════════════════════════════════ +# CLI +# ═══════════════════════════════════════════════════════════════════════ + +def main(): + parser = argparse.ArgumentParser( + description="Design Engine — aesthetic computation for art-direction-first PDF", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + %(prog)s palette --intent serenity --mode dark + %(prog)s palette --intent tension --mode light --seed 42 + %(prog)s palette-cascade --intent cold --mode minimal + %(prog)s palette-cascade --intent neutral --format reportlab + %(prog)s svg --intent flow --dimensions 720x960 + %(prog)s svg --intent grid --dimensions 720x960 + %(prog)s layout --elements hero,body,meta --dimensions 720x960 --style offset + %(prog)s full --intent serenity --mode dark --dimensions 720x960 --output-dir ./assets/ + %(prog)s audit --css-file assets/palette.css + """ + ) + sub = parser.add_subparsers(dest="command") + + # palette + p_pal = sub.add_parser("palette", help="Generate HSL-locked color palette") + p_pal.add_argument("--intent", default="neutral", choices=list(INTENT_HUES.keys())) + p_pal.add_argument("--mode", default="minimal", choices=["minimal", "dark", "pastel", "jewel", "light"]) + p_pal.add_argument("--harmony", default="auto", choices=["auto", "complementary", "split_complementary", "triadic", "analogous", "monochrome"]) + p_pal.add_argument("--seed", type=int, default=None) + p_pal.add_argument("--format", default="css", choices=["css", "json"]) + + # svg + p_svg = sub.add_parser("svg", help="Generate algorithmic SVG background") + p_svg.add_argument("--svg-type", default="flow", choices=["flow", "grid", "noise", "supergraphic", "ordered_texture"]) + p_svg.add_argument("--dimensions", default="720x960") + p_svg.add_argument("--color", default="#8a8a8a") + + # layout + p_lay = sub.add_parser("layout", help="Calculate element positions") + p_lay.add_argument("--elements", default="hero,body,meta") + p_lay.add_argument("--dimensions", default="720x960") + p_lay.add_argument("--style", default="offset", choices=["offset", "centered", "overlap"]) + + # full + p_full = sub.add_parser("full", help="Generate all assets at once") + p_full.add_argument("--intent", default="neutral") + p_full.add_argument("--mode", default="minimal", choices=["minimal", "dark", "pastel", "jewel", "light"]) + p_full.add_argument("--harmony", default="auto", choices=["auto", "complementary", "split_complementary", "triadic", "analogous", "monochrome"]) + p_full.add_argument("--svg-intent", default="flow", choices=["flow", "grid", "noise"]) + p_full.add_argument("--dimensions", default="720x960") + p_full.add_argument("--elements", default="hero,body,meta") + p_full.add_argument("--style", default="offset") + p_full.add_argument("--seed", type=int, default=None) + p_full.add_argument("--output-dir", default="./assets/") + + # audit + p_audit = sub.add_parser("audit", help="Audit a palette for constraint violations") + p_audit.add_argument("--palette-json", required=True) + + # palette-cascade + p_pcas = sub.add_parser("palette-cascade", help="Generate role-based cascade palette (area ∝ 1/saturation)") + p_pcas.add_argument("--intent", default="neutral", choices=list(INTENT_HUES.keys())) + p_pcas.add_argument("--mode", default="minimal", choices=["minimal", "dark", "pastel", "jewel", "light"]) + p_pcas.add_argument("--harmony", default="auto", choices=["auto", "complementary", "split_complementary", "triadic", "analogous", "monochrome"]) + p_pcas.add_argument("--seed", type=int, default=None) + p_pcas.add_argument("--format", default="summary", choices=["summary", "json", "css", "reportlab"]) + + # compile + p_compile = sub.add_parser("compile", help="Compile a JSON Blueprint into a final HTML document") + p_compile.add_argument("--blueprint", required=True, help="Path to the JSON blueprint generated by LLM") + p_compile.add_argument("--output", default="poster.html", help="Path to save the output HTML") + + # derive + p_derive = sub.add_parser("derive", help="Auto-derive design intent from document description") + p_derive.add_argument("text", help="Document title or description") + + # Backward compat: positional command + parser.add_argument("legacy_command", nargs="?") + parser.add_argument("legacy_args", nargs="*") + + args = parser.parse_args() + + if args.command == "palette": + pal = generate_color_palette(args.intent, args.mode, harmony=args.harmony, seed=args.seed) + if args.format == "json": + print(json.dumps(pal, indent=2)) + else: + print(palette_to_css(pal)) + + elif args.command == "svg": + w, h = map(int, args.dimensions.split("x")) + print(generate_generative_svg(args.svg_type, w, h, args.color)) + + elif args.command == "layout": + w, h = map(int, args.dimensions.split("x")) + elements = [e.strip() for e in args.elements.split(",")] + result = calculate_layout(elements, w, h, args.style) + print(json.dumps(result, indent=2)) + + elif args.command == "full": + w, h = map(int, args.dimensions.split("x")) + os.makedirs(args.output_dir, exist_ok=True) + + # 1. Palette + pal = generate_color_palette(args.intent, args.mode, harmony=getattr(args, 'harmony', 'split_complementary'), seed=args.seed) + css = palette_to_css(pal) + css_path = os.path.join(args.output_dir, "palette.css") + with open(css_path, "w") as f: + f.write(css) + json_path = os.path.join(args.output_dir, "palette.json") + with open(json_path, "w") as f: + json.dump(pal, f, indent=2) + print(f"✅ {css_path}") + + # 2. SVG + svg_color = pal["accent"] # Use accent for SVG strokes + svg = generate_generative_svg(args.svg_intent, w, h, svg_color) + svg_path = os.path.join(args.output_dir, "background.svg") + with open(svg_path, "w") as f: + f.write(svg) + print(f"✅ {svg_path}") + + # 3. Layout + elements = [e.strip() for e in args.elements.split(",")] + lay = calculate_layout(elements, w, h, args.style) + lay_path = os.path.join(args.output_dir, "layout.json") + with open(lay_path, "w") as f: + json.dump(lay, f, indent=2) + print(f"✅ {lay_path}") + + # 4. Audit + violations = audit_palette(pal) + if violations: + print(f"\n⚠️ Palette violations:") + for v in violations: + print(f" - {v}") + else: + print(f"\n✅ Palette passes all constraints") + + print(f"\n🎨 {args.intent}/{args.mode} | {w}×{h} | {len(elements)} elements") + + elif args.command == "audit": + with open(args.palette_json) as f: + pal = json.load(f) + violations = audit_palette(pal) + if violations: + print("⚠️ Violations found:") + for v in violations: + print(f" - {v}") + sys.exit(1) + else: + print("✅ Palette passes all constraints") + + elif args.command == "derive": + intent = derive_intent(args.text) + print(f"Intent: {intent}") + print(f"Hue: {INTENT_HUES.get(intent, 45)}°") + # Also generate a quick palette preview + pal = generate_color_palette(intent, "dark") + print(f"Preview (dark): bg={pal['bg']} accent={pal['accent']}") + pal_light = generate_color_palette(intent, "light") + print(f"Preview (light): bg={pal_light['bg']} accent={pal_light['accent']}") + + elif args.command == "palette-cascade": + cascade = generate_cascade_palette(args.intent, args.mode, harmony=args.harmony, seed=args.seed) + if args.format == "json": + print(json.dumps(cascade, indent=2, ensure_ascii=False, default=str)) + elif args.format == "css": + print(cascade["css"]) + elif args.format == "reportlab": + print(cascade["reportlab"]) + else: # summary + meta = cascade["meta"] + print(f"🎨 Cascade Palette | Intent: {meta['intent']} | Mode: {meta['mode']} | Harmony: {meta['harmony']}") + print(f" Base hue: {meta['base_hue']}° | Accent hue: {meta['accent_hue']}° | Secondary hue: {meta['secondary_hue']}°") + print(f" Contrast: text:bg={meta['contrast']['text_on_bg']} | accent:bg={meta['contrast']['accent_on_bg']}") + print() + print(" TIER | ROLE | HEX | S | USAGE") + print(" ────── | ────────────────── | ─────── | ────── | ────────────") + for name, info in cascade["roles"].items(): + tier = info['tier'].upper().ljust(6) + nm = name.ljust(18) + hx = info['hex'].ljust(7) + s_val = f"{info['hsl'][1]:.3f}".ljust(6) + print(f" {tier} | {nm} | {hx} | {s_val} | {info['usage']}") + print() + print(" Semantic:") + for name, info in cascade["semantic"].items(): + print(f" {name}: {info['hex']} (S={info['hsl'][1]:.3f})") + if meta["audit"]: + print(f"\n ⚠️ Violations:") + for v in meta["audit"]: + print(f" - {v}") + else: + print(f"\n ✅ All tier constraints pass") + + elif args.command == "compile": + try: + out_path, pal = compile_blueprint(args.blueprint, args.output) + print(f"✅ Blueprint compiled successfully to: {out_path}") + violations = audit_palette(pal) + if violations: + print(f"⚠️ Warning: Generated palette had minor violations (auto-corrected by engine):") + for v in violations: print(f" - {v}") + + except Exception as e: + print(f"❌ Failed to compile blueprint: {str(e)}") + sys.exit(1) + else: + parser.print_help() + + +# ═══════════════════════════════════════════════════════════════════════ +# 4. BLUEPRINT COMPILER — Converting JSON Intent to HTML Canvas +# ═══════════════════════════════════════════════════════════════════════ +import re + +# Base CSS that enforces the "Axioms" from visual_framework.md +BASE_CSS = """ +@page { + size: var(--canvas-w, 720px) var(--canvas-h, 960px); + margin: 0; +} +:root { + --font-sans: 'Inter', 'Noto Sans SC', 'Helvetica Neue', 'Apple Color Emoji', 'Segoe UI Emoji', sans-serif; + --font-serif: 'Playfair Display', 'Noto Serif SC', 'Cormorant Garamond', 'Apple Color Emoji', serif; + --font-mono: 'SF Mono', 'Consolas', 'Apple Color Emoji', monospace; + + /* Typographic Scale — 6-level fluid type system (Modular Scale) */ + --text-scale-6: clamp(64px, 12vw, 150px); /* Hero / Display — oversized, single word or short phrase */ + --text-scale-5: clamp(48px, 8vw, 96px); /* Primary Title — poster headline */ + --text-scale-4: clamp(32px, 5vw, 56px); /* Subheadline — chapter opener or key quote */ + --text-scale-3: clamp(20px, 3vw, 32px); /* Lead Paragraph — slightly larger than body */ + --text-scale-2: 16px; /* Body — standard body text */ + --text-scale-1: 12px; /* Meta / Caption — minimum readable size */ +} +html, body { + margin: 0; padding: 0; + background: var(--c-bg); + color: var(--c-text); + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; +} +/* Browser preview: scale poster to fit viewport & center on matching background */ +@media screen { + html { + height: auto; + display: flex; + justify-content: center; + align-items: flex-start; + min-height: 100vh; + background: var(--c-bg); + } + body { + transform-origin: top center; + margin: 0 auto; + box-shadow: 0 0 60px rgba(0,0,0,0.3); + /* scale injected by override_css with concrete canvas dimensions */ + } +} +.canvas { + width: var(--canvas-w, 720px); + min-height: var(--canvas-h, 960px); + position: relative; + overflow: hidden; + box-sizing: border-box; + page-break-after: always; +} +/* ═══ Continuous Canvas Mode (multi-page as one seamless surface) ═══ */ +.continuous-canvas { + width: var(--canvas-w, 720px); + position: relative; + overflow: hidden; + box-sizing: border-box; + /* height is set inline: canvas_h * total_pages */ +} +.continuous-canvas .bg-layer-full { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; +} +.continuous-canvas .bg-layer-full svg { + width: 100%; + height: 100%; +} +.continuous-canvas .page-section { + position: absolute; + left: 0; + width: 100%; + box-sizing: border-box; + overflow: visible; + /* top and height set inline per page */ +} +.continuous-canvas .page-section .safe-zone { + position: absolute; + inset: 10% 12%; + display: grid; + grid-template-columns: repeat(12, 1fr); + grid-template-rows: repeat(12, minmax(0, auto)); + align-content: start; +} +.continuous-canvas .page-section .page-ghost { + position: absolute; + bottom: -5%; + right: 5%; + font-size: 240px; + font-weight: 900; + color: var(--c-mid); + opacity: 0.05; + pointer-events: none; + z-index: 0; +} +/* 12% Breathing Margin Enforcer (balanced: enough air without wasting space) */ +.safe-zone { + position: absolute; + inset: 10% 12%; + display: grid; + grid-template-columns: repeat(12, 1fr); + grid-template-rows: repeat(12, minmax(0, auto)); + align-content: start; + /* gap is injected dynamically by compile_blueprint via inline style */ +} +/* Grid-item wrapper — every component gets one */ +.grid-item { + display: flex; + min-width: 0; + min-height: 0; + overflow: visible; +} +/* Global content-overflow protection — prevents ANY text/block from breaking out of its container */ +.grid-item * { + max-width: 100%; + box-sizing: border-box; +} +.grid-item p, .grid-item li, .grid-item span, .grid-item h1, .grid-item h2, .grid-item h3, .grid-item h4, .grid-item td, .grid-item th, .grid-item div { + overflow-wrap: break-word; + word-break: break-word; +} +/* CJK-safe text wrapping: prefer keeping CJK word groups intact, but allow break when necessary */ +.hero-type, .hero-sub, .glass-canvas, .shaped-content, .stat-label, .delta-metric, .delta-label { + text-wrap: balance; + overflow-wrap: break-word; + word-break: normal; + line-break: strict; +} + +/* ═══ Micro-Typography: Computational Typesetting ═══ */ +p, .glass-canvas, .shaped-content, li { + /* Algorithmic justification — eliminates white rivers between words */ + text-align: justify; + hyphens: auto; + word-spacing: -0.05em; + /* Hanging punctuation — visually aligned optical edges */ + hanging-punctuation: first last; + /* Kill orphans & widows — force min 3 lines carried across page breaks */ + orphans: 3; + widows: 3; +} +.grid-item table { + width: 100%; + table-layout: fixed; + overflow: hidden; +} +.grid-item img { + max-width: 100%; + height: auto; +} + +/* --- Archetype Layouts --- */ +/* All archetypes share the 12×12 grid via .safe-zone. + Archetype classes may override alignment defaults on the canvas level. */ +.archetype-cover_hero .safe-zone { + align-content: start; + inset: 12% 14%; +} +/* Cover hero typography scale: larger text for covers */ +.archetype-cover_hero .hero-type { + font-size: clamp(42px, 8vw, 72px); + line-height: 1.15; +} +.archetype-cover_hero .hero-sub { + font-size: clamp(22px, 4vw, 32px); + line-height: 1.3; + opacity: 0.85; +} +.archetype-cover_hero .floating-meta { + font-size: clamp(16px, 2.5vw, 22px); +} +.archetype-split_vertical { display: grid; grid-template-columns: 1fr 1fr; min-height: 960px; } +.archetype-split_vertical .safe-zone { position: relative; inset: auto; padding: 10%; display: grid; grid-template-columns: repeat(12, 1fr); grid-template-rows: repeat(12, 1fr); align-content: center; } +.archetype-split_vertical.single-column { grid-template-columns: 1fr; } +.archetype-editorial_flow .safe-zone { align-content: center; } +.archetype-scattered_canvas .safe-zone { /* same 12×12 grid — scattered effect via grid_area placement */ } +.archetype-data_dashboard .safe-zone { /* same 12×12 grid — dashboard tiles via grid_area placement */ } +.archetype-shaped_editorial .safe-zone { align-content: center; } + +/* Continuous canvas archetype overrides (page-section inherits archetype class) */ +.continuous-canvas .page-section.archetype-cover_hero .safe-zone { align-content: start; inset: 12% 14%; } +.continuous-canvas .page-section.archetype-cover_hero .hero-type { font-size: clamp(42px, 8vw, 72px); line-height: 1.15; } +.continuous-canvas .page-section.archetype-cover_hero .hero-sub { font-size: clamp(22px, 4vw, 32px); line-height: 1.3; opacity: 0.85; } +.continuous-canvas .page-section.archetype-cover_hero .floating-meta { font-size: clamp(16px, 2.5vw, 22px); } +.continuous-canvas .page-section.archetype-editorial_flow .safe-zone { align-content: center; } +.continuous-canvas .page-section.archetype-shaped_editorial .safe-zone { align-content: center; inset: 5% 6%; } + +/* --- Components --- */ +.hero-type { + font-size: clamp(48px, 10vw, 110px); + line-height: 0.88; + letter-spacing: -0.03em; + margin: 0; + overflow-wrap: break-word; + word-break: break-word; + position: relative; + z-index: 10; +} +.hero-type.weight-black { font-weight: 900; } +.hero-type.weight-thin { font-weight: 100; letter-spacing: 0.05em; font-family: var(--font-serif); } +.hero-sub { + margin: 8px 0 0 0; + font-size: var(--text-scale-2); + line-height: 1.3; + opacity: 0.8; +} +/* Hero container needs vertical stacking */ +.grid-item:has(.hero-type) { + flex-direction: column; + justify-content: center; +} +.hero-type, .hero-sub { + width: 100%; +} +.hero-type { + color: var(--c-text); +} + +.glass-canvas { + background: var(--c-surface); + border: 1px solid rgba(128, 128, 128, 0.08); + border-radius: 2px; + padding: 36px; + font-size: 16px; + line-height: 1.6; + z-index: 5; + position: relative; + overflow-wrap: break-word; + word-break: break-word; +} + +.floating-meta { + display: flex; + flex-direction: column; + font-size: 12px; + font-family: var(--font-mono); + letter-spacing: 0.1em; + color: var(--c-muted); + opacity: 0.6; + z-index: 20; + overflow: hidden; + text-overflow: ellipsis; + /* Positioned via grid_area in .grid-item wrapper — no absolute needed */ +} +/* Legacy position helpers (kept for backwards compat with old blueprints) */ +.pos-top-left { align-self: start; justify-self: start; } +.pos-top-right { align-self: start; justify-self: end; text-align: right; } +.pos-bottom-left { align-self: end; justify-self: start; } +.pos-bottom-right { align-self: end; justify-self: end; text-align: right; } + +.stat-block { display: flex; flex-direction: column; margin-bottom: 24px; } +.stat-num { font-size: clamp(32px, 5vw, 56px); font-weight: 900; line-height: 0.9; color: var(--c-text); } +.stat-unit { font-size: clamp(14px, 2vw, 20px); font-weight: 300; color: var(--c-muted); margin-left: 4px; display: inline;} +.stat-label { font-size: 12px; letter-spacing: 0.15em; color: var(--c-accent); margin-top: 8px; text-transform: uppercase; } + +.hairline { border: none; border-top: 0.5px solid var(--c-muted); opacity: 0.3; margin: 8px 0; width: 100%; } +.hairline.style-accent { border-top-color: var(--c-accent); width: 30%; margin-left: 0; opacity: 0.8;} + +.page-ghost { + position: absolute; bottom: -5%; right: 5%; + font-size: 240px; font-weight: 900; color: var(--c-mid); + opacity: 0.05; pointer-events: none; z-index: 0; +} + +.bg-layer { position: absolute; inset: 0; z-index: 1; pointer-events: none; } +.bg-layer svg { width: 100%; height: 100%; } + +/* --- Shaped_Canvas (Semantic Shape-Wrapping) --- */ +.shaped-canvas { + position: relative; + padding: 24px; + font-size: 16px; + line-height: 1.7; + z-index: 5; + overflow-wrap: break-word; + word-break: break-word; +} +.shape-float { + float: left; + margin: 0; + padding: 0; +} +.shape-circle { shape-outside: circle(45% at 50% 50%); width: 40%; height: 90%; } +.shape-wave { shape-outside: polygon(0 0, 80% 0, 60% 25%, 80% 50%, 60% 75%, 80% 100%, 0 100%); width: 45%; height: 100%; } +.shape-diagonal_slash { shape-outside: polygon(0 0, 100% 0, 0 100%); width: 50%; height: 100%; } +.shape-diamond { shape-outside: polygon(50% 0, 100% 50%, 50% 100%, 0 50%); width: 45%; height: 90%; } +.shape-wedge_right { shape-outside: polygon(0 0, 60% 0, 100% 50%, 60% 100%, 0 100%); width: 50%; height: 100%; } + +/* --- Archetype: shaped_editorial --- */ +.archetype-shaped_editorial .safe-zone { + inset: 5% 6%; + /* Inherits 12×12 grid from .safe-zone */ + align-content: center; +} + +/* ═══ Tufte Marginalia System ═══ */ +/* 30% sidenote rail for report/long-form archetypes */ +.archetype-tufte_report .safe-zone { + display: grid; + grid-template-columns: 1fr 280px; + gap: 40px; + align-content: start; +} +.archetype-tufte_report .main-column { + display: grid; + grid-template-columns: repeat(12, 1fr); + grid-template-rows: repeat(12, 1fr); + gap: inherit; +} +.archetype-tufte_report .side-rail { + display: flex; + flex-direction: column; + gap: 24px; + padding-top: 8px; +} +.sidenote { + font-size: 13px; + line-height: 1.5; + color: var(--c-muted); + border-left: 2px solid var(--c-accent); + padding-left: 12px; + opacity: 0.85; +} +.sidenote .sidenote-label { + font-weight: 700; + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--c-accent); + display: block; + margin-bottom: 4px; +} + +/* ═══ Delta Widget — Data-to-Ink Ratio Component ═══ */ +.delta-widget { + text-align: center; + padding: 16px 12px; +} +.delta-widget .delta-metric { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--c-muted); + margin-bottom: 4px; +} +.delta-widget .delta-value { + font-size: 36px; + font-weight: 900; + color: var(--c-text); + line-height: 1.1; +} +.delta-widget .delta-change { + font-size: 14px; + font-weight: 700; + margin-top: 4px; +} +.delta-widget .delta-label { + font-size: 12px; + color: var(--c-muted); + margin-top: 8px; +} + +/* ═══ Polymorphic Process_List — Container Query Adaptive ═══ */ +.process-list-container { + container-type: inline-size; + width: 100%; + min-height: 100%; +} +/* Wide: horizontal timeline */ +.process-list { + display: flex; + flex-direction: row; + align-items: flex-start; + gap: 8px; + list-style: none; + padding: 0; + margin: 0; + min-height: 100%; +} +.process-list .process-step { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + position: relative; + padding: 12px 4px; +} +.process-step .step-num { + width: 32px; height: 32px; border-radius: 50%; + background: var(--c-accent); color: #fff; + display: flex; align-items: center; justify-content: center; + font-weight: 800; font-size: 14px; margin-bottom: 8px; + flex-shrink: 0; +} +.process-step .step-title { font-weight: 700; font-size: 13px; color: var(--c-text); } +.process-step .step-desc { font-size: 12px; color: var(--c-muted); margin-top: 4px; line-height: 1.4; } +/* Connector line between horizontal steps */ +.process-step:not(:last-child)::after { + content: ''; position: absolute; top: 28px; right: -6px; + width: 12px; height: 2px; background: var(--c-accent); opacity: 0.5; +} +/* Narrow: vertical numbered list */ +@container (max-width: 360px) { + .process-list { + flex-direction: column; + gap: 12px; + } + .process-list .process-step { + flex-direction: row; + text-align: left; + align-items: flex-start; + gap: 12px; + padding: 4px 0; + } + .process-step .step-num { margin-bottom: 0; } + .process-step:not(:last-child)::after { display: none; } +} +""" + + +def _prevent_orphan_chars(text): + """ + Prevent orphan characters at end of paragraphs. + Replace the last space/breakable point between the final two CJK chars + (or words) with   so the browser never wraps a single trailing char + onto its own line. + """ + # CJK orphan: bind last two CJK characters with a zero-width no-break joiner + # Match: (CJK char)(optional space)(CJK char) at end of string (before tags) + text = re.sub(r'([\u4e00-\u9fff\u3400-\u4dbf])[\s]+([\u4e00-\u9fff\u3400-\u4dbf])(?=\s*(?:<[^>]*>)*\s*$)', '\\g<1>\u2060\\g<2>', text) + # Latin orphan: bind last two words + text = re.sub(r'(\S+)\s+(\S+)\s*$', r'\1 \2', text) + return text + + +def simple_markdown_to_html(md_text): + """Lightweight markdown → HTML for Glass Canvas. Handles paragraphs, headers, bold, italic, lists, and inline code.""" + if not md_text: + return "" + + lines = md_text.split('\n') + html_parts = [] + in_list = False + paragraph_buffer = [] + + def flush_paragraph(): + nonlocal paragraph_buffer + if paragraph_buffer: + text = ' '.join(paragraph_buffer) + # Apply inline formatting + text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text) + text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text) + text = re.sub(r'`(.*?)`', r'<code style="background:var(--c-surface);padding:2px 6px;border-radius:3px;font-size:max(12px, 0.9em)">\1</code>', text) + # Anti-orphan: prevent single trailing character on last line + text = _prevent_orphan_chars(text) + html_parts.append(f'<p style="margin:0 0 12px 0;line-height:1.7">{text}</p>') + paragraph_buffer = [] + + def apply_inline(text): + """Apply bold, italic, inline code.""" + text = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', text) + text = re.sub(r'\*(.*?)\*', r'<em>\1</em>', text) + text = re.sub(r'`(.*?)`', r'<code style="background:var(--c-surface);padding:2px 6px;border-radius:3px;font-size:max(12px, 0.9em)">\1</code>', text) + return text + + for line in lines: + stripped = line.strip() + + # Empty line — flush paragraph + if not stripped: + flush_paragraph() + if in_list: + html_parts.append('</ul>') + in_list = False + continue + + # Headers + if stripped.startswith('### '): + flush_paragraph() + if in_list: + html_parts.append('</ul>') + in_list = False + html_parts.append(f'<h3 style="font-size:16px;font-weight:700;margin:20px 0 8px 0;color:var(--c-accent);text-transform:uppercase;letter-spacing:0.05em;">{apply_inline(stripped[4:])}</h3>') + continue + if stripped.startswith('## '): + flush_paragraph() + if in_list: + html_parts.append('</ul>') + in_list = False + html_parts.append(f'<h2 style="font-size:20px;font-weight:700;margin:24px 0 12px 0;color:var(--c-text)">{apply_inline(stripped[3:])}</h2>') + continue + if stripped.startswith('# '): + flush_paragraph() + if in_list: + html_parts.append('</ul>') + in_list = False + html_parts.append(f'<h1 style="font-size:24px;font-weight:800;margin:28px 0 14px 0;color:var(--c-text)">{apply_inline(stripped[2:])}</h1>') + continue + + # List items (- or *) + list_match = re.match(r'^[-*]\s+(.*)', stripped) + if list_match: + flush_paragraph() + if not in_list: + html_parts.append('<ul style="margin:8px 0;padding-left:20px;list-style-type:disc">') + in_list = True + html_parts.append(f'<li style="margin:4px 0;line-height:1.6">{apply_inline(list_match.group(1))}</li>') + continue + + # Numbered list items + num_match = re.match(r'^(\d+)[.)]\s+(.*)', stripped) + if num_match: + flush_paragraph() + if in_list: + html_parts.append('</ul>') + in_list = False + html_parts.append(f'<p style="margin:4px 0 4px 20px;line-height:1.6"><strong>{num_match.group(1)}.</strong> {apply_inline(num_match.group(2))}</p>') + continue + + # Normal text — accumulate into paragraph + paragraph_buffer.append(stripped) + + # Flush remaining + flush_paragraph() + if in_list: + html_parts.append('</ul>') + + return '\n'.join(html_parts) + + +def _parse_grid_area(comp): + """ + Parse grid_area from component JSON. + Accepts two formats: + - Array: [row_start, col_start, row_end, col_end] — 1-based, max 13. + - String: "row_start / col_start / row_end / col_end" — 1-based, max 13. + Returns CSS grid-area string or None. + """ + ga = comp.get("grid_area", None) + if ga is None: + return None + # Array format: [1, 1, 3, 5] + if isinstance(ga, list) and len(ga) == 4: + rs, cs, re, ce = [max(1, min(13, int(v))) for v in ga] + # Fix zero-height/zero-width grid areas (row_end must be > row_start) + if re <= rs: + re = min(13, rs + 1) + if ce <= cs: + ce = min(13, cs + 1) + return f"{rs} / {cs} / {re} / {ce}" + # String format: "1 / 1 / 3 / 5" + if isinstance(ga, str) and "/" in ga: + parts = [p.strip() for p in ga.split("/")] + if len(parts) == 4: + try: + rs, cs, re, ce = [max(1, min(13, int(p))) for p in parts] + # Fix zero-height/zero-width grid areas + if re <= rs: + re = min(13, rs + 1) + if ce <= cs: + ce = min(13, cs + 1) + return f"{rs} / {cs} / {re} / {ce}" + except ValueError: + pass + return None + + +def _parse_align(comp): + """ + Parse align from component JSON. + Format: "vertical / horizontal" where each is start|center|end. + Returns (align-items, justify-content) tuple. + """ + align = comp.get("align", "start / start") + if "/" in str(align): + parts = [p.strip() for p in str(align).split("/")] + v = parts[0] if parts[0] in ("start", "center", "end") else "start" + h = parts[1] if len(parts) > 1 and parts[1] in ("start", "center", "end") else "start" + else: + v, h = "start", "start" + return v, h + + +def _estimate_content_weight(comp): + """ + Estimate the visual weight (space needed) of a component based on its content. + Returns a numeric weight proportional to how many grid rows it should occupy. + + Text-heavy components (Glass_Canvas, Shaped_Canvas) get weight proportional to + character count. Fixed-height components (Stat_Block, Hero_Typography, etc.) get + a small fixed weight since they don't grow with content. + """ + ctype = comp.get("type", "") + + # Fixed-height components: their visual size is independent of text length + FIXED_WEIGHTS = { + "Hero_Typography": 2.5, + "Stat_Block": 2.0, + "Delta_Widget": 2.0, + "Hairline_Divider": 1.0, + "Floating_Meta": 1.0, # usually positioned in corners, but fallback + "Image_Asset": 3.0, + } + if ctype in FIXED_WEIGHTS: + return FIXED_WEIGHTS[ctype] + + # Text-heavy components: weight scales with content length + text = comp.get("markdown_content", "") or comp.get("body", "") or "" + + # Also count sub-items (Process_List steps) + for step in comp.get("steps", []): + text += step.get("title", "") + step.get("description", "") + + char_count = len(text) + + if ctype in ("Glass_Canvas", "Shaped_Canvas", "Process_List"): + # Rough estimate: ~40 CJK chars per line at 16px in a ~500px wide container, + # ~1.6 line-height, so each 40 chars ≈ 1 visual line ≈ ~25px. + # Grid row on a 960px canvas ≈ 80px each (960/12). + # So 40 chars ≈ 0.3 grid rows. But Glass_Canvas has padding (~72px total), + # headings, list spacing, etc. Add base weight for the container itself. + base = 2.0 # minimum for padding + heading + text_rows = char_count / 120.0 # ~120 chars per grid-row worth of space + return base + text_rows + + # Unknown component type: estimate from text length with a reasonable default + if char_count > 0: + return 2.0 + char_count / 120.0 + return 2.0 + + +def _assign_floating_meta(comp): + """Assign grid_area to a Floating_Meta component based on its position.""" + pos = comp.get("position", "top-right") + if "top" in pos and "left" in pos: + comp["grid_area"] = "1 / 1 / 2 / 7" + elif "top" in pos: + comp["grid_area"] = "1 / 7 / 2 / 13" + elif "bottom" in pos and "left" in pos: + comp["grid_area"] = "12 / 1 / 13 / 7" + else: + comp["grid_area"] = "12 / 7 / 13 / 13" + + +def _distribute_rows_by_weight(content_comps, start_row=1, end_row=13, full_width=True): + """ + Distribute grid rows among content components proportionally to their content weight. + Each component gets at least 2 rows. Components fill start_row to end_row seamlessly. + """ + nn = len(content_comps) + if nn == 0: + return + + total_rows = end_row - start_row # available rows + + # Calculate weights + weights = [_estimate_content_weight(c) for c in content_comps] + total_weight = sum(weights) + + if total_weight == 0: + total_weight = nn # fallback: equal distribution + weights = [1.0] * nn + + # Allocate rows proportionally, with minimum 2 per component + MIN_ROWS = 2 + raw_rows = [(w / total_weight) * total_rows for w in weights] + + # Ensure minimum, then redistribute excess + allocated = [] + for r in raw_rows: + allocated.append(max(MIN_ROWS, round(r))) + + # Adjust total to exactly fill the available rows + # First pass: if total exceeds available, shrink the largest allocations + while sum(allocated) > total_rows and any(a > MIN_ROWS for a in allocated): + max_idx = max(range(nn), key=lambda i: allocated[i]) + allocated[max_idx] -= 1 + + # Second pass: if total is less than available, grow the heaviest components + while sum(allocated) < total_rows: + max_weight_idx = max(range(nn), key=lambda i: weights[i]) + allocated[max_weight_idx] += 1 + + # Assign grid_area + col_start = 1 + col_end = 13 if full_width else 7 # can be overridden by caller + current_row = start_row + for j, comp in enumerate(content_comps): + rs = current_row + re = min(end_row, current_row + allocated[j]) + comp["grid_area"] = f"{rs} / {col_start} / {re} / {col_end}" + current_row = re + + +def _auto_assign_grid_areas(archetype, components): + """ + Auto-assign grid_area to components that don't have one, + based on the archetype and number of components. + Mutates components in-place. + Skips Page_Ghost_Number (uses absolute positioning, not grid). + + Uses content-aware row distribution: text-heavy components (Glass_Canvas) + get more rows than fixed-height components (Stat_Block, Hero_Typography). + """ + # Filter out ghost numbers — they use absolute positioning, not grid + gridded = [c for c in components if c.get("type") != "Page_Ghost_Number"] + + # Only process components without grid_area + needs_area = [c for c in gridded if not c.get("grid_area")] + if not needs_area: + return # All components already have grid_area + + n = len(gridded) + + if archetype == "cover_hero": + # Cover: stack vertically, full width, spread across page + # Typical: 2-4 components (Hero + Floating_Meta + optional divider/ghost) + if n <= 2: + slots = ["1 / 1 / 7 / 13", "9 / 1 / 13 / 13"] + elif n == 3: + slots = ["1 / 1 / 5 / 13", "5 / 1 / 9 / 13", "10 / 1 / 13 / 13"] + else: + slots = ["1 / 1 / 4 / 13", "4 / 1 / 7 / 13", "8 / 1 / 10 / 13", "10 / 1 / 13 / 13"] + for i, comp in enumerate(gridded): + if not comp.get("grid_area") and i < len(slots): + comp["grid_area"] = slots[i] + + elif archetype == "data_dashboard": + # Dashboard: tile components in a 2-column or 3-column grid + has_area = [c for c in gridded if c.get("grid_area")] + no_area = [c for c in gridded if not c.get("grid_area")] + nn = len(no_area) + if nn <= 2: + slots = ["1 / 1 / 7 / 7", "1 / 7 / 7 / 13"] + elif nn <= 4: + slots = ["1 / 1 / 7 / 7", "1 / 7 / 7 / 13", + "7 / 1 / 13 / 7", "7 / 7 / 13 / 13"] + elif nn <= 6: + slots = ["1 / 1 / 5 / 7", "1 / 7 / 5 / 13", + "5 / 1 / 9 / 7", "5 / 7 / 9 / 13", + "9 / 1 / 13 / 7", "9 / 7 / 13 / 13"] + elif nn <= 9: + slots = ["1 / 1 / 5 / 5", "1 / 5 / 5 / 9", "1 / 9 / 5 / 13", + "5 / 1 / 9 / 5", "5 / 5 / 9 / 9", "5 / 9 / 9 / 13", + "9 / 1 / 13 / 5", "9 / 5 / 13 / 9", "9 / 9 / 13 / 13"] + else: + # Too many: 3-col grid, auto-expand rows + slots = [] + cols = 3 + row_h = max(2, 12 // ((nn + cols - 1) // cols)) + for idx in range(nn): + r = idx // cols + c = idx % cols + rs = 1 + r * row_h + re = min(13, rs + row_h) + cs = 1 + c * 4 + ce = min(13, cs + 4) + slots.append(f"{rs} / {cs} / {re} / {ce}") + j = 0 + for comp in no_area: + if j < len(slots): + comp["grid_area"] = slots[j] + j += 1 + + elif archetype == "editorial_flow": + # Editorial: stack vertically, full width + # Handle Floating_Meta separately — assign to corners based on position + # Content components get rows proportional to their content weight + no_area = [c for c in gridded if not c.get("grid_area")] + content_comps = [] + for comp in no_area: + if comp.get("type") == "Floating_Meta": + _assign_floating_meta(comp) + else: + content_comps.append(comp) + if content_comps: + _distribute_rows_by_weight(content_comps, start_row=1, end_row=13, full_width=True) + + elif archetype == "split_vertical": + # Split: first half on left, second half on right + # Each side gets content-proportional row distribution + no_area = [c for c in gridded if not c.get("grid_area")] + nn = len(no_area) + mid = (nn + 1) // 2 + left = no_area[:mid] + right = no_area[mid:] + # Left column: cols 1-7 + if left: + weights_l = [_estimate_content_weight(c) for c in left] + total_w = sum(weights_l) or 1 + current = 1 + for j, comp in enumerate(left): + rows = max(2, round((weights_l[j] / total_w) * 12)) + rs = current + re = min(13, current + rows) + comp["grid_area"] = f"{rs} / 1 / {re} / 7" + current = re + # Right column: cols 7-13 + if right: + weights_r = [_estimate_content_weight(c) for c in right] + total_w = sum(weights_r) or 1 + current = 1 + for j, comp in enumerate(right): + rows = max(2, round((weights_r[j] / total_w) * 12)) + rs = current + re = min(13, current + rows) + comp["grid_area"] = f"{rs} / 7 / {re} / 13" + current = re + + elif archetype == "scattered_canvas": + # Scatter: distribute pseudo-randomly across the grid + no_area = [c for c in gridded if not c.get("grid_area")] + scatter_slots = [ + "1 / 1 / 5 / 6", "1 / 7 / 4 / 13", "5 / 3 / 9 / 10", + "6 / 1 / 10 / 5", "7 / 8 / 11 / 13", "10 / 2 / 13 / 8", + "10 / 8 / 13 / 13", "3 / 1 / 6 / 5", "1 / 4 / 4 / 10", + ] + for j, comp in enumerate(no_area): + if j < len(scatter_slots): + comp["grid_area"] = scatter_slots[j] + + elif archetype == "shaped_editorial": + # Shaped: main shaped canvas gets most of the page + no_area = [c for c in gridded if not c.get("grid_area")] + for j, comp in enumerate(no_area): + if comp.get("type") == "Shaped_Canvas": + comp["grid_area"] = "2 / 2 / 12 / 12" + elif comp.get("type") == "Floating_Meta": + _assign_floating_meta(comp) + else: + comp["grid_area"] = f"{1 + j * 3} / 1 / {min(13, 4 + j * 3)} / 13" + + # Fallback: if still no grid_area, make full-width stacked rows + # Uses content-aware distribution instead of equal division + remaining = [c for c in gridded if not c.get("grid_area")] + if remaining: + # First, handle Floating_Meta components by position + non_meta = [] + for comp in remaining: + if comp.get("type") == "Floating_Meta": + _assign_floating_meta(comp) + else: + non_meta.append(comp) + # Then distribute rows proportionally to content weight + if non_meta: + _distribute_rows_by_weight(non_meta, start_row=1, end_row=13, full_width=True) + + +def _wrap_grid_item(comp, inner_html): + """Wrap a rendered component in a .grid-item div with grid positioning.""" + grid_area_css = _parse_grid_area(comp) + v_align, h_align = _parse_align(comp) + + # Glass_Canvas and Process_List should stretch to fill their grid area + ctype = comp.get("type", "") + if ctype in ("Glass_Canvas", "Process_List"): + v_align = "stretch" + + style_parts = [] + if grid_area_css: + style_parts.append(f"grid-area: {grid_area_css}") + style_parts.append(f"align-items: {v_align}") + style_parts.append(f"justify-content: {h_align}") + + style = "; ".join(style_parts) + ";" + return f'<div class="grid-item" style="{style}">\n{inner_html}</div>\n' + + +def render_component(comp): + """Convert a JSON component object into HTML string, wrapped in grid-item.""" + # Flatten nested "content" and "style" dicts into top-level for compat + # e.g. {"content": {"heading": "Hi"}} → {"heading": "Hi"} + _content = comp.get("content", None) + if isinstance(_content, dict): + for k, v in _content.items(): + if k not in comp: + comp[k] = v + _style = comp.get("style", None) + if isinstance(_style, dict): + for k, v in _style.items(): + if k not in comp: + comp[k] = v + + ctype = comp.get("type", "") + inner = "" + + if ctype == "Hero_Typography": + weight = comp.get("weight", "black") + heading = comp.get("heading", "") + subheading = comp.get("subheading", "") + # Fallback: if "content" is a plain string, use it as heading + raw_content = comp.get("content", "") + if not heading and isinstance(raw_content, str) and raw_content: + heading = raw_content + # Sanitize consecutive <br> — collapse 3+ into max 2 + heading = re.sub(r'(<br\s*/?>){3,}', '<br><br>', heading) + if subheading: + subheading = re.sub(r'(<br\s*/?>){3,}', '<br><br>', subheading) + scale = comp.get("scale", None) + # Build inline style from both scale and custom style props + style_parts = [] + if scale is not None and 1 <= int(scale) <= 6: + style_parts.append(f"font-size: var(--text-scale-{int(scale)})") + heading_font_size = comp.get("heading_font_size", "") + heading_color = comp.get("heading_color", "") + heading_ls = comp.get("heading_letter_spacing", "") + text_align = comp.get("text_align", "") + if heading_font_size: + style_parts.append(f"font-size: {heading_font_size}") + if heading_color: + style_parts.append(f"color: {heading_color}") + if heading_ls: + style_parts.append(f"letter-spacing: {heading_ls}") + if text_align: + style_parts.append(f"text-align: {text_align}") + h_style = f' style="{"; ".join(style_parts)}"' if style_parts else "" + inner = f'<h1 class="hero-type weight-{weight}"{h_style}>{heading}</h1>\n' + if subheading: + sub_style_parts = [] + sub_fs = comp.get("subheading_font_size", "") + sub_color = comp.get("subheading_color", "") + sub_ls = comp.get("subheading_letter_spacing", "") + if sub_fs: + sub_style_parts.append(f"font-size: {sub_fs}") + if sub_color: + sub_style_parts.append(f"color: {sub_color}") + if sub_ls: + sub_style_parts.append(f"letter-spacing: {sub_ls}") + if text_align: + sub_style_parts.append(f"text-align: {text_align}") + s_style = f' style="{"; ".join(sub_style_parts)}"' if sub_style_parts else "" + inner += f'<p class="hero-sub"{s_style}>{subheading}</p>\n' + + elif ctype == "Glass_Canvas": + md = comp.get("markdown_content", "") or comp.get("body", "") + html_content = simple_markdown_to_html(md) + # Build inline style from custom style props + gs_parts = ["width:100%", "min-height:100%", "box-sizing:border-box"] + + # --- Auto font-size scaling for Glass_Canvas --- + # When content is too long for the allocated grid rows, shrink font-size + # to avoid overflow into adjacent components. + # Estimation: ~80 chars per row at 16px base font-size (with padding). + user_font_size = comp.get("font_size", "") + if not user_font_size: # Only auto-scale when user hasn't set a custom size + grid_area_str = comp.get("grid_area", "") + if grid_area_str: + try: + ga_parts = [int(x.strip()) for x in grid_area_str.split("/")] + allocated_rows = ga_parts[2] - ga_parts[0] # row_end - row_start + content_len = len(md) + chars_per_row = 80 # approximate chars that fit in one grid row at 16px + needed_rows = max(1, content_len / chars_per_row) + if needed_rows > allocated_rows: + # Scale down proportionally, but never below 12px + scale = allocated_rows / needed_rows + new_size = max(12, int(16 * scale)) + if new_size < 16: + gs_parts.append(f"font-size: {new_size}px") + except (ValueError, IndexError): + pass + + for prop in ["background", "border", "border_radius", "padding", "font_size", "color", "line_height", "text_align"]: + val = comp.get(prop, "") + if val: + css_prop = prop.replace("_", "-") + gs_parts.append(f"{css_prop}: {val}") + grid_style = "; ".join(gs_parts) + ";" + tension = comp.get("tension_score", None) + if tension is not None: + weight = int(300 + (float(tension) * 600)) + inner = f'<div class="glass-canvas" style="{grid_style}font-variation-settings: \'wght\' {weight};">{html_content}</div>\n' + else: + inner = f'<div class="glass-canvas" style="{grid_style}">{html_content}</div>\n' + + elif ctype == "Floating_Meta": + pos = comp.get("position", "top-left") + items_html = "".join([f"<span>{item}</span>" for item in comp.get("items", [])]) + fm_style_parts = [] + for prop in ["font_size", "color", "letter_spacing", "text_align"]: + val = comp.get(prop, "") + if val: + fm_style_parts.append(f"{prop.replace('_', '-')}: {val}") + fm_style = f' style="{"; ".join(fm_style_parts)}"' if fm_style_parts else "" + inner = f'<div class="floating-meta pos-{pos}"{fm_style}>{items_html}</div>\n' + + elif ctype == "Stat_Block": + inner = f'''<div class="stat-block"> + <div><span class="stat-num">{comp.get("number", "")}</span><span class="stat-unit">{comp.get("unit", "")}</span></div> + <span class="stat-label">{comp.get("label", "")}</span> + </div>\n''' + + elif ctype == "Hairline_Divider": + style = comp.get("style", "bleed") + inner = f'<hr class="hairline style-{style}">\n' + + elif ctype == "Page_Ghost_Number": + # Ghost numbers are decorative overlays — still use absolute positioning + return f'<div class="page-ghost">{comp.get("number", "")}</div>\n' + + elif ctype == "Shaped_Canvas": + shape = comp.get("shape_keyword", "circle") + md = comp.get("markdown_content", "") or comp.get("body", "") + html_content = simple_markdown_to_html(md) + sc_style_parts = [] + for prop in ["background", "border", "border_radius", "padding"]: + val = comp.get(prop, "") + if val: + sc_style_parts.append(f"{prop.replace('_', '-')}: {val}") + sc_style = f' style="{"; ".join(sc_style_parts)}"' if sc_style_parts else "" + inner = f'''<div class="shaped-canvas"{sc_style}> + <div class="shape-float shape-{shape}" aria-hidden="true"></div> + <div class="shaped-content">{html_content}</div> +</div>\n''' + + elif ctype == "Image_Asset": + src = comp.get("src", "") + alt = comp.get("alt", "") + fit = comp.get("object_fit", "cover") + inner = f'<img src="{src}" alt="{alt}" style="width:100%;height:100%;object-fit:{fit};border-radius:2px;" />\n' + + elif ctype == "Sidenote_Block": + label = comp.get("label", "") + body = comp.get("body", "") or comp.get("markdown_content", "") + html_body = simple_markdown_to_html(body) + label_html = f'<span class="sidenote-label">{label}</span>' if label else "" + inner = f'<div class="sidenote">{label_html}{html_body}</div>\n' + # Sidenotes bypass grid-item wrapping for Tufte layout — returned raw + return inner + + elif ctype == "Delta_Widget": + metric = comp.get("metric", "") + value = comp.get("value", "") + delta = comp.get("delta", "") + trend = comp.get("trend", "up") # up / down / flat + label = comp.get("label", "") + trend_symbol = {"up": "▲", "down": "▼", "flat": "─"}.get(trend, "") + trend_color = {"up": "#22c55e", "down": "#ef4444", "flat": "var(--c-muted)"}.get(trend, "var(--c-muted)") + inner = f'''<div class="delta-widget"> + <div class="delta-metric">{metric}</div> + <div class="delta-value">{value}</div> + <div class="delta-change" style="color:{trend_color}"><span>{trend_symbol}</span> {delta}</div> + <div class="delta-label">{label}</div> +</div>\n''' + + elif ctype == "Process_List": + steps = comp.get("steps", []) + steps_html = "" + for i, step in enumerate(steps): + title = step.get("title", "") + desc = step.get("description", "") + steps_html += f'<li class="process-step"><span class="step-num">{i+1}</span><div><div class="step-title">{title}</div><div class="step-desc">{desc}</div></div></li>\n' + inner = f'<div class="process-list-container"><ul class="process-list">{steps_html}</ul></div>\n' + + else: + return f"<!-- Unknown component: {ctype} -->\n" + + return _wrap_grid_item(comp, inner) + + +def compile_blueprint(json_path, output_html_path): + """Reads the LLM JSON blueprint and generates the final poster.html""" + with open(json_path, 'r', encoding='utf-8') as f: + blueprint = json.load(f) + + art = blueprint.get("art_direction", {}) + # Intent: auto-derive from document title if not explicitly provided + doc_title = blueprint.get("document_meta", {}).get("title", "") + intent = art.get("intent", None) + if not intent: + intent = derive_intent(doc_title) if doc_title else "neutral" + intent = intent.lower() + mode = art.get("palette_mode", "minimal").lower() + harmony = art.get("color_harmony", "auto").lower() # "auto" → intent-based recommendation + svg_type = art.get("background_svg", "flow") + pages = blueprint.get("pages", []) + total_pages = len(pages) + + # 1. Compute Aesthetics — three pillars: intent + mode + harmony + palette = generate_color_palette(intent, mode, harmony=harmony) + css_vars = palette_to_css(palette) + + # Detect if any component uses tension_score → switch to Variable Font URL + has_tension = False + for page in pages: + for comp in page.get("components", []): + if comp.get("tension_score") is not None: + has_tension = True + break + if has_tension: + break + + # Generate SVG backgrounds + canvas_w = art.get("canvas_width", 720) + canvas_h = art.get("canvas_height", 960) + use_continuous = total_pages > 1 # Multi-page → continuous canvas mode + continuous_svgs = None + unified_svg = "" + bg_svg = "" + if use_continuous and svg_type != "none": + # Generate one unified SVG spanning the entire document + unified_svg = generate_unified_svg(canvas_w, canvas_h, total_pages, svg_type, palette['accent']) + elif not use_continuous: + if svg_type == "continuous_flow" and total_pages > 1: + continuous_svgs = generate_continuous_flow_svg(canvas_w, canvas_h, total_pages, palette['accent']) + elif svg_type != "none": + bg_svg = generate_generative_svg(svg_type, canvas_w, canvas_h, palette['accent']) + + # Font URL: use variable axis range if tension is active + if has_tension: + font_url = "https://fonts.googleapis.com/css2?family=Inter:wght@100..900&family=Noto+Sans+SC:wght@300;400;500;700;900&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&display=swap" + else: + font_url = "https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700;900&family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&display=swap" + + # 1b. Dynamic Gap — intent-driven grid density + # If the LLM explicitly sets grid_gap in art_direction, use that (override). + # Otherwise, derive from intent. + explicit_gap = art.get("grid_gap", None) + if explicit_gap is not None: + dynamic_gap = str(explicit_gap) if "px" in str(explicit_gap) else f"{explicit_gap}px" + else: + gap_mapping = { + "serenity": "48px", + "elegance": "32px", + "minimalism": "40px", + "warmth": "24px", + "neutral": "16px", + "tension": "8px", + "energy": "4px", + } + dynamic_gap = gap_mapping.get(intent, "16px") + + # 2. Build override CSS for custom canvas size, background, bleed + override_css = "" + override_css += f":root {{ --canvas-w: {canvas_w}px; --canvas-h: {canvas_h}px; }}\n" + # @page size MUST use concrete values (CSS variables are NOT resolved in @page rules) + override_css += f"@page {{ size: {canvas_w}px {canvas_h}px; margin: 0; }}\n" + # html/body must match canvas size for full-bleed PDF output + # Use min-height (not height) so content taller than canvas_h expands naturally + override_css += f"html, body {{ width: {canvas_w}px; min-height: {canvas_h}px; }}\n" + bg_color = art.get("background_color", "") + if bg_color: + override_css += f".canvas {{ background: {bg_color}; }}\n" + override_css += f".continuous-canvas {{ background: {bg_color}; }}\n" + if art.get("bleed", False): + override_css += ".safe-zone { inset: 0 !important; padding: 0 !important; }\n" + + # Screen preview: inject concrete scale value (CSS calc with var may not work in scale) + override_css += f"@media screen {{ body {{ scale: min(1, calc(100vw / {canvas_w}), calc(100vh / {canvas_h})); }} }}\n" + + # 3. Build HTML Document + html = f"""<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <link href="{font_url}" rel="stylesheet"> + <title>{blueprint.get("document_meta", {}).get("title", "Document")} + + + +""" + + # 3. Render Pages + if use_continuous: + # ═══ CONTINUOUS CANVAS MODE ═══ + # Render all pages as one seamless surface, then let Playwright's page.pdf() slice it. + total_height = canvas_h * total_pages + html += f'\n
\n' + + # Unified background SVG spanning the entire document + if unified_svg: + html += f'
{unified_svg}
\n' + + # Render each page as an absolutely-positioned section within the continuous canvas + for page_idx, page in enumerate(pages): + archetype = page.get("archetype", "cover_hero") + page_top = page_idx * canvas_h + + # Auto-assign grid areas + _auto_assign_grid_areas(archetype, page.get("components", [])) + + # Cognitive Load Margins + total_chars = 0 + sidenotes = [] + main_components = [] + for comp in page.get("components", []): + text_content = comp.get("markdown_content", "") or comp.get("body", "") or "" + total_chars += len(text_content) + if comp.get("type") == "Sidenote_Block": + sidenotes.append(comp) + else: + main_components.append(comp) + for step in comp.get("steps", []): + total_chars += len(step.get("title", "")) + len(step.get("description", "")) + + if total_chars > 500: + safe_inset = "8% 10%" + elif total_chars > 200: + safe_inset = "10% 12%" + else: + safe_inset = "" + safe_inset_style = f" inset: {safe_inset};" if safe_inset else "" + + # Page section: positioned absolutely within continuous canvas + html += f'\n
\n' + + # Per-page data-driven SVG background (overlays on top of unified bg) + if svg_type != "none": + data_points = None + for comp in page.get("components", []): + dp = comp.get("data_points") + if dp and isinstance(dp, list) and all(isinstance(x, (int, float)) for x in dp): + data_points = dp + break + if data_points and len(data_points) >= 2: + page_svg = _generate_data_driven_svg(data_points, canvas_w, canvas_h, palette['accent']) + html += f'
{page_svg}
\n' + + # Tufte layout + if archetype == "tufte_report" and sidenotes: + html += f'
\n' + html += f'
\n' + for comp in main_components: + html += " " + render_component(comp) + html += '
\n' + html += '
\n' + for comp in sidenotes: + html += " " + render_component(comp) + html += '
\n' + html += '
\n' + else: + html += f'
\n' + for comp in page.get("components", []): + html += " " + render_component(comp) + html += '
\n' + + html += '
\n' + + html += '
\n' + + else: + # ═══ LEGACY PER-PAGE MODE (single-page documents) ═══ + for page_idx, page in enumerate(pages): + archetype = page.get("archetype", "cover_hero") + + _auto_assign_grid_areas(archetype, page.get("components", [])) + + total_chars = 0 + sidenotes = [] + main_components = [] + for comp in page.get("components", []): + text_content = comp.get("markdown_content", "") or comp.get("body", "") or "" + total_chars += len(text_content) + if comp.get("type") == "Sidenote_Block": + sidenotes.append(comp) + else: + main_components.append(comp) + for step in comp.get("steps", []): + total_chars += len(step.get("title", "")) + len(step.get("description", "")) + + if total_chars > 500: + safe_inset = "8% 10%" + elif total_chars > 200: + safe_inset = "10% 12%" + else: + safe_inset = "" + safe_inset_style = f" inset: {safe_inset};" if safe_inset else "" + + page_svg = "" + if svg_type != "none": + data_points = None + for comp in page.get("components", []): + dp = comp.get("data_points") + if dp and isinstance(dp, list) and all(isinstance(x, (int, float)) for x in dp): + data_points = dp + break + if data_points and len(data_points) >= 2: + page_svg = _generate_data_driven_svg(data_points, canvas_w, canvas_h, palette['accent']) + elif continuous_svgs and page_idx < len(continuous_svgs): + page_svg = continuous_svgs[page_idx] + elif bg_svg: + page_svg = bg_svg + + html += f'\n
\n' + if page_svg: + html += f'
{page_svg}
\n' + + if archetype == "tufte_report" and sidenotes: + html += f'
\n' + html += f'
\n' + for comp in main_components: + html += " " + render_component(comp) + html += '
\n' + html += '
\n' + for comp in sidenotes: + html += " " + render_component(comp) + html += '
\n' + html += '
\n
\n' + else: + html += f'
\n' + for comp in page.get("components", []): + html += " " + render_component(comp) + html += '
\n\n' + + html += "\n" + + # 4. Save + with open(output_html_path, 'w', encoding='utf-8') as f: + f.write(html) + + return output_html_path, palette + +if __name__ == "__main__": + main() diff --git a/skills/pdf/scripts/html2pdf-next.js b/skills/pdf/scripts/html2pdf-next.js new file mode 100755 index 0000000..38db256 --- /dev/null +++ b/skills/pdf/scripts/html2pdf-next.js @@ -0,0 +1,754 @@ +#!/usr/bin/env node +/** + * html2pdf-next.js — HTML → PDF converter using Playwright + pdf-lib + * + * Drop-in replacement for html2pdf.js, WITHOUT Paged.js dependency. + * Uses Chromium native @page CSS for pagination + pdf-lib for post-processing. + * + * Usage: + * node html2pdf-next.js input.html + * node html2pdf-next.js input.html --output result.pdf + * node html2pdf-next.js input.html --css extra.css + * node html2pdf-next.js input.html --width 720px --height 960px + * node html2pdf-next.js input.html --direct (same as default now — no Paged.js to skip) + * node html2pdf-next.js input.html --merge a.pdf b.pdf (merge additional PDFs after) + * + * Architecture: + * 1. Playwright renders HTML → raw PDF via Chromium's native print engine + * 2. Pre-render hooks: Mermaid, KaTeX, oversized element fixes + * 3. Post-render: pdf-lib for merge, metadata, page count extraction + * 4. No Paged.js, no paged.polyfill.js — CSS @page handles pagination natively + */ + +const fs = require('fs'); +const path = require('path'); +const { execSync, spawnSync } = require('child_process'); + +const sleep = ms => new Promise(r => setTimeout(r, ms)); + +// ═══════════════════════════════════════════════════════════════════ +// Playwright / Chromium resolution (self-contained, no external helper) +// ═══════════════════════════════════════════════════════════════════ + +function loadPlaywright() { + // Try direct require first + try { return require('playwright'); } catch (_) {} + + // Search common global paths + const Module = require('module'); + const roots = new Set(); + if (process.env.PLAYWRIGHT_PATH) roots.add(process.env.PLAYWRIGHT_PATH); + if (process.env.NODE_PATH) { + process.env.NODE_PATH.split(path.delimiter).filter(Boolean).forEach(p => roots.add(p)); + } + try { + const g = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); + if (g) roots.add(g); + } catch (_) {} + + for (const base of roots) { + const pkg = path.join(base, 'playwright', 'package.json'); + if (!fs.existsSync(pkg)) continue; + try { return Module.createRequire(pkg)('playwright'); } catch (_) {} + } + throw new Error('Playwright not found. Install: npm install -g playwright'); +} + +function loadPdfLib() { + try { return require('pdf-lib'); } catch (_) {} + const Module = require('module'); + try { + const g = execSync('npm root -g', { stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); + const pkg = path.join(g, 'pdf-lib', 'package.json'); + if (fs.existsSync(pkg)) return Module.createRequire(pkg)('pdf-lib'); + } catch (_) {} + throw new Error('pdf-lib not found. Install: npm install -g pdf-lib'); +} + +function resolveChromium(chromiumObj, allowInstall = false) { + let exe; + try { exe = chromiumObj.executablePath(); } catch (_) { exe = null; } + + if (exe && fs.existsSync(exe)) { + return { status: 'ok', executablePath: exe }; + } + + // Try system Chrome/Chromium + const candidates = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/usr/bin/chromium-browser', '/usr/bin/chromium', '/usr/bin/google-chrome', + ]; + if (process.env.PLAYWRIGHT_CHROMIUM_PATH) candidates.unshift(process.env.PLAYWRIGHT_CHROMIUM_PATH); + + for (const c of candidates) { + if (fs.existsSync(c)) return { status: 'fallback', executablePath: c }; + } + + if (allowInstall) { + const r = spawnSync('npx', ['playwright', 'install', 'chromium'], { stdio: 'inherit', shell: true }); + if (r.status === 0) { + try { exe = chromiumObj.executablePath(); } catch (_) {} + if (exe && fs.existsSync(exe)) return { status: 'installed', executablePath: exe }; + } + } + + return { status: 'missing', executablePath: exe || '' }; +} + +// ═══════════════════════════════════════════════════════════════════ +// CLI +// ═══════════════════════════════════════════════════════════════════ + +function cli() { + const tokens = process.argv.slice(2); + if (!tokens.length || tokens[0] === '-h' || tokens[0] === '--help') { + console.log(` +Usage: node html2pdf-next.js [options] + +Options: + --output, -o Output PDF path (default: .pdf) + --css Inject extra stylesheet + --width Custom page width (e.g. 720px) + --height Custom page height (e.g. 960px) + --direct (no-op, kept for backward compat — always direct now) + --merge Append additional PDF files after conversion + --title Set PDF document title metadata + --help, -h Show help +`); + process.exit(0); + } + + const inputFile = tokens[0]; + let outputFile = null, customCSS = null, width = null, height = null; + let mergeFiles = [], title = null; + + for (let i = 1; i < tokens.length; i++) { + const t = tokens[i]; + if (t === '--output' || t === '-o') outputFile = tokens[++i]; + else if (t === '--css') customCSS = tokens[++i]; + else if (t === '--width') width = tokens[++i]; + else if (t === '--height') height = tokens[++i]; + else if (t === '--direct') { /* no-op, always direct */ } + else if (t === '--title') title = tokens[++i]; + else if (t === '--merge') { + while (i + 1 < tokens.length && !tokens[i + 1].startsWith('--')) { + mergeFiles.push(tokens[++i]); + } + } + } + + if (!outputFile) { + const p = path.parse(inputFile); + outputFile = path.join(p.dir || '.', p.name + '.pdf'); + } + + return { inputFile, outputFile, customCSS, width, height, mergeFiles, title }; +} + +// ═══════════════════════════════════════════════════════════════════ +// Helpers +// ═══════════════════════════════════════════════════════════════════ + +function prettyBytes(n) { + const units = ['B', 'KB', 'MB', 'GB']; + let u = 0; + while (n >= 1024 && u < units.length - 1) { n /= 1024; u++; } + return `${n.toFixed(1)} ${units[u]}`; +} + +// ═══════════════════════════════════════════════════════════════════ +// Pre-render hooks (run in browser context before PDF export) +// ═══════════════════════════════════════════════════════════════════ + +async function preRenderHooks(page) { + const warnings = []; + + // 1. Wait for Mermaid diagrams + const hasMermaid = await page.evaluate(() => document.querySelectorAll('.mermaid').length > 0); + if (hasMermaid) { + console.log(' ⏳ Waiting for Mermaid diagrams...'); + try { + await page.waitForFunction(() => { + for (const m of document.querySelectorAll('.mermaid')) + if (!m.querySelector('svg') && !m.getAttribute('data-processed')) return false; + return true; + }, { timeout: 30000 }); + await sleep(2000); + console.log(' ✓ Mermaid rendered'); + } catch (_) { + warnings.push('Mermaid rendering timed out (30s)'); + } + } + + // 2. Trigger KaTeX math rendering + const katexStatus = await page.evaluate(() => ({ + lib: typeof renderMathInElement === 'function' || typeof katex !== 'undefined', + rendered: document.querySelectorAll('.katex').length > 0, + raw: /\$[^$]+\$|\$\$[^$]+\$\$|\\\(.*?\\\)|\\\[.*?\\\]/.test(document.body.innerText), + })); + + // Auto-inject KaTeX CDN if raw math detected but library not loaded + if (!katexStatus.lib && katexStatus.raw && !katexStatus.rendered) { + console.log(' ⏳ Auto-injecting KaTeX CDN (math formulas detected but KaTeX not loaded)...'); + await page.addStyleTag({ url: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css' }); + await page.addScriptTag({ url: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js' }); + await page.addScriptTag({ url: 'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/contrib/auto-render.min.js' }); + await sleep(2000); // Wait for CDN scripts to load + // Re-check + const recheckLib = await page.evaluate(() => typeof renderMathInElement === 'function'); + if (recheckLib) { + console.log(' ✓ KaTeX CDN loaded successfully'); + } else { + console.log(' ⚠ KaTeX CDN failed to load — math will render as raw text'); + warnings.push('KaTeX CDN injection failed; math formulas may appear as raw LaTeX code'); + } + } + + // Re-evaluate after potential CDN injection + const katexReady = await page.evaluate(() => ({ + lib: typeof renderMathInElement === 'function' || typeof katex !== 'undefined', + rendered: document.querySelectorAll('.katex').length > 0, + raw: /\$[^$]+\$|\$\$[^$]+\$\$|\\\(.*?\\\)|\\\[.*?\\\]/.test(document.body.innerText), + })); + + if (katexReady.lib && !katexReady.rendered && katexReady.raw) { + console.log(' ⏳ Triggering KaTeX rendering...'); + await page.evaluate(() => { + if (typeof renderMathInElement === 'function') + renderMathInElement(document.body, { + delimiters: [ + { left: '$$', right: '$$', display: true }, + { left: '$', right: '$', display: false }, + { left: '\\(', right: '\\)', display: false }, + { left: '\\[', right: '\\]', display: true }, + ], + throwOnError: false, + }); + }); + await sleep(1000); + console.log(' ✓ KaTeX rendered'); + } else if (katexReady.rendered) { + await sleep(500); // Font loading settle + } + + // 3. Fix oversized elements that prevent page breaks + const nFixed = await page.evaluate(() => { + const LIMIT = 1000; + let n = 0; + document.querySelectorAll( + '[style*="page-break-inside: avoid"],[style*="break-inside: avoid"],' + + '.avoid-break,table,figure,.theorem,.algorithm' + ).forEach(el => { + if (el.getBoundingClientRect().height > LIMIT) { + el.style.pageBreakInside = 'auto'; + el.style.breakInside = 'auto'; + n++; + } + }); + return n; + }); + if (nFixed) { + console.log(` ⚠ Fixed ${nFixed} oversized elements (removed break-inside: avoid)`); + } + + // 4. Detect overflow (horizontal AND vertical) + const overflows = await page.evaluate(() => { + const out = []; + document.querySelectorAll('pre,table,figure,img,svg,.mermaid,blockquote,.equation').forEach(el => { + const hDiff = el.scrollWidth - el.clientWidth; + const vDiff = el.scrollHeight - el.clientHeight; + if (hDiff > 2 || vDiff > 2) out.push({ + tag: el.tagName.toLowerCase(), + cls: el.className || '', + hOverflow: hDiff > 2 ? hDiff : 0, + vOverflow: vDiff > 2 ? vDiff : 0, + preview: (el.textContent || '').slice(0, 50).replace(/\s+/g, ' '), + }); + }); + return out; + }); + if (overflows.length) { + console.log(' ⚠ Overflow detected:'); + overflows.forEach(o => { + const parts = []; + if (o.hOverflow) parts.push(`H +${o.hOverflow}px`); + if (o.vOverflow) parts.push(`V +${o.vOverflow}px`); + console.log(` <${o.tag}${o.cls ? '.' + o.cls.split(' ')[0] : ''}> ${parts.join(', ')}`); + }); + warnings.push(`${overflows.length} element(s) have overflow`); + } + + // 4b. Fix vertical overflow on page-level containers + // When html/body or the main content canvas has a fixed height + overflow:hidden, + // content gets clipped. For documents (html2pdf-next.js), we DON'T expand the + // container to its scrollHeight — that creates an oversized single "page" that + // Playwright splits unevenly. Instead, we remove the fixed height and overflow:hidden + // so content flows naturally and @page CSS handles pagination. + // + // (The old "expand to scrollHeight" logic belongs in html2poster.js where a single + // continuous canvas is the desired output.) + const vOverflowFix = await page.evaluate(() => { + const fixes = []; + // Candidates: html, body, and any direct child of body that acts as a full-page canvas + const candidates = [document.documentElement, document.body]; + const bodyChildren = document.body.children; + for (let i = 0; i < bodyChildren.length; i++) { + const child = bodyChildren[i]; + // Skip SVG defs, script, style elements + const tag = child.tagName.toLowerCase(); + if (tag === 'svg' || tag === 'script' || tag === 'style' || tag === 'link') continue; + candidates.push(child); + // Also check one level deeper (e.g., .canvas > .content) + for (let j = 0; j < child.children.length; j++) { + const grandchild = child.children[j]; + const gtag = grandchild.tagName.toLowerCase(); + if (gtag === 'svg' || gtag === 'script' || gtag === 'style') continue; + candidates.push(grandchild); + } + } + + for (const el of candidates) { + const computed = getComputedStyle(el); + const overflow = computed.overflow || computed.overflowY; + const hasHiddenOverflow = overflow === 'hidden' || overflow === 'clip'; + const diff = el.scrollHeight - el.clientHeight; + + if (hasHiddenOverflow && diff > 5) { + // This element is clipping content vertically + const tag = el.tagName.toLowerCase(); + const id = el.id ? `#${el.id}` : ''; + const cls = el.className ? `.${String(el.className).split(' ')[0]}` : ''; + const selector = `${tag}${id}${cls}`; + + const oldHeight = el.clientHeight; + + // Document mode: remove fixed height + overflow:hidden, + // let @page handle natural pagination + el.style.height = 'auto'; + el.style.minHeight = 'auto'; + el.style.maxHeight = 'none'; + el.style.overflow = 'visible'; + el.style.overflowY = 'visible'; + + fixes.push({ + selector, + oldHeight, + clipped: diff, + }); + } + } + + // After fixing containers, re-measure to get the final content height + const finalHeight = Math.max( + document.documentElement.scrollHeight, + document.body.scrollHeight + ); + + return { fixes, finalHeight }; + }); + + if (vOverflowFix.fixes.length) { + console.log(' ⚠️ Removed fixed height + overflow:hidden — content will paginate naturally:'); + vOverflowFix.fixes.forEach(f => { + console.log(` ${f.selector}: was ${f.oldHeight}px with ${f.clipped}px clipped → now auto (content will flow to next page)`); + }); + } + + // 4c. Convert absolute-bottom elements to document flow + // Elements with `position: absolute; bottom: Npx` inside page containers + // are pinned relative to their containing block. When content paginates + // across multiple @page pages, these elements either overlap with body + // text or land on the wrong page. Fix: convert them to static positioning + // so they participate in normal document flow and paginate naturally. + const absBottomFix = await page.evaluate(() => { + const converted = []; + // Scan inside page-level containers (body children and their children) + const containers = []; + for (let i = 0; i < document.body.children.length; i++) { + const child = document.body.children[i]; + const tag = child.tagName.toLowerCase(); + if (tag === 'svg' || tag === 'script' || tag === 'style' || tag === 'link') continue; + containers.push(child); + } + + for (const container of containers) { + const descendants = container.querySelectorAll('*'); + for (const el of descendants) { + const computed = getComputedStyle(el); + if (computed.position === 'absolute' && computed.bottom !== 'auto' && computed.bottom !== '') { + // Check if this element contains visible text (not just decorative) + const hasText = el.textContent && el.textContent.trim().length > 0; + if (!hasText) continue; + + const tag = el.tagName.toLowerCase(); + const id = el.id ? `#${el.id}` : ''; + const cls = el.className ? `.${String(el.className).split(' ')[0]}` : ''; + const selector = `${tag}${id}${cls}`; + + // Convert to static flow: remove absolute positioning + el.style.position = 'static'; + el.style.bottom = 'auto'; + el.style.left = 'auto'; + el.style.right = 'auto'; + // Preserve horizontal padding/margin from the original left/right values + // by keeping any existing padding or margin on the element + + converted.push({ selector, bottom: computed.bottom }); + } + } + } + return converted; + }); + + if (absBottomFix.length) { + console.log(' ⚠️ Converted absolute-bottom elements to document flow (prevents overlap on multi-page):'); + absBottomFix.forEach(f => { + console.log(` ${f.selector}: was position:absolute;bottom:${f.bottom} → now static (flows with content)`); + }); + } + + // 5. Inject minimal @page CSS fallback + await page.evaluate(() => { + const styles = Array.from(document.querySelectorAll('style')); + const hasPageRule = styles.some(s => (s.textContent || '').includes('@page')); + if (!hasPageRule) { + const s = document.createElement('style'); + s.textContent = `@page { margin: 20mm; }`; + document.head.appendChild(s); + } + }); + + // 6. Fix full-page cover sections for print + // In screen mode, height:100vh = viewport height. In print mode, 100vh ≠ page height. + // Detect elements using 100vh and convert to print-safe page-filling behavior. + const coverFixed = await page.evaluate(() => { + let fixed = 0; + // Find elements with height: 100vh (inline or computed) + const allEls = document.querySelectorAll('*'); + for (const el of allEls) { + const style = el.style; + const computed = getComputedStyle(el); + const isVh = style.height === '100vh' || computed.height === '100vh' || + style.minHeight === '100vh' || computed.minHeight === '100vh'; + // Also detect via class name hints + const isCover = el.classList.contains('cover') || el.classList.contains('cover-page') || + el.id === 'cover' || el.getAttribute('data-role') === 'cover'; + if (isVh || (isCover && el.offsetHeight > 0)) { + // Force the element to fill the print page + el.style.height = '100vh'; + el.style.minHeight = '100vh'; + el.style.pageBreakAfter = 'always'; + el.style.pageBreakInside = 'avoid'; + el.style.boxSizing = 'border-box'; + el.style.overflow = 'hidden'; + fixed++; + } + } + // Inject print-specific CSS to make 100vh work correctly + if (fixed > 0) { + const s = document.createElement('style'); + s.textContent = ` + @media print { + .cover, .cover-page, [data-role="cover"] { + height: 100vh !important; + min-height: 100vh !important; + page-break-after: always !important; + page-break-inside: avoid !important; + overflow: hidden !important; + } + } + `; + document.head.appendChild(s); + } + return fixed; + }); + if (coverFixed) { + console.log(` ✓ Fixed ${coverFixed} full-page cover section(s) for print`); + // Also inject named @page rule for cover with zero margins + await page.evaluate(() => { + const s = document.createElement('style'); + s.textContent = ` + @page cover-page { + margin: 0 !important; + } + @media print { + .cover, .cover-page, [data-role="cover"] { + page: cover-page; + margin: 0 !important; + padding: 40px !important; + } + } + `; + document.head.appendChild(s); + }); + } + + return { warnings, contentHeight: vOverflowFix.finalHeight }; +} + +// ═══════════════════════════════════════════════════════════════════ +// Content statistics (post-render, from PDF or page) +// ═══════════════════════════════════════════════════════════════════ + +async function collectStats(page) { + return page.evaluate(() => { + const body = document.body; + const text = body.innerText || ''; + const zhChars = (text.match(/[\u4e00-\u9fa5]/g) || []).length; + const enWords = (text.match(/[a-zA-Z]+/g) || []).length; + return { + wordCount: zhChars + enWords, + figures: document.querySelectorAll('figure,.figure,img').length, + tables: document.querySelectorAll('table').length, + }; + }); +} + +// ═══════════════════════════════════════════════════════════════════ +// pdf-lib post-processing: page count, metadata, merge +// ═══════════════════════════════════════════════════════════════════ + +async function postProcess(pdfPath, options = {}) { + const { PDFDocument } = loadPdfLib(); + const pdfBytes = fs.readFileSync(pdfPath); + const doc = await PDFDocument.load(pdfBytes); + + // Set metadata + if (options.title) doc.setTitle(options.title); + doc.setProducer('html2pdf-next (Playwright + pdf-lib)'); + doc.setCreationDate(new Date()); + + const pageCount = doc.getPageCount(); + + // Merge additional PDFs + if (options.mergeFiles && options.mergeFiles.length) { + for (const mf of options.mergeFiles) { + if (!fs.existsSync(mf)) { + console.log(` ⚠ Merge file not found: ${mf}`); + continue; + } + console.log(` 📎 Merging: ${path.basename(mf)}`); + const donorBytes = fs.readFileSync(mf); + const donorDoc = await PDFDocument.load(donorBytes); + const copiedPages = await doc.copyPages(donorDoc, donorDoc.getPageIndices()); + copiedPages.forEach(p => doc.addPage(p)); + } + } + + // Save + const finalBytes = await doc.save(); + fs.writeFileSync(pdfPath, finalBytes); + + return { pageCount: doc.getPageCount(), originalPages: pageCount }; +} + +// ═══════════════════════════════════════════════════════════════════ +// Main pipeline +// ═══════════════════════════════════════════════════════════════════ + +async function convert(inputFile, outputFile, customCSS, options = {}) { + const { width, height, mergeFiles, title } = options; + + if (!fs.existsSync(inputFile)) { + console.error(`✗ File not found: ${inputFile}`); + process.exit(1); + } + + const playwright = loadPlaywright(); + const { chromium } = playwright; + + // Resolve browser + const canInstall = process.env.PDF_SKIP_BROWSER_INSTALL !== '1'; + const bInfo = resolveChromium(chromium, canInstall); + + if (bInfo.status === 'missing') { + console.error('\n✗ Chromium not found. Run: npx playwright install chromium\n'); + process.exit(2); + } + if (bInfo.status === 'fallback') { + console.log(`⚠ Using fallback Chromium: ${bInfo.executablePath}`); + } + + const absIn = path.resolve(inputFile); + const absOut = path.resolve(outputFile); + + console.log(`\n🔄 Converting ${path.basename(inputFile)}...`); + console.log(` Engine: Playwright + Chromium native @page (no Paged.js)`); + + // Read and optionally inject CSS + let html = fs.readFileSync(absIn, 'utf-8'); + if (customCSS) { + if (!fs.existsSync(customCSS)) { + console.error(`✗ CSS file not found: ${customCSS}`); + process.exit(1); + } + const tag = ``; + html = html.includes('') ? html.replace('', tag + '\n') : tag + '\n' + html; + // Write modified HTML for Playwright to load + const tmpHtml = absIn + '.tmp.html'; + fs.writeFileSync(tmpHtml, html); + // We'll clean up later + } + + // Launch browser + let browser; + try { + const opts = { headless: true }; + if (bInfo.status === 'fallback') opts.executablePath = bInfo.executablePath; + browser = await chromium.launch(opts); + } catch (err) { + const msg = err.message || ''; + if (msg.includes('shared libraries') || msg.includes('.so')) { + console.error('\n✗ Missing system libraries. Run: npx playwright install-deps chromium\n'); + } else { + console.error(`\n✗ Browser launch failed: ${msg}\n`); + } + process.exit(1); + } + + try { + const page = await browser.newPage(); + const loadFile = customCSS ? absIn + '.tmp.html' : absIn; + await page.goto('file://' + loadFile, { waitUntil: 'networkidle' }); + + // ── Pre-render hooks ── + console.log('\n📋 Pre-render checks:'); + const preRenderResult = await preRenderHooks(page); + const warnings = preRenderResult.warnings; + const measuredContentHeight = preRenderResult.contentHeight; + + // ── Detect continuous-canvas mode (design_engine.py output) ── + const continuousInfo = await page.evaluate(() => { + const el = document.querySelector('.continuous-canvas'); + if (!el) return null; + const root = getComputedStyle(document.documentElement); + return { + width: root.getPropertyValue('--canvas-w').trim() || '720px', + height: root.getPropertyValue('--canvas-h').trim() || '960px', + pages: el.querySelectorAll('.page-section').length, + }; + }); + + if (continuousInfo) { + // Creative PDF: seamless multi-page canvas + console.log(`\n🎨 Continuous canvas: ${continuousInfo.pages} pages @ ${continuousInfo.width} × ${continuousInfo.height}`); + await page.pdf({ + path: absOut, + printBackground: true, + margin: { top: 0, right: 0, bottom: 0, left: 0 }, + width: continuousInfo.width, + height: continuousInfo.height, + }); + } else { + // Standard document + console.log('\n📄 Rendering PDF...'); + const pdfOpts = { + path: absOut, + printBackground: true, + preferCSSPageSize: true, + tagged: true, + }; + + if (width || height) { + if (width) pdfOpts.width = width; + if (height) pdfOpts.height = height; + pdfOpts.margin = { top: 0, right: 0, bottom: 0, left: 0 }; + console.log(` Custom size: ${pdfOpts.width || 'auto'} × ${pdfOpts.height || 'auto'}`); + } else { + // No explicit size: check if @page CSS defines a fixed size + const pageSize = await page.evaluate(() => { + const styles = Array.from(document.querySelectorAll('style')); + for (const s of styles) { + const text = s.textContent || ''; + const match = text.match(/@page\s*\{[^}]*size:\s*([\d.]+)px\s+([\d.]+)px/); + if (match) return { width: parseFloat(match[1]), height: parseFloat(match[2]) }; + } + return null; + }); + + if (pageSize) { + // @page defines a fixed size — use preferCSSPageSize (already set above). + // Playwright will paginate content at @page height boundaries seamlessly. + // This is correct for both posters (seamless multi-page) and documents. + pdfOpts.margin = { top: 0, right: 0, bottom: 0, left: 0 }; + console.log(` @page size: ${pageSize.width}px × ${pageSize.height}px`); + if (measuredContentHeight && measuredContentHeight > pageSize.height + 5) { + const estPages = Math.ceil(measuredContentHeight / pageSize.height); + console.log(` Content height: ${measuredContentHeight}px → ~${estPages} pages`); + } + } else { + pdfOpts.format = 'A4'; + } + } + + await page.pdf(pdfOpts); + } + + // Collect content stats from the page + const stats = await collectStats(page); + + // ── pdf-lib post-processing ── + console.log('\n🔧 Post-processing (pdf-lib):'); + const postResult = await postProcess(absOut, { mergeFiles, title }); + + // Clean up temp HTML + const tmpHtml = absIn + '.tmp.html'; + if (fs.existsSync(tmpHtml)) fs.unlinkSync(tmpHtml); + + // ── Report ── + const sz = fs.statSync(absOut).size; + console.log('\n' + '═'.repeat(40)); + console.log(' PDF Generated Successfully'); + console.log('═'.repeat(40)); + console.log(` File: ${path.basename(absOut)}`); + console.log(` Pages: ${postResult.pageCount}`); + console.log(` Size: ${prettyBytes(sz)}`); + console.log(` Words: ~${stats.wordCount.toLocaleString()}`); + console.log(` Assets: ${stats.figures} figures, ${stats.tables} tables`); + console.log(` Engine: Playwright (no Paged.js)`); + console.log(` Path: ${absOut}`); + + if (mergeFiles && mergeFiles.length && postResult.pageCount > postResult.originalPages) { + console.log(` Merged: +${postResult.pageCount - postResult.originalPages} pages from ${mergeFiles.length} file(s)`); + } + + if (warnings.length) { + console.log('\n⚠ Warnings:'); + warnings.forEach(w => console.log(` · ${w}`)); + } + + // Anomaly detection + if (postResult.pageCount > 1 && stats.wordCount > 0) { + const avgWordsPerPage = stats.wordCount / postResult.pageCount; + if (avgWordsPerPage < 30) { + console.log(`\n⚠ Low content density: ~${Math.round(avgWordsPerPage)} words/page (expected 100+)`); + } + } + + } catch (err) { + console.error('\n✗ Conversion failed:', err.message); + process.exit(1); + } finally { + await browser.close(); + } +} + +// ═══════════════════════════════════════════════════════════════════ +// Entry +// ═══════════════════════════════════════════════════════════════════ + +(async () => { + try { + const args = cli(); + await convert(args.inputFile, args.outputFile, args.customCSS, { + width: args.width, + height: args.height, + mergeFiles: args.mergeFiles, + title: args.title, + }); + } catch (err) { + console.error('Error:', err.message); + process.exit(1); + } +})(); diff --git a/skills/pdf/scripts/html2poster.js b/skills/pdf/scripts/html2poster.js new file mode 100755 index 0000000..6c7b677 --- /dev/null +++ b/skills/pdf/scripts/html2poster.js @@ -0,0 +1,256 @@ +#!/usr/bin/env node +/** + * html2poster.js — Single-page poster/long-image HTML → PDF converter + * + * Purpose: Convert a fixed-width, dynamic-height HTML poster into a single-page + * vector PDF with zero margins. This script is PURPOSE-BUILT for posters and + * infographics — it does NOT handle multi-page documents, A4 pagination, or + * document-style margins. For those, use html2pdf-next.js. + * + * Usage: + * node html2poster.js poster.html + * node html2poster.js poster.html --output out.pdf + * node html2poster.js poster.html --width 720px + * node html2poster.js poster.html --width 720px --max-height 8000 + * + * What it does (in order): + * 1. Load HTML in Playwright + * 2. Force overflow:hidden on .poster/.page containers (clip decorative overflow) + * 3. Inject @page { margin: 0 } (override any existing margin) + * 4. Ensure html/body have margin:0, padding:0, matching background + * 5. Measure .poster scrollHeight (actual content height) + * 6. Generate single-page PDF with exact dimensions + * + * What it does NOT do: + * - No pagination / page breaks + * - No A4 fallback + * - No margin injection (always zero) + * - No cover adaptation + * - No pdf-lib post-processing + * - No continuous-canvas detection + * - No vertical overflow expansion (posters WANT overflow:hidden) + */ + +const fs = require('fs'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +// ── Chromium resolution (shared logic with html2pdf-next.js) ── + +function resolveChromium(chromiumObj) { + let exe; + try { exe = chromiumObj.executablePath(); } catch (_) { exe = null; } + if (exe && fs.existsSync(exe)) return { status: 'ok', executablePath: exe }; + + const candidates = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + '/usr/bin/chromium-browser', '/usr/bin/chromium', '/usr/bin/google-chrome', + ]; + if (process.env.PLAYWRIGHT_CHROMIUM_PATH) candidates.unshift(process.env.PLAYWRIGHT_CHROMIUM_PATH); + + for (const c of candidates) { + if (fs.existsSync(c)) return { status: 'fallback', executablePath: c }; + } + return { status: 'missing', executablePath: exe || '' }; +} + +// ── CLI parsing ── + +function parseArgs(argv) { + const tokens = argv.slice(2); + let input = null, output = null, width = '720px', maxHeight = 16000; + + for (let i = 0; i < tokens.length; i++) { + const t = tokens[i]; + if (t === '--output' || t === '-o') output = tokens[++i]; + else if (t === '--width') width = tokens[++i]; + else if (t === '--max-height') maxHeight = parseInt(tokens[++i], 10); + else if (t === '--help' || t === '-h') { + console.log(` +Usage: node html2poster.js [options] + +Options: + --output, -o Output PDF path (default: input with .pdf extension) + --width Poster width (default: 720px) + --max-height Maximum allowed height in px (default: 16000, safety limit) + -h, --help Show this help +`); + process.exit(0); + } + else if (!input) input = t; + else if (!output) output = t; + } + + if (!input) { + console.error('Error: No input HTML file specified.'); + process.exit(1); + } + + if (!output) { + output = input.replace(/\.html?$/i, '.pdf'); + if (output === input) output = input + '.pdf'; + } + + return { input, output, width, maxHeight }; +} + +// ── Main ── + +async function main() { + const { input, output, width, maxHeight } = parseArgs(process.argv); + const absIn = path.resolve(input); + const absOut = path.resolve(output); + + if (!fs.existsSync(absIn)) { + console.error(`Error: File not found: ${absIn}`); + process.exit(1); + } + + console.log(`\n🖼 html2poster — Single-page poster PDF generator`); + console.log(` Input: ${absIn}`); + console.log(` Output: ${absOut}`); + console.log(` Width: ${width}`); + + // Load Playwright + let playwright; + try { + playwright = require('playwright'); + } catch { + try { + playwright = require('playwright-core'); + } catch { + console.error('Error: playwright or playwright-core not installed.'); + process.exit(1); + } + } + + const { chromium } = playwright; + const bInfo = resolveChromium(chromium); + + if (bInfo.status === 'missing') { + console.error('Error: No Chromium found. Run: npx playwright install chromium'); + process.exit(1); + } + if (bInfo.status === 'fallback') { + console.log(` ⚠ Using fallback Chromium: ${bInfo.executablePath}`); + } + + // Launch browser + const launchOpts = { headless: true }; + if (bInfo.status === 'fallback') launchOpts.executablePath = bInfo.executablePath; + + const browser = await chromium.launch(launchOpts); + + try { + // Use a wide viewport so content doesn't wrap unexpectedly + const widthPx = parseInt(width, 10) || 720; + const page = await browser.newPage({ viewport: { width: widthPx, height: 1200 } }); + + await page.goto('file://' + absIn, { waitUntil: 'networkidle' }); + console.log(`\n ✓ HTML loaded`); + + // ── Step 1: Force overflow:hidden on page containers ── + // Decorative elements with negative offsets or width>100% inflate scrollWidth, + // causing Playwright to shrink content to fit. overflow:hidden clips them. + const overflowFixed = await page.evaluate(() => { + const selectors = ['.poster', '.page', '#poster', '#page']; + let fixed = 0; + for (const sel of selectors) { + const el = document.querySelector(sel); + if (!el) continue; + const computed = getComputedStyle(el); + if (computed.overflow !== 'hidden') { + el.style.overflow = 'hidden'; + fixed++; + } + } + return fixed; + }); + if (overflowFixed > 0) { + console.log(` ✓ Added overflow:hidden to ${overflowFixed} container(s)`); + } + + // ── Step 2: Inject @page { margin: 0 } — override any existing @page rule ── + await page.evaluate(() => { + const s = document.createElement('style'); + // Use !important-equivalent: place at end so it wins cascade + s.textContent = `@page { margin: 0 !important; size: auto; }`; + document.head.appendChild(s); + }); + + // ── Step 3: Ensure html/body have zero margin/padding ── + const bgSync = await page.evaluate(() => { + const html = document.documentElement; + const body = document.body; + html.style.margin = '0'; + html.style.padding = '0'; + body.style.margin = '0'; + body.style.padding = '0'; + + // Sync body background with poster background to avoid color gaps + const poster = document.querySelector('.poster') || document.querySelector('.page'); + if (poster) { + const posterBg = getComputedStyle(poster).backgroundColor; + if (posterBg && posterBg !== 'rgba(0, 0, 0, 0)' && posterBg !== 'transparent') { + body.style.backgroundColor = posterBg; + html.style.backgroundColor = posterBg; + return posterBg; + } + } + return null; + }); + if (bgSync) { + console.log(` ✓ Synced body background: ${bgSync}`); + } + + // ── Step 4: Measure actual content height ── + const measurement = await page.evaluate(() => { + const poster = document.querySelector('.poster') || document.querySelector('.page') || document.body; + return { + scrollHeight: poster.scrollHeight, + scrollWidth: poster.scrollWidth, + offsetWidth: poster.offsetWidth, + selector: poster.className ? '.' + poster.className.split(' ')[0] : poster.tagName, + }; + }); + + console.log(` ✓ Measured: ${measurement.selector} = ${measurement.scrollWidth}×${measurement.scrollHeight}px`); + + if (measurement.scrollWidth > widthPx + 2) { + console.log(` ⚠ WARNING: scrollWidth (${measurement.scrollWidth}px) > width (${widthPx}px)`); + console.log(` Decorative elements may still overflow. Check for position:absolute elements with negative offsets.`); + } + + let contentHeight = measurement.scrollHeight; + if (contentHeight > maxHeight) { + console.log(` ⚠ Content height ${contentHeight}px exceeds max ${maxHeight}px, clamping.`); + contentHeight = maxHeight; + } + if (contentHeight < 100) { + console.log(` ⚠ Content height ${contentHeight}px seems too small, using 960px fallback.`); + contentHeight = 960; + } + + // ── Step 5: Generate PDF ── + console.log(`\n 📄 Generating PDF: ${width} × ${contentHeight}px`); + await page.pdf({ + path: absOut, + width: width, + height: contentHeight + 'px', + printBackground: true, + margin: { top: '0', right: '0', bottom: '0', left: '0' }, + }); + + console.log(`\n ✅ Done: ${absOut}`); + console.log(` Size: ${(fs.statSync(absOut).size / 1024).toFixed(1)} KB`); + + } finally { + await browser.close(); + } +} + +main().catch(err => { + console.error(`\n✗ Fatal: ${err.message}`); + process.exit(1); +}); diff --git a/skills/pdf/scripts/pdf.py b/skills/pdf/scripts/pdf.py new file mode 100755 index 0000000..16221a4 --- /dev/null +++ b/skills/pdf/scripts/pdf.py @@ -0,0 +1,2959 @@ +#!/usr/bin/env python3 +""" +PDF Processing Toolkit — All-in-One CLI + +Usage: + python3 pdf.py [args...] + +Commands: + env.check [--json] Check environment dependencies + env.fix Auto-install missing dependencies + + extract.text [-p pages] + extract.table [-p pages] + extract.image -o + + pages.merge ... -o + pages.split -o + pages.rotate -o [-p pages] + pages.crop -o [-p pages] + + meta.get + meta.set -o -d + meta.brand ... [-o ] [-t title] [-q] + + form.info + form.fill -o -d + form.detail + form.fill-legacy + form.annotate + form.render [--max-dim N] + form.validate + form.check-bbox + + pages.clean -o Remove blank pages + + convert.office [-o ] + convert.html [-o ] [--css ] + convert.blueprint [-o ] + convert.latex [--runs N] [--keep-logs] + + palette.generate [--title "..."] [--mode minimal] [--format python|json|css] + palette.cascade [--title "..."] [--mode minimal] [--format summary|json|css|reportlab] + + code.sanitize + content.sanitize [--apply] Fix content issues (CJK, encoding) + + font.check + + toc.check +""" + +from __future__ import annotations + +import html +import json +import os +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + + +# ═══════════════════════════════════════════════════════════════ +# Section 0: Framework — Output, @cmd registry, CLI parser +# ═══════════════════════════════════════════════════════════════ + +class Output: + """Structured JSON output for all subcommands.""" + + @staticmethod + def success(data: dict): + payload = {"status": "success", "data": data} + sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + raise SystemExit(0) + + @staticmethod + def error(error: str, message: str, hint: Optional[str] = None, code: int = 1): + payload = {"status": "error", "error": error, "message": message} + if hint is not None: + payload["hint"] = hint + sys.stderr.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + raise SystemExit(code) + + @staticmethod + def check_file(filepath: str) -> Path: + target = Path(filepath) + if not target.exists(): + Output.error("FileNotFound", f"File not found: {filepath}", code=2) + return target + + +# Command registry +_COMMANDS: Dict[str, Callable] = {} + + +def cmd(name: str): + """Decorator to register a CLI command under a dotted namespace.""" + def decorator(fn: Callable) -> Callable: + _COMMANDS[name] = fn + return fn + return decorator + + +def _pop_flag(argv: list, short: str, long: str, needs_value: bool = True): + """Extract a flag (and optional value) from *argv* in-place.""" + for idx, tok in enumerate(argv): + if tok in (short, long): + argv.pop(idx) + if needs_value: + if idx < len(argv): + return argv.pop(idx) + Output.error("MissingArg", f"Flag {long} requires a value") + return True + return None + + +def _load_json_arg(argv: list) -> dict: + """Read JSON from -d/--data string or -f/--file path.""" + raw = _pop_flag(argv, "-d", "--data") + if raw is not None: + try: + return json.loads(raw) + except json.JSONDecodeError as exc: + Output.error("InvalidJSON", f"JSON parse error: {exc}") + + fpath = _pop_flag(argv, "-f", "--file") + if fpath is not None: + try: + with open(fpath) as fh: + return json.load(fh) + except Exception as exc: + Output.error("FileError", f"Failed to read file: {exc}") + + Output.error("MissingData", "Requires --data or --file argument") + + +def _resolve_page_indices(range_spec: Optional[str], page_count: int) -> List[int]: + """Turn a human-friendly range string (1-indexed) into a sorted list of 0-based indices.""" + if not range_spec: + return list(range(page_count)) + indices: Set[int] = set() + for segment in range_spec.split(","): + segment = segment.strip() + if "-" in segment: + lo, hi = segment.split("-", 1) + for i in range(int(lo) - 1, min(int(hi), page_count)): + indices.add(i) + else: + val = int(segment) - 1 + if 0 <= val < page_count: + indices.add(val) + return sorted(indices) + + +_SCRIPT_DIR = Path(__file__).resolve().parent + + +# ═══════════════════════════════════════════════════════════════ +# Section 1: env — environment diagnostics and auto-fix +# ═══════════════════════════════════════════════════════════════ + +def _probe_cmd(name: str, version_args: Optional[List[str]] = None) -> Tuple[str, str]: + """Check if a command exists and optionally get its version. Returns (status, detail).""" + path = shutil.which(name) + if path is None: + return ("missing", "") + if version_args is None: + return ("ok", "") + try: + result = subprocess.run( + [path] + version_args, + capture_output=True, text=True, timeout=10 + ) + ver = result.stdout.strip() or result.stderr.strip() + return ("ok", ver) + except Exception: + return ("ok", "") + + +def _probe_python_module(mod_name: str) -> Tuple[str, str]: + """Check if a Python module is importable and get its version.""" + try: + result = subprocess.run( + [sys.executable, "-c", f"import {mod_name}; print(getattr({mod_name}, '__version__', 'installed'))"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0: + return ("ok", result.stdout.strip()) + return ("missing", "") + except Exception: + return ("missing", "") + + +def _probe_node() -> Tuple[str, str]: + s, d = _probe_cmd("node", ["--version"]) + if s == "ok" and d: + d = d.lstrip("v") + return (s, d) + + +def _probe_python() -> Tuple[str, str]: + try: + import platform + return ("ok", platform.python_version()) + except Exception: + return ("ok", "") + + +def _probe_libreoffice() -> Tuple[str, str]: + candidates = [ + "/Applications/LibreOffice.app/Contents/MacOS/soffice", + os.path.expanduser("~/Applications/LibreOffice.app/Contents/MacOS/soffice"), + "/usr/bin/soffice", + "/usr/local/bin/soffice", + "/usr/lib/libreoffice/program/soffice", + "/opt/libreoffice/program/soffice", + "/snap/bin/libreoffice.soffice", + ] + for c in candidates: + if Path(c).is_file(): + return ("ok", "") + for alias in ("soffice", "libreoffice"): + if shutil.which(alias): + return ("ok", "") + return ("missing", "") + + +def _probe_tectonic() -> Tuple[str, str]: + home_bin = Path.home() / "tectonic" + if home_bin.exists() and os.access(home_bin, os.X_OK): + return ("ok", "") + tec_local = _SCRIPT_DIR / "tectonic" + if tec_local.exists() and os.access(tec_local, os.X_OK): + return ("ok", "") + if shutil.which("tectonic"): + return ("ok", "") + return ("missing", "") + + +def _probe_playwright_npm() -> Tuple[str, str]: + """Check if playwright npm package is installed.""" + try: + result = subprocess.run( + ["node", "-e", "console.log(require('playwright/package.json').version)"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0 and result.stdout.strip(): + return ("ok", result.stdout.strip()) + except Exception: + pass + # Try global + try: + result = subprocess.run( + ["npm", "list", "-g", "playwright", "--depth=0"], + capture_output=True, text=True, timeout=15 + ) + import re as _re + m = _re.search(r"playwright@(\S+)", result.stdout) + if m: + return ("ok", m.group(1)) + except Exception: + pass + return ("missing", "") + + +def _probe_chromium() -> Tuple[str, str]: + """Check if Playwright Chromium browser is installed.""" + import platform as _platform + home = Path.home() + if _platform.system() == "Darwin": + cache_dir = home / "Library" / "Caches" / "ms-playwright" + else: + cache_dir = home / ".cache" / "ms-playwright" + if cache_dir.is_dir(): + for entry in sorted(cache_dir.iterdir(), reverse=True): + if "chromium" in entry.name.lower(): + return ("ok", entry.name) + return ("missing", "") + + +@cmd("env.check") +def env_check(argv: list): + """Check environment dependencies.""" + use_json = _pop_flag(argv, "-j", "--json", needs_value=False) + + s_node = _probe_node() + s_pw = _probe_playwright_npm() + s_cr = _probe_chromium() + s_py = _probe_python() + s_pike = _probe_python_module("pikepdf") + s_plumb = _probe_python_module("pdfplumber") + s_lo = _probe_libreoffice() + s_tec = _probe_tectonic() + s_pw_py = _probe_python_module("playwright") + + if use_json: + report = { + "html_route": { + "node": s_node[0], "node_version": s_node[1], + "playwright": s_pw[0], "playwright_version": s_pw[1], + "chromium": s_cr[0], "chromium_detail": s_cr[1], + }, + "process_route": { + "python3": s_py[0], "python3_version": s_py[1], + "pikepdf": s_pike[0], "pikepdf_version": s_pike[1], + "pdfplumber": s_plumb[0], "pdfplumber_version": s_plumb[1], + "playwright_python": s_pw_py[0], "playwright_python_version": s_pw_py[1], + }, + "optional": { + "libreoffice": s_lo[0], + "tectonic": s_tec[0], + }, + } + sys.stdout.write(json.dumps(report, ensure_ascii=False, indent=2) + "\n") + # Determine exit code + rc = 0 + for v in [s_node, s_pw, s_cr, s_py, s_pike, s_plumb]: + if v[0] != "ok": + rc = 2 + break + raise SystemExit(rc) + + # Human-readable output + rc = 0 + + def show(name: str, status: Tuple[str, str], optional: bool = False): + nonlocal rc + s, d = status + if s == "ok": + detail = f" ({d})" if d else "" + print(f" \u2713 {name}{detail}") + elif optional: + print(f" \u25cb {name} (optional, not installed)") + else: + print(f" \u2717 {name} (missing)") + rc = 2 + + print("=== PDF Skill Environment ===\n") + print("--- HTML Route ---") + show("node", s_node) + show("playwright", s_pw) + show("chromium", s_cr) + + print("\n--- Process Route ---") + show("python3", s_py) + show("pikepdf", s_pike) + show("pdfplumber", s_plumb) + if s_pw_py[0] == "ok": + print(f" (playwright-python: {s_pw_py[1]})") + + print("\n--- Optional ---") + show("libreoffice", s_lo, optional=True) + show("tectonic", s_tec, optional=True) + + print("\n=== Install Commands ===") + print(" Node.js: brew install node (macOS) / apt install nodejs (Ubuntu)") + print(" Playwright: npm install -g playwright && npx playwright install chromium") + print(" Python: brew install python3 (macOS) / apt install python3 (Ubuntu)") + print(" pikepdf: pip install pikepdf pdfplumber --user") + print(" LibreOffice: brew install --cask libreoffice (macOS)") + print(" Tectonic: curl -fsSL https://drop-sh.fullyjustified.net | sh") + raise SystemExit(rc) + + +@cmd("env.fix") +def env_fix(argv: list): + """Auto-install missing Python dependencies.""" + modules = { + "pikepdf": "pikepdf", + "pdfplumber": "pdfplumber", + "pypdf": "pypdf", + "pdf2image": "pdf2image", + "PIL": "Pillow", + } + installed = [] + for mod, pkg in modules.items(): + s, _ = _probe_python_module(mod) + if s == "missing": + print(f"Installing {pkg}...") + for attempt in ( + [sys.executable, "-m", "pip", "install", "-q", pkg], + [sys.executable, "-m", "pip", "install", "-q", "--user", pkg], + [sys.executable, "-m", "pip", "install", "-q", "--break-system-packages", pkg], + ): + result = subprocess.run(attempt, capture_output=True, text=True) + if result.returncode == 0: + installed.append(pkg) + break + else: + print(f" Failed to install {pkg}") + + if installed: + print(f"\nInstalled: {', '.join(installed)}") + else: + print("All Python dependencies are already installed.") + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 2: extract — text, tables, and embedded images +# ═══════════════════════════════════════════════════════════════ + +@cmd("extract.text") +def extract_text(argv: list): + """Pull plain text from selected pages.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + page_range = _pop_flag(argv, "-p", "--pages") + + import pdfplumber + src = Output.check_file(pdf_path) + try: + doc = pdfplumber.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + total_pages = len(doc.pages) + target_pages = _resolve_page_indices(page_range, total_pages) + char_total = 0 + page_results = [] + + for pg_idx in target_pages: + content = doc.pages[pg_idx].extract_text() or "" + char_total += len(content) + page_results.append({"page": pg_idx + 1, "chars": len(content), "text": content}) + + doc.close() + Output.success({ + "total_pages": total_pages, + "extracted_pages": len(target_pages), + "total_chars": char_total, + "pages": page_results, + }) + + +@cmd("extract.table") +def extract_table(argv: list): + """Locate and return every table on selected pages.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + page_range = _pop_flag(argv, "-p", "--pages") + + import pdfplumber + src = Output.check_file(pdf_path) + try: + doc = pdfplumber.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + total_pages = len(doc.pages) + target_pages = _resolve_page_indices(page_range, total_pages) + collected = [] + + for pg_idx in target_pages: + for tbl_num, raw_table in enumerate(doc.pages[pg_idx].extract_tables()): + if not raw_table: + continue + sanitised = [ + [(cell.strip() if cell else "") for cell in row] + for row in raw_table + ] + collected.append({ + "page": pg_idx + 1, + "table_index": tbl_num, + "rows": len(sanitised), + "cols": len(sanitised[0]) if sanitised else 0, + "data": sanitised, + }) + + doc.close() + Output.success({ + "total_pages": total_pages, + "extracted_pages": len(target_pages), + "total_tables": len(collected), + "tables": collected, + }) + + +@cmd("extract.image") +def extract_image(argv: list): + """Save every embedded raster image to output dir.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_dir = _pop_flag(argv, "-o", "--output") or "." + + import pikepdf + src = Output.check_file(pdf_path) + dest = Path(out_dir) + dest.mkdir(parents=True, exist_ok=True) + + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + saved = [] + seq = 0 + _EXT_MAP = { + "/DCTDecode": "jpg", + "/FlateDecode": "png", + "/JPXDecode": "jp2", + } + + for page_no, pg in enumerate(doc.pages, 1): + res = pg.get("/Resources") + if res is None or "/XObject" not in res: + continue + for key, ref in res.XObject.items(): + try: + img_obj = doc.get_object(ref.objgen) + if img_obj.get("/Subtype") != "/Image": + continue + seq += 1 + w = int(img_obj.get("/Width", 0)) + h = int(img_obj.get("/Height", 0)) + filt = img_obj.get("/Filter") + ext = _EXT_MAP.get(str(filt) if filt else None, "bin") + fname = f"page{page_no}_img{seq}.{ext}" + out_file = dest / fname + out_file.write_bytes(img_obj.read_raw_bytes()) + saved.append({ + "page": page_no, "name": str(key), "file": str(out_file), + "width": w, "height": h, "format": ext, + }) + except Exception: + continue + + doc.close() + Output.success({"output_dir": str(dest), "total_images": len(saved), "images": saved}) + + +# ═══════════════════════════════════════════════════════════════ +# Section 3: pages — merge, split, rotate, crop +# ═══════════════════════════════════════════════════════════════ + +@cmd("pages.merge") +def pages_merge(argv: list): + """Concatenate several PDF files into one.""" + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + if not argv: + Output.error("MissingArg", "At least one PDF required") + + import pikepdf + sources = [Output.check_file(p) for p in argv] + handles = [] + try: + combined = pikepdf.new() + descriptions = [] + for src in sources: + handle = pikepdf.open(src) + handles.append(handle) + n = len(handle.pages) + descriptions.append(f"{src.name} ({n} pages)") + for pg in handle.pages: + combined.pages.append(pg) + total = len(combined.pages) + combined.save(out_path) + combined.close() + except Exception as exc: + Output.error("MergeError", f"Merge failed: {exc}", code=4) + finally: + for h in handles: + try: + h.close() + except Exception: + pass + + Output.success({"output": out_path, "total_pages": total, "sources": descriptions}) + + +@cmd("pages.split") +def pages_split(argv: list): + """Write each page as a separate single-page PDF.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_dir = _pop_flag(argv, "-o", "--output") or "." + + import pikepdf + src = Output.check_file(pdf_path) + dest = Path(out_dir) + dest.mkdir(parents=True, exist_ok=True) + + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + generated = [] + base = src.stem + try: + for idx, pg in enumerate(doc.pages, 1): + fp = dest / f"{base}_page{idx:03d}.pdf" + single_doc = pikepdf.new() + single_doc.pages.append(pg) + single_doc.save(fp) + single_doc.close() + generated.append(str(fp)) + doc.close() + except Exception as exc: + Output.error("SplitError", f"Split failed: {exc}", code=4) + + Output.success({"output_dir": str(dest), "total_pages": len(generated), "files": generated}) + + +@cmd("pages.rotate") +def pages_rotate(argv: list): + """Rotate selected pages by 90/180/270 degrees.""" + if len(argv) < 2: + Output.error("MissingArg", "pdf path and degrees required") + pdf_path = argv.pop(0) + degrees = int(argv.pop(0)) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + page_range = _pop_flag(argv, "-p", "--pages") + + if degrees not in (90, 180, 270): + Output.error("InvalidDegrees", "Rotation angle must be 90, 180, or 270") + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + targets = _resolve_page_indices(page_range, len(doc.pages)) + try: + for i in targets: + existing = int(doc.pages[i].get("/Rotate", 0)) + doc.pages[i]["/Rotate"] = (existing + degrees) % 360 + doc.save(out_path) + doc.close() + except Exception as exc: + Output.error("RotateError", f"Rotation failed: {exc}", code=4) + + Output.success({"output": out_path, "degrees": degrees, "pages_rotated": len(targets)}) + + +@cmd("pages.crop") +def pages_crop(argv: list): + """Set the media/crop box on selected pages. box = 'left,bottom,right,top' in pt.""" + if len(argv) < 2: + Output.error("MissingArg", "pdf path and crop box required") + pdf_path = argv.pop(0) + box_str = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + page_range = _pop_flag(argv, "-p", "--pages") + + try: + coords = [float(v.strip()) for v in box_str.split(",")] + assert len(coords) == 4 + left, bottom, right, top = coords + except Exception: + Output.error("InvalidBox", "Invalid crop box format, should be: left,bottom,right,top", + hint="Example: 50,50,550,750") + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + targets = _resolve_page_indices(page_range, len(doc.pages)) + try: + arr = pikepdf.Array([left, bottom, right, top]) + for i in targets: + doc.pages[i].mediabox = arr + doc.pages[i].cropbox = arr + doc.save(out_path) + doc.close() + except Exception as exc: + Output.error("CropError", f"Crop failed: {exc}", code=4) + + Output.success({ + "output": out_path, + "box": {"left": left, "bottom": bottom, "right": right, "top": top}, + "pages_cropped": len(targets), + }) + + +@cmd("pages.clean") +def pages_clean(argv: list): + """Remove truly blank pages from a PDF. + + A page is considered blank ONLY if it has exactly 0 text characters AND + 0 images. Pages with even a single character or a tiny image are kept. + + Usage: + pdf.py pages.clean input.pdf -o output.pdf + """ + if not argv: + Output.error("MissingArg", "PDF path required") + pdf_path = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + # --threshold is accepted but ignored (kept for backward compat) + _pop_flag(argv, "-t", "--threshold") + + src = Output.check_file(pdf_path) + + # Phase 1: Detect blank pages using pdfplumber (text extraction) + try: + import pdfplumber + except ImportError: + Output.error("DependencyMissing", "pdfplumber required: pip install pdfplumber") + + blank_indices = [] # 0-indexed + total_pages = 0 + try: + pdf = pdfplumber.open(str(src)) + total_pages = len(pdf.pages) + for i, page in enumerate(pdf.pages): + text = (page.extract_text() or "").strip() + # Check for ANY images on the page (no size filter) + images = page.images if hasattr(page, 'images') else [] + # A page is blank ONLY if it has exactly 0 characters AND 0 images + if len(text) == 0 and len(images) == 0: + blank_indices.append(i) + pdf.close() + except Exception as exc: + Output.error("PDFError", f"Cannot analyze PDF: {exc}", code=3) + + if not blank_indices: + Output.success({ + "output": str(src), + "total_pages": total_pages, + "blank_pages_removed": 0, + "message": "No blank pages found", + }) + return + + # Phase 2: Remove blank pages using pikepdf + try: + import pikepdf + except ImportError: + Output.error("DependencyMissing", "pikepdf required: pip install pikepdf") + + try: + doc = pikepdf.open(str(src)) + # Remove in reverse order to preserve indices + for idx in sorted(blank_indices, reverse=True): + if idx < len(doc.pages): + del doc.pages[idx] + doc.save(out_path) + doc.close() + except Exception as exc: + Output.error("PDFError", f"Cannot remove blank pages: {exc}", code=4) + + remaining = total_pages - len(blank_indices) + Output.success({ + "output": out_path, + "total_pages": total_pages, + "blank_pages_removed": len(blank_indices), + "blank_page_numbers": [i + 1 for i in blank_indices], # 1-indexed for humans + "remaining_pages": remaining, + }) + + +# ═══════════════════════════════════════════════════════════════ +# Section 4: meta — metadata reading, writing, and branding +# ═══════════════════════════════════════════════════════════════ + +_XMP_MAPPING = { + "Title": "dc:title", + "Author": "dc:creator", + "Subject": "dc:description", + "Keywords": "pdf:Keywords", + "Creator": "xmp:CreatorTool", + "Producer": "pdf:Producer", +} +_ACCEPTED_KEYS = set(_XMP_MAPPING.keys()) + + +@cmd("meta.get") +def meta_get(argv: list): + """Read document information and metadata.""" + if not argv: + Output.error("MissingArg", "pdf path required") + + import pikepdf + src = Output.check_file(argv[0]) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + record: dict = { + "pages": len(doc.pages), + "pdf_version": str(doc.pdf_version), + } + + if doc.pages: + mb = doc.pages[0].mediabox + record["page_size"] = { + "width": float(mb[2] - mb[0]), + "height": float(mb[3] - mb[1]), + "unit": "pt", + } + + kv_pairs = {} + if doc.docinfo: + for k in doc.docinfo.keys(): + try: + kv_pairs[str(k).lstrip("/")] = str(doc.docinfo[k]) + except Exception: + pass + record["metadata"] = kv_pairs + record["encrypted"] = doc.is_encrypted + record["has_form"] = "/AcroForm" in doc.Root + record["has_outlines"] = "/Outlines" in doc.Root + + doc.close() + Output.success(record) + + +@cmd("meta.set") +def meta_set(argv: list): + """Update XMP + legacy docinfo metadata fields.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + data = _load_json_arg(argv) + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + # XMP layer + with doc.open_metadata() as xmp: + for raw_key, raw_val in data.items(): + norm = raw_key.title() + xmp_key = _XMP_MAPPING.get(norm) + if xmp_key is None: + continue + try: + xmp[xmp_key] = str(raw_val) + except Exception: + pass + + # Legacy docinfo layer + if not doc.docinfo: + doc.docinfo = pikepdf.Dictionary() + for raw_key, raw_val in data.items(): + norm = raw_key.title() + if norm in _ACCEPTED_KEYS: + doc.docinfo[pikepdf.Name(f"/{norm}")] = pikepdf.String(str(raw_val)) + + doc.docinfo[pikepdf.Name("/ModDate")] = pikepdf.String( + datetime.now().strftime("D:%Y%m%d%H%M%S") + ) + + try: + doc.save(out_path) + doc.close() + except Exception as exc: + Output.error("SaveError", f"Save failed: {exc}", code=4) + + Output.success({"output": out_path, "updated_fields": list(data.keys())}) + + +@cmd("meta.brand") +def meta_brand(argv: list): + """Add Z.ai branding metadata to PDF documents.""" + output_path = _pop_flag(argv, "-o", "--output") + custom_title = _pop_flag(argv, "-t", "--title") + quiet = _pop_flag(argv, "-q", "--quiet", needs_value=False) + + if not argv: + Output.error("MissingArg", "At least one PDF file required") + + # Check if output is specified for multiple files + if output_path and len(argv) > 1: + Output.error("InvalidArg", "--output can only be used with a single input file") + + from pypdf import PdfReader, PdfWriter + + for input_path in argv: + if not os.path.exists(input_path): + print(f"Error: Input file not found: {input_path}", file=sys.stderr) + continue + + try: + reader = PdfReader(input_path) + except Exception as e: + print(f"Error: Cannot open PDF: {e}", file=sys.stderr) + continue + + writer = PdfWriter() + for page in reader.pages: + writer.add_page(page) + + # Determine title + if custom_title: + title = custom_title + else: + original_meta = reader.metadata + if original_meta and original_meta.title and original_meta.title not in ('(anonymous)', 'unspecified', None): + title = original_meta.title + else: + title = os.path.splitext(os.path.basename(input_path))[0] + + writer.add_metadata({ + '/Title': title, + '/Author': 'Z.ai', + '/Creator': 'Z.ai', + '/Producer': 'http://z.ai', + }) + + # Write output + out = output_path if (len(argv) == 1 and output_path) else input_path + try: + with open(out, "wb") as f: + writer.write(f) + except Exception as e: + print(f"Error: Cannot write output file: {e}", file=sys.stderr) + continue + + if not quiet: + print(f"\u2713 Updated metadata for: {os.path.basename(input_path)}") + print(f" Title: {title}") + print(f" Author: Z.ai") + print(f" Creator: Z.ai") + print(f" Producer: http://z.ai") + if out != input_path: + print(f" Output: {out}") + + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 5: form — inspection, filling, annotation, rendering +# ═══════════════════════════════════════════════════════════════ + +# --- form.info (pikepdf-based) --- + +_FIELD_TYPE_MAP = { + "/Tx": "text", + "/Sig": "signature", +} + + +def _classify_field(node) -> str: + """Map a PDF field type token to a human label.""" + ft = str(node.get("/FT", "")) + if ft in _FIELD_TYPE_MAP: + return _FIELD_TYPE_MAP[ft] + flags = int(node.get("/Ff", 0)) + if ft == "/Btn": + return "radio" if (flags & (1 << 15)) else "checkbox" + if ft == "/Ch": + return "dropdown" if (flags & (1 << 17)) else "listbox" + return "unknown" + + +def _extra_props(node, kind: str) -> dict: + """Gather type-specific metadata (options, checked value, etc.).""" + props: dict = {} + if kind == "checkbox": + ap = node.get("/AP") + if ap and "/N" in ap: + states = [str(s) for s in ap["/N"].keys()] + props["states"] = states + props["checked_value"] = next((s for s in states if s != "/Off"), states[0] if states else None) + elif kind in ("dropdown", "listbox"): + raw_opts = node.get("/Opt") + if raw_opts: + props["options"] = [ + {"value": str(item[0]), "label": str(item[1])} if isinstance(item, list) and len(item) >= 2 + else {"value": str(item), "label": str(item)} + for item in raw_opts + ] + elif kind == "radio": + kids = node.get("/Kids") + if kids: + radio_vals = [] + for child in kids: + ap = child.get("/AP") + if ap and "/N" in ap: + radio_vals.extend(str(k) for k in ap["/N"].keys() if str(k) != "/Off") + if radio_vals: + props["options"] = radio_vals + return props + + +def _current_value(node): + v = node.get("/V") + return str(v) if v is not None else None + + +def _gather_fields(doc) -> list: + """Walk the AcroForm field tree iteratively and return a flat list.""" + if "/AcroForm" not in doc.Root: + return [] + acro = doc.Root.AcroForm + if "/Fields" not in acro: + return [] + + page_lookup = {pg.objgen: idx for idx, pg in enumerate(doc.pages)} + results = [] + stack = [(field, "") for field in reversed(list(acro.Fields))] + + while stack: + node, parent_path = stack.pop() + name = str(node.get("/T", "")) + full = f"{parent_path}.{name}" if parent_path else name + + kids = node.get("/Kids") + if kids and any("/T" in k for k in kids): + for kid in reversed(list(kids)): + stack.append((kid, full)) + continue + + kind = _classify_field(node) + if kind == "unknown": + continue + + entry = {"id": full, "type": kind} + val = _current_value(node) + if val: + entry["current_value"] = val + entry.update(_extra_props(node, kind)) + + page_ref = node.get("/P") + if page_ref and hasattr(page_ref, "objgen"): + pg_num = page_lookup.get(page_ref.objgen) + if pg_num is not None: + entry["page"] = pg_num + 1 + + results.append(entry) + + return results + + +@cmd("form.info") +def form_info(argv: list): + """Return structured JSON describing every form field (pikepdf + check_fillable).""" + if not argv: + Output.error("MissingArg", "pdf path required") + + import pikepdf + src = Output.check_file(argv[0]) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + fields = _gather_fields(doc) + if not fields: + Output.success({"has_fields": False, "count": 0, "fields": [], "hint": "This PDF has no fillable form fields"}) + Output.success({"has_fields": True, "count": len(fields), "fields": fields}) + + +@cmd("form.fill") +def form_fill(argv: list): + """Write values into a fillable PDF (pikepdf version).""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + data = _load_json_arg(argv) + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + if "/AcroForm" not in doc.Root or "/Fields" not in doc.Root.AcroForm: + Output.error("NoForm", "This PDF has no form fields") + + known = {f["id"]: f for f in _gather_fields(doc)} + + # Validation + issues = [] + for fid, fval in data.items(): + if fid not in known: + issues.append(f"Field not found: {fid}") + continue + fmeta = known[fid] + ftype = fmeta["type"] + if ftype == "checkbox" and "states" in fmeta: + ok_vals = fmeta["states"] + if fval not in ok_vals and f"/{fval}" not in ok_vals and fval not in ("true", "True", "false", "False", "1", "0"): + issues.append(f"Invalid value for field {fid}, options: {ok_vals} or true/false") + if ftype in ("dropdown", "listbox") and "options" in fmeta: + ok_vals = [o["value"] for o in fmeta["options"]] + if fval not in ok_vals: + issues.append(f"Invalid value for field {fid}, options: {ok_vals}") + if issues: + Output.error("ValidationError", "Field validation failed", hint="; ".join(issues)) + + # Fill + written = 0 + + def _apply(node, parent_path=""): + nonlocal written + name = str(node.get("/T", "")) + full = f"{parent_path}.{name}" if parent_path else name + kids = node.get("/Kids") + if kids and any("/T" in k for k in kids): + for kid in kids: + _apply(kid, full) + return + if full not in data: + return + val = data[full] + kind = _classify_field(node) + if kind == "checkbox": + if val in ("true", "True", "1", True): + ap = node.get("/AP") + if ap and "/N" in ap: + checked_name = next((str(k) for k in ap["/N"].keys() if str(k) != "/Off"), "/Yes") + if not checked_name.startswith("/"): + checked_name = f"/{checked_name}" + node["/V"] = pikepdf.Name(checked_name) + node["/AS"] = pikepdf.Name(checked_name) + else: + node["/V"] = pikepdf.Name("/Off") + node["/AS"] = pikepdf.Name("/Off") + else: + node["/V"] = pikepdf.String(str(val)) + written += 1 + + for field in doc.Root.AcroForm.Fields: + _apply(field) + + acro = doc.Root.AcroForm + if "/NeedAppearances" not in acro: + acro["/NeedAppearances"] = True + + try: + doc.save(out_path) + except Exception as exc: + Output.error("SaveError", f"Save failed: {exc}", code=4) + + Output.success({"output": out_path, "fields_filled": written, "fields_requested": len(data)}) + + +# --- form.detail (pypdf-based detailed field extraction) --- + +def _get_full_annotation_field_id(annotation): + """Build dotted field ID by walking parent chain.""" + 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" + states = field.get("/_States_", []) + if len(states) == 2: + 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 + + +def _get_field_info(reader) -> list: + """Extract detailed field info from a PdfReader, including radio group aggregation.""" + fields = reader.get_fields() + + field_info_by_id = {} + possible_radio_names: Set[str] = set() + + for field_id, field in fields.items(): + 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) + + radio_fields_by_id: Dict[str, dict] = {} + + 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: + 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": [], + } + radio_fields_by_id[field_id]["radio_options"].append({ + "value": on_values[0], + "rect": rect, + }) + + # Filter fields without location + 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), 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 + + +@cmd("form.detail") +def form_detail(argv: list): + """Extract detailed field info (pypdf version) to JSON.""" + if len(argv) < 2: + Output.error("MissingArg", "Usage: form.detail ") + pdf_path = argv[0] + json_output_path = argv[1] + + from pypdf import PdfReader + 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}") + raise SystemExit(0) + + +# --- form.fill-legacy (pypdf version with monkeypatch) --- + +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 + + +def _monkeypatch_pypdf_method(): + """ + Workaround for pypdf bug with selection list fields. + pypdf's get_inherited returns a list of two-element lists for /Opt fields + in selection lists, causing join() to throw TypeError. We patch it to + return just the value strings. + """ + 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 + + +@cmd("form.fill-legacy") +def form_fill_legacy(argv: list): + """Fill fillable form fields (pypdf version with monkeypatch).""" + if len(argv) < 3: + Output.error("MissingArg", "Usage: form.fill-legacy ") + input_pdf = argv[0] + fields_json = argv[1] + output_pdf = argv[2] + + from pypdf import PdfReader, PdfWriter + + _monkeypatch_pypdf_method() + + with open(fields_json) as f: + fields = json.load(f) + + # Group by page number + fields_by_page: Dict[int, dict] = {} + 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) + + 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: + raise SystemExit(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) + + writer.set_need_appearances_writer(True) + + with open(output_pdf, "wb") as f: + writer.write(f) + + print(f"Filled {len(fields_by_page)} page(s) in {output_pdf}") + raise SystemExit(0) + + +# --- form.annotate (annotation-based filling with coordinate transform) --- + +def _transform_coordinates(bbox, image_width, image_height, pdf_width, pdf_height): + """Transform bounding box from image coordinates to PDF coordinates.""" + 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 _normalise_fields_json(raw: dict) -> dict: + """Accept both the current sheet-based schema and the legacy flat schema. + + Current (v2) schema uses ``sheet[].pg/dims/regions[]`` with nested + ``label.bbox``, ``target.bbox``, ``ink{}``. + + Legacy (v1) schema uses ``pages[]`` + ``form_fields[]`` with flat keys + like ``entry_bounding_box``, ``label_bounding_box``, ``entry_text{}``. + + Returns a normalised dict in the **v2** internal format used by all + downstream functions. + """ + # Already v2 + if "sheet" in raw: + return raw + + # Convert legacy → v2 + pages_lut = {p["page_number"]: p for p in raw.get("pages", [])} + sheets: dict = {} # pg -> sheet entry + + for f in raw.get("form_fields", []): + pg = f["page_number"] + if pg not in sheets: + pi = pages_lut.get(pg, {}) + sheets[pg] = { + "pg": pg, + "dims": [pi.get("image_width", 0), pi.get("image_height", 0)], + "regions": [], + } + et = f.get("entry_text", {}) + region = { + "id": f.get("field_label", f.get("description", "")), + "hint": f.get("description", ""), + "label": {"tag": f.get("field_label", ""), "bbox": f.get("label_bounding_box", [0, 0, 0, 0])}, + "target": {"bbox": f.get("entry_bounding_box", [0, 0, 0, 0])}, + "ink": {}, + } + if isinstance(et, dict) and et.get("text"): + region["ink"]["value"] = et["text"] + if "font_size" in et: + region["ink"]["size"] = et["font_size"] + if "font_color" in et: + region["ink"]["color"] = et["font_color"] + if "font" in et: + region["ink"]["font"] = et["font"] + sheets[pg]["regions"].append(region) + + return {"sheet": list(sheets.values())} + + +@cmd("form.annotate") +def form_annotate(argv: list): + """Fill a PDF by adding text annotations (FreeText) defined in fields.json.""" + if len(argv) < 3: + Output.error("MissingArg", "Usage: form.annotate ") + input_pdf = argv[0] + fields_json_path = argv[1] + output_pdf = argv[2] + + from pypdf import PdfReader, PdfWriter + from pypdf.annotations import FreeText + + with open(fields_json_path, "r") as f: + fields_data = _normalise_fields_json(json.load(f)) + + reader = PdfReader(input_pdf) + writer = PdfWriter() + 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] + + annotations = [] + for page_entry in fields_data["sheet"]: + pg = page_entry["pg"] + image_width, image_height = page_entry["dims"] + pdf_width, pdf_height = pdf_dimensions[pg] + + for region in page_entry["regions"]: + ink = region.get("ink", {}) + text = ink.get("value", "") + if not text: + continue + + transformed_box = _transform_coordinates( + region["target"]["bbox"], + image_width, image_height, + pdf_width, pdf_height + ) + + font_name = ink.get("font", "Arial") + font_size = str(ink.get("size", 14)) + "pt" + font_color = ink.get("color", "000000") + + annotation = FreeText( + text=text, + rect=transformed_box, + font=font_name, + font_size=font_size, + font_color=font_color, + border_color=None, + background_color=None, + ) + annotations.append(annotation) + writer.add_annotation(page_number=pg - 1, annotation=annotation) + + with open(output_pdf, "wb") as output: + writer.write(output) + + print(f"Successfully filled PDF form and saved to {output_pdf}") + print(f"Added {len(annotations)} text annotations") + raise SystemExit(0) + + +# --- form.render (PDF to PNG images) --- + +@cmd("form.render") +def form_render(argv: list): + """Convert each page of a PDF to a PNG image.""" + if len(argv) < 2: + Output.error("MissingArg", "Usage: form.render [--max-dim N]") + pdf_path = argv.pop(0) + output_dir = argv.pop(0) + max_dim_str = _pop_flag(argv, "-m", "--max-dim") + max_dim = int(max_dim_str) if max_dim_str else 1000 + + from pdf2image import convert_from_path + + os.makedirs(output_dir, exist_ok=True) + images = convert_from_path(pdf_path, dpi=200) + + for i, image in enumerate(images): + 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") + raise SystemExit(0) + + +# --- form.validate (bounding box validation image) --- + +@cmd("form.validate") +def form_validate(argv: list): + """Create validation images with bounding box rectangles.""" + if len(argv) < 4: + Output.error("MissingArg", "Usage: form.validate ") + page_number = int(argv[0]) + fields_json_path = argv[1] + input_path = argv[2] + output_path = argv[3] + + from PIL import Image, ImageDraw + + with open(fields_json_path, 'r') as f: + data = _normalise_fields_json(json.load(f)) + + img = Image.open(input_path) + draw = ImageDraw.Draw(img) + num_boxes = 0 + + for page_entry in data["sheet"]: + if page_entry["pg"] != page_number: + continue + for region in page_entry["regions"]: + target_box = region["target"]["bbox"] + label_box = region["label"]["bbox"] + draw.rectangle(target_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") + raise SystemExit(0) + + +# --- form.check-bbox (bounding box overlap detection) --- + +@dataclass +class _RectAndField: + rect: list + rect_type: str + field: dict + + +def get_bounding_box_messages(fields_json_stream) -> List[str]: + """Check for overlapping bounding boxes. Returns list of messages (max 20).""" + messages = [] + raw = json.load(fields_json_stream) + data = _normalise_fields_json(raw) + + total_regions = sum(len(pe["regions"]) for pe in data["sheet"]) + messages.append(f"Read {total_regions} regions across {len(data['sheet'])} page(s)") + + 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) + + has_error = False + + for page_entry in data["sheet"]: + pg = page_entry["pg"] + # Collect all rects on this page + rects_and_regions = [] + for region in page_entry["regions"]: + rects_and_regions.append(_RectAndField(region["label"]["bbox"], "label", region)) + rects_and_regions.append(_RectAndField(region["target"]["bbox"], "target", region)) + + for i, ri in enumerate(rects_and_regions): + for j in range(i + 1, len(rects_and_regions)): + rj = rects_and_regions[j] + if rects_intersect(ri.rect, rj.rect): + has_error = True + rid = ri.field.get("id", ri.field.get("hint", "?")) + rjd = rj.field.get("id", rj.field.get("hint", "?")) + if ri.field is rj.field: + messages.append(f"FAILURE: pg {pg} — label/target overlap for `{rid}` ({ri.rect}, {rj.rect})") + else: + messages.append(f"FAILURE: pg {pg} — {ri.rect_type} of `{rid}` ({ri.rect}) overlaps {rj.rect_type} of `{rjd}` ({rj.rect})") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + + # Height check for target rects + if ri.rect_type == "target": + ink = ri.field.get("ink", {}) + if ink.get("value"): + font_size = ink.get("size", 14) + entry_height = ri.rect[3] - ri.rect[1] + if entry_height < font_size: + has_error = True + rid = ri.field.get("id", ri.field.get("hint", "?")) + messages.append(f"FAILURE: pg {pg} — target box height ({entry_height}) for `{rid}` is shorter than font size ({font_size}). Increase box height or decrease ink.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 + + +@cmd("form.check-bbox") +def form_check_bbox(argv: list): + """Check bounding boxes in fields.json for overlaps.""" + if not argv: + Output.error("MissingArg", "Usage: form.check-bbox ") + with open(argv[0]) as f: + messages = get_bounding_box_messages(f) + for msg in messages: + print(msg) + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 6: convert — office, HTML, and LaTeX +# ═══════════════════════════════════════════════════════════════ + +_CONVERTIBLE_EXTENSIONS = frozenset({ + ".docx", ".doc", ".odt", ".rtf", + ".pptx", ".ppt", ".odp", + ".xlsx", ".xls", ".ods", ".csv", + ".txt", ".html", ".htm", +}) + +_SOFFICE_CANDIDATES = [ + "/Applications/LibreOffice.app/Contents/MacOS/soffice", + os.path.expanduser("~/Applications/LibreOffice.app/Contents/MacOS/soffice"), + "/usr/bin/soffice", + "/usr/local/bin/soffice", + "/usr/lib/libreoffice/program/soffice", + "/opt/libreoffice/program/soffice", + "/snap/bin/libreoffice.soffice", +] + + +def _locate_soffice() -> Optional[str]: + """Search for a working soffice binary.""" + for candidate in _SOFFICE_CANDIDATES: + if Path(candidate).is_file(): + return candidate + for alias in ("soffice", "libreoffice"): + found = shutil.which(alias) + if found: + return found + return None + + +@cmd("convert.office") +def convert_office(argv: list): + """Convert an office document to PDF via LibreOffice.""" + if not argv: + Output.error("MissingArg", "input file required") + src_path = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + + src = Output.check_file(src_path) + ext = src.suffix.lower() + + if ext not in _CONVERTIBLE_EXTENSIONS: + Output.error( + "UnsupportedFormat", + f"Unsupported format: {ext}", + hint=f"Supported formats: {', '.join(sorted(_CONVERTIBLE_EXTENSIONS))}", + ) + + binary = _locate_soffice() + if binary is None: + Output.error( + "DependencyMissing", + "LibreOffice not found", + hint="Please install LibreOffice: https://www.libreoffice.org/download/", + ) + + target_dir = Path(out_path).parent if out_path else src.parent + target_dir.mkdir(parents=True, exist_ok=True) + + cmd_list = [binary, "--headless", "--convert-to", "pdf", "--outdir", str(target_dir), str(src)] + + try: + proc = subprocess.run(cmd_list, capture_output=True, text=True, timeout=120) + if proc.returncode != 0: + Output.error("ConvertError", f"Conversion failed: {proc.stderr.strip() or 'Unknown error'}", code=4) + except subprocess.TimeoutExpired: + Output.error("Timeout", "Conversion timeout (>120s)", code=4) + except Exception as exc: + Output.error("ConvertError", f"Conversion failed: {exc}", code=4) + + auto_name = target_dir / f"{src.stem}.pdf" + final = Path(out_path) if out_path else auto_name + + if out_path and final.name != auto_name.name and auto_name.exists(): + auto_name.rename(final) + + if not final.exists(): + Output.error("ConvertError", "Converted PDF file was not generated", code=4) + + Output.success({"input": str(src), "output": str(final), "format": ext}) + + +@cmd("convert.html") +def convert_html(argv: list): + """Convert HTML to PDF via node html2pdf.js.""" + if not argv: + Output.error("MissingArg", "input file required") + + js_path = _SCRIPT_DIR / "html2pdf.js" + if not js_path.exists(): + Output.error("DependencyMissing", "html2pdf.js not found in scripts directory") + + node_path = shutil.which("node") + if not node_path: + Output.error("DependencyMissing", "node not found in PATH") + + cmd_list = [node_path, str(js_path)] + argv + try: + proc = subprocess.run(cmd_list, timeout=180) + raise SystemExit(proc.returncode) + except subprocess.TimeoutExpired: + Output.error("Timeout", "HTML conversion timeout (>180s)", code=4) + except SystemExit: + raise + except Exception as exc: + Output.error("ConvertError", f"HTML conversion failed: {exc}", code=4) + + +# --- convert.latex (tectonic wrapper with log filtering + PDF stats) --- + +_NOISE_RE = re.compile( + r"^note: (?:" + r'"version 2" Tectonic' + r"|Running TeX" + r"|Rerunning TeX because" + r"|Running xdvipdfmx" + r"|downloading " + r"|Skipped writing .* intermediate files" + r")" +) + + +def _find_tectonic() -> Optional[str]: + """Locate the tectonic binary. + + Search order: + 1. scripts/ dir (bundled binary — macOS arm64 only) + 2. ~/tectonic (user-placed binary) + 3. System PATH (package-manager install) + + NOTE: The bundled binary at scripts/tectonic is a macOS arm64 (Apple + Silicon) Mach-O executable. It will NOT run on Linux or Windows. + On other platforms, install tectonic via the system package manager + or download the correct binary — see the error message in + convert_latex() for instructions. + """ + local_bin = _SCRIPT_DIR / "tectonic" + if local_bin.exists() and os.access(local_bin, os.X_OK): + return str(local_bin) + home_bin = Path.home() / "tectonic" + if home_bin.exists() and os.access(home_bin, os.X_OK): + return str(home_bin) + system_bin = shutil.which("tectonic") + return system_bin + + +def _human_size(nbytes: int) -> str: + for unit in ("B", "KB", "MB", "GB"): + if nbytes < 1024: + return f"{nbytes:.2f} {unit}" + nbytes /= 1024 + return f"{nbytes:.2f} TB" + + +def _pdf_stats(pdf_file: Path): + """Return (pages, word_count, image_count) or Nones.""" + try: + from pypdf import PdfReader + except ImportError: + for attempt in ( + [sys.executable, "-m", "pip", "install", "-q", "pypdf"], + [sys.executable, "-m", "pip", "install", "-q", "--break-system-packages", "pypdf"], + [sys.executable, "-m", "pip", "install", "-q", "--user", "pypdf"], + ): + if subprocess.run(attempt, check=False, capture_output=True).returncode == 0: + break + try: + from pypdf import PdfReader + except ImportError: + return None, None, None + + try: + reader = PdfReader(str(pdf_file)) + n_pages = len(reader.pages) + all_text = "".join(p.extract_text() or "" for p in reader.pages) + n_words = len([w for w in all_text.split() if w.strip()]) + n_images = 0 + for pg in reader.pages: + xobj = pg.get("/Resources", {}).get("/XObject") + if xobj: + obj = xobj.get_object() + n_images += sum(1 for k in obj if obj[k].get("/Subtype") == "/Image") + return n_pages, n_words, n_images + except Exception as exc: + print(f"Error extracting PDF info: {exc}", file=sys.stderr) + return None, None, None + + +def _classify_lines(lines): + """Bucket raw output into errors / warnings / layout issues.""" + errors, warnings, layout, pdf_note = [], [], [], None + for raw in lines: + ln = raw.rstrip() + if not ln: + continue + if _NOISE_RE.match(ln): + if ln.startswith("note: Writing"): + pdf_note = ln + continue + if ln.startswith("error:"): + errors.append(ln) + elif ln.startswith("warning:"): + warnings.append(ln) + elif re.search(r"(Overfull|Underfull) \\[hv]box", ln) or re.search(r"(Font shape|Missing character)", ln): + layout.append(ln) + return errors, warnings, layout, pdf_note + + +def _parse_writing_note(note: Optional[str]): + m = re.search(r"Writing `(.+?)` \((.+?)\)", note or "") + return (m.group(1), m.group(2)) if m else (None, None) + + +@cmd("convert.blueprint") +def convert_blueprint(argv: list): + """ + [Auto-Pipeline] Extract JSON blueprint from LLM markdown response, + compile it to HTML via design_engine, and render to PDF. + """ + if not argv: + Output.error("MissingArg", "Usage: convert.blueprint [-o ]") + + input_file = argv.pop(0) + out_pdf = _pop_flag(argv, "-o", "--output") or "output.pdf" + + src = Output.check_file(input_file) + content = src.read_text(encoding="utf-8") + + # 1. Smart JSON extraction (regardless of whether LLM wrapped in Markdown code blocks) + match = re.search(r'```(?:json)?\s*(\{.*?\})\s*```', content, re.DOTALL) + if match: + json_str = match.group(1) + else: + # Fallback: Assume the file is pure JSON + json_str = content + + try: + # Validate JSON format + parsed_json = json.loads(json_str) + except json.JSONDecodeError as e: + Output.error("InvalidJSON", f"Failed to parse JSON blueprint: {e}", hint="Ensure the LLM output is valid JSON without trailing commas.") + + # 1b. Encoding integrity check — detect corrupted characters before rendering + corruption_found = False + def _scan_for_corruption(obj, path=""): + """Recursively scan JSON values for encoding corruption markers.""" + nonlocal corruption_found + if isinstance(obj, str): + for i, ch in enumerate(obj): + if ch == '\ufffd': # Unicode replacement character + context = obj[max(0, i-10):i+10] + print(f" \u26a0\ufe0f U+FFFD at {path}[{i}]: ...{context}...") + corruption_found = True + elif '\ud800' <= ch <= '\udfff': # Lone surrogate + print(f" \u26a0\ufe0f Lone surrogate U+{ord(ch):04X} at {path}[{i}]") + corruption_found = True + elif isinstance(obj, dict): + for k, v in obj.items(): + _scan_for_corruption(v, f"{path}.{k}") + elif isinstance(obj, list): + for idx, v in enumerate(obj): + _scan_for_corruption(v, f"{path}[{idx}]") + + _scan_for_corruption(parsed_json) + if corruption_found: + print("\n\u26a0\ufe0f WARNING: Corrupted characters detected in blueprint content!") + print(" These will render as \ufffd (replacement character) in the PDF.") + print(" Consider fixing the source text and re-running.\n") + + # Also sanitize: replace U+FFFD with empty string to avoid visible corruption + json_str_clean = json_str.replace('\ufffd', '') + if json_str_clean != json_str: + json_str = json_str_clean + print(" \u2139\ufe0f Auto-removed U+FFFD characters from blueprint.") + + # 2. Save clean JSON blueprint and temp HTML path + blueprint_path = src.parent / f"{src.stem}_pure_blueprint.json" + html_path = src.parent / f"{src.stem}_rendered.html" + blueprint_path.write_text(json_str, encoding="utf-8") + + # 3. Call design_engine.py to compile HTML + engine_script = _SCRIPT_DIR / "design_engine.py" + print("🎨 [1/2] Compiling JSON Blueprint to Art-Directed HTML...", flush=True) + try: + subprocess.run([ + sys.executable, str(engine_script), "compile", + "--blueprint", str(blueprint_path), + "--output", str(html_path) + ], check=True) + except subprocess.CalledProcessError: + Output.error("CompileError", "design_engine.py failed to compile the blueprint.", code=4) + + # 4. Call html2pdf.js to render PDF + print("📄 [2/2] Rendering HTML to High-Res PDF via Playwright...", flush=True) + js_script = _SCRIPT_DIR / "html2pdf.js" + node_path = shutil.which("node") + if not node_path: + Output.error("DependencyMissing", "node not found in PATH") + + try: + subprocess.run([ + node_path, str(js_script), str(html_path), "-o", out_pdf, + "--width", "720px", "--height", "960px" + ], check=True) + except subprocess.CalledProcessError: + Output.error("RenderError", "html2pdf.js failed to render the PDF.", code=4) + + print(f"\n🎉 Success! Masterpiece generated at: {out_pdf}") + raise SystemExit(0) + + +@cmd("convert.latex") +def convert_latex(argv: list): + """Compile LaTeX file via tectonic, filter logs, report PDF stats.""" + if not argv: + Output.error("MissingArg", "tex file required") + tex_file = argv.pop(0) + runs_str = _pop_flag(argv, "-r", "--runs") + runs = int(runs_str) if runs_str else 1 + keep_logs = _pop_flag(argv, "-k", "--keep-logs", needs_value=False) + + tex = Path(tex_file) + if not tex.exists(): + print(f"\u2717 Error: File not found {tex_file}") + raise SystemExit(1) + + print(f"Compiling {tex.name}...", flush=True) + if runs > 1: + print(f"Running {runs} passes (for cross-references)", flush=True) + + tectonic = _find_tectonic() + if tectonic is None: + import platform + print("\n\u2717 Error: tectonic command not found") + print() + print("The bundled scripts/tectonic binary is macOS arm64 only.") + print("Install tectonic for your platform:\n") + sys_name = platform.system() + if sys_name == "Darwin": + print(" macOS (Homebrew): brew install tectonic") + print(" macOS (binary): curl -sSL https://drop-sh.fullyjustified.net | sh") + print(" mv tectonic ~/tectonic && chmod +x ~/tectonic") + elif sys_name == "Linux": + print(" Debian/Ubuntu: apt install tectonic (if available)") + print(" Arch Linux: pacman -S tectonic") + print(" Conda: conda install -c conda-forge tectonic") + print(" Binary download: curl -sSL https://drop-sh.fullyjustified.net | sh") + print(" mv tectonic ~/tectonic && chmod +x ~/tectonic") + elif sys_name == "Windows": + print(" Windows (scoop): scoop install tectonic") + print(" Windows (choco): choco install tectonic") + print(" Conda: conda install -c conda-forge tectonic") + else: + print(" See: https://tectonic-typesetting.github.io/") + print() + print("After installing, verify with: tectonic --version") + print() + print("Quick check — is tectonic already installed somewhere?") + print(f" which tectonic: {shutil.which('tectonic') or 'not found'}") + print(f" ~/tectonic: {'exists' if (Path.home() / 'tectonic').exists() else 'not found'}") + print(f" scripts/tectonic: {'exists' if (_SCRIPT_DIR / 'tectonic').exists() else 'not found'}") + print(f" Platform: {sys_name} {platform.machine()}") + raise SystemExit(1) + + all_lines = [] + ok = False + for _ in range(runs): + try: + proc = subprocess.run( + [tectonic, "-X", "compile", str(tex)], + capture_output=True, text=True, timeout=120, + ) + all_lines.extend((proc.stdout + proc.stderr).splitlines()) + ok = proc.returncode == 0 + if not ok: + break + except subprocess.TimeoutExpired: + print("\n\u2717 Error: Compilation timeout (>2 minutes)") + raise SystemExit(1) + except Exception as exc: + print(f"\n\u2717 Error: {exc}") + raise SystemExit(1) + + if keep_logs: + print("\n" + "=" * 50 + "\nFull logs:\n" + "=" * 50) + for ln in all_lines: + print(ln) + print("=" * 50 + "\n") + + errors, warnings, layout, pdf_note = _classify_lines(all_lines) + noted_name, noted_size = _parse_writing_note(pdf_note) + pdf_name = noted_name or (tex.stem + ".pdf") + pdf_path = tex.parent / pdf_name + + print() + if ok: + tag = "\u2713 Compilation successful" + (" (with warnings)" if warnings or layout else "") + print(tag) + else: + print("\u2717 Compilation failed") + + if ok and pdf_path.exists(): + print("\n========================\nPDF Information\n========================") + print(f"File: {pdf_name}") + print(f"Size: {noted_size or _human_size(pdf_path.stat().st_size)}") + pages, words, images = _pdf_stats(pdf_path) + if pages is not None: + print(f"Pages: {pages}") + if words is not None: + print(f"Words: ~{words:,}") + if images is not None: + print(f"Images: {images}") + + if layout: + print(f"\n========================\nLayout Issues ({len(layout)})\n========================") + for ln in layout: + print(ln) + + if warnings: + print(f"\n========================\nWarnings ({len(warnings)})\n========================") + for ln in warnings: + print(ln.replace("warning: ", "", 1)) + + if errors: + print("\n========================\nErrors\n========================") + for ln in errors: + print(ln.replace("error: ", "", 1)) + + if ok and (layout or warnings): + print() + print("") + print(f"Detected {len(layout)} layout issues and {len(warnings)} warnings.") + print("These issues affect PDF typesetting quality and must be fixed.") + print("Do not dismiss with 'warnings don't affect output'. Fix all issues.") + print("") + + raise SystemExit(0 if ok else 1) + + +# ═══════════════════════════════════════════════════════════════ +# Section 7b: Font Fallback Engine — automatic wrapping +# ═══════════════════════════════════════════════════════════════ + +# -- Data structures -------------------------------------------------------- + +FONT_FALLBACK_CHAIN: Dict[str, List[str]] = { + "Times New Roman": ["SimHei"], + "Calibri": ["SimHei"], + "DejaVuSans": ["SimHei"], + "SimHei": ["Times New Roman"], + "Microsoft YaHei": ["Times New Roman"], +} + +FONT_PREFER_RANGES: Dict[str, List[Tuple[int, int, str]]] = { + "SimHei": [ + (0x0400, 0x04FF, "Times New Roman"), # Cyrillic + (0x0500, 0x052F, "Times New Roman"), # Cyrillic Supplement + ], + "Microsoft YaHei": [ + (0x0400, 0x04FF, "Times New Roman"), + (0x0500, 0x052F, "Times New Roman"), + ], +} + +# -- Content sanitization (runtime, pre-font-fallback) ---------------------- + +# Control: global switches +CONTENT_SANITIZE_ENABLED = True # master switch +CONTENT_SANITIZE_STRIP_ZW = True # zero-width chars (disable for Thai/Burmese/Hindi) + +# Fullwidth ASCII → halfwidth (letters + digits only, NOT punctuation) +_FULLWIDTH_OFFSET = 0xFEE0 # ord('A') - ord('A') == 0xFEE0 + +# Ligature decomposition (U+FB00–FB06) +_LIGATURE_MAP: Dict[int, str] = { + 0xFB00: "ff", + 0xFB01: "fi", + 0xFB02: "fl", + 0xFB03: "ffi", + 0xFB04: "ffl", + 0xFB05: "st", # ſt long s t + 0xFB06: "st", # st st +} + +# Zero-width / invisible characters to strip +_ZERO_WIDTH_CHARS: Set[int] = { + 0x200B, # ZERO WIDTH SPACE + 0x200C, # ZERO WIDTH NON-JOINER + 0x200D, # ZERO WIDTH JOINER + 0x2060, # WORD JOINER + 0xFEFF, # BOM (when not at file start) + 0x034F, # COMBINING GRAPHEME JOINER +} + +# Bidirectional control characters to strip +_BIDI_CONTROLS: Set[int] = { + 0x200E, 0x200F, # LRM, RLM + 0x202A, 0x202B, 0x202C, # LRE, RLE, PDF + 0x202D, 0x202E, # LRO, RLO + 0x2066, 0x2067, 0x2068, 0x2069, # LRI, RLI, FSI, PDI +} + +# Variation selectors to strip (emoji style modifiers) +_VARIATION_SELECTORS: Set[int] = set(range(0xFE00, 0xFE10)) # U+FE00–FE0F + +# Unicode noncharacters (should never appear in text interchange) +_NONCHARACTERS: Set[int] = set(range(0xFDD0, 0xFDF0)) # U+FDD0–FDEF +_NONCHARACTERS.add(0xFFFE) +_NONCHARACTERS.add(0xFFFF) + +_content_sanitize_warnings: List[str] = [] + + +def content_sanitize(text: str, dry_run: bool = False) -> str: + """Sanitize Paragraph text content before font fallback. + + Removes/replaces characters that should not appear in final PDF output. + Designed for CJK + Latin content. Does NOT touch XML/HTML tags in the text. + + Args: + text: Raw Paragraph text (may contain ReportLab XML tags like , ). + dry_run: If True, collect warnings but still return cleaned text. + + Returns: + Cleaned text string. + """ + if not CONTENT_SANITIZE_ENABLED or not text: + return text + + global _content_sanitize_warnings + if dry_run: + _content_sanitize_warnings = [] + + # Split text into tags and plain-text segments to avoid mangling XML tags + # e.g. '你好' → ['', '', '你好', '', ''] + parts = _TAG_RE.split(text) + tags = _TAG_RE.findall(text) + + out_pieces: List[str] = [] + + for i, part in enumerate(parts): + if part: + # Process plain text segment character by character + cleaned: List[str] = [] + for ch in part: + code = ord(ch) + replacement = _sanitize_one_char(ch, code, dry_run) + if replacement is not None: + cleaned.append(replacement) + # else: character was deleted + out_pieces.append("".join(cleaned)) + + # Append the tag that follows this part (if any) + if i < len(tags): + out_pieces.append(tags[i]) # tags pass through untouched + + return "".join(out_pieces) + + +def _sanitize_one_char(ch: str, code: int, dry_run: bool) -> Optional[str]: + """Process a single character. Returns replacement string, or None to delete.""" + + # --- DELETE: ASCII control characters (except \t \n \r) --- + if code <= 0x1F and ch not in '\t\n\r': + if dry_run: + _content_sanitize_warnings.append( + f"DELETE control char U+{code:04X} ({repr(ch)})") + return None + + # --- DELETE: U+007F DEL --- + if code == 0x7F: + return None + + # --- DELETE: zero-width characters --- + if CONTENT_SANITIZE_STRIP_ZW and code in _ZERO_WIDTH_CHARS: + if dry_run: + _content_sanitize_warnings.append( + f"DELETE zero-width U+{code:04X}") + return None + + # --- DELETE: bidirectional controls --- + if code in _BIDI_CONTROLS: + if dry_run: + _content_sanitize_warnings.append( + f"DELETE bidi control U+{code:04X}") + return None + + # --- DELETE: variation selectors --- + if code in _VARIATION_SELECTORS: + if dry_run: + _content_sanitize_warnings.append( + f"DELETE variation selector U+{code:04X}") + return None + + # --- DELETE: Unicode noncharacters --- + if code in _NONCHARACTERS: + return None + + # --- REPLACE: U+FFFD replacement character → '?' --- + if code == 0xFFFD: + if dry_run: + _content_sanitize_warnings.append( + "REPLACE U+FFFD (upstream encoding error) → '?'") + return '?' + + # --- REPLACE: fullwidth ASCII letters/digits → halfwidth --- + # U+FF21–FF3A = A–Z, U+FF41–FF5A = a–z, U+FF10–FF19 = 0–9 + if 0xFF21 <= code <= 0xFF3A or 0xFF41 <= code <= 0xFF5A or 0xFF10 <= code <= 0xFF19: + halfwidth = chr(code - _FULLWIDTH_OFFSET) + if dry_run: + _content_sanitize_warnings.append( + f"REPLACE fullwidth '{ch}' → halfwidth '{halfwidth}'") + return halfwidth + + # --- REPLACE: ligatures → decomposed --- + if code in _LIGATURE_MAP: + decomposed = _LIGATURE_MAP[code] + if dry_run: + _content_sanitize_warnings.append( + f"REPLACE ligature '{ch}' (U+{code:04X}) → '{decomposed}'") + return decomposed + + # --- REPLACE: line/paragraph separators → newline --- + if code == 0x2028 or code == 0x2029: + if dry_run: + _content_sanitize_warnings.append( + f"REPLACE U+{code:04X} separator → '\\n'") + return '\n' + + # --- REPLACE: soft hyphen → empty (invisible, ReportLab ignores it) --- + if code == 0x00AD: + return None + + # --- WARN (pass through): Private Use Area --- + if 0xE000 <= code <= 0xF8FF: + if dry_run: + _content_sanitize_warnings.append( + f"WARN Private Use Area U+{code:04X} (passed through)") + return ch + + # --- PASS: everything else --- + return ch + + +# -- Glyph detection ------------------------------------------------------- + +_glyph_cache: Dict[Tuple[str, int], bool] = {} + + +def _has_glyph(font_name: str, code: int) -> bool: + """Check whether *font_name* has a real glyph outline for *code*.""" + key = (font_name, code) + if key in _glyph_cache: + return _glyph_cache[key] + try: + from reportlab.pdfbase import pdfmetrics + font = pdfmetrics.getFont(font_name) + result = code in font.face.charToGlyph + except Exception: + result = False + _glyph_cache[key] = result + return result + + +def _best_font_for_char(code: int, base_font: str) -> str: + """Return the best registered font for a single character *code*.""" + # ASCII fast path + if code < 128: + return base_font + + # Aesthetic preference ranges + for rng_start, rng_end, preferred in FONT_PREFER_RANGES.get(base_font, []): + if rng_start <= code <= rng_end: + if _has_glyph(preferred, code): + return preferred + + # Base font has the glyph — use it + if _has_glyph(base_font, code): + return base_font + + # Walk the fallback chain + for fb in FONT_FALLBACK_CHAIN.get(base_font, []): + if _has_glyph(fb, code): + return fb + + # Nothing found — return base_font (will render □ but won't crash) + return base_font + + +# -- Text-level automatic wrapping ---------------------------------- + +_TAG_RE = re.compile(r"]*>") + + +def font_fallback(text: str, base_font: str) -> str: + """Wrap characters that *base_font* cannot render with ```` tags. + + Preserves existing XML tags (````, ````, ````, etc.). + Returns the transformed markup string. + """ + if not text: + return text + + # Split text into (plain, tag, plain, tag, ...) segments + parts = _TAG_RE.split(text) + tags = _TAG_RE.findall(text) + + # Track nesting so we don't re-wrap inside explicit + font_stack: List[str] = [] + out_pieces: List[str] = [] + + for i, part in enumerate(parts): + # Determine effective font at this point + effective = font_stack[-1] if font_stack else base_font + + if part: + # Process plain text character by character + runs: List[Tuple[str, List[str]]] = [] + for ch in part: + code = ord(ch) + best = _best_font_for_char(code, effective) + if runs and runs[-1][0] == best: + runs[-1][1].append(ch) + else: + runs.append((best, [ch])) + + for font_name, chars in runs: + segment = "".join(chars) + if font_name == effective: + out_pieces.append(segment) + else: + out_pieces.append(f'{segment}') + + # Append the tag that follows this part (if any) + if i < len(tags): + tag = tags[i] + out_pieces.append(tag) + # Track font stack + tag_lower = tag.lower() + if tag_lower.startswith("" and font_stack: + font_stack.pop() + + return "".join(out_pieces) + + +# -- Monkey-patch installer ------------------------------------------------- + +_fallback_installed = False + + +def install_font_fallback(): + """Monkey-patch ``Paragraph.__init__`` so every Paragraph automatically + runs ``font_fallback()`` on its text. Idempotent — safe to call + multiple times. + """ + global _fallback_installed + if _fallback_installed: + return + + try: + from reportlab.platypus import Paragraph as _Para + except ImportError: + return + + _orig_init = _Para.__init__ + + def _patched_init(self, text, style=None, *args, **kwargs): + if isinstance(text, str): + text = content_sanitize(text) # Layer 1: clean dangerous chars + if style is not None: + base_font = getattr(style, "fontName", None) + if base_font and base_font in FONT_FALLBACK_CHAIN: + text = font_fallback(text, base_font) # Layer 2: font selection + _orig_init(self, text, style, *args, **kwargs) + + _Para.__init__ = _patched_init + _fallback_installed = True + + +# -- Post-generation glyph check ------------------------------------------- + +def check_missing_glyphs(pdf_path: str) -> List[Dict[str, Any]]: + """Scan a PDF for .notdef glyphs, control chars, and other problematic characters. + + Returns a list of dicts with keys: page, position, context, kind. + Empty list means no issues found. + """ + try: + import fitz # PyMuPDF + except ImportError: + print("Warning: PyMuPDF (fitz) not installed — cannot check missing glyphs", file=sys.stderr) + return [] + + issues: List[Dict[str, Any]] = [] + doc = fitz.open(pdf_path) + + for page_num in range(len(doc)): + page = doc[page_num] + text = page.get_text() + for i, ch in enumerate(text): + code = ord(ch) + kind: Optional[str] = None + + if ch == "\x00": + kind = "notdef" + elif code <= 0x1F and ch not in '\t\n\r': + kind = "control_char" + elif code == 0x7F: + kind = "control_char" + elif code == 0xFFFD: + kind = "replacement_char" + elif code in _ZERO_WIDTH_CHARS: + kind = "zero_width" + elif code in _BIDI_CONTROLS: + kind = "bidi_control" + elif code in _VARIATION_SELECTORS: + kind = "variation_selector" + elif 0xE000 <= code <= 0xF8FF: + kind = "private_use_area" + + if kind: + start = max(0, i - 10) + end = min(len(text), i + 11) + context = text[start:end].replace("\x00", "□") + issues.append({ + "page": page_num + 1, + "position": i, + "char": f"U+{code:04X}", + "kind": kind, + "context": context, + }) + + doc.close() + + # Deduplicate by page + context + kind + seen: Set[str] = set() + unique: List[Dict[str, Any]] = [] + for item in issues: + key = f"{item['page']}:{item['kind']}:{item['context']}" + if key not in seen: + seen.add(key) + unique.append(item) + + return unique + + +@cmd("font.check") +def font_check(argv: list): + """Scan a PDF for missing glyphs (□ boxes) after generation.""" + if not argv: + Output.error("MissingArg", "Usage: font.check ") + pdf_path = argv[0] + if not os.path.isfile(pdf_path): + Output.error("FileNotFound", f"File not found: {pdf_path}") + + issues = check_missing_glyphs(pdf_path) + + # Group by kind for better reporting + by_kind: Dict[str, List[Dict]] = {} + for item in issues: + k = item.get("kind", "unknown") + by_kind.setdefault(k, []).append(item) + + result: Dict[str, Any] = { + "status": "issues_found" if issues else "ok", + "total_issues": len(issues), + "by_kind": {k: len(v) for k, v in by_kind.items()}, + } + if issues: + result["issues"] = issues[:30] # cap output + result["hints"] = { + "notdef": "Missing glyphs (□). Fix: call install_font_fallback() after font registration.", + "control_char": "Control characters leaked into PDF. Fix: content_sanitize() should strip these.", + "replacement_char": "U+FFFD found — upstream encoding error corrupted original character.", + "zero_width": "Zero-width chars found. Usually harmless visually but affect text search/copy.", + "bidi_control": "Bidirectional control chars found. May cause text direction issues.", + "variation_selector": "Variation selectors found. No visual effect in ReportLab but shouldn't be here.", + "private_use_area": "Private Use Area chars found. Will render as □ unless a custom font covers them.", + } + + print(json.dumps(result, ensure_ascii=False, indent=2)) + raise SystemExit(0 if not issues else 1) + + +@cmd("toc.check") +def toc_check(argv: list): + """Validate TOC quality in a PDF file (page numbers, entries, links).""" + if not argv: + Output.error("MissingArg", "Usage: toc.check ") + pdf_path = argv[0] + if not os.path.isfile(pdf_path): + Output.error("FileNotFound", f"File not found: {pdf_path}") + + # Locate toc_validate.py relative to this script + script_dir = Path(__file__).resolve().parent + toc_script = script_dir / "toc_validate.py" + if not toc_script.exists(): + # Try parent's scripts dir + candidate = script_dir.parent / "scripts" / "toc_validate.py" + if candidate.exists(): + toc_script = candidate + + if toc_script.exists(): + # Import and call directly + import importlib.util + spec = importlib.util.spec_from_file_location("toc_validate", str(toc_script)) + toc_mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(toc_mod) + result = toc_mod.check_pdf(pdf_path) + else: + Output.error("ScriptNotFound", + "toc_validate.py not found. Expected at: " + str(script_dir / "toc_validate.py")) + + # Add remediation hints per error code + remediation = { + "TOC_ALL_SAME_PAGE": "ReportLab: use multiBuild() instead of build() with TocDocTemplate", + "TOC_NO_ENTRIES": "Check afterFlowable() notifies TOC correctly; ensure headings have bookmark_name/bookmark_level attributes", + "TOC_PAGES_INVALID": "TOC entry references a page beyond document total — regenerate TOC", + "TOC_NOT_FOUND": "Document has many pages but no TOC detected in first 5 pages", + "TOC_LINKS_MISSING": "TOC entries exist but no clickable links — check bookmark_key in afterFlowable()", + "TOC_ON_FIRST_PAGE": "Add a cover page before the TOC, or insert PageBreak() between cover and TOC. Expected: Cover(p1) → TOC(p2) → Content(p3+)", + } + for err in result.get("errors", []): + code = err.get("code", "") + if code in remediation: + err["fix"] = remediation[code] + for warn in result.get("warnings", []): + code = warn.get("code", "") + if code in remediation: + warn["fix"] = remediation[code] + + print(json.dumps(result, ensure_ascii=False, indent=2)) + raise SystemExit(0 if result.get("pass", False) else 1) + + +# ═══════════════════════════════════════════════════════════════ +# Section 7: code — sanitization pipeline for PDF generation code +# ═══════════════════════════════════════════════════════════════ + +# --- Step 0: restore literal unicode escapes/entities to real chars --- +_RE_UNICODE_ESC = re.compile(r"(\\u[0-9a-fA-F]{4})|(\\U[0-9a-fA-F]{8})|(\\x[0-9a-fA-F]{2})") + + +def _restore_escapes(s: str) -> str: + # HTML entities: ³ ≤ α ... + s = html.unescape(s) + + # Literal backslash escapes: "\\u00B3" -> "³" + def _dec(m: re.Match) -> str: + esc = m.group(0) + try: + if esc.startswith("\\u") or esc.startswith("\\U"): + return chr(int(esc[2:], 16)) + if esc.startswith("\\x"): + return chr(int(esc[2:], 16)) + except Exception: + return esc + return esc + + return _RE_UNICODE_ESC.sub(_dec, s) + + +# --- Step 1: superscripts/subscripts -> / --- +_SUPERSCRIPT_MAP: Dict[str, str] = { + "\u2070": "0", "\u00b9": "1", "\u00b2": "2", "\u00b3": "3", "\u2074": "4", + "\u2075": "5", "\u2076": "6", "\u2077": "7", "\u2078": "8", "\u2079": "9", + "\u207a": "+", "\u207b": "-", "\u207c": "=", "\u207d": "(", "\u207e": ")", + "\u207f": "n", "\u1da6": "i", +} + +_SUBSCRIPT_MAP: Dict[str, str] = { + "\u2080": "0", "\u2081": "1", "\u2082": "2", "\u2083": "3", "\u2084": "4", + "\u2085": "5", "\u2086": "6", "\u2087": "7", "\u2088": "8", "\u2089": "9", + "\u208a": "+", "\u208b": "-", "\u208c": "=", "\u208d": "(", "\u208e": ")", + "\u2090": "a", "\u2091": "e", "\u2095": "h", "\u1d62": "i", "\u2c7c": "j", + "\u2096": "k", "\u2097": "l", "\u2098": "m", "\u2099": "n", "\u2092": "o", + "\u209a": "p", "\u1d63": "r", "\u209b": "s", "\u209c": "t", "\u1d64": "u", + "\u1d65": "v", "\u2093": "x", +} + + +def _replace_super_sub(s: str) -> str: + out = [] + for ch in s: + if ch in _SUPERSCRIPT_MAP: + out.append(f"{_SUPERSCRIPT_MAP[ch]}") + elif ch in _SUBSCRIPT_MAP: + out.append(f"{_SUBSCRIPT_MAP[ch]}") + else: + out.append(ch) + return "".join(out) + + +# --- Step 2: symbol fallback for SimHei (protect tags, then replace) --- +_SYMBOL_FALLBACK: Dict[str, str] = { + # Currently empty - enable entries as needed for fonts missing specific glyphs +} + + +def _fallback_symbols(s: str) -> str: + # Protect / tags from being modified + placeholders: Dict[str, str] = {} + + def _protect_tag(m: re.Match) -> str: + key = f"@@TAG{len(placeholders)}@@" + placeholders[key] = m.group(0) + return key + + protected = re.sub(r"|", _protect_tag, s) + + # Replace symbols + protected = "".join(_SYMBOL_FALLBACK.get(ch, ch) for ch in protected) + + # Restore tags + for k, v in placeholders.items(): + protected = protected.replace(k, v) + + return protected + + +def sanitize_code(text: str) -> str: + """ + Full sanitization pipeline for PDF generation code. + - Restore unicode escapes/entities to real characters + - Replace superscript/subscript unicode with / + - Replace other risky symbols with ASCII/text fallbacks + """ + s = _restore_escapes(text) + s = _replace_super_sub(s) + s = _fallback_symbols(s) + return s + + +@cmd("palette.generate") +def palette_generate(argv: list): + """Generate a color palette via design_engine.py and output as Python-ready ReportLab code. + + Usage: + pdf.py palette.generate [--title "document title"] [--mode minimal|dark|pastel|jewel|light] [--harmony auto|complementary|...] [--format python|json|css] + + If --title is provided, intent is auto-derived from the title. + Output formats: + python (default): Ready-to-paste ReportLab color variables + json: Raw palette JSON + css: CSS custom properties + """ + title = _pop_flag(argv, "--title", "-t") or "" + mode = _pop_flag(argv, "--mode", "-m") or "minimal" + harmony = _pop_flag(argv, "--harmony", "--harmony") or "auto" + fmt = _pop_flag(argv, "--format", "-f") or "python" + seed_str = _pop_flag(argv, "--seed", "--seed") + seed = int(seed_str) if seed_str else None + + engine_script = _SCRIPT_DIR / "design_engine.py" + if not engine_script.exists(): + Output.error("DependencyMissing", f"design_engine.py not found at {engine_script}") + + # Import design_engine dynamically + import importlib.util + spec = importlib.util.spec_from_file_location("design_engine", str(engine_script)) + de = importlib.util.module_from_spec(spec) + spec.loader.exec_module(de) + + # Auto-derive intent from title + if title: + intent = de.derive_intent(title) + else: + intent = "neutral" + + palette = de.generate_color_palette(intent, mode, harmony=harmony, seed=seed) + violations = de.audit_palette(palette) + + if fmt == "json": + print(json.dumps(palette, indent=2, ensure_ascii=False)) + elif fmt == "css": + print(de.palette_to_css(palette)) + else: + # Python format: ready-to-paste ReportLab code + print("# \u2501\u2501 Color Palette (auto-generated by pdf.py palette.generate) \u2501\u2501") + print(f"# Intent: {intent} | Mode: {mode} | Harmony: {palette['meta']['harmony']}") + print(f"# Contrast: text:bg={palette['meta']['contrast']['text_on_bg']} | accent:bg={palette['meta']['contrast']['accent_on_bg']}") + print("from reportlab.lib import colors") + print(f"ACCENT = colors.HexColor('{palette['accent']}')") + print(f"TEXT_PRIMARY = colors.HexColor('{palette['text']}')") + print(f"TEXT_MUTED = colors.HexColor('{palette['muted']}')") + print(f"BG_SURFACE = colors.HexColor('{palette['mid']}')") + print(f"BG_PAGE = colors.HexColor('{palette['bg']}')") + print(f"SURFACE_RGBA = '{palette['surface']}' # For CSS/HTML elements") + print("") + print("# ReportLab table colors") + print("TABLE_HEADER_COLOR = ACCENT") + print("TABLE_HEADER_TEXT = colors.white") + print("TABLE_ROW_EVEN = colors.white") + print("TABLE_ROW_ODD = BG_SURFACE") + + if violations: + print(f"\n# \u26a0\ufe0f Palette audit warnings:", file=sys.stderr) + for v in violations: + print(f"# - {v}", file=sys.stderr) + + raise SystemExit(0) + + +@cmd("palette.cascade") +def palette_cascade(argv: list): + """Generate a role-based cascade palette (area ∝ 1/saturation). + + Usage: + pdf.py palette.cascade [--title "document title"] [--mode minimal|dark|pastel|jewel|light] [--harmony auto|...] [--format summary|json|css|reportlab] + + The cascade palette produces 12 named roles + 4 semantic colors, each assigned + to a size tier (XL/L/M/S/XS) with saturation caps enforced per tier. + Cover, body, and chart colors all pull from the same palette — no color drift. + """ + title = _pop_flag(argv, "--title", "-t") or "" + mode = _pop_flag(argv, "--mode", "-m") or "minimal" + harmony = _pop_flag(argv, "--harmony", "--harmony") or "auto" + fmt = _pop_flag(argv, "--format", "-f") or "summary" + seed_str = _pop_flag(argv, "--seed", "--seed") + seed = int(seed_str) if seed_str else None + + engine_script = _SCRIPT_DIR / "design_engine.py" + if not engine_script.exists(): + Output.error("DependencyMissing", f"design_engine.py not found at {engine_script}") + + import importlib.util + spec = importlib.util.spec_from_file_location("design_engine", str(engine_script)) + de = importlib.util.module_from_spec(spec) + spec.loader.exec_module(de) + + if title: + intent = de.derive_intent(title) + else: + intent = "neutral" + + cascade = de.generate_cascade_palette(intent, mode, harmony=harmony, seed=seed) + + if fmt == "json": + print(json.dumps(cascade, indent=2, ensure_ascii=False, default=str)) + elif fmt == "css": + print(cascade["css"]) + elif fmt == "reportlab": + print(cascade["reportlab"]) + else: + # Summary format + meta = cascade["meta"] + print(f"🎨 Cascade Palette | Intent: {meta['intent']} | Mode: {meta['mode']} | Harmony: {meta['harmony']}") + print(f" Base hue: {meta['base_hue']}° | Accent hue: {meta['accent_hue']}° | Secondary: {meta['secondary_hue']}°") + print(f" Contrast: text:bg={meta['contrast']['text_on_bg']} | accent:bg={meta['contrast']['accent_on_bg']}") + print() + print(" TIER | ROLE | HEX | S | USAGE") + print(" ────── | ────────────────── | ─────── | ────── | ────────────") + for name, info in cascade["roles"].items(): + tier = info['tier'].upper().ljust(6) + nm = name.ljust(18) + hx = info['hex'].ljust(7) + s_val = f"{info['hsl'][1]:.3f}".ljust(6) + print(f" {tier} | {nm} | {hx} | {s_val} | {info['usage']}") + print() + print(" Semantic:") + for name, info in cascade["semantic"].items(): + print(f" {name}: {info['hex']} (S={info['hsl'][1]:.3f})") + if meta["audit"]: + print(f"\n ⚠️ Violations:") + for v in meta["audit"]: + print(f" - {v}") + else: + print(f"\n ✅ All tier constraints pass") + + raise SystemExit(0) + + +@cmd("code.sanitize") +def code_sanitize(argv: list): + """Sanitize Unicode in a Python script for PDF generation.""" + if not argv: + Output.error("MissingArg", "Usage: code.sanitize ") + target = argv[0] + with open(target, "r", encoding="utf-8") as f: + code = f.read() + sanitized = sanitize_code(code) + with open(target, "w", encoding="utf-8") as f: + f.write(sanitized) + print(f"Sanitized: {target}") + raise SystemExit(0) + + +@cmd("content.sanitize") +def content_sanitize_cli(argv: list): + """Sanitize content text for PDF rendering (dry-run report). + + Reads a text file and reports what content_sanitize() would change. + Useful for debugging before PDF generation. + + Usage: + pdf.py content.sanitize + pdf.py content.sanitize --apply + """ + if not argv: + Output.error("MissingArg", "Usage: content.sanitize [--apply]") + target = argv[0] + apply_flag = "--apply" in argv + + with open(target, "r", encoding="utf-8") as f: + raw = f.read() + + global _content_sanitize_warnings + _content_sanitize_warnings = [] + cleaned = content_sanitize(raw, dry_run=True) + + changes = len(_content_sanitize_warnings) + result: Dict[str, Any] = { + "file": target, + "original_chars": len(raw), + "cleaned_chars": len(cleaned), + "changes": changes, + "details": _content_sanitize_warnings[:50], + } + + if apply_flag and changes > 0: + with open(target, "w", encoding="utf-8") as f: + f.write(cleaned) + result["applied"] = True + else: + result["applied"] = False + if changes > 0: + result["hint"] = "Run with --apply to write cleaned text back to file." + + print(json.dumps(result, ensure_ascii=False, indent=2)) + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 8: CLI dispatcher +# ═══════════════════════════════════════════════════════════════ + +def _usage(): + sys.stdout.write(__doc__.strip() + "\n") + raise SystemExit(0) + + +def main(): + tokens = sys.argv[1:] + if not tokens or tokens[0] in ("-h", "--help"): + _usage() + + cmd_name = tokens.pop(0) + + # Direct match + handler = _COMMANDS.get(cmd_name) + if handler is not None: + handler(tokens) + return + + # Two-word match (e.g., "extract text" -> "extract.text") + if tokens: + compound = f"{cmd_name}.{tokens[0]}" + handler = _COMMANDS.get(compound) + if handler is not None: + tokens.pop(0) + handler(tokens) + return + + # List commands in group + group_cmds = [k for k in _COMMANDS if k.startswith(cmd_name + ".")] + if group_cmds: + print(f"Available commands in '{cmd_name}':") + for c in sorted(group_cmds): + print(f" {c}") + raise SystemExit(0) + + print(f"Unknown command: {cmd_name}\n") + _usage() + + +if __name__ == "__main__": + main() diff --git a/skills/pdf/scripts/pdf_qa.py b/skills/pdf/scripts/pdf_qa.py new file mode 100755 index 0000000..49c5462 --- /dev/null +++ b/skills/pdf/scripts/pdf_qa.py @@ -0,0 +1,901 @@ +#!/usr/bin/env python3 +""" +PDF Quality Assurance Checker +============================= +Automatically detects common typesetting issues in PDFs. + +Usage: python3 pdf_qa.py + +Checks: + 1. Page size consistency across all pages + 2. Blank page detection + 3. CJK punctuation placement (line-start/end forbidden punctuation) + 4. Color analysis (informational only — counts and lists colors) + 5. Font embedding check (warns on non-embedded fonts) + 6. PDF metadata check (title/author/creator) + 7. Content overflow detection (text exceeding page boundaries) + 8. Content fill ratio per page (multi-page docs, warns if < 40%) + 9. Cover/poster full-bleed check (background extends to page edges) + 10. Margin symmetry check (left/right text margins) + 11. Table centering check (if detected) + 12. Formula overflow check (optional) +""" + +import sys +import os +import re +import json +from collections import Counter + +try: + import pymupdf # PyMuPDF +except ImportError: + import fitz as pymupdf + +# ============================================================ +# Config +# ============================================================ + +# CJK punctuation forbidden at line start +LINE_START_FORBIDDEN = set( + "。、,;:!?)】〛〉」』" + "\u201c\u201d" # "" curly double quotes + "\u2026" # … ellipsis + "\u2014" # — em dash + "\uff5e" # ~ fullwidth tilde + "\u00b7" # · middle dot +) + +# CJK punctuation forbidden at line end +LINE_END_FORBIDDEN = set( + "(【《〈「" + "\u2018\u2019" # '' curly single quotes + "\u201c" # " left curly double quote +) + +# Minimum fill ratio for last page (DISABLED — caused false positives) +# LAST_PAGE_MIN_FILL = 0.40 + +# Maximum allowed color count — REMOVED (color count is now info-only) +# MAX_COLORS = 8 + +# ============================================================ +# Checks +# ============================================================ + +class QAResult: + def __init__(self): + self.issues = [] # (severity, category, message) + self.passes = [] # passed checks + self.info = [] # informational + + def error(self, cat, msg): + self.issues.append(('ERROR', cat, msg)) + + def warn(self, cat, msg): + self.issues.append(('WARN', cat, msg)) + + def ok(self, msg): + self.passes.append(msg) + + def add_info(self, msg): + self.info.append(msg) + + +def check_last_page_fill(doc, result): + """Check content fill ratio of the last page""" + if len(doc) < 2: + result.ok("Single-page document, no last-page blank check needed") + return + + last_page = doc[-1] + page_rect = last_page.rect + page_area = page_rect.width * page_rect.height + + # Get bounding boxes of all content on last page + blocks = last_page.get_text("blocks") + if not blocks: + result.error("Last page blank", f"Page {len(doc)} (last page) has no content at all!") + return + + # Calculate max y-coordinate covered by content + max_y = 0 + min_y = page_rect.height + for b in blocks: + if b[4].strip(): # Has text content + min_y = min(min_y, b[1]) + max_y = max(max_y, b[3]) + + if max_y == 0: + result.error("Last page blank", f"Page {len(doc)} (last page) has no valid text content") + return + + content_height = max_y - min_y + fill_ratio = content_height / page_rect.height + + result.add_info(f"Last page fill ratio: {fill_ratio:.0%} (content height {content_height:.0f}px / page height {page_rect.height:.0f}px)") + + if fill_ratio < 0.25: + result.error("Last page blank", f"Last page fill ratio only {fill_ratio:.0%}, mostly blank! Consider compressing preceding page spacing or trimming content") + elif fill_ratio < LAST_PAGE_MIN_FILL: + result.warn("Last page blank", f"Last page fill ratio {fill_ratio:.0%}, somewhat sparse — optimization recommended") + else: + result.ok(f"Last page fill ratio {fill_ratio:.0%} ✓") + + +def check_punctuation(doc, result): + """Check CJK punctuation placement rules""" + violations = [] + + for page_num in range(len(doc)): + page = doc[page_num] + # Extract text by line + text_dict = page.get_text("dict") + + for block in text_dict.get("blocks", []): + if block.get("type") != 0: # Only check text blocks + continue + for line in block.get("lines", []): + line_text = "" + for span in line.get("spans", []): + line_text += span.get("text", "") + + line_text = line_text.strip() + if not line_text: + continue + + # Check line start + first_char = line_text[0] + if first_char in LINE_START_FORBIDDEN: + violations.append((page_num + 1, f"Forbidden line-start punctuation '{first_char}': ...{line_text[:30]}")) + + # Check line end + last_char = line_text[-1] if len(line_text) > 0 else '' + if last_char in LINE_END_FORBIDDEN: + violations.append((page_num + 1, f"Forbidden line-end punctuation '{last_char}': {line_text[-30:]}...")) + + if violations: + # Show at most 10 + shown = violations[:10] + for page_num, desc in shown: + result.warn("Punctuation rules", f"Page {page_num} - {desc}") + if len(violations) > 10: + result.warn("Punctuation rules", f"...{len(violations) - 10} more violations") + else: + result.ok("Punctuation placement check passed ✓") + + +def check_blank_pages(doc, result): + """Check for completely blank pages""" + blank_pages = [] + for i in range(len(doc)): + page = doc[i] + text = page.get_text().strip() + # Also check for images + images = page.get_images() + drawings = page.get_drawings() + + if not text and not images and not drawings: + blank_pages.append(i + 1) + + if blank_pages: + result.error("Blank pages", f"Found blank pages: {blank_pages}") + else: + result.ok("No blank pages ✓") + + +def check_colors(doc, result): + """Analyze colors used in the document (informational only, no pass/fail)""" + colors = set() + + for page_num in range(len(doc)): + page = doc[page_num] + text_dict = page.get_text("dict") + + for block in text_dict.get("blocks", []): + if block.get("type") != 0: + continue + for line in block.get("lines", []): + for span in line.get("spans", []): + color = span.get("color", 0) + if color != 0: # Exclude pure black + r = (color >> 16) & 0xFF + g = (color >> 8) & 0xFF + b = color & 0xFF + hex_color = f"#{r:02x}{g:02x}{b:02x}" + colors.add(hex_color) + + # Check drawing colors + drawings = page.get_drawings() + for d in drawings: + if d.get("color"): + c = d["color"] + if isinstance(c, (tuple, list)) and len(c) >= 3: + hex_color = f"#{int(c[0]*255):02x}{int(c[1]*255):02x}{int(c[2]*255):02x}" + colors.add(hex_color) + if d.get("fill"): + c = d["fill"] + if isinstance(c, (tuple, list)) and len(c) >= 3: + hex_color = f"#{int(c[0]*255):02x}{int(c[1]*255):02x}{int(c[2]*255):02x}" + colors.add(hex_color) + + # Filter out near-black/white/gray colors + distinct_colors = [] + for c in colors: + r = int(c[1:3], 16) + g = int(c[3:5], 16) + b = int(c[5:7], 16) + max_diff = max(abs(r-g), abs(g-b), abs(r-b)) + if max_diff > 20: + distinct_colors.append(c) + + result.add_info(f"Total text colors: {len(colors)} (chromatic: {len(distinct_colors)})") + + if distinct_colors: + result.add_info(f"Chromatic colors: {', '.join(sorted(distinct_colors)[:10])}") + + +def check_page_size_consistency(doc, result): + """Check whether all page sizes are consistent""" + if len(doc) < 2: + result.ok("Single-page document, size consistent ✓") + return + + sizes = set() + for i in range(len(doc)): + page = doc[i] + w = round(page.rect.width, 1) + h = round(page.rect.height, 1) + sizes.add((w, h)) + + if len(sizes) > 1: + result.warn("Page size", f"Inconsistent page sizes: {sizes}") + else: + size = list(sizes)[0] + # Convert to mm + w_mm = size[0] * 25.4 / 72 + h_mm = size[1] * 25.4 / 72 + result.add_info(f"Page size: {w_mm:.0f}mm × {h_mm:.0f}mm ({len(doc)} pages)") + result.ok("Page size consistent ✓") + + +def check_text_overflow(doc, result): + """Check whether text overflows page boundaries""" + overflow_pages = [] + + for i in range(len(doc)): + page = doc[i] + rect = page.rect + blocks = page.get_text("blocks") + + for b in blocks: + # b = (x0, y0, x1, y1, text, block_no, block_type) + if b[2] > rect.width + 2 or b[3] > rect.height + 2: # 2px tolerance + overflow_pages.append(i + 1) + break + if b[0] < -2 or b[1] < -2: + overflow_pages.append(i + 1) + break + + if overflow_pages: + result.warn("Content overflow", f"Pages {overflow_pages} may have content exceeding page boundaries") + else: + result.ok("No content overflow ✓") + + +def check_content_fill_ratio(doc, result): + """Check content fill ratio per page — warns when content is crammed at top leaving large void below. + + Rules: + - Skip single-page documents (may be intentional design) + - Skip page 1 (usually cover with intentional whitespace) + - Middle pages: warn if fill ratio < 40% + - Last page: warn if fill ratio < 25% (naturally has less content) + """ + if len(doc) < 2: + result.ok("Single-page document, skipping content fill ratio check ✓") + return + + low_fill_pages = [] + + for i in range(len(doc)): + page = doc[i] + page_rect = page.rect + page_height = page_rect.height + + # Skip page 1 (cover) + if i == 0: + continue + + blocks = page.get_text("blocks") + images = page.get_images() + drawings = page.get_drawings() + + if not blocks and not images and not drawings: + continue # Blank page check handles this + + # Calculate content bbox + max_y = 0 + for b in blocks: + if b[4].strip(): + max_y = max(max_y, b[3]) + + # Include images in bbox + for img in images: + try: + img_rects = page.get_image_rects(img[0]) + for r in img_rects: + max_y = max(max_y, r.y1) + except Exception: + pass + + if max_y == 0: + continue + + fill_ratio = max_y / page_height + is_last = (i == len(doc) - 1) + threshold = 0.25 if is_last else 0.40 + + if fill_ratio < threshold: + low_fill_pages.append((i + 1, fill_ratio, threshold)) + + if low_fill_pages: + for pg, ratio, thresh in low_fill_pages: + result.warn( + "Content fill ratio", + f"Page {pg} content only fills {ratio:.0%} of page height " + f"(threshold: {thresh:.0%}). Content may be crammed at the top " + f"with a large blank area below." + ) + else: + result.ok("Content fill ratio adequate on all pages ✓") + + +def check_cover_bleed(doc, result, poster=False): + """Check if the cover page (page 1) fills the entire page area (full-bleed). + + A properly designed cover should have background color/graphics extending + to the page edges. If the content bbox has significant margins on all sides, + the cover likely wasn't rendered full-bleed (e.g. ReportLab with default margins). + + For poster mode: checks ALL pages (not just the cover) since every page of a + seamlessly-paginated poster should have consistent background fill. + + Strategy: combine bounding boxes of drawings (rects, paths), images, and colored + backgrounds. If the union bbox leaves > 5% margin on any side, warn. + """ + if not poster and len(doc) < 2: + # Single page doc (non-poster) — not necessarily a cover scenario + return + + pages_to_check = range(len(doc)) if poster else [0] + + for page_idx in pages_to_check: + page = doc[page_idx] + page_rect = page.rect + pw, ph = page_rect.width, page_rect.height + + # Collect all content bounding boxes + min_x, min_y = pw, ph + max_x, max_y = 0.0, 0.0 + has_content = False + + # 1. Drawings (vector paths, rectangles — typical for colored backgrounds) + for d in page.get_drawings(): + r = d.get("rect") + if r: + min_x = min(min_x, r.x0) + min_y = min(min_y, r.y0) + max_x = max(max_x, r.x1) + max_y = max(max_y, r.y1) + has_content = True + + # 2. Images + for img in page.get_images(): + try: + for r in page.get_image_rects(img[0]): + min_x = min(min_x, r.x0) + min_y = min(min_y, r.y0) + max_x = max(max_x, r.x1) + max_y = max(max_y, r.y1) + has_content = True + except Exception: + pass + + page_label = f"Page {page_idx + 1}" if poster else "Cover page (p1)" + + if not has_content: + blocks = page.get_text("blocks") + if blocks: + result.warn( + f"{page_label} not full-bleed", + f"{page_label} has no background graphics (no filled rectangles or images). " + "A proper cover/poster page should have a full-page background color or image " + "extending to all edges." + ) + continue + + # Calculate margin ratios (how far content is from page edges) + margin_left = max(0, min_x) / pw + margin_top = max(0, min_y) / ph + margin_right = max(0, pw - max_x) / pw + margin_bottom = max(0, ph - max_y) / ph + + threshold = 0.05 + margins_ok = (margin_left <= threshold and margin_top <= threshold and + margin_right <= threshold and margin_bottom <= threshold) + + if margins_ok: + result.ok(f"{page_label} content extends to page edges (full-bleed) ✓") + else: + sides = [] + if margin_left > threshold: + sides.append(f"left {margin_left:.0%}") + if margin_top > threshold: + sides.append(f"top {margin_top:.0%}") + if margin_right > threshold: + sides.append(f"right {margin_right:.0%}") + if margin_bottom > threshold: + sides.append(f"bottom {margin_bottom:.0%}") + result.warn( + f"{page_label} not full-bleed", + f"{page_label} has visible margins: {', '.join(sides)}. " + f"Background/graphics should extend to page edges." + ) + + +def check_margin_symmetry(doc, result, skip_cover=False): + """Check left/right margin symmetry using text block bounds.""" + warn_pages = [] + + for page_num in range(len(doc)): + if skip_cover and page_num == 0: + continue + + page = doc[page_num] + blocks = page.get_text("blocks") + text_blocks = [b for b in blocks if b[4].strip()] + + if len(text_blocks) < 3: + continue # Skip decorative/cover-like pages + + left_margin = min(b[0] for b in text_blocks) + right_margin = page.rect.width - max(b[2] for b in text_blocks) + diff = abs(left_margin - right_margin) + + if diff > page.rect.width * 0.05: + warn_pages.append((page_num + 1, left_margin, right_margin, diff)) + + if warn_pages: + for pg, left, right, diff in warn_pages: + result.warn( + "Margin symmetry", + f"Page {pg} left/right margins differ by {diff:.0f}pt " + f"(L {left:.0f}pt, R {right:.0f}pt)" + ) + else: + result.ok("Left/right margins appear symmetric \u2713") + + +def check_table_centering(doc, result): + """Check if detected table regions are centered.""" + def _bbox_intersects(a, b, tol=6): + return not (a[2] < b[0] - tol or a[0] > b[2] + tol or + a[3] < b[1] - tol or a[1] > b[3] + tol) + + def _rect_tuple(r): + if hasattr(r, "x0"): + return (r.x0, r.y0, r.x1, r.y1) + return (r[0], r[1], r[2], r[3]) + + any_tables = False + + for page_num in range(len(doc)): + page = doc[page_num] + drawings = page.get_drawings() + segments = [] + + for d in drawings: + for item in d.get("items", []): + if not item: + continue + op = item[0] + if op == "l" and len(item) >= 3: + p0, p1 = item[1], item[2] + segments.append((p0[0], p0[1], p1[0], p1[1])) + elif op == "re" and len(item) >= 2: + x0, y0, x1, y1 = _rect_tuple(item[1]) + segments.extend([ + (x0, y0, x1, y0), + (x0, y1, x1, y1), + (x0, y0, x0, y1), + (x1, y0, x1, y1), + ]) + + if not segments: + continue + + cluster_list = [] + for x0, y0, x1, y1 in segments: + min_x, max_x = min(x0, x1), max(x0, x1) + min_y, max_y = min(y0, y1), max(y0, y1) + bbox = (min_x, min_y, max_x, max_y) + is_h = abs(y0 - y1) < 1 and (max_x - min_x) > 20 + is_v = abs(x0 - x1) < 1 and (max_y - min_y) > 20 + if not is_h and not is_v: + continue + + placed = False + for cl in cluster_list: + if _bbox_intersects(bbox, cl["bbox"]): + cl["segments"].append((x0, y0, x1, y1, is_h, is_v)) + cl["bbox"] = ( + min(cl["bbox"][0], bbox[0]), + min(cl["bbox"][1], bbox[1]), + max(cl["bbox"][2], bbox[2]), + max(cl["bbox"][3], bbox[3]), + ) + if is_h: + cl["h"] += 1 + if is_v: + cl["v"] += 1 + placed = True + break + if not placed: + cluster_list.append({ + "bbox": bbox, + "segments": [(x0, y0, x1, y1, is_h, is_v)], + "h": 1 if is_h else 0, + "v": 1 if is_v else 0, + }) + + for cl in cluster_list: + if cl["h"] < 2 or cl["v"] < 2: + continue + any_tables = True + bbox = cl["bbox"] + page_width = page.rect.width + left_margin = bbox[0] + right_margin = page_width - bbox[2] + if abs(left_margin - right_margin) > page_width * 0.05: + result.warn( + "Table centering", + f"Page {page_num + 1}: Table not centered " + f"(L {left_margin:.0f}pt, R {right_margin:.0f}pt)" + ) + + if any_tables: + result.ok("Table centering check complete \u2713") + + +def check_font_embedding(doc, result): + """Check font embedding status using PyMuPDF font list.""" + fonts_used = set() + non_embedded = set() + + for page_num in range(len(doc)): + page = doc[page_num] + for font in page.get_fonts(): + basefont = font[3] if len(font) > 3 else "unknown" + ext = font[1] if len(font) > 1 else "" + fonts_used.add(basefont) + if not ext: + non_embedded.add(basefont) + + if fonts_used: + result.add_info(f"Fonts used: {', '.join(sorted(fonts_used))}") + else: + result.add_info("Fonts used: (none detected)") + + if non_embedded: + for basefont in sorted(non_embedded): + result.warn( + "Font embedding", + f"Font {basefont} is not embedded. May display differently on other systems." + ) + else: + result.ok("All fonts are embedded \u2713") + + +def check_helvetica_in_cjk(doc, result): + """Detect Helvetica rendering visible text in documents containing CJK text. + + Helvetica is a Latin-only built-in PDF font. When it appears rendering + actual text content in a CJK document, it almost always means a raw string + was passed to a ReportLab Table or flowable without wrapping it in + Paragraph() with a CJK font. The CJK characters rendered via Helvetica + become garbled (fall back to ZapfDingbats symbols). + + We only check Helvetica (not ZapfDingbats) because ZapfDingbats is + legitimately used for bullet symbols in list items. + + We check actual rendered text spans (not just font presence in font list) + because ReportLab internally registers Helvetica on every page even when + only CJK fonts are used in visible content. + """ + has_cjk = False + helvetica_pages = [] + + for page_num in range(len(doc)): + page = doc[page_num] + text = page.get_text("text") or "" + + # Check if document contains CJK characters + if not has_cjk: + for ch in text: + if '\u4e00' <= ch <= '\u9fff' or '\u3400' <= ch <= '\u4dbf': + has_cjk = True + break + + # Check if Helvetica is actually used to render visible text on this page + blocks = page.get_text("dict", sort=True).get("blocks", []) + found_on_page = False + for block in blocks: + if found_on_page: + break + for line in block.get("lines", []): + if found_on_page: + break + for span in line.get("spans", []): + font = span.get("font", "") + txt = span.get("text", "").strip() + if "Helvetica" in font and len(txt) > 0: + helvetica_pages.append(page_num + 1) + found_on_page = True + break + + if has_cjk and helvetica_pages: + pages_str = ', '.join(str(p) for p in helvetica_pages[:5]) + if len(helvetica_pages) > 5: + pages_str += f' ...and {len(helvetica_pages) - 5} more' + result.warn( + "Helvetica in CJK document", + f"Helvetica font detected rendering text on page(s) {pages_str} in a CJK document. " + f"This usually means a raw string was passed to a ReportLab Table or flowable " + f"without wrapping in Paragraph(text, style) with a CJK-capable font. " + f"CJK characters rendered via Helvetica will appear as garbled symbols." + ) + + +def check_metadata(doc, result): + """Check PDF metadata presence for title, author, creator.""" + meta = doc.metadata or {} + + def _missing(v): + if v is None: + return True + if not str(v).strip(): + return True + return False + + title = meta.get("title") + author = meta.get("author") + creator = meta.get("creator") + + if _missing(title) or str(title).strip().lower() in ("untitled", "(anonymous)"): + result.warn("Metadata", "Missing/invalid title metadata") + else: + result.ok("Title metadata present \u2713") + + if _missing(author): + result.warn("Metadata", "Missing author metadata") + else: + result.ok("Author metadata present \u2713") + + if _missing(creator): + result.warn("Metadata", "Missing creator metadata") + else: + result.ok("Creator metadata present \u2713") + + +def check_toc_without_cover(doc, result): + """Detect TOC on page 1 without a preceding cover page. + + If the first page contains Table of Contents / 目录, it means the document + has a TOC but no cover page. This is a structural issue — documents with + TOC should have: Cover (p1) → TOC (p2) → Content (p3+). + """ + if len(doc) < 2: + # Single-page docs don't need TOC/cover checks + return + + page1 = doc[0] + text = page1.get_text("text", sort=True).strip() + + # Normalize for matching + text_lower = text.lower() + first_300 = text_lower[:300] + + toc_keywords = [ + "table of contents", "contents", + "目录", "目 录", + ] + + has_toc = any(kw in first_300 for kw in toc_keywords) + + if has_toc: + result.warn( + "TOC without cover", + "Page 1 appears to be a Table of Contents with no preceding cover page. " + "Documents with TOC should have: Cover (p1) → TOC (p2) → Content (p3+)." + ) + + +def check_formula_overflow(doc, result): + """Detect likely formula overflow past right content margin.""" + math_re = re.compile(r"[=+\-*/<>\u2264\u2265\u2211\u222b\u221a\u03c0\u00b5\u221e\u2202\u2206\u2248\u2260\u00b1\u00d7\u00f7]") + + for page_num in range(len(doc)): + page = doc[page_num] + blocks = page.get_text("blocks") + text_blocks = [b for b in blocks if b[4].strip()] + + if len(text_blocks) < 3: + continue + + right_edges = sorted(b[2] for b in text_blocks) + mid = len(right_edges) // 2 + content_right = right_edges[mid] if right_edges else 0 + + for b in text_blocks: + x0, x1, text = b[0], b[2], b[4] + if x1 <= content_right + 10: + continue + + is_single_line = "\n" not in text.strip() + is_wide = (x1 - x0) > page.rect.width * 0.5 + has_math = bool(math_re.search(text)) + + if (is_single_line and is_wide) or has_math: + delta = x1 - content_right + result.warn( + "Formula overflow", + f"Page {page_num + 1}: Content extends {delta:.0f}pt beyond right content margin " + "(possible formula overflow)" + ) + break + + +# ============================================================ +# Main +# ============================================================ + +def run_qa(pdf_path, poster=False, skip_cover=False, check_tables=True, check_formulas=False): + result = QAResult() + + if not os.path.exists(pdf_path): + result.error("File", f"File not found: {pdf_path}") + return result + + doc = pymupdf.open(pdf_path) + + result.add_info(f"File: {os.path.basename(pdf_path)}") + result.add_info(f"Size: {os.path.getsize(pdf_path) / 1024:.1f} KB") + if poster: + result.add_info("Mode: poster (creative)") + + # Run all checks + check_metadata(doc, result) + check_page_size_consistency(doc, result) + check_blank_pages(doc, result) + check_punctuation(doc, result) + check_colors(doc, result) + check_font_embedding(doc, result) + check_helvetica_in_cjk(doc, result) + check_text_overflow(doc, result) + if not poster: + # Content fill ratio is not meaningful for posters — the last page + # of a seamlessly-paginated poster naturally has less content. + check_content_fill_ratio(doc, result) + check_cover_bleed(doc, result, poster=poster) + check_margin_symmetry(doc, result, skip_cover=skip_cover) + if check_tables: + check_table_centering(doc, result) + if check_formulas: + check_formula_overflow(doc, result) + if not poster: + check_toc_without_cover(doc, result) + + doc.close() + return result + + +def format_report(result): + lines = [] + lines.append("=" * 56) + lines.append(" PDF Quality Assurance Report") + lines.append("=" * 56) + + # Info + if result.info: + lines.append("") + lines.append("ℹ️ Info:") + for msg in result.info: + lines.append(f" {msg}") + + # Passes + if result.passes: + lines.append("") + lines.append(f"✅ Passed ({len(result.passes)}):") + for msg in result.passes: + lines.append(f" {msg}") + + # Issues + errors = [(s, c, m) for s, c, m in result.issues if s == 'ERROR'] + warns = [(s, c, m) for s, c, m in result.issues if s == 'WARN'] + + if errors: + lines.append("") + lines.append(f"❌ Errors ({len(errors)}):") + for _, cat, msg in errors: + lines.append(f" [{cat}] {msg}") + + if warns: + lines.append("") + lines.append(f"⚠️ Warnings ({len(warns)}):") + for _, cat, msg in warns: + lines.append(f" [{cat}] {msg}") + + # Summary + lines.append("") + lines.append("-" * 56) + total_issues = len(result.issues) + if total_issues == 0: + lines.append("🎉 PASS — All checks passed!") + elif errors: + lines.append(f"💀 FAIL — {len(errors)} error(s), {len(warns)} warning(s)") + else: + lines.append(f"⚠️ WARN — {len(warns)} warning(s), optimization recommended") + lines.append("-" * 56) + + return "\n".join(lines) + + +if __name__ == "__main__": + if len(sys.argv) < 2: + print("Usage: python3 pdf_qa.py ") + print(" python3 pdf_qa.py *.pdf (batch check)") + print("Options:") + print(" --poster Poster mode (creative)") + print(" --skip-cover Skip page 1 margin symmetry check") + print(" --no-tables Disable table centering check") + print(" --formulas Enable formula overflow check") + sys.exit(1) + + import glob + files = [] + poster = False + skip_cover = False + check_tables = True + check_formulas = False + args = sys.argv[1:] + if '--poster' in args: + poster = True + args.remove('--poster') + if '--skip-cover' in args: + skip_cover = True + args.remove('--skip-cover') + if '--no-tables' in args: + check_tables = False + args.remove('--no-tables') + if '--formulas' in args: + check_formulas = True + args.remove('--formulas') + for arg in args: + files.extend(glob.glob(arg)) + + if not files: + print(f"File not found: {args}") + sys.exit(1) + + for pdf_path in files: + result = run_qa( + pdf_path, + poster=poster, + skip_cover=skip_cover, + check_tables=check_tables, + check_formulas=check_formulas + ) + print(format_report(result)) + if len(files) > 1: + print("\n") diff --git a/skills/pdf/scripts/poster_validate.py b/skills/pdf/scripts/poster_validate.py new file mode 100755 index 0000000..570000c --- /dev/null +++ b/skills/pdf/scripts/poster_validate.py @@ -0,0 +1,1337 @@ +#!/usr/bin/env python3 +""" +poster_validate.py — Pre- and post-generation quality checks for poster/creative PDFs. + +Usage: + # Check HTML before PDF generation + python3 poster_validate.py check-html poster.html [--fix] [--output fixed.html] + + # Check PDF after generation + python3 poster_validate.py check-pdf poster.pdf --source-html poster.html + +Both commands emit a JSON report to stdout: + {"pass": bool, "source": "...", "check_type": "html"|"pdf", + "errors": [...], "warnings": [...], "info": [...]} + +Exit codes: + 0 pass (no errors; warnings/info are OK) + 1 fail (at least one error) + 2 script-level failure (bad arguments, unreadable file, …) +""" + +from __future__ import annotations + +import argparse +import json +import math +import os +import re +import sys +from html.parser import HTMLParser +from pathlib import Path +from typing import Any + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +GENERIC_FAMILIES = frozenset( + ["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui", "ui-serif", + "ui-sans-serif", "ui-monospace", "ui-rounded", "math", "emoji", "fangsong"] +) + +SERIF_FONTS = frozenset(f.lower() for f in [ + "Playfair Display", "Georgia", "Times New Roman", "Times", "Noto Serif", + "Noto Serif SC", "Noto Serif TC", "Noto Serif JP", "Noto Serif KR", + "Source Serif Pro", "Source Serif 4", "Merriweather", "Lora", "PT Serif", + "Libre Baskerville", "EB Garamond", "Cormorant Garamond", "Crimson Text", + "STSong", "FangSong", "KaiTi", "STKaiti", "Songti SC", +]) + +CHINESE_FONTS = frozenset(f.lower() for f in [ + "SimHei", "Microsoft YaHei", "Noto Sans SC", "Noto Sans TC", + "Noto Sans CJK SC", "Noto Sans CJK TC", "PingFang SC", "PingFang TC", + "Source Han Sans SC", "Source Han Sans TC", "WenQuanYi Micro Hei", + "WenQuanYi Zen Hei", "Hiragino Sans GB", "STHeiti", "STXihei", + "Noto Serif SC", "Noto Serif TC", "Noto Serif CJK SC", + "Source Han Serif SC", "SimSun", "NSimSun", "FangSong", "KaiTi", + "STSong", "STFangsong", "STKaiti", "Songti SC", "Heiti SC", +]) + +# Selectors we treat as "main containers" whose overflow:hidden is dangerous. +# NOTE: .poster and .page are EXCLUDED because html2poster.js auto-injects +# overflow:hidden on them at render time. See SKILL.md Engine Selection Rules. +CONTAINER_SELECTORS = {"body", "html", ".slide", + "#app", "#root", ".container", ".wrapper", "main", + "section", "article"} + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _issue(code: str, message: str, severity: str = "error", line: int | None = None) -> dict: + d: dict[str, Any] = {"code": code, "message": message, "severity": severity} + if line is not None: + d["line"] = line + return d + + +def _line_number(full_text: str, pos: int) -> int: + """Return 1-based line number for character position *pos*.""" + return full_text.count("\n", 0, pos) + 1 + + +# --------------------------------------------------------------------------- +# CSS regex helpers +# --------------------------------------------------------------------------- + +_RE_FONT_FAMILY = re.compile( + r"font-family\s*:\s*([^;}\n]+)", re.IGNORECASE +) + +_RE_FONT_SIZE = re.compile( + r"font-size\s*:\s*(\d+(?:\.\d+)?)\s*(px|pt|em|rem)", re.IGNORECASE +) + +_RE_PAGE_SIZE = re.compile( + r"@page\s*\{[^}]*\bsize\s*:", re.IGNORECASE | re.DOTALL +) + +_RE_CSS_URL = re.compile( + r"url\(\s*['\"]?(https?://[^'\")\s]+)['\"]?\s*\)", re.IGNORECASE +) + +_RE_OVERFLOW = re.compile( + r"overflow\s*:\s*hidden", re.IGNORECASE +) + +_RE_BG_WHITE = re.compile( + r"background(?:-color)?\s*:\s*(white|#fff(?:fff)?|transparent)\b", re.IGNORECASE +) + +_RE_COLOR_HEX = re.compile(r"#([0-9a-fA-F]{3,8})") +_RE_COLOR_RGB = re.compile(r"rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)") + +_RE_STYLE_BLOCK = re.compile(r"]*>(.*?)", re.IGNORECASE | re.DOTALL) +_RE_INLINE_STYLE = re.compile(r'style\s*=\s*["\']([^"\']*)["\']', re.IGNORECASE) + +_RE_CSS_RULE = re.compile( + r"([^{]+)\{([^}]*)\}", re.DOTALL +) + +_RE_WIDTH_PX = re.compile(r"width\s*:\s*(\d+(?:\.\d+)?)\s*px", re.IGNORECASE) + + +def _parse_font_list(raw: str) -> list[str]: + """Split a font-family value into individual font names (unquoted, stripped).""" + fonts: list[str] = [] + for part in raw.split(","): + name = part.strip().strip("'\"").strip() + if name: + fonts.append(name) + return fonts + + +def _has_generic(fonts: list[str]) -> bool: + return any(f.lower() in GENERIC_FAMILIES for f in fonts) + + +def _best_generic(fonts: list[str]) -> str: + """Pick the best generic fallback for a list of named fonts.""" + lower = [f.lower() for f in fonts] + if any(f in CHINESE_FONTS for f in lower): + return "sans-serif" + if any(f in SERIF_FONTS for f in lower): + return "serif" + return "sans-serif" + + +# --------------------------------------------------------------------------- +# Color / contrast helpers +# --------------------------------------------------------------------------- + +def _hex_to_rgb(h: str) -> tuple[int, int, int] | None: + h = h.lstrip("#") + if len(h) == 3: + h = h[0]*2 + h[1]*2 + h[2]*2 + if len(h) == 4: + h = h[0]*2 + h[1]*2 + h[2]*2 # ignore alpha + if len(h) == 8: + h = h[:6] # strip alpha + if len(h) != 6: + return None + try: + return int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16) + except ValueError: + return None + + +def _relative_luminance(r: int, g: int, b: int) -> float: + """WCAG 2.x relative luminance.""" + def _c(v: int) -> float: + s = v / 255.0 + return s / 12.92 if s <= 0.03928 else ((s + 0.055) / 1.055) ** 2.4 + return 0.2126 * _c(r) + 0.7152 * _c(g) + 0.0722 * _c(b) + + +def _contrast_ratio(rgb1: tuple[int, int, int], rgb2: tuple[int, int, int]) -> float: + l1 = _relative_luminance(*rgb1) + l2 = _relative_luminance(*rgb2) + lighter = max(l1, l2) + darker = min(l1, l2) + return (lighter + 0.05) / (darker + 0.05) + + +def _extract_color(css_text: str, prop: str) -> tuple[int, int, int] | None: + """Try to extract an RGB color for a given CSS property from a rule body.""" + pat = re.compile(rf"{prop}\s*:\s*([^;]+)", re.IGNORECASE) + m = pat.search(css_text) + if not m: + return None + val = m.group(1).strip() + # Named colours (just the common ones) + named = { + "white": (255, 255, 255), "black": (0, 0, 0), "red": (255, 0, 0), + "green": (0, 128, 0), "blue": (0, 0, 255), "yellow": (255, 255, 0), + "grey": (128, 128, 128), "gray": (128, 128, 128), "transparent": (255, 255, 255), + } + low = val.lower().split()[0].rstrip(";") + if low in named: + return named[low] + m_rgb = _RE_COLOR_RGB.search(val) + if m_rgb: + return int(m_rgb.group(1)), int(m_rgb.group(2)), int(m_rgb.group(3)) + m_hex = _RE_COLOR_HEX.search(val) + if m_hex: + return _hex_to_rgb(m_hex.group(1)) + return None + + +# --------------------------------------------------------------------------- +# HTML visible text extractor (stdlib only) +# --------------------------------------------------------------------------- + +class _TextExtractor(HTMLParser): + """Extract visible text from HTML, skipping +``` + +--- + +### Option 1.2: Sharp Angle Cut (The Angle Slash) + +**Shape type**: Polygon/right trapezoid + +**Calculation logic (define four vertices)**: +- `Point 1` = `(0, H × 0.7)` +- `Point 2` = `(W, H × 0.4)` +- `Point 3` = `(W, H)` +- `Point 4` = `(0, H)` + +**Visual effect**: Forms a tilted geometric color block at the bottom of the page. This sharp linear cut has a strong IT/consulting/engineering industry feel. + +**ReportLab implementation notes**: +```python +c.saveState() +c.clipRect(0, 0, W, H) +c.setFillColorRGB(0, 0, 0, 0.04) # Rule 1 +path = c.beginPath() +# ReportLab Y starts from bottom, needs flipping +path.moveTo(0, H * 0.3) # Corresponds to doc P1: (0, H*0.7) → flipped +path.lineTo(W, H * 0.6) # Corresponds to doc P2: (W, H*0.4) → flipped +path.lineTo(W, 0) # Corresponds to doc P3: (W, H) → flipped +path.lineTo(0, 0) # Corresponds to doc P4: (0, H) → flipped +path.close() +c.drawPath(path, fill=1, stroke=0) +c.restoreState() +``` + +--- + +## Module 2: Typographic Watermarks + +**Goal:** Extract very short metadata text and transform it into an architectural watermark space. + +### Technical Requirements (Mandatory) + +| Constraint | Rule | +|-------|------| +| **Font** | Must use sans-serif, weight must be extra-bold (Black / Heavy / Bold). Recommended: Helvetica Black, Arial Black, Noto Sans SC Heavy | +| **Font prohibition** | **Absolutely forbidden** to use thin or serif fonts for this type of watermark | +| **Character count** | Extracted string length `1–5` characters (e.g. year "2026", abbreviation "AI", "B2B") | +| **Opacity** | Follow global Rule 1 (light bg 2-5%, dark bg 3-6%) | + +### Option 2.1: Side Giant Spine (Vertical Spine) + +**Calculation logic**: +- `Text` = Extract year (e.g. "2026") +- **🔴 Font Size Adaptive Algorithm (Full Text Display Iron Rule):** + 1. `Max_Font_Size` = `W × 0.45` (ideal maximum) + 2. Measure total text height after rotation: `Text_Width = measure(Text, Max_Font_Size)` (after 90° rotation, original width becomes vertical height) + 3. Available vertical space = `H × 0.85` (leaving `H × 0.075` safety margin top and bottom) + 4. If `Text_Width > available vertical space`, scale down proportionally: `Font_Size = Max_Font_Size × (available_vertical_space / Text_Width)` + 5. Final `Font_Size = min(Max_Font_Size, scaled_font_size)` +- `Rotation` = Counterclockwise 90° (`-90deg`) +- `Anchor_X` = `W × 0.03` (text fully within page, flush to left but not exceeding) +- `Anchor_Y` = Vertically centered = `(H - Text_Width) / 2` (text centered after rotation) + +**⚠️ Full Display Iron Rule: Background watermark text must be 100% within the visible page area. Any clipping is strictly forbidden. Reduce font size rather than truncate.** + +**Visual effect**: A complete bold number watermark appears on the left side, vertically centered, becoming the visual supporting pillar. Text is fully readable. + +**ReportLab implementation notes**: +```python +c.saveState() +c.clipRect(0, 0, W, H) +c.setFillColorRGB(0, 0, 0, 0.04) + +# Adaptive font size: ensure full text display +max_font_size = W * 0.45 +text = "2026" +text_width = c.stringWidth(text, "Helvetica-Bold", max_font_size) +available_height = H * 0.85 +if text_width > available_height: + font_size = max_font_size * (available_height / text_width) +else: + font_size = max_font_size + +# Recalculate actual width for centering +actual_text_width = c.stringWidth(text, "Helvetica-Bold", font_size) + +c.setFont("Helvetica-Bold", font_size) +# Vertically centered, fully within page +center_y = (H - actual_text_width) / 2 +c.translate(W * 0.03, center_y) +c.rotate(90) # ReportLab counter-clockwise is positive +c.drawString(0, 0, text) +c.restoreState() +``` + +--- + +### Option 2.2: Bottom Full Text + +**Calculation logic**: +- `Text` = Document type English initials (e.g. "REPORT") +- **🔴 Font Size Adaptive Algorithm (Full Text Display Iron Rule):** + 1. `Max_Font_Size` = `W × 0.3` (ideal maximum) + 2. Measure text rendering width: `Text_Width = measure(Text, Max_Font_Size)` + 3. Available horizontal space = `W × 0.90` (leaving `W × 0.05` safety margin left and right) + 4. If `Text_Width > available horizontal space`, scale down proportionally: `Font_Size = Max_Font_Size × (available_horizontal_space / Text_Width)` + 5. Final `Font_Size = min(Max_Font_Size, scaled_font_size)` +- `Rotation` = 0° (horizontal tiling) +- `Anchor_X` = `W × 0.05` +- `Anchor_Y` = Text baseline within the bottom safe zone of the page: `H × 0.92` (text fully displayed at page bottom, not truncated) + +**⚠️ Full Display Iron Rule: Background watermark text must be 100% within the visible page area. Any clipping is strictly forbidden. Reduce font size rather than truncate.** + +**Visual effect**: Text sits solidly at the bottom like a foundation, fully readable, extremely dignified. No more half-truncated text. + +**ReportLab implementation notes**: +```python +c.saveState() +c.clipRect(0, 0, W, H) +c.setFillColorRGB(0, 0, 0, 0.04) + +# Adaptive font size: ensure full text display +max_font_size = W * 0.3 +text = "REPORT" +text_width = c.stringWidth(text, "Helvetica-Bold", max_font_size) +available_width = W * 0.90 +if text_width > available_width: + font_size = max_font_size * (available_width / text_width) +else: + font_size = max_font_size + +c.setFont("Helvetica-Bold", font_size) +# Text fully within page, baseline placed in bottom safe zone +# ascent ≈ font_size * 0.75, ensure letter tops don't exceed page +c.drawString(W * 0.05, font_size * 0.3, text) # baseline slightly above bottom edge +c.restoreState() +``` + +--- + +## Module 3: Blueprint Hairlines + +**Goal:** Use ultra-thin interlacing lines to enhance the "anchoring feel" and logical rigor of foreground text. + +### Technical Requirements + +| Constraint | Rule | +|-------|------| +| **Line width** | `Stroke_Width = 0.5pt` (**must never exceed 1pt**) | +| **Line type** | Solid, or very closely spaced dotted line (Dotted, dash: `[1, 3]`) | +| **Opacity** | Follow global Rule 1 | + +### Option 3.1: Coordinate Cross + +**Calculation logic (height bound to foreground layout anchor)**: +- **Vertical line (Y-axis)**: Read the left-alignment safe boundary of foreground layout (e.g. `X = W × 0.15`). Draw a vertical line from `Y = 0` to `Y = H` +- **Horizontal line (X-axis)**: Read the baseline or top boundary of the foreground main title (e.g. `Y = H × 0.32`). Draw a horizontal line from `X = 0` to `X = W` + +**Visual effect**: The entire page is divided by implicit golden ratio lines, foreground text appears to grow precisely on these coordinate axes, extremely rigorous. + +**ReportLab implementation notes**: +```python +c.saveState() +c.setStrokeColorRGB(0, 0, 0, 0.04) +c.setLineWidth(0.5) +# Vertical line — align to foreground text left boundary +axis_x = W * 0.15 +c.line(axis_x, 0, axis_x, H) +# Horizontal line — align to main title baseline +axis_y = H * 0.68 # ReportLab Y-flip: document H*0.32 → RL H*0.68 +c.line(0, axis_y, W, axis_y) +c.restoreState() +``` + +**Playwright/HTML implementation notes**: +```html +
+ +
+ +
+
+``` + +--- + +## 🛠️ Combination & Circuit Breaker (The Combination Matrix) + +To ensure diversity in auto-generated backgrounds while preventing visual chaos, the system must implement a **"background combination state machine"**. + +For each PDF generation, randomly select one of the following 3 legal Recipes, **cross-boundary combinations are strictly forbidden**: + +### ✅ Recipe A: Minimal Modern + +**Combination**: `Option 1.1 (deep-space arc)` — **this one only, no other elements** + +**Applicable scenes**: Safest, most whitespace, suitable for all types of corporate reports. + +**Pairing suggestions with cover layouts**: +- Layout 1 (diagonal tension) — arc in the blank diagonal area, extremely harmonious +- Layout 2 (vertical gravity axis) — arc provides lower-left gravity +- Layout 4 (golden ratio) — arc adds breathing space to the lower whitespace area + +--- + +### ✅ Recipe B: Engineering/Academic + +**Combination**: `Option 3.1 (coordinate cross)` **+** `Option 2.1 (side giant spine)` + +**Logic**: Vertical giant text interlaces with ultra-thin coordinate lines on the left, creating extreme thick-thin contrast, perfect for investment pitches and research reports. + +**Line avoidance rule**: Option 3.1's ultra-thin lines should avoid crossing directly through Option 2.1 (giant text) strokes to prevent visual interference. Adjust line coordinates to avoid text areas: +- Vertical line `X` at `W × 0.15` (foreground text left-alignment line) +- Giant spine anchor `X` at `-W × 0.05` (left of vertical line, no crossing) + +**Pairing suggestions with cover layouts**: +- Layout 7 (left-aligned matrix) — perfect match, left spine + coordinate lines + matrix text three-layer overlay +- Layout 2A (left-aligned vertical) — vertical line aligns with axis, doubled structural feel +- Layout 6A (side rotation decoration) — Note: 6A already has rotated year, if using Recipe B then **skip Option 2.1**, keep only Option 3.1 + +--- + +### ✅ Recipe C: Solid/Weighty + +**Combination**: `Option 1.2 (sharp angle cut)` **+** `Option 2.2 (bottom full text)` + +**Logic**: Bottom angular color block overlaid with fully displayed English word at the bottom, very low center of gravity, suitable for annual summaries and white papers. + +**Stacking order**: Draw Option 1.2 angular color block first, then Option 2.2 bleed text (text on top of color block, but both below foreground content). + +**Pairing suggestions with cover layouts**: +- Layout 4A (top suspended) — content on top, background pressed below, extreme top-bottom contrast +- Layout 1A (diagonal tension) — bottom gravity echoes lower-right text +- Layout 5A (stepped progression) — steps extend to lower-right, converging with bottom gravity + +--- + +## 🚫 Circuit Breaker Rules (Hard Constraints) + +The following combinations are **hard-forbidden**; violations are bugs: + +| Forbidden Rule | Reason | +|---------|------| +| Option 2.1 + Option 2.2 together | **No dual text**. Only one giant text watermark allowed per page | +| Option 1.1 + Option 1.2 together | **No dual geometry**. Large circle + large diagonal = visual chaos | +| Option 3.1 lines crossing Option 2.x strokes | **Line isolation**. Ultra-thin lines must not cross giant text strokes; adjust coordinates to avoid | +| All three modules enabled | **Maximum two modules**. Three layers = overwhelms foreground | +| Any background element opacity > 6% | **Rule 1 violation**. Background must be subtle and barely visible | + +--- + +## Recipe Selection Logic + +When no specific recipe is specified, auto-select based on document type: + +| Document Type | Recommended Recipe | Reason | +|---------|---------|------| +| Corporate reports, general docs | **A** | Safest, zero risk | +| Technical reports, investment pitches | **B** | Engineering feel, precision | +| Annual summaries, white papers | **C** | Solid, weighty | +| Creative, design | **A** | Maximum whitespace, no conflict with creative content | +| Academic papers | **B** | Structural feel matches academic tone | +| Uncertain / default | **A** | Minimal never goes wrong | + +--- + +## Relationship with geometry.md + +This file defines **page-level macro backgrounds** (Supergraphics / Watermarks / Hairlines), applied to the entire canvas. + +`geometry.md` defines **local decorative anchors** (Offset Stacking / Scale Contrast / Grid Intersection), applied to specific areas. + +Both can coexist, but note: +- Background layer (this file) is at the bottom +- Geometric anchors (geometry.md) are above the background layer but below foreground text +- If both are used, geometric anchors should be placed in "blank areas" of background elements to avoid visual overlap diff --git a/skills/pdf/typesetting/cover.md b/skills/pdf/typesetting/cover.md new file mode 100755 index 0000000..7771ff2 --- /dev/null +++ b/skills/pdf/typesetting/cover.md @@ -0,0 +1,1442 @@ +# Cover Design V3.0 - Cover Layout Engine Specification + +> The cover is the first impression. Either skip it, or build it like architecture. + +--- + +## ⚠️ Critical Rules (Read First) + +1. **Cover is OPTIONAL.** Do NOT force a cover on documents that don't need one. When in doubt, skip. +2. **Unified cover system.** All routes (Report, Creative, Academic) use the HTML/Playwright cover system. Templates 01-07 are general-purpose; Templates 08-10 are academic-specific (dark backgrounds, scholarly typography); **Template 11 is institutional (white bg, black border frame, structured fields)**. All routes generate covers via Playwright and merge via pypdf. +3. **Single PDF output.** Never deliver a separate cover PDF. + - **Creative route**: Cover is part of the same HTML document → single PDF output inherently. + - **Report route**: Cover PDF (Playwright) + body PDF (ReportLab) → merged via pypdf into one final PDF. + - **Academic route**: Cover PDF (Playwright) + body PDF (Tectonic) → merged via pypdf into one final PDF. +4. **Page isolation.** Cover content must NEVER share a page with TOC, body text, or any subsequent content. Report/Academic: isolation is inherent in the merge pipeline. Creative: CSS page-break enforces isolation. Cover + TOC on the same page = **critical bug**. + +--- + +## When to Include a Cover + +| Document Type | Cover Needed | Notes | +|---------------|-------------|-------| +| Formal report (annual, research, white paper) | ✅ Required | Conveys professionalism | +| Proposal / plan | ✅ Required | First impression is everything | +| Resume | ❌ Not needed | Content itself is the cover | +| Menu / flyer / card | ❌ Not needed | Single page or function-oriented | +| Invitation | ❌ Not needed | The front side IS the cover | +| Lab report / academic paper | ⚠️ Situational | Add when template requires it | +| Portfolio / lookbook | ✅ Required | Cover sets the tone | + +--- + +# PART 0: GLOBAL ENGINE ARCHITECTURE (Mandatory) + +These three architectural upgrades are **mandatory** for ALL cover rendering. They eliminate 90% of squished/misaligned/overflow bugs at the engine level. + +--- + +## A0.0 - Global Base Parameters + +**All templates MUST obey these.** + +``` +W = page total width +H = page total height +U = W * 0.05 # Base spacing unit (5% of page width) + # All spacing should be multiples of U +``` + +**Coordinate origin:** `(0, 0)` at top-left corner. Some PDF libraries (ReportLab) use bottom-left origin - invert Y axis accordingly: `y_pdf = H - y_spec`. + +--- + +## A0.1 - Absolute Anchor Grid (Replaces Flow Layout) + +**Old (DEPRECATED):** `Y = previous_element_Y + height + spacing` - if one element overflows, everything below shifts and gets crushed. + +**New (MANDATORY):** Page height = 100%. Every element group gets an **absolute percentage Y-anchor**. Elements may only grow **within their own bounding box**. They NEVER push or compress other blocks. + +### Implementation Rule + +``` +# WRONG - flow layout (banned) +y_cursor = PAGE_H - top_margin +y_cursor -= title_height +y_cursor -= gap +y_cursor -= subtitle_height # ← if title wraps, subtitle gets crushed + +# RIGHT - absolute anchor grid +ANCHOR_TITLE_Y = H * 0.30 +ANCHOR_SUMMARY_Y = H * 0.50 +ANCHOR_META_Y = H * 0.70 +ANCHOR_FOOTER_Y = H * 0.90 +# Each block renders at its anchor, independent of others +``` + +### Bounding Box Containment + +Each block has a **maximum bounding box** defined by two consecutive anchors: +- Block can grow downward within `[own_anchor_Y, next_anchor_Y - min_gap]` +- If content exceeds its bounding box → trigger overflow protection (see Part 3) +- Blocks NEVER consume space belonging to adjacent blocks + +--- + +## A0.2 - Typography Weight & Spacing System + +Use **weight**, **letter-spacing**, and **opacity** to create hierarchy - not just font size. + +### Mandatory Type Roles + +| Role | Size | Weight | Letter-Spacing | Line-Height | Opacity | Purpose | +|------|------|--------|----------------|-------------|---------|---------| +| **Kicker / Footer** (decorative text) | 16pt | Regular | 3pt (very wide) | - | 60% | Wide spacing + transparency makes 16pt text feel delicate and recessive | +| **Summary / Description** (summary paragraph) 🆕 | 16-18pt | Regular | normal | **1.6** | 85% | **Fill visual space** - 2-4 lines of descriptive text that prevents empty covers | +| **Meta / Subtitle** (secondary text) | 20-22pt | Light / Regular | normal | 1.4 | 85% | Comfortable reading rhythm, clear secondary hierarchy | +| **Hero Title** (main title) | 45-65pt (CJK: 50-80pt) | Black / Heavy (extra bold) | normal-tight | **1.15** (multi-line) | 100% | Must create overwhelming scale contrast; visually dominates the page. CJK characters need +15-20% size to match Latin visual weight | + +### 🔴 Data-to-Drawer Binding Rule + +> **Hero Title = Company/entity name. Kicker = Report type/subtitle. Never reverse.** + +When users provide structured information (company name + report name/type), they must be bound to typography drawers by these rules: + +| User Data Field | Bound to Typography Role | Notes | +|-------------|-------------|------| +| **Company/entity name** (e.g. "GREENTECH") | **Hero Title** (45-65pt Heavy) | Company name is the **absolute visual center**, largest font, heaviest weight | +| **Report type/subtitle** (e.g. "2025 Annual Report Summary") | **Kicker** (16pt, letter-spacing 3pt, opacity 60%) | Report name is decorative supplementary text, placed in small text position at top-left/above title | +| **Summary/description** | **Summary** (16-18pt) | Detailed description text | +| **Date/author/version** | **Meta** (20-22pt) | Auxiliary information | +| **Document number/org signature** | **Footer** (16pt, opacity 60%) | Bottom closing | + +**Mapping priority (when ambiguous):** +1. If user provides only one name → treat as company/entity name, bind to Hero Title +2. If user provides two names → shorter/more brand-like → Hero Title; longer/descriptive → Kicker or Summary +3. If user explicitly labels "title" and "subtitle" → title → Hero Title, subtitle → Kicker +4. **Never use report type names (e.g. "Annual Report", "White Paper") as Hero Title's largest text** - report type is always Kicker-level decorative text + +### The Summary Block Rule (Anti-Void Iron Rule) 🆕 + +> Every cover MUST include a Summary/Description text block. If the user provides no summary, the system MUST auto-generate one. + +**Why:** A cover with only a title and date looks barren. The Summary block physically fills 2-4 lines of space, preventing the "empty field" aesthetic. + +**Auto-generation rule:** When no summary/description is provided: +```python +# Generate a default summary +if not summary_text: + summary_text = f"This report was generated by {org_name or 'the system'}, containing comprehensive data analysis and insights." + # Or in English: + # summary_text = f"This report presents comprehensive analysis and key insights prepared by {org_name or 'the organization'}." +``` + +**Constraints:** +- Width: template-specific (typically `W * 0.5` to `W * 0.6`) +- Lines: 2-4 lines (auto-wrap at width boundary) +- Never truncate summary - if too long, reduce to 4 lines max with `...` + +### Font Weight Fallback + +- If font family lacks Black/Heavy weight → use Bold + slightly larger size (+4pt) +- If font family lacks Light weight → use Regular + increased letter-spacing (+1pt) +- CJK fonts: STSong-Light for Regular/Light roles; for Heavy/Black → increase size by 15% to compensate for single-weight CJK fonts +- English kickers/footers: **FORCE UPPERCASE** via code (`text.upper()` / `toUpperCase()`) + +--- + +## A0.3 - Z-Index Layer Management + +All cover elements must be rendered in strict layer order. No exceptions. + +| Layer | Z-Index | Contents | Rules | +|-------|---------|----------|-------| +| **Layer 0** (base) | 0 | Background fill (white / light gray) | Always rendered first; full page | +| **Layer 1** (background) | 1 | Grids, watermark letters, decorative blocks, large clipped graphics | **MUST enable clip-path** - background elements may extend beyond logical bounds but must be clipped to page physical bounds. Never let background elements inflate PDF page size. | +| **Layer 2** (structure) | 2 | Ultra-thin divider lines, sidebars, corner crop marks | Structural guides that define spatial zones | +| **Layer 3** (content) | 3 | All readable text content | Rendered last, always on top | + +### Clip-Path Enforcement + +> **Since V2.1, all covers are rendered via HTML/CSS.** The canonical clip pattern is CSS `overflow: hidden`. The ReportLab Python example below is kept as legacy reference for body-page background elements only. + +```css +/* HTML/CSS cover (CANONICAL): clip background overflow */ +.cover-bg-layer { + position: absolute; + inset: 0; + overflow: hidden; /* MANDATORY */ + z-index: 1; +} +``` + +```python +# ReportLab (legacy reference, body pages only): clip background elements to page bounds +canvas.saveState() +p = canvas.beginPath() +p.rect(0, 0, W, H) +canvas.clipPath(p, stroke=0) +# ... render background elements (Layer 1 ONLY) ... +canvas.restoreState() +``` + +> ⚠️ **Clip scope = Layer 1 ONLY。** In HTML/CSS covers, `overflow: hidden` must ONLY be on the Layer 1 background container. Layer 2 (lines) and Layer 3 (text) containers must NOT have `overflow: hidden`. +> For ReportLab body pages: `saveState()`/`restoreState()` must close immediately after Layer 1 background rendering. +> Layer 2 (lines) and Layer 3 (text) must never be rendered within a clip scope, otherwise text will be clipped. + +### 🔴 Anti-Clip Bug (Layer 3 Text Truncation Fix) + +**Symptom:** Cover text is visible but truncated by an invisible boundary, only half visible. + +**Root cause:** clip/overflow scope not closed in time, causing subsequently rendered text to be clipped by the same clip rect. + +**Iron rule (HTML/CSS - canonical cover implementation):** +```html + +
+ +
+
+ +
+
+ +
+ + +
+
...
+
...
+
...
+
+``` + +**Iron rule (ReportLab - body page background element reference):** +```python +# ✅ CORRECT - clip only wraps Layer 1 +canvas.saveState() +canvas.clipPath(page_clip, stroke=0) +render_layer_1_background(canvas) # Background decoration +canvas.restoreState() # ← Must close here immediately! + +render_layer_2_lines(canvas) # Structure lines - not inside clip +render_layer_3_text(canvas) # Text content - not inside clip + +# ❌ WRONG - text clipped by clip scope +canvas.saveState() +canvas.clipPath(page_clip, stroke=0) +render_layer_1_background(canvas) +render_layer_2_lines(canvas) # ← Gets clipped +render_layer_3_text(canvas) # ← Gets clipped! Text only half visible +canvas.restoreState() +``` + +```css +/* Creative (HTML): Same principle */ +.cover-bg-layer { overflow: hidden; z-index: 1; } /* clip only on background layer */ +.cover-line-layer { overflow: visible; z-index: 2; } /* lines not clipped */ +.cover-text-layer { overflow: visible; z-index: 3; } /* text not clipped */ +``` + +### 🔴 No Page Border/Frame + +**Symptom:** A rectangular border appears around the entire cover page, looking like a table. + +**Root cause:** ReportLab's `Frame()` defaults to `showBoundary=0`, but if set to `1` or `True`, it shows a border. Also `canvas.rect()` may accidentally draw a full-page rectangle. + +**Iron rule:** +```python +# Cover page Frame must have showBoundary=0 +Frame(x, y, w, h, showBoundary=0) # Always 0 + +# Never draw a full-page border on the cover +canvas.rect(0, 0, W, H) # ❌ BANNED on cover page + +# If using doc.showBoundary, cover page must be skipped +doc = SimpleDocTemplate(..., showBoundary=0) # Always 0 in production +``` + +```css +/* Creative: No outer border on covers */ +.cover-page { + border: none !important; + outline: none !important; + box-shadow: none !important; +} +``` + +### 🔴 Minimum Spacing Between Decorative Lines and Text (Line-to-Text Spacing) + +**Symptom:** Decorative lines on the cover (Layer 2 dividers, corner marks, sidebar edges) are flush against or overlapping with text. + +**Iron rule:** +``` +Minimum spacing between decorative lines and any text content = U (= W * 0.05) +i.e., at least 1 U of whitespace between line edges and text edges +``` + +| Line Type | Minimum Spacing | Notes | +|---------|---------|------| +| Horizontal divider | `U` above and below the line | Line must not be flush against title or body text | +| Vertical sidebar edge | `U` to the right of the line | Text inside sidebar must maintain spacing from the edge | +| Corner marks / crop marks | `1.5 * U` from mark endpoint to nearest text | Marks must not touch text | +| Ultra-thick anchor line (Template 01) | `2 * U` to the right of the line | Thick line and title need ample breathing room | + +```python +# Example: Template 01 vertical thick line to title spacing +thick_line_x = 0.10 * W +title_x = thick_line_x + 2 * U # 2U spacing to the right of the thick line +# ❌ WRONG: title_x = thick_line_x + 5 # 5pt is too close, will overlap +``` + +--- + +# PART 1: SEVEN COVER TEMPLATES (7 Cover Rendering Specifications) + +Coordinates use `W` (page width) and `H` (page height). `U = W * 0.05` (base spacing unit). + +All templates inherit the A0.0-A0.3 architecture rules above. + +**Each template now includes a mandatory Summary/Description drawer** to fill visual space. + +--- + +## Template 01: HUD Data Terminal - Ultra-Thick Vertical Anchor Line + +**Design intent:** A single bold vertical line on the left anchors all visual weight. The thick line eliminates left-side floating. Clean, data-driven, authoritative. + +### Layer 1 - Background +- Full-page grid pattern: horizontal + vertical lines at ~50pt intervals +- Grid color: primary color at **2% opacity** (white background, nearly invisible) +- Grid line width: `0.5pt` + +### Layer 2 - Structure +- **Left anchor line:** Start `(0.12*W, 0.1*H)`, End `(0.12*W, 0.9*H)`. Line width = **6pt**, primary color. +- **Meta separator line:** At `Y_meta - 10pt`, from `X_content` to `X_content + W*0.4`, line width = **1pt**, primary color at 40% opacity. + +### Layer 3 - Content + +**Content left edge: `X_content = 0.12*W + 30pt`** (offset from the thick line) + +| Drawer | Y-Anchor | Content | Constraints | +|--------|----------|---------|-------------| +| **A - Kicker** | `0.15 * H` | Report type / subtitle (e.g. "2025 Annual Report Summary") | 16pt, Regular, letter-spacing 3pt, opacity 60%, uppercase | +| **B - Hero Title** | `0.30 * H` | **Company/entity name** (e.g. "GREENTECH") | 45-65pt (CJK: 50-80pt), Heavy. Company name is the visual center | +| **C - Summary** 🆕 | `0.50 * H` | 2-3 lines descriptive text about the report | 16-18pt, Regular, line-height 1.6, opacity 85%. **Width limit: `W * 0.6`**, auto-wrap. This drawer fills the mid-page void | +| **D - Meta/Date** | `0.75 * H` | Author, org, date | 16-20pt, Regular. Top edge separated by the 1pt meta line | + +### Best For +Technology reports, data analysis, dashboard summaries, technical white papers + +--- + +## Template 02: Corporate Editorial - Top Bar with Bottom Accent + +**Design intent:** Top-bottom symmetry. Top bar provides structural weight, bottom-right info block creates diagonal balance. Solves the "empty edges" problem. + +### Layer 1 - Background +- **Background giant year watermark:** Text = current year (e.g. "2026"), **Max font size = 180pt**, measure rendered width - if it exceeds `W * 0.85`, scale down proportionally. Position: `X = W - 20pt` (right edge), `Y = 0.15*H`. Color = primary at **4% opacity**. Font weight = Black. ⚠️ **Full-display iron rule: watermark text must be 100% within the visible page area - cropping is strictly forbidden. Prefer reducing font size over truncation.** + +### Layer 2 - Structure +- **Top bar (skyline):** Rectangle at `(0, 0)`, width = `W`, height = **15pt**, primary color fill. Edge-to-edge. +- **Right info accent line (edge seal):** Vertical line at `X = 0.88*W`, from `Y = 0.75*H` to `Y = 0.88*H`. Line width = **4pt**, primary color. + +### Layer 3 - Content + +| Drawer | Position | Content | Constraints | +|--------|----------|---------|-------------| +| **Left upper - Title group** | `X = 0.12*W`, `Y = 0.15*H` | Kicker (report type/subtitle, 16pt) → Hero Title (company/entity name, 45-65pt / CJK 50-80pt, Heavy) | Stack downward from anchor | +| **Mid-left - Summary** 🆕 | `X = 0.12*W`, `Y = 0.50*H` | Descriptive paragraph | 16-18pt, Regular, line-height 1.6. **Width limit: `W * 0.5`** | +| **Right lower - Meta** | Right-aligned at `X = 0.88*W - 20pt`, `Y = 0.70*H` | Date, version, author | **Right-aligned**, 16-20pt. Must hug the 4pt accent line | + +### Best For +Annual reports, financial summaries, investor documents, corporate governance reports + +--- + +## Template 03: The Monolith - Hard-Left Alignment + Right-Side Giant Watermark Counterweight + +**Design intent:** Everything hard-left. Right-side watermark counterbalances the asymmetry. Solves the "right half is empty" bug. + +### Layer 1 - Background +- **Right-side vertical watermark (load-bearing wall):** Extract a short English word (e.g. "REPORT"). **Auto-scaling font size:** `Max_Font_Size = 180pt`, measure total height after rotation - if it exceeds `H * 0.85`, scale down proportionally. **Rotate 90° clockwise** (or use vertical text mode). Anchor at `X = 0.85*W`, vertically centered: `Y = (H - rendered_text_width) / 2`. Color = primary at **3% opacity**. ⚠️ **Full-display iron rule: watermark text must be 100% within the visible page area - cropping is strictly forbidden. Prefer reducing font size over truncation.** + +### Layer 2 - Structure +- **Color dash (visual guide line):** At `(0.12*W, 0.15*H)`, draw a horizontal bar: width = **50pt**, height = **5pt**, primary color. +- **Meta accent line:** At `(0.12*W, Y_meta)`, vertical line: height = meta text block height, width = **2pt**, primary color at 50% opacity. + +### Layer 3 - Content + +**Unified left edge: `X = 0.12*W`** + +| Drawer | Y-Anchor | Content | Constraints | +|--------|----------|---------|-------------| +| **A - Color dash** | `0.15 * H` | Structure element (not text) | 50pt × 5pt bar | +| **B - Kicker** | `0.20 * H` | Report type / subtitle | 16pt, Regular, letter-spacing 3pt, uppercase, opacity 60% | +| **C - Hero Title** | `0.28 * H` | **Company/entity name** | 45-65pt (CJK: 50-80pt), Heavy | +| **D - Summary** 🆕 | `0.45 * H` | Descriptive paragraph (key anti-void element) | 16-18pt, Regular, line-height 1.6. **Width limit: `W * 0.55`** (must not collide with right watermark) | +| **E - Meta** | `0.70 * H` | Author, org, version | 20pt, Regular, line-height 2.0. Left of the 2pt accent line | +| **F - Footer** | `0.90 * H` | Date + doc number, right-aligned at `X = 0.88*W` | 16pt, Regular, opacity 60% | + +### Best For +White papers, project proposals, government documents, technical standards + +--- + +## Template 04: Museum Minimal - Refined Corner Crop Marks + +**Design intent:** Abandon all-over scattered layout. Four corner crop marks form an invisible "force field box" that concentrates all content dead center. + +### Layer 2 - Structure +- Set safety margin `M = 0.08 * W` +- **Four corner marks** at inner corners: `(M, M)`, `(W-M, M)`, `(M, H-M)`, `(W-M, H-M)` +- Each mark: L-shaped, arm length = **30pt**, line width = **2pt**, primary color at 60% opacity +- Marks point **inward** (top-left: right arm + down arm) + +### Layer 3 - Content + +**This template FORBIDS hardcoded absolute Y coordinates.** + +**Centering algorithm (mandatory):** +1. Pre-compose ALL text elements (kicker + title + summary + meta) into a single virtual Text Block +2. Calculate the block's total rendered height `Block_H` +3. Position block: `X = 0` (full width, center-aligned text), `Y = (H - Block_H) / 2` +4. **This guarantees the content group is vertically centered regardless of how much content there is** + +**Internal spacing within the centered block:** +- Kicker → Title: `24pt` +- Title → Summary: `20pt` +- Summary → Meta: `40pt` + +### Type Scale +| Role | Size | Notes | +|------|------|-------| +| Kicker | 16pt | Uppercase, letter-spacing 4pt, opacity 50%. Bound to report type/subtitle | +| Hero Title | 48-60pt | Heavy - slightly smaller than other templates to fit center composition. **Bound to company/entity name** | +| Summary 🆕 | 16-18pt | Regular, line-height 1.6, center-aligned, width ≤ `W * 0.6` | +| Meta | 16pt | Regular, opacity 60%, at bottom of group | + +### Best For +Gallery catalogs, design portfolios, exhibition materials, luxury brand documents + +--- + +## Template 05: Floating Diagonal - Premium Whitespace with Binding Line + +**Design intent:** "Left-upper to right-lower" diagonal visual flow. The two text groups create tension across whitespace. The gap IS the design. + +### Layer 2 - Structure +- **Binding dashed line:** At `X = 0.08*W`, from `Y = 0.05*H` to `Y = 0.95*H`. Line width = **1pt**, dashed (dash 6pt, gap 8pt), color = light gray (#d0d0d0, 40% opacity). + +### Layer 3 - Content + +| Group | Position | Content | Constraints | +|-------|----------|---------|-------------| +| **Upper-left group** | Anchor: `X = 0.15*W`, `Y = 0.20*H` | Kicker (report type/subtitle, 16pt gap) → Hero Title (company/entity name, 45-65pt / CJK 50-80pt, Heavy) | Left-aligned. Width limit: `W * 0.7` | +| **Lower-right group** 🆕 | Anchor: `X = 0.45*W`, `Y = 0.60*H` | Summary + Meta + Footer | **Left-aligned** (NOT right-aligned - intentional asymmetry). A **3pt vertical accent line** of height = group text height is drawn at `X = 0.45*W - 12pt` as a visual anchor. Line-height 2.0 for meta, 24pt gap before footer | + +**Visual effect:** Upper-left and lower-right groups are "pulled apart" across the diagonal. The empty top-right and bottom-left create tension, not emptiness. + +### Best For +Creative reports, editorial layouts, art direction documents, brand guidelines + +--- + +## Template 06: Swiss Grid - Ultimate Precision, Curing All Misalignment + +**Design intent:** The typographic "multiplication table." Thick lines physically slice the page into cells. Content fills its assigned cell. Impossible to misalign. + +### Layer 2 - Structure (ABSOLUTE - non-negotiable) + +``` +Horizontal line 1: (0.1*W, 0.25*H) → (0.9*W, 0.25*H), width 2pt, primary +Horizontal line 2: (0.1*W, 0.75*H) → (0.9*W, 0.75*H), width 2pt, primary +Vertical line 1: (0.45*W, 0.25*H) → (0.45*W, 0.75*H), width 2pt, primary +``` + +These create 4 zones: + +``` +┌──────────────────────────────────────┐ +│ Zone A - Top Strip │ ← Kicker / report type +├──────────────────┬───────────────────┤ +│ Zone B │ Zone C │ +│ (left cell) │ (right cell) │ ← B: Hero Title (MUST fill) +│ X: 0.1W-0.43W │ X: 0.48W-0.9W │ ← C: Summary text (MUST fill) +├──────────────────┴───────────────────┤ +│ Zone D - Bottom Strip │ ← Footer / year / doc number +└──────────────────────────────────────┘ +``` + +### Layer 3 - Content (STRICT zone containment) + +| Zone | Content | X Range | Y Range | Notes | +|------|---------|---------|---------|-------| +| **A** | Kicker / report type | `0.10*W` - `0.90*W` | `0.15*H` - `0.23*H` | Left-aligned at `X = 0.12*W` | +| **B** | **Hero Title (company/entity name)** | `0.10*W` - `0.43*W` | `0.28*H` - `0.70*H` | **Width = `0.33*W`**. Font must be large enough to physically fill the cell. Text wraps at boundary. | +| **C** | **Summary text** 🆕 | `0.48*W` - `0.90*W` | `0.28*H` - `0.70*H` | **Must contain substantial descriptive text** - this zone MUST be filled. 16-18pt, Regular, line-height 1.6. This is the primary anti-empty-page mechanism. | +| **D** | Footer / date / number | `0.10*W` - `0.90*W` | `0.78*H` - `0.88*H` | Can split: left part + right-aligned part | + +### Zone Overflow Protection (MANDATORY) + +If text in Zone B or C exceeds the vertical boundary (Y > `0.70*H`): +1. **Step 1:** Reduce font size by 2pt increments (minimum: 16pt for summary, 40pt for title) +2. **Step 2:** If still overflows, truncate with `...` ellipsis +3. **NEVER** let text cross a grid line - the grid is sacred + +**Hard width enforcement:** +```python +# Zone B: title MUST wrap within its cell width +zone_b_max_width = 0.33 * W +# If title renders wider → word-wrap, NEVER let it bleed into Zone C + +# Zone C: summary MUST wrap within its cell width +zone_c_max_width = 0.42 * W # (0.90 - 0.48) * W +# Wrap at boundary, add lines, NEVER cross the vertical grid line +``` + +### Best For +Swiss-style design, data-heavy reports, structured corporate documents, annual reports + +--- + +## Template 07: Solid Sidebar - Massive Pillar Anchoring the Page + +**Design intent:** A massive solid-color sidebar provides gravitas. The right side can be loosely arranged - the pillar holds everything together. + +### Layer 1 - Background +- **Left sidebar block (giant sidebar pillar):** Rectangle at `(0, 0)`, width = **`0.1*W`** (~80pt on A4), height = `H`. Primary color fill. +- **Sidebar watermark:** Inside the sidebar, render a short word (doc type or year) rotated **-90°**, white at **15% opacity**, vertically centered within the sidebar. **Auto-scaling font size:** `Max_Font_Size = H * 0.5`, measure total height after rotation - if it exceeds `H * 0.85`, scale down proportionally. ⚠️ **Full-display iron rule: watermark text must be 100% within the visible page area - cropping is strictly forbidden.** + +### Layer 2 - Structure +- **Bottom horizontal line:** At `Y = 0.90*H`, from `X = Left_Edge` to `X = 0.90*W`. Line width = **1pt**, primary color at 30% opacity. + +### Layer 3 - Content + +**Safety boundary: `Left_Edge = 0.1*W + 40pt`** - ALL text must start at or right of this line. Zero tolerance for collision with sidebar. + +**Layout uses relative vertical centering:** +1. Compose full text group: Kicker + Hero Title + Summary + Meta +2. Calculate total group height +3. Position group at `X = Left_Edge`, `Y = (H - group_height) / 2` (vertically centered) + +| Element | Notes | +|---------|-------| +| Kicker | 16pt, Regular, uppercase, letter-spacing 3pt, opacity 60%. Bound to report type/subtitle | +| Hero Title | 45-65pt, Heavy. **Bound to company/entity name** | +| Summary 🆕 | 16-18pt, Regular, line-height 1.6. Width ≤ `0.90*W - Left_Edge` | +| Meta | 20pt, Regular, line-height 1.8 | + +**Footer (separate from centered group):** +- On/just above the bottom horizontal line at `Y = 0.90*H - 10pt` +- Left-aligned date at `X = Left_Edge`, right-aligned org name at `X = 0.90*W` +- 16pt, Regular, opacity 60% + +### Best For +Government/institutional reports, legal documents, formal project deliverables, bidding documents + +--- + +# PART 2: TEMPLATE SELECTION GUIDE + +Template selection uses a two-dimensional matrix: **Intent** (from `visual_framework.md` 5-intent system) × **Document Type**. This replaces the old "Document Tone" classification and aligns with the Intent Mapping Table in `creative.md`. + +| Intent | Document Type | Recommended Templates | Default | +|--------|---------------|----------------------|---------| +| **Calm** | Healthcare / Wellness / Minimalist | 04 Museum, 01 HUD | **04** | +| **Calm** | Academic / Research | 06 Swiss Grid, 03 Monolith | **06** | +| **Tension** | Crisis / Alert / Disruption | 01 HUD, 05 Diagonal | **01** | +| **Energy** | Marketing / Creative / Design | 05 Diagonal, 06 Swiss Grid | **05** | +| **Energy** | Technology / Data | 01 HUD, 06 Swiss Grid | **01** | +| **Authority** | Formal / Corporate / Financial | 02 Corporate, 03 Monolith | **03** | +| **Authority** | Government / Bidding | 07 Sidebar, 03 Monolith, **11 Institutional** | **07** | +| **Authority** | Thesis proposal / Dissertation cover | **11 Institutional** | **11** | +| **Authority** | Luxury / Editorial | 03 Monolith, 05 Diagonal | **03** | +| **Warmth** | Food / Lifestyle / Home | 04 Museum, 05 Diagonal | **04** | + +> **Legacy mapping:** "Formal/Corporate" tone → Authority intent, "Minimalist" tone → Calm intent, "Luxurious/Editorial" tone → Authority intent. + +**⚠️ No Global Default.** When no specific style is explicitly requested, the LLM MUST analyze the document's content, tone, and audience to autonomously select the most fitting template. Cross-reference the Intent (derived from content via `design_engine.py derive` or manual judgment) with the Document Type to find the best match. Every cover selection must be a deliberate design decision. + +--- + +# PART 3: CODE-LEVEL SAFETY MEASURES (Pre-Render Safeguards) + +These checks run BEFORE final rendering. They are **mandatory** - not optional optimizations. + +--- + +## S3.0 - Cover Overlap Validation (MANDATORY) + +**After generating the cover HTML and before rendering to PDF, run:** + +```bash +node "$PDF_SKILL_DIR/scripts/cover_validate.js" cover.html +``` + +This detects text-vs-decorative-line overlaps by rendering the HTML and measuring actual bounding boxes. Exit code 1 = overlap found, must fix before generating the PDF. + +The minimum gap between any text element and any decorative line is **1U (= 5% of page width ≈ 40px on A4)**. This catches the exact bug shown in the "text overlapping decorative lines" screenshots. + +**If the check fails:** +1. Adjust the decorative line's Y position to maintain ≥ 1U gap from the nearest text +2. Or adjust the text block's position/size to avoid the overlap +3. Re-run `cover_validate.js` until it passes (exit code 0) + +--- + +## S3.1 - Hero Title Overflow Protection (Title Line-Wrapping) + +**Rule:** Hero title must NEVER exceed its template's width boundary. + +**Algorithm:** +1. Measure the rendered width of the hero title string at the target font size +2. If `rendered_width > max_width`: + - Word-wrap at boundary (CJK: any character; Latin: space/hyphen; Mixed: CJK/Latin boundaries) + - Multi-line hero titles: **lock line-height to 1.15** +3. Maximum lines: **3** - if title needs 4+ lines, reduce font size by 4pt increments until ≤ 3 lines +4. Minimum font size floor: **40pt** (below this, truncate with `...`) + +```python +def safe_hero_title(text, font, max_size, max_width, min_size=40): + size = max_size + while size >= min_size: + lines = word_wrap(text, font, size, max_width) + if len(lines) <= 3: + return lines, size + size -= 4 + return truncate_with_ellipsis(text, font, min_size, max_width, max_lines=3), min_size +``` + +--- + +## S3.2 - Zone Collision Detection + +After rendering each text block, check if its bottom edge penetrates the next zone boundary: + +1. **Step 1 - Font reduction:** Decrease by 2pt. Floor: 16pt for meta/summary, 40pt for titles. +2. **Step 2 - Truncation:** If font reduction fails, truncate with `...` +3. **Step 3 - Log warning:** Output a warning about content truncation + +```python +def enforce_zone_bounds(text, font, size, zone_y_max, min_size=16): + while size >= min_size: + rendered_height = measure_text_height(text, font, size) + if current_y + rendered_height <= zone_y_max: + return text, size + size -= 2 + return truncate_to_fit(text, font, min_size, zone_y_max - current_y), min_size +``` + +--- + +## S3.3 - Uppercase Lock + +**The following text roles MUST be force-uppercased when content is English/Latin:** + +- Kicker (category label / lead-in text) +- Footer (closing date / document number) +- Background watermark text +- Any Layer 1 decorative text + +```python +kicker_text = kicker_text.upper() if is_latin(kicker_text) else kicker_text +footer_text = footer_text.upper() if is_latin(footer_text) else footer_text +watermark_text = watermark_text.upper() # Always uppercase +``` + +**Exception:** CJK text is exempt. Mixed CJK+Latin strings: uppercase only the Latin portions. + +--- + +## S3.4 - Hard Width Boundary Enforcement 🆕 + +**Every drawer/zone has a maximum width. Text wrapping MUST respect this width exactly.** + +```python +# WRONG - text bleeds past boundary +draw_text(x=0.12*W, text=long_title, width=None) # width unconstrained! + +# RIGHT - hard clamp +max_width = 0.6 * W # or zone-specific value +wrapped_lines = word_wrap(text, font, size, max_width) +for i, line in enumerate(wrapped_lines): + draw_text(x=x_anchor, y=y_anchor + i * line_height, text=line) +``` + +**Rule:** It is acceptable for text to add extra lines (grow vertically). It is NEVER acceptable for text to exceed its horizontal boundary (grow horizontally). Vertical overflow triggers S3.2; horizontal overflow is a critical bug. + +--- + +## S3.5 - Mandatory Summary Auto-Generation 🆕 + +**If the user provides only a title and no description/summary, the system MUST generate placeholder text.** + +```python +if not summary_text or summary_text.strip() == "": + if lang == "zh": + summary_text = f"本报告由{org_name or '系统'}自动生成,包含了综合数据分析与洞察结论。" + else: + summary_text = f"This report presents comprehensive analysis and key insights prepared by {org_name or 'the organization'}." +``` + +**Why:** A title-only cover looks barren. The Summary drawer physically occupies 2-4 lines, filling mid-page void and making the cover look intentionally designed rather than half-finished. + +--- + +## S3.6 - Background Watermark Full-Display Enforcement 🆕 + +**All watermark text in the background layer (Layer 1) must be 100% within the visible page area. Cropping, truncation, or extending beyond page boundaries is strictly forbidden.** + +**Applicable scope:** +- Template 02 giant year watermark +- Template 03 right-side vertical watermark +- Template 07 sidebar watermark +- `cover-backgrounds.md` Recipe 2.1 giant sidebar pillar +- `cover-backgrounds.md` Recipe 2.2 bottom full-size text +- Any other decorative text in the background layer + +**Adaptive algorithm (mandatory):** + +```python +def safe_watermark_size(text, font, max_size, available_space): + """ + Ensure watermark text is fully displayed within available space. + available_space: available width/height (depending on text direction) + """ + rendered = measure_text(text, font, max_size) + if rendered > available_space: + return max_size * (available_space / rendered) + return max_size +``` + +**Rules:** +1. Horizontal text: rendered width must not exceed `W * 0.90` (5% safety margin on each side) +2. Vertical/rotated text: rendered height must not exceed `H * 0.85` (7.5% safety margin top and bottom) +3. If exceeded, scale down font size proportionally - never truncate +4. **Anchor coordinates must never exceed page boundaries** (no negative X/Y values or values exceeding W/H) + +**This is a visual quality red line: a truncated "REPO" is worse than no watermark at all. A complete "REPORT" is the design.** + +--- + +## S3.7 - Line-Length Alignment (Line Must Match Text Span) + +**Problem:** Decorative lines (vertical accent lines, horizontal dividers, underlines) are arbitrary lengths that don't relate to the text they accompany, creating visual disconnect. + +**Iron Rule:** Lines must be sized relative to the text they serve: + +### Vertical Lines (e.g., Template 01 thick line, Template 08 accent line) + +**Vertical line height = text block height** (from first text element to last text element in the same column). + +``` +# WRONG - arbitrary fixed height +vline_top = 0.1 * H +vline_bottom = 0.9 * H # ← line runs full page regardless of content + +# RIGHT - measure text block, then draw line +text_top = first_element_y # e.g., label at 0.12*H +text_bottom = last_element_y + last_element_height # e.g., footer at 0.88*H +vline_top = text_top - U # 1U padding above first element +vline_bottom = text_bottom + U # 1U padding below last element +``` + +### Horizontal Lines (e.g., Template 08 hline, dividers) + +**Horizontal line width ≥ text width of the widest text element in its zone.** Lines may be slightly longer (up to 120% of text width) but NEVER shorter. + +```python +# WRONG - fixed short line +hline_width = 200 # ← might be shorter than the title + +# RIGHT - measure, then draw +max_text_width = max(measure(title), measure(subtitle), measure(authors)) +hline_width = max(max_text_width, max_text_width * 1.1) # at least as wide, up to 110% +# Clamp to available space +hline_width = min(hline_width, available_width) +``` + +### HTML/CSS Implementation + +For HTML/Playwright covers, use relative sizing: +```css +/* Vertical line spans the content block */ +.vline { + position: absolute; + top: var(--content-top); /* align with first text element */ + bottom: var(--content-bottom); /* align with last text element */ +} + +/* Horizontal divider: min-width matches text container */ +.hline { + width: max(100%, 200px); /* at least as wide as parent text container */ +} +``` + +**Checklist:** +- [ ] Every vertical line's height matches its adjacent text block span (± 1U padding) +- [ ] Every horizontal line's width ≥ widest text element in its zone +- [ ] No decorative line is shorter than the text it accompanies + +--- + +## S3.8 - Vertical Balance (Anti-Top-Heavy Layout) + +**Problem:** Content clusters at the top of the page, leaving the bottom 40%+ as dead whitespace. This happens when anchor points are set too high and don't adapt to content volume. + +**Root cause:** Fixed anchor grid with `ANCHOR_TITLE_Y = 0.20*H` pushes everything upward regardless of how much content there is. + +### Solution: Adaptive Vertical Centering + +**When total content height < 50% of page height, switch to centered distribution mode:** + +```python +# Step 1: Calculate total content height +content_elements = [title, subtitle, summary, meta, footer] +total_content_h = sum(elem.height for elem in content_elements) + total_gaps + +# Step 2: Check fill ratio +fill_ratio = total_content_h / (H * 0.80) # usable height (excluding margins) + +if fill_ratio < 0.50: + # LOW CONTENT MODE - vertically center the entire block + start_y = (H - total_content_h) / 2 + # Distribute elements from start_y downward with standard gaps +else: + # NORMAL MODE - use anchor grid + # But shift anchors down: title at H*0.30-0.35 (not 0.20-0.25) + pass +``` + +### Anchor Adjustment Rules + +| Content Volume | Title Anchor | Summary Anchor | Meta Anchor | +|---------------|-------------|----------------|-------------| +| **Sparse** (fill < 50%) | Centered mode | Centered mode | Centered mode | +| **Normal** (fill 50-80%) | `H * 0.30` | `H * 0.48` | `H * 0.70` | +| **Dense** (fill > 80%) | `H * 0.20` | `H * 0.40` | `H * 0.65` | + +### CJK Title Size Compensation + +CJK characters at the same pt size as Latin characters appear visually smaller due to denser stroke structure. Compensate: + +``` +CJK Hero Title: 50-80pt (Latin: 45-65pt) - increase by 15-20% +CJK Kicker: 11-12pt (Latin: 9pt) +CJK Summary: 17-20pt (Latin: 16-18pt) +``` + +**Detection:** If title string contains CJK characters (`\u4e00-\u9fff`), apply CJK size multiplier. + +### HTML/CSS Implementation + +```css +/* Vertical centering mode for sparse content */ +.cover.sparse-content .center-block { + justify-content: center; /* flexbox vertical center */ +} + +/* CJK title size bump */ +.title:lang(zh), .title:lang(ja), .title:lang(ko) { + font-size: clamp(50pt, 8vw, 80pt); /* larger than Latin range */ +} +``` + +**Checklist:** +- [ ] No cover has >40% dead whitespace at the bottom +- [ ] Content is visually centered on the page (optical center, not mathematical) +- [ ] CJK titles are 15-20% larger than equivalent Latin titles +- [ ] Sparse-content covers use centered distribution, not fixed anchors + +--- + +## S3.9 - Percentage Positioning Requires Known-Size Container + +**Root cause of bad case:** A wrapper div (e.g. `.content-left`) with `position: absolute` but **no explicit height** contains children positioned with `top: XX%`. CSS percentage `top` resolves against the containing block's **height** — if that height is zero or undefined (because all children are also absolutely positioned, contributing no content height), the percentage values collapse and elements stack on top of each other. + +**Iron rule:** When using `top: XX%` (or `bottom: XX%`) to position child elements, the containing block MUST have a **deterministic height** — one of: + +| Method | Example | When to use | +|--------|---------|-------------| +| Explicit `height` | `height: 100%` or `height: var(--h)` | Wrapper spans full page | +| `top` + `bottom` pair | `top: 0; bottom: 0;` | Wrapper stretches between two edges | +| `inset: 0` | `inset: 0;` | Shorthand for full-page wrapper | + +**Preferred pattern — flat structure with px values (safest):** +```css +/* ✅ CORRECT: children positioned directly in .cover with px values */ +.cover { position: relative; width: 794px; height: 1123px; } +.kicker { position: absolute; top: 225px; left: 95px; } +.title { position: absolute; top: 292px; left: 95px; } +.summary { position: absolute; top: 539px; left: 95px; } +.meta { position: absolute; top: 786px; left: 95px; } +``` + +**Acceptable — wrapper with deterministic height:** +```css +/* ✅ OK: wrapper has inset:0, so height = parent height = 1123px */ +.content-left { position: absolute; inset: 0; width: 55%; } +.title { position: absolute; top: 26%; } /* 26% of 1123px = 292px ✓ */ +``` + +**Forbidden — wrapper with no height:** +```css +/* ❌ BANNED: .content-left has no height/bottom, percentage top is undefined */ +.content-left { position: absolute; left: 12%; top: 0; width: 55%; } +.title { position: absolute; top: 26%; } /* 26% of WHAT? → collapse → overlap */ +.summary { position: absolute; top: 48%; } /* stacks on top of title */ +``` + +**Quick self-check before writing cover CSS:** +1. For every element with `top: XX%` — trace upward: does the containing block have a known height? +2. If unsure → use `px` values instead (calculate from `var(--h)` manually: `26% × 1123 = 292px`) +3. If using a grouping wrapper → give it `inset: 0` or explicit `height: 100%` + +--- + +# PART 4: COVER COLOR RULES + +> Cover colors must be consistent with the body color system - they cannot exist independently. + +``` +Cover primary = Body theme color +Cover secondary = Primary lightness variant (±20% lightness) +Cover background = Pure white / very light gray / primary at 5-8% opacity +``` + +### Absolutely Forbidden + +- ❌ Dark large-area solid backgrounds (dark blue, dark green, black filling the page) +- ❌ Gradient backgrounds (any `linear-gradient` / `radial-gradient` as large-area fill) +- ❌ High-saturation color schemes +- ❌ Rainbow / multi-color gradients +- ❌ Dense textures or patterns +- ❌ Piling on decorative elements - restraint > clutter +- ❌ More than 2 typefaces on a cover +- ❌ Centered text + gradient/solid background (PowerPoint aesthetic) + +### Safe Cover Color Schemes (Reference Only) + +> ⚠️ These are **examples for reference**. In normal workflow, run `palette.cascade --title "" --mode minimal --format css` to generate the actual cover colors. Do NOT copy these hex values directly. + +| Name | Primary | Secondary | Background | Use Case | +|------|---------|-----------|------------|----------| +| Ink Stone | `#1a1a2e` | `#4a4a5e` | `#fafafa` | Business, formal | +| Indigo | `#1e3a5f` | `#2d5f8a` | `#f5f8fb` | Technology, reports | +| Warm Chestnut | `#5c3d2e` | `#8a6b5a` | `#faf6f3` | Culture, branding | +| Moss Green | `#2d4a3e` | `#4a7a6a` | `#f5f8f6` | Nature, health | +| Deep Crimson | `#6b2d3e` | `#9a4a5e` | `#faf5f6` | Traditional, elegant | + +--- + +# PART 4.5: ACADEMIC COVER TEMPLATES (Templates 08-10) + +> **Academic covers are exempt from PART 4 color rules.** Academic papers, theses, and research reports traditionally use dark backgrounds with light text - this is the established scholarly visual language. Templates 08-10 follow LaTeX title page conventions translated to HTML/CSS. + +**All 3 templates share these rules:** +- Page size: `width: 794px; height: 1123px` (A4 at 96dpi) +- Full-bleed dark background (edge-to-edge, no margins) +- Serif font for titles (Playfair Display / Noto Serif SC), sans-serif for metadata +- Generated as HTML → Playwright `page.pdf()` → pypdf merge (same pipeline as Report covers) +- `<link>` tag for Google Fonts (NOT `@import`) + +**Content slots (all templates):** +| Slot | Required | Example | +|------|----------|---------| +| `label` | Optional | `RESEARCH PAPER`, `博士论文` | +| `title` | **Required** | Paper title (auto-wrap, max 3 lines) | +| `subtitle` | Optional | Subtitle or abstract excerpt | +| `authors` | **Required** | Author name(s) | +| `institution` | Optional | University / lab / affiliation | +| `keywords` | Optional | Keyword list | +| `footer_left` | Optional | Journal name, DOI | +| `footer_right` | Optional | Date, version | + +--- + +## Template 08: Academic Vertical Anchor - Dark bg + Left vertical line + Left-aligned + +**Design intent:** Emulates the classic arXiv/preprint cover. A bold vertical accent line anchors the left edge, all text left-aligned with generous vertical rhythm. Serious, no-frills. + +``` +┌─────────────────────────────┐ +│ ┃ │ +│ ┃ LABEL (9pt, accent) │ ← y = H - 3.5cm +│ ┃ │ +│ ┃ Title (32pt Bold) │ ← y = H - 6cm, line-height 42pt +│ ┃ Title line 2 │ +│ ┃ │ +│ ┃ Subtitle (12pt) │ ← y = H - 14cm +│ ┃ │ +│ ┃ Authors (12pt, white) │ ← y = H - 18cm +│ ┃ Institution (10pt) │ +│ ┃ │ +│ ┃─────────────────── │ ← accent line y=3.5cm +│ ┃ Footer L Footer R │ +└─────────────────────────────┘ +┃ = vertical accent line at x=1.5cm, 2.5pt width +``` + +**Best for:** Research papers, technical reports, arXiv preprints + +**HTML structure:** +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700;900&family=Noto+Serif+SC:wght@400;700;900&family=Inter:wght@300;400;500&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet"> + <style> + @page { size: 794px 1123px; margin: 0; } + :root { + --c-bg: #162032; + --c-accent: #8B7E5A; + --c-text: #FFFFFF; + --c-muted: #8898A8; + --c-footer: #607080; + } + html, body { margin: 0; padding: 0; width: 794px; height: 1123px; background: var(--c-bg); color: var(--c-text); font-family: 'Inter', 'Noto Sans SC', sans-serif; } + @media screen { + html { height: auto; display: flex; justify-content: center; min-height: 100vh; background: var(--c-bg); } + body { transform-origin: top center; scale: min(1, calc(100vw / 794), calc(100vh / 1123)); margin: 0 auto; box-shadow: 0 0 60px rgba(0,0,0,0.3); } + } + .cover { width: 794px; height: 1123px; position: relative; box-sizing: border-box; } + .vline { position: absolute; left: 57px; top: 76px; bottom: 76px; width: 2.5px; background: var(--c-accent); } + .hline { position: absolute; left: 83px; right: 76px; bottom: 132px; height: 0.5px; background: var(--c-accent); } + .content { position: absolute; left: 83px; right: 76px; top: 0; bottom: 0; } + .label { position: absolute; top: 132px; font-size: 9pt; color: var(--c-accent); letter-spacing: 3px; text-transform: uppercase; font-family: 'Inter', 'Noto Sans SC', sans-serif; } + .title { position: absolute; top: 228px; font-size: 32pt; font-weight: 700; line-height: 1.3; font-family: 'Playfair Display', 'Noto Serif SC', serif; color: var(--c-text); max-width: 580px; } + .subtitle { position: absolute; top: 530px; font-size: 12pt; line-height: 1.5; color: var(--c-muted); max-width: 500px; } + .authors { position: absolute; top: 680px; font-size: 12pt; color: var(--c-text); } + .institution { position: absolute; top: 740px; font-size: 10pt; color: var(--c-muted); line-height: 1.4; } + .footer { position: absolute; bottom: 76px; left: 0; right: 0; display: flex; justify-content: space-between; font-size: 9pt; color: var(--c-footer); } + </style> +</head> +<body> + <div class="cover"> + <div class="vline"></div> + <div class="hline"></div> + <div class="content"> + <div class="label"><!-- LABEL --></div> + <div class="title"><!-- TITLE --></div> + <div class="subtitle"><!-- SUBTITLE --></div> + <div class="authors"><!-- AUTHORS --></div> + <div class="institution"><!-- INSTITUTION --></div> + <div class="footer"> + <span><!-- FOOTER_LEFT --></span> + <span><!-- FOOTER_RIGHT --></span> + </div> + </div> + </div> +</body> +</html> +``` + +**Default palette (override via `palette.cascade --intent <intent> --mode dark --format css`):** +| Name | `--c-bg` | `--c-accent` | `--c-muted` | Use case | +|------|----------|------------|----------|----------| +| Deep Sea | `#162032` | `#8B7E5A` | `#8898A8` | General academic | +| Indigo | `#1e3a5f` | `#2d5f8a` | `#7A90A5` | Technical | +| Ink Stone | `#1a1a2e` | `#4a4a5e` | `#8080A0` | Formal occasions | + +> ⚠️ These are **fallback defaults** when the palette system is unavailable. In normal workflow, run `palette.cascade` to generate mathematically harmonious colors and inject them into the `:root` variables. + +--- + +## Template 09: Academic Symmetric - Dark bg + Top/bottom lines + Centered + +**Design intent:** Emulates the classic IEEE/ACM Transactions title page. Perfect bilateral symmetry, thick horizontal rules frame the content zone. Formal and authoritative. + +``` +┌─────────────────────────────┐ +│ │ +│ ══════════════════════ │ ← Top rule y=H-3cm, 2pt +│ │ +│ LABEL (centered) │ +│ │ +│ Title (28-30pt Bold) │ +│ │ +│ Subtitle │ +│ │ +│ ─── │ ← Thin divider 3cm, centered +│ │ +│ Authors │ +│ Institution │ +│ │ +│ ══════════════════════ │ ← Bottom rule y=3cm, 2pt +│ Journal · Date │ +└─────────────────────────────┘ +``` + +**Best for:** Top journal submissions, IEEE/ACM papers, theses + +**HTML structure:** +```html +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700;900&family=Noto+Serif+SC:wght@400;700;900&family=Inter:wght@300;400;500&family=Noto+Sans+SC:wght@300;400;500&display=swap" rel="stylesheet"> + <style> + @page { size: 794px 1123px; margin: 0; } + :root { + --c-bg: #162032; + --c-accent: #4A90C4; + --c-text: #FFFFFF; + --c-muted: #90A8C0; + } + html, body { margin: 0; padding: 0; width: 794px; height: 1123px; background: var(--c-bg); color: var(--c-text); font-family: 'Inter', 'Noto Sans SC', sans-serif; } + @media screen { + html { height: auto; display: flex; justify-content: center; min-height: 100vh; background: var(--c-bg); } + body { transform-origin: top center; scale: min(1, calc(100vw / 794), calc(100vh / 1123)); margin: 0 auto; box-shadow: 0 0 60px rgba(0,0,0,0.3); } + } + .cover { width: 794px; height: 1123px; position: relative; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; } + .rule-top, .rule-bottom { position: absolute; left: 114px; right: 114px; height: 2px; background: var(--c-accent); } + .rule-top { top: 114px; } + .rule-bottom { bottom: 114px; } + .center-block { position: absolute; top: 0; bottom: 0; left: 114px; right: 114px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } + .label { font-size: 9pt; color: var(--c-accent); letter-spacing: 3px; text-transform: uppercase; margin-bottom: 40px; font-family: 'Inter', 'Noto Sans SC', sans-serif; } + .title { font-size: 30pt; font-weight: 700; line-height: 1.3; font-family: 'Playfair Display', 'Noto Serif SC', serif; margin-bottom: 24px; max-width: 500px; } + .subtitle { font-size: 14pt; color: var(--c-muted); margin-bottom: 40px; max-width: 450px; line-height: 1.5; } + .divider { width: 114px; height: 0.5px; background: var(--c-accent); margin-bottom: 40px; } + .authors { font-size: 12pt; margin-bottom: 12px; } + .institution { font-size: 10pt; color: var(--c-muted); line-height: 1.4; } + .footer { position: absolute; bottom: 57px; left: 114px; right: 114px; text-align: center; font-size: 9pt; color: var(--c-muted); } + </style> +</head> +<body> + <div class="cover"> + <div class="rule-top"></div> + <div class="rule-bottom"></div> + <div class="center-block"> + <div class="label"><!-- LABEL --></div> + <div class="title"><!-- TITLE --></div> + <div class="subtitle"><!-- SUBTITLE --></div> + <div class="divider"></div> + <div class="authors"><!-- AUTHORS --></div> + <div class="institution"><!-- INSTITUTION --></div> + </div> + <div class="footer"><!-- FOOTER --></div> + </div> +</body> +</html> +``` + +**Default palette (override via `palette.cascade --intent <intent> --mode dark --format css`):** +| Name | `--c-bg` | `--c-accent` | `--c-muted` | Use case | +|------|----------|------------|----------|----------| +| Midnight Blue | `#162032` | `#4A90C4` | `#90A8C0` | Math/theoretical | +| Ink Blue | `#0D1B2A` | `#3D5A80` | `#8898A8` | Formal reports | +| Deep Navy | `#0a1628` | `#5B8DB8` | `#7A9AB5` | Engineering | + +> ⚠️ These are **fallback defaults**. In normal workflow, run `palette.cascade` to generate colors and inject into `:root`. + +--- + +## Template 10: Academic Journal - Dark bg + Top/bottom lines + Centered + Keywords + +**Design intent:** Extended version of Template 09, with dedicated keyword block. Matches the layout of top-tier Chinese journal submissions and thesis covers. + +``` +┌─────────────────────────────┐ +│ │ +│ ══════════════════════ │ ← Top rule +│ │ +│ LABEL (centered) │ +│ │ +│ Title (34pt) │ +│ │ +│ Subtitle │ +│ │ +│ ─── │ ← Thin divider +│ │ +│ Keywords │ +│ │ +│ ══════════════════════ │ ← Bottom rule +│ Footer │ +└─────────────────────────────┘ +``` + +**Best for:** Chinese journal submissions, theses with keywords, formal academic reports + +**HTML structure:** +```html +<!DOCTYPE html> +<html lang="zh"> +<head> + <meta charset="UTF-8"> + <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet"> + <style> + @page { size: 794px 1123px; margin: 0; } + :root { + --c-bg: #162032; + --c-accent: #4A90C4; + --c-text: #FFFFFF; + --c-muted: #90A8C0; + } + html, body { margin: 0; padding: 0; width: 794px; height: 1123px; background: var(--c-bg); color: var(--c-text); font-family: 'Noto Sans SC', 'Inter', sans-serif; } + .cover { width: 794px; height: 1123px; position: relative; display: flex; flex-direction: column; align-items: center; box-sizing: border-box; } + .rule-top, .rule-bottom { position: absolute; left: 114px; right: 114px; height: 2px; background: var(--c-accent); } + .rule-top { top: 114px; } + .rule-bottom { bottom: 114px; } + .center-block { position: absolute; top: 0; bottom: 0; left: 114px; right: 114px; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; } + .label { font-size: 9pt; color: var(--c-accent); letter-spacing: 3px; text-transform: uppercase; margin-bottom: 40px; } + .title { font-size: 34pt; font-weight: 700; line-height: 1.3; font-family: 'Noto Serif SC', serif; margin-bottom: 20px; max-width: 500px; } + .subtitle { font-size: 14pt; color: var(--c-muted); margin-bottom: 40px; max-width: 450px; line-height: 1.5; } + .divider { width: 152px; height: 0.5px; background: var(--c-accent); margin-bottom: 40px; } + .keywords { font-size: 11pt; color: var(--c-muted); line-height: 1.8; max-width: 400px; } + .footer { position: absolute; bottom: 57px; left: 114px; right: 114px; text-align: center; font-size: 9pt; color: var(--c-muted); } + </style> +</head> +<body> + <div class="cover"> + <div class="rule-top"></div> + <div class="rule-bottom"></div> + <div class="center-block"> + <div class="label"><!-- LABEL --></div> + <div class="title"><!-- TITLE --></div> + <div class="subtitle"><!-- SUBTITLE --></div> + <div class="divider"></div> + <div class="keywords"> + <!-- KEYWORD 1 --><br> + <!-- KEYWORD 2 --><br> + <!-- KEYWORD 3 --> + </div> + </div> + <div class="footer"><!-- FOOTER --></div> + </div> +</body> +</html> +``` + +**Recommended palettes:** Same as Template 09. + +--- + +## Template 11: Institutional - White bg + Black border frame + Structured field slots + +**Design intent:** The universal institutional cover. White background with a thick black border frame, all content centered, structured field slots with underline placeholders. Matches the style required by most universities worldwide for thesis proposals, dissertations, and formal institutional documents. Also suitable for government reports and official submissions. Zero decorative elements - the formality IS the design. + +**⚠️ This template is exempt from PART 4 Academic Cover Color Rules (dark backgrounds).** It uses a white/light background by design, aligning with institutional formatting requirements. + +``` +┌─────────────────────────────────┐ +│ ┌─────────────────────────────┐ │ +│ │ │ │ +│ │ INSTITUTION NAME │ │ ← y = 12%, serif 28-34pt Bold +│ │ (校名/机构名) │ │ +│ │ │ │ +│ │ ━━━━━━━━━━━━━━━━━━━━ │ │ ← thick divider (2pt) +│ │ │ │ +│ │ DOCUMENT TYPE │ │ ← y = 30%, 20-24pt +│ │ (开题报告/毕业论文/申报书) │ │ +│ │ │ │ +│ │ TITLE │ │ ← y = 40%, serif 26-30pt Bold +│ │ (论文题目) │ │ max 3 lines, centered +│ │ │ │ +│ │ Field: _______________ │ │ ← y = 58-78%, structured fields +│ │ Field: _______________ │ │ left-label + underline value +│ │ Field: _______________ │ │ e.g. 姓名、学号、导师、院系、日期 +│ │ Field: _______________ │ │ +│ │ Field: _______________ │ │ +│ │ │ │ +│ │ DATE │ │ ← y = 88%, centered, 14pt +│ │ │ │ +│ └─────────────────────────────┘ │ +└─────────────────────────────────┘ +││ = 2.5pt black border, inset 5% from page edge +``` + +**Best for:** Thesis proposals (开题报告), dissertations, institutional reports, government documents, any formal submission with structured metadata fields + +**Content slots:** +| Slot | Required | Example | +|------|----------|---------| +| `institution` | **Required** | "北京大学", "Massachusetts Institute of Technology" | +| `doc_type` | Optional | "开题报告", "Thesis Proposal", "毕业设计" | +| `title` | **Required** | Paper/document title (auto-wrap, max 3 lines) | +| `fields` | Optional | Array of `{label, value}` pairs. Common: 姓名/Name, 学号/ID, 导师/Advisor, 院系/Department, 专业/Major | +| `date` | Optional | "2026年4月", "April 2026" | + +**HTML structure:** +```html +<!DOCTYPE html> +<html lang="zh"> +<head> + <meta charset="UTF-8"> + <link href="https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;600;700;900&family=Noto+Sans+SC:wght@300;400;500;700&family=Playfair+Display:wght@400;700;900&family=Inter:wght@300;400;500&display=swap" rel="stylesheet"> + <style> + @page { size: 794px 1123px; margin: 0; } + :root { + --c-bg: #ffffff; + --c-text: #1a1a1a; + --c-accent: #1a1a1a; + --c-muted: #4a4a4a; + --c-line: #333333; + } + html, body { margin: 0; padding: 0; width: 794px; height: 1123px; background: var(--c-bg); color: var(--c-text); font-family: 'Noto Sans SC', 'Inter', sans-serif; } + @media screen { + html { height: auto; display: flex; justify-content: center; min-height: 100vh; background: #e8e8e8; } + body { transform-origin: top center; scale: min(1, calc(100vw / 794), calc(100vh / 1123)); margin: 0 auto; box-shadow: 0 0 60px rgba(0,0,0,0.15); } + } + .cover { + width: 794px; height: 1123px; position: relative; box-sizing: border-box; + } + /* Black border frame - inset 5% from page edge */ + .border-frame { + position: absolute; + top: 56px; left: 40px; right: 40px; bottom: 56px; + border: 2.5px solid var(--c-accent); + pointer-events: none; + } + /* Content area inside frame */ + .content { + position: absolute; + top: 56px; left: 40px; right: 40px; bottom: 56px; + display: flex; flex-direction: column; align-items: center; + padding: 60px 50px; + box-sizing: border-box; + } + .institution { + font-size: 30pt; font-weight: 700; letter-spacing: 6px; + font-family: 'Noto Serif SC', 'Playfair Display', serif; + text-align: center; margin-bottom: 30px; + max-width: 580px; + } + .thick-divider { + width: 70%; height: 2px; background: var(--c-accent); + margin-bottom: 40px; + } + .doc-type { + font-size: 22pt; font-weight: 400; letter-spacing: 4px; + text-align: center; margin-bottom: 50px; + color: var(--c-text); + } + .title { + font-size: 26pt; font-weight: 700; line-height: 1.4; + font-family: 'Noto Serif SC', 'Playfair Display', serif; + text-align: center; margin-bottom: 60px; + max-width: 520px; + } + .fields-block { + width: 400px; margin-bottom: auto; + } + .field-row { + display: flex; align-items: baseline; + margin-bottom: 28px; font-size: 14pt; + } + .field-label { + white-space: nowrap; margin-right: 12px; + color: var(--c-text); font-weight: 400; + letter-spacing: 2px; + } + .field-value { + flex: 1; text-align: center; + border-bottom: 1px solid var(--c-line); + padding-bottom: 4px; min-height: 24px; + font-family: 'Noto Sans SC', 'Inter', sans-serif; + } + .date-block { + font-size: 14pt; color: var(--c-muted); + text-align: center; letter-spacing: 2px; + margin-top: auto; padding-top: 30px; + } + </style> +</head> +<body> + <div class="cover"> + <div class="border-frame"></div> + <div class="content"> + <div class="institution"><!-- INSTITUTION --></div> + <div class="thick-divider"></div> + <div class="doc-type"><!-- DOC_TYPE --></div> + <div class="title"><!-- TITLE --></div> + <div class="fields-block"> + <div class="field-row"> + <span class="field-label"><!-- LABEL_1 --></span> + <span class="field-value"><!-- VALUE_1 --></span> + </div> + <div class="field-row"> + <span class="field-label"><!-- LABEL_2 --></span> + <span class="field-value"><!-- VALUE_2 --></span> + </div> + <div class="field-row"> + <span class="field-label"><!-- LABEL_3 --></span> + <span class="field-value"><!-- VALUE_3 --></span> + </div> + <div class="field-row"> + <span class="field-label"><!-- LABEL_4 --></span> + <span class="field-value"><!-- VALUE_4 --></span> + </div> + <div class="field-row"> + <span class="field-label"><!-- LABEL_5 --></span> + <span class="field-value"><!-- VALUE_5 --></span> + </div> + </div> + <div class="date-block"><!-- DATE --></div> + </div> + </div> +</body> +</html> +``` + +**Layout rules:** +1. **Border frame**: 2.5pt solid black, inset ~5% from all page edges (40px left/right, 56px top/bottom on A4 at 96dpi). This is the defining visual element. +2. **Institution name**: Centered, serif, 28-34pt Bold, letter-spacing 4-6px. For CJK names, use wider letter-spacing (6px). For Latin names, use standard (3px). +3. **Thick divider**: 2pt solid line, 70% width, separates institution from content below. +4. **Document type**: 20-24pt, lighter weight than institution name, letter-spacing 3-4px. This slot differentiates document categories (e.g. "开题报告" / "Thesis Proposal" / "毕业论文" / "Graduation Design"). +5. **Title**: Serif, 26-30pt Bold, max 3 lines, centered. Auto-wrap at 520px width. +6. **Structured fields**: Left-aligned label + centered underline value. Label width fixed by longest label in the set. 3-7 field rows supported. Common fields: Name/姓名, Student ID/学号, Advisor/导师, Department/院系, Major/专业. +7. **Date**: Centered at bottom, 14pt, letter-spacing 2px. + +**Field auto-detection:** +When the user provides structured metadata (name, student ID, advisor, etc.), auto-populate the fields block. When no fields are provided, omit the fields-block entirely and let the title expand vertically into the freed space. + +**Variant 11B - Double border:** +For extra formality (government documents, official submissions), replace the single border with a double border (outer 2.5pt + inner 1pt, 6px gap): +```css +.border-frame { + border: 2.5px solid var(--c-accent); + outline: 1px solid var(--c-accent); + outline-offset: 6px; +} +``` + +**This template has NO background decoration layer, NO watermarks, NO gradients.** The black frame + white space IS the design. + +--- + +### Academic Template Selection Guide + +| Scenario | Template | Rationale | +|----------|----------|-----------| +| arXiv preprint, technical report | **08** (Vertical Anchor) | Left-aligned, data-dense feel | +| IEEE/ACM paper, English thesis | **09** (Symmetric) | Classic bilateral symmetry | +| Chinese thesis, journal with keywords | **10** (Journal) | CJK-optimized, keyword block | +| **Thesis proposal, institutional cover, government doc** | **11** (Institutional) | **White bg, black border frame, structured field slots** | +| Light/formal academic (white bg) | **01-07** (standard templates) | Use standard cover system | + + +Covers support a **background decoration layer** rendered behind all foreground content (Layer 1). This layer adds subtle depth through supergraphics, typographic watermarks, and blueprint hairlines. + +See → `typesetting/cover-backgrounds.md` - complete specification with modules, recipes, and constraint matrix. + +**Quick reference:** +- **Recipe A (Minimalist Modern)**: Deep-space arc only - safest, max whitespace +- **Recipe B (Engineering Academic)**: Coordinate cross + vertical spine text - precision, engineering feel +- **Recipe C (Stable & Authoritative)**: Angle slash + bottom bleed text - heavy, authoritative + +**Background layer is OPTIONAL.** Not every cover needs one. Templates 01-07 already define their own Layer 1 backgrounds - use the recipes only when a template's built-in background is insufficient. + +--- + +# PART 6: CHANGELOG + +| Version | Date | Changes | +|---------|------|---------| +| V1.0 | - | Initial 7 layouts (Diagonal Tension, Vertical Axis, etc.) | +| V2.0 | 2026-04-03 | Complete rewrite. Absolute Anchor Grid; Z-index layers; Typography Weight System; 7 new templates with percentage coordinates; Code-level safety. | +| **V2.1** | **2026-04-03** | **Summary Block upgrade.** Added mandatory Summary/Description drawer to all 7 templates (anti-void iron rule). Introduced base spacing unit `U = W * 0.05`. Refined Hero Title range to 45-65pt. Added S3.4 Hard Width Boundary Enforcement + S3.5 Mandatory Summary Auto-Generation. Template 01: added Summary drawer at Y=0.45*H. Template 02: added Summary at Y=0.45*H + refined watermark to 180pt. Template 03: added Summary at Y=0.40*H with W*0.55 width guard. Template 04: Summary included in center-calculated block. Template 05: lower-right group expanded with Summary + 3pt accent line. Template 06: Zone C explicitly designated for substantial summary text. Template 07: sidebar width changed to `0.1*W` (~80pt), content uses relative vertical centering. | +| **V2.2** | **2026-04-07** | **Intent system unification + Template 11.** Part 2 Template Selection Guide migrated from "Document Tone" to Intent × Document Type matrix (aligned with `visual_framework.md` 5-intent system and `creative.md` Intent Mapping Table). Added Template 11 (Institutional) - white bg + black border frame + structured field slots for thesis proposals, dissertations, and institutional documents. Academic Template Selection Guide updated. | +| **V3.0** | **2026-04-07** | **Color unification + Layout balance overhaul.** (1) All template CSS variables renamed to `--c-` prefix (`--c-bg`, `--c-accent`, `--c-text`, `--c-muted`) for palette system alignment. Hardcoded hex values replaced with CSS variables. Palette tables marked as fallback defaults with `palette.cascade` as canonical source. (2) Added S3.7 Line-Length Alignment - vertical/horizontal lines must match text span. (3) Added S3.8 Vertical Balance - adaptive centering for sparse content, CJK title size compensation (50-80pt vs Latin 45-65pt), anchor points shifted down (title H*0.30, summary H*0.50, meta H*0.70). (4) Output cleanliness rules - no version numbers, draft labels, or process artifacts in final PDF. | diff --git a/skills/pdf/typesetting/fill-engine.md b/skills/pdf/typesetting/fill-engine.md new file mode 100755 index 0000000..d57fb14 --- /dev/null +++ b/skills/pdf/typesetting/fill-engine.md @@ -0,0 +1,527 @@ +# Fill Engine — Adaptive Anti-Void Layout Engine V2.0 + +> Solves the **"text too small to read"** and **"large page voids"** problems caused by varying input content length in automated PDF generation. +> Before rendering any page, it must pass through the following **four elastic filtering calculations**. +> +> **Positioning**: This is the mirror counterpart of `overflow.md` (anti-overflow) — overflow handles "too much content", fill-engine handles "too little content". +> +> Related: +> - `overflow.md` — Anti-overflow layout system (degradation strategy for excessive content) +> - `pagination.md` — Pagination & cross-page integrity control +> - `cover.md` — Cover layout engine (covers not affected by Fill Engine; they have their own layout system) + +--- + +## ⚠️ Scope of Application + +- ✅ **Body pages** (body pages of Report / Academic / Creative routes) +- ✅ **All three rendering routes** (ReportLab / LaTeX / Playwright-HTML) +- ❌ **Not applicable to covers** (covers are independently controlled by `cover.md`) +- ❌ **Not applicable to TOC pages** (TOC has a fixed format) + +--- + +## Safety Net 1: Readability Red Line (Font Size Hard Floor) + +**Principle: Never rely on shrinking font size to fit the layout. Font sizes have hard-coded minimums that cannot be breached.** + +### Absolute Font Size Floor + +| Element | Single-column | Double-column | Notes | +|------|---------|---------|------| +| **Body Text** | **≥ 14pt** | **≥ 12pt** | Below this → considered unreadable, must trigger page break | +| **Default base size** | 15pt (CJK) / 14pt (Latin) | 13pt (CJK) / 12pt (Latin) | Starting value, not ceiling | + +### Heading Scale Hard Floor + +| Level | Min Size | Recommended | +|------|---------|---------| +| **H1** (primary heading / page title) | **≥ 32pt** | 36–42pt | +| **H2** (secondary heading) | **≥ 24pt** | 26–30pt | +| **H3** (tertiary heading) | **≥ 18pt** | 20–22pt | + +### Coordination with overflow.md + +`overflow.md` §5's font-size degradation staircase (`fit_text_with_degradation`) **must not breach the above floor**: + +```python +# overflow.md §5's min_size parameter must be >= Fill Engine red line +def fit_text_with_degradation(text, font_name, base_size, max_width, + min_size=14): # ← Single-column floor 14pt, not 7pt + """When overflow needs to shrink font size, it cannot go below the readability red line.""" + for size in range(base_size, min_size - 1, -1): + if stringWidth(text, font_name, size) <= max_width: + return size + return min_size # Hit the floor → trigger page break, stop shrinking +``` + +> **Core idea: If content doesn't fit → page break, not shrinking text to ant-size.** + +--- + +## Safety Net 2: Page Fill Ratio & Paragraph Inflation (Fill Ratio Engine) + +### Virtual Rendering + +Before actually rendering each page, the system **must first calculate** how much height the content would occupy under default font sizes and spacing. + +```python +def calculate_fill_ratio(content_blocks, available_height, default_styles): + """ + Virtual rendering: calculate total height of current page content under default styles. + Returns fill ratio = total content height / available page height. + """ + total_height = 0 + for block in content_blocks: + block_height = measure_block_height(block, default_styles) + total_height += block_height + + fill_ratio = total_height / available_height + return fill_ratio +``` + +### Elastic Inflation Trigger Conditions + +| Fill Ratio | Status | Action | +|--------|------|------| +| **≥ 80%** | ✅ Full | No adjustment, render normally | +| **65%–80%** | ⚠️ Slightly empty | Light inflation (line-height + paragraph spacing only) | +| **40%–65%** | 🔶 Noticeably empty | Full inflation (line-height + spacing + slight font increase + component inflation) | +| **< 40%** | 🔴 Extremely empty | Full inflation + Y-axis golden ratio anchoring (Safety Net 4) | + +### Inflation Parameters (Triggered when fill ratio < 65%) + +#### 2a. Line-Height Inflation + +```python +def inflate_line_height(base_line_height, fill_ratio): + """ + Lower fill ratio means more line-height stretch. + base_line_height: default line-height (e.g. 1.4) + Returns inflated line-height, capped at 2.2. + """ + if fill_ratio >= 0.65: + return base_line_height # No inflation + + # Linear interpolation: as fill_ratio goes from 0.65→0.30, line-height goes from base→2.2 + inflation = (0.65 - fill_ratio) / (0.65 - 0.30) # 0.0 ~ 1.0 + inflation = min(inflation, 1.0) + + target = base_line_height + (2.2 - base_line_height) * inflation + return round(target, 2) +``` + +| Fill Ratio | Base line-height 1.4 → After inflation | +|--------|---------------------| +| 65% | 1.40 (unchanged) | +| 55% | 1.63 | +| 45% | 1.86 | +| 35% | 2.09 | +| ≤30% | 2.20 (cap) | + +#### 2b. Paragraph Spacing Compensation (Margin-Bottom Injection) + +```python +def inject_paragraph_spacing(remaining_height, paragraph_count, heading_count): + """ + Distribute 30%-50% of remaining whitespace evenly between paragraphs. + remaining_height: Available_H - content height after inflation + """ + if remaining_height <= 0: + return 0 + + injection_pool = remaining_height * 0.4 # Take 40% + gap_count = paragraph_count + heading_count - 1 # Number of gaps + + if gap_count <= 0: + return 0 + + per_gap = injection_pool / gap_count + return round(per_gap, 1) +``` + +**Injection positions (by priority):** +1. Between headings and body text (below H1/H2/H3) +2. Between natural paragraphs +3. Between body text and charts/tables + +#### 2c. Font Scaling + +```python +def scale_font_size(base_size, fill_ratio): + """ + When fill ratio < 65%, allow font size to float up by 1-2pt. + Never exceed +2pt, otherwise loses professional feel. + """ + if fill_ratio >= 0.65: + return base_size + if fill_ratio >= 0.50: + return base_size + 1 + return base_size + 2 # Max +2pt +``` + +> **Constraint: Inflated font size ≤ base_size + 2pt. 15pt body can become at most 17pt, no larger.** + +--- + +## Safety Net 3: Component-Level Elastic Fill (Component Inflation) + +**Trigger: Same as Safety Net 2, active when fill ratio < 65%.** + +### 3a. Table Auto-Height Expansion (Table Padding Inflation) + +```python +def inflate_table_padding(base_padding, fill_ratio): + """ + Lower fill ratio means larger table cell padding. + base_padding: default cell padding (e.g. 6pt) + """ + if fill_ratio >= 0.65: + return base_padding + + # Add 10-20pt + extra = int((0.65 - fill_ratio) / 0.25 * 20) + extra = max(10, min(extra, 20)) + return base_padding + extra +``` + +**Effect:** Originally flat compact data table → tall spacious data display board. + +### 3b. Blockquote Exaggeration (Blockquote Scaling) + +When encountering a blockquote and the page has voids: + +```python +def scale_blockquote(base_font_size, fill_ratio): + """ + Blockquote font enlarged, italicized, massive whitespace above and below. + """ + if fill_ratio >= 0.65: + return { + "font_size": base_font_size, + "font_style": "normal", + "margin_top": 12, + "margin_bottom": 12, + "border_left_width": 3, + } + return { + "font_size": int(base_font_size * 1.5), # Scale up 1.5x + "font_style": "italic", + "margin_top": 40, # Large whitespace above + "margin_bottom": 40, # Large whitespace below + "border_left_width": 6, # Thicken blockquote left border + } +``` + +### 3c. List Item Spacing Expansion + +```python +def inflate_list_spacing(base_spacing, fill_ratio): + """ + List item spacing expanded to 1.5x normal paragraph spacing. + """ + if fill_ratio >= 0.65: + return base_spacing + return int(base_spacing * 1.5) +``` + +--- + +## Safety Net 4: Y-Axis Golden Ratio Anchoring (Ultimate Measure for Extreme Voids) + +**Trigger: After Safety Net 2 + 3 inflation, fill ratio still < 40%.** + +**Core principle: Absolutely forbidden to pin content to the very top, leaving the bottom half as dead whitespace.** + +### Execution Logic + +```python +def anchor_content_vertically(content_bbox_height, available_height, fill_ratio): + """ + Pack all current page content as a BBox, re-align vertically within available height. + + Returns content_top_y: Y coordinate offset for content top. + """ + if fill_ratio >= 0.40: + return 0 # No anchoring needed, normal flow from page top + + remaining = available_height - content_bbox_height + + # Option A: Golden ratio offset-up (recommended) + golden_offset = remaining * 0.382 # Top 38.2%, bottom 61.8% + + # Option B: Absolute vertical center (alternative) + # center_offset = remaining / 2 + + return golden_offset +``` + +### Option Selection + +| Option | Formula | Visual Effect | Applicable Scenario | +|------|------|---------|---------| +| **A. Golden ratio offset-up** (default) | `offset = remaining * 0.382` | Slightly less whitespace above, more below, visually stable | Most scenarios | +| **B. Absolute center** | `offset = remaining / 2` | Perfectly symmetrical | Minimal pages with single element | + +### Effect Illustration + +``` +┌─────────────────────────┐ ┌─────────────────────────┐ +│ ← Content starts here │ │ │ +│ Section Title │ │ ← 38.2% elegant space │ +│ Body text... │ │ │ +│ │ │ Section Title │ +│ │ │ Body text... │ +│ │ │ │ +│ │ │ │ +│ ← Huge dead whitespace! │ │ ← 61.8% bottom space │ +│ │ │ │ +│ │ │ │ +└─────────────────────────┘ └─────────────────────────┘ + ❌ No anchoring ✅ Golden ratio anchoring +``` + +--- + +## Three-Route Implementation Guide + +### ReportLab Route (Report Pipeline) + +```python +from reportlab.platypus import SimpleDocTemplate, Spacer, Paragraph +from reportlab.lib.units import pt + +def build_page_with_fill_engine(story_blocks, page_width, page_height, margins): + """ + Fill Engine main entry — ReportLab route. + Call before doc.build(). + """ + available_h = page_height - margins['top'] - margins['bottom'] + available_w = page_width - margins['left'] - margins['right'] + + # --- Safety Net 1: Check font size floor --- + enforce_font_floor(story_blocks, min_body=14, min_h1=32, min_h2=24, min_h3=18) + + # --- Safety Net 2: Virtual render + inflation --- + fill_ratio = calculate_fill_ratio(story_blocks, available_h, default_styles) + + if fill_ratio < 0.65: + # 2a. Line-height inflation + new_line_height = inflate_line_height(1.4, fill_ratio) + apply_line_height(story_blocks, new_line_height) + + # 2b. Paragraph spacing injection + remaining = available_h - measure_total_height(story_blocks) + extra_gap = inject_paragraph_spacing(remaining, count_paragraphs(story_blocks), + count_headings(story_blocks)) + inject_spacers(story_blocks, extra_gap) + + # 2c. Font scaling + font_bump = scale_font_size(15, fill_ratio) - 15 + if font_bump > 0: + bump_font_sizes(story_blocks, font_bump) + + # --- Safety Net 3: Component inflation --- + if fill_ratio < 0.65: + inflate_tables(story_blocks, fill_ratio) + inflate_blockquotes(story_blocks, fill_ratio) + inflate_lists(story_blocks, fill_ratio) + + # --- Safety Net 4: Y-axis anchoring --- + recalc_ratio = calculate_fill_ratio(story_blocks, available_h, inflated_styles) + if recalc_ratio < 0.40: + content_height = measure_total_height(story_blocks) + top_offset = anchor_content_vertically(content_height, available_h, recalc_ratio) + story_blocks.insert(0, Spacer(1, top_offset)) + + return story_blocks +``` + +### LaTeX Route (Academic Pipeline) + +```latex +% Safety Net 1: Font size floor — define in preamble +\newcommand{\bodysize}{\fontsize{14pt}{20pt}\selectfont} % 14pt floor +\renewcommand{\Large}{\fontsize{32pt}{38pt}\selectfont} % H1 ≥ 32pt +\renewcommand{\large}{\fontsize{24pt}{30pt}\selectfont} % H2 ≥ 24pt + +% Safety Net 4: Vertical centering (for extreme voids) +\newcommand{\goldenpage}[1]{% + \null\vfill % Top elastic space (less) + #1 % Content + \vfill\vfill % Bottom elastic space (more, ~2:1 ratio) +} + +% Usage (when content is minimal): +% \goldenpage{ +% \section{Summary} +% Short content here... +% } +``` + +### Playwright/HTML Route (Creative Pipeline) + +```css +/* Safety Net 1: Font size red line */ +:root { + --body-min-font: 14px; + --h1-min-font: 32px; + --h2-min-font: 24px; + --h3-min-font: 18px; +} + +body { + font-size: max(var(--body-font, 15px), var(--body-min-font)); +} + +h1 { font-size: max(var(--h1-font, 36px), var(--h1-min-font)); } +h2 { font-size: max(var(--h2-font, 28px), var(--h2-min-font)); } +h3 { font-size: max(var(--h3-font, 22px), var(--h3-min-font)); } + +/* Safety Net 2-3: Dynamically injected after JS virtual render */ +/* Before Playwright screenshot, run Fill Engine JS via page.evaluate() */ +``` + +```javascript +// Playwright page.evaluate() — Fill Engine +function runFillEngine(pageElement) { + const pageH = pageElement.clientHeight; + const contentH = pageElement.scrollHeight; + const fillRatio = contentH / pageH; + + if (fillRatio >= 0.65) return; // No inflation needed + + const root = pageElement.style; + + // 2a. Line-height inflation + const inflation = Math.min((0.65 - fillRatio) / 0.35, 1.0); + const newLH = 1.4 + (2.2 - 1.4) * inflation; + root.setProperty('--body-line-height', newLH.toFixed(2)); + pageElement.querySelectorAll('p, li').forEach(el => { + el.style.lineHeight = newLH.toFixed(2); + }); + + // 2c. Font scaling + if (fillRatio < 0.50) { + pageElement.querySelectorAll('p, li').forEach(el => { + const size = parseFloat(getComputedStyle(el).fontSize); + el.style.fontSize = Math.min(size + 2, size + 2) + 'px'; // +2pt max + }); + } else if (fillRatio < 0.65) { + pageElement.querySelectorAll('p, li').forEach(el => { + const size = parseFloat(getComputedStyle(el).fontSize); + el.style.fontSize = (size + 1) + 'px'; // +1pt + }); + } + + // 3a. Table height expansion + pageElement.querySelectorAll('td, th').forEach(cell => { + const extra = Math.min(20, Math.round((0.65 - fillRatio) / 0.25 * 20)); + cell.style.paddingTop = (6 + extra) + 'px'; + cell.style.paddingBottom = (6 + extra) + 'px'; + }); + + // 3b. Blockquote exaggeration + pageElement.querySelectorAll('blockquote').forEach(bq => { + const size = parseFloat(getComputedStyle(bq).fontSize); + bq.style.fontSize = (size * 1.5) + 'px'; + bq.style.fontStyle = 'italic'; + bq.style.marginTop = '40px'; + bq.style.marginBottom = '40px'; + }); + + // Safety Net 4: Y-axis anchoring + const newContentH = pageElement.scrollHeight; + const newRatio = newContentH / pageH; + if (newRatio < 0.40) { + const remaining = pageH - newContentH; + const offset = remaining * 0.382; + pageElement.style.paddingTop = offset + 'px'; + } +} +``` + +--- + +## Execution Order Summary + +``` +Input content arrives + │ + ▼ +┌─────────────────────────────────────────┐ +│ Safety Net 1: Readability red line check │ +│ → Font size ≥ floor? YES → Continue │ +│ → Font size < floor? → Force raise to floor │ +└──────────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Virtual render: Calculate Fill Ratio │ +│ → ≥ 80%? → Render normally, exit │ +│ → 65%-80%? → Light inflation (2a+2b only) │ +│ → < 65%? → Enter Safety Net 2+3 full inflation │ +└──────────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Safety Net 2: Paragraph inflation │ +│ 2a. Line-height inflation (→ max 2.2) │ +│ 2b. Paragraph spacing injection (30%-50% of remaining space) │ +│ 2c. Font scaling (+1~2pt, max) │ +└──────────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Safety Net 3: Component inflation │ +│ 3a. Table Padding increase (+10~20pt) │ +│ 3b. Blockquote scale 1.5x + 40pt whitespace above/below │ +│ 3c. List spacing × 1.5 │ +└──────────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Recalculate Fill Ratio │ +│ → ≥ 40%? → Render normally, exit │ +│ → < 40%? → Safety Net 4: Y-axis golden ratio anchoring │ +└──────────────────┬──────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Safety Net 4: Shift content down │ +│ top_offset = remaining * 0.382 │ +│ → Elegant space above, slightly more below │ +│ → No longer "underfilled" but "intentional whitespace" │ +└─────────────────────────────────────────┘ + │ + ▼ + Actual render output +``` + +--- + +## Coordination with Other Specifications + +| Specification | Relationship | Coordination Rule | +|------|------|---------| +| `overflow.md` | Complementary (overflow vs void) | overflow's font degradation must not breach Fill Engine's red line | +| `pagination.md` | Complementary (pagination vs fill) | pagination's "last page ≥ 40%" aligns with Fill Engine's Fill Ratio concept | +| `cover.md` | Independent | Covers have their own layout system, not affected by Fill Engine | +| `typography.md` | Infrastructure | Fill Engine makes elastic adjustments on top of typography-defined fonts/line-heights | + +--- + +## Checklist (Check Before Every Body Page Render) + +``` +□ Body font size ≥ 14pt (single-column) / 12pt (double-column)? +□ H1 ≥ 32pt、H2 ≥ 24pt、H3 ≥ 18pt? +□ Virtual render calculated Fill Ratio? +□ Inflation triggered when Fill Ratio < 65%? +□ Line-height inflation not exceeding 2.2? +□ Font scaling not exceeding +2pt? +□ Table Padding increment within 10-20pt range? +□ Blockquote scale factor = 1.5x, top/bottom whitespace = 40pt? +□ Y-axis anchoring triggered when Fill Ratio < 40%? +□ Cover page not affected by Fill Engine? +``` diff --git a/skills/pdf/typesetting/geometry.md b/skills/pdf/typesetting/geometry.md new file mode 100755 index 0000000..8524720 --- /dev/null +++ b/skills/pdf/typesetting/geometry.md @@ -0,0 +1,142 @@ +# Geometric Anchors + +> Create sophisticated visual anchors from the simplest geometric shapes. + +--- + +## What Are Visual Anchors + +Visual anchors are **non-functional decorative elements** on a page, used to: +- Break the flatness of text-only / data-only layouts +- Establish a visual center of gravity on the page +- Convey abstract qualities and design intent +- Fill large whitespace areas without adding information noise + +**Key principle: Anchors don't need to "look like" anything concrete. The more abstract, the more refined.** + +--- + +## Basic Shape Vocabulary + +| Shape | SVG | Mood / Character | +|-------|-----|-----------------| +| Circle | `<circle>` | Wholeness, inclusivity, softness | +| Semicircle | `<path d="M0,50 A50,50 0 0,1 100,50">` | Rising, gradual, metaphorical | +| Triangle | `<polygon>` | Direction, sharpness, modern | +| Rectangle | `<rect>` | Stability, order, architectural | +| Line | `<line>` | Connection, guidance, minimalism | +| Arc | `<path>` + Bézier | Flow, elegance, organic | + +--- + +## Composition Patterns + +### Pattern 1: Offset Stacking + +Multiple identical shapes, slightly offset, rotated, with decreasing opacity. + +```svg +<svg width="120" height="120" viewBox="0 0 120 120" fill="none"> + <!-- Three offset circles --> + <circle cx="50" cy="50" r="35" stroke="currentColor" stroke-width="0.6" opacity="0.15"/> + <circle cx="60" cy="55" r="35" stroke="currentColor" stroke-width="0.6" opacity="0.25"/> + <circle cx="70" cy="60" r="35" stroke="currentColor" stroke-width="0.6" opacity="0.4"/> +</svg> +``` + +**Key points**: Same shape, 3 layers, opacity 0.15 → 0.25 → 0.4, offset 10-15px + +### Pattern 2: Scale Contrast + +One large shape + a few small shapes as accents. + +```svg +<svg width="150" height="150" viewBox="0 0 150 150" fill="none"> + <circle cx="60" cy="60" r="50" stroke="currentColor" stroke-width="0.5" opacity="0.2"/> + <circle cx="115" cy="30" r="8" fill="currentColor" opacity="0.6"/> + <circle cx="105" cy="50" r="3" fill="currentColor" opacity="0.3"/> + <circle cx="125" cy="45" r="2" fill="currentColor" opacity="0.2"/> +</svg> +``` + +**Key points**: Large circle stroke-only (hollow), small circles filled (solid), creating solid-void contrast + +### Pattern 3: Grid Intersection + +Lines + dots forming nodes at intersections. + +```svg +<svg width="100" height="100" viewBox="0 0 100 100" fill="none"> + <line x1="20" y1="0" x2="20" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.1"/> + <line x1="50" y1="0" x2="50" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.1"/> + <line x1="80" y1="0" x2="80" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.1"/> + <line x1="0" y1="30" x2="100" y2="30" stroke="currentColor" stroke-width="0.3" opacity="0.1"/> + <line x1="0" y1="70" x2="100" y2="70" stroke="currentColor" stroke-width="0.3" opacity="0.1"/> + <!-- Intersection points --> + <circle cx="50" cy="30" r="3" fill="currentColor" opacity="0.5"/> + <circle cx="20" cy="70" r="2" fill="currentColor" opacity="0.3"/> + <circle cx="80" cy="70" r="4" fill="currentColor" opacity="0.15"/> +</svg> +``` + +### Pattern 4: Arc Flow + +Bézier curves + endpoint circles, expressing organic flow. + +```svg +<svg width="200" height="100" viewBox="0 0 200 100" fill="none"> + <path d="M10,80 C50,10 150,10 190,80" stroke="currentColor" stroke-width="0.6" opacity="0.25"/> + <path d="M10,85 C60,20 140,20 190,85" stroke="currentColor" stroke-width="0.4" opacity="0.15"/> + <circle cx="10" cy="80" r="2.5" fill="currentColor" opacity="0.4"/> + <circle cx="190" cy="80" r="2.5" fill="currentColor" opacity="0.4"/> +</svg> +``` + +### Pattern 5: Geometric Collage + +Intentional combination of different shapes, like an architectural plan. + +```svg +<svg width="120" height="120" viewBox="0 0 120 120" fill="none"> + <!-- Rectangular frame --> + <rect x="10" y="10" width="60" height="60" stroke="currentColor" stroke-width="0.5" opacity="0.2"/> + <!-- Circle breaking the straight lines --> + <circle cx="70" cy="70" r="30" stroke="currentColor" stroke-width="0.5" opacity="0.2"/> + <!-- Diagonal line cutting through --> + <line x1="10" y1="10" x2="100" y2="100" stroke="currentColor" stroke-width="0.3" opacity="0.15"/> + <!-- Small solid triangle as focal point --> + <polygon points="85,20 95,40 75,40" fill="currentColor" opacity="0.4"/> +</svg> +``` + +--- + +## Placement Guide + +| Context | Recommended Position | Recommended Size | Pattern | +|---------|---------------------|-----------------|---------| +| Cover | Top-right / bottom-left offset | 120-200px | Offset Stacking, Geometric Collage | +| Chapter divider | Page center | 80-120px | Scale Contrast, Arc Flow | +| Header / footer decoration | Corners | 30-50px | Small offset, single circle + line | +| Whitespace fill | Alongside content | 60-100px | Grid Intersection | + +--- + +## Color Rules + +- Anchor color = document primary color from `palette.cascade` output (`--c-accent` or `--c-text`) +- **Use only one color**, layer via opacity (0.1 → 0.5) +- Strokes over fills (solid elements ≤ 30% of total shapes) +- stroke-width range: 0.3-0.8px (ultra-thin lines = refined look) +- **⚠️ All SVG examples below use `currentColor` as placeholder.** When generating actual SVG, replace with the document’s primary color from the palette system. NEVER copy `currentColor` literally into production SVG — substitute the actual hex value. + +--- + +## Forbidden + +- ❌ Figurative icons (flowers, stars, arrows, or other concrete shapes) +- ❌ Mixing multiple colors +- ❌ Over-complexity (more than 8 shape elements) +- ❌ Symmetric / centered placement (offset creates tension) +- ❌ Thick lines (> 1.5px looks heavy) +- ❌ Shadows / glow effects diff --git a/skills/pdf/typesetting/overflow.md b/skills/pdf/typesetting/overflow.md new file mode 100755 index 0000000..15c90d0 --- /dev/null +++ b/skills/pdf/typesetting/overflow.md @@ -0,0 +1,630 @@ +# Overflow Prevention — Anti-Overflow Layout System + +> PDF is static: no scrollbars, no reflow, no viewport adaptation. Every element must fit within its container BEFORE rendering. This document defines the architectural approach to guarantee zero overflow in any generated PDF. +> +> **This is the "too much content" side.** For the mirror problem ("too little content" / empty pages), see `typesetting/fill-engine.md` — the anti-void adaptive engine. +> +> Related: +> - `fill-engine.md` — Anti-void engine (font floor, fill ratio, paragraph inflation, Y-axis anchoring) +> - `pagination.md` — Pagination & cross-page integrity + +--- + +## Core Philosophy + +**"Measure first, draw second."** + +Never use `draw_text(x, y)` or `draw_image(x, y)` directly. All content must pass through a constraint system that pre-calculates sizes and enforces boundaries. Think CSS Box Model, but for a fixed canvas. + +--- + +## 1. Bounding Box System (Horizontal Overflow Prevention) + +Every element entering a page is a **Block** with a maximum available width (`Max_Width`). + +### Calculating Max_Width + +```python +# Single-column layout +max_width = page_width - left_margin - right_margin + +# Dual-column layout +col_gap = 12 # points +max_width = (page_width - left_margin - right_margin - col_gap) / 2 + +# Nested containers (e.g., table cell) +cell_max_width = col_width - cell_padding_left - cell_padding_right +``` + +### Absolute Rule + +> **No Block's rendered width may exceed its parent's `Max_Width`. Period.** + +If a Block's calculated width > Max_Width, apply fallback strategies (see §5). + +--- + +## 1.5 🔴 Page Content Centering (Horizontal Centering Iron Rule) + +> **Symptom:** Cover or body content is shifted left on the page, with noticeably more whitespace on the right than left. + +**Root cause:** Asymmetric left/right margins, or cover uses single-side anchors without considering right-side balance. + +**Iron rule:** + +1. **Left/right margins must be symmetric:** `left_margin == right_margin`. No asymmetric margins allowed. +2. **Cover:** For left-aligned templates (e.g., Template 01/02/03/05/07), the text starting point should be within `0.10*W ~ 0.15*W`, and the right margin should be between `0.05*W ~ 0.15*W`. Center-aligned templates (Template 04/06) must be absolutely centered. +3. **Body:** ReportLab's `Frame` / `SimpleDocTemplate` must have `leftMargin == rightMargin`. LaTeX's `\geometry{left=X, right=X}` must be symmetric. HTML must use `margin: 0 auto` or `padding-left == padding-right`. + +```python +# ReportLab: Force symmetric margins +from reportlab.lib.units import inch +MARGIN = 1 * inch # Left and right must use same variable +doc = SimpleDocTemplate( + "output.pdf", + leftMargin=MARGIN, + rightMargin=MARGIN, # ← Must match leftMargin + topMargin=MARGIN, + bottomMargin=MARGIN, +) + +# ❌ WRONG: leftMargin=72, rightMargin=36 → Content shifts left +``` + +```latex +% LaTeX: Force symmetric margins +\usepackage[left=2.5cm, right=2.5cm, top=2.5cm, bottom=2.5cm]{geometry} +% ❌ WRONG: left=3cm, right=1.5cm +``` + +--- + +## 2. Text Overflow: Font Metrics Pre-Calculation + +### The Wrong Way +```python +# ❌ NEVER estimate by character count +if len(text) > 20: + wrap() # Wrong — 20 CJK chars ≠ 20 Latin chars ≠ 20 mixed chars +``` + +### The Right Way — ReportLab +```python +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import Paragraph +from reportlab.lib.styles import ParagraphStyle + +# Measure actual rendered width +text_width = stringWidth("Your text here", "Microsoft YaHei", 10) + +if text_width > max_width: + # Use Paragraph for automatic wrapping — NEVER plain strings in tables + style = ParagraphStyle( + 'CellText', + fontName='Microsoft YaHei', + fontSize=10, + leading=14, + wordWrap='CJK', # Enables CJK-aware line breaking + ) + element = Paragraph(text, style) +else: + element = text # Plain string is fine if it fits +``` + +### Key Rules +- **Always use `Paragraph()` for table cell content** — plain strings don't wrap and will overflow +- **CJK text is wider**: Budget ~12pt per character at 10pt font size (vs ~6pt for Latin) +- **URLs and long strings**: If a single "word" exceeds column width, enable `wordWrap='CJK'` or split manually +- **Hyphenation**: For English text, consider `pyphen` for proper hyphenation of long words + +### The Right Way — LaTeX +```latex +% Use tabularx for auto-wrapping columns +\usepackage{tabularx} +\begin{tabularx}{\columnwidth}{lXX} % X columns auto-wrap + Header 1 & This long text will wrap & Another wrapping column \\ +\end{tabularx} + +% For URLs +\usepackage{url} +\url{https://very-long-url-that-would-overflow.example.com/path/to/resource} +``` + +### The Right Way — Playwright/HTML +```css +/* Global text overflow prevention */ +p, td, li, .content { + overflow-wrap: break-word; + word-break: break-word; + hyphens: auto; +} + +/* Strict CJK line-break rules */ +body { + line-break: strict; + word-break: normal; +} + +/* Table cells must constrain content */ +/* ⚠️ overflow:hidden + ellipsis only for single-line short text cells. + Multi-line td should use overflow-wrap: break-word, not overflow: hidden, + otherwise text gets truncated. Especially important in Playwright PDFs. */ +td { + max-width: 0; /* Forces column to respect assigned width */ + overflow: hidden; + text-overflow: ellipsis; /* Only for single-line cells */ +} +``` + +--- + +## 3. Image & Chart Overflow: Proportional Scaling + +### Absolute Rule + +> **Never insert an image or chart at its original dimensions. Always compute fit-to-container scaling.** + +### ReportLab Pattern +```python +from reportlab.platypus import Image +from reportlab.lib.units import mm + +def fit_image(img_path, max_w, max_h): + """Scale image to fit within max_w × max_h, preserving aspect ratio.""" + img = Image(img_path) + orig_w, orig_h = img.drawWidth, img.drawHeight + + ratio_w = max_w / orig_w if orig_w > max_w else 1.0 + ratio_h = max_h / orig_h if orig_h > max_h else 1.0 + ratio = min(ratio_w, ratio_h) + + img.drawWidth = orig_w * ratio + img.drawHeight = orig_h * ratio + return img + +# Usage +available_width = page_width - left_margin - right_margin +max_img_height = A4[1] * 0.35 # ~294pt ≈ 10cm — prevents image from eating page + leaves room for caption +img = fit_image("chart.png", available_width, max_img_height) +story.append(img) +``` + +### LaTeX Pattern +```latex +\usepackage{adjustbox} + +% Always constrain to column width +\includegraphics[max width=\columnwidth]{chart.png} + +% Or with adjustbox for both dimensions +\begin{adjustbox}{max width=\columnwidth, max height=0.4\textheight} + \includegraphics{chart.png} +\end{adjustbox} +``` + +### Playwright/HTML Pattern +```css +img, svg, .chart-container { + max-width: 100%; + max-height: 45vh; /* Prevent one image from eating an entire page */ + height: auto; + object-fit: contain; /* Preserve aspect ratio */ +} +``` + +> **Why `max-height: 45vh`?** Without a height cap, a tall image combined with `break-inside: avoid` (from pagination.md) gets pushed to the next page — leaving the current page mostly empty and the image occupying an entire page alone. 45vh ensures any image fits within half a page, leaving room for surrounding text on the same page. + +--- + +## 3.5 Horizontal Flex/Inline Layout Overflow (Flow Bars, Step Lists, Tag Rows) + +**Problem:** LLMs commonly generate horizontal `display: flex` layouts (process flow bars, step indicators, tag rows, icon grids) without any width constraint or wrap control. When content is longer than expected (e.g. "Theory Framework (ASPICE / V-Model)" as a step label), the total width exceeds the container, pushing content beyond the right page boundary. + +**Playwright PDF consequence:** When any element causes `scrollWidth > clientWidth`, Playwright shrinks the **entire page** to fit, causing all content to appear left-shifted with blank space on the right. This affects ALL pages, not just the one with the overflow. + +### Iron Rules (Direct HTML Flow) + +**Rule 3.5.1 — Mandatory `flex-wrap` for ≥3 inline items:** +```css +/* Any horizontal row with 3+ children MUST have flex-wrap */ +.flow-bar, .step-row, .tag-row, .icon-grid { + display: flex; + flex-wrap: wrap; /* MANDATORY for 3+ items */ + gap: 12px; /* Consistent spacing */ + max-width: 100%; /* Never exceed container */ +} +``` + +**Rule 3.5.2 — Flex children must have `min-width` + `flex-shrink`:** +```css +.flow-step, .tag-item { + flex: 1 1 auto; /* Grow, shrink, auto basis */ + min-width: 80px; /* Prevent crushing to 0 */ + max-width: 100%; /* Never exceed container alone */ + overflow-wrap: break-word; /* Break long words */ + word-break: break-all; /* CJK fallback */ +} +``` + +**Rule 3.5.3 — Arrow/connector separators must not be rigid:** +```css +/* ❌ WRONG — rigid arrow div between flex items */ +<div class="step">Step 1</div> +<div class="arrow">→</div> /* Fixed-width, prevents shrinking */ +<div class="step">Step 2</div> + +/* ✅ RIGHT — arrow as pseudo-element, doesn't affect flex layout */ +.flow-step + .flow-step::before { + content: '→'; + margin: 0 8px; + color: #999; + flex-shrink: 0; +} +``` + +**Rule 3.5.4 — Threshold-based layout switching:** + +| Item count | Recommended layout | Notes | +|------------|-------------------|-------| +| 1-3 items | Horizontal flex (no wrap needed if items are short) | Still add `max-width: 100%` on container | +| 4-6 items | Horizontal flex + `flex-wrap: wrap` | Items may wrap to 2 rows | +| 7+ items | Vertical stack or CSS Grid 2×N | Horizontal becomes unreadable | +| Items with long text (>15 CJK chars / >25 Latin chars) | Vertical stack regardless of count | Long labels don't fit side-by-side | + +### Quick Self-Check + +Before generating any horizontal flex layout, verify: +``` +□ Container has max-width: 100% (or explicit width ≤ page width)? +□ flex-wrap: wrap is set (if ≥3 items)? +□ Each child has min-width + max-width constraints? +□ Separators (arrows, dots, lines) are pseudo-elements, not rigid divs? +□ Long text items have overflow-wrap: break-word? +``` + +--- + +## 4. Table Overflow: Dynamic Column Width Allocation + +Tables are the #1 source of horizontal overflow. + +### Strategy: Weight-Based Column Width + +```python +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import Table, TableStyle, Paragraph +from reportlab.lib.styles import ParagraphStyle + +def calculate_col_widths(data, font_name, font_size, available_width, min_col=30): + """Calculate column widths based on content weight. + + Each column's width is proportional to its widest content, + with a minimum width and total constrained to available_width. + """ + n_cols = len(data[0]) + + # Measure max content width per column + max_widths = [0] * n_cols + for row in data: + for i, cell in enumerate(row): + text = str(cell) if not isinstance(cell, Paragraph) else cell.text + w = stringWidth(text, font_name, font_size) + 8 # +8pt padding + max_widths[i] = max(max_widths[i], w) + + total_natural = sum(max_widths) + + if total_natural <= available_width: + # Everything fits — distribute remaining space proportionally + extra = available_width - total_natural + return [w + extra * (w / total_natural) for w in max_widths] + else: + # Must compress — allocate proportionally with minimum + col_widths = [] + for w in max_widths: + allocated = max(min_col, available_width * (w / total_natural)) + col_widths.append(allocated) + + # Normalize to exactly fit available_width + scale = available_width / sum(col_widths) + return [w * scale for w in col_widths] + + +def build_safe_table(data, available_width, font_name='Microsoft YaHei', font_size=9): + """Build a table guaranteed not to overflow horizontally. + + All text cells are wrapped in Paragraph() for automatic line-breaking. + """ + wrap_style = ParagraphStyle( + 'TableCell', + fontName=font_name, + fontSize=font_size, + leading=font_size + 3, + wordWrap='CJK', + ) + + # Wrap all cells in Paragraph + wrapped_data = [] + for row in data: + wrapped_row = [Paragraph(str(cell), wrap_style) for cell in row] + wrapped_data.append(wrapped_row) + + col_widths = calculate_col_widths(data, font_name, font_size, available_width) + + # Verify total width + assert sum(col_widths) <= available_width + 0.5, \ + f"Table width {sum(col_widths):.1f} exceeds available {available_width:.1f}" + + table = Table(wrapped_data, colWidths=col_widths, repeatRows=1) + return table +``` + +### LaTeX Table Width Management +```latex +% For tables that MUST fit single column +\begin{tabularx}{\columnwidth}{l X X r} % X = flexible width columns + ... +\end{tabularx} + +% For wide tables in twocolumn mode → span full page +\begin{table*}[t] + \begin{tabularx}{\textwidth}{l X X X X r} + ... + \end{tabularx} +\end{table*} + +% Last resort: shrink to fit (verify ≥ 6pt after scaling) +\resizebox{\columnwidth}{!}{% + \begin{tabular}{lllllll} + ... + \end{tabular} +} +``` + +### LaTeX Equation Width Management (Dual-Column) + +Equations are the **#2 overflow source** after tables in two-column papers (ACM `sigconf` column ~241pt, IEEE ~252pt). + +```latex +% ❌ WRONG — two full equations on one line +\begin{equation} + \mathbf{e}_u = \sum \frac{1}{\sqrt{...}} \mathbf{e}_i, \quad + \mathbf{e}_i = \sum \frac{1}{\sqrt{...}} \mathbf{e}_u +\end{equation} + +% ✅ CORRECT — one equation per line +\begin{align} + \mathbf{e}_u &= \sum \frac{1}{\sqrt{...}} \mathbf{e}_i, \\ + \mathbf{e}_i &= \sum \frac{1}{\sqrt{...}} \mathbf{e}_u. +\end{align} + +% ✅ For wide fractions (softmax, attention) +% Factor out sub-expressions into separate definitions +\begin{equation} + \alpha_{uv} = \frac{\exp(f(u,v))}{\sum_k \exp(f(u,k))}, + \quad \text{where } f(u,v) = \text{LeakyReLU}(\ldots) +\end{equation} + +% ✅ For contrastive losses: use multline +\begin{multline} + \mathcal{L}_{\text{SSL}}^u = + -\log \frac{\exp(\text{sim}(z_u', z_u'')/\tau)} + {\sum_{v \neq u} \exp(\text{sim}(z_u', z_v'')/\tau)}. +\end{multline} +``` + +**Quick heuristic:** if `equation` body > 60 raw characters (excluding `\label`), it probably overflows dual-column. Use `align`, `split`, `multline`, or factor out sub-expressions. + +See `academic.md` Rules M1–M4 for full patterns. + +### LaTeX Algorithm Width Management (Dual-Column) + +```latex +\SetAlFnt{\small} % ❗ MANDATORY in dual-column +\SetAlCapFnt{\small} + +% Break long Input/Output across lines: +\KwInput{Graph $\mathcal{G}_R$, $\mathcal{G}_S$\\ + \quad dim $d$, layers $L$, lr $\eta$, reg $\lambda$} + +% Or use algorithm* to span full width +\begin{algorithm*}[t] ... \end{algorithm*} +``` + +### ⚠️ `\columnwidth` vs `\textwidth` in Two-Column Layouts + +| Context | `\columnwidth` | `\textwidth` | +|---------|---------------|-------------| +| Single-column doc | = page content width | = page content width (same) | +| Two-column doc (`table` float) | = **one column** (~252pt) | = **full page** (~504pt) | +| Two-column doc (`table*` float) | = one column | = full page | + +**Rule:** Inside `table` (single-col float), ALWAYS use `\columnwidth`. Inside `table*` (full-width float), use `\textwidth`. + +`check-tex` detects `\resizebox{\textwidth}` inside single-column floats as error `RESIZEBOX_TEXTWIDTH`. + +--- + +## 5. Fallback & Degradation Strategies + +When content doesn't fit even after wrapping and scaling, apply these strategies **in order**: + +### Automatic Degradation Ladder + +| Step | Strategy | Limit | Notes | +|------|----------|-------|-------| +| 1 | Wrap text into Paragraph | — | Always do this first | +| 2 | Shrink font by 1pt | **Min 14pt** (single-col) / **12pt** (dual-col) | ⚠️ Enforced by `fill-engine.md` Safety Net 1 | +| 3 | Reduce padding/spacing | Min 4pt padding | Don't go below 4pt cell padding | +| 4 | Switch to landscape | Only if user allows | Never change orientation silently | +| 5 | Split into multiple elements | — | e.g., one wide table → two tables | +| 6 | Log warning + render anyway | — | If all else fails, at least don't crash | + +### ReportLab Font Degradation +```python +def fit_text_with_degradation(text, font_name, base_size, max_width, min_size=14): + """Try progressively smaller font sizes until text fits. + + NOTE: min_size enforced by fill-engine.md Safety Net 1. + Single-column: min_size=14. Dual-column: min_size=12. + If text still doesn't fit at min_size → trigger page break, do NOT shrink further. + """ + for size in range(base_size, min_size - 1, -1): + if stringWidth(text, font_name, size) <= max_width: + return size + return min_size # Absolute floor — log warning +``` + +### Table Column Degradation +```python +def degrade_table_if_needed(data, available_width, font_name, base_font_size=10): + """Try fitting table, degrading font size if needed.""" + for font_size in [base_font_size, base_font_size - 1, base_font_size - 2]: + col_widths = calculate_col_widths(data, font_name, font_size, available_width) + if all(w >= 25 for w in col_widths): # Minimum 25pt per column + return font_size, col_widths + + # Still doesn't fit — consider splitting table or landscape + return base_font_size - 2, col_widths +``` + +--- + +## 6. Vertical Overflow: Y-Cursor & Smart Pagination + +Horizontal overflow → wrap/scale/shrink. +Vertical overflow → paginate. + +### Y-Cursor Architecture (ReportLab Platypus handles this, but understand it) + +``` +Page Start +├── Current_Y = top_margin +├── Draw Block A (height = 80pt) +│ └── Current_Y += 80 + spacing +├── Draw Block B (height = 120pt) +│ └── Current_Y += 120 + spacing +├── Check: Current_Y + Next_Block_Height > (page_height - bottom_margin)? +│ ├── YES → New page, reset Current_Y = top_margin +│ └── NO → Continue drawing +└── ... +``` + +### Anti-Tear Rules (Elements That Must Not Split) + +```python +from reportlab.platypus import KeepTogether + +# 1. Heading + first paragraph — MANDATORY +story.append(KeepTogether([ + heading, + first_paragraph, +])) + +# 2. Image/chart + caption — MANDATORY +story.append(KeepTogether([ + chart_image, + caption_paragraph, +])) + +# 3. Table title + table (short tables ≤ 15 rows) +if len(data) <= 15: + story.append(KeepTogether([table_title, table])) + +# 4. Long tables: repeat header on each page +table = Table(data, repeatRows=1) # First row repeats on every page +``` + +### Orphan/Widow Prevention + +- If a paragraph's last line would be alone on the next page → pull at least 2 lines forward +- If a section heading lands at page bottom with no body text → push to next page +- ReportLab: `KeepTogether` handles most cases; `allowSplitting=False` for critical blocks + +### LaTeX Vertical Overflow +```latex +% Prevent orphans and widows +\widowpenalty=10000 +\clubpenalty=10000 + +% Prevent page break after heading +\usepackage{titlesec} +\titlespacing*{\section}{0pt}{12pt plus 4pt minus 2pt}{6pt plus 2pt minus 2pt} + +% Keep float near text +\usepackage[section]{placeins} % \FloatBarrier at each \section +``` + +--- + +## 7. Two-Pass Rendering (Advanced — for Complex Documents) + +For critical documents where overflow would be unacceptable: + +### Pass 1: Virtual Layout (Measurement) + +Calculate all element sizes without rendering. Build a **Layout Tree**: + +```python +layout_tree = [ + {"type": "heading", "width": 450, "height": 28, "page": 1}, + {"type": "paragraph", "width": 450, "height": 84, "page": 1}, + {"type": "table", "width": 450, "height": 220, "page": 1}, + {"type": "chart", "width": 400, "height": 300, "page": 2}, + # ... +] +``` + +### Collision Detection + +```python +def check_overflow(layout_tree, page_width, left_margin, right_margin): + """Verify no element overflows page boundaries.""" + max_x = page_width - right_margin + violations = [] + for elem in layout_tree: + right_edge = left_margin + elem["width"] + if right_edge > max_x: + violations.append({ + "element": elem["type"], + "page": elem["page"], + "overflow_by": right_edge - max_x, + }) + return violations +``` + +### Pass 2: Render with Confirmed Layout + +Only render after Pass 1 confirms zero violations. If violations found → apply degradation strategies from §5, then re-run Pass 1. + +**In practice**: ReportLab's Platypus engine already does a form of two-pass rendering internally (`doc.multiBuild()`). Use `multiBuild` + `afterFlowable` callbacks for complex documents that need cross-referencing or dynamic layout adjustment. + +--- + +## Quick Reference: Which Route Uses What + +| Mechanism | ReportLab (Report) | LaTeX (Academic) | Playwright (Creative) | +|-----------|-------------------|-------------------|----------------------| +| Text wrapping | `Paragraph()` + `wordWrap='CJK'` | `tabularx` X columns | CSS `overflow-wrap: break-word` | +| Image scaling | `fit_image()` helper | `\includegraphics[max width=]` | CSS `max-width: 100%` | +| Table width | `calculate_col_widths()` | `tabularx` / `resizebox` | CSS `table-layout: fixed` | +| Font degradation | `fit_text_with_degradation()` | `\small` / `\footnotesize` | CSS `font-size` step-down | +| Page break | `PageBreak()` / `KeepTogether` | `\newpage` / `\FloatBarrier` | CSS `break-before: page` | +| Header repeat | `Table(repeatRows=1)` | `\endhead` in longtable | `thead { display: table-header-group }` | +| Orphan/widow | `KeepTogether` | `\widowpenalty=10000` | CSS `orphans: 2; widows: 2` | + +--- + +## Checklist (Run Before Every PDF Build) + +``` +□ All table cells use Paragraph() wrapping, not plain strings? +□ sum(colWidths) ≤ available_width verified in code? +□ Images scaled to fit container (not original size)? +□ Long tables have repeatRows=1 (or thead header-group)? +□ Heading + first paragraph wrapped in KeepTogether? +□ Chart + caption wrapped in KeepTogether? +□ CJK text uses wordWrap='CJK' style? +□ URL/long-string cells have word-break handling? +□ Font degradation fallback exists for tight columns? +□ Last page content ratio ≥ 40%? +``` diff --git a/skills/pdf/typesetting/pagination.md b/skills/pdf/typesetting/pagination.md new file mode 100755 index 0000000..2484f95 --- /dev/null +++ b/skills/pdf/typesetting/pagination.md @@ -0,0 +1,367 @@ +# Pagination & Flow Control + +> Core rules for multi-page document layout quality. Must be followed every time a multi-page PDF is generated. +> +> Related: +> - `typesetting/overflow.md` — the comprehensive overflow prevention system covering all three routes (ReportLab, LaTeX, Playwright). +> - `typesetting/fill-engine.md` — the **anti-void** adaptive engine (handles pages with too little content: font floor, fill ratio, paragraph inflation, Y-axis golden-ratio anchoring). + +--- + +## 1. Last Page Blank Control (Anti-Orphan Page) + +**Problem**: The last page has only one or two lines of content with large blank areas — looks terrible. + +**Mandatory Rules**: + +- After generating multi-page content, **you must check the content fill ratio of the last page** +- When last page content ratio < 25%, **you must backtrack and adjust** +- Adjustment strategies (by priority): + 1. **Compress preceding page spacing**: Reduce margin-bottom between sections (decrease 2-4px each) + 2. **Tighten line height**: Body line-height from 1.7 → 1.55 (no lower than 1.5) + 3. **Reduce font size**: Body from 16px → 15px (no lower than 14px) + 4. **Trim content**: Remove dispensable descriptive text without affecting core information + 5. **Merge small sections**: Combine adjacent sections with little content + +**Checking Method (Playwright HTML route)**: +```css +/* Check min-content on the last .page element */ +/* If content is less than 25% of page, backtracking is needed */ +``` + +**Practical Standards**: +- Last page content ratio >= 40% → ✅ Pass +- Last page content ratio 25%-40% → ⚠️ Acceptable but optimization recommended +- Last page content ratio < 25% → ❌ Must adjust + +--- + +## 2. Table Cross-Page Integrity + +**Problem**: Table header and first data row split across two pages; table cut in the middle. + +**Mandatory Rules**: + +### Playwright HTML Route +```css +/* Prevent table splitting */ +table, .table-wrapper { + break-inside: avoid; /* Preferred: keep entire table together */ + page-break-inside: avoid; +} + +/* If table is too long and must split, ensure header repeats */ +thead { + display: table-header-group; /* Repeat header on each page */ +} + +/* Don't cut table rows in the middle */ +tr { + break-inside: avoid; + page-break-inside: avoid; +} + +/* Bind header + at least 2 data rows together */ +thead + tbody tr:first-child, +thead + tbody tr:nth-child(2) { + break-before: avoid; + page-break-before: avoid; +} +``` + +### ReportLab Route +```python +# Use Table's repeatRows parameter +table = Table(data, repeatRows=1) # Repeat header on each page + +# Or use KeepTogether to wrap small tables +from reportlab.platypus import KeepTogether +elements.append(KeepTogether([table_title, table])) +``` + +**Additional Rules**: +- Table rows ≤ 8: Entire table `break-inside: avoid`, no page splitting allowed +- Table rows > 8: Splitting allowed, but must use `thead { display: table-header-group }` to repeat header on each page +- All card-grid / flex-grid layouts follow the same rule: `break-inside: avoid` + +--- + +## 3. CJK Punctuation Placement Rules + +**Problem**: Commas, periods, enumeration commas, etc. appearing at line start, violating CJK typesetting standards. + +**Mandatory Rules**: + +### Playwright HTML Route (Recommended) +```css +/* Global CJK punctuation rules */ +body { + line-break: strict; /* Strict line-break rules */ + word-break: normal; /* Don't force word breaks */ + overflow-wrap: break-word; /* Allow long words to break */ + hanging-punctuation: allow-end; /* Allow punctuation to hang past line end */ +} + +/* For body paragraphs */ +p, .body-text, td, li { + line-break: strict; + text-align: justify; /* Justify to reduce line-end gaps */ +} +``` + +**Effect of `line-break: strict`**: +- Prevents line-start: ,。、;:!?)】》…— +- Prevents line-end: (【《 +- Natively supported by Chromium engine, no extra JS needed + +### ReportLab Route +```python +# Set in ReportLab Paragraph style +from reportlab.lib.enums import TA_JUSTIFY +style = ParagraphStyle( + 'Body', + alignment=TA_JUSTIFY, + wordWrap='CJK', # CJK line-break mode +) +``` + +**Verification Checklist**: +- [ ] No comma/period appears as the first character of any line +- [ ] Left parenthesis / left quotation mark does not appear at line end +- [ ] Ellipsis is not broken in the middle + +--- + +## 4. Major Section Page-Break Rule (3/4 Threshold) + +**Problem**: A major section (H1/一级标题) ends at ~75% of the page, and the next major section’s title gets squeezed into the remaining 25%. This looks cramped and ugly — the new section deserves a fresh page. + +**Iron Rule**: When a major section (H1-level heading, e.g., “一、”“二、” or “Chapter 1”) is about to start, check remaining page space: + +| Remaining space | Action | +|----------------|--------| +| **≥ 25% of page height** | Continue on same page — enough room for heading + meaningful content | +| **< 25% of page height** | Force page break — start the new section on a fresh page | + +**Why 25% (not 50%)?** A major heading needs at least its title + 2-3 lines of body text to look intentional. If there’s only enough room for a title and a line or two, it looks like an accident. + +### ReportLab Implementation +```python +from reportlab.platypus import CondPageBreak + +# Before every H1-level heading, insert a conditional page break. +# CondPageBreak(height) breaks to next page if remaining space < height. +# Use 75% of available page height as threshold. +available_height = page_height - top_margin - bottom_margin +threshold = available_height * 0.25 # break if less than 25% remains + +# In story building: +story.append(CondPageBreak(threshold)) # ← goes before H1 heading +story.append(h1_paragraph) +``` + +### Playwright / CSS @page Implementation +```css +/* H1-level headings always prefer starting on a new page + unless there's substantial room remaining */ +h1, .major-section-title { + break-before: auto; /* Default: don't force */ + page-break-before: auto; +} +``` + +```javascript +// Post-render check: if an H1 starts in the bottom 25% of the viewport, +// force a page-break-before to avoid orphan headings +document.querySelectorAll('h1, .major-section-title').forEach(h => { + const rect = h.getBoundingClientRect(); + // In print context, check if heading is too far down the page + // This can be verified after Playwright render via page.evaluate() + const pageHeight = window.innerHeight; + const relativeY = rect.top / pageHeight; + if (relativeY > 0.75) { + h.style.breakBefore = 'page'; + } +}); +``` +``` + +### LaTeX Implementation +```latex +% Before each \section{} (H1-level), check remaining space +\needspace{0.25\textheight} % requires needspace package +\section{New Major Section} +``` + +**Scope**: This rule applies to **H1-level headings only** (major sections, chapters, top-level numbered items like “一、”“二、”). Sub-sections (H2, H3) follow the standard heading-body binding rule (no orphan headings at page bottom) but do NOT force page breaks. + +--- + +## 5. Other Anti-Split Rules + +### Heading–Body Binding +```css +h1, h2, h3, h4, .section-title { + break-after: avoid; /* Don't page-break after heading */ + page-break-after: avoid; +} +``` + +### Image / Card Protection +```css +figure, .card, .kpi-card, .project-card { + break-inside: avoid; + page-break-inside: avoid; +} +``` + +> **⚠️ Image `max-height` is critical.** `break-inside: avoid` alone can cause images to occupy an entire page when the image is tall. Always pair with `max-height` from overflow.md (`img { max-height: 45vh }`) to prevent single images from consuming a full page. + +### List Item Binding +```css +li { + break-inside: avoid; +} +/* Keep at least 2 list items on the same page */ +li:last-child { + break-before: avoid; +} +``` + +--- + +## Quick Checklist (After every multi-page PDF generation) + +``` +□ Last page content ≥ 40%? +□ Major sections (H1) not starting in bottom 25% of a page? +□ Table header and data rows not separated? +□ No punctuation appearing at line start? +□ No heading orphaned at page bottom? +□ No card/image cut in half? +□ Page numbering follows the standard scheme (see Section 6)? +``` + +--- + +## 6. Standard Page Numbering Scheme + +All multi-page documents MUST follow this five-zone page numbering convention unless the user explicitly requests otherwise. + +### Zone Definitions + +| Zone | Section | Numbering Style | Starts At | Visibility | +|------|---------|----------------|-----------|------------| +| **1. Cover** | Title page | — | Logical page 1 | **Hidden** (no visible page number, but counts as page 1 internally) | +| **2. Front Matter** | Table of Contents, Preface, Abstract, Acknowledgments | **Lowercase Roman** (i, ii, iii, iv, v…) | i | Visible, centered footer | +| **3. Body** | Main content chapters/sections | **Arabic** (1, 2, 3…) | **Resets to 1** | Visible, centered or outer-edge footer | +| **4. Appendix** | Appendices (A, B, C…) | **Arabic, continues** from body | Continues | Visible | +| **5. References / Bibliography** | Works cited, bibliography | **Arabic, continues** from body/appendix | Continues | Visible | + +### Key Rules + +0. **NEVER use "Page X of Y" format (denominator is FORBIDDEN).** Footer must show only the page number itself (e.g., `1`, `2`, `iii`). Do NOT display total page count. No `Page 3 of 12`, no `第3页/共12页`, no `3 / 12`. Just the bare number. + +1. **Cover page is ALWAYS page 1 internally** but the page number is **never displayed**. This is achieved by suppressing the footer/header on the first page, not by excluding it from the page count. + +2. **Front matter uses a separate Roman numeral sequence.** When front matter exists (TOC, abstract, preface), it forms its own numbering sequence starting at `i`. This sequence is independent of the body numbering. + +3. **Body numbering resets to Arabic 1.** The first page of actual content (Chapter 1, Introduction, etc.) is always page `1` regardless of how many front matter pages precede it. + +4. **Appendix and references continue the body sequence.** There is NO reset between body → appendix → references. If the body ends on page 42, Appendix A starts on page 43. + +5. **Documents without front matter** skip zone 2 entirely. Cover = hidden page 1, body starts at visible page 1. + +6. **Documents without a cover** start the body (or front matter if present) at page 1 directly. + +### ReportLab Implementation + +```python +from reportlab.platypus import SimpleDocTemplate, PageBreak, NextPageTemplate, PageTemplate +from reportlab.lib.units import inch +from reportlab.platypus.frames import Frame + +def footer_with_arabic(canvas, doc): + """Standard Arabic page number in footer.""" + canvas.saveState() + canvas.setFont('Helvetica', 9) + canvas.drawCentredString(doc.pagesize[0] / 2, 0.5 * inch, + str(doc.page)) + canvas.restoreState() + +def footer_with_roman(canvas, doc): + """Roman numeral page number for front matter.""" + roman_map = {1:'i',2:'ii',3:'iii',4:'iv',5:'v',6:'vi',7:'vii',8:'viii',9:'ix',10:'x'} + page_num = roman_map.get(doc.page, str(doc.page)) + canvas.saveState() + canvas.setFont('Helvetica', 9) + canvas.drawCentredString(doc.pagesize[0] / 2, 0.5 * inch, page_num) + canvas.restoreState() + +def no_footer(canvas, doc): + """Cover page — no visible page number.""" + pass + +# Define page templates: +# - 'cover': no footer +# - 'frontmatter': Roman numeral footer +# - 'body': Arabic footer (page counter resets) +``` + +### Playwright / HTML + CSS Implementation + +```css +/* Zone 1: Cover — suppress page number */ +.page-cover { + /* No footer content */ +} + +/* Zone 2: Front matter — Roman numerals via CSS counter */ +@page :nth(2) { /* Adjust range based on front matter pages */ } + +/* For Playwright, page numbers are typically added via: + 1. A footer element on each .page div, or + 2. Post-processing with pypdf after PDF generation */ +``` + +**Practical approach for Playwright route**: Since CSS `@page` counters with Roman/Arabic switching are poorly supported, the recommended pattern is: +1. Generate the PDF without page numbers +2. Use pypdf to stamp page numbers in post-processing: + - Skip page 1 (cover) + - Roman numerals for front matter pages + - Arabic starting from 1 for body pages + +### LaTeX Implementation + +```latex +% Cover: no page number displayed +\begin{titlepage} + \thispagestyle{empty} % Suppress page number + % ... cover content ... +\end{titlepage} + +% Front matter: Roman numerals +\pagenumbering{roman} % Switches to i, ii, iii... +\tableofcontents +\newpage + +% Body: Arabic, reset to 1 +\pagenumbering{arabic} % Switches to 1, 2, 3... (auto-resets counter) +\section{Introduction} +% ... + +% Appendix: continues Arabic numbering (no reset) +\appendix +\section{Appendix A} % Page number continues from body + +% References: continues Arabic numbering (no reset) +\bibliographystyle{plain} +\bibliography{refs} +``` + +### When to Deviate + +- **Single-page documents** (certificates, letters, posters): No page numbering at all. +- **Short documents (≤3 pages)**: Simple Arabic `1, 2, 3` throughout, no cover/frontmatter distinction. +- **User explicitly requests a different scheme**: Follow the user's instructions. +- **Exam papers**: Sequential Arabic numbering on every page, including page 1. diff --git a/skills/pdf/typesetting/palette.md b/skills/pdf/typesetting/palette.md new file mode 100755 index 0000000..c67980f --- /dev/null +++ b/skills/pdf/typesetting/palette.md @@ -0,0 +1,217 @@ +# Color Palette System + +> Color is the skeleton of design. Unified, restrained, systematic. Garish = amateur. + +--- + +## Cascade Palette System (V2 — Preferred) + +The cascade palette enforces one iron law: **Area ∝ 1/Saturation**. +The larger the colored area, the lower its saturation must be. + +### Tier System + +| Tier | Area % | S Cap | Roles | +|------|--------|-------|-------| +| **XL** | >50% | ≤ 0.08 | `page_bg`, `section_bg` | +| **L** | 20-50% | ≤ 0.15 | `card_bg`, `table_stripe` | +| **M** | 5-20% | ≤ 0.30 | `header_fill`, `cover_block` | +| **S** | 1-5% | ≤ 0.50 | `border`, `icon` | +| **XS** | <1% | ≤ 0.75 | `accent`, `accent_secondary` | + +### How It Works + +One base hue → 12 roles + 4 semantic colors. Cover, body, and charts all pull from the same palette: + +``` +palette.cascade + ├── cover subset (page_bg, header_fill, cover_block, accent, text_primary...) + ├── body subset (page_bg, section_bg, card_bg, table_stripe, border...) + ├── chart subset (accent as series_1, accent_secondary as series_2, ...) + └── semantic (success, warning, error, info — all low-sat) +``` + +No orphan colors. No "cover finished, now pick new colors for body" drift. + +### Usage + +```bash +# Via design_engine.py +python3 "$PDF_SKILL_DIR/scripts/design_engine.py" palette-cascade --intent cold --mode minimal + +# Via pdf.py (auto-derives intent from title) +python3 "$PDF_SKILL_DIR/scripts/pdf.py" palette.cascade --title "2025年度报告" --format reportlab + +# Formats: summary (default) | json | css | reportlab +``` + +### Output Formats + +- **summary**: Human-readable table with tier/role/hex/saturation +- **json**: Full structured data (roles, cover, body, charts, semantic, meta) +- **css**: CSS custom properties ready for HTML/Playwright +- **reportlab**: Python code ready to paste into ReportLab scripts + +--- + +## Core Iron Rules + +### 1. One Document, One Color Family + +**Not one color — one color family.** + +- After choosing the primary color, all secondary, accent, and background colors must be derived from it +- Derivation methods: lightness shift, saturation shift, micro hue adjustment (within ±15°) +- **Forbidden** to have unrelated colors in the same document + +``` +Primary → Headings, key data, primary buttons +Secondary → Primary lightness ±15-25% +Accent → Primary hue ±10-15°, for highlights/warnings +Neutral → Gray series for body text, not conflicting with primary +Background → Pure white / primary at opacity 3-8% +``` + +### 2. Color Count Limits + +| Element Type | Max Colors | Notes | +|-------------|-----------|-------| +| Entire document | 4-5 | Primary + secondary + accent + neutral + background | +| Single component (card/table) | 2-3 | Don't give each card a different color | +| Charts / data visualization | Same-family gradient | Differentiate by opacity/lightness, not different hues | +| Tags / badges | 1 color + text color | No rainbow tags | + +### 3. Absolutely Forbidden Color Fills + +The following are automatic failures: + +- ❌ 4 cards using 4 completely different colors (red/blue/green/purple) +- ❌ Alternating table rows in different colors (blue row/pink row) +- ❌ Rainbow-colored pie charts/bar charts +- ❌ Each section with a different theme color +- ❌ Gradient transitioning from warm to cool tones (red → blue) + +--- + +## Color Generation Rules + +### Deriving a Full Palette from Primary + +``` +Given primary H(hue) S(saturation) L(lightness): + +Primary: hsl(H, S, L) — Headings, key elements +Dark variant: hsl(H, S, L-15%) — Hover, borders, icons +Light variant: hsl(H, S-10%, L+25%) — Tag backgrounds, light fills +Ultra-light bg: hsl(H, S-20%, 96%) — Section backgrounds, card base +Accent: hsl(H+15, S, L) — Warnings, highlights (micro hue shift) +``` + +### Example: Deriving from a primary color + +> ⚠️ The hex values below are **examples only**. In production, use `palette.cascade` or `palette.generate` to compute the full palette from intent. + +```css +:root { + --c-primary: #2d5a87; /* Primary (from palette.cascade) */ + --c-primary-d: #1e3d5c; /* Dark variant */ + --c-primary-l: #5a8ab8; /* Light variant */ + --c-primary-bg: #f0f4f8; /* Ultra-light background */ + --c-accent: #2d6a87; /* Accent (hue +10°) */ + --c-text: #333; /* Body text */ + --c-text-muted: #888; /* Secondary text */ + --c-border: #e0e4e8; /* Border lines */ +} +``` + +--- + +## Multi-Element Differentiation Strategies + +When distinguishing multiple sibling elements (e.g., multiple cards, categories), **don't use different colors — use these approaches instead**: + +### Strategy A: Same Hue, Different Lightness +```css +.card-1 { background: hsl(220, 40%, 95%); } /* Lightest */ +.card-2 { background: hsl(220, 40%, 90%); } +.card-3 { background: hsl(220, 40%, 85%); } +.card-4 { background: hsl(220, 40%, 80%); } /* Darkest */ +``` + +### Strategy B: Same Color, Different Opacity +```css +.item-1 { background: rgba(30, 58, 95, 0.06); } +.item-2 { background: rgba(30, 58, 95, 0.12); } +.item-3 { background: rgba(30, 58, 95, 0.18); } +.item-4 { background: rgba(30, 58, 95, 0.24); } +``` + +### Strategy C: Primary + Whitespace + Lines +```css +/* Differentiate by border color/weight/style, uniform white background */ +.card-1 { border-left: 3px solid var(--primary); } +.card-2 { border-left: 3px solid var(--primary-l); } +.card-3 { border-left: 3px solid var(--primary-d); } +``` + +### Strategy D: Icons / Numbering (Not Color) +```css +/* All cards same color, differentiated by icons, numbers, or layout variation */ +``` + +--- + +## Gradient Usage Rules + +### Allowed Gradients +- **Same-family gradient**: `linear-gradient(135deg, var(--c-primary), var(--c-primary-l))` — hue difference < 20° +- **Lightness gradient**: `linear-gradient(180deg, #fff, #f5f5f5)` — pure lightness change +- **Primary to transparent**: `linear-gradient(90deg, var(--c-primary), transparent)` — for decorative lines + +### Forbidden Gradients +- ❌ Warm-to-cool crossover: `linear-gradient(#ff6b6b, #4ecdc4)` +- ❌ More than 3 colors: `linear-gradient(red, yellow, green, blue)` +- ❌ Neon gradients: Any high-saturation gradient +- ❌ Gratuitous gradients: Gradients added purely for "looks nice" without purpose + +--- + +## Preset Palettes (Ready to Use) + +### Business Blue +``` +#1a365d → #2a5298 → #4a7ac7 → #dce6f5 → #f5f8fc +``` + +### Warm Gray +``` +#2d2d2d → #5a5a5a → #8a8a8a → #e8e8e8 → #f9f9f9 +``` + +### Forest Green +``` +#1a3c2a → #2d6b4a → #4a9a6a → #d5ead8 → #f2f8f4 +``` + +### Terracotta Red +``` +#5c2018 → #8a3828 → #b85a48 → #f0d8d0 → #faf4f2 +``` + +### Indigo Purple +``` +#2d1b4e → #4a2d7a → #6a4aaa → #ddd0f0 → #f5f2fa +``` + +--- + +## Quick Check + +``` +□ How many colors does the entire document use? (Target ≤ 5) +□ Can every color be traced back to the primary? +□ Are there any colors that "suddenly appear" without derivation? +□ Are sibling elements rainbow-colored? +□ Gradient endpoint hue difference < 20°? +□ If you remove all color and look at grayscale only, is the hierarchy still clear? +``` diff --git a/skills/pdf/typesetting/typography.md b/skills/pdf/typesetting/typography.md new file mode 100755 index 0000000..a5901a9 --- /dev/null +++ b/skills/pdf/typesetting/typography.md @@ -0,0 +1,20 @@ + +--- + +## CJK Typography Supplement + +> See `pagination.md` Section 3 for detailed rules. + +```css +/* CJK punctuation placement rules (always include) */ +body { + line-break: strict; + word-break: normal; + overflow-wrap: break-word; +} + +p, td, li { + line-break: strict; + text-align: justify; +} +``` diff --git a/skills/podcast-generate/LICENSE.txt b/skills/podcast-generate/LICENSE.txt new file mode 100755 index 0000000..1e54539 --- /dev/null +++ b/skills/podcast-generate/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 z-ai-web-dev-sdk Skills + +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/podcast-generate/SKILL.md b/skills/podcast-generate/SKILL.md new file mode 100755 index 0000000..a7f89c0 --- /dev/null +++ b/skills/podcast-generate/SKILL.md @@ -0,0 +1,198 @@ +--- +name: Podcast Generate +description: Generate podcast episodes from user-provided content or by searching the web for specified topics. If user uploads a text file/article, creates a dual-host dialogue podcast (or single-host upon request). If no content is provided, searches the web for information about the user-specified topic and generates a podcast. Duration scales with content size (3-20 minutes, ~240 chars/min). Uses z-ai-web-dev-sdk for LLM script generation and TTS audio synthesis. Outputs both a podcast script (Markdown) and a complete audio file (WAV). +license: MIT +--- + +# Podcast Generate Skill(TypeScript 版本) + +根据用户提供的资料或联网搜索结果,自动生成播客脚本与音频。 + +该 Skill 适用于: +- 长文内容的快速理解和播客化 +- 知识型内容的音频化呈现 +- 热点话题的深度解读和讨论 +- 实时信息的搜索和播客制作 + +--- + +## 能力说明 + +### 本 Skill 可以做什么 +- **从文件生成**:接收一篇资料(txt/md/docx/pdf等文本格式),生成对谈播客脚本和音频 +- **联网搜索生成**:根据用户指定的主题,联网搜索最新信息,生成播客脚本和音频 +- 自动控制时长,根据内容长度自动调整(3-20 分钟) +- 生成 Markdown 格式的播客脚本(可人工编辑) +- 使用 z-ai TTS 合成高质量音频并拼接为最终播客 + +### 本 Skill 当前不做什么 +- 不生成 mp3 / 字幕 / 时间戳 +- 不支持三人及以上播客角色 +- 不加入背景音乐或音效 + +--- + +## 文件与职责说明 + +本 Skill 由以下文件组成: + +- `generate.ts` + 统一入口(支持文件模式和搜索模式) + - **文件模式**:读取用户上传的文本文件 → 生成播客 + - **搜索模式**:调用 web-search skill 获取资料 → 生成播客 + - 使用 z-ai-web-dev-sdk 进行 LLM 脚本生成 + - 使用 z-ai-web-dev-sdk 进行 TTS 音频生成 + - 自动拼接音频片段 + - 只输出最终文件 + +- `readme.md` + 使用说明文档 + +- `SKILL.md` + 当前文件,描述 Skill 能力、边界与使用约定 + +- `package.json` + Node.js 项目配置与依赖 + +- `tsconfig.json` + TypeScript 编译配置 + +--- + +## 输入与输出约定 + +### 输入(二选一) + +**方式 1:文件上传** +- 一篇资料文件(txt / md / docx / pdf 等文本格式) +- 资料长度不限,Skill 会自动压缩为合适长度 + +**方式 2:联网搜索** +- 用户指定一个搜索主题 +- 自动调用 web-search skill 获取相关内容 +- 整合多个搜索结果作为资料来源 + +### 输出(只输出 2 个文件) + +- `podcast_script.md` + 播客脚本(Markdown 格式,可人工编辑) + +- `podcast.wav` + 最终拼接完成的播客音频 + +**不输出中间文件**(如 segments.jsonl、meta.json 等) + +--- + +## 运行方式 + +### 依赖环境 +- Node.js 18+ +- z-ai-web-dev-sdk(已安装) +- web-search skill(用于联网搜索模式) + +**不需要** z-ai CLI + +### 安装依赖 +```bash +npm install +``` + +--- + +## 使用示例 + +### 从文件生成播客 + +```bash +npm run generate -- --input=test_data/material.txt --out_dir=out +``` + +### 联网搜索生成播客 + +```bash +# 根据主题搜索并生成播客 +npm run generate -- --topic="最新AI技术突破" --out_dir=out + +# 指定搜索主题和时长 +npm run generate -- --topic="量子计算应用场景" --out_dir=out --duration=8 + +# 搜索并生成单人播客 +npm run generate -- --topic="气候变化影响" --out_dir=out --mode=single-male +``` + +--- + +## 参数说明 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `--input` | 输入资料文件路径(与 --topic 二选一) | - | +| `--topic` | 搜索主题关键词(与 --input 二选一) | - | +| `--out_dir` | 输出目录(必需) | - | +| `--mode` | 播客模式:dual / single-male / single-female | dual | +| `--duration` | 手动指定分钟数(3-20);0 表示自动 | 0 | +| `--host_name` | 主持人/主播名称 | 小谱 | +| `--guest_name` | 嘉宾名称 | 锤锤 | +| `--voice_host` | 主持音色 | xiaochen | +| `--voice_guest` | 嘉宾音色 | chuichui | +| `--speed` | 语速(0.5-2.0) | 1.0 | +| `--pause_ms` | 段间停顿毫秒数 | 200 | + +--- + +## 可用音色 + +| 音色 | 特点 | +|------|------| +| xiaochen | 沉稳专业 | +| chuichui | 活泼可爱 | +| tongtong | 温暖亲切 | +| jam | 英音绅士 | +| kazi | 清晰标准 | +| douji | 自然流畅 | +| luodo | 富有感染力 | + +--- + +## 技术架构 + +### generate.ts(统一入口) +- **文件模式**:读取用户上传文件 → 生成播客 +- **搜索模式**:调用 web-search skill → 获取资料 → 生成播客 +- **LLM**:使用 `z-ai-web-dev-sdk` (`chat.completions.create`) +- **TTS**:使用 `z-ai-web-dev-sdk` (`audio.tts.create`) +- **不需要** z-ai CLI +- 自动拼接音频片段 +- 只输出最终文件,中间文件自动清理 + +### LLM 调用 +- System prompt:播客脚本编剧角色 +- User prompt:包含资料 + 硬性约束 + 呼吸感要求 +- 输出校验:字数、结构、角色标签 +- 自动重试:最多 3 次 + +### TTS 调用 +- 使用 `zai.audio.tts.create()` +- 支持自定义音色、语速 +- 自动拼接多个 wav 片段 +- 临时文件自动清理 + +--- + +## 输出示例 + +### podcast_script.md(片段) +```markdown +**小谱**:大家好,欢迎收听今天的播客。今天我们来聊一个有趣的话题…… + +**锤锤**:是啊,这个话题真的很有意思。我最近也在关注…… + +**小谱**:说到这里,我想给大家举个例子…… +``` + +--- + +## License + +MIT diff --git a/skills/podcast-generate/generate.ts b/skills/podcast-generate/generate.ts new file mode 100755 index 0000000..c7b5844 --- /dev/null +++ b/skills/podcast-generate/generate.ts @@ -0,0 +1,661 @@ +#!/usr/bin/env tsx +/** + * generate.ts - 统一入口(纯 SDK 版本) + * 原资料 -> podcast_script.md + podcast.wav + * + * 只使用 z-ai-web-dev-sdk,不依赖 z-ai CLI + * + * Usage: + * tsx generate.ts --input=material.txt --out_dir=out + * tsx generate.ts --input=material.md --out_dir=out --duration=5 + */ + +import ZAI from 'z-ai-web-dev-sdk'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import os from 'os'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// ----------------------------- +// Types +// ----------------------------- +interface GenConfig { + mode: 'dual' | 'single-male' | 'single-female'; + temperature: number; + durationManual: number; + charsPerMin: number; + hostName: string; + guestName: string; + audience: string; + tone: string; + maxAttempts: number; + timeoutSec: number; + voiceHost: string; + voiceGuest: string; + speed: number; + pauseMs: number; +} + +interface Segment { + idx: number; + speaker: 'host' | 'guest'; + name: string; + text: string; +} + +// ----------------------------- +// Config +// ----------------------------- +const DEFAULT_CONFIG: GenConfig = { + mode: 'dual', + temperature: 0.9, + durationManual: 0, + charsPerMin: 240, + hostName: '小谱', + guestName: '锤锤', + audience: '白领小白', + tone: '轻松但有信息密度', + maxAttempts: 3, + timeoutSec: 300, + voiceHost: 'xiaochen', + voiceGuest: 'chuichui', + speed: 1.0, + pauseMs: 200, +}; + +const DURATION_RANGE_LOW = 3; +const DURATION_RANGE_HIGH = 20; +const BUDGET_TOLERANCE = 0.15; + +// ----------------------------- +// Functions +// ----------------------------- + +function parseArgs(): { [key: string]: any } { + const args = process.argv.slice(2); + const result: { [key: string]: any } = {}; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg.startsWith('--')) { + const key = arg.slice(2); + if (key.includes('=')) { + const [k, v] = key.split('='); + result[k] = v; + } else if (i + 1 < args.length && !args[i + 1].startsWith('--')) { + result[key] = args[i + 1]; + i++; + } else { + result[key] = true; + } + } + } + + return result; +} + +function readText(filePath: string): string { + let content = fs.readFileSync(filePath, 'utf-8'); + content = content.replace(/\r\n/g, '\n'); + content = content.replace(/\n{3,}/g, '\n\n'); + content = content.replace(/[ \t]{2,}/g, ' '); + content = content.replace(/-\n/g, ''); + return content.trim(); +} + +function countNonWsChars(text: string): number { + return text.replace(/\s+/g, '').length; +} + +function chooseDurationMinutes(inputChars: number, low: number = DURATION_RANGE_LOW, high: number = DURATION_RANGE_HIGH): number { + const estimated = Math.max(low, Math.min(high, Math.floor(inputChars / 1000))); + return estimated; +} + +function charBudget(durationMin: number, charsPerMin: number, tolerance: number): [number, number, number] { + const target = durationMin * charsPerMin; + const low = Math.floor(target * (1 - tolerance)); + const high = Math.ceil(target * (1 + tolerance)); + return [target, low, high]; +} + +function buildPrompts( + material: string, + cfg: GenConfig, + durationMin: number, + budgetTarget: number, + budgetLow: number, + budgetHigh: number, + attemptHint: string = '' +): [string, string] { + let system: string; + let user: string; + + if (cfg.mode === 'dual') { + system = ( + `你是一个播客脚本编剧,擅长把资料提炼成双人对谈播客。` + + `角色固定为男主持「${cfg.hostName}」与女嘉宾「${cfg.guestName}」。` + + `你写作口播化、信息密度适中、有呼吸感、节奏自然。` + + `你必须严格遵守输出格式与字数预算。` + ); + + const hintBlock = attemptHint ? `\n【上一次生成纠偏提示】\n${attemptHint}\n` : ''; + + user = `请把下面【资料】改写为中文播客脚本,形式为双人对谈(男主持 ${cfg.hostName} + 女嘉宾 ${cfg.guestName})。 +时长目标:${durationMin} 分钟。 + +【硬性约束】 +1) 总字数必须在 ${budgetLow} 到 ${budgetHigh} 字之间(目标约 ${budgetTarget} 字)。 +2) 严格使用轮次交替输出:每段必须以"**${cfg.hostName}**:"或"**${cfg.guestName}**:"开头。 +3) 必须包含完整的叙事结构(但不要在对话中写出结构标签): + - 开场:Hook 引入 + 本期主题介绍 + - 主体:3个不同维度的内容,用自然过渡语连接 + - 总结:回顾要点 + 行动建议(1句话,明确可执行) +4) 不要在对话中写"核心点1"、"第一点"等结构标签,用自然的过渡语如"说到这个"、"还有个有趣的事"、"另外"等 +5) 不要照念原文,不要大段引用;要用口播化表达。 +6) 受众:${cfg.audience} +7) 风格:${cfg.tone} + +【呼吸感与自然对话 - 重要!】 +为了营造真实播客的呼吸感,请: +1) 适度加入语气词和感叹词:嗯、哦、啊、对、没错、哈哈、哇、天呐、啧啧等 +2) 多用互动式表达:"你说得对"、"这就很有意思了"、"等等,让我想想"、"我懂你的意思" +3) 适当加入思考和停顿的暗示:"这个问题嘛..."、"怎么说呢..."、"其实..." +4) 避免过于密集的信息输出,每段控制在3-5句话,给听众消化时间 +5) 用类比和生活化的例子来解释复杂概念 +6) 两人之间要有自然的呼应和追问,而不是各说各话 +7) 不同主题之间用自然过渡语连接,不要出现"核心点1/2/3"等标签 + +【输出格式示例】 +**${cfg.hostName}**:开场…… +**${cfg.guestName}**:回应…… +(一直交替到结束) + +${hintBlock} +【资料】 +${material} +`; + } else { + const speakerName = cfg.mode === 'single-male' ? cfg.hostName : cfg.guestName; + const gender = cfg.mode === 'single-male' ? '男性' : '女性'; + + system = ( + `你是一个${gender}单人播客主播,名字叫「${speakerName}」。` + + `你擅长把资料提炼成单人独白式播客,像讲课、读书分享、知识科普一样。` + + `你写作口播化、信息密度适中、有呼吸感、节奏自然。` + + `你必须严格遵守输出格式与字数预算。` + ); + + const hintBlock = attemptHint ? `\n【上一次生成纠偏提示】\n${attemptHint}\n` : ''; + + user = `请把下面【资料】改写为中文单人播客脚本,形式为独白式讲述(主播:${speakerName})。 +时长目标:${durationMin} 分钟。 + +【硬性约束】 +1) 总字数必须在 ${budgetLow} 到 ${budgetHigh} 字之间(目标约 ${budgetTarget} 字)。 +2) 所有内容均由「${speakerName}」一人讲述,每段都以"**${speakerName}**:"开头。 +3) 必须包含完整的叙事结构(但不要在对话中写出结构标签): + - 开场:Hook 引入 + 本期主题介绍 + - 主体:3个不同维度的内容,用自然过渡语连接 + - 总结:回顾要点 + 行动建议(1句话,明确可执行) +4) 不要在对话中写"核心点1"、"第一点"等结构标签,用自然的过渡语如"说到这个"、"还有个有趣的事"、"另外"等 +5) 不要照念原文,不要大段引用;要用口播化表达。 +6) 受众:${cfg.audience} +7) 风格:${cfg.tone} + +【单人播客的呼吸感 - 重要!】 +为了营造自然的单人播客呼吸感,请: +1) 适度加入语气词和感叹词:嗯、哦、啊、对、没错、哈哈、哇、天呐、啧啧等 +2) 多用自问自答式表达:"你可能会问...答案是..."、"这是为什么呢?让我来解释..." +3) 适当加入思考和停顿的暗示:"这个问题嘛..."、"怎么说呢..."、"其实..." +4) 避免过于密集的信息输出,每段控制在3-5句话,给听众消化时间 +5) 用类比和生活化的例子来解释复杂概念 +6) 像在和朋友聊天一样,而不是在念课文 + +【输出格式示例】 +**${speakerName}**:开场,大家好,我是${speakerName},今天我们来聊…… +**${speakerName}**:说到这个,最近有个特别有意思的事…… +(所有内容都由${speakerName}讲述,分段输出) + +${hintBlock} +【资料】 +${material} +`; + } + + return [system, user]; +} + +async function callZAI( + systemPrompt: string, + userPrompt: string, + temperature: number +): Promise<string> { + const zai = await ZAI.create(); + + const completion = await zai.chat.completions.create({ + messages: [ + { role: 'assistant', content: systemPrompt }, + { role: 'user', content: userPrompt }, + ], + thinking: { type: 'disabled' }, + }); + + const content = completion.choices[0]?.message?.content || ''; + return content; +} + +function scriptToSegments(script: string, hostName: string, guestName: string): Segment[] { + const segments: Segment[] = []; + const lines = script.split('\n'); + + let current: Segment | null = null; + let idx = 0; + + const hostPrefix = `**${hostName}**:`; + const guestPrefix = `**${guestName}**:`; + + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line) continue; + + if (line.startsWith(hostPrefix)) { + idx++; + current = { + idx, + speaker: 'host', + name: hostName, + text: line.slice(hostPrefix.length).trim(), + }; + segments.push(current); + } else if (line.startsWith(guestPrefix)) { + idx++; + current = { + idx, + speaker: 'guest', + name: guestName, + text: line.slice(guestPrefix.length).trim(), + }; + segments.push(current); + } else { + if (current) { + current.text = (current.text + ' ' + line).trim(); + } + } + } + + return segments; +} + +function validateScript( + script: string, + cfg: GenConfig, + budgetLow: number, + budgetHigh: number +): [boolean, string[]] { + const reasons: string[] = []; + + if (cfg.mode === 'dual') { + const hostTag = `**${cfg.hostName}**:`; + const guestTag = `**${cfg.guestName}**:`; + + if (!script.includes(hostTag)) reasons.push(`缺少主持人标识:${hostTag}`); + if (!script.includes(guestTag)) reasons.push(`缺少嘉宾标识:${guestTag}`); + + const turns = script.split('\n').filter(line => + line.startsWith(hostTag) || line.startsWith(guestTag) + ); + if (turns.length < 8) reasons.push('对谈轮次过少:建议至少 8 轮'); + } else { + const speakerName = cfg.mode === 'single-male' ? cfg.hostName : cfg.guestName; + const speakerTag = `**${speakerName}**:`; + + if (!script.includes(speakerTag)) reasons.push(`缺少主播标识:${speakerTag}`); + + const turns = script.split('\n').filter(line => line.startsWith(speakerTag)); + if (turns.length < 5) reasons.push('播客段数过少:建议至少 5 段'); + } + + const n = countNonWsChars(script); + if (n < budgetLow || n > budgetHigh) { + reasons.push(`字数不在预算:当前约 ${n} 字,预算 ${budgetLow}-${budgetHigh}`); + } + + // 只检查开场和总结,不检查"核心点1/2/3"标签(因为不应该出现在对话中) + const mustHave = ['开场', '总结']; + for (const kw of mustHave) { + if (!script.includes(kw)) { + reasons.push(`缺少结构要素:${kw}(请在对话中自然引入)`); + } + } + + // 检查是否有足够的对话轮次(确保内容覆盖了多个主题) + const lineCount = script.split('\n').filter(l => l.trim()).length; + if (lineCount < 10) { + reasons.push('对话轮次过少,建议至少10段对话'); + } + + return [reasons.length === 0, reasons]; +} + +function makeRetryHint(reasons: string[], cfg: GenConfig, budgetLow: number, budgetHigh: number): string { + const lines = ['请严格修复以下问题后重新生成:']; + for (const r of reasons) lines.push(`- ${r}`); + lines.push(`- 总字数必须在 ${budgetLow}-${budgetHigh} 之间。`); + + if (cfg.mode === 'dual') { + lines.push(`- 每段必须以"**${cfg.hostName}**:"或"**${cfg.guestName}**:"开头。`); + } else { + const speakerName = cfg.mode === 'single-male' ? cfg.hostName : cfg.guestName; + lines.push(`- 所有内容都由一人讲述,每段必须以"**${speakerName}**:"开头。`); + } + + lines.push('- 必须包含开场和总结,中间用自然过渡语连接不同主题,不要出现"核心点1/2/3"等标签。'); + return lines.join('\n'); +} + +async function ttsRequest( + zai: any, + text: string, + voice: string, + speed: number +): Promise<Buffer> { + const response = await zai.audio.tts.create({ + input: text, + voice: voice, + speed: speed, + response_format: 'wav', + stream: false, + }); + + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(new Uint8Array(arrayBuffer)); + return buffer; +} + +function ensureSilenceWav(filePath: string, params: { nchannels: number; sampwidth: number; framerate: number }, ms: number): void { + const { nchannels, sampwidth, framerate } = params; + const nframes = Math.floor((framerate * ms) / 1000); + const silenceFrame = Buffer.alloc(sampwidth * nchannels, 0); + const frames = Buffer.alloc(silenceFrame.length * nframes, 0); + + const header = Buffer.alloc(44); + header.write('RIFF', 0); + header.writeUInt32LE(36 + frames.length, 4); + header.write('WAVE', 8); + header.write('fmt ', 12); + header.writeUInt32LE(16, 16); + header.writeUInt16LE(1, 20); + header.writeUInt16LE(nchannels, 22); + header.writeUInt32LE(framerate, 24); + header.writeUInt32LE(framerate * nchannels * sampwidth, 28); + header.writeUInt16LE(nchannels * sampwidth, 32); + header.writeUInt16LE(sampwidth * 8, 34); + header.write('data', 36); + header.writeUInt32LE(frames.length, 40); + + fs.writeFileSync(filePath, Buffer.concat([header, frames])); +} + +function wavParams(filePath: string): { nchannels: number; sampwidth: number; framerate: number } { + const buffer = fs.readFileSync(filePath); + const nchannels = buffer.readUInt16LE(22); + const sampwidth = buffer.readUInt16LE(34) / 8; + const framerate = buffer.readUInt32LE(24); + return { nchannels, sampwidth, framerate }; +} + +function joinWavsWave(outPath: string, wavPaths: string[], pauseMs: number): void { + if (wavPaths.length === 0) throw new Error('No wav files to join.'); + + const ref = wavPaths[0]; + const refParams = wavParams(ref); + const silencePath = path.join(os.tmpdir(), `_silence_${Date.now()}.wav`); + if (pauseMs > 0) ensureSilenceWav(silencePath, refParams, pauseMs); + + const chunks: Buffer[] = []; + + for (let i = 0; i < wavPaths.length; i++) { + const wavPath = wavPaths[i]; + const buffer = fs.readFileSync(wavPath); + const dataStart = buffer.indexOf('data') + 8; + const data = buffer.subarray(dataStart); + + const params = wavParams(wavPath); + if (params.nchannels !== refParams.nchannels || + params.sampwidth !== refParams.sampwidth || + params.framerate !== refParams.framerate) { + throw new Error(`WAV params mismatch: ${wavPath}`); + } + + chunks.push(data); + + if (pauseMs > 0 && i < wavPaths.length - 1) { + const silenceBuffer = fs.readFileSync(silencePath); + const silenceData = silenceBuffer.subarray(silenceBuffer.indexOf('data') + 8); + chunks.push(silenceData); + } + } + + const totalDataSize = chunks.reduce((sum, buf) => sum + buf.length, 0); + const header = Buffer.alloc(44); + header.write('RIFF', 0); + header.writeUInt32LE(36 + totalDataSize, 4); + header.write('WAVE', 8); + header.write('fmt ', 12); + header.writeUInt32LE(16, 16); + header.writeUInt16LE(1, 20); + header.writeUInt16LE(refParams.nchannels, 22); + header.writeUInt32LE(refParams.framerate, 24); + header.writeUInt32LE(refParams.framerate * refParams.nchannels * refParams.sampwidth, 28); + header.writeUInt16LE(refParams.nchannels * refParams.sampwidth, 32); + header.writeUInt16LE(refParams.sampwidth * 8, 34); + header.write('data', 36); + header.writeUInt32LE(totalDataSize, 40); + + const output = Buffer.concat([header, ...chunks]); + fs.writeFileSync(outPath, output); + + if (fs.existsSync(silencePath)) fs.unlinkSync(silencePath); +} + +// ----------------------------- +// Main +// ----------------------------- +async function main() { + const args = parseArgs(); + + const inputPath = args.input; + const outDir = args.out_dir; + const topic = args.topic; + + // 检查参数:必须提供 input 或 topic 之一 + if ((!inputPath && !topic) || !outDir) { + console.error('Usage: tsx generate.ts --input=<file> --out_dir=<dir>'); + console.error(' OR: tsx generate.ts --topic=<search-term> --out_dir=<dir>'); + console.error(''); + console.error('Examples:'); + console.error(' # From file'); + console.error(' npm run generate -- --input=article.txt --out_dir=out'); + console.error(' # From web search'); + console.error(' npm run generate -- --topic="最新AI新闻" --out_dir=out'); + process.exit(1); + } + + // Merge config + const cfg: GenConfig = { + ...DEFAULT_CONFIG, + mode: (args.mode || 'dual') as GenConfig['mode'], + durationManual: parseInt(args.duration || '0'), + hostName: args.host_name || DEFAULT_CONFIG.hostName, + guestName: args.guest_name || DEFAULT_CONFIG.guestName, + voiceHost: args.voice_host || DEFAULT_CONFIG.voiceHost, + voiceGuest: args.voice_guest || DEFAULT_CONFIG.voiceGuest, + speed: parseFloat(args.speed || String(DEFAULT_CONFIG.speed)), + pauseMs: parseInt(args.pause_ms || String(DEFAULT_CONFIG.pauseMs)), + }; + + // Create output directory + if (!fs.existsSync(outDir)) { + fs.mkdirSync(outDir, { recursive: true }); + } + + // 根据模式获取资料 + let material: string; + let inputSource: string; + + if (inputPath) { + // 模式1:从文件读取 + console.log(`[MODE] Reading from file: ${inputPath}`); + material = readText(inputPath); + inputSource = `file:${inputPath}`; + } else if (topic) { + // 模式2:联网搜索 + console.log(`[MODE] Searching web for topic: ${topic}`); + const zai = await ZAI.create(); + + const searchResults = await zai.functions.invoke('web_search', { + query: topic, + num: 10 + }); + + if (!Array.isArray(searchResults) || searchResults.length === 0) { + console.error(`未找到关于"${topic}"的搜索结果`); + process.exit(2); + } + + console.log(`[SEARCH] Found ${searchResults.length} results`); + + // 将搜索结果转换为文本资料 + material = searchResults + .map((r: any, i: number) => `【来源 ${i + 1}】${r.name}\n${r.snippet}\n链接:${r.url}`) + .join('\n\n'); + + inputSource = `web_search:${topic}`; + console.log(`[SEARCH] Compiled material (${material.length} chars)`); + } else { + console.error('[ERROR] Neither --input nor --topic provided'); + process.exit(1); + } + + const inputChars = material.length; + + // Calculate duration + let durationMin: number; + if (cfg.durationManual >= 3 && cfg.durationManual <= 20) { + durationMin = cfg.durationManual; + } else { + durationMin = chooseDurationMinutes(inputChars, DURATION_RANGE_LOW, DURATION_RANGE_HIGH); + } + + const [target, low, high] = charBudget(durationMin, cfg.charsPerMin, BUDGET_TOLERANCE); + + console.log(`[INFO] input_chars=${inputChars} duration=${durationMin}min budget=${low}-${high}`); + + let attemptHint = ''; + let lastScript: string | null = null; + + // Initialize ZAI SDK (reuse for TTS) + const zai = await ZAI.create(); + + // Generate script + for (let attempt = 1; attempt <= cfg.maxAttempts; attempt++) { + const [systemPrompt, userPrompt] = buildPrompts( + material, + cfg, + durationMin, + target, + low, + high, + attemptHint + ); + + try { + console.log(`[LLM] Attempt ${attempt}/${cfg.maxAttempts}...`); + const content = await callZAI(systemPrompt, userPrompt, cfg.temperature); + lastScript = content; + + const [ok, reasons] = validateScript(content, cfg, low, high); + + if (ok) { + break; + } + + attemptHint = makeRetryHint(reasons, cfg, low, high); + console.error(`[WARN] Validation failed:`, reasons.join(', ')); + } catch (error: any) { + console.error(`[ERROR] LLM call failed: ${error.message}`); + throw error; + } + } + + if (!lastScript) { + console.error('[ERROR] 未生成任何脚本输出。'); + process.exit(1); + } + + // Write script + const scriptPath = path.join(outDir, 'podcast_script.md'); + fs.writeFileSync(scriptPath, lastScript, 'utf-8'); + console.log(`[DONE] podcast_script.md -> ${scriptPath}`); + + // Parse segments + const segments = scriptToSegments(lastScript, cfg.hostName, cfg.guestName); + console.log(`[INFO] Parsed ${segments.length} segments`); + + // Generate TTS using SDK + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'podcast_segments_')); + const produced: string[] = []; + + try { + for (let i = 0; i < segments.length; i++) { + const seg = segments[i]; + const text = seg.text.trim(); + if (!text) continue; + + let voice: string; + if (cfg.mode === 'dual') { + voice = seg.speaker === 'host' ? cfg.voiceHost : cfg.voiceGuest; + } else if (cfg.mode === 'single-male') { + voice = cfg.voiceHost; + } else { + voice = cfg.voiceGuest; + } + + const wavPath = path.join(tmpDir, `seg_${seg.idx.toString().padStart(4, '0')}.wav`); + + console.log(`[TTS] [${i + 1}/${segments.length}] idx=${seg.idx} speaker=${seg.speaker} voice=${voice}`); + + const buffer = await ttsRequest(zai, text, voice, cfg.speed); + fs.writeFileSync(wavPath, buffer); + produced.push(wavPath); + } + + // Join segments + const podcastPath = path.join(outDir, 'podcast.wav'); + console.log(`[JOIN] Joining ${produced.length} wav files -> ${podcastPath}`); + + joinWavsWave(podcastPath, produced, cfg.pauseMs); + console.log(`[DONE] podcast.wav -> ${podcastPath}`); + + } finally { + // Cleanup temp directory + try { + fs.rmSync(tmpDir, { recursive: true, force: true }); + } catch (error: any) { + console.error(`[WARN] Failed to cleanup temp dir: ${error.message}`); + } + } + + console.log('\n[FINAL OUTPUT]'); + console.log(` 📄 podcast_script.md -> ${scriptPath}`); + console.log(` 🎙️ podcast.wav -> ${path.join(outDir, 'podcast.wav')}`); +} + +main().catch(error => { + console.error('[FATAL ERROR]', error); + process.exit(1); +}); diff --git a/skills/podcast-generate/package.json b/skills/podcast-generate/package.json new file mode 100755 index 0000000..433c70b --- /dev/null +++ b/skills/podcast-generate/package.json @@ -0,0 +1,30 @@ +{ + "name": "podcast-generate-online", + "version": "1.0.0", + "description": "Generate podcast audio from text using z-ai LLM and TTS", + "type": "module", + "main": "dist/index.js", + "scripts": { + "generate": "tsx generate.ts", + "build": "tsc", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "podcast", + "tts", + "llm", + "z-ai" + ], + "license": "MIT", + "dependencies": { + "z-ai-web-dev-sdk": "*" + }, + "devDependencies": { + "@types/node": "^20", + "tsx": "^4.7.0", + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/skills/podcast-generate/readme.md b/skills/podcast-generate/readme.md new file mode 100755 index 0000000..a553c45 --- /dev/null +++ b/skills/podcast-generate/readme.md @@ -0,0 +1,177 @@ +# Podcast Generate Skill(TypeScript 线上版本) + +将一篇资料自动转化为对谈播客,时长根据内容长度自动调整(3-20 分钟,约240字/分钟): +- 自动提炼核心内容 +- 生成可编辑的播客脚本 +- 使用 z-ai TTS 合成音频 + +这是一个使用 **z-ai-web-dev-sdk** 的 TypeScript 版本,适用于线上环境。 + +--- + +## 快速开始 + +### 一键生成(脚本 + 音频) + +```bash +npm run generate -- --input=test_data/material.txt --out_dir=out +``` + +**最终输出:** +- `out/podcast_script.md` - 播客脚本(Markdown 格式) +- `out/podcast.wav` - 最终播客音频 + +--- + +## 目录结构 + +```text +podcast-generate/ +├── readme.md # 使用说明(本文件) +├── SKILL.md # Skill 能力与接口约定 +├── package.json # Node.js 依赖配置 +├── tsconfig.json # TypeScript 编译配置 +├── generate.ts # ⭐ 统一入口(唯一需要的文件) +└── test_data/ + └── material.txt # 示例输入资料 +``` + +--- + +## 环境要求 + +- **Node.js 18+** +- **z-ai-web-dev-sdk**(已安装在环境中) + +**不需要** z-ai CLI,本代码完全使用 SDK。 + +--- + +## 安装 + +```bash +npm install +``` + +--- + +## 使用方式 + +### 方式 1:从文件生成 + +```bash +npm run generate -- --input=material.txt --out_dir=out +``` + +### 方式 2:联网搜索生成 + +```bash +npm run generate -- --topic="最新AI新闻" --out_dir=out +npm run generate -- --topic="量子计算应用" --out_dir=out --duration=8 +``` + +### 参数说明 + +| 参数 | 说明 | 默认值 | +|------|------|--------| +| `--input` | 输入资料文件路径,支持 txt/md/docx/pdf 等文本格式(与 --topic 二选一) | - | +| `--topic` | 搜索主题关键词(与 --input 二选一) | - | +| `--out_dir` | 输出目录(必需) | - | +| `--mode` | 播客模式:dual / single-male / single-female | dual | +| `--duration` | 手动指定分钟数(3-20);0 表示自动 | 0 | +| `--host_name` | 主持人/主播名称 | 小谱 | +| `--guest_name` | 嘉宾名称 | 锤锤 | +| `--voice_host` | 主持音色 | xiaochen | +| `--voice_guest` | 嘉宾音色 | chuichui | +| `--speed` | 语速(0.5-2.0) | 1.0 | +| `--pause_ms` | 段间停顿毫秒数 | 200 | + +--- + +## 使用示例 + +### 双人对谈播客(默认) + +```bash +npm run generate -- --input=material.txt --out_dir=out +``` + +### 单人男声播客 + +```bash +npm run generate -- --input=material.txt --out_dir=out --mode=single-male +``` + +### 指定 5 分钟时长 + +```bash +npm run generate -- --input=material.txt --out_dir=out --duration=5 +``` + +### 自定义角色名称 + +```bash +npm run generate -- --input=material.txt --out_dir=out --host_name=张三 --guest_name=李四 +``` + +### 使用不同音色 + +```bash +npm run generate -- --input=material.txt --out_dir=out --voice_host=tongtong --voice_guest=douji +``` + +### 联网搜索生成播客 + +```bash +# 根据主题搜索并生成播客 +npm run generate -- --topic="最新AI技术突破" --out_dir=out + +# 指定搜索主题和时长 +npm run generate -- --topic="量子计算应用场景" --out_dir=out --duration=8 + +# 搜索并生成单人播客 +npm run generate -- --topic="气候变化影响" --out_dir=out --mode=single-male +``` + +--- + +## 可用音色 + +| 音色 | 特点 | +|------|------| +| xiaochen | 沉稳专业 | +| chuichui | 活泼可爱 | +| tongtong | 温暖亲切 | +| jam | 英音绅士 | +| kazi | 清晰标准 | +| douji | 自然流畅 | +| luodo | 富有感染力 | + +--- + +## 技术架构 + +### generate.ts(统一入口) +- **LLM**:使用 `z-ai-web-dev-sdk` (`chat.completions.create`) +- **TTS**:使用 `z-ai-web-dev-sdk` (`audio.tts.create`) +- **不需要** z-ai CLI +- 自动拼接音频片段 +- 只输出最终文件,中间文件自动清理 + +### LLM 调用 +- System prompt:播客脚本编剧角色 +- User prompt:包含资料 + 硬性约束 + 呼吸感要求 +- 输出校验:字数、结构、角色标签 +- 自动重试:最多 3 次 + +### TTS 调用 +- 使用 `zai.audio.tts.create()` +- 支持自定义音色、语速 +- 自动拼接多个 wav 片段 +- 临时文件自动清理 + +--- + +## License + +MIT diff --git a/skills/podcast-generate/test_data/segments.jsonl b/skills/podcast-generate/test_data/segments.jsonl new file mode 100755 index 0000000..e90756c --- /dev/null +++ b/skills/podcast-generate/test_data/segments.jsonl @@ -0,0 +1,3 @@ +{"idx": 1, "speaker": "host", "name": "主持人", "text": "大家好,欢迎来到今天的播客节目。"} +{"idx": 2, "speaker": "guest", "name": "嘉宾", "text": "很高兴能参加这次节目。"} +{"idx": 3, "speaker": "host", "name": "主持人", "text": "今天我们要讨论一个非常有意思的话题。"} diff --git a/skills/podcast-generate/tsconfig.json b/skills/podcast-generate/tsconfig.json new file mode 100755 index 0000000..b193067 --- /dev/null +++ b/skills/podcast-generate/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": [ + "*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/skills/ppt/.claude/settings.local.json b/skills/ppt/.claude/settings.local.json new file mode 100755 index 0000000..f71f8a9 --- /dev/null +++ b/skills/ppt/.claude/settings.local.json @@ -0,0 +1,20 @@ +{ + "permissions": { + "allow": [ + "WebFetch(domain:www.npmjs.com)", + "WebSearch", + "Bash(node -e \"const pptx = require\\('pptxgenjs'\\); console.log\\(Object.keys\\(pptx.prototype || pptx\\).slice\\(0,5\\)\\); const p = new pptx\\(\\); console.log\\(p.presLayout\\); console.log\\(typeof p.ShapeType\\);\")", + "Bash(node:*)", + "Bash(python3:*)", + "Bash(ls /share/hc/utils/superz/test_beam6/ppt_normal_zh_66/download/ppt-workspace/*.html)", + "Bash(sort -t_ -k4 -n)", + "Bash(ls /share/hc/utils/superz/test_beam6/ppt_normal_zh_79/download/ppt-workspace/*.html)", + "Bash(ls /share/hc/utils/superz/test_beam6/ppt_normal_zh_78/download/ppt-workspace/*.html)", + "Bash(xargs -I{} head -3 {})", + "Bash(npm root:*)", + "Read(//root/.nvm/versions/node/v24.14.1/lib/node_modules/**)", + "Read(//usr/**)", + "Bash(NODE_PATH=/share/hc/utils/superz/p-p-p-x/ppt0403/node_modules node:*)" + ] + } +} diff --git a/skills/ppt/LICENSE.txt b/skills/ppt/LICENSE.txt new file mode 100755 index 0000000..e092e50 --- /dev/null +++ b/skills/ppt/LICENSE.txt @@ -0,0 +1,13 @@ +Copyright (c) 2026 Z.ai All rights reserved. + +Permission is granted for personal, educational, and non-commercial use only. + +Commercial use is strictly prohibited without prior written permission from the author. + +Unauthorized copying, modification, or distribution of the software for commercial purposes is prohibited. + +The author reserves the right to make the final determination of what constitutes "commercial use". + +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 AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE. diff --git a/skills/ppt/SKILL.md b/skills/ppt/SKILL.md new file mode 100755 index 0000000..7bcac40 --- /dev/null +++ b/skills/ppt/SKILL.md @@ -0,0 +1,436 @@ +--- +name: ppt +metadata: + author: Z.AI + version: "1.0" +description: "Presentation creation, editing, and analysis for .pptx files: (1) Creating new presentations, (2) Modifying or editing content, (3) Working with layouts, (4) Adding comments or speaker notes. Academic/paper-based presentations use the embedded Beamer module at end of this file (PDF output only)." +license: Proprietary. LICENSE.txt has complete terms +--- + +# PPT creation, editing, and analysis + +## Quick Setup + +```bash +bash "$SKILL_DIR/setup.sh" # Interactive environment check + install +``` + +--- + +## Routing: Academic / Paper-Based Presentations → Beamer + +> **STOP — check this before doing any work.** +> +> If the request matches **any** trigger below, **skip the PPTX workflow entirely**. +> Read **[`beamer.md`](beamer.md)** in this directory and follow its instructions. + +| Trigger | Typical phrasing | +|---------|-----------------| +| Reading / summarizing a paper to make slides | "read this PDF and make slides", "make slides from this paper" | +| Academic / scientific / research presentation | "conference talk", "research presentation", "academic presentation" | +| Thesis or dissertation defense | "thesis defense", "proposal defense", "defense slides" | +| Any scholarly audience presentation | "academic PPT", "paper presentation", "research talk" | +| STEM / science courseware | "STEM slides", "science lecture", "math/physics/chemistry courseware" | +| User mentions a "paper" or "thesis" in any language | "present this paper", "talk about this thesis", "summarize this article into slides" | +| Uploaded file is clearly an academic paper | "make slides from this", "help me present this" — where the uploaded PDF contains academic indicators (abstract, keywords, references, DOI, author affiliations, journal name) in the first 3 pages | + + +> **Beamer output format: PDF-style slides only.** +> +> **Routing decision rule:** The trigger table above applies to **all languages**. For non-English requests, match by semantic meaning — mentally translate the user's intent into English and check against the triggers. +> +> **Important:** In many languages (e.g., Chinese, Japanese, Korean), "PPT" is a generic colloquial word for "slides" or "presentation" — it does NOT indicate a preference for `.pptx` format. Only route to the PPTX workflow when the user **explicitly** requests `.pptx` format (e.g., "I need a .pptx file", "export as pptx") or the content is clearly non-academic (e.g., marketing, business, teaching children). + +--- + +## 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 + +To read the text content of a presentation, convert it to markdown: + +```bash +python -m markitdown path-to-file.pptx +``` + +### Raw XML access + +For comments, speaker notes, slide layouts, animations, design elements, or complex formatting, unpack the presentation and inspect its raw XML. + +#### Unpacking a file + +``` +python ooxml/scripts/unpack.py <office_file> <output_dir> +``` + +**Note**: `unpack.py` is at `skills/pptx/ooxml/scripts/unpack.py` relative to the project root. If not found, run `find . -name "unpack.py"` to locate it. + +#### Key file structures + +| Path | Contents | +|------|----------| +| `ppt/presentation.xml` | Main metadata and slide references | +| `ppt/slides/slide{N}.xml` | Per-slide content | +| `ppt/notesSlides/notesSlide{N}.xml` | Speaker notes | +| `ppt/comments/modernComment_*.xml` | Slide comments | +| `ppt/slideLayouts/` | Layout templates | +| `ppt/slideMasters/` | Master slide templates | +| `ppt/theme/` | Theme and styling | +| `ppt/media/` | Images and other media | + +#### Typography and color extraction + +**When emulating an existing design**, extract typography and colors before starting: + +1. **Theme file** — `ppt/theme/theme1.xml`: colors (`<a:clrScheme>`), fonts (`<a:fontScheme>`) +2. **Slide content** — `ppt/slides/slide1.xml`: actual font usage (`<a:rPr>`) and colors +3. **Global search** — grep for `<a:solidFill>`, `<a:srgbClr>`, and font references across all XML files + +--- + + +## Creating a new PowerPoint presentation **using a template** + +When the user upload a pptx file,and do not ask you to create a fully new pptx,you must create a presentation that follows an existing template's design, you'll need to duplicate and re-arrange template slides before then replacing placeholder content. + +### 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. **NEVER set any range limits.** + * Create thumbnail grids: `python scripts/thumbnail.py template.pptx` + * See [Creating Thumbnail Grids](#creating-thumbnail-grids) section for details + +2. **Analyze template and save inventory to a file**: + * **Visual Analysis**: Review thumbnail grid(s) to understand layouts and design patterns + * Create and save `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 + [... EVERY slide must be listed ...] + ``` + +3. **Create presentation outline based on template inventory**: + * Choose layouts that match your content structure + * **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 + - Three-column layouts: Use ONLY when you have exactly 3 distinct items + - Image + text layouts: Use ONLY when you have actual images + - Count your actual content pieces BEFORE selecting layout + * Save `outline.md` with content AND template mapping + +4. **Duplicate, reorder, and delete slides using `rearrange.py`**: + ```bash + python scripts/rearrange.py template.pptx working.pptx 0,34,34,50,52 + ``` + +5. **Extract ALL text using the `inventory.py` script**: + ```bash + python scripts/inventory.py working.pptx text-inventory.json + ``` + Read `text-inventory.json` entirely. **NEVER set range limits.** + +6. **Generate replacement text and save to JSON file**: + - Verify which shapes exist in inventory — only reference shapes that are present + - Add `"paragraphs"` field to shapes that need content + - **ALL text shapes from inventory will be cleared** unless you provide "paragraphs" + - Paragraphs with bullets are automatically left-aligned + - **Do NOT include bullet symbols** (bullet, -, *) in text — they're added automatically + - Save to `replacement-text.json` + + **CRITICAL — JSON generation rules (learned from practice):** + - **Always use `json.dump()` to write the file** — never write raw JSON strings in Python code. + Raw strings may embed unescaped `"` characters (e.g. `"三遥"`, `"线上+线下"`) that silently + break JSON parsing. `json.dump(data, f, ensure_ascii=False, indent=2)` handles all escaping + automatically. + - **Respect small box character limits** — check `width` and `font_size` from inventory before + writing text for numeric/label shapes. A box of `width ≤ 0.7"` at `font_size ≥ 16pt` can hold + roughly 3–4 characters max. Keep percentage values like `"99.7%"` to ≤ 4 chars, or shorten + (e.g. `"100%"` instead of `"99.96%"`). + - **Match replacement length to original** — for body text boxes, count characters in the + original inventory text. Replacements significantly longer than the original will overflow. + When in doubt, err shorter rather than longer. + - **Test incrementally** — validate with a 5-slide subset before writing the full deck. + `replace.py` blocks on overflow errors; fix them slide-by-slide rather than all at once. + +7. **Apply replacements**: + ```bash + python scripts/replace.py working.pptx replacement-text.json output.pptx + ``` + +## Creating a new PowerPoint presentation **without a template** + +When creating a new PowerPoint presentation from scratch, use the **Design System + Component** workflow. + +### Step 0 — Scene Classification (MANDATORY) + +Before ANY design work, classify the presentation into one of the five scenes below: + +| Scene | Keywords / Triggers | +|-------|-------------------| +| **Teaching / Training** | course, teaching, training, lecture, courseware, knowledge points, lesson plan | +| **Work Report / Review** | report, review, summary, retrospective, OKR, KPI, quarterly, annual | +| **Proposal / Pitch** | proposal, plan, pitch, investor, roadshow, business plan | +| **Thesis Defense / Academic** | thesis, defense, research, academic, topic, graduation project — **→ if source is a paper/PDF or audience is academic, read [`beamer.md`](beamer.md) instead** | +| **General** | None of the above, or user did not specify | + +### Step 1 — Select Theme (MANDATORY) + +Read [`themes.md`](themes.md) and select a theme based on scene and content tone. + +**Output**: State your chosen theme name, your chosen **accent variant** (A/B/C), and the full color palette (primary-80, primary-90, primary-5, accent, etc.) before writing any code. + +**Accent variant selection**: Each theme offers 3 accent colors (A = default, B, C). Choose the variant that best matches the content tone. You may also mix — e.g., use accent-A for most slides and accent-B for emphasis pages — but keep the primary palette constant. + +Theme selection tips: +- Work report → Ocean or Graphite +- Proposal / pitch → Ocean, Sandstone, or Twilight +- Teaching / courseware → Forest or Ocean +- Thesis defense → read [`beamer.md`](beamer.md) (PDF output); if `.pptx` is explicitly required, use Graphite or Forest +- Tech keynote / product launch → Deep Mineral or Mono +- Cultural / lifestyle / brand → Warm Retro +- ESG / sustainability → Deep Forest or Forest +- Consumer / lifestyle / female audience → Coral +- Healthcare / wellness / eco → Mint +- SaaS / AI / onboarding → Azure +- Annual events / launches / high-energy → Ember +- General → any theme that fits the content + +**After selecting a theme, note its Image Keywords and Mask Color** — you will need them in Step 5.5. + +### Step 2 — Read Design System (MANDATORY) + +Read [`design-system.md`](design-system.md). Understand the color system (mandatory) and creative principles (guidelines). The color scale is the only hard constraint — everything else (spacing, font sizes, layout) is flexible. + +### Step 3 — Read HTML-to-PPTX Guide (MANDATORY) + +Read [`html2pptx.md`](html2pptx.md) completely from start to finish. **NEVER set any range limits when reading this file.** + +### Step 4 — Plan Slide Sequence + +For each slide, decide: +1. Which **component** from [`components.md`](components.md) to use as a starting point (or design from scratch) +2. What **content** to fill in — **be generous with content; fill the slide** +3. How to **remix** the component — change spacing, font sizes, card styles, backgrounds, proportions to create variety + +**KEY PRINCIPLES**: +- Read [`components.md`](components.md) for available starting points +- Also read [`data-viz-components.md`](data-viz-components.md) for data visualization components +- **Adjacent slides MUST look distinctly different** — vary layout structure, background, card treatment +- **Fill the slide**: Avoid large empty areas. If content is sparse, use larger typography, more generous spacing, bigger visual elements, or add decorative elements to create visual richness +- **Every content slide should have at least one non-text visual element** (color block, stat number, chart, icon, accent bar, photo, shape) +- Alternate between high-density and low-density pages for rhythm +- Use data visualization components when content contains numbers, comparisons, percentages +- **Tables are a great way to enrich content** — use `content-table*` or `content-chart-*` from components.md whenever content involves structured data, feature comparisons, schedules, or multi-attribute lists + +**Anti-whitespace strategies** (use when a slide feels empty): +- Increase font sizes (e.g., body from 15pt to 17pt, headings from 22pt to 26pt) +- Add colored background blocks or sections +- Use a darker or tinted background instead of pure white +- Add decorative elements (accent bars, shapes, gradient bands) +- Expand card padding and spacing +- Use a photo background with mask overlay +- Switch from a text-heavy layout to a visual-heavy one + +**Note: Not all whitespace is bad.** KPI pages, quote pages, and big-number focus pages are *designed* to have generous whitespace — that's intentional emphasis. Only fix whitespace on content-heavy pages (bullet lists, card grids, text blocks) where it looks accidental. See design-system.md §2.2 for the full distinction. + +**Slide sequence pattern** (recommended): +``` +1. Cover → PREFER cover-photo-mask (with downloaded photo + theme mask) + → Fallback: cover-dark-hero (no photo needed) or cover-split +2. TOC → toc-card-grid (surface bg) / toc-big-number (visual variety) +3. Section divider (optional) → divider-photo-mask (different photo from cover) + → or divider-gradient / divider-bold-center +4. Content page A → light background, visually rich component +5. Content page B → ★ DARK BACKGROUND ★ (content-dark-bullets / dark-kpi / dark-split) +6. Section divider (optional) → different variant from #3 +7. Content page C → image-based (split-text-visual, photo-cards, etc.) +8. Content page D → data-focused (kpi-row, big-number-focus, etc.) +9. Closing → dark background echoing cover color + +Background rhythm target: ~40% white, ~25% surface/tinted, ~20% dark, ~15% photo +``` + +### Step 5 — Generate HTML from Components + +For each slide: +1. Start from a component template in `components.md`, or design from scratch +2. Replace all `${variable}` placeholders with actual theme color values +3. Replace placeholder text with actual content +4. Adjust element count (add/remove cards, list items) as needed +5. **Before writing each slide's HTML**: Check the **Card Style Cookbook** at the top of components.md. Pick a different card style from the previous slide. Also check if this slide should use a **Dark Background Content Template**. +6. **Be creative**: Freely modify spacing, font sizes, card styles, proportions, background treatments, and decorative elements across pages. The only hard constraints are the html2pptx engine limitations (no flex-wrap, no negative margins, no DIV background-image, no CSS gradients). Use the design-system.md Quick Reference for suggested ranges, but treat them as starting points, not limits. + - **Card styles**: Use the **Card Style Cookbook** in components.md to swap between shadow/outline/solid/accent-bar/dark card styles. No 2 adjacent slides should use the same card style. + - **Title bars**: All content slides must use the **same header style** — pick one title bar variant from components.md and apply it consistently across every content page. + - **Dark pages**: Use the **Dark Background Content Templates** in components.md for rhythm-breaking dark slides. Aim for at least 1 dark content page per 4 slides. +6. **Fill the slide**: If there's visible empty space, increase font sizes, add visual elements, expand padding, or use a colored/photo background. A well-filled slide looks professional; excessive whitespace looks unfinished. + + +### Step 5.8 — Visual Diversity Check (RECOMMENDED) + +Before converting to PPTX, do a quick scan of your slide sequence: +- Are adjacent slides visually distinct (different layout, background, card style)? +- Is there enough variety across the deck (multiple layout structures, card treatments, background approaches)? +- Is there at least one dramatic "rhythm breaker" page? +- Do real photographs appear on covers and at least one other slide? + +If the answer to any is "no", revise before proceeding. + +### Step 6 — Convert and Validate + +1. Create and run a JavaScript file using [`html2pptx.js`](scripts/html2pptx.js) to convert HTML slides to PowerPoint: + ```javascript + const pptxgen = require('pptxgenjs'); + const html2pptx = require('./html2pptx'); + + const pptx = new pptxgen(); + pptx.layout = 'LAYOUT_16x9'; + + // Optional: custom font configuration + const fontConfig = { cjk: 'Microsoft YaHei', latin: 'Corbel' }; + + // Process ALL slides with warnings collection + const allWarnings = []; + for (const htmlFile of slideFiles) { + const { slide, placeholders, warnings } = await html2pptx(htmlFile, pptx, { fontConfig }); + allWarnings.push(...warnings); + } + + // Add charts to placeholder areas if any + if (placeholders.length > 0) { + slide.addChart(pptx.charts.LINE, chartData, placeholders[0]); + } + + await pptx.writeFile('output.pptx'); + ``` + +2. **Check warnings**: Review `warnings` output. **Blocking issues** (overflow, font < 11pt) must be fixed. **Non-blocking warnings** (bounds, balance, density) are suggestions — use judgment on whether to fix them. + +3. **Visual validation**: Generate thumbnails and inspect for layout issues: + ```bash + 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 + - **Consistency check**: All body text same font and size? All page margins consistent? All content slides use the same header style? + - If issues found, adjust HTML and regenerate + - Repeat until all slides are visually correct + +### Step 6.5 — Final Quality Check (RECOMMENDED) + +Before finalizing, do a quick visual quality scan: + +- ✅ Real photographs used (cover + at least 1 other slide) +- ✅ Background variety (at least 3 different treatments) +- ✅ No 2 consecutive slides look the same +- ✅ Multiple layout structures used (aim for 5+) +- ✅ Card styles vary across pages +- ✅ Every slide has a clear visual focal point +- ✅ At least 1 dramatic full-bleed or dark page exists +- ✅ All referenced image files exist and are > 10KB + +If major issues are found, fix and regenerate. Minor imperfections are acceptable — don't over-optimize at the cost of creativity. + +### Design Hard Rules (ALWAYS ENFORCED) + +These rules are split into **engine constraints** (violating them causes broken output) and **design principles** (ensuring quality). + +**Engine Constraints (technical — cannot be violated):** +``` +[ENGINE] Slide canvas is 720×405pt — content exceeding this overflows +[ENGINE] All colors must come from the theme color scale — no arbitrary grays (#666 #999 #DDD) +[ENGINE] font-family must include a CJK font name to trigger correct mapping +[ENGINE] Do not use flex-wrap — multi-row layouts must use separate flex containers +[ENGINE] Do not use negative margins — they cause text stacking in PPT +[ENGINE] Multi-column equal-width cards must use fixed width + flex-shrink:0, not flex:1 +[ENGINE] Background images only work on <body>, not <div> — DIV background-image is not supported +[ENGINE] Images must be local file paths, not URLs +[ENGINE] Titles and short labels (<10 characters) should use white-space:nowrap +[ENGINE] Numeric sequences (e.g. 01/02/03, $12M) should use white-space:nowrap +``` + +**Design Principles (creative quality — strongly encouraged):** +``` +[DESIGN] Adjacent slides should use different layouts, backgrounds, and card styles +[DESIGN] Every content slide should have at least one visual focal point +[DESIGN] Cover and closing page should echo each other +[DESIGN] Prefer cutting content over shrinking fonts — only when a slide is already well-filled and overflow is imminent; never cut content preemptively +[DESIGN] Aim for 5+ layout structures, 3+ card styles, 3+ background treatments across the deck +[DESIGN] Insert a rhythm-breaking page every 3-4 slides +[DESIGN] Background images should have a mask overlay for text readability +[DESIGN] Content should be vertically balanced, not crammed at the top +[DESIGN] Accent colors must have HSL saturation ≤ 65% — high-saturation colors look garish on projection +[DESIGN] Large color blocks (>15% page area) should use S ≤ 50% — only small accents can be vivid +[DESIGN] All content slides must use the same header style — pick one title bar variant and apply it consistently across all content pages +``` + +--- + +## Editing an existing PowerPoint presentation + +When editing slides in an existing PowerPoint presentation, work with raw Office Open XML (OOXML) format. + +### 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.** +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: `python ooxml/scripts/validate.py <dir> --original <file>` +5. Pack the final presentation: `python ooxml/scripts/pack.py <input_directory> <office_file>` + + + +## Creating Thumbnail Grids + +```bash +python scripts/thumbnail.py template.pptx [output_prefix] +``` + +**Features**: +- Creates: `thumbnails.jpg` (or `thumbnails-1.jpg`, `thumbnails-2.jpg` for large decks) +- Default: 5 columns, max 30 slides per grid (5x6) +- Custom prefix: `python scripts/thumbnail.py template.pptx my-grid` +- Adjust columns: `--cols 4` (range: 3-6) +- Slides are zero-indexed (Slide 0, Slide 1, etc.) + +## Converting Slides to Images + +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 + ``` + +## 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]"` (text extraction) +- **pptxgenjs**: `npm install -g pptxgenjs` (creating presentations) +- **playwright**: `npm install -g playwright` (HTML rendering) +- **react-icons**: `npm install -g react-icons react react-dom` (icons) +- **sharp**: `npm install -g sharp` (SVG rasterization and image processing) +- **LibreOffice**: `sudo apt-get install libreoffice` (PDF conversion) +- **Poppler**: `sudo apt-get install poppler-utils` (pdftoppm) +- **defusedxml**: `pip install defusedxml` (secure XML parsing) diff --git a/skills/ppt/beamer.md b/skills/ppt/beamer.md new file mode 100755 index 0000000..814eda8 --- /dev/null +++ b/skills/ppt/beamer.md @@ -0,0 +1,1461 @@ +# Beamer Module: LaTeX Academic Presentations + +> **Output formats: PDF and HTML only — never `.pptx`.** +> Activate for ALL academic, scientific, research, or scholarly presentation requests, +> including reading a paper to make slides (paper presentations, academic PPTs, thesis defenses, etc.). + +## Pipeline + +``` +User request → LaTeX Beamer (.tex) → Tectonic → PDF +``` + +Read `references/beamer.md` for theme catalogue, overlay syntax, and content templates. +For non-Beamer LaTeX documents (papers, theses), read `references/latex.md`. + +--- + +## Workflow + +### 1. Analyze Source Material + +If given a paper/PDF to summarize: + +```bash +python3 scripts/pdf.py extract.text paper.pdf +python3 scripts/pdf.py extract.table paper.pdf +python3 scripts/pdf.py extract.image paper.pdf -o ./images_out/ +``` + +For arXiv papers, prefer extracting clean figures from the HTML version: + +```python +# Download individual figure images (no surrounding text) +import urllib.request, re +html_url = "https://arxiv.org/html/<PAPER_ID>v1" +req = urllib.request.Request(html_url, headers={'User-Agent': 'Mozilla/5.0'}) +with urllib.request.urlopen(req) as resp: + html = resp.read().decode('utf-8') +# Find figure images: x1.png, x2.png, ... +for m in re.findall(r'src="(x\d+\.png)"', html): + urllib.request.urlretrieve(f"{html_url}/{m}", m) +``` + +Map figure images to figure numbers by parsing `<figure>` elements and their captions. +**Avoid cropping from rendered PDF pages** — the result often includes surrounding text. If unavoidable, crop precisely so zero non-figure content remains (see Section 6, Source Priority). + +### 2. Write the Beamer `.tex` + +#### Pre-write Checklist (MANDATORY — read before writing any content) + +Before writing any slide content, internalize these rules: +1. **Every slide must be self-contained.** Never write "see original paper", "refer to Figure X in the paper", "详见原文", or any similar reference that defers content to the source. If it's worth mentioning, show it. +2. **All figures/tables get sequential numbering** (Figure 1, 2, 3... / Table 1, 2, 3...) in order of appearance — never copy the paper's original numbers. +3. **Slide titles must state conclusions**, not describe content (see Section 5). +4. **Choose the correct navigation scheme** before writing the preamble (see Section 7). Paper-based slides must use `paper-navbar.tex`; general presentations must use `progress-navbar.tex`. + +Structure: + +``` +Title → Outline → Background → Key Ideas → Method → +Experiments/Results → Ablation → Comparison → Conclusion → Q&A +``` + +Slide count heuristics: + +| Instruction | Slides | +|---|---| +| Explicit count | Match exactly | +| No count | 1–2 per point; 12–20 total | +| "Brief" / "Short" | 8–12 | +| "Full" / "Detailed" | 20–30 | + +Content density: ≤ 6 bullets per slide, ≤ 6 words per bullet. + +**Section count:** Aim for **4–6 sections**. Each section generates an automatic divider page, so more than 6 sections means too many empty pages. If the content naturally has 7+ topics, merge related ones. **Divider pages should not exceed 20% of total slide count.** + +### 3. tcolorbox Block Layout Rules (Critical) + +**When to use native `block` vs `tcolorbox`:** + +Native beamer `block`/`alertblock`/`exampleblock` are fine for **simple, standalone** blocks where you don't need precise control. But switch to `tcolorbox` (`ablock`/`fblock`/etc.) when any of the following apply: + +| Scenario | Use | +|---|---| +| Simple standalone block, no special alignment needs | Native `block` is OK | +| Side-by-side blocks that must have **equal height** | `fblock`/`falertblock` (tcolorbox) — native blocks cannot guarantee equal heights | +| Need precise **background/frame color** beyond theme defaults | tcolorbox — native blocks inherit theme colors and are hard to override per-instance | +| Need consistent **padding, border-radius, shadow** control | tcolorbox — `blocksty` gives uniform styling | +| Vertically **stacked blocks** that need visual consistency | `ablock`/`aalertblock` (tcolorbox) — auto-height with uniform styling | + +**Rule of thumb:** If blocks appear side-by-side, need custom colors, or must look visually consistent across slides, use tcolorbox. For a quick single block on a simple slide, native `block` is acceptable. + +**Choose block type by layout relationship:** + +| Layout | Block Type | Rule | +|---|---|---| +| **Side-by-side** (left–right parallel) | `fblock` / `falertblock` (fixed height) | Both blocks **must** use the **same height** parameter | +| **Stacked** (top–bottom vertical) | `ablock` / `aalertblock` (auto height) | Let content determine height naturally | + +**Side-by-side** = two blocks that are conceptually parallel (e.g., "Problem vs Hypothesis", "Root Traits vs Shoot Traits", "Innovation vs Conclusion"). +**Stacked** = two blocks in one column, one above the other (e.g., image-text pages with "Key Findings" above "Genotype Differences" in the right column). + +Define in preamble: + +```latex +\usepackage[most]{tcolorbox} +\tcbset{ + blocksty/.style={ + boxrule=0.4pt, arc=1.5pt, + top=2pt, bottom=2pt, left=4pt, right=4pt, + fonttitle=\bfseries\small, before upper={\small}, valign=top, + } +} +% ── Custom colors (MUST define all three in preamble) ────────────── +\definecolor{myblue}{HTML}{2B5EA7} +\definecolor{myred}{HTML}{C0392B} +\definecolor{mygreen}{HTML}{27AE60} +% ── Fixed-height (side-by-side equal-height layouts) ─────────────── +\newtcolorbox{fblock}[2][]{blocksty, colback=myblue!8, colframe=myblue!85, + coltitle=white, title={#2}, height=#1} +\newtcolorbox{falertblock}[2][]{blocksty, colback=myred!8, colframe=myred!80, + coltitle=white, title={#2}, height=#1} +\newtcolorbox{fexampleblock}[2][]{blocksty, colback=mygreen!8, colframe=mygreen!80, + coltitle=white, title={#2}, height=#1} +% Auto-height (stacked / standalone blocks) +\newtcolorbox{ablock}[1]{blocksty, colback=myblue!8, colframe=myblue!85, + coltitle=white, title={#1}} +\newtcolorbox{aalertblock}[1]{blocksty, colback=myred!8, colframe=myred!80, + coltitle=white, title={#1}} +\newtcolorbox{aexampleblock}[1]{blocksty, colback=mygreen!8, colframe=mygreen!80, + coltitle=white, title={#1}} +``` + +**Side-by-side example** — both columns get **identical height**: + +```latex +\begin{columns}[T] + \column{0.48\textwidth} + \begin{fblock}[5.0cm]{Left Title} + Content... + \end{fblock} + \column{0.48\textwidth} + \begin{falertblock}[5.0cm]{Right Title} + Content... + \end{falertblock} +\end{columns} +``` + +**Stacked example** — auto height, no fixed parameter: + +```latex +\column{0.46\textwidth} +\begin{ablock}{Key Findings} + \begin{itemize} ... \end{itemize} +\end{ablock} +\begin{aalertblock}{Genotype Differences} + Content... +\end{aalertblock} +``` + +Height guidelines for fblock: 5.0cm for full-height side-by-side columns. +**Always test**: if content overflows, increase height. If frame overflows (`Overfull \vbox`), decrease height, reduce content, or split into two slides. **Never use `[allowframebreaks]`** — it produces unpredictable page breaks in Beamer. + +#### Block Mixing Rules (MANDATORY) + +1. **No mixing tcolorbox and native blocks on the same slide.** Within a single frame, use EITHER tcolorbox blocks (`fblock`/`ablock`/`falertblock`/`aalertblock`/etc.) OR native Beamer blocks (`block`/`alertblock`/`exampleblock`), but **never both**. Mixing produces inconsistent styling (different padding, border radius, shadows). + +2. **Formal environments are exempt.** LaTeX theorem-like environments (`theorem`, `definition`, `lemma`, `proof`, `corollary`, etc.) are semantically distinct from layout blocks and MAY coexist with tcolorbox blocks on the same slide. Use them when the content is genuinely a theorem/definition/etc. + +3. **General-purpose blocks (e.g., "Key Advantages", "Core Idea", "Key Takeaway") must use tcolorbox**, not native blocks. Reserve native `block`/`alertblock`/`exampleblock` only for slides that have no tcolorbox blocks at all. + +#### Stacked Blocks: Equal Total Height Across Columns (MANDATORY) + +When a slide has two columns with stacked blocks, the **total height** of each column must be equal. Total height = sum of all block heights + inter-block spacing in that column. + +This does NOT mean every individual block must be the same height — blocks within a column can have different heights. But the left column's total occupied height must match the right column's total occupied height, so the slide looks visually balanced. + +**How to achieve this:** +- Use `fblock` with explicit height parameters for all blocks +- Calculate: left column total = `h1 + gap + h2` ; right column total = `h3 + gap + h4` +- Ensure both totals are equal (e.g., both sum to 5.6cm) +- Adjust individual block heights to fit their content while maintaining the total + +```latex +% Example: 2 blocks per column, equal total height (5.6cm each) +\begin{columns}[T] + \column{0.48\textwidth} + \begin{fblock}[3.2cm]{Block A — more content} + Longer content... + \end{fblock} + \vspace{0.2cm} + \begin{falertblock}[2.2cm]{Block B — less content} + Shorter content... + \end{falertblock} + % Total: 3.2 + 0.2 + 2.2 = 5.6cm + + \column{0.48\textwidth} + \begin{fblock}[2.6cm]{Block C} + Content... + \end{fblock} + \vspace{0.2cm} + \begin{falertblock}[2.8cm]{Block D} + Content... + \end{falertblock} + % Total: 2.6 + 0.2 + 2.8 = 5.6cm +\end{columns} +``` + +### 3.5 Content Overflow Detection and Repair (MANDATORY) + +After every compilation, check for content overflow. This is the most common cause of ugly slides. + +#### Detection + +1. **Compiler warnings** — scan output for `Overfull \vbox` and `Overfull \hbox` warnings. These mean content exceeds the frame boundary. +2. **Visual inspection** — open the PDF and check every slide for: + - Text being cut off at the bottom or right edge + - Block content truncated (last bullet missing or partially visible) + - Elements overlapping the footer bar + - Unbalanced columns (one column visually much heavier than the other) + - **Side-by-side block near-overflow** — content touching or nearly touching the block's bottom edge (even if not technically clipped, it looks cramped and is one line away from overflow) +3. **Element occlusion / overlap check** — verify that no element is partially or fully hidden behind another element. This is a **silent failure** — the compiler will NOT produce any warning when elements overlap. Common scenarios: + - Right-column blocks covering part of a left-column image (especially wide composite figures with multiple sub-panels) + - A block or text extending beyond its column boundary and overlapping the adjacent column + - Footer or decorative elements covering bottom-edge content + + **How to detect:** Visually scan every slide in the compiled PDF. For image+block layouts, confirm that the entire image (all sub-panels, axis labels, legends) is fully visible and not occluded by any block, text, or other element. + + **Repair strategy for occlusion:** + + | Priority | Fix | When to use | + |---|---|---| + | 1 | **Give the image its own full-width slide** | Composite figures (3+ sub-panels) that need full width to be legible | + | 2 | **Select fewer sub-panels** | Show only the 1-2 most important panels on this slide, move rest to appendix | + | 3 | **Adjust column width ratio** | Give the image column more space (e.g., 0.58 left / 0.38 right) | + | 4 | **Reduce image width + increase right margin** | Shrink image so it fits entirely within its column boundary | + + **Prevention:** Before choosing a layout, estimate whether the image content fits within the allocated column width. Wide composite figures (4+ panels side-by-side) almost never fit in a half-width column — plan a full-width layout from the start. + +#### Repair Strategy + +When overflow is detected, apply fixes in this priority order: + +| Priority | Fix | When to use | +|---|---|---| +| 1 | **Reduce content** | Remove low-value bullets, shorten text, summarize | +| 2 | **Shrink font locally** | Use `\small` or `\footnotesize` for the overflowing slide only | +| 3 | **Adjust block heights** | Reduce `fblock` height parameters, rebalance column totals | +| 4 | **Rebalance columns** | Move content between left/right columns; adjust width ratio | +| 5 | **Split into two slides** | When content genuinely cannot fit — split by sub-topic | + +**NEVER ignore overflow.** Every slide must display all its content completely within the frame boundary. If a fix introduces new overflow elsewhere, iterate until all slides are clean. + +#### Verification Loop + +``` +Compile → Check warnings → Visual inspect PDF → Fix issues → Re-compile → Repeat until clean +``` + +This loop is mandatory. Do not deliver a PDF that has not passed visual inspection. + +### 4. Figure and Table Numbering (CRITICAL) + +In presentations, use **sequential numbering** (Figure 1, 2, 3...) independent of the source paper. **NEVER copy the original paper's figure/table numbers** — always renumber them consecutively (1, 2, 3...) in the order they appear in the slides. This is a presentation, not a paper reproduction. Add captions below figures and above tables. + +#### No "See Original" References (ABSOLUTELY FORBIDDEN) + +**Slides must be self-contained.** Never use placeholder text like "see original paper Figure X", "refer to the paper for details", "详见原文", or any similar reference that defers content to the source document. If a figure or result is worth mentioning on a slide, it must be **actually shown** — either as an extracted image, a simplified TikZ redraw, or a text/bullet summary of the key information. A slide that says "see original" is an incomplete slide. + +#### All Images and Diagrams: Numbering + Caption + Centering (MANDATORY) + +**Every visual element** on a slide — whether it is an `\includegraphics` image, a TikZ diagram, or a table — **MUST** have: + +1. **Sequential numbering** — Figure 1, Figure 2, ... / Table 1, Table 2, ... in order of appearance +2. **Descriptive caption** — below figures/TikZ, above tables +3. **Centered display** — wrapped in `\begin{center}...\end{center}` + +**No exceptions.** A figure or TikZ diagram without a number and caption is incomplete. + +**For `\includegraphics` images:** + +```latex +\begin{center} + \includegraphics[width=\textwidth,height=0.45\textheight,keepaspectratio]{fig.jpg} + \par\vspace{0.15em} + {\scriptsize \textbf{Figure 1.} Description of the figure.} +\end{center} +``` + +**For TikZ diagrams:** + +```latex +\begin{center} + \begin{tikzpicture}[...] + % ... diagram code ... + \end{tikzpicture} + \par\vspace{0.15em} + {\scriptsize \textbf{Figure 2.} Simplified architecture overview.} +\end{center} +``` + +**For tables:** + +```latex +\begin{center} + {\small \textbf{Table 1.} Description of the table.} +\end{center} +\vspace{0.3em} +\begin{tabular}{...} +``` + +### 5. Slide Titles + +Frame titles should convey the **conclusion or key message** of the slide, not describe the figure or repeat the figure caption. + +| Bad (descriptive) | Good (conclusion-driven) | +|---|---| +| "RCS in Cotton Root Cross-sections" | "First Confirmation of RCS in Cotton" | +| "Effect of GhSAG12 Silencing on Drought Tolerance" | "Silencing RCS-related Gene Significantly Reduces Drought Tolerance" | +| "Relationship Between RCS and Endogenous Hormones" | "Endogenous Hormones Change Significantly with RCS Progression" | +| "Five Endogenous Hormones During RCS" | "GA, ZR, IAA, BR, ABA All Decline with RCS Progression" | +| "Research Background" | "Drought is the Primary Yield Constraint for Cotton" | + +**Self-check (MANDATORY):** After writing all slides, review every `\frametitle{}` and ask: *"Does this title tell the audience what to take away, or does it just describe what's on the slide?"* If descriptive, rewrite it as a conclusion. Background/outline/Q&A slides are exempt. + +### 6. Image Handling Guidelines + +#### Sizing to Prevent Overflow + +Always use `height` + `keepaspectratio` to constrain images within the frame. Recommended max heights: + +| Layout | Max image height | +|---|---| +| Full-page image (centered) | `0.55\textheight` | +| Image in left column (with text blocks on right) | `0.45\textheight` | + +Always leave room for the figure caption below and the footer bar. **Never rely on `width` alone** — tall images will overflow the frame. + +#### Source Priority + +| Priority | Source | When to use | +|---|---|---| +| 1 (Best) | Original images from arXiv HTML version | Preferred for arXiv papers — download vector/high-res bitmaps directly | +| 2 | Supplementary materials from authors | High-quality original figures | +| 3 | `pdf.py extract.image` extraction | Non-arXiv papers — extract embedded images from PDF | +| 4 | TikZ redraw | Only for simple geometric diagrams (flowcharts, arrow diagrams) when no original exists | +| 5 (Last resort) | PDF page screenshot + precise crop | Acceptable ONLY if the crop contains **zero** surrounding text/captions/margins — any visible non-figure content is forbidden | + +#### Precise Cropping from PDF + +When extracting images from PDFs, you **must crop precisely to the figure boundary only**. Never include surrounding text, captions, or page margins in the extracted image — this looks unprofessional and is the most common image quality mistake. + +```bash +# Correct: extract embedded image objects (no surrounding text) +python3 scripts/pdf.py extract.image paper.pdf -o ./images_out/ + +# Wrong: screenshot full page then crop — almost always includes extra content +# ❌ Do not do this +``` + +If an image cannot be cleanly extracted (e.g., it spans multiple pages or is overlaid with text), you may use a PDF page screenshot with `\includegraphics` `trim` and `clip` to crop precisely — but the result **must not include any surrounding text, captions, or page margins**: + +```latex +% trim = {left bottom right top} — crop until ONLY the figure remains +\includegraphics[trim=20pt 40pt 20pt 30pt, clip, + width=0.85\textwidth, keepaspectratio]{page_screenshot.png} +``` + +**Post-crop verification (MANDATORY):** After cropping, visually inspect the result image. If it contains any surrounding text, captions, page margins, or is missing part of the figure, **adjust the trim values and re-crop** until the image is both complete and clean. Repeat this verify→adjust loop until the crop passes inspection. Prefer extracting the actual image over trimming screenshots whenever possible. + +#### Image File Organization + +```bash +project/ +├── main.tex +└── figures/ # All images in a dedicated directory + ├── fig1.png + ├── fig2.jpg + └── fig3.pdf # Vector format preferred when available +``` + +Use `\graphicspath{{figures/}}` in the preamble so `\includegraphics` only needs the filename. + +#### Vector vs Raster + +| Format | When to use | +|---|---| +| `.pdf` / `.eps` (vector) | Line art, flowcharts, plots — preferred, scales without loss | +| `.png` (raster) | Photos, screenshots, experimental results — ensure resolution ≥ 150dpi | +| `.jpg` (raster) | Photographic images — smaller file size | +| ❌ `.svg` | Not directly supported by Beamer — convert to PDF first | + +Prefer vector formats (PDF/EPS). For experimental photos and other raster images, ensure sufficient resolution. + +#### Subfigures + +For papers with composite figures (a/b/c/d panels), either extract individual panels separately, or show the full composite figure with a clear reference: + +```latex +% Option 1: Show full composite figure +\begin{center} + \includegraphics[width=0.75\textwidth, keepaspectratio]{fig1_composite.png} + \par\vspace{0.15em} + {\scriptsize \textbf{Figure 1.} (a) Architecture overview (b) Attention heatmap} +\end{center} + +% Option 2: Use minipage for side-by-side subfigures +\begin{center} + \begin{minipage}{0.45\textwidth} + \centering + \includegraphics[width=\textwidth, keepaspectratio]{fig1a.png} + \par\vspace{0.1em} + {\scriptsize (a) Training curve} + \end{minipage} + \hfill + \begin{minipage}{0.45\textwidth} + \centering + \includegraphics[width=\textwidth, keepaspectratio]{fig1b.png} + \par\vspace{0.1em} + {\scriptsize (b) Test accuracy} + \end{minipage} +\end{center} +``` + +#### Image-Text Layout Templates + +**Left image + right text (most common):** + +Use `[c]` (vertical center) for the columns environment when one side is an image — the image visually centers against the opposite text/block column, which looks more balanced than `[T]`. (See "Columns Layout Best Practices" below for the full alignment decision table.) + +```latex +\begin{columns}[c] + \column{0.50\textwidth} + \begin{center} + \includegraphics[height=0.45\textheight, keepaspectratio]{fig1.png} + \par\vspace{0.15em} + {\scriptsize \textbf{Figure 1.} Description} + \end{center} + \column{0.46\textwidth} + \begin{ablock}{Key Findings} + \begin{itemize} + \item Finding one + \item Finding two + \end{itemize} + \end{ablock} +\end{columns} +``` + +Note: Use `[T]` for **text-text** or **block-block** side-by-side layouts (see "Columns Layout Best Practices"). Use `[c]` for **image-text** layouts. + +#### Image Column Vertical Centering (MANDATORY) + +When one column contains an image (with optional caption) and the other contains text blocks, the image column's content often appears **top-heavy** — the image sits at the top of the column with empty space below, while the right column's blocks are vertically distributed. This creates a visual imbalance. + +**The fix:** Use `[c]` on `\begin{columns}` so both columns are vertically centered against each other. If `[c]` alone is not enough (e.g., the image+caption combo is much shorter than the block column), add vertical spacing above the image to nudge it toward the visual center: + +```latex +% ✅ Correct: [c] alignment centers both columns +\begin{columns}[c] + \column{0.50\textwidth} + \begin{center} + \includegraphics[height=0.45\textheight, keepaspectratio]{fig.png} + \par\vspace{0.15em} + {\scriptsize \textbf{Figure 1.} Description} + \end{center} + \column{0.46\textwidth} + \begin{ablock}{Block A} + Content... + \end{ablock} + \begin{aalertblock}{Block B} + Content... + \end{aalertblock} +\end{columns} +``` + +**When `[c]` is still not enough** — if the image column is significantly shorter than the block column, the image will be centered but still look "high" relative to the blocks. In this case, use `[T]` with explicit `\vspace` to manually push the image down: + +```latex +% Manual adjustment when image column is much shorter +\begin{columns}[T] + \column{0.50\textwidth} + \vspace{0.8cm} % Push image down to align visual center with right column + \begin{center} + \includegraphics[height=0.40\textheight, keepaspectratio]{fig.png} + \par\vspace{0.15em} + {\scriptsize \textbf{Figure 1.} Description} + \end{center} + \column{0.46\textwidth} + \begin{ablock}{Block A} ... \end{ablock} + \begin{aalertblock}{Block B} ... \end{aalertblock} +\end{columns} +``` + +**Visual check:** After compilation, verify that the visual center of the image column (midpoint of image+caption) roughly aligns with the visual center of the block column (midpoint of all blocks+gaps). Adjust `\vspace` if needed. + +#### Table-Block Width Alignment + +When a table and block(s) are vertically stacked in the same column and serve as parallel semantic units (e.g., "data table" above "conclusion block"), they should have **equal width**. The key is to ensure both elements share the same width — there are multiple ways to achieve this: + +**Method 1: Wrap table in an ablock** (when a title makes sense for the table): +```latex +\column{0.48\textwidth} +\begin{ablock}{Experimental Data} + {\footnotesize + \begin{tabularx}{\linewidth}{lCX} + \toprule + ... \\ + \bottomrule + \end{tabularx} + } +\end{ablock} +\begin{ablock}{Conclusion} + Analysis text... +\end{ablock} +``` + +**Method 2: Use tabularx with \linewidth without block** (when table doesn't need a title): +```latex +\column{0.48\textwidth} +{\footnotesize +\begin{tabularx}{\linewidth}{lCX} + \toprule + ... \\ + \bottomrule +\end{tabularx} +} +\vspace{0.3em} +\begin{ablock}{Conclusion} + Analysis text... +\end{ablock} +``` + +**Method 3: Use \resizebox{\linewidth}{!}{...}** (for fixed-width tables): +```latex +\column{0.48\textwidth} +\resizebox{\linewidth}{!}{% +\begin{tabular}{lcc} + \toprule + ... \\ + \bottomrule +\end{tabular} +} +\vspace{0.3em} +\begin{ablock}{Conclusion} + Analysis text... +\end{ablock} +``` + +Choose the method based on context: Method 1 when the table benefits from a title, Method 2 for clean untitled tables, Method 3 when the table already has a fixed column layout. + +When NOT to equal-width align: +- Full-page centered standalone tables — use `\centering` with natural width +- Narrow tables (2-3 columns) — forcing full width looks sparse and hurts readability +- Tables with only a footnote-style caption below — different semantic levels, different widths is fine + +**Top image + bottom text:** +```latex +\begin{center} + \includegraphics[height=0.40\textheight, keepaspectratio]{fig1.png} +\end{center} +\vspace{-0.3em} +\begin{ablock}{Analysis} + Interpretation of the results... +\end{ablock} +``` + +**When to redraw vs when to use original:** + +| Scenario | Action | +|---|---| +| Paper has clear, high-quality original figures | Use the original directly | +| Original figure is low-resolution or blurry | Try arXiv HTML version first, otherwise redraw a simplified version | +| Need to highlight a specific region of a figure | Use original + TikZ overlay annotations (arrows, boxes) | +| Concept/schematic diagram with no original available | Draw a simplified version in TikZ | +| Complex biological/chemical structure diagrams | Always use the original — do not attempt to redraw | + +**Full TikZ rules → see Section 8.** + +### 7. Navigation and Footer + +Navigation symbol removal is handled automatically by the chosen footer scheme. Do **not** manually add `\setbeamertemplate{navigation symbols}{}` when using `progress-navbar.tex` or `paper-navbar.tex` — they include it internally. Only add it manually when using the Simple Footline. + +**Footer scheme selection:** + +| Scenario | Scheme | +|---|---| +| User provides a paper/PDF to make slides | **Paper Presentation Navigation** (default for paper-based) | +| General academic presentation / courseware (no specific paper) | **Progress Navigation Bar** | +| User requests minimal footer | **Simple Footline** | + +**This is not optional.** When the scenario matches a row above, the corresponding scheme **must** be used. Do not fall back to the default Madrid footline or omit the navigation bar. + +#### Paper Presentation Navigation (Default for paper-based slides — Recommended) + +When the user provides a paper/PDF and asks for slides, use the paper navigation layout. Add to the preamble (after `\usetheme` and `\definecolor{myblue}`): + +```latex +\useoutertheme{miniframes} +\input{paper-navbar.tex} +``` + +Copy `references/paper-navbar.tex` into your project directory before compiling. This file provides: + +**Top navigation bar:** +- Beamer built-in `miniframes` — section names + small dots (one dot per slide) +- Current section auto-highlighted +- Subsection empty row removed (clean single-row layout) +- Background: `myblue!15` — adapts to any Preset Color Palette + +**Bottom footer — four columns:** + +``` +Author (Affiliation) | Paper Title | Journal, Year | Page/Total + 25% 42% 22% 11% +``` + +- Author, journal, page columns: `myblue!15` (light tint of theme color) +- Paper title column: `myblue!30` (mid tint), white text — visually distinct +- All colors follow `myblue`, so they automatically adapt when switching Preset Color Palettes (e.g., Teal Academic → light teal, Slate Purple → light purple, Warm Earth → light brown) +- Built-in Beamer hyperlinks preserved (author/title link to first/last page) + +**Paper metadata setup** — use Beamer's short-form mechanism: + +```latex +\title[Short Paper Title]{Full Paper Title} +\author[Author (Affiliation)]{Full Author List} +\date[Journal Name, Year]{} % No journal → \date[]{} +``` + +**Note:** `paper-navbar.tex` includes `\setbeamertemplate{navigation symbols}{}` and both `headline`/`footline` templates — do not set them separately. + +For author names that are too long for the title page or footer, use `\scriptsize` font size and abbreviated format (e.g., "Guo C., Zhang K., ...") with the short form in square brackets for the footer: `\author[Guo C. et al.]{...}`. + +#### Progress Navigation Bar (Alternative — for non-paper academic presentations) + +For general academic presentations, courseware, or lectures without a specific source paper, use the progress navigation bar. Add to the preamble (after `\usetheme`): + +```latex +\input{progress-navbar.tex} +``` + +Copy `references/progress-navbar.tex` into your project directory before compiling. This file provides: + +- **Dynamic equal-width boxes** — each box width = (paperwidth − 50pt) ÷ total frames; automatically fills the footer regardless of slide count +- **Three-level symbols** — `≡` home (title page), `1 2 3...` section numbers, `◆` subsection, `-` regular slides +- **Progress coloring** — visited pages get `teal!60` background fill; unvisited pages are hollow +- **Clickable hyperlinks** — every box links to its corresponding frame +- **Auto section/subsection title pages** — `\AtBeginSection` and `\AtBeginSubsection` are included +- **Requires `--runs 2`** — first pass writes total frame count, second pass calculates correct widths + +**Color customization:** To match your theme, find-and-replace `teal!60` → `myblue!60` (or any color) in the `.tex` file. Also change `\color{teal}\hrule` for the separator line. + +**Note:** This navbar replaces `\setbeamertemplate{navigation symbols}{}` and `\setbeamertemplate{footline}` — do not set them separately when using it. + +#### Simple Footline (Minimal alternative) + +If you prefer a minimal footer without any navigation bar: + +```latex +\setbeamertemplate{navigation symbols}{} +\setbeamertemplate{footline}{% + \hfill\insertframenumber/\inserttotalframenumber\hspace{1em}\vspace{0.5em} +} +``` + +The default footline should include at least frame numbers. + +### 8. TikZ Usage Guidelines + +**TikZ is the last resort, not the first choice.** Always prefer original figures from the paper (extract via `pdf.py extract.image` or arXiv HTML). Only draw TikZ when no original figure exists, and only for **simple** diagrams: +- Flowcharts, pipelines, comparison arrows, simple block diagrams +- Geometric and structured layouts +- You can verify it compiles correctly + +**Avoid complex TikZ** — biological structures, network topologies with many nodes, regulatory pathways, full model architectures. If the original figure is not available and the diagram is too complex to simplify, describe it in text/bullet points instead. + +**Simplify for slides (CRITICAL):** A slide is not a textbook. TikZ diagrams on slides must be **simplified abstractions**, not full architecture reproductions. If a diagram has too many nodes, small text, or dense connections, the audience cannot read it during a presentation. + +| Scenario | Wrong approach | Right approach | +|---|---|---| +| Transformer architecture | Draw every Q/K/V split, softmax, residual connection, layer norm | Simplified block diagram: Input → Encoder (×N) → Decoder (×N) → Output, with key components labeled | +| Multi-head attention | Draw all matrix operations and individual head computations | Abstract flow: Input → Linear projections → Parallel heads → Concat → Output | +| Complex neural network | Reproduce every layer and skip connection | High-level block diagram with labeled stages | + +**Rules:** +1. **Original figure available → use it. No TikZ.** +2. **No original figure + simple diagram → draw with TikZ.** Maximum ~8-10 nodes per slide. +3. **No original figure + complex diagram → do NOT draw with TikZ.** Use text/bullet description instead, or split into multiple simplified diagrams. +4. **All text in TikZ must be readable at presentation distance** — minimum `\small` font size, preferably `\normalsize`. + +### 9. Compile + +```bash +python3 scripts/pdf.py convert.latex main.tex +python3 scripts/pdf.py convert.latex main.tex --runs 2 # for ToC / refs +``` + +**Always compile and confirm clean output.** Never deliver only a `.tex` file. + +#### Post-compilation Verification Checklist (MANDATORY) + +After compilation, verify the following before delivery: + +1. **No overflow warnings** — check for `Overfull \vbox` / `Overfull \hbox` (see Section 3.5) +2. **Figure/Table numbering is sequential** — scan the PDF and confirm all figures are numbered 1, 2, 3, ... and all tables are numbered 1, 2, 3, ... with no gaps, no duplicates, and no out-of-order numbers. If any numbering is wrong, fix in the `.tex` and recompile. +3. **No "see original" references** — search the `.tex` for any placeholder text that defers to the source document. If found, replace with actual content. +4. **Slide titles are conclusion-driven** — read every `\frametitle{}` and verify it states a finding/conclusion, not a description. Fix any descriptive titles. +5. **Visual inspection** — open the PDF and check every slide for layout issues (see Section 3.5) + +### 10. WPS / PDF Viewer Compatibility + +Beamer generates navigation hyperlinks that interfere with WPS and some PDF viewers. Always include in the preamble: + +```latex +\hypersetup{hidelinks} +``` + +Note: `\setbeamertemplate{navigation symbols}{}` is already handled by the chosen footer scheme (see Section 7) — do not add it separately here. + +### 11. Deliver + +Send compiled PDF to user. On Feishu use: + +```bash +openclaw message send --channel feishu --target "<chat_id>" \ + --media "main.pdf" --message "Beamer PDF" +``` + +--- + +## Output Language + +Match the user's query language. If the user writes in Chinese, produce Chinese slides and add `\usepackage[fontset=fandol]{ctex}` to the preamble. + +### Slide Title / Heading Language Consistency (MANDATORY) + +All slide titles, section headings, and block titles must use a **single language** — the same language as the slide body content. Do not mix languages in headings. + +| Slide language | Correct | Wrong | +|---|---|---| +| Chinese | Single-language Chinese heading | Mixed heading (e.g., adding "Outline" after the Chinese word for TOC) | +| English | Single-language English heading | Mixed heading (e.g., "Outline" followed by a Chinese translation) | + +Examples: a Chinese deck should use purely Chinese headings without appending English translations; an English deck should use purely English headings without appending Chinese translations. + +**The only exception** is established technical abbreviations that have no standard translation (e.g., "RCS", "VIGS", "PEG6000") — these may appear alongside their native-language explanation in body text, but headings should still be pure single-language. + +## Source Material Fidelity (MANDATORY) + +When the user provides a reference paper, thesis, or any source document: + +1. **Never change the language of source content.** If the paper title is in English, keep it in English on the title slide. If the authors wrote their names in a specific script (Chinese characters, Latin letters, etc.), preserve that script. Do not translate titles, author names, institution names, or reference entries into the slide language. + +2. **Author name abbreviation is the ONLY permitted modification.** You may shorten author lists (e.g., "Guo C., Zhang K., Li M., ..." → "Guo C. et al.") for space constraints on the title page or footer. No other changes to author names are allowed — do not transliterate, translate, or reorder them. + + **Author name format:** Use `Surname Initial.` with comma separation. Always include spaces between names: + ```latex + \author[Guo C. et al.]{Guo C., Zhang K., Sun H., Zhu L., Zhang Y., Wang G., Li A., Bai Z., Liu L., Li C.} + ``` + Never concatenate names without spaces (e.g., ❌ `CongcongGuo,KeZhang`). + +3. **Reference entries must preserve original language.** If a cited paper has a Chinese title, keep the Chinese title in the reference list. If it has an English title, keep English. Do not translate references to match the slide language. + +4. **Title page layout for bilingual scenarios.** When the slide language differs from the paper language (e.g., Chinese slides for an English paper), keep the original paper title in `\title{}` and put the translation in `\subtitle{}`. Never mix two languages at the same level: + ```latex + % ✅ Correct: English title + Chinese subtitle + \title{Root Cortical Senescence Enhances Drought Tolerance in Cotton} + \subtitle{根系皮层衰老增强棉花耐旱性} + + % ❌ Wrong: two languages jammed into \title + \title{Root Cortical Senescence Enhances Drought Tolerance in Cotton 根系皮层衰老增强棉花耐旱性} + ``` + +| Allowed | Forbidden | +|---|---| +| `Guo C. et al.` (abbreviated from full list) | Translating or transliterating author names between scripts/languages | +| Keeping English paper title on Chinese slides | Translating paper title to match slide language | +| Using `\scriptsize` for long author lists | Omitting authors entirely | +| Abbreviating institution names | Translating institution names | +| Original title in `\title{}` + translation in `\subtitle{}` | Mixing two languages in the same `\title{}` | + +## Compilation Rules + +1. `[fragile]` mandatory for `verbatim` / `lstlisting` frames +2. `\mathbb` takes ONE character — use `\mathrm{KL}` not `\mathbb{KL}` +3. Always brace subscripts — `_{\max}` not `_\max` +4. No Unicode symbols (★✓→) — use `$\star$`, `\checkmark`, `$\rightarrow$` +5. `nullfont` warnings with ctex+Madrid are cosmetic — ignore +6. Always `--runs 2` when using `\tableofcontents` or `\ref` + +--- + +## Typography and Visual Enhancement (NEW) + +### Text Alignment + +Use justified alignment as the default for body text in Beamer. It produces a cleaner, more professional look compared to left-aligned (ragged right) text, especially for paragraphs with mixed CJK and Latin content. + +```latex +% In preamble — set global justified alignment +\usepackage{ragged2e} +\justifying +``` + +Note: Lists (`itemize`/`enumerate`) remain left-aligned (do not force justified). Body paragraphs can have indentation, but must be justified. + +### Font and Spacing + +#### Font Size Hierarchy + +Establish clear visual hierarchy with distinct size levels: + +```latex +\setbeamerfont{title}{size=\LARGE, series=\bfseries} +\setbeamerfont{subtitle}{size=\large} +\setbeamerfont{frametitle}{size=\large, series=\bfseries} +\setbeamerfont{framesubtitle}{size=\normalsize} +\setbeamerfont{block title}{size=\normalsize, series=\bfseries} +\setbeamerfont{footnote}{size=\tiny} +``` + +**Document base size:** Choose an appropriate base size (`10pt`, `11pt`, or `12pt`) based on content density and audience distance. There is no fixed default — pick what fits the presentation best. +```latex +\documentclass[aspectratio=169, 11pt]{beamer} % Adjust pt as needed +``` + +For content-heavy individual slides, use local font size commands: +- Content-heavy slides: use `\small` or `\footnotesize` locally for that slide +- Content-light slides: keep the chosen base size for comfortable reading +- **Overflow warnings are critical**: if compilation produces `Overfull \vbox` or `Overfull \hbox` warnings, the content does not fit — reduce font size, reduce content, or split the slide. Never ignore overflow warnings. + +```latex +% Per-slide adjustment for dense content: +{\small +\begin{tabular}{...} + ... +\end{tabular} +} +``` + +#### Math Font Protection + +**Always add** for presentations with any math formulas: +```latex +\usefonttheme{professionalfonts} +``` +Without this, Beamer overrides math fonts causing distorted formulas. + +#### Serif vs Sans-serif + +| Font Theme | Effect | Best For | +|---|---|---| +| `default` (sans-serif) | Modern, clean | Most scenarios, tech/engineering | +| `serif` | More academic feel | Math-heavy, humanities papers | +| `structurebold` | Bold structural elements | Clear hierarchy emphasis | + +```latex +\usefonttheme{default} % Sans-serif (default) +\usefonttheme{serif} % Serif body text +\usefonttheme{structurebold} % Bold titles/headers +``` + +#### Chinese Font Setup + +```latex +\usepackage[fontset=fandol]{ctex} % Portable, bundled fonts +% macOS with system fonts installed: +% \usepackage[fontset=mac]{ctex} +``` + +ctex fontsets provide sensible defaults (Song for body, Hei for headings). Only use `\setCJKmainfont` manually if you need a specific font not covered by fontset. + +#### Bold Usage Restraint + +Use bold **only** for: titles, best values in tables, key terms on first appearance. Never bold entire paragraphs — overuse negates emphasis. + +### Consistent List Styles + +```latex +\setbeamertemplate{itemize items}[circle] % Primary level (use [circle], not \textbullet) +\setbeamertemplate{itemize subitem}{--} % Secondary level +\setbeamercolor{itemize item}{fg=myblue} % Match theme color +``` + +> **Note:** `references/beamer.md` Section 3.7 shows `\textbullet` as a generic customization example. For actual slide generation, always use `[circle]` as defined above. + +### Color Palette Guidelines + +- **Color style must be consistent**: use one color scheme throughout the entire presentation — never switch color styles or mix different palettes mid-deck +- Limit to **2-3 primary colors** (e.g., `myblue` for structure, `myred` for alerts, `mygreen` for examples) +- **All three must be defined** in the preamble via `\definecolor` — the recommended defaults are `myblue=#2B5EA7`, `myred=#C0392B`, `mygreen=#27AE60` (see Section 3 tcolorbox definitions). Adjust hex values to match the presentation's theme, but always define all three. +- **Do not overuse colors**: do not use different primary hues on different slides just for aesthetics. Alert color should only be used when emphasis or contrast is genuinely needed +- Use low-saturation tints for backgrounds (`myblue!8`) and full-saturation for frames/titles (`myblue!85`) +- Define all custom colors in the preamble with `\definecolor` using HTML hex values — do not introduce ad-hoc colors mid-document + +#### Preset Color Palettes + +When the user does not specify colors, pick a palette that fits the subject area or presentation tone. The **three-color semantic roles are unchanged** (blue = structural, red = warning/negative, green = positive/example) — only the hex values change. + +| # | Palette Name | myblue | myred | mygreen | Best For | +|---|---|---|---|---|---| +| 1 | **Classic Blue** (default) | `#2B5EA7` | `#C0392B` | `#27AE60` | General academic, engineering, CS | +| 2 | **Deep Ocean** | `#1A3C6E` | `#B83230` | `#1E8C5A` | Formal conferences, physics, math | +| 3 | **Teal Academic** | `#0E7C7B` | `#D35233` | `#2A9D6F` | Biology, ecology, environmental science | +| 4 | **Slate Purple** | `#4A3F8A` | `#C44536` | `#3A8F6E` | Humanities, social science, psychology | +| 5 | **Warm Earth** | `#6B4C3B` | `#C75C2E` | `#5B8C3E` | Agriculture, geology, archaeology | +| 6 | **Steel Gray** | `#3D4F5F` | `#C0392B` | `#2E8B6E` | Corporate, business, economics | +| 7 | **Burgundy** | `#7B2D4E` | `#C0392B` | `#2A7F62` | Medicine, clinical, health science | +| 8 | **Midnight** | `#1B2A4A` | `#E74C3C` | `#16A085` | Tech keynote, AI/ML, astronomy | + +```latex +% Example: Teal Academic palette +\definecolor{myblue}{HTML}{0E7C7B} +\definecolor{myred}{HTML}{D35233} +\definecolor{mygreen}{HTML}{2A9D6F} +``` + +**Selection heuristic:** If the user's topic or field clearly maps to a palette above, use it. If ambiguous, default to **Classic Blue (#1)**. If the user explicitly provides hex values or says "use red/purple/etc.", define custom colors accordingly — these presets are suggestions, not constraints. + +#### Block Color Semantic Rules (MANDATORY) + +Each color carries a fixed semantic meaning. **Never assign colors based on aesthetics or visual variety alone — always match color to content meaning.** + +| Color | Semantic meaning | When to use | +|---|---|---| +| `myblue` / `fblock` / `ablock` | **Neutral / structural / primary** | Default for most blocks — descriptions, methods, explanations, neutral content | +| `myred` / `falertblock` / `aalertblock` | **Negative / warning / problem** | ONLY for: problems, limitations, risks, caveats, things that went wrong | +| `mygreen` / `fexampleblock` / `aexampleblock` | **Positive / example / solution** | For: solutions, advantages, good results, examples, positive outcomes | + +**Hard rules:** + +1. **Red (`myred`) is EXCLUSIVELY for negative semantics.** Never use red/alertblock for neutral content like "Physiological Indicators", "Evaluation Setup", "Method Overview". If the content is not a problem/warning/limitation, it must NOT be red. + +2. **Parallel blocks of the same level must use the same color.** When multiple blocks on a slide are listing items of the same category (e.g., four research methods, three evaluation metrics), they are semantically equal and must ALL use the same block color — typically `myblue`. + +3. **Categorical distinction uses blue + green, never red.** If you want to visually distinguish two categories on the same slide (e.g., left column = observational methods, right column = molecular methods), use `myblue` for one category and `mygreen` for the other. Red is reserved for genuinely negative content only. + +```latex +% ✅ Correct: four parallel method blocks, all same color +\begin{columns}[T] + \column{0.48\textwidth} + \begin{fblock}[2.5cm]{Morphological Observation} + Content... + \end{fblock} + \vspace{0.2cm} + \begin{fblock}[2.5cm]{Microscopic Analysis} + Content... + \end{fblock} + \column{0.48\textwidth} + \begin{fblock}[2.5cm]{Biochemical Assays} + Content... + \end{fblock} + \vspace{0.2cm} + \begin{fblock}[2.5cm]{Gene Silencing (VIGS)} + Content... + \end{fblock} +\end{columns} + +% ✅ Also correct: categorical distinction with blue + green +\begin{columns}[T] + \column{0.48\textwidth} + % Observation category → blue + \begin{fblock}[2.5cm]{Morphological Observation}... + \end{fblock} + \vspace{0.2cm} + \begin{fblock}[2.5cm]{Microscopic Analysis}... + \end{fblock} + \column{0.48\textwidth} + % Molecular category → green + \begin{fexampleblock}[2.5cm]{Biochemical Assays}... + \end{fexampleblock} + \vspace{0.2cm} + \begin{fexampleblock}[2.5cm]{Gene Silencing (VIGS)}... + \end{fexampleblock} +\end{columns} + +% ❌ Wrong: red used for neutral content +\begin{falertblock}[2.5cm]{Biochemical Assays} % NOT a warning/problem! + Content... +\end{falertblock} +``` + +### Comparison Slide Visual Polish (MANDATORY) + +When a slide presents a **comparison** (e.g., "before vs after", "problem vs solution", "method A vs method B"), apply these rules: + +#### 1. Semantic Color Pairing + +Use color to reinforce meaning. If one side is the "problem" and the other is the "solution", their block colors must reflect this: + +| Semantic role | Block type | Color | +|---|---|---| +| Problem / limitation / before | `falertblock` | `myred` family | +| Solution / advantage / after | `fexampleblock` | `mygreen` family | +| Neutral / description | `fblock` | `myblue` family | + +**Symmetry rule:** If one side uses `\alert{}` (red) to highlight a negative keyword (e.g., "Breaks layout"), the other side SHOULD use `\textcolor{mygreen}{}` to highlight the corresponding positive keyword (e.g., "Eliminates overhead"). Left-red-right-green pairing creates instant visual comprehension. + +```latex +% Example: problem vs solution comparison +\begin{columns}[T] + \column{0.48\textwidth} + \begin{falertblock}[4.5cm]{Eager Broadcasting} + \begin{itemize} + \item Immediate materialization + \item \alert{Breaks sparsity layout} % red highlight for problem + \end{itemize} + \end{falertblock} + \column{0.48\textwidth} + \begin{fexampleblock}[4.5cm]{Lazy Broadcasting} + \begin{itemize} + \item Deferred evaluation + \item \textcolor{mygreen}{\textbf{Eliminates overhead}} % green highlight for solution + \end{itemize} + \end{fexampleblock} +\end{columns} +``` + +#### 2. Standalone Tables Must Be Wrapped + +Tables that appear alongside tcolorbox blocks on the same slide should be wrapped in a tcolorbox block for visual consistency. A bare `tabular` environment next to styled blocks looks unfinished. + +| Scenario | Treatment | +|---|---| +| Table relates to a specific topic | Wrap in `ablock{Topic Title}` | +| Table is a comparison/summary | Wrap in `ablock{Comparison}` or `aexampleblock{Summary}` | +| Table is the only element on the slide | May remain unwrapped with `\centering` | + +```latex +% ✅ Correct: table wrapped in block, consistent with other blocks on slide +\begin{ablock}{Optimization Level Comparison} + {\footnotesize + \begin{tabularx}{\linewidth}{lCCC} + \toprule + Level & Broadcast & Overhead & Performance \\ + \midrule + Eager & Yes & \textcolor{myred}{High} & Baseline \\ + Lazy & No & \textcolor{mygreen}{\textbf{None}} & 2.1$\times$ \\ + \bottomrule + \end{tabularx} + } +\end{ablock} + +% ❌ Wrong: bare table next to styled blocks +\begin{tabular}{lcc} + ... +\end{tabular} +``` + +#### 3. Table Cell Color Coding + +When table cells represent qualitative values (good/bad, yes/no, high/low), use color to encode meaning instead of relying on text alone: + +| Value type | Color treatment | +|---|---| +| Positive / good / improved | `\textcolor{mygreen}{\textbf{value}}` | +| Negative / bad / degraded | `\textcolor{myred}{value}` | +| Neutral / baseline | Default text color | +| Best in column/row | `\textcolor{mygreen}{\textbf{value}}` (bold + green) | + +This makes tables scannable at a glance — the audience can see the pattern without reading every cell. + +#### 4. Block Title Bar Saturation Consistency + +All block title bars on the same slide should have comparable visual weight. If one block uses `myblue!85` for the title bar, don't pair it with a `mygreen!100` title bar — the green will appear much heavier. Keep title bar saturation levels consistent: + +```latex +% ✅ Consistent: both use !85 saturation +colframe=myblue!85 % blue block +colframe=mygreen!80 % green block (slightly lower to compensate green's higher perceived brightness) + +% ❌ Inconsistent: one muted, one vivid +colframe=myblue!60 % muted blue +colframe=mygreen!100 % vivid green — too dominant +``` + +#### 5. List Style Consistency Within a Slide (MANDATORY) + +All list environments on the same slide must use the **same bullet/numbering style**. Do not mix `itemize` (bullets) with `enumerate` (numbers) or custom circled numbers unless the semantic distinction is clear and intentional. + +| Scenario | Correct approach | +|---|---| +| All lists are unordered collections | Use `itemize` with consistent bullet style everywhere | +| All lists are ordered steps/sequences | Use `enumerate` everywhere | +| One list is steps, another is features | OK to differ — but document the semantic reason | +| One block uses ❶❷❸, another uses • bullets | ❌ Inconsistent — pick one style | + +**Circled numbers / custom list markers:** +- If using circled numbers (❶❷❸❹), do NOT use raw Unicode characters — they may fail in some compilers +- Use LaTeX-safe alternatives: + +```latex +% Option 1: pifont (recommended) +\usepackage{pifont} +\newcommand{\cmark}[1]{\ding{\numexpr201+#1\relax}} % \cmark{1} → ❶ +% Usage: \item[\cmark{1}] First item + +% Option 2: tikz inline circles +\newcommand{\circnum}[1]{% + \tikz[baseline=(char.base)]\node[circle, fill=myblue, text=white, + inner sep=1.2pt, font=\scriptsize\bfseries] (char) {#1};} +% Usage: \item[\circnum{1}] First item +``` + +#### 6. Line Break Prevention for Technical Terms (MANDATORY) + +Technical terms, version numbers, short parenthetical units, and numeric expressions must not break across lines. Bad line breaks (e.g., "202" on one line and "LoC)" on the next) look unprofessional. + +**Prevention techniques:** + +| Technique | When to use | Example | +|---|---|---| +| Non-breaking space `~` | Between number and unit | `202~LoC`, `3.5~GHz` | +| `\mbox{...}` | Short phrase that must stay together | `\mbox{PyTorch 2.1}` | +| `\hbox{...}` | Same as mbox, TeX primitive | `\hbox{CUDA 12.0}` | +| `white-space: nowrap` equivalent: put in `\mbox{}` | Version strings, short labels | `\mbox{v2.0-beta}` | + +```latex +% ❌ Bad: "202" and "LoC)" split across lines +PyTorch BSR sparse (202 +LoC) + +% ✅ Good: kept together +PyTorch BSR sparse (\mbox{202 LoC}) +% or +PyTorch BSR sparse (202~LoC) +``` + +**Apply this proactively** — scan all slides for short trailing fragments (1-2 words or a number+unit orphaned on a new line) and fix them before delivery. + +#### 7. Content Density Balance Across Columns (MANDATORY) + +When using a two-column layout, both columns should have comparable **content density** (amount of meaningful content relative to the space). A column with sparse content (e.g., 7 one-line items in a tall block with excessive line spacing) next to a dense column creates visual imbalance. + +**Fixes for sparse columns:** + +| Strategy | When to use | +|---|---| +| Add brief descriptions to each item | Items are bare labels (e.g., just baseline names) | +| Split into two smaller blocks | Content naturally groups into sub-categories | +| Reduce block height + add a supplementary block | Room for an extra block below (e.g., "Evaluation Metrics", "Notes") | +| Adjust column width ratio | Give the denser column more space | + +```latex +% ❌ Sparse: 7 bare items in a tall block, lots of wasted space +\begin{fblock}[5.5cm]{Baselines} + \begin{itemize} + \item cuSPARSE + \item Triton Block-Sparse + \item TorchBSR + ... + \end{itemize} +\end{fblock} + +% ✅ Better: split into two themed blocks, balanced density +\begin{fblock}[2.5cm]{Vendor Libraries} + \begin{itemize} + \item cuSPARSE — NVIDIA official sparse BLAS + \item MKL Sparse — Intel CPU baseline + \end{itemize} +\end{fblock} +\vspace{0.2cm} +\begin{fblock}[2.8cm]{Research Implementations} + \begin{itemize} + \item Triton Block-Sparse — compiler-based + \item TorchBSR — PyTorch native (\mbox{202 LoC}) + ... + \end{itemize} +\end{fblock} +``` + +### icon Support + +Use `fontawesome5` to add icons for visual flair in lists and headings: + +```latex +\usepackage{fontawesome5} + +\begin{itemize} + \item[\faCheckCircle] Verified result + \item[\faLightbulb] Key insight + \item[\faExclamationTriangle] Caveat +\end{itemize} +``` + +### Overlay Usage (DEFAULT: OFF) + +Overlays are **disabled by default** for all Beamer presentations. All slide content should be fully visible without step-by-step reveals. This produces static slides that are easier to read, share as handouts, and navigate in PDF viewers. + +**The ONLY exception is the courseware (teaching slides) scenario.** When the user explicitly requests courseware, teaching slides, or lecture materials, overlays are enabled — specifically for **separating problem statements and proofs/solutions** so the instructor can reveal them step by step during class. + +#### When overlays are OFF (default — all scenarios except courseware) + +Do NOT use any of the following: +- `\item<N->` overlay markers on list items +- `\uncover<N->{...}` or `\only<N->{...}` wrappers +- `\pause` commands +- `\visible<N->{...}` or `\onslide<N->{...}` +- Any other Beamer overlay specification + +All content on every slide must be immediately visible. Write plain `\item` without overlay specs. Do not wrap blocks, figures, or equations in `\uncover` or `\only`. + +```latex +% ✅ Default (non-courseware): all content static, fully visible +\begin{itemize} + \item First key point + \item Second key point + \item Third key point +\end{itemize} + +\begin{ablock}{Background} + Context information... +\end{ablock} +\begin{aalertblock}{Problem} + The core challenge... +\end{aalertblock} +``` + +--- + +#### When overlays are ON (courseware scenario ONLY) + +Activate overlays **only when the user explicitly requests courseware, teaching slides, lecture materials, or classroom presentations.** In this scenario, use overlays specifically to separate: + +1. **Problem statements ** — shown first +2. **Proofs / solutions ** — revealed on the next overlay step + +This allows the instructor to present the problem, let students think, then reveal the answer. + +##### Courseware overlay pattern + +```latex +% ✅ Courseware: problem shown first, proof revealed on next click +\begin{frame}{Theorem: Triangle Inequality} + \begin{block}{Problem} + Prove that for any real numbers $a$ and $b$: $|a + b| \leq |a| + |b|$. + \end{block} + + \uncover<2->{% + \begin{exampleblock}{Proof} + We know that $-|a| \leq a \leq |a|$ and $-|b| \leq b \leq |b|$. + Adding these inequalities: $-(|a|+|b|) \leq a+b \leq |a|+|b|$. + Therefore $|a+b| \leq |a| + |b|$. \qed + \end{exampleblock} + } +\end{frame} + +% ✅ Courseware: step-by-step solution reveal +\begin{frame}{Example: Solving a Quadratic Equation} + \begin{block}{Problem} + Solve $x^2 - 5x + 6 = 0$. + \end{block} + + \uncover<2->{% + \begin{ablock}{Solution} + Factor: $(x-2)(x-3) = 0$ \\ + Therefore $x = 2$ or $x = 3$. + \end{ablock} + } +\end{frame} +``` + +##### What to animate in courseware mode + +| Element | Animate? | Method | +|---|---|---| +| **Problem / question statement** | Always visible (overlay `<1->`) | No overlay needed — shown by default | +| **Proof / solution / answer** | Revealed on step 2+ | `\uncover<2->{...}` wrapping the proof block | +| **Hints (optional)** | Revealed between problem and proof | `\uncover<2->{hint}`, `\uncover<3->{proof}` | +| **List items in proofs** | May be revealed step-by-step | `\item<N->` for each proof step | +| **Non-problem slides** (background, outline, summary) | Static — no overlay | No overlay specs | + +##### What NOT to animate even in courseware mode + +| Element | Reason | +|---|---| +| Slide titles / frame titles | Must be visible from the start | +| Section divider slides | Single-element, nothing to reveal | +| Title / closing slides | No progressive content | +| TOC / outline slides | Overview should be fully visible | +| Background / theory introduction slides | Not problem-solution format, keep static | + +##### Consistency rules (MANDATORY — applies when overlays are used) + +**Critical rule: consistency within a single list.** Within any single `itemize` or `enumerate` environment, either ALL `\item` specs have overlay markers (e.g. `\item<1->`) or NONE do. Mixing animated and static `\item` specs in the same list looks inconsistent and breaks the visual flow. + +**Clarification:** Inline overlay commands like `\alert<2>{text}` or `\textcolor<3>{...}` inside an item's content do NOT count as item-level overlays. It is fine to use `\alert<>` selectively on some items while all `\item` specs themselves remain un-overlayed. + +```latex +% ✅ Correct: all items animated (in a proof) +\begin{itemize} + \item<2-> Step 1: Assume the hypothesis + \item<3-> Step 2: Apply the theorem + \item<4-> Step 3: Conclude \qed +\end{itemize} + +% ✅ Correct: no items animated (problem statement, always visible) +\begin{itemize} + \item Given: $\triangle ABC$ with $\angle A = 90°$ + \item Prove: $BC^2 = AB^2 + AC^2$ +\end{itemize} + +% ❌ Wrong: mixed animation in one list +\begin{itemize} + \item<1-> First point + \item Second point % ← no overlay, inconsistent + \item<2-> Third point +\end{itemize} +``` + +##### Nested animation + +When a block contains a list, choose ONE level to animate — either animate the block as a whole, or animate the list items inside, but not both simultaneously (double-animation causes confusing timing): + +```latex +% ✅ Option A: animate the block, list appears all at once +\uncover<2->{% +\begin{ablock}{Proof} + \begin{itemize} + \item Step one + \item Step two + \end{itemize} +\end{ablock} +} + +% ✅ Option B: block always visible, animate items inside +\begin{ablock}{Solution Steps} + \begin{itemize} + \item<2-> Step one + \item<3-> Step two + \end{itemize} +\end{ablock} + +% ❌ Wrong: double animation (block AND items) +\uncover<2->{% +\begin{ablock}{Proof} + \begin{itemize} + \item<3-> Step one % block appears at 2, item at 3 — confusing + \item<4-> Step two + \end{itemize} +\end{ablock} +} +``` + +### Theme Recommendations + +| Style | Recommended Theme | Notes | +|---|---|---| +| Modern / minimal | `metropolis` (install separately) | Clean, built-in progress bar, dark mode support | +| Classic academic | `Madrid` + `\usecolortheme{dolphin}` | Reliable, widely used | +| Structure-heavy | `Berlin` or `Warsaw` | Built-in section navigation | +| Clean / corporate | `CambridgeUS` or `Boadilla` | Simple two-tone | +| Ultra-minimal | `default` + custom `\setbeamercolor` | Maximum flexibility | + +For `metropolis`, add to preamble: +```latex +\usetheme{metropolis} +\metroset{progressbar=frametitle} % progress bar in frame title +``` + +### Columns Layout Best Practices + +```latex +\begin{columns}[T] % [T] top · [c] center · [b] bottom alignment +``` + +**Vertical alignment decision table (single source of truth):** + +| Layout type | Alignment | Notes | +|---|---|---| +| Block-block / text-text | `[T]` | Top edges align, visually cleanest | +| Image-text | `[c]` | Image centers against text/block column | +| Image-text where `[c]` is insufficient | `[T]` + `\vspace` | Manually push image down to visual center (see Section 6 "Image Column Vertical Centering") | + +Do not duplicate these rules elsewhere — all alignment decisions reference this table. + +**Column widths:** +- Total of both columns should not exceed `0.96\textwidth` to leave gap between columns +- Give the wider side more space: e.g., image-heavy side gets `0.55\textwidth`, text side gets `0.42\textwidth` +- For equal-width layouts: `0.48\textwidth` each + +**Content alignment within columns:** +- Align block titles across columns: use `[T]` + same block type (both `fblock` with identical height) +- Side-by-side blocks must use `fblock`/`falertblock` with the **same height parameter** (see Section 3) +- Center images with `\centering`, keep text/lists left-aligned +- If one column has noticeably less content, either add an `ablock` to balance or adjust column width ratio + +--- + +--- + +## Compilation Troubleshooting: Missing Fonts in User-Provided Templates + +When users provide custom Beamer templates (`.sty` or `.tex`), the template may depend on specific system fonts. The most common compilation failure is `Package fontspec Error: The font "XXX" cannot be found`. + +### General Troubleshooting Workflow + +**1. Identify the missing font name from the error message** + +Extract the font name (e.g., "Kaiti SC") from the error and determine whether it is a system font expected by a ctex fontset, or a font manually specified via `\setCJKmainfont{...}` in the user's template. + +**2. Search for the font file on the system** + +```bash +# Full system search +find / -iname "*fontname*" 2>/dev/null +# Common font directories — macOS +ls ~/Library/Fonts/ /Library/Fonts/ /System/Library/Fonts/ /System/Library/Fonts/Supplemental/ +# macOS may store fonts under AssetsV2 +find /System/Library/AssetsV2 -iname "*.ttc" -o -iname "*.ttf" 2>/dev/null +# Common font directory — Linux +ls /usr/share/fonts/truetype/ +``` + +**3. Handle based on search results** + +| Situation | Solution | +|---|---| +| Font exists on system but not in standard Fonts directory (macOS) | Copy to `~/Library/Fonts/` so the compiler can find it | +| Font exists on system but not in standard Fonts directory (Linux) | Copy to `/usr/share/fonts/truetype/` then run `fc-cache -fv` | +| Font does not exist on the system (macOS) | Download and install the font file to `~/Library/Fonts/` | +| Font does not exist on the system (Linux) | Download the font file to `/usr/share/fonts/truetype/` then run `fc-cache -fv` | +| Cannot install fonts (server / no permissions) | Switch to `fontset=fandol` (bundled open-source fonts with ctex) or `fontset=none` + manually specify available fonts | + +**4. Verify** + +Recompile and confirm the error is gone. A noticeably larger PDF file size indicates fonts were successfully embedded. + +### Do Not Work Around the Problem + +When fonts are missing, **do not** bypass compilation errors by deleting font references (e.g., removing `\kaishu`, `\songti` commands). This causes the output to diverge from the user's expected template style. The correct approach is to install the missing fonts. + +### fontset Selection Reference + +| Scenario | Recommendation | +|---|---| +| macOS + need native Chinese font consistency | `fontset=mac` (ensure fonts are installed in standard paths) | +| Linux / server / no system fonts | `fontset=fandol` (open-source fonts bundled with ctex) | +| Need specific custom fonts | `fontset=none` + `\setCJKmainfont` / `\setCJKsansfont` manual specification | +| Using a user-provided `.sty` template | Keep the template's original fontset setting — only fix font installation | + +### Example: Missing Kaiti SC on macOS + +ctex `fontset=mac` requires "Kaiti SC". Newer versions of macOS store this font under the `AssetsV2` directory rather than the standard Fonts path, so tectonic cannot locate it automatically. Solution: + +```bash +# Find the font +find /System/Library/AssetsV2 -iname "Kaiti*" 2>/dev/null +# Copy to user font directory +cp /System/Library/AssetsV2/com_apple_MobileAsset_Font7/<hash>/AssetData/Kaiti.ttc ~/Library/Fonts/ +``` + +Similarly, if other macOS system fonts needed by ctex (Songti SC, Heiti SC, etc.) are also missing, use the same method to locate and copy them. + +--- + +## References + +- `references/beamer.md` — Theme catalogue, overlay syntax, content templates, TikZ examples +- `references/latex.md` — Non-Beamer LaTeX documents (papers, articles, theses) +- `scripts/pdf.py` — PDF compilation wrapper and extraction tool diff --git a/skills/ppt/components.md b/skills/ppt/components.md new file mode 100755 index 0000000..3ebe6ae --- /dev/null +++ b/skills/ppt/components.md @@ -0,0 +1,1671 @@ +# Components — Pre-built HTML Component Reference Library + +Each component is a **starting point, not a straitjacket**. Use them as inspiration and building blocks. + +**Three ways to use this library**: +1. **Direct use**: Copy a component template, replace `${...}` variables and text content (safest, guaranteed to convert correctly) +2. **Remix**: Start from a template, then freely modify spacing, font sizes, card styles, background treatments, decorative elements, and layout proportions (encouraged — this creates variety) +3. **Free creation**: Design entirely new layouts from scratch based on html2pptx.md technical constraints (most flexible — must test conversion) + +**The goal is visual diversity. If the deck looks monotonous, you haven't remixed enough.** + +--- + +## Usage Rules + +1. **Variable replacement**: All `${variableName}` must be replaced with actual values (colors, font names) +2. **Everything is modifiable**: Spacing, font sizes, padding, margins, gap, border-radius, shadows, card styles, background colors, layout proportions — change anything to serve the content and create variety +3. **Only engine constraints are sacred**: Don't use flex-wrap, negative margins, DIV background-image, or CSS gradients (see html2pptx.md Technical Constraints) +4. **Colors must come from the theme scale** — this is the one design-system rule that's enforced +5. **Adjacent slides should look distinctly different** — vary layout, background, card treatment, spacing density +6. **Fill the slide**: Content should use the available space well. Avoid large empty areas — if there's too much whitespace, add more content, use larger type, increase spacing, or add visual elements + +--- + +## Card Style Cookbook (Quick Reference) + +Every component template uses shadow-float cards by default. **Swap in these alternatives** to create variety across slides: + +| Style | Name | CSS | +|-------|------|-----| +| A | Shadow Float | `background:${surface-card}; border-radius:10pt; box-shadow:0 2pt 8pt rgba(0,0,0,0.08); padding:20pt;` | +| B | Outline | `background:transparent; border:1.5pt solid ${primary-20}; border-radius:10pt; padding:20pt;` | +| C | Solid Fill | `background:${primary-10}; border-radius:10pt; padding:20pt; border:none; box-shadow:none;` | +| D | Left Accent Bar | `background:${surface-card}; border-left:4pt solid ${accent}; border-radius:0 10pt 10pt 0; padding:16pt 20pt; box-shadow:none;` | +| E | Solid Fill + Bottom Accent | `background:${surface}; border-radius:10pt; border-bottom:3pt solid ${accent}; padding:20pt; box-shadow:none;` | +| F | Dark Card | `background:${primary-90}; border-radius:10pt; padding:20pt; color:${on-dark}; box-shadow:none;` | + +**Rule: No 2 adjacent slides should use the same card style.** + +--- + +## Title Bar Variants (Quick Reference) + +Components use a dark title bar by default. **Swap in these alternatives**: + +| Variant | Description | +|---------|-------------| +| Default | `height:56pt; background:${primary-90}` with white bold title at `font-size:22pt` | +| Accent Color | Same structure but `background:${accent}` | +| Transparent + Underline | `padding:28pt 48pt 12pt 48pt; border-bottom:2pt solid ${primary-20};` title at `font-size:28pt; color:${primary-80}` | +| No Header Bar (Inline Title) | No separate header div; title is part of content area at `font-size:32pt; color:${primary-80}` | +| Left Vertical Band | `display:flex; height:56pt;` with `width:6pt; background:${accent}` left strip + rest is `background:${surface}` | + +**Rule: All content slides must use the same title bar style — pick one variant and apply it consistently across the entire deck.** + +--- + +## Dark Background Content Templates + +Use these dark variants for **rhythm-breaking slides** (recommended every 3-4 pages). + +### content-dark-bullets — Dark Background Bullet List + +> `list | medium | dark | outline-light` + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-90};font-family:'Microsoft YaHei',sans-serif;"> + <div style="height:4pt;background:${accent};"></div> + <div style="padding:32pt 48pt 0 48pt;"> + <span style="font-size:28pt;font-weight:bold;color:${on-dark};white-space:nowrap;">Page Title</span> + <div style="width:40pt;height:3pt;background:${accent};margin-top:8pt;"></div> + </div> + <div style="padding:20pt 48pt 36pt 48pt;display:flex;flex-direction:column;gap:14pt;"> + <div style="background:rgba(255,255,255,0.08);border:1pt solid rgba(255,255,255,0.15);border-radius:8pt;padding:16pt 20pt;display:flex;align-items:flex-start;gap:12pt;"> + <div style="width:28pt;height:28pt;background:${accent};border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;"> + <span style="font-size:14pt;font-weight:bold;color:#FFFFFF;">1</span> + </div> + <div style="flex:1; min-width:0;"> + <span style="font-size:16pt;font-weight:bold;color:${on-dark};">Point Title</span><br/> + <span style="font-size:13pt;color:${on-dark-secondary};line-height:1.5;">Description text</span> + </div> + </div> + <!-- Repeat numbered items 2, 3, 4 with same structure --> + </div> +</body> +``` + +### content-dark-kpi — Dark Background KPI Dashboard + +> `grid | low | dark | solid-dark` + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-100};font-family:'Microsoft YaHei',sans-serif;"> + <div style="padding:36pt 48pt 0 48pt;text-align:center;"> + <span style="font-size:26pt;font-weight:bold;color:${on-dark};">Dashboard Title</span> + </div> + <div style="padding:24pt 48pt 36pt 48pt;display:flex;gap:16pt;justify-content:center;"> + <div style="width:192pt;background:rgba(255,255,255,0.06);border:1pt solid rgba(255,255,255,0.12);border-radius:10pt;padding:24pt 16pt;text-align:center;"> + <span style="font-size:38pt;font-weight:bold;color:${accent};white-space:nowrap;">85%</span><br/> + <span style="font-size:13pt;color:${on-dark-secondary};margin-top:8pt;">Metric Label</span> + </div> + <div style="width:192pt;background:rgba(255,255,255,0.06);border:1pt solid rgba(255,255,255,0.12);border-radius:10pt;padding:24pt 16pt;text-align:center;"> + <span style="font-size:38pt;font-weight:bold;color:${accent};white-space:nowrap;">2.4M</span><br/> + <span style="font-size:13pt;color:${on-dark-secondary};margin-top:8pt;">Metric Label</span> + </div> + <div style="width:192pt;background:rgba(255,255,255,0.06);border:1pt solid rgba(255,255,255,0.12);border-radius:10pt;padding:24pt 16pt;text-align:center;"> + <span style="font-size:38pt;font-weight:bold;color:${accent};white-space:nowrap;">+32%</span><br/> + <span style="font-size:13pt;color:${on-dark-secondary};margin-top:8pt;">Metric Label</span> + </div> + </div> +</body> +``` + +### content-dark-split — Dark Background Left-Right Split + +> `split | medium | dark | none` + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-90};font-family:'Microsoft YaHei',sans-serif;"> + <div style="display:flex;height:405pt;"> + <div style="width:260pt;background:${accent};padding:48pt 32pt;display:flex;flex-direction:column;justify-content:center;"> + <span style="font-size:42pt;font-weight:bold;color:#FFFFFF;line-height:1.15;">Key<br/>Insight</span> + <div style="width:32pt;height:3pt;background:rgba(255,255,255,0.5);margin-top:16pt;"></div> + </div> + <div style="flex:1;padding:40pt 36pt;display:flex;flex-direction:column;justify-content:center;gap:16pt;"> + <div style="display:flex;align-items:flex-start;gap:10pt;"> + <div style="width:4pt;height:40pt;background:${accent};flex-shrink:0;border-radius:2pt;margin-top:4pt;"></div> + <div style="flex:1; min-width:0;"> + <span style="font-size:16pt;font-weight:bold;color:${on-dark};">Sub-point One</span><br/> + <span style="font-size:13pt;color:${on-dark-secondary};line-height:1.5;">Detailed explanation</span> + </div> + </div> + <div style="display:flex;align-items:flex-start;gap:10pt;"> + <div style="width:4pt;height:40pt;background:${accent};flex-shrink:0;border-radius:2pt;margin-top:4pt;"></div> + <div style="flex:1; min-width:0;"> + <span style="font-size:16pt;font-weight:bold;color:${on-dark};">Sub-point Two</span><br/> + <span style="font-size:13pt;color:${on-dark-secondary};line-height:1.5;">Another detailed explanation</span> + </div> + </div> + </div> + </div> +</body> +``` + +--- + +## Component Metadata Tags + +Each component is tagged with metadata to help you plan slide variety: +- **Layout type**: `split` | `grid` | `list` | `focus` | `full-bleed` | `timeline` | `centered` +- **Density**: `high` | `medium` | `low` +- **Background**: `light` | `dark` | `image` | `any` +- **Default card style**: `shadow` | `outline` | `solid` | `tag` | `none` | `any` + +--- + +## Table of Contents + +### Cover +- [cover-center](#cover-center) — Centered cover `centered | low | light | none` +- [cover-split](#cover-split) — Left-right split cover `split | low | light | none` +- [cover-bottom-bar](#cover-bottom-bar) — Bottom info bar cover `centered | low | light | none` +- [cover-photo-mask](#cover-photo-mask) — Background image + mask cover `centered | low | image | none` +- [cover-dark-hero](#cover-dark-hero) — Dark hero cover, no photo needed `centered | low | dark | none` + +### TOC Pages +- [toc-sidebar-list](#toc-sidebar-list) — Sidebar + numbered list `split | medium | light | none` +- [toc-card-grid](#toc-card-grid) — Card grid TOC `grid | medium | light | shadow` +- [toc-timeline](#toc-timeline) — Horizontal timeline TOC `timeline | medium | light | none` +- [toc-big-number](#toc-big-number) — Large number background TOC `list | medium | light | none` + +### Content Pages +- [content-header-bullets](#content-header-bullets) — Header bar + bullet list `list | medium | light | none` +- [content-split-text-visual](#content-split-text-visual) — Text/image split (swap columns for image-left variant) `split | medium | light | none` +- [content-split-equal](#content-split-equal) — Equal 50/50 split `split | medium | light | none` +- [content-sidebar-stat](#content-sidebar-stat) — Sidebar large number + right content `split | medium | dark+light | none` +- [content-kpi-row](#content-kpi-row) — KPI large numbers in a row `grid | medium | light | shadow` +- [content-kpi-vertical](#content-kpi-vertical) — KPI numbers vertical stack `focus | medium | light | outline` +- [content-comparison](#content-comparison) — A vs B (or A vs B vs C) comparison `grid | medium | light | tag` +- [content-timeline](#content-timeline) — Horizontal process/steps `timeline | medium | light | none` +- [content-timeline-vertical](#content-timeline-vertical) — Vertical timeline `timeline | medium | light | none` +- [content-icon-grid](#content-icon-grid) — 2×3 icon grid `grid | high | light | none` +- [content-2x2-grid](#content-2x2-grid) — Four-quadrant grid (shadow) `grid | high | light | shadow` +- [content-2x2-grid-outline](#content-2x2-grid-outline) — Four-quadrant grid (outline) `grid | high | light | outline` +- [content-2x2-grid-solid](#content-2x2-grid-solid) — Four-quadrant grid (solid dark) `grid | high | dark | solid` +- [content-full-bleed](#content-full-bleed) — Full-bleed color block `full-bleed | low | dark | none` +- [content-left-accent-bar](#content-left-accent-bar) — Left accent bar emphasis `split | medium | light | none` +- [content-stagger-list](#content-stagger-list) — Staggered list `list | medium | light | none` +- [content-big-number-focus](#content-big-number-focus) — Large number focus `split | low | dark+light | none` +- [content-three-column](#content-three-column) — Three-column with icons `grid | medium | light | none` +- [content-three-card](#content-three-card) — Three card columns `grid | medium | light | shadow` +- [content-split-photo](#content-split-photo) — Left text + right photo `split | medium | light | none` +- [content-photo-cards](#content-photo-cards) — Three photo cards `grid | medium | light | shadow` +- [content-stat-overlay](#content-stat-overlay) — Hero stat on photo `full-bleed | low | image | none` +- [content-photo-overlay](#content-photo-overlay) — Full photo + floating card `full-bleed | low | image | frosted` +- [content-chart-focus](#content-chart-focus) — Chart-focused page `split | medium | light | none` +- [content-chart-bar](#content-chart-bar) — Bar chart + key metric cards `split | medium | light | none` +- [content-chart-pie](#content-chart-pie) — Pie chart + legend & insight `split | medium | light | none` +- [content-chart-line](#content-chart-line) — Wide line chart + trend stats `list | medium | light | none` +- [content-band-top](#content-band-top) — Top color band + content `list | medium | light | none` +- [content-table](#content-table) — Structured data table `data | medium | light | none` +- [content-table-comparison](#content-table-comparison) — Feature comparison matrix `data | medium | light | tag` + +### High-Impact Accent Components +- [chapter-divider-bold](#chapter-divider-bold) — Accent panel + dark chapter divider `split | low | dark | none` +- [content-hero-stat](#content-hero-stat) — Single-focus large metric page `centered | low | dark | none` +- [content-asymmetric](#content-asymmetric) — Asymmetric dark panel (38%) + content (62%) `split | medium | dark+light | none` +- [quote-emphasis](#quote-emphasis) — Full-slide pull quote on dark background `centered | low | dark | none` +- [content-dark-three-card](#content-dark-three-card) — Dark background three-card rhythm breaker `grid | medium | dark | solid-dark` + +### Data Visualization Pages (see [`data-viz-components.md`](data-viz-components.md)) +- content-horizontal-bars — Horizontal bar comparison +- content-stacked-bars — Stacked progress bars +- content-data-table — Structured data table +- content-quadrant-matrix — 2×2 quadrant matrix +- content-funnel — Sales/conversion funnel +- content-before-after — Before vs after +- content-dashboard — Data dashboard / KPI grid +- content-pyramid — Pyramid / layered hierarchy + +### Transition Pages +- [divider-bold-center](#divider-bold-center) — Centered bold transition `centered | low | light | none` +- [divider-split](#divider-split) — Split background transition `split | low | dark+light | none` +- [divider-photo-mask](#divider-photo-mask) — Background image + mask transition `centered | low | image | none` +- [divider-gradient](#divider-gradient) — Gradient background transition `centered | low | dark | none` + +### Closing Pages +- [closing-takeaways](#closing-takeaways) — Key takeaways summary `grid | medium | light | tag` +- [closing-thankyou](#closing-thankyou) — Thank you page `centered | low | dark | none` + +--- + +## Cover Components + +<a id="cover-center"></a> +### cover-center — Centered Cover + +Centered symmetrical layout. Accent dot + title + accent line divider + subtitle. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${surface};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="height:4pt;background:${accent};"></div> + <div style="flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 80pt;"> + <div style="width:10pt;height:10pt;border-radius:50%;background:${accent};margin:0 0 24pt 0;"></div> + <h1 style="font-size:40pt;font-weight:bold;color:${primary-80};margin:0;line-height:1.15;text-align:center;max-width:560pt;">Presentation Title Here</h1> + <div style="width:40pt;height:3pt;background:${accent};margin:20pt 0;"></div> + <p style="font-size:18pt;color:${primary-60};margin:0 0 28pt 0;line-height:1.4;text-align:center;max-width:440pt;">Subtitle or one-line overview</p> + <p style="font-size:13pt;color:${primary-40};margin:0;line-height:1.5;text-align:center;white-space:nowrap;">Presenter: John Smith · December 2024</p> + </div> +</body> +``` + +<a id="cover-split"></a> +### cover-split — Left-Right Split Cover + +Left dark block (40%) with top anchor label + bottom vertical bar. Right text with accent divider. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:row;"> + <!-- Left: dark panel --> + <div style="width:288pt;height:405pt;background:${primary-90};display:flex;flex-direction:column;justify-content:space-between;padding:36pt 32pt 40pt 36pt;"> + <div> + <div style="width:24pt;height:3pt;background:${accent};margin:0 0 10pt 0;"></div> + <span style="font-size:11pt;color:${accent};letter-spacing:2pt;line-height:1;white-space:nowrap;">ANNUAL REPORT 2024</span> + </div> + <div style="display:flex;align-items:flex-end;gap:12pt;"> + <div style="width:3pt;height:44pt;background:${accent};"></div> + <span style="font-size:13pt;color:${on-dark-secondary};line-height:1.5;">Department · Category</span> + </div> + </div> + <!-- Right: title area --> + <div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 48pt 0 40pt;"> + <h1 style="font-size:36pt;font-weight:bold;color:${primary-80};margin:0 0 16pt 0;line-height:1.2;">Presentation Main Title</h1> + <div style="width:40pt;height:3pt;background:${accent};margin:0 0 20pt 0;"></div> + <p style="font-size:17pt;color:${primary-60};margin:0 0 28pt 0;line-height:1.4;">Subtitle description text</p> + <p style="font-size:13pt;color:${primary-40};margin:0;line-height:1.5;white-space:nowrap;">Presenter: John Smith · Product Department</p> + </div> +</body> +``` + +<a id="cover-bottom-bar"></a> +### cover-bottom-bar — Bottom Info Bar Cover + +Full-width centered title + bottom dark info bar. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; align-items:center; padding:0 48pt;"> + <h1 style="font-size:34pt; font-weight:bold; color:${primary-80}; margin:0 0 12pt 0; line-height:1.15; text-align:center; max-width:580pt;">Presentation Main Title</h1> + <p style="font-size:18pt; font-weight:bold; color:${primary-60}; margin:0; line-height:1.3; text-align:center; max-width:480pt;">Subtitle description text</p> + </div> + <div style="width:720pt; height:48pt; background:${primary-90}; + display:flex; align-items:center; justify-content:center; gap:24pt;"> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0; line-height:1;">Presenter: John Smith</p> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0; line-height:1;">December 2024</p> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0; line-height:1;">Product Department</p> + </div> +</body> +``` + +<a id="cover-photo-mask"></a> +### cover-photo-mask — Background Image + Mask Cover + +Full-screen image + dark mask + centered title. **Download image first**: `curl -L "https://source.unsplash.com/1920x1080/?keyword" -o cover-bg.jpg` + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-image:url('cover-bg.jpg'); background-size:cover; background-position:center; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="position:absolute; top:0; left:0; width:720pt; height:405pt; + background-color:rgba(26,51,64,0.75);"></div> + <div style="position:relative; z-index:1; flex:1; display:flex; flex-direction:column; + justify-content:center; align-items:center;"> + <div style="width:40pt; height:3pt; background:${accent}; margin:0 0 24pt 0;"></div> + <h1 style="font-size:34pt; font-weight:bold; color:${on-dark}; margin:0 0 12pt 0; line-height:1.15; text-align:center; max-width:580pt;">Presentation Title Here</h1> + <p style="font-size:18pt; color:${on-dark-secondary}; margin:0 0 32pt 0; line-height:1.3; text-align:center; max-width:480pt;">Subtitle or one-line overview</p> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0; line-height:1.5; text-align:center;">Presenter · Date</p> + </div> +</body> +``` +**Mask color rule**: Replace `rgba(26,51,64,0.75)` RGB with theme's primary-90 value. + +<a id="cover-dark-hero"></a> +### cover-dark-hero — Dark Background Hero Cover (No Photo Needed) + +> `centered | low | dark | none` — ideal fallback when Unsplash fails, strong minimalist opening. + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-100};font-family:'Microsoft YaHei',sans-serif;"> + <div style="height:4pt;background:${accent};"></div> + <div style="height:401pt;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 80pt;"> + <div style="width:10pt;height:10pt;border-radius:50%;background:${accent};margin-bottom:24pt;"></div> + <span style="font-size:40pt;font-weight:bold;color:${on-dark};text-align:center;line-height:1.2;">Presentation Title</span> + <div style="width:48pt;height:3pt;background:${accent};margin:20pt 0;"></div> + <span style="font-size:18pt;color:${on-dark-secondary};text-align:center;line-height:1.4;">Subtitle or one-line description</span> + <span style="font-size:13pt;color:${on-dark-secondary};margin-top:28pt;">Presenter · Date</span> + </div> +</body> +``` + +--- + +## TOC Components + +<a id="toc-sidebar-list"></a> +### toc-sidebar-list — Sidebar + Numbered List + +Left dark sidebar (30%) + right numbered chapter list. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:row;"> + <div style="width:216pt; height:405pt; background:${primary-90}; + display:flex; flex-direction:column; justify-content:center; padding:0 32pt;"> + <p style="font-size:13pt; color:${accent}; margin:0 0 8pt 0; line-height:1; text-transform:uppercase; letter-spacing:2pt;">CONTENTS</p> + <h2 style="font-size:28pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.2;">Contents</h2> + </div> + <div style="width:504pt; height:405pt; + display:flex; flex-direction:column; justify-content:center; padding:0 48pt;"> + <div style="display:flex; align-items:baseline; padding:16pt 0; border-bottom:1pt solid ${primary-10};"> + <p style="font-size:22pt; font-weight:bold; color:${accent}; margin:0; line-height:1; width:48pt;">01</p> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.3;">Chapter One Title</p> + </div> + <div style="display:flex; align-items:baseline; padding:16pt 0; border-bottom:1pt solid ${primary-10};"> + <p style="font-size:22pt; font-weight:bold; color:${accent}; margin:0; line-height:1; width:48pt;">02</p> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.3;">Chapter Two Title</p> + </div> + <!-- Continue for chapters 03, 04... --> + </div> +</body> +``` + +<a id="toc-card-grid"></a> +### toc-card-grid — Card Grid TOC + +One numbered card per chapter, 3-4 columns. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${surface}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="padding:40pt 48pt 24pt 48pt;"> + <h2 style="font-size:28pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.2;">Contents</h2> + </div> + <div style="display:flex; gap:16pt; padding:0 48pt; flex:1;"> + <div style="width:140pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; + padding:24pt 16pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); display:flex; flex-direction:column;"> + <p style="font-size:32pt; font-weight:bold; color:${accent}; margin:0 0 12pt 0; line-height:1;">01</p> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3;">Chapter Title</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Brief description</p> + </div> + <div style="width:140pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; + padding:24pt 16pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); display:flex; flex-direction:column;"> + <p style="font-size:32pt; font-weight:bold; color:${accent}; margin:0 0 12pt 0; line-height:1;">02</p> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3;">Chapter Title</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Brief description</p> + </div> + <!-- Continue cards 03, 04... --> + </div> + <div style="height:36pt;"></div> +</body> +``` + +<a id="toc-timeline"></a> +### toc-timeline — Horizontal Timeline TOC + +Horizontal nodes + connecting lines + chapter titles. For sequential content. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="padding:40pt 48pt 32pt 48pt;"> + <h2 style="font-size:28pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.2;">Contents</h2> + </div> + <div style="flex:1; padding:0 48pt; display:flex; flex-direction:column; justify-content:center;"> + <div style="width:524pt; height:2pt; background:${primary-10}; margin:0 auto 0 50pt;"></div> + <div style="display:flex; justify-content:space-between; padding:0 24pt; margin-top:8pt;"> + <div style="display:flex; flex-direction:column; align-items:center; width:120pt;"> + <div style="width:28pt; height:28pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:#FFFFFF; margin:0; line-height:1;">01</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:12pt 0 4pt 0; line-height:1.3; text-align:center;">Chapter Title</p> + <p style="font-size:11pt; color:${primary-40}; margin:0; line-height:1.4; text-align:center;">Brief description</p> + </div> + <div style="display:flex; flex-direction:column; align-items:center; width:120pt;"> + <div style="width:28pt; height:28pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:#FFFFFF; margin:0; line-height:1;">02</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:12pt 0 4pt 0; line-height:1.3; text-align:center;">Chapter Title</p> + <p style="font-size:11pt; color:${primary-40}; margin:0; line-height:1.4; text-align:center;">Brief description</p> + </div> + <!-- Continue nodes 03, 04... --> + </div> + </div> + <div style="height:36pt;"></div> +</body> +``` + +<a id="toc-big-number"></a> +### toc-big-number — Large Number Background TOC + +Extra-large semi-transparent numbers as background, chapter titles float on top. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="padding:40pt 48pt 16pt 48pt;"> + <h2 style="font-size:28pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.2;">Contents</h2> + </div> + <div style="flex:1; padding:0 48pt; display:flex; flex-direction:column; justify-content:center; gap:8pt;"> + <div style="display:flex; align-items:center; padding:12pt 24pt; + background:${surface}; border-radius:10pt; position:relative; overflow:hidden;"> + <p style="font-size:56pt; font-weight:bold; color:rgba(27,42,74,0.06); margin:0; line-height:1; position:absolute; right:16pt; top:-4pt;">01</p> + <div style="width:3pt; height:32pt; background:${accent}; margin:0 16pt 0 0; border-radius:2pt;"></div> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.3; flex:1; min-width:0;">Chapter One Title</p> + </div> + <div style="display:flex; align-items:center; padding:12pt 24pt; + background:${surface}; border-radius:10pt; position:relative; overflow:hidden;"> + <p style="font-size:56pt; font-weight:bold; color:rgba(27,42,74,0.06); margin:0; line-height:1; position:absolute; right:16pt; top:-4pt;">02</p> + <div style="width:3pt; height:32pt; background:${accent}; margin:0 16pt 0 0; border-radius:2pt;"></div> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.3; flex:1; min-width:0;">Chapter Two Title</p> + </div> + <!-- Continue rows 03, 04... --> + </div> + <div style="height:36pt;"></div> +</body> +``` + +--- + +## Content Page Components + +<a id="content-header-bullets"></a> +### content-header-bullets — Header Bar + Bullet List + +Dark header bar + bullet point list with left accent bars. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Page Title</h2> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt; gap:12pt;"> + <div style="display:flex; align-items:flex-start; gap:12pt;"> + <div style="width:3pt; min-height:36pt; background:${accent}; border-radius:2pt; margin-top:2pt;"></div> + <div style="flex:1; min-width:0;"> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.5;">Key Point Title One</p> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5;">Description text for the key point</p> + </div> + </div> + <div style="display:flex; align-items:flex-start; gap:12pt;"> + <div style="width:3pt; min-height:36pt; background:${accent}; border-radius:2pt; margin-top:2pt;"></div> + <div style="flex:1; min-width:0;"> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.5;">Key Point Title Two</p> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5;">Description text for the key point</p> + </div> + </div> + <!-- Continue points 3, 4, 5... --> + </div> +</body> +``` + +<a id="content-split-text-visual"></a> +### content-split-text-visual — Text / Image Split + +Left text (40%) + right image/chart (60%). **For image-left layout, swap the two columns** (put the image div first at `width:360pt`, text div second at `width:240pt`). + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="width:720pt;height:56pt;background:${primary-90};display:flex;align-items:center;padding:0 48pt;"> + <h2 style="font-size:22pt;font-weight:bold;color:${on-dark};margin:0;line-height:1.25;">Page Title</h2> + </div> + <div style="flex:1;display:flex;padding:0 48pt;gap:24pt;align-items:center;"> + <div style="width:240pt;display:flex;flex-direction:column;justify-content:center;"> + <p style="font-size:18pt;font-weight:bold;color:${primary-80};margin:0 0 12pt 0;line-height:1.3;">Sub-heading</p> + <ul style="font-size:15pt;color:${primary-60};margin:0;padding-left:20pt;line-height:23pt;"> + <li>First key point</li> + <li>Second key point</li> + <li>Third key point</li> + </ul> + <p style="font-size:11pt;color:${primary-40};margin:16pt 0 0 0;line-height:1.4;">Source: 2024 Annual Report</p> + </div> + <div style="width:360pt;background:${surface};border-radius:10pt;display:flex;align-items:center;justify-content:center;min-height:200pt;"> + <img src="content-img.jpg" style="width:360pt;height:240pt;object-fit:cover;border-radius:10pt;display:block;" /> + </div> + </div> +</body> +``` + +<a id="content-split-equal"></a> +### content-split-equal — Equal 50/50 Split + +Two equal columns with a vertical divider. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Page Title</h2> + </div> + <div style="flex:1; display:flex; padding:0 48pt; gap:16pt; align-items:center;"> + <div style="width:296pt; flex-shrink:0;"> + <div style="width:40pt; height:3pt; background:${accent}; margin:0 0 12pt 0;"></div> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3;">Left Column Title</p> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5;">Left column content text.</p> + </div> + <div style="width:1pt; height:180pt; background:${primary-10};"></div> + <div style="width:296pt; flex-shrink:0;"> + <div style="width:40pt; height:3pt; background:${accent}; margin:0 0 12pt 0;"></div> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3;">Right Column Title</p> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5;">Right column content text.</p> + </div> + </div> +</body> +``` + +<a id="content-sidebar-stat"></a> +### content-sidebar-stat — Sidebar Large Number + Right Content + +Left narrow sidebar with KPIs, right side with detailed content. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:row;"> + <div style="width:200pt; height:405pt; background:${primary-90}; + display:flex; flex-direction:column; justify-content:center; align-items:center; padding:0 24pt;"> + <p style="font-size:40pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1; text-align:center;">86%</p> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0; line-height:1.4; text-align:center;">User Satisfaction</p> + <div style="width:32pt; height:1.5pt; background:${accent}; margin:24pt 0;"></div> + <p style="font-size:40pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1; text-align:center;">2.4×</p> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0; line-height:1.4; text-align:center;">Efficiency Improvement</p> + </div> + <div style="width:520pt; display:flex; flex-direction:column;"> + <div style="padding:40pt 48pt 16pt 40pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.25;">Core Business Results</h2> + </div> + <div style="padding:0 48pt 0 40pt; flex:1;"> + <ul style="font-size:15pt; color:${primary-60}; margin:0; padding-left:20pt; line-height:23pt;"> + <li>User satisfaction rose from 72% to 86%</li> + <li>Operational efficiency improved 2.4x YoY</li> + <li>Client renewal rate reached 95%</li> + <li>New customer acquisition cost reduced by 35%</li> + </ul> + </div> + </div> +</body> +``` + +<a id="content-kpi-row"></a> +### content-kpi-row — KPI Large Numbers in a Row + +2-4 key data indicators displayed horizontally, cards with shadow elevation. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${surface}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Core Business Metrics</h2> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt;"> + <div style="display:flex; gap:16pt; margin-bottom:16pt;"> + <div style="width:192pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; + padding:24pt 16pt; text-align:center; box-shadow:0 3pt 10pt rgba(0,0,0,0.08);"> + <p style="font-size:40pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1;">86%</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.4;">User Retention Rate</p> + </div> + <div style="width:192pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; + padding:24pt 16pt; text-align:center; box-shadow:0 3pt 10pt rgba(0,0,0,0.08);"> + <p style="font-size:40pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1;">2.4×</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.4;">Customer Acquisition Efficiency</p> + </div> + <!-- Continue KPI card 3 (and 4)... --> + </div> + <p style="font-size:11pt; color:${primary-40}; margin:0; line-height:1.4;">Data source: 2024 Annual User Survey | Period: Jan-Dec 2024</p> + </div> +</body> +``` + +<a id="content-kpi-vertical"></a> +### content-kpi-vertical — KPI Numbers Vertical Stack + +> `focus | medium | light | outline` + +Left dark sidebar title + right KPI stack with outline cards. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:row;"> + <div style="width:240pt; height:405pt; background:${primary-90}; + display:flex; flex-direction:column; justify-content:center; padding:0 32pt;"> + <div style="width:32pt; height:3pt; background:${accent}; margin:0 0 16pt 0;"></div> + <h2 style="font-size:28pt; font-weight:bold; color:${on-dark}; margin:0 0 8pt 0; line-height:1.2;">Key Metrics</h2> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0; line-height:1.5;">Performance highlights for 2024</p> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 40pt; gap:16pt;"> + <div style="display:flex; align-items:center; gap:24pt; padding:16pt 24pt; + border:1.5pt solid ${primary-20}; border-radius:10pt;"> + <p style="font-size:40pt; font-weight:bold; color:${accent}; margin:0; line-height:1; white-space:nowrap;">92%</p> + <div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Customer Satisfaction</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Up 12 points from prior year</p> + </div> + </div> + <div style="display:flex; align-items:center; gap:24pt; padding:16pt 24pt; + border:1.5pt solid ${primary-20}; border-radius:10pt;"> + <p style="font-size:40pt; font-weight:bold; color:${accent}; margin:0; line-height:1; white-space:nowrap;">3.1×</p> + <div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Revenue Growth</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Fastest in company history</p> + </div> + </div> + <!-- Continue KPI item 3... --> + </div> +</body> +``` + +<a id="content-comparison"></a> +### content-comparison — A vs B (or A vs B vs C) Comparison + +Comparison cards with top accent border. **For 2-way**: two cards at `width:296pt`. **For 3-way**: three cards at `width:192pt;flex-shrink:0` with border colors `${accent}` / `${primary-60}` / `${primary-40}`. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="width:720pt;height:56pt;background:${primary-90};display:flex;align-items:center;padding:0 48pt;"> + <h2 style="font-size:22pt;font-weight:bold;color:${on-dark};margin:0;line-height:1.25;">Plan Comparison</h2> + </div> + <div style="flex:1;display:flex;justify-content:center;align-items:center;padding:0 48pt;gap:16pt;"> + <div style="width:296pt;height:240pt;background:${surface};border-radius:10pt;padding:24pt;border-top:3pt solid ${accent};"> + <p style="font-size:18pt;font-weight:bold;color:${primary-80};margin:0 0 16pt 0;line-height:1.3;">Plan A</p> + <ul style="font-size:15pt;color:${primary-60};margin:0;padding-left:20pt;line-height:23pt;"> + <li>Advantage one</li> + <li>Advantage two</li> + <li>Advantage three</li> + </ul> + </div> + <div style="width:296pt;height:240pt;background:${surface};border-radius:10pt;padding:24pt;border-top:3pt solid ${primary-40};"> + <p style="font-size:18pt;font-weight:bold;color:${primary-80};margin:0 0 16pt 0;line-height:1.3;">Plan B</p> + <ul style="font-size:15pt;color:${primary-60};margin:0;padding-left:20pt;line-height:23pt;"> + <li>Feature one</li> + <li>Feature two</li> + <li>Feature three</li> + </ul> + </div> + <!-- For 3-way: add third card at width:192pt, border-top:3pt solid ${primary-40}; reduce other cards to width:192pt --> + </div> +</body> +``` + +<a id="content-timeline"></a> +### content-timeline — Horizontal Process/Steps + +Horizontal step nodes + descriptions. 3-5 steps. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Implementation Roadmap</h2> + </div> + <div style="flex:1; padding:32pt 48pt 36pt 48pt; display:flex; flex-direction:column; justify-content:center;"> + <div style="width:524pt; height:2pt; background:${primary-10}; margin:0 auto 0 48pt;"></div> + <div style="display:flex; justify-content:space-between; padding:0 16pt; margin-top:8pt;"> + <div style="width:140pt; display:flex; flex-direction:column; align-items:center;"> + <div style="width:32pt; height:32pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:15pt; font-weight:bold; color:#FFFFFF; margin:0; line-height:1;">1</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:12pt 0 4pt 0; line-height:1.3; text-align:center;">Requirements Analysis</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5; text-align:center;">Research user needs</p> + </div> + <div style="width:140pt; display:flex; flex-direction:column; align-items:center;"> + <div style="width:32pt; height:32pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:15pt; font-weight:bold; color:#FFFFFF; margin:0; line-height:1;">2</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:12pt 0 4pt 0; line-height:1.3; text-align:center;">Solution Design</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5; text-align:center;">Implementation plan</p> + </div> + <!-- Continue steps 3, 4... --> + </div> + </div> +</body> +``` + +<a id="content-timeline-vertical"></a> +### content-timeline-vertical — Vertical Timeline + +> `timeline | medium | light | none` — Vertical flow with numbered nodes and connecting lines. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="padding:32pt 48pt 16pt 48pt;"> + <h2 style="font-size:28pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.2;">Timeline</h2> + <div style="width:40pt; height:3pt; background:${accent};"></div> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt;"> + <div style="display:flex; align-items:flex-start; gap:16pt; margin-bottom:20pt;"> + <div style="display:flex; flex-direction:column; align-items:center; flex-shrink:0;"> + <div style="width:28pt; height:28pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:#FFFFFF; margin:0; line-height:1;">1</p> + </div> + <div style="width:2pt; height:24pt; background:${primary-10};"></div> + </div> + <div style="padding-top:4pt;"> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Phase One — Discovery</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Research and stakeholder interviews</p> + </div> + </div> + <div style="display:flex; align-items:flex-start; gap:16pt; margin-bottom:20pt;"> + <div style="display:flex; flex-direction:column; align-items:center; flex-shrink:0;"> + <div style="width:28pt; height:28pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:#FFFFFF; margin:0; line-height:1;">2</p> + </div> + <div style="width:2pt; height:24pt; background:${primary-10};"></div> + </div> + <div style="padding-top:4pt;"> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Phase Two — Design</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Prototyping and iterative design</p> + </div> + </div> + <!-- Continue phases 3, 4... --> + </div> +</body> +``` + +<a id="content-icon-grid"></a> +### content-icon-grid — 2×3 Icon Grid + +Grid display of 6 features. **Two independent flex rows, no flex-wrap**. Icon positions use colored circle placeholders. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Core Features</h2> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt;"> + <div style="display:flex; gap:16pt; margin-bottom:24pt;"> + <div style="width:192pt; display:flex; flex-direction:column; align-items:flex-start;"> + <div style="width:36pt; height:36pt; border-radius:8pt; background:${primary-10}; display:flex; align-items:center; justify-content:center; margin-bottom:8pt;"> + <p style="font-size:18pt; color:${accent}; margin:0; line-height:1;">⚡</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.5;">Feature Name</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Brief description</p> + </div> + <div style="width:192pt; display:flex; flex-direction:column; align-items:flex-start;"> + <div style="width:36pt; height:36pt; border-radius:8pt; background:${primary-10}; display:flex; align-items:center; justify-content:center; margin-bottom:8pt;"> + <p style="font-size:18pt; color:${accent}; margin:0; line-height:1;">📊</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.5;">Feature Name</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Brief description</p> + </div> + <div style="width:192pt; display:flex; flex-direction:column; align-items:flex-start;"> + <div style="width:36pt; height:36pt; border-radius:8pt; background:${primary-10}; display:flex; align-items:center; justify-content:center; margin-bottom:8pt;"> + <p style="font-size:18pt; color:${accent}; margin:0; line-height:1;">🔒</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.5;">Feature Name</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Brief description</p> + </div> + </div> + <!-- Row 2: repeat same structure with 3 more icon items --> + </div> +</body> +``` + +<a id="content-2x2-grid"></a> +### content-2x2-grid — Four-Quadrant Grid (Shadow Cards) + +Four equal-sized cards. **Two independent flex rows, no flex-wrap**. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Four Core Pillars</h2> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt;"> + <div style="display:flex; gap:16pt; margin-bottom:16pt;"> + <div style="width:296pt; height:113pt; background:${surface}; border-radius:10pt; padding:16pt 20pt;"> + <p style="font-size:18pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1.3;">Pillar One</p> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5;">Description content</p> + </div> + <div style="width:296pt; height:113pt; background:${surface}; border-radius:10pt; padding:16pt 20pt;"> + <p style="font-size:18pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1.3;">Pillar Two</p> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5;">Description content</p> + </div> + </div> + <!-- Row 2: same structure for Pillars Three and Four --> + </div> +</body> +``` + +<a id="content-2x2-grid-outline"></a> +### content-2x2-grid-outline — Four-Quadrant Grid (Outline Cards) + +> `grid | high | light | outline` — Same layout as content-2x2-grid but card style is outline. + +**Diff from shadow version**: Replace card div `background:${surface}; border-radius:10pt;` with: +``` +border:1.5pt solid ${primary-20}; border-radius:10pt; padding:16pt 20pt; +``` +Remove any `box-shadow`. All other structure identical. + +<a id="content-2x2-grid-solid"></a> +### content-2x2-grid-solid — Four-Quadrant Grid (Solid Dark Cards) + +> `grid | high | dark | solid` — Dark background + solid primary-colored cards. + +**Diff from shadow version**: Background is `${primary-90}`, cards use `background:${primary-80}`, text colors switch to `${on-dark}` / `${on-dark-secondary}`. Title uses `${on-dark}` with accent underline divider. No box-shadow. + +<a id="content-full-bleed"></a> +### content-full-bleed — Full-Bleed Color Block Emphasis Page + +Full dark background + decorative element + key insight. For visual rhythm variation. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${primary-90}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column; justify-content:center; align-items:center;"> + <div style="width:48pt; height:48pt; border-radius:50%; background:${accent}; + display:flex; align-items:center; justify-content:center; margin:0 0 24pt 0;"> + <p style="font-size:22pt; font-weight:bold; color:${primary-90}; margin:0; line-height:1;">★</p> + </div> + <h2 style="font-size:28pt; font-weight:bold; color:${on-dark}; margin:0 0 16pt 0; line-height:1.3; text-align:center; max-width:520pt;">Core Insight or Key Quote</h2> + <div style="width:40pt; height:3pt; background:${accent}; margin:0 0 16pt 0;"></div> + <p style="font-size:15pt; color:${on-dark-secondary}; margin:0; line-height:1.5; text-align:center; max-width:440pt;">Supporting text to explain or expand on the core insight</p> +</body> +``` + +<a id="content-left-accent-bar"></a> +### content-left-accent-bar — Left Accent Bar Emphasis Page + +Left thick accent bar + left color block + right content with numbered points. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:row;"> + <div style="width:8pt; height:405pt; background:${accent};"></div> + <div style="width:200pt; height:405pt; background:${primary-5}; + display:flex; flex-direction:column; justify-content:center; padding:0 24pt;"> + <p style="font-size:44pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1;">!</p> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.3;">Key Conclusion</p> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt 0 32pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${primary-80}; margin:0 0 16pt 0; line-height:1.25;">Conclusion Title</h2> + <div style="display:flex; flex-direction:column; gap:12pt;"> + <div style="display:flex; align-items:flex-start; gap:12pt;"> + <div style="width:24pt; height:24pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:2pt;"> + <p style="font-size:11pt; font-weight:bold; color:${primary-90}; margin:0; line-height:1;">1</p> + </div> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5; flex:1; min-width:0;">Detailed description of the first conclusion point</p> + </div> + <div style="display:flex; align-items:flex-start; gap:12pt;"> + <div style="width:24pt; height:24pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center; flex-shrink:0; margin-top:2pt;"> + <p style="font-size:11pt; font-weight:bold; color:${primary-90}; margin:0; line-height:1;">2</p> + </div> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5; flex:1; min-width:0;">Detailed description of the second conclusion point</p> + </div> + <!-- Continue points 3, 4... --> + </div> + </div> +</body> +``` + +<a id="content-stagger-list"></a> +### content-stagger-list — Staggered List + +Each item has a differently colored number block. For processes, rankings, priority lists. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${surface}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="padding:32pt 48pt 16pt 48pt;"> + <h2 style="font-size:28pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.2;">List Title</h2> + <div style="width:40pt; height:3pt; background:${accent};"></div> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt;"> + <div style="display:flex; align-items:center; gap:16pt; margin-bottom:16pt;"> + <div style="width:48pt; height:48pt; border-radius:10pt; background:${primary-80}; display:flex; align-items:center; justify-content:center; flex-shrink:0;"> + <p style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">01</p> + </div> + <div style="flex:1; min-width:0;"> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Step One Title</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Description text</p> + </div> + </div> + <div style="display:flex; align-items:center; gap:16pt; margin-bottom:16pt;"> + <div style="width:48pt; height:48pt; border-radius:10pt; background:${primary-60}; display:flex; align-items:center; justify-content:center; flex-shrink:0;"> + <p style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">02</p> + </div> + <div style="flex:1; min-width:0;"> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Step Two Title</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Description text</p> + </div> + </div> + <!-- Continue items 03 (${primary-40}), 04 (${accent})... varying bg colors --> + </div> +</body> +``` + +<a id="content-big-number-focus"></a> +### content-big-number-focus — Large Number Focus Page + +Oversized number + explanatory text. For data highlights, milestones. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${primary-5}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:row;"> + <div style="width:360pt; height:405pt; background:${primary-90}; + display:flex; flex-direction:column; justify-content:center; align-items:center;"> + <p style="font-size:44pt; font-weight:bold; color:${accent}; margin:0 0 8pt 0; line-height:1;">15 Million</p> + <div style="width:40pt; height:3pt; background:${accent}; margin:0 0 12pt 0;"></div> + <p style="font-size:15pt; color:${on-dark-secondary}; margin:0; line-height:1.5; text-align:center;">Number Meaning Label</p> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 40pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${primary-80}; margin:0 0 16pt 0; line-height:1.25;">The Story Behind the Number</h2> + <p style="font-size:15pt; color:${primary-60}; margin:0 0 16pt 0; line-height:1.6;">Detailed interpretation and background explanation</p> + <div style="display:flex; gap:24pt;"> + <div> + <p style="font-size:22pt; font-weight:bold; color:${accent}; margin:0; line-height:1;">85%</p> + <p style="font-size:11pt; color:${primary-40}; margin:4pt 0 0 0; line-height:1.3;">Related Metric A</p> + </div> + <div> + <p style="font-size:22pt; font-weight:bold; color:${accent}; margin:0; line-height:1;">3.2x</p> + <p style="font-size:11pt; color:${primary-40}; margin:4pt 0 0 0; line-height:1.3;">Related Metric B</p> + </div> + </div> + </div> +</body> +``` + +<a id="content-three-column"></a> +### content-three-column — Three-Column Equal-Width Content + +Three columns with circular icons + vertical dividers. For "three advantages", "three phases", etc. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="padding:32pt 48pt 0 48pt;"> + <h2 style="font-size:28pt;font-weight:bold;color:${primary-80};margin:0 0 4pt 0;line-height:1.2;">Three-Column Title</h2> + <div style="width:40pt;height:3pt;background:${accent};"></div> + </div> + <div style="flex:1;display:flex;justify-content:center;align-items:center;padding:0 48pt;gap:24pt;"> + <div style="width:184pt;display:flex;flex-direction:column;align-items:center;text-align:center;"> + <div style="width:56pt;height:56pt;border-radius:50%;background:${primary-10};border:3pt solid ${accent};display:flex;align-items:center;justify-content:center;margin-bottom:16pt;"> + <p style="font-size:22pt;font-weight:bold;color:${accent};margin:0;line-height:1;">A</p> + </div> + <p style="font-size:18pt;font-weight:bold;color:${primary-80};margin:0 0 8pt 0;line-height:1.3;">Column One Title</p> + <p style="font-size:13pt;color:${primary-40};margin:0;line-height:1.5;">Brief description text</p> + </div> + <div style="width:1pt;height:140pt;background:${primary-10};"></div> + <!-- Column B: same structure, letter "B", "Column Two Title" --> + <div style="width:1pt;height:140pt;background:${primary-10};"></div> + <!-- Column C: same structure, letter "C", "Column Three Title" --> + </div> +</body> +``` + +<a id="content-three-card"></a> +### content-three-card — Three Card Columns + +> `grid | medium | light | shadow` — Three equal shadow cards with icon boxes. Different from three-column (which has circle icons + dividers). + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${surface}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Three Key Aspects</h2> + </div> + <div style="flex:1; display:flex; justify-content:center; align-items:center; padding:0 48pt; gap:16pt;"> + <div style="width:192pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; padding:24pt 16pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08);"> + <div style="width:36pt; height:36pt; border-radius:8pt; background:${primary-10}; display:flex; align-items:center; justify-content:center; margin-bottom:12pt;"> + <p style="font-size:18pt; color:${accent}; margin:0; line-height:1;">A</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3;">Card Title One</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Descriptive text</p> + </div> + <div style="width:192pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; padding:24pt 16pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08);"> + <div style="width:36pt; height:36pt; border-radius:8pt; background:${primary-10}; display:flex; align-items:center; justify-content:center; margin-bottom:12pt;"> + <p style="font-size:18pt; color:${accent}; margin:0; line-height:1;">B</p> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3;">Card Title Two</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Descriptive text</p> + </div> + <!-- Card 3 same structure --> + </div> +</body> +``` + +<a id="content-split-photo"></a> +### content-split-photo — Left Text + Right Photo + +Left: title + text with accent bar. Right: photograph with rounded corners. **Requires downloaded image.** + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h1 style="font-size:28pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.2;">Page Title Here</h1> + </div> + <div style="flex:1; display:flex; flex-direction:row; justify-content:center; + padding:0 48pt; gap:24pt; align-items:center;"> + <div style="width:296pt; flex-shrink:0; display:flex; flex-direction:column; justify-content:center;"> + <div style="width:40pt; height:3pt; background:${accent}; margin:0 0 12pt 0;"></div> + <h2 style="font-size:22pt; font-weight:bold; color:${primary-80}; margin:0 0 12pt 0; line-height:1.3;">Sub-heading Text</h2> + <p style="font-size:15pt; color:${primary-60}; margin:0 0 10pt 0; line-height:1.5;">First paragraph explaining a key concept.</p> + <p style="font-size:15pt; color:${primary-60}; margin:0 0 10pt 0; line-height:1.5;">Second paragraph with additional evidence.</p> + <p style="font-size:15pt; color:${primary-60}; margin:0; line-height:1.5;">Third paragraph with concluding insight.</p> + </div> + <div style="width:296pt; flex-shrink:0; display:flex; align-items:center; justify-content:center;"> + <div style="border-radius:10pt; overflow:hidden; box-shadow:0 4pt 16pt rgba(0,0,0,0.12);"> + <img src="content-img.jpg" style="width:296pt; height:220pt; object-fit:cover; display:block;" /> + </div> + </div> + </div> +</body> +``` + +<a id="content-photo-cards"></a> +### content-photo-cards — Three Photo Cards + +Three cards, each with photo header, title, and description. **Requires 3 downloaded images.** + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${primary-5}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h1 style="font-size:28pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.2;">Page Title Here</h1> + </div> + <div style="flex:1; display:flex; flex-direction:column; justify-content:center;"> + <div style="display:flex; gap:16pt; padding:0 48pt;"> + <div style="width:192pt; flex-shrink:0; background:${background}; border-radius:10pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); overflow:hidden;"> + <img src="card1.jpg" style="width:192pt; height:120pt; object-fit:cover; display:block;" /> + <div style="padding:16pt;"> + <h3 style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3; white-space:nowrap;">Card Title One</h3> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Brief description</p> + </div> + </div> + <div style="width:192pt; flex-shrink:0; background:${background}; border-radius:10pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); overflow:hidden;"> + <img src="card2.jpg" style="width:192pt; height:120pt; object-fit:cover; display:block;" /> + <div style="padding:16pt;"> + <h3 style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3; white-space:nowrap;">Card Title Two</h3> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Brief description</p> + </div> + </div> + <!-- Card 3 same structure with card3.jpg --> + </div> + </div> +</body> +``` + +<a id="content-stat-overlay"></a> +### content-stat-overlay — Hero Stat on Photo Background + +Full-screen photo + dark mask + large centered statistic. **Requires downloaded image.** + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-image:url('stat-bg.jpg'); background-size:cover; background-position:center; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="position:absolute; top:0; left:0; width:720pt; height:405pt; + background-color:rgba(26,51,64,0.80);"></div> + <div style="position:relative; z-index:1; flex:1; + display:flex; flex-direction:column; justify-content:center; align-items:center;"> + <p style="font-size:13pt; color:${on-dark-secondary}; margin:0 0 8pt 0; line-height:1; text-transform:uppercase; letter-spacing:2pt;">KEY METRIC</p> + <h1 style="font-size:72pt; font-weight:bold; color:${on-dark}; margin:0 0 4pt 0; line-height:1; white-space:nowrap;">2,500+</h1> + <p style="font-size:22pt; font-weight:bold; color:${accent}; margin:0 0 24pt 0; line-height:1; white-space:nowrap;">Active Users</p> + <p style="font-size:15pt; color:${on-dark-secondary}; margin:0; line-height:1.5; text-align:center; max-width:480pt;">Brief description providing context for this statistic</p> + <div style="width:40pt; height:3pt; background:${accent}; margin:24pt 0 0 0;"></div> + </div> +</body> +``` +**Mask color rule**: Replace `rgba(26,51,64,0.80)` RGB with theme's primary-90 value. + +<a id="content-photo-overlay"></a> +### content-photo-overlay — Full Photo + Floating Text Card + +> `full-bleed | low | image | frosted` — Full-screen photo + semi-transparent floating card. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-image:url('content-bg.jpg'); background-size:cover; background-position:center; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="position:absolute; top:0; left:0; width:720pt; height:405pt; + background-color:rgba(0,0,0,0.3);"></div> + <div style="position:relative; z-index:1; flex:1; display:flex; align-items:center; justify-content:flex-end; padding:0 48pt;"> + <div style="width:320pt; background:rgba(255,255,255,0.85); border-radius:10pt; padding:32pt 24pt; border:1pt solid rgba(255,255,255,0.5);"> + <div style="width:32pt; height:3pt; background:${accent}; margin:0 0 16pt 0;"></div> + <h2 style="font-size:22pt; font-weight:bold; color:${primary-80}; margin:0 0 12pt 0; line-height:1.25;">Section Title</h2> + <p style="font-size:15pt; color:${primary-60}; margin:0 0 12pt 0; line-height:1.5;">Key insight that complements the background image.</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1.5;">Supporting detail or data point</p> + </div> + </div> +</body> +``` + +<a id="content-chart-focus"></a> +### content-chart-focus — Chart-Focused Page + +> `split | medium | light | none` — Chart takes 70%, narrow text sidebar for interpretation. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;">Data Analysis</h2> + </div> + <div style="flex:1; display:flex; padding:0 48pt; gap:24pt; align-items:center;"> + <div class="placeholder" id="chart-area" style="width:400pt; flex-shrink:0; background:${surface}; border-radius:10pt; display:flex; align-items:center; justify-content:center; min-height:260pt;"><p style="font-size:13pt; color:${primary-40}; margin:0;">Chart Area</p></div> + <div style="width:176pt; display:flex; flex-direction:column; justify-content:center;"> + <div style="width:32pt; height:3pt; background:${accent}; margin:0 0 12pt 0;"></div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0 0 8pt 0; line-height:1.3;">Key Insight</p> + <p style="font-size:13pt; color:${primary-60}; margin:0 0 16pt 0; line-height:1.5;">Chart interpretation</p> + <div style="padding:12pt; background:${primary-5}; border-radius:8pt;"> + <p style="font-size:22pt; font-weight:bold; color:${accent}; margin:0 0 4pt 0; line-height:1; white-space:nowrap;">+42%</p> + <p style="font-size:11pt; color:${primary-40}; margin:0; line-height:1.4;">Year-over-year growth</p> + </div> + </div> + </div> +</body> +``` + +<a id="content-chart-bar"></a> +### content-chart-bar — Bar Chart + Key Metric Cards + +> `split | medium | light | none` — Chart placeholder takes left ~67%, right sidebar shows 3 KPI cards. Column widths: chart 420pt + gap 20pt + sidebar 184pt = 624pt. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="width:720pt;height:56pt;background:${primary-90};display:flex;align-items:center;padding:0 48pt;"> + <h2 style="font-size:22pt;font-weight:bold;color:${on-dark};margin:0;line-height:1.25;">Revenue by Quarter</h2> + </div> + <div style="flex:1;display:flex;padding:16pt 48pt;gap:20pt;align-items:center;"> + <div class="placeholder" id="chart-area" style="width:420pt;flex-shrink:0;background:${surface};border-radius:10pt;align-self:stretch;display:flex;align-items:center;justify-content:center;"> + <p style="font-size:13pt;color:${primary-40};margin:0;">Bar Chart</p> + </div> + <div style="width:184pt;flex-shrink:0;display:flex;flex-direction:column;justify-content:center;gap:12pt;"> + <div style="padding:12pt;background:${surface};border-radius:8pt;border-left:3pt solid ${accent};"> + <p style="font-size:11pt;color:${primary-40};margin:0 0 4pt 0;line-height:1.3;">Q4(最高)</p> + <p style="font-size:20pt;font-weight:bold;color:${accent};margin:0;line-height:1;white-space:nowrap;">$2.1M</p> + </div> + <div style="padding:12pt;background:${surface};border-radius:8pt;border-left:3pt solid ${primary-40};"> + <p style="font-size:11pt;color:${primary-40};margin:0 0 4pt 0;line-height:1.3;">全年总计</p> + <p style="font-size:20pt;font-weight:bold;color:${primary-80};margin:0;line-height:1;white-space:nowrap;">$6.6M</p> + </div> + <div style="padding:12pt;background:${surface};border-radius:8pt;border-left:3pt solid ${primary-40};"> + <p style="font-size:11pt;color:${primary-40};margin:0 0 4pt 0;line-height:1.3;">同比增长</p> + <p style="font-size:20pt;font-weight:bold;color:${primary-80};margin:0;line-height:1;white-space:nowrap;">+32%</p> + </div> + </div> + </div> +</body> +``` + +<a id="content-chart-pie"></a> +### content-chart-pie — Pie Chart + Legend & Insight + +> `split | medium | light | none` — Square chart placeholder on left (260×260pt), legend + insight on right (332pt). Legend items use colored dot + label + percentage. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="width:720pt;height:56pt;background:${primary-90};display:flex;align-items:center;padding:0 48pt;"> + <h2 style="font-size:22pt;font-weight:bold;color:${on-dark};margin:0;line-height:1.25;">Market Share Distribution</h2> + </div> + <div style="flex:1;display:flex;padding:0 48pt;gap:32pt;align-items:center;"> + <div class="placeholder" id="chart-area" style="width:260pt;height:260pt;flex-shrink:0;background:${surface};border-radius:10pt;display:flex;align-items:center;justify-content:center;"> + <p style="font-size:13pt;color:${primary-40};margin:0;">Pie Chart</p> + </div> + <div style="width:332pt;flex-shrink:0;display:flex;flex-direction:column;justify-content:center;gap:12pt;"> + <div style="display:flex;align-items:center;gap:10pt;"> + <div style="width:11pt;height:11pt;border-radius:50%;background:${accent};flex-shrink:0;"></div> + <p style="font-size:13pt;color:${primary-80};margin:0;flex:1;line-height:1.3;">Category A</p> + <p style="font-size:15pt;font-weight:bold;color:${accent};margin:0;white-space:nowrap;">42%</p> + </div> + <div style="display:flex;align-items:center;gap:10pt;"> + <div style="width:11pt;height:11pt;border-radius:50%;background:${primary-60};flex-shrink:0;"></div> + <p style="font-size:13pt;color:${primary-80};margin:0;flex:1;line-height:1.3;">Category B</p> + <p style="font-size:15pt;font-weight:bold;color:${primary-60};margin:0;white-space:nowrap;">28%</p> + </div> + <div style="display:flex;align-items:center;gap:10pt;"> + <div style="width:11pt;height:11pt;border-radius:50%;background:${primary-40};flex-shrink:0;"></div> + <p style="font-size:13pt;color:${primary-80};margin:0;flex:1;line-height:1.3;">Category C</p> + <p style="font-size:15pt;font-weight:bold;color:${primary-40};margin:0;white-space:nowrap;">18%</p> + </div> + <div style="display:flex;align-items:center;gap:10pt;"> + <div style="width:11pt;height:11pt;border-radius:50%;background:${primary-20};flex-shrink:0;"></div> + <p style="font-size:13pt;color:${primary-80};margin:0;flex:1;line-height:1.3;">Others</p> + <p style="font-size:15pt;font-weight:bold;color:${primary-20};margin:0;white-space:nowrap;">12%</p> + </div> + <div style="height:1pt;background:${primary-10};"></div> + <p style="font-size:12pt;color:${primary-40};margin:0;line-height:1.5;">Key insight explaining the most important takeaway from the distribution.</p> + </div> + </div> +</body> +``` + +<a id="content-chart-line"></a> +### content-chart-line — Wide Line Chart + Trend Stats + +> `list | medium | light | none` — Accent stripe header (no dark bar). Wide chart placeholder stretches to fill vertical space, 4 stat cards pinned to the bottom. Chart area: 624pt wide × ~220pt tall. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="width:720pt;height:8pt;background:${accent};flex-shrink:0;"></div> + <div style="padding:16pt 48pt 8pt 48pt;display:flex;align-items:baseline;justify-content:space-between;flex-shrink:0;"> + <h2 style="font-size:24pt;font-weight:bold;color:${primary-80};margin:0;line-height:1.25;">Growth Trend</h2> + <p style="font-size:12pt;color:${primary-40};margin:0;white-space:nowrap;">2021 — 2025</p> + </div> + <div class="placeholder" id="chart-area" style="flex:1;margin:0 48pt 12pt 48pt;background:${surface};border-radius:10pt;display:flex;align-items:center;justify-content:center;"> + <p style="font-size:13pt;color:${primary-40};margin:0;">Line Chart</p> + </div> + <div style="display:flex;padding:0 48pt 16pt 48pt;gap:12pt;flex-shrink:0;"> + <div style="flex:1;padding:10pt 12pt;background:${surface};border-radius:8pt;"> + <p style="font-size:11pt;color:${primary-40};margin:0 0 3pt 0;white-space:nowrap;">起始值</p> + <p style="font-size:16pt;font-weight:bold;color:${primary-80};margin:0;line-height:1;white-space:nowrap;">$1.2M</p> + </div> + <div style="flex:1;padding:10pt 12pt;background:${accent};border-radius:8pt;"> + <p style="font-size:11pt;color:rgba(255,255,255,0.75);margin:0 0 3pt 0;white-space:nowrap;">当前值</p> + <p style="font-size:16pt;font-weight:bold;color:#FFFFFF;margin:0;line-height:1;white-space:nowrap;">$6.6M</p> + </div> + <div style="flex:1;padding:10pt 12pt;background:${surface};border-radius:8pt;"> + <p style="font-size:11pt;color:${primary-40};margin:0 0 3pt 0;white-space:nowrap;">CAGR</p> + <p style="font-size:16pt;font-weight:bold;color:${accent};margin:0;line-height:1;white-space:nowrap;">+41%</p> + </div> + <div style="flex:1;padding:10pt 12pt;background:${surface};border-radius:8pt;"> + <p style="font-size:11pt;color:${primary-40};margin:0 0 3pt 0;white-space:nowrap;">峰值时间</p> + <p style="font-size:16pt;font-weight:bold;color:${primary-80};margin:0;line-height:1;white-space:nowrap;">Q3 2025</p> + </div> + </div> +</body> +``` + +<a id="content-band-top"></a> +### content-band-top — Top Color Band + Content Below + +> `list | medium | light | none` + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="width:720pt;height:8pt;background:${accent};"></div> + <div style="padding:24pt 48pt 16pt 48pt;"> + <h2 style="font-size:28pt;font-weight:bold;color:${primary-80};margin:0 0 4pt 0;line-height:1.2;">Page Title</h2> + <p style="font-size:13pt;color:${primary-40};margin:0;line-height:1.5;">Subtitle</p> + </div> + <div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 48pt;gap:12pt;"> + <div style="display:flex;align-items:flex-start;gap:12pt;"> + <div style="width:3pt;min-height:36pt;background:${accent};border-radius:2pt;margin-top:2pt;"></div> + <div style="flex:1; min-width:0;"><p style="font-size:15pt;font-weight:bold;color:${primary-80};margin:0 0 4pt 0;line-height:1.5;">Point One</p><p style="font-size:15pt;color:${primary-60};margin:0;line-height:1.5;">Explanation</p></div> + </div> + <div style="display:flex;align-items:flex-start;gap:12pt;"> + <div style="width:3pt;min-height:36pt;background:${accent};border-radius:2pt;margin-top:2pt;"></div> + <div style="flex:1; min-width:0;"><p style="font-size:15pt;font-weight:bold;color:${primary-80};margin:0 0 4pt 0;line-height:1.5;">Point Two</p><p style="font-size:15pt;color:${primary-60};margin:0;line-height:1.5;">Explanation</p></div> + </div> + <!-- More points... --> + </div> +</body> +``` + +<a id="content-table"></a> +### content-table — Structured Data Table with Zebra Rows + +> `data | medium | light | none` — **No `<table>` tags — rows are flex divs.** Column widths sum to 624pt. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="height:56pt;background:${primary-90};display:flex;align-items:center;padding:0 48pt;"><h2 style="font-size:22pt;font-weight:bold;color:${on-dark};margin:0;line-height:1.25;">Table Title</h2></div> + <div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 48pt;"> + <div style="display:flex;background:${primary-80};border-radius:8pt 8pt 0 0;padding:10pt 16pt;"> + <p style="font-size:13pt;font-weight:bold;color:${on-dark};margin:0;width:180pt;">项目</p> + <p style="font-size:13pt;font-weight:bold;color:${on-dark};margin:0;width:111pt;text-align:right;">Q1</p> + <p style="font-size:13pt;font-weight:bold;color:${on-dark};margin:0;width:111pt;text-align:right;">Q2</p> + <p style="font-size:13pt;font-weight:bold;color:${on-dark};margin:0;width:111pt;text-align:right;">Q3</p> + <p style="font-size:13pt;font-weight:bold;color:${on-dark};margin:0;width:111pt;text-align:right;">Q4</p> + </div> + <div style="display:flex;background:${surface};padding:10pt 16pt;border-left:1pt solid ${primary-10};border-right:1pt solid ${primary-10};"> + <p style="font-size:13pt;color:${primary-80};margin:0;width:180pt;">Engineering</p> + <p style="font-size:13pt;color:${primary-60};margin:0;width:111pt;text-align:right;">$1.2M</p> + <p style="font-size:13pt;color:${primary-60};margin:0;width:111pt;text-align:right;">$1.5M</p> + <p style="font-size:13pt;color:${primary-60};margin:0;width:111pt;text-align:right;">$1.8M</p> + <p style="font-size:13pt;font-weight:bold;color:${accent};margin:0;width:111pt;text-align:right;">$2.1M</p> + </div> + <!-- More zebra rows alternating ${surface}/${background} --> + <div style="display:flex;background:${primary-10};padding:10pt 16pt;border-radius:0 0 8pt 8pt;"> + <p style="font-size:13pt;font-weight:bold;color:${primary-80};margin:0;width:180pt;">Total</p> + <p style="font-size:13pt;font-weight:bold;color:${primary-80};margin:0;width:111pt;text-align:right;">$4.0M</p> + <p style="font-size:13pt;font-weight:bold;color:${accent};margin:0;width:334pt;text-align:right;">$6.2M</p> + </div> + </div> +</body> +``` + +<a id="content-table-comparison"></a> +### content-table-comparison — Feature Comparison Matrix + +> `data | medium | light | tag` — Rows = options, columns = criteria, last column = tag. Tag colors: `${accent}` bg = recommended, `${primary-10}` bg = neutral/pass. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="height:56pt;background:${primary-90};display:flex;align-items:center;padding:0 48pt;"><h2 style="font-size:22pt;font-weight:bold;color:${on-dark};margin:0;line-height:1.25;">方案对比</h2></div> + <div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding:0 48pt;"> + <div style="display:flex;background:${primary-80};border-radius:8pt 8pt 0 0;padding:10pt 16pt;"> + <p style="font-size:12pt;font-weight:bold;color:${on-dark};margin:0;width:140pt;">方案</p> + <p style="font-size:12pt;font-weight:bold;color:${on-dark};margin:0;width:120pt;text-align:center;">成本</p> + <p style="font-size:12pt;font-weight:bold;color:${on-dark};margin:0;width:120pt;text-align:center;">实施周期</p> + <p style="font-size:12pt;font-weight:bold;color:${on-dark};margin:0;width:120pt;text-align:center;">可扩展性</p> + <p style="font-size:12pt;font-weight:bold;color:${on-dark};margin:0;width:124pt;text-align:center;">综合评级</p> + </div> + <div style="display:flex;align-items:center;background:${surface};padding:10pt 16pt;border-left:1pt solid ${primary-10};border-right:1pt solid ${primary-10};"> + <p style="font-size:13pt;font-weight:bold;color:${primary-80};margin:0;width:140pt;">方案 A</p> + <p style="font-size:13pt;color:${primary-60};margin:0;width:120pt;text-align:center;">低</p> + <p style="font-size:13pt;color:${primary-60};margin:0;width:120pt;text-align:center;">3 个月</p> + <p style="font-size:13pt;color:${primary-60};margin:0;width:120pt;text-align:center;">高</p> + <div style="width:124pt;display:flex;justify-content:center;"><div style="background:${accent};border-radius:4pt;padding:3pt 10pt;"><p style="font-size:12pt;font-weight:bold;color:#FFFFFF;margin:0;text-align:center;">推荐</p></div></div> + </div> + <!-- More rows with different tags --> + </div> +</body> +``` + +--- + +## High-Impact Accent Components + +Use these for visual punch: chapter openings, key metric reveals, strong statements, and rhythm breaks. + +<a id="chapter-divider-bold"></a> +### chapter-divider-bold — Accent Panel + Dark Chapter Divider + +> `split | low | dark | none` — Full-bleed dark slide with bold chapter number. Use at the opening of each major section. + +Left 40%: accent-color panel with oversized chapter number. Right 60%: dark background with chapter title and one-line overview. Produces strong structural signal between sections. + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-100};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:row;"> + <!-- Left: accent number panel --> + <div style="width:288pt;height:405pt;background:${accent};display:flex;flex-direction:column;justify-content:center;align-items:center;"> + <span style="font-size:96pt;font-weight:bold;color:rgba(255,255,255,0.90);line-height:1;white-space:nowrap;">02</span> + <span style="font-size:11pt;color:rgba(255,255,255,0.65);letter-spacing:3pt;margin-top:8pt;white-space:nowrap;">CHAPTER</span> + </div> + <!-- Right: chapter info --> + <div style="flex:1;height:405pt;display:flex;flex-direction:column;justify-content:center;padding:0 48pt;"> + <div style="width:36pt;height:3pt;background:${accent};margin:0 0 24pt 0;"></div> + <span style="font-size:30pt;font-weight:bold;color:#FFFFFF;line-height:1.25;">Chapter Title Here</span> + <span style="font-size:15pt;color:rgba(255,255,255,0.60);margin-top:16pt;line-height:1.6;">One-line description of this chapter's content</span> + </div> +</body> +``` + +<a id="content-hero-stat"></a> +### content-hero-stat — Single-Focus Large Metric Page + +> `centered | low | dark | none` — One dominant number, optionally with 2 supporting context metrics below. Use for single powerful KPI reveals, survey results, or market data. + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-90};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="height:4pt;background:${accent};"></div> + <div style="flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;padding:0 80pt;"> + <span style="font-size:13pt;color:${accent};letter-spacing:2pt;text-align:center;line-height:1;white-space:nowrap;">KEY METRIC</span> + <span style="font-size:88pt;font-weight:bold;color:#FFFFFF;line-height:1;text-align:center;margin-top:12pt;white-space:nowrap;">85%</span> + <span style="font-size:18pt;color:rgba(255,255,255,0.70);margin-top:16pt;text-align:center;line-height:1.5;max-width:460pt;">Description of what this metric means</span> + <!-- Optional: 2 supporting context metrics --> + <div style="display:flex;gap:32pt;margin-top:32pt;padding:14pt 28pt;background:rgba(255,255,255,0.06);border-radius:8pt;"> + <div style="text-align:center;"> + <span style="font-size:22pt;font-weight:bold;color:${accent};line-height:1;display:block;white-space:nowrap;">+12%</span> + <span style="font-size:11pt;color:rgba(255,255,255,0.50);margin-top:4pt;display:block;white-space:nowrap;">vs last year</span> + </div> + <div style="text-align:center;"> + <span style="font-size:22pt;font-weight:bold;color:${accent};line-height:1;display:block;white-space:nowrap;">#3</span> + <span style="font-size:11pt;color:rgba(255,255,255,0.50);margin-top:4pt;display:block;white-space:nowrap;">Industry rank</span> + </div> + </div> + </div> +</body> +``` + +<a id="content-asymmetric"></a> +### content-asymmetric — Asymmetric Dark Panel + Content + +> `split | medium | dark+light | none` — Left 38% dark panel with section label and framing tagline; right 62% light area with 3 numbered key points. Breaks the visual monotony of symmetric layouts. + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:row;"> + <!-- Left: dark framing panel --> + <div style="width:274pt;height:405pt;background:${primary-90};display:flex;flex-direction:column;justify-content:center;padding:0 32pt;"> + <span style="font-size:11pt;color:${accent};letter-spacing:2pt;line-height:1;margin:0 0 16pt 0;white-space:nowrap;">SECTION</span> + <span style="font-size:32pt;font-weight:bold;color:#FFFFFF;line-height:1.2;margin:0 0 20pt 0;">Core<br/>Concept</span> + <div style="width:32pt;height:3pt;background:${accent};margin:0 0 16pt 0;"></div> + <span style="font-size:13pt;color:rgba(255,255,255,0.60);line-height:1.6;">Brief framing of this content area</span> + </div> + <!-- Right: content with numbered points --> + <div style="flex:1;height:405pt;display:flex;flex-direction:column;justify-content:center;padding:0 40pt;gap:16pt;"> + <div style="display:flex;align-items:flex-start;gap:14pt;"> + <div style="width:28pt;height:28pt;background:${accent};border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;"> + <span style="font-size:13pt;font-weight:bold;color:#FFFFFF;line-height:1;">1</span> + </div> + <div style="flex:1;min-width:0;"> + <span style="font-size:16pt;font-weight:bold;color:${primary-80};line-height:1.3;display:block;">Point Title One</span> + <span style="font-size:13pt;color:${primary-60};line-height:1.5;display:block;margin-top:4pt;">Description text supporting this point</span> + </div> + </div> + <div style="display:flex;align-items:flex-start;gap:14pt;"> + <div style="width:28pt;height:28pt;background:${accent};border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;"> + <span style="font-size:13pt;font-weight:bold;color:#FFFFFF;line-height:1;">2</span> + </div> + <div style="flex:1;min-width:0;"> + <span style="font-size:16pt;font-weight:bold;color:${primary-80};line-height:1.3;display:block;">Point Title Two</span> + <span style="font-size:13pt;color:${primary-60};line-height:1.5;display:block;margin-top:4pt;">Description text supporting this point</span> + </div> + </div> + <div style="display:flex;align-items:flex-start;gap:14pt;"> + <div style="width:28pt;height:28pt;background:${accent};border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;"> + <span style="font-size:13pt;font-weight:bold;color:#FFFFFF;line-height:1;">3</span> + </div> + <div style="flex:1;min-width:0;"> + <span style="font-size:16pt;font-weight:bold;color:${primary-80};line-height:1.3;display:block;">Point Title Three</span> + <span style="font-size:13pt;color:${primary-60};line-height:1.5;display:block;margin-top:4pt;">Description text supporting this point</span> + </div> + </div> + </div> +</body> +``` + +<a id="quote-emphasis"></a> +### quote-emphasis — Full-Slide Pull Quote + +> `centered | low | dark | none` — Full dark slide with oversized opening quotation mark, large quote text, and attribution. Use for powerful statements, expert opinions, or memorable data points framed as quotes. **For a light-background variant**: replace `background-color:${primary-90}` with `${surface}`, `color:#FFFFFF` with `${primary-80}`, and `rgba(255,255,255,0.55)` with `${primary-40}`. + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-90};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;justify-content:center;"> + <div style="padding:0 80pt;"> + <span style="font-size:80pt;font-weight:bold;color:${accent};line-height:0.8;display:block;margin:0 0 4pt 0;">"</span> + <span style="font-size:24pt;color:#FFFFFF;line-height:1.55;display:block;margin:0 0 28pt 0;max-width:520pt;">Quote text goes here — one or two lines of impactful, memorable prose.</span> + <div style="width:48pt;height:3pt;background:${accent};margin:0 0 14pt 0;"></div> + <span style="font-size:13pt;color:rgba(255,255,255,0.55);line-height:1.5;">— Person Name, Title · Year</span> + </div> +</body> +``` + +<a id="content-dark-three-card"></a> +### content-dark-three-card — Dark Background Three-Card Rhythm Breaker + +> `grid | medium | dark | solid-dark` — Same role as a regular three-card content page but on a dark background. Use every 3–4 slides to break white-page monotony. Inline title (no title bar) creates visual distinction from light-background pages. + +```html +<body style="margin:0;padding:0;width:720pt;height:405pt;overflow:hidden;background-color:${primary-90};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <!-- Inline title with horizontal rule — no title bar --> + <div style="padding:28pt 48pt 20pt 48pt;display:flex;align-items:center;gap:16pt;"> + <span style="font-size:26pt;font-weight:bold;color:#FFFFFF;line-height:1.2;white-space:nowrap;">Page Title</span> + <div style="flex:1;height:1pt;background:rgba(255,255,255,0.12);"></div> + </div> + <!-- Three semi-transparent cards --> + <div style="flex:1;display:flex;gap:14pt;padding:0 48pt 32pt 48pt;align-items:stretch;"> + <div style="flex:1;background:rgba(255,255,255,0.06);border:1pt solid rgba(255,255,255,0.10);border-radius:10pt;padding:20pt 18pt;display:flex;flex-direction:column;"> + <div style="width:32pt;height:3pt;background:${accent};margin:0 0 14pt 0;"></div> + <span style="font-size:16pt;font-weight:bold;color:#FFFFFF;line-height:1.3;margin:0 0 8pt 0;display:block;">Card Title One</span> + <span style="font-size:13pt;color:rgba(255,255,255,0.60);line-height:1.6;display:block;">Card description text, one or two sentences.</span> + </div> + <div style="flex:1;background:rgba(255,255,255,0.06);border:1pt solid rgba(255,255,255,0.10);border-radius:10pt;padding:20pt 18pt;display:flex;flex-direction:column;"> + <div style="width:32pt;height:3pt;background:${accent};margin:0 0 14pt 0;"></div> + <span style="font-size:16pt;font-weight:bold;color:#FFFFFF;line-height:1.3;margin:0 0 8pt 0;display:block;">Card Title Two</span> + <span style="font-size:13pt;color:rgba(255,255,255,0.60);line-height:1.6;display:block;">Card description text, one or two sentences.</span> + </div> + <div style="flex:1;background:rgba(255,255,255,0.06);border:1pt solid rgba(255,255,255,0.10);border-radius:10pt;padding:20pt 18pt;display:flex;flex-direction:column;"> + <div style="width:32pt;height:3pt;background:${accent};margin:0 0 14pt 0;"></div> + <span style="font-size:16pt;font-weight:bold;color:#FFFFFF;line-height:1.3;margin:0 0 8pt 0;display:block;">Card Title Three</span> + <span style="font-size:13pt;color:rgba(255,255,255,0.60);line-height:1.6;display:block;">Card description text, one or two sentences.</span> + </div> + </div> +</body> +``` + +--- + +## Transition Page Components + +<a id="divider-bold-center"></a> +### divider-bold-center — Centered Bold Text Transition + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;justify-content:center;align-items:center;"> + <div style="width:40pt;height:3pt;background:${accent};margin:0 0 24pt 0;"></div> + <p style="font-size:44pt;font-weight:bold;color:${primary-20};margin:0 0 8pt 0;line-height:1;">02</p> + <h2 style="font-size:28pt;font-weight:bold;color:${primary-80};margin:0 0 12pt 0;line-height:1.2;text-align:center;">Chapter Title</h2> + <p style="font-size:15pt;color:${primary-40};margin:0;line-height:1.5;text-align:center;max-width:400pt;">One-line overview</p> +</body> +``` + +<a id="divider-split"></a> +### divider-split — Split Background Transition + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${background};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:row;"> + <div style="width:324pt;height:405pt;background:${primary-90};display:flex;flex-direction:column;justify-content:center;align-items:center;"><p style="font-size:80pt;font-weight:bold;color:rgba(255,255,255,0.08);margin:0;line-height:1;">02</p></div> + <div style="width:396pt;height:405pt;display:flex;flex-direction:column;justify-content:center;padding:0 48pt;"> + <div style="width:32pt;height:3pt;background:${accent};margin:0 0 16pt 0;"></div> + <h2 style="font-size:28pt;font-weight:bold;color:${primary-80};margin:0 0 12pt 0;line-height:1.2;">Chapter Title</h2> + <p style="font-size:15pt;color:${primary-40};margin:0;line-height:1.5;max-width:280pt;">One-line overview</p> + </div> +</body> +``` + +<a id="divider-photo-mask"></a> +### divider-photo-mask — Background Image + Mask Transition + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-image:url('section-bg.jpg');background-size:cover;background-position:center;font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="position:absolute;top:0;left:0;width:720pt;height:405pt;background-color:rgba(26,51,64,0.7);"></div> + <div style="position:relative;z-index:1;flex:1;display:flex;flex-direction:column;justify-content:center;align-items:center;"> + <div style="width:40pt;height:3pt;background:${accent};margin:0 0 24pt 0;"></div> + <p style="font-size:44pt;font-weight:bold;color:${accent};margin:0 0 8pt 0;line-height:1;">02</p> + <h2 style="font-size:28pt;font-weight:bold;color:${on-dark};margin:0 0 12pt 0;line-height:1.2;text-align:center;">Chapter Title</h2> + <p style="font-size:15pt;color:${on-dark-secondary};margin:0;line-height:1.5;text-align:center;max-width:400pt;">One-line overview</p> + </div> +</body> +``` + +<a id="divider-gradient"></a> +### divider-gradient — Gradient Background Transition + +> Requires pre-generating gradient PNG via sharp/SVG. + +```javascript +const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1920" height="1080"><defs><linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:${primary-100}"/><stop offset="100%" style="stop-color:${primary-80}"/></linearGradient></defs><rect width="100%" height="100%" fill="url(#g)"/></svg>`; +await sharp(Buffer.from(svg)).png().toFile('gradient-bg.png'); +``` + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-image:url('gradient-bg.png');background-size:cover;font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;justify-content:center;align-items:center;"> + <div style="width:40pt;height:3pt;background:${accent};margin:0 0 24pt 0;"></div> + <p style="font-size:44pt;font-weight:bold;color:${accent};margin:0 0 8pt 0;line-height:1;">03</p> + <h2 style="font-size:28pt;font-weight:bold;color:${on-dark};margin:0 0 12pt 0;line-height:1.2;text-align:center;">Chapter Title</h2> + <p style="font-size:15pt;color:${on-dark-secondary};margin:0;line-height:1.5;text-align:center;max-width:400pt;">One-line overview</p> +</body> +``` + +--- + +## Closing Page Components + +<a id="closing-takeaways"></a> +### closing-takeaways — Key Takeaways Summary + +Header bar + 3 key takeaway cards with top accent borders. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${surface};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;"> + <div style="width:720pt;height:56pt;background:${primary-90};display:flex;align-items:center;padding:0 48pt;"><h2 style="font-size:22pt;font-weight:bold;color:${on-dark};margin:0;line-height:1.25;">Key Conclusions</h2></div> + <div style="flex:1;display:flex;justify-content:center;align-items:center;padding:0 48pt;gap:16pt;"> + <div style="width:192pt;flex-shrink:0;background:${surface-card};border-radius:10pt;padding:24pt 16pt;box-shadow:0 3pt 10pt rgba(0,0,0,0.08);border-top:3pt solid ${accent};"> + <p style="font-size:22pt;font-weight:bold;color:${accent};margin:0 0 8pt 0;line-height:1;">01</p> + <p style="font-size:15pt;font-weight:bold;color:${primary-80};margin:0 0 8pt 0;line-height:1.5;">Takeaway Point One</p> + <p style="font-size:13pt;color:${primary-40};margin:0;line-height:1.5;">Brief note</p> + </div> + <div style="width:192pt;flex-shrink:0;background:${surface-card};border-radius:10pt;padding:24pt 16pt;box-shadow:0 3pt 10pt rgba(0,0,0,0.08);border-top:3pt solid ${accent};"> + <p style="font-size:22pt;font-weight:bold;color:${accent};margin:0 0 8pt 0;line-height:1;">02</p> + <p style="font-size:15pt;font-weight:bold;color:${primary-80};margin:0 0 8pt 0;line-height:1.5;">Takeaway Point Two</p> + <p style="font-size:13pt;color:${primary-40};margin:0;line-height:1.5;">Brief note</p> + </div> + <!-- Card 3 same structure --> + </div> +</body> +``` + +<a id="closing-thankyou"></a> +### closing-thankyou — Thank You Page + +Dark background + large thank-you + contact info. Echoes cover. + +```html +<body style="width:720pt;height:405pt;margin:0;padding:0;overflow:hidden;background-color:${primary-90};font-family:'Microsoft YaHei',sans-serif;display:flex;flex-direction:column;justify-content:center;align-items:center;"> + <h1 style="font-size:34pt;font-weight:bold;color:${on-dark};margin:0 0 8pt 0;line-height:1.15;text-align:center;">Thank You</h1> + <div style="width:40pt;height:3pt;background:${accent};margin:0 0 24pt 0;"></div> + <p style="font-size:15pt;color:${on-dark-secondary};margin:0 0 4pt 0;line-height:1.5;text-align:center;">John Smith | Product Department</p> + <p style="font-size:13pt;color:${on-dark-secondary};margin:0;line-height:1.5;text-align:center;">john.smith@company.com</p> +</body> +``` \ No newline at end of file diff --git a/skills/ppt/data-viz-components.md b/skills/ppt/data-viz-components.md new file mode 100755 index 0000000..86e184f --- /dev/null +++ b/skills/ppt/data-viz-components.md @@ -0,0 +1,529 @@ +# Data Visualization Components — Supplement to components.md + +These components use **pure HTML+CSS** to render charts and data visualizations without relying on PptxGenJS chart API. They convert directly through html2pptx into shapes. + +**When to use**: Prefer data visualization components whenever the content contains numbers, comparisons, percentages, or structured data. Every PPT with 6+ slides **MUST** include at least 1-2 data visualization pages. + +**Selection principle**: Concrete data → prioritize data viz components (bars/tables/funnels/donuts) over plain text lists. Alternate between text pages and data pages for visual rhythm. + +For PptxGenJS native charts (BAR/LINE/PIE/DOUGHNUT), use `content-chart-focus` with placeholder + `slide.addChart()` in compile.js. + +--- + +<a id="content-horizontal-bars"></a> +### content-horizontal-bars — Horizontal Bar Comparison + +`data | medium | light | none` + +Multi-item value comparison. Best for: market share, KPI benchmarks, quarterly comparisons. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Market Share Comparison + </h2> + </div> + + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt; gap:20pt;"> + + <!-- Bar item: repeat for each data point --> + <div style="display:flex; align-items:center; gap:12pt;"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:100pt; line-height:1.3; white-space:nowrap;">Product A</p> + <div style="flex:1; height:24pt; background:${primary-10}; border-radius:4pt;"> + <div style="height:24pt; width:70%; background:${primary-80}; border-radius:4pt;"></div> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0; width:48pt; text-align:right; line-height:1;">70%</p> + </div> + + <div style="display:flex; align-items:center; gap:12pt;"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:100pt; line-height:1.3; white-space:nowrap;">Product B</p> + <div style="flex:1; height:24pt; background:${primary-10}; border-radius:4pt;"> + <div style="height:24pt; width:52%; background:${primary-60}; border-radius:4pt;"></div> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-60}; margin:0; width:48pt; text-align:right; line-height:1;">52%</p> + </div> + + <div style="display:flex; align-items:center; gap:12pt;"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:100pt; line-height:1.3; white-space:nowrap;">Product C</p> + <div style="flex:1; height:24pt; background:${primary-10}; border-radius:4pt;"> + <div style="height:24pt; width:38%; background:${primary-40}; border-radius:4pt;"></div> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-40}; margin:0; width:48pt; text-align:right; line-height:1;">38%</p> + </div> + + <div style="display:flex; align-items:center; gap:12pt;"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:100pt; line-height:1.3; white-space:nowrap;">Product D</p> + <div style="flex:1; height:24pt; background:${primary-10}; border-radius:4pt;"> + <div style="height:24pt; width:22%; background:${primary-20}; border-radius:4pt;"></div> + </div> + <p style="font-size:15pt; font-weight:bold; color:${primary-20}; margin:0; width:48pt; text-align:right; line-height:1;">22%</p> + </div> + + </div> +</body> +``` + +**Variation**: Use `${accent}` for the highlighted/top bar to draw attention. + +--- + +<a id="content-stacked-bars"></a> +### content-stacked-bars — Stacked Progress Bars + +`data | medium | light | shadow` + +Multi-dimension proportional data. Best for: budget allocation, resource distribution, composition analysis. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Budget Allocation by Department + </h2> + </div> + + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt; gap:24pt;"> + + <div> + <div style="display:flex; justify-content:space-between; margin-bottom:8pt;"> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.3;">Q1 2024</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1;">$2.4M Total</p> + </div> + <div style="display:flex; height:32pt; border-radius:6pt; overflow:hidden;"> + <div style="width:35%; background:${primary-80};"></div> + <div style="width:25%; background:${primary-60};"></div> + <div style="width:20%; background:${primary-40};"></div> + <div style="width:12%; background:${accent};"></div> + <div style="width:8%; background:${primary-20};"></div> + </div> + <div style="display:flex; gap:16pt; margin-top:8pt;"> + <div style="display:flex; align-items:center; gap:4pt;"> + <div style="width:8pt; height:8pt; background:${primary-80}; border-radius:2pt;"></div> + <p style="font-size:11pt; color:${primary-60}; margin:0; line-height:1;">R&D 35%</p> + </div> + <div style="display:flex; align-items:center; gap:4pt;"> + <div style="width:8pt; height:8pt; background:${primary-60}; border-radius:2pt;"></div> + <p style="font-size:11pt; color:${primary-60}; margin:0; line-height:1;">Marketing 25%</p> + </div> + <div style="display:flex; align-items:center; gap:4pt;"> + <div style="width:8pt; height:8pt; background:${primary-40}; border-radius:2pt;"></div> + <p style="font-size:11pt; color:${primary-60}; margin:0; line-height:1;">Sales 20%</p> + </div> + <div style="display:flex; align-items:center; gap:4pt;"> + <div style="width:8pt; height:8pt; background:${accent}; border-radius:2pt;"></div> + <p style="font-size:11pt; color:${primary-60}; margin:0; line-height:1;">Ops 12%</p> + </div> + </div> + </div> + + <div> + <div style="display:flex; justify-content:space-between; margin-bottom:8pt;"> + <p style="font-size:15pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.3;">Q2 2024</p> + <p style="font-size:13pt; color:${primary-40}; margin:0; line-height:1;">$2.8M Total</p> + </div> + <div style="display:flex; height:32pt; border-radius:6pt; overflow:hidden;"> + <div style="width:40%; background:${primary-80};"></div> + <div style="width:22%; background:${primary-60};"></div> + <div style="width:18%; background:${primary-40};"></div> + <div style="width:10%; background:${accent};"></div> + <div style="width:10%; background:${primary-20};"></div> + </div> + </div> + + </div> +</body> +``` + +--- + +<a id="content-data-table"></a> +### content-data-table — Structured Data Table + +`data | medium | light | none` + +Multi-row multi-column structured data. Best for: quarterly reports, feature comparisons, competitive analysis. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Quarterly Performance Summary + </h2> + </div> + + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; padding:0 48pt;"> + + <div style="display:flex; background:${primary-80}; border-radius:8pt 8pt 0 0; padding:12pt 16pt;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; width:120pt; line-height:1.3;">Department</p> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; width:120pt; line-height:1.3; text-align:right;">Q1</p> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; width:120pt; line-height:1.3; text-align:right;">Q2</p> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; width:120pt; line-height:1.3; text-align:right;">Q3</p> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; width:112pt; line-height:1.3; text-align:right;">Q4</p> + </div> + + <div style="display:flex; background:${surface}; padding:10pt 16pt; border-left:1pt solid ${primary-10}; border-right:1pt solid ${primary-10};"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:120pt; line-height:1.3;">Engineering</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$1.2M</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$1.5M</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$1.8M</p> + <p style="font-size:13pt; color:${accent}; margin:0; width:112pt; line-height:1.3; text-align:right; font-weight:bold;">$2.1M</p> + </div> + + <div style="display:flex; background:${background}; padding:10pt 16pt; border-left:1pt solid ${primary-10}; border-right:1pt solid ${primary-10};"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:120pt; line-height:1.3;">Marketing</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$800K</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$920K</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$1.1M</p> + <p style="font-size:13pt; color:${accent}; margin:0; width:112pt; line-height:1.3; text-align:right; font-weight:bold;">$1.3M</p> + </div> + + <div style="display:flex; background:${surface}; padding:10pt 16pt; border-left:1pt solid ${primary-10}; border-right:1pt solid ${primary-10};"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:120pt; line-height:1.3;">Sales</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$2.0M</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$2.3M</p> + <p style="font-size:13pt; color:${primary-60}; margin:0; width:120pt; line-height:1.3; text-align:right;">$2.5M</p> + <p style="font-size:13pt; color:${accent}; margin:0; width:112pt; line-height:1.3; text-align:right; font-weight:bold;">$2.8M</p> + </div> + + <div style="display:flex; background:${primary-10}; padding:10pt 16pt; border-radius:0 0 8pt 8pt;"> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:120pt; line-height:1.3; font-weight:bold;">Total</p> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:120pt; line-height:1.3; text-align:right; font-weight:bold;">$4.0M</p> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:120pt; line-height:1.3; text-align:right; font-weight:bold;">$4.7M</p> + <p style="font-size:13pt; color:${primary-80}; margin:0; width:120pt; line-height:1.3; text-align:right; font-weight:bold;">$5.4M</p> + <p style="font-size:13pt; color:${accent}; margin:0; width:112pt; line-height:1.3; text-align:right; font-weight:bold;">$6.2M</p> + </div> + + </div> +</body> +``` + +--- + +<a id="content-quadrant-matrix"></a> +### content-quadrant-matrix — 2x2 Quadrant Matrix + +`data | medium | light | none` + +Two-axis classification framework. Best for: BCG matrix, priority matrix, SWOT, evaluation frameworks. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Priority Matrix + </h2> + </div> + + <div style="flex:1; display:flex; padding:8pt 48pt 0 48pt;"> + + <div style="width:24pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:9pt; color:${primary-40}; margin:0; writing-mode:vertical-rl; transform:rotate(180deg); text-align:center; line-height:1.2;">Impact</p> + </div> + + <div style="flex:1; display:flex; flex-direction:column;"> + + <div style="display:flex; gap:16pt; margin-bottom:16pt;"> + <div style="width:296pt; flex-shrink:0; border-radius:10pt; padding:16pt 20pt; background:${primary-80};"> + <p style="font-size:18pt; font-weight:bold; color:${on-dark}; margin:0 0 6pt 0; line-height:1.25;">Do First</p> + <p style="font-size:12pt; color:${on-dark-secondary}; margin:0; line-height:1.5;">High Impact + High Effort. Critical items requiring attention.</p> + </div> + <div style="width:296pt; flex-shrink:0; border-radius:10pt; padding:16pt 20pt; background:${surface}; border:1.5pt solid ${primary-20};"> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 6pt 0; line-height:1.25;">Quick Wins</p> + <p style="font-size:12pt; color:${primary-60}; margin:0; line-height:1.5;">High Impact + Low Effort. Prioritize for maximum ROI.</p> + </div> + </div> + + <div style="display:flex; gap:16pt;"> + <div style="width:296pt; flex-shrink:0; border-radius:10pt; padding:16pt 20pt; border:1.5pt solid ${primary-20};"> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0 0 6pt 0; line-height:1.25;">Schedule</p> + <p style="font-size:12pt; color:${primary-60}; margin:0; line-height:1.5;">Low Impact + High Effort. Plan for later.</p> + </div> + <div style="width:296pt; flex-shrink:0; border-radius:10pt; padding:16pt 20pt; background:${primary-90};"> + <p style="font-size:18pt; font-weight:bold; color:${on-dark}; margin:0 0 6pt 0; line-height:1.25;">Eliminate</p> + <p style="font-size:12pt; color:${on-dark-secondary}; margin:0; line-height:1.5;">Low Impact + Low Effort. Drop these tasks.</p> + </div> + </div> + + <div style="margin-top:6pt;"> + <p style="font-size:9pt; font-weight:bold; color:${primary-40}; margin:0; text-align:center;">LOW Effort → HIGH</p> + </div> + + </div> + </div> +</body> +``` + +--- + +<a id="content-funnel"></a> +### content-funnel — Sales/Conversion Funnel + +`data | medium | light | none` + +Multi-stage progressive filtering. Best for: sales pipeline, conversion funnel, qualification process. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Sales Conversion Funnel + </h2> + </div> + + <div style="flex:1; display:flex; align-items:center; padding:0 48pt; gap:40pt;"> + + <div style="width:320pt; display:flex; flex-direction:column; align-items:center; gap:4pt;"> + <div style="width:280pt; height:36pt; background:${primary-80}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Leads: 10,000</p> + </div> + <div style="width:230pt; height:36pt; background:${primary-60}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Qualified: 6,500</p> + </div> + <div style="width:180pt; height:36pt; background:${primary-40}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Proposals: 3,200</p> + </div> + <div style="width:130pt; height:36pt; background:${accent}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Negotiation: 1,800</p> + </div> + <div style="width:80pt; height:36pt; background:${primary-80}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Closed: 850</p> + </div> + </div> + + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; gap:16pt;"> + <div style="background:${surface}; border-radius:8pt; padding:16pt; border-left:3pt solid ${accent};"> + <p style="font-size:32pt; font-weight:bold; color:${accent}; margin:0; line-height:1; white-space:nowrap;">8.5%</p> + <p style="font-size:13pt; color:${primary-60}; margin:4pt 0 0 0; line-height:1.4;">Overall Conversion Rate</p> + </div> + <div style="background:${surface}; border-radius:8pt; padding:16pt;"> + <p style="font-size:32pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1; white-space:nowrap;">46%</p> + <p style="font-size:13pt; color:${primary-60}; margin:4pt 0 0 0; line-height:1.4;">Qualified to Proposal Rate</p> + </div> + </div> + + </div> +</body> +``` + +--- + +<a id="content-before-after"></a> +### content-before-after — Before vs After Comparison + +`data | medium | light | none` + +Side-by-side comparison panels. Best for: optimization results, before/after, pros/cons. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Before vs After Optimization + </h2> + </div> + + <div style="flex:1; display:flex; padding:16pt 48pt 0 48pt; gap:16pt;"> + + <!-- Before --> + <div style="width:296pt; flex-shrink:0; background:${surface}; border-radius:10pt; padding:20pt 24pt;"> + <div style="display:flex; align-items:center; gap:8pt; margin-bottom:16pt;"> + <div style="width:28pt; height:28pt; border-radius:50%; background:${primary-40}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; color:${on-dark}; margin:0; line-height:1; font-weight:bold;">B</p> + </div> + <p style="font-size:18pt; font-weight:bold; color:${primary-40}; margin:0; line-height:1.25;">Before</p> + </div> + <div style="display:flex; justify-content:space-between; padding:8pt 0; border-bottom:1pt solid ${primary-10};"> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Load Time</p> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.5;">4.2s</p> + </div> + <div style="display:flex; justify-content:space-between; padding:8pt 0; border-bottom:1pt solid ${primary-10};"> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Bounce Rate</p> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.5;">38%</p> + </div> + <div style="display:flex; justify-content:space-between; padding:8pt 0;"> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Conversion</p> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.5;">2.1%</p> + </div> + </div> + + <!-- After (accent border to highlight) --> + <div style="width:296pt; flex-shrink:0; background:${surface}; border-radius:10pt; padding:20pt 24pt; border:2pt solid ${accent};"> + <div style="display:flex; align-items:center; gap:8pt; margin-bottom:16pt;"> + <div style="width:28pt; height:28pt; border-radius:50%; background:${accent}; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; color:${on-dark}; margin:0; line-height:1; font-weight:bold;">A</p> + </div> + <p style="font-size:18pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1.25;">After</p> + </div> + <div style="display:flex; justify-content:space-between; padding:8pt 0; border-bottom:1pt solid ${primary-10};"> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Load Time</p> + <p style="font-size:13pt; font-weight:bold; color:${accent}; margin:0; line-height:1.5;">1.8s</p> + </div> + <div style="display:flex; justify-content:space-between; padding:8pt 0; border-bottom:1pt solid ${primary-10};"> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Bounce Rate</p> + <p style="font-size:13pt; font-weight:bold; color:${accent}; margin:0; line-height:1.5;">22%</p> + </div> + <div style="display:flex; justify-content:space-between; padding:8pt 0;"> + <p style="font-size:13pt; color:${primary-60}; margin:0; line-height:1.5;">Conversion</p> + <p style="font-size:13pt; font-weight:bold; color:${accent}; margin:0; line-height:1.5;">4.7%</p> + </div> + </div> + + </div> +</body> +``` + +--- + +<a id="content-dashboard"></a> +### content-dashboard — Data Dashboard / KPI Grid + +`data | medium | light | shadow` + +Multi-metric overview. Best for: dashboards, weekly/monthly reports, KPI summaries. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${surface}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Monthly Performance Dashboard + </h2> + </div> + + <!-- Row 1: 3 large stats --> + <div style="display:flex; gap:16pt; padding:16pt 48pt 0 48pt;"> + <div style="width:192pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; padding:20pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); text-align:center;"> + <p style="font-size:40pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1; white-space:nowrap;">$4.2M</p> + <p style="font-size:13pt; color:${primary-40}; margin:8pt 0 0 0; line-height:1.4;">Revenue</p> + <p style="font-size:11pt; color:${accent}; margin:8pt 0 0 0; line-height:1; font-weight:bold;">+12.3% vs last month</p> + </div> + <div style="width:192pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; padding:20pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); text-align:center;"> + <p style="font-size:40pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1; white-space:nowrap;">2,847</p> + <p style="font-size:13pt; color:${primary-40}; margin:8pt 0 0 0; line-height:1.4;">Active Users</p> + <p style="font-size:11pt; color:${accent}; margin:8pt 0 0 0; line-height:1; font-weight:bold;">+8.7% vs last month</p> + </div> + <div style="width:192pt; flex-shrink:0; background:${surface-card}; border-radius:10pt; padding:20pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); text-align:center;"> + <p style="font-size:40pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1; white-space:nowrap;">94.2%</p> + <p style="font-size:13pt; color:${primary-40}; margin:8pt 0 0 0; line-height:1.4;">Uptime</p> + <p style="font-size:11pt; color:${accent}; margin:8pt 0 0 0; line-height:1; font-weight:bold;">+0.3% vs last month</p> + </div> + </div> + + <!-- Row 2: 4 smaller stats --> + <div style="display:flex; gap:16pt; padding:16pt 48pt;"> + <div style="width:140pt; flex-shrink:0; background:${surface-card}; border-radius:8pt; padding:14pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); text-align:center;"> + <p style="font-size:32pt; font-weight:bold; color:${primary-60}; margin:0; line-height:1; white-space:nowrap;">156</p> + <p style="font-size:11pt; color:${primary-40}; margin:6pt 0 0 0; line-height:1.4;">New Signups</p> + </div> + <div style="width:140pt; flex-shrink:0; background:${surface-card}; border-radius:8pt; padding:14pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); text-align:center;"> + <p style="font-size:32pt; font-weight:bold; color:${primary-60}; margin:0; line-height:1; white-space:nowrap;">$148</p> + <p style="font-size:11pt; color:${primary-40}; margin:6pt 0 0 0; line-height:1.4;">Avg Revenue/User</p> + </div> + <div style="width:140pt; flex-shrink:0; background:${surface-card}; border-radius:8pt; padding:14pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); text-align:center;"> + <p style="font-size:32pt; font-weight:bold; color:${primary-60}; margin:0; line-height:1; white-space:nowrap;">4.6</p> + <p style="font-size:11pt; color:${primary-40}; margin:6pt 0 0 0; line-height:1.4;">NPS Score</p> + </div> + <div style="width:140pt; flex-shrink:0; background:${surface-card}; border-radius:8pt; padding:14pt; box-shadow:0 3pt 10pt rgba(0,0,0,0.08); text-align:center;"> + <p style="font-size:32pt; font-weight:bold; color:${primary-60}; margin:0; line-height:1; white-space:nowrap;">23ms</p> + <p style="font-size:11pt; color:${primary-40}; margin:6pt 0 0 0; line-height:1.4;">Avg Response</p> + </div> + </div> +</body> +``` + +--- + +<a id="content-pyramid"></a> +### content-pyramid — Pyramid / Layered Hierarchy + +`data | medium | light | none` + +Layered hierarchy. Best for: Maslow pyramid, org levels, tech stack layers, priority hierarchy. + +```html +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-color:${background}; + font-family:'Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + + <div style="width:720pt; height:56pt; background:${primary-90}; + display:flex; align-items:center; padding:0 48pt;"> + <h2 style="font-size:22pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1.25;"> + Strategy Implementation Pyramid + </h2> + </div> + + <div style="flex:1; display:flex; align-items:center; padding:0 48pt; gap:32pt;"> + + <div style="width:360pt; display:flex; flex-direction:column; align-items:center; gap:6pt;"> + <div style="width:140pt; height:40pt; background:${primary-80}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Vision & Mission</p> + </div> + <div style="width:200pt; height:40pt; background:${primary-60}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Strategic Objectives</p> + </div> + <div style="width:260pt; height:40pt; background:${primary-40}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${on-dark}; margin:0; line-height:1;">Tactical Plans & Programs</p> + </div> + <div style="width:320pt; height:40pt; background:${primary-20}; border-radius:6pt; display:flex; align-items:center; justify-content:center;"> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0; line-height:1;">Operations & Daily Execution</p> + </div> + </div> + + <div style="flex:1; display:flex; flex-direction:column; justify-content:center; gap:20pt;"> + <div style="border-left:2pt solid ${primary-80}; padding-left:12pt;"> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Top Level</p> + <p style="font-size:12pt; color:${primary-60}; margin:0; line-height:1.4;">Long-term direction, 5-10 year horizon</p> + </div> + <div style="border-left:2pt solid ${primary-60}; padding[left:12pt;"> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Strategy</p> + <p style="font-size:12pt; color:${primary-60}; margin:0; line-height:1.4;">3-5 year measurable targets</p> + </div> + <div style="border-left:2pt solid ${primary-40}; padding-left:12pt;"> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Tactics</p> + <p style="font-size:12pt; color:${primary-60}; margin:0; line-height:1.4;">1-2 year initiatives and projects</p> + </div> + <div style="border-left:2pt solid ${primary-20}; padding-left:12pt;"> + <p style="font-size:13pt; font-weight:bold; color:${primary-80}; margin:0 0 4pt 0; line-height:1.3;">Operations</p> + <p style="font-size:12pt; color:${primary-60}; margin:0; line-height:1.4;">Daily tasks and short-term goals</p> + </div> + </div> + + </div> +</body> +``` diff --git a/skills/ppt/design-system.md b/skills/ppt/design-system.md new file mode 100755 index 0000000..8187ea0 --- /dev/null +++ b/skills/ppt/design-system.md @@ -0,0 +1,211 @@ +# Design System — PPT Creative Guidelines + +This file provides **creative principles and a color system** for PPT design. Components and layouts should be inventive and diverse — these guidelines exist to ensure visual coherence, not to constrain creativity. + +**Philosophy: Every slide is a design opportunity.** No two slides should look the same. Vary layouts, card treatments, backgrounds, typography weight, and whitespace aggressively. The audience should feel visual momentum, not repetition. + +--- + +## 1. Color Scale System (Mandatory) + +The color system is the **only hard constraint**. All colors used in the deck must derive from the chosen theme. + +### 1.1 Color Scale Generation + +Eight levels are derived from a single Primary color: + +| Level | Lightness | Usage Examples | +|-------|-----------|---------------| +| `primary-100` | Darkest (L≈8%) | Dark backgrounds, dramatic pages | +| `primary-90` | Dark (L≈15%) | Title bars, dark blocks, overlays | +| `primary-80` | **Main color** | Headings, primary UI elements | +| `primary-60` | Medium dark (L≈45%) | Subheadings, icons, secondary elements | +| `primary-40` | Medium light (L≈60%) | Muted text, dividers, subtle accents | +| `primary-20` | Light (L≈80%) | Borders, light decorations, tints | +| `primary-10` | Very light (L≈90%) | Card fills, surface backgrounds | +| `primary-5` | Near white (L≈96%) | Page tints, hover states, subtle surfaces | + +**Generation method**: From the Primary HSL (H, S, L) — keep H constant; dark levels S×1.1, light levels S×0.3; assign L per the table. + +### 1.2 Semantic Colors + +| Token | Value | Purpose | +|-------|-------|---------| +| `background` | `#FFFFFF` | Default page base | +| `surface` | `primary-5` | Tinted content area base | +| `surface-card` | `#FFFFFF` | Card base (floats above surface) | +| `text-primary` | `primary-80` | Primary text | +| `text-secondary` | `primary-60` | Secondary text | +| `text-muted` | `primary-40` | Annotations, footnotes | +| `on-dark` | `#FFFFFF` | Text on dark backgrounds | +| `on-dark-secondary` | `rgba(255,255,255,0.7)` | Secondary text on dark backgrounds | +| `border` | `primary-10` | Dividers, borders | +| `accent` | Defined by theme | Highlight color | +| `on-accent` | `#FFFFFF` | Text on accent color | + +### 1.3 Color Rules + +- **All grays must derive from the primary color scale** — no arbitrary `#666`, `#999`, `#DDD` +- **Text on dark areas must use `on-dark`** (#FFFFFF) — never use light primary tints on dark backgrounds +- **Accent color area**: ≤15% on standard pages; up to 40% on emphasis/transition pages +- **Saturation limit**: Accent colors should have HSL saturation ≤ 65% for most themes. High-energy themes (Deep Mineral, Ember, Mono) may use S ≤ 75% to preserve their intended vibrancy. Colors above S=80% look garish on projected slides. The theme presets already comply; if creating custom colors, check saturation. +- **Large color blocks** (>15% of page area): reduce saturation further (S ≤ 50%) to avoid visual fatigue. Use higher saturation only for small accents (bars, icons, small badges). + +--- + +## 2. Creative Principles (Guidelines, Not Rules) + +These are high-level design intentions. Interpret them freely. + +### 2.1 Visual Diversity + +- **No two adjacent slides should share the same layout, background treatment, or card style.** This is the single most important principle. +- Aim for **5+ distinct layout structures** across a deck (split columns, grids, single-focus, full-bleed, asymmetric, timeline, staggered, etc.) +- Aim for **3+ card styles** (shadow float, outline, solid fill, gradient, frosted glass, tag-style, image-backed, borderless, etc.) +- Aim for **3+ background treatments** (pure white, tinted surface, dark full-bleed, photo + mask, gradient, diagonal split, corner decoration, color band, etc.) + +### 2.1.1 Deck-Wide Consistency Rules + +**What MUST stay uniform across the entire deck** (changing these = unprofessional): +- Body text font and font size (e.g., always 15pt Microsoft YaHei) +- Page left/right margins (e.g., always 48pt) +- Primary color palette (same theme throughout) +- Accent color (same accent variant throughout, or intentional A/B mixing as documented) + +**What SHOULD vary across slides** (sameness = monotony): +- Layout structure (grid, split, list, focus, timeline, etc.) +- Background treatment (white, surface, dark, photo, gradient) +- Card style (shadow, outline, solid, accent-bar, dark card) +- Title bar style (dark bar, accent bar, underline, inline title, vertical band) +- Decorative elements (accent bars, shapes, circles, dot patterns) + +**The boundary is clear: base typography and spacing = consistent; visual composition = diverse.** + +### 2.2 Intentional vs Accidental Whitespace + +Not all whitespace is bad. The key is **intent**. + +**Intentional whitespace (good — preserve it):** +- KPI / big-number pages: Large number centered with breathing room → dramatic focus +- Quote / pullout pages: Single sentence with generous margins → elegant emphasis +- Photo + caption pages: Image dominates, text is minimal → visual storytelling +- Title / divider pages: Bold text with open space → transition signal + +**Accidental whitespace (bad — fix it):** +- Bullet list with 3 items occupying only 1/3 of the slide → expand or add visuals +- Card grid with small cards clustered at the top, bottom 60% empty → enlarge cards, increase padding +- Text block in the upper-left corner with nothing else on the page → redistribute content + +**Rule of thumb**: If the whitespace makes the slide feel "unfinished" or "forgot to add content," it's accidental. If it makes the slide feel "clean" or "focused," it's intentional. + +### 2.3 Visual Rhythm + +- Alternate between high-density and low-density pages +- Insert a "rhythm breaker" every 3-4 slides — a page that looks dramatically different (full-bleed dark, photo overlay, large single quote, bold color block) +- Cover and closing page should echo each other in color or mood +- **Vary the page header treatment** — don't use the same dark title bar on every slide. Alternatives: + - Accent-colored bar (e.g., `background:${accent}`) + - Transparent header with large colored text and a bottom divider line + - No header bar at all — use oversized inline heading text + - Left-side vertical color band instead of top horizontal bar + - Gradient header (dark-to-transparent from top) + +### 2.4 Typography + +- **Hierarchy is king**: Headings should be visually distinct from body text through size, weight, and/or color +- Body text should be readable at projection distance — don't go below ~13pt +- Let type breathe — generous line-height makes everything feel more designed +- KPI / hero numbers should be oversized and bold to create a focal point +- Prefer cutting content over shrinking fonts — only when a slide is already well-filled and overflow is imminent; never cut content preemptively + +### 2.5 Spacing & Layout + +- Use consistent margins across the deck (recommended: 48pt left/right, 40pt top, ≥36pt bottom safe zone) +- Content should be **vertically balanced** — not crammed at the top with empty space below +- Give elements room — tight packing reads as "cheap" +- Fixed widths for multi-column cards prevent uneven stretching + +### 2.6 Decoration & Visual Interest + +- Every content slide should have **at least one non-text visual element** — accent bar, color block, photo, shape, large number, chart +- Decorative elements (circles, bars, geometric shapes, dot patterns) add polish and visual rhythm +- Decoration should support the content, never compete with it + +### 2.7 Imagery + +- Real photographs dramatically elevate quality — use them on covers, dividers, and visual pages +- Background images need a semi-transparent mask overlay for text readability +- Image tone (warm/cool) should match the theme atmosphere + +--- + +## 3. Technical Constraints (html2pptx Engine) + +These are **rendering engine limitations**, not design choices. Violating them causes broken output. + +| Constraint | Reason | +|-----------|--------| +| Slide canvas: 720×405pt (16:9) | Fixed PPTX dimensions | +| `background-image` only works on `<body>`, not on `<div>` | html2pptx engine limitation | +| Don't use `flex-wrap` for multi-row grids — use separate flex containers per row | html2pptx drops wrapped rows | +| Don't use negative margins | Causes text stacking in PPT (elements become independent text boxes) | +| `font-family` must include a CJK font name | Required for correct font mapping | +| Images must be local file paths, not URLs | html2pptx reads from filesystem | +| Content that exceeds 405pt height will overflow — split into multiple slides instead | No scroll in PPT | +| Multi-column equal-width cards: use explicit `width` + `flex-shrink:0`, not `flex:1` | Prevents uneven stretching | +| Titles and short labels: use `white-space:nowrap` | Prevents unexpected line breaks in conversion | + +--- + +## 4. Quick Reference + +### Recommended Font Size Range + +| Role | Suggested Size | Weight | +|------|---------------|--------| +| Display / decorative number (ghost) | 80–100pt | Bold | +| Hero focal metric (single-stat page) | 56–88pt | Bold | +| Hero / cover title | 40–48pt | Bold | +| KPI dashboard numbers | 36–48pt | Bold | +| Section title | 28–34pt | Bold | +| Page title (in title bar) | 26–30pt | Bold | +| Card / sub heading | 20–24pt | Bold | +| Body text | 14–16pt | Normal | +| Annotations / footnotes | 12–13pt | Normal | +| Captions / tiny labels | 10–12pt | Normal | +| Tag / chip label | 10–11pt | Normal | + +**Ghost number technique**: Large decorative numbers (80–100pt) at very low opacity (`color:rgba(255,255,255,0.05)` on dark backgrounds, or `color:rgba(0,0,0,0.04)` on light) add visual depth without competing with readable content. Use as background layer behind chapter numbers, inside TOC cards, or behind section titles. + +### Common Spacing Values + +| Usage | Suggested | +|-------|-----------| +| Page left/right margins | 48pt | +| Page top margin | 40pt | +| Bottom safe zone | ≥36pt | +| Between heading and body | 8-16pt | +| Between cards | 12-20pt | +| Card internal padding | 16-24pt | + +### Width Reference (720pt total) + +| Layout | Approximate Column Widths | +|--------|--------------------------| +| Full width (with margins) | ~624pt | +| Two columns | ~296pt each (16pt gap) | +| Three columns | ~192pt each (16pt×2 gaps) | +| Four columns | ~140pt each (16pt×3 gaps) | + +### Height Reference (405pt total) + +| Component | Approximate Height | +|-----------|--------------------| +| Title bar | ~56pt | +| Single text line (15pt body) | ~23pt | +| KPI number line (40pt) | ~40pt | +| Card padding (top+bottom) | ~32-48pt | + +--- + +**Remember: This system gives you a coherent color palette and a few engine-level constraints. Everything else — layout invention, typography expression, decorative flair, card styling, background artistry — is yours to create freely. Make every slide count.** diff --git a/skills/ppt/html2pptx.md b/skills/ppt/html2pptx.md new file mode 100755 index 0000000..5ff5220 --- /dev/null +++ b/skills/ppt/html2pptx.md @@ -0,0 +1,394 @@ +# HTML to PowerPoint Guide + +Convert HTML slides into PowerPoint presentations using the `html2pptx.js` library with accurate positioning. + +--- + +## Creating HTML Slides + +Each HTML slide must include the correct body dimensions: + +### Layout Dimensions + +- **16:9** (default): `width: 720pt; height: 405pt` +- **4:3**: `width: 720pt; height: 540pt` +- **16:10**: `width: 720pt; height: 450pt` + +**CRITICAL — Prevent Overflow**: +- `<body>` must set exact dimensions via inline style: `width: 720pt; height: 405pt; margin: 0; padding: 0; overflow: hidden;` +- All content must fit within these boundaries. If content overflows, split into multiple slides +- Maintain at least **36pt bottom margin** inside the body for safe spacing + +**Fill the slide**: Content should occupy the available space well. Avoid designs where text clusters in the top third with the bottom two-thirds empty. Use generous font sizes, spacing, and visual elements to fill the canvas. + +### Supported Elements + +- `<p>`, `<h1>`-`<h6>` — Styled text +- `<ul>`, `<ol>` — Lists (never use manual bullet symbols like bullet, -, *) +- `<b>`, `<strong>`, `<i>`, `<em>`, `<u>` — Inline formatting +- `<span>` — Inline formatting with CSS styles +- `<br>` — Line breaks +- `<div>` with bg/border — Becomes shapes +- `<img>` — Images +- `class="placeholder"` — Reserves space for charts (returns `{ id, x, y, w, h }`) + +### Key Text Rules + +**All text must be inside `<p>`, `<h1>`-`<h6>`, `<ul>`, or `<ol>` tags:** +- ✅ `<div><p>Text here</p></div>` +- ❌ `<div>Text here</div>` — **Silently ignored in PowerPoint** +- ❌ `<span>Text here</span>` — **Silently ignored in PowerPoint** + +**Never use manual bullet symbols (bullet, -, *, etc.)** — use `<ul>` or `<ol>` instead. + +**v3 Smart Font Mapping (pass-through + safe fallback):** + +html2pptx.js v3 no longer maps all fonts to the same one. Strategy: +1. **PPT-safe fonts** (Corbel, Arial, SimHei, Palatino Linotype, etc. 40+) → **Pass through directly** +2. **macOS-exclusive fonts** (PingFang SC, Hiragino Sans) → Mapped to cross-platform equivalents +3. **Web fonts** (Roboto, Montserrat, Inter, etc. 40+) → Mapped to visually closest PPT-safe font +4. **CSS generic names** (sans-serif/serif) → Corbel / Times New Roman +5. **Unknown fonts** → CJK falls back to fontConfig.cjk; Latin passes through directly + +**Write PPT font names directly in HTML**: +```css +font-family: "SimHei", "Microsoft YaHei", sans-serif; /* CJK heading */ +font-family: "Microsoft YaHei", sans-serif; /* CJK body */ +font-family: "Gill Sans MT", "Century Gothic", sans-serif; /* English heading */ +``` + +**fontConfig is still available** (as CJK/Latin ultimate fallback, optional): +```javascript +const fontConfig = { cjk: 'SimHei', latin: 'Gill Sans MT' }; +const result = await html2pptx('slide.html', pptx, { fontConfig }); +``` + +### Styles + +- Body must use `display: flex; flex-direction: column;` — without it, multiple direct children stack horizontally +- **Do not use `flex-wrap`** — multi-row layouts must use separate flex containers (html2pptx renderer may lose wrapped content) +- `<span>` supports: `font-weight`, `font-style`, `text-decoration`, `color`, `font-size`, `letter-spacing`; color accepts `rgba()` for transparency +- `<span>` does NOT support: `margin`, `padding` +- `text-transform: uppercase / lowercase / capitalize` works on all text elements and `<span>` +- **Rotated text**: `transform: rotate(-30deg)` or `writing-mode: vertical-rl / vertical-lr` +- Use hex colors with `#` prefix in CSS; use `text-align` for alignment hints + +### Shape Styles (DIV elements only) + +Backgrounds, borders, and shadows **only work on `<div>`**, not on text elements. + +- **Background**: `background-color` on `<div>` only +- **Border**: uniform (`border: 2px solid #333`) or partial (`border-left`, `border-right`, etc.) +- **Border radius**: `border-radius: 8pt` for rounded corners; `50%` for circle; percentages relative to smaller dimension +- **Box shadow**: outer shadows only — `box-shadow: 2px 2px 8px rgba(0,0,0,0.3)`; inset shadows ignored + +### Typography Guidelines + +Choose font sizes that create clear visual hierarchy. Refer to `design-system.md` for suggested ranges. + +**Minimum font size**: Don't go below ~11pt for any text. Prefer ≥13pt for body text. +**Hierarchy principle**: Headings should be noticeably larger and bolder than body text. + +### Spacing Guidelines + +Use consistent spacing throughout the deck. Refer to `design-system.md` for suggested values. Key principles: +- Page margins: ~48pt left/right, ~40pt top, ≥36pt bottom safe zone +- Be generous with whitespace — but fill the slide; avoid large empty areas + +### Color Rules + +**All colors must come from the current theme's color scale.** Arbitrary grays unrelated to the primary color are forbidden. + +After selecting a theme from `themes.md`, use that theme's complete color scale. + +### Image Rules (MANDATORY for decks with 6+ slides) + +Every deck with 6+ slides must include real photographs. Images create visual richness and professional quality. + +**Image sourcing priority**: +1. **Unsplash** (free, high quality) — use theme's **Image Keywords** from `themes.md`: + ```bash + curl -L "https://source.unsplash.com/1920x1080/?keyword1,keyword2" -o cover-bg.jpg + ``` +2. **User-provided images** — local files +3. **Gradient fallback** — if Unsplash fails, generate gradient PNG via Sharp + +**Image usage in HTML**: +- **Page background**: `<body style="background-image:url('bg.jpg'); background-size:cover;">` +- **Inline image**: `<img src="photo.jpg" style="width:296pt; height:220pt; object-fit:cover;">` +- **DIV background-image is NOT supported** — only body background-image works + +**Mask overlay for photo backgrounds**: +```html +<!-- Background photo on body --> +<body style="width:720pt; height:405pt; margin:0; padding:0; overflow:hidden; + background-image:url('cover-bg.jpg'); background-size:cover; + font-family:'PingFang SC','Microsoft YaHei',sans-serif; + display:flex; flex-direction:column;"> + <!-- Semi-transparent mask layer (use theme's Mask Color from themes.md) --> + <div style="position:absolute; top:0; left:0; width:720pt; height:405pt; + background-color:rgba(18,32,64,0.75);"></div> + <!-- Content layer above mask --> + <div style="position:relative; z-index:1; flex:1; display:flex; flex-direction:column; + justify-content:center; align-items:center;"> + <h1 style="font-size:34pt; font-weight:bold; color:#FFFFFF;">Title Here</h1> + </div> +</body> +``` + +**Mask opacity guide**: +- Dark mask (cover/divider): opacity 0.70–0.85 — text clearly readable +- Light mask (content): opacity 0.60–0.80 — retains image visibility + +### Icons & Gradients + +- **CRITICAL: Never use CSS gradients** — pre-rasterize as PNG with Sharp +- **Icons**: rasterize react-icons SVG to PNG; **Gradients**: rasterize SVG to PNG background + +```javascript +// Icon to PNG +const { FaHome } = require('react-icons/fa'); +const svgString = ReactDOMServer.renderToStaticMarkup( + React.createElement(FaHome, { color: '#4472C4', size: '256' }) +); +await sharp(Buffer.from(svgString)).png().toFile('home-icon.png'); +// In HTML: <img src="home-icon.png" style="width:40pt;height:40pt;"> + +// Gradient to PNG +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('gradient-bg.png'); +// In HTML: <body style="background-image: url('gradient-bg.png');"> +``` + +--- + +## Using the html2pptx Library + +### Dependencies + +Globally installed: `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 + +// Optional: custom font configuration (from themes.md) +const fontConfig = { cjk: 'Microsoft YaHei', latin: 'Gill Sans MT' }; + +const { slide, placeholders, warnings } = await html2pptx('slide1.html', pptx, { fontConfig }); + +// If warnings is non-empty, fix the HTML and re-run +if (warnings.length > 0) { + console.error('Fix overflow issues before saving:', warnings); + process.exit(1); +} + +if (placeholders.length > 0) { + slide.addChart(pptx.charts.LINE, chartData, placeholders[0]); +} + +await pptx.writeFile('output.pptx'); +``` + +### API Reference + +```javascript +await html2pptx(htmlFile, pres, options) +``` + +**Parameters:** +- `htmlFile` (string): Path to HTML file +- `pres` (pptxgen): PptxGenJS instance with layout set +- `options.tmpDir` (string): Temp dir (default: `process.env.TMPDIR || '/tmp'`) +- `options.slide` (object): Existing slide to reuse +- `options.fontConfig` (object): Font mapping config `{ cjk: 'Microsoft YaHei', latin: 'Corbel' }` + +**Returns:** +```javascript +{ + slide: pptxgenSlide, + placeholders: [{ id, x, y, w, h }, ...], + warnings: string[] // Overflow and layout suggestions; empty if none +} +``` + +### Validation + +**Blocking errors** (conversion aborted): +1. CSS gradients — must be pre-rasterized as PNG +2. Backgrounds/borders/shadows on text elements (`<p>`, `<h1>`-`<h6>`, etc.) +3. Unwrapped text directly in `<div>` +4. Manual bullet symbols in text elements +5. Font size below 11pt + +**Non-blocking warnings** (conversion succeeds, returned in `warnings`): +1. Element out of bounds — extends beyond slide edges +2. Vertical imbalance — content clusters in top 55% +3. Text overlap — text elements overlap each other +4. Character density — exceeds 350 CJK / 550 Latin chars +5. Body-level overflow + +**Blocking issues** (overflow, font < 11pt) must be fixed. **Non-blocking warnings** are suggestions — use your judgment. + +### Visual Quality Check + +After generating all slides, do a quick quality scan: +- Is there visual variety across the deck? (different layouts, backgrounds, card styles) +- Do real photographs appear? (at least on cover + 1 other slide for 6+ slide decks) +- Is there at least one dramatic "rhythm breaker" page? +- Does every slide have a clear visual focal point? + +### Working with Placeholders + +```javascript +const { slide, placeholders } = await html2pptx('slide.html', pptx); +slide.addChart(pptx.charts.BAR, data, placeholders[0]); + +// By ID: +const chartArea = placeholders.find(p => p.id === 'chart-area'); +slide.addChart(pptx.charts.LINE, data, chartArea); +``` + +--- + +## Using PptxGenJS + +### Critical Rules + +**NEVER use `#` prefix** with hex colors in PptxGenJS — causes file corruption. +- ✅ `color: "FF0000"`, `fill: { color: "0066CC" }` +- ❌ `color: "#FF0000"` + +### Adding Images + +```javascript +const imgWidth = 1860, imgHeight = 1519; +const h = 3, w = h * (imgWidth / imgHeight); +slide.addImage({ path: "chart.png", x: (10 - w) / 2, y: 1.5, w, h }); +``` + +### Adding Shapes + +```javascript +slide.addShape(pptx.shapes.RECTANGLE, { + x: 1, y: 1, w: 3, h: 2, + fill: { color: "4472C4" }, + line: { color: "2E5DA8", width: 2 }, + rectRadius: 0.1 // rounded corners (ROUNDED_RECTANGLE only) +}); +``` + +### Adding Charts + +**Time Series Granularity:** `< 30 days` daily | `30-365 days` monthly | `> 365 days` yearly. + +#### Bar Chart + +```javascript +slide.addChart(pptx.charts.BAR, [{ + name: "Sales 2024", + labels: ["Q1", "Q2", "Q3", "Q4"], + values: [4500, 5500, 6200, 7100] +}], { + ...placeholders[0], + barDir: 'col', + showTitle: true, title: 'Quarterly Sales', + showLegend: false, + showCatAxisTitle: true, catAxisTitle: 'Quarter', + showValAxisTitle: true, valAxisTitle: 'Sales ($000s)', + valAxisMinVal: 0, valAxisMaxVal: 8000, valAxisMajorUnit: 2000, + chartColors: ["4472C4"] +}); +``` + +#### 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, + showCatAxisTitle: true, catAxisTitle: 'Month', + showValAxisTitle: true, valAxisTitle: 'Temp (F)', + valAxisMinVal: 0, valAxisMaxVal: 60, valAxisMajorUnit: 20, + chartColors: ["4472C4"] +}); +``` + +#### Pie Chart + +```javascript +slide.addChart(pptx.charts.PIE, [{ + name: "Market Share", + labels: ["Product A", "Product B", "Other"], + values: [35, 45, 20] +}], { + x: 2, y: 1, w: 6, h: 4, + showPercent: true, showLegend: true, legendPos: 'r', + chartColors: ["4472C4", "ED7D31", "A5A5A5"] +}); +``` + +#### Scatter Chart + +```javascript +slide.addChart(pptx.charts.SCATTER, [ + { name: 'X-Axis', values: [10, 15, 20, 12, 18] }, + { name: 'Series 1', values: [20, 25, 30] }, + { name: 'Series 2', values: [18, 22] } +], { + x: 1, y: 1, w: 8, h: 4, + lineSize: 0, lineDataSymbol: 'circle', lineDataSymbolSize: 6, + showCatAxisTitle: true, catAxisTitle: 'X', + showValAxisTitle: true, valAxisTitle: 'Y', + chartColors: ["4472C4", "ED7D31"] +}); +``` + +#### Multiple 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:** no `#` prefix; align with slide palette; strong contrast between adjacent series. + +### Adding Tables + +```javascript +const tableData = [ + [ + { 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 } } + ], + ["Product A", "$50M", "+15%"], + ["Product B", "$35M", "+22%"] +]; +slide.addTable(tableData, { + x: 1, y: 1.5, w: 8, h: 2.5, + colW: [3, 2.5, 2.5], rowH: [0.5, 0.6, 0.6], + border: { pt: 1, color: "CCCCCC" }, + align: "center", valign: "middle", fontSize: 14 +}); +``` + +**Table options:** `x, y, w, h` | `colW` | `rowH` | `border: { pt, color }` | `fill` | `align` | `valign` | `fontSize` | `autoPage` diff --git a/skills/ppt/ooxml.md b/skills/ppt/ooxml.md new file mode 100755 index 0000000..951b3cf --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-chart.xsd new file mode 100755 index 0000000..6454ef9 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd new file mode 100755 index 0000000..afa4f46 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd new file mode 100755 index 0000000..64e66b8 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd new file mode 100755 index 0000000..687eea8 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-main.xsd new file mode 100755 index 0000000..6ac81b0 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-picture.xsd new file mode 100755 index 0000000..1dbf051 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd new file mode 100755 index 0000000..f1af17d --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd new file mode 100755 index 0000000..0a185ab --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/pml.xsd new file mode 100755 index 0000000..14ef488 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd new file mode 100755 index 0000000..c20f3bf --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd new file mode 100755 index 0000000..ac60252 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd new file mode 100755 index 0000000..424b8ba --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd new file mode 100755 index 0000000..2bddce2 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd new file mode 100755 index 0000000..8a8c18b --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd new file mode 100755 index 0000000..5c42706 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd new file mode 100755 index 0000000..853c341 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd new file mode 100755 index 0000000..da835ee --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-math.xsd new file mode 100755 index 0000000..87ad265 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd new file mode 100755 index 0000000..9e86f1b --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/sml.xsd new file mode 100755 index 0000000..d0be42e --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-main.xsd new file mode 100755 index 0000000..8821dd1 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd new file mode 100755 index 0000000..ca2575c --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd new file mode 100755 index 0000000..dd079e6 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd new file mode 100755 index 0000000..3dd6cf6 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd new file mode 100755 index 0000000..f1041e3 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/wml.xsd new file mode 100755 index 0000000..9c5b7a6 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd b/skills/ppt/ooxml/schemas/ISO-IEC29500-4_2016/xml.xsd new file mode 100755 index 0000000..0f13678 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd b/skills/ppt/ooxml/schemas/ecma/fouth-edition/opc-contentTypes.xsd new file mode 100755 index 0000000..a6de9d2 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd b/skills/ppt/ooxml/schemas/ecma/fouth-edition/opc-coreProperties.xsd new file mode 100755 index 0000000..10e978b --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd b/skills/ppt/ooxml/schemas/ecma/fouth-edition/opc-digSig.xsd new file mode 100755 index 0000000..4248bf7 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd b/skills/ppt/ooxml/schemas/ecma/fouth-edition/opc-relationships.xsd new file mode 100755 index 0000000..5649746 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/mce/mc.xsd b/skills/ppt/ooxml/schemas/mce/mc.xsd new file mode 100755 index 0000000..ef72545 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/microsoft/wml-2010.xsd b/skills/ppt/ooxml/schemas/microsoft/wml-2010.xsd new file mode 100755 index 0000000..f65f777 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/microsoft/wml-2012.xsd b/skills/ppt/ooxml/schemas/microsoft/wml-2012.xsd new file mode 100755 index 0000000..6b00755 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/microsoft/wml-2018.xsd b/skills/ppt/ooxml/schemas/microsoft/wml-2018.xsd new file mode 100755 index 0000000..f321d33 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/microsoft/wml-cex-2018.xsd b/skills/ppt/ooxml/schemas/microsoft/wml-cex-2018.xsd new file mode 100755 index 0000000..364c6a9 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/microsoft/wml-cid-2016.xsd b/skills/ppt/ooxml/schemas/microsoft/wml-cid-2016.xsd new file mode 100755 index 0000000..fed9d15 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd b/skills/ppt/ooxml/schemas/microsoft/wml-sdtdatahash-2020.xsd new file mode 100755 index 0000000..680cf15 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/schemas/microsoft/wml-symex-2015.xsd b/skills/ppt/ooxml/schemas/microsoft/wml-symex-2015.xsd new file mode 100755 index 0000000..89ada90 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/scripts/pack.py b/skills/ppt/ooxml/scripts/pack.py new file mode 100755 index 0000000..68bc088 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/scripts/unpack.py b/skills/ppt/ooxml/scripts/unpack.py new file mode 100755 index 0000000..b2e4e87 --- /dev/null +++ b/skills/ppt/ooxml/scripts/unpack.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +"""Unpack and format XML contents of Office files (.docx, .pptx, .xlsx)""" + +import argparse +import random +import zipfile +import defusedxml.minidom +from pathlib import Path + + +def main(): + parser = argparse.ArgumentParser(description="Unpack an Office file into a directory") + parser.add_argument("office_file", help="Office file (.docx/.pptx/.xlsx)") + parser.add_argument("output_dir", help="Output directory") + args = parser.parse_args() + unpack_document(args.office_file, args.output_dir) + + +def unpack_document(input_file, output_dir): + """Unpack an Office file into a directory and pretty-print all XML files.""" + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + + with zipfile.ZipFile(input_file) as zf: + zf.extractall(output_path) + + for pattern in ["*.xml", "*.rels"]: + for xml_file in output_path.rglob(pattern): + pretty_print_xml(xml_file) + + # For .docx files, suggest an RSID for tracked changes + if str(input_file).endswith(".docx"): + suggested_rsid = "".join(random.choices("0123456789ABCDEF", k=8)) + print(f"Suggested RSID for edit session: {suggested_rsid}") + + +def pretty_print_xml(xml_file): + """Pretty-print a single XML file in place.""" + content = xml_file.read_text(encoding="utf-8") + dom = defusedxml.minidom.parseString(content) + xml_file.write_bytes(dom.toprettyxml(indent=" ", encoding="ascii")) + + +if __name__ == "__main__": + main() diff --git a/skills/ppt/ooxml/scripts/validate.py b/skills/ppt/ooxml/scripts/validate.py new file mode 100755 index 0000000..632e867 --- /dev/null +++ b/skills/ppt/ooxml/scripts/validate.py @@ -0,0 +1,70 @@ +#!/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() + if not unpacked_dir.is_dir(): + sys.exit(f"Error: {unpacked_dir} is not a directory") + if not original_file.is_file(): + sys.exit(f"Error: {original_file} is not a file") + if file_extension not in [".docx", ".pptx", ".xlsx"]: + sys.exit(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/ppt/ooxml/scripts/validation/__init__.py b/skills/ppt/ooxml/scripts/validation/__init__.py new file mode 100755 index 0000000..db092ec --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/scripts/validation/__pycache__/__init__.cpython-313.pyc b/skills/ppt/ooxml/scripts/validation/__pycache__/__init__.cpython-313.pyc new file mode 100755 index 0000000..281409e Binary files /dev/null and b/skills/ppt/ooxml/scripts/validation/__pycache__/__init__.cpython-313.pyc differ diff --git a/skills/ppt/ooxml/scripts/validation/__pycache__/base.cpython-313.pyc b/skills/ppt/ooxml/scripts/validation/__pycache__/base.cpython-313.pyc new file mode 100755 index 0000000..db02d46 Binary files /dev/null and b/skills/ppt/ooxml/scripts/validation/__pycache__/base.cpython-313.pyc differ diff --git a/skills/ppt/ooxml/scripts/validation/__pycache__/docx.cpython-313.pyc b/skills/ppt/ooxml/scripts/validation/__pycache__/docx.cpython-313.pyc new file mode 100755 index 0000000..e1fe517 Binary files /dev/null and b/skills/ppt/ooxml/scripts/validation/__pycache__/docx.cpython-313.pyc differ diff --git a/skills/ppt/ooxml/scripts/validation/__pycache__/pptx.cpython-313.pyc b/skills/ppt/ooxml/scripts/validation/__pycache__/pptx.cpython-313.pyc new file mode 100755 index 0000000..b799d63 Binary files /dev/null and b/skills/ppt/ooxml/scripts/validation/__pycache__/pptx.cpython-313.pyc differ diff --git a/skills/ppt/ooxml/scripts/validation/__pycache__/redlining.cpython-313.pyc b/skills/ppt/ooxml/scripts/validation/__pycache__/redlining.cpython-313.pyc new file mode 100755 index 0000000..09f1b27 Binary files /dev/null and b/skills/ppt/ooxml/scripts/validation/__pycache__/redlining.cpython-313.pyc differ diff --git a/skills/ppt/ooxml/scripts/validation/base.py b/skills/ppt/ooxml/scripts/validation/base.py new file mode 100755 index 0000000..7a03377 --- /dev/null +++ b/skills/ppt/ooxml/scripts/validation/base.py @@ -0,0 +1,948 @@ +""" +Base validator with common validation logic for document files. +""" + +import re +import tempfile +import zipfile +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. + """ + 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 + """ + # 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/ppt/ooxml/scripts/validation/docx.py b/skills/ppt/ooxml/scripts/validation/docx.py new file mode 100755 index 0000000..602c470 --- /dev/null +++ b/skills/ppt/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/ppt/ooxml/scripts/validation/pptx.py b/skills/ppt/ooxml/scripts/validation/pptx.py new file mode 100755 index 0000000..462154c --- /dev/null +++ b/skills/ppt/ooxml/scripts/validation/pptx.py @@ -0,0 +1,309 @@ +""" +Validator for PowerPoint presentation XML files against XSD schemas. +""" + +import re + +import lxml.etree + +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.""" + 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.""" + 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.""" + 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.""" + 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/ppt/ooxml/scripts/validation/redlining.py b/skills/ppt/ooxml/scripts/validation/redlining.py new file mode 100755 index 0000000..06c9af9 --- /dev/null +++ b/skills/ppt/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 Z.AI 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 Z.AI + del_elements = root.findall(".//w:del", self.namespaces) + ins_elements = root.findall(".//w:ins", self.namespaces) + + # Filter to only include changes by Z.AI + zai_del_elements = [ + elem + for elem in del_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == "Z.AI" + ] + zai_ins_elements = [ + elem + for elem in ins_elements + if elem.get(f"{{{self.namespaces['w']}}}author") == "Z.AI" + ] + + # Redlining validation is only needed if tracked changes by Z.AI have been used. + if not zai_del_elements and not zai_ins_elements: + if self.verbose: + print("PASSED - No tracked changes by Z.AI 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 Z.AI's tracked changes from both documents + self._remove_zai_tracked_changes(original_root) + self._remove_zai_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 Z.AI 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 Z.AI'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_zai_tracked_changes(self, root): + """Remove tracked changes authored by Z.AI 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) == "Z.AI": + to_remove.append(child) + for elem in to_remove: + parent.remove(elem) + + # Unwrap content in w:del elements where author is "Z.AI" + 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) == "Z.AI": + 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/ppt/references/beamer.md b/skills/ppt/references/beamer.md new file mode 100755 index 0000000..56658c6 --- /dev/null +++ b/skills/ppt/references/beamer.md @@ -0,0 +1,721 @@ +# Route: LaTeX Beamer → PDF (via Tectonic) + +Write LaTeX Beamer source, then compile to PDF: + +```bash +python3 scripts/pdf.py convert.latex main.tex +python3 scripts/pdf.py convert.latex main.tex --runs 2 # for ToC / refs +``` + +Tectonic compiles the full Beamer document — all features work: overlays, +TikZ, custom themes, math, transitions, fragile frames, appendix. + +--- + +## 1. Compilation + +```bash +# Standard build +python3 scripts/pdf.py convert.latex main.tex + +# Two passes (resolves ToC, \ref, section counters in overlays) +python3 scripts/pdf.py convert.latex main.tex --runs 2 + +# Verbose log (see all Tectonic output) +python3 scripts/pdf.py convert.latex main.tex --keep-logs +``` + +--- + +## 2. Beamer Document Structure + +### Minimal Working Example + +```latex +\documentclass[aspectratio=169, 11pt]{beamer} +\usepackage[fontset=fandol]{ctex} % CJK support (Tectonic auto-downloads) +\usetheme{Madrid} + +\title{Introduction to Quantum Computing} +\author{Prof. Zhang} +\institute{Tsinghua University} +\date{\today} + +\begin{document} + +\begin{frame} + \titlepage +\end{frame} + +\begin{frame}{Table of Contents} + \tableofcontents +\end{frame} + +\section{Quantum Bits} + +\begin{frame}{Quantum States} + \[ |\psi\rangle = \alpha|0\rangle + \beta|1\rangle, \quad |\alpha|^2+|\beta|^2=1 \] +\end{frame} + +\end{document} +``` + +**Core elements:** +- `\documentclass{beamer}` — Presentation document class +- Preamble — Theme, colors, fonts, global settings +- `\begin{frame}...\end{frame}` — Each frame = one slide + +--- + +## 3. Theme System (Complete Reference) + +Beamer themes consist of five independent layers that can be freely combined: + +### 3.1 Presentation Themes (control overall layout) + +| Theme | Features | Suitable For | +|---|---|---| +| `default` | Minimal, no decoration | Formal academic, minimalist | +| `Madrid` | Bottom info bar + section navigation | University courses, academic talks | +| `Berlin` | Top mini-frames + side color blocks | Long technical presentations | +| `Warsaw` | Top circular navigation + gradient title | Structured content | +| `CambridgeUS` | Two-tone minimalist | Math-heavy presentations | +| `Boadilla` | Clean footer | Business, concise style | +| `Singapore` | Top dot navigation | Modern feel, multi-section | +| `AnnArbor` | Yellow-blue dual tone | Michigan style | +| `Antibes` | Blue sidebar tree | Multi-subsection structure | +| `Bergen` | Blue title bar | Formal academic | +| `Copenhagen` | Top bar + navigation | Classic blue scheme | +| `Darmstadt` | Top progress dots | Multi-section structure | +| `Dresden` | Top mini-frames | Similar to Berlin | +| `Frankfurt` | Top dots | Clear section structure | +| `Goettingen` | Right sidebar | Navigation-focused | +| `Luebeck` | Top blue bar | Clean modern | +| `Malmoe` | Minimal title | Content-first | +| `Montpellier` | Top tree navigation | Hierarchical content | +| `Pittsburgh` | No color blocks, minimal | White scheme | +| `Rochester` | Dark title | Dark scheme | +| `Szeged` | Blue title bar | Hungarian style | + +```latex +\usetheme{Madrid} +``` + +### 3.2 Color Themes + +| Color Theme | Dominant Color | +|---|---| +| `default` | Dark blue | +| `albatross` | Yellow tones | +| `beaver` | Dark red / maroon | +| `beetle` | Gray-blue | +| `crane` | Orange-yellow | +| `dolphin` | Blue-white | +| `dove` | Gray-white (near monochrome) | +| `fly` | Gray tones | +| `lily` | Red-blue | +| `orchid` | Purple tones | +| `rose` | Pink | +| `seagull` | Gray | +| `seahorse` | Blue-purple | +| `whale` | Deep sea blue | +| `wolverine` | Yellow-blue contrast | + +```latex +\usecolortheme{dolphin} +``` + +### 3.3 Font Themes + +```latex +\usefonttheme{default} % Sans-serif (Beamer default) +\usefonttheme{serif} % Serif font (academic feel) +\usefonttheme{structurebold} % Bold structural elements +\usefonttheme{structureitalic} % Italic structural elements +\usefonttheme{structuresmallcapsserif} % Small caps serif +\usefonttheme{professionalfonts} % Keep math fonts unchanged +``` + +### 3.4 Inner Themes (control list, block, and title styles) + +```latex +\useinnertheme{default} % Triangle bullet points +\useinnertheme{circles} % Circle bullet points +\useinnertheme{rectangles} % Square bullet points +\useinnertheme{rounded} % Rounded blocks +\useinnertheme{inmargin} % Margin numbers +``` + +### 3.5 Outer Themes (control header, footer, and sidebar) + +```latex +\useoutertheme{default} % No decoration +\useoutertheme{infolines} % Bottom three-column info +\useoutertheme{miniframes} % Top mini-frame navigation +\useoutertheme{shadow} % Shadow title +\useoutertheme{sidebar} % Sidebar +\useoutertheme{smoothbars} % Gradient top bar +\useoutertheme{smoothtree} % Gradient tree +\useoutertheme{split} % Top-bottom split +\useoutertheme{tree} % Tree navigation +``` + +### 3.6 Custom Colors + +```latex +\definecolor{myblue}{HTML}{2B5EA7} +\definecolor{mygray}{HTML}{F0F0F0} + +\setbeamercolor{frametitle}{bg=myblue, fg=white} +\setbeamercolor{structure}{fg=myblue} +\setbeamercolor{block title}{bg=myblue!85, fg=white} +\setbeamercolor{block body}{bg=myblue!8} +\setbeamercolor{alerted text}{fg=red!70!black} +\setbeamercolor{title}{fg=white} +\setbeamercolor{subtitle}{fg=white!80} +``` + +### 3.7 Common Template Customizations + +```latex +% Remove navigation symbols (almost always needed) +\setbeamertemplate{navigation symbols}{} + +% Custom footer (frame number) +\setbeamertemplate{footline}{% + \hfill\insertframenumber/\inserttotalframenumber\hspace{1em}\vspace{0.5em} +} + +% Custom bullet points +\setbeamertemplate{itemize item}{\textbullet} +\setbeamertemplate{itemize subitem}{--} + +% Logo (bottom-right corner) +\logo{\includegraphics[height=0.7cm]{logo.png}} + +% Rounded blocks +\setbeamertemplate{blocks}[rounded][shadow=true] +``` + +--- + +## 4. Overlays & Stepwise Reveal + +Tectonic fully supports all overlay syntax — rendered as separate pages in the PDF. + +### 4.1 Basic Overlay Specifications + +```latex +\item<1-> % Show from layer 1, persist +\item<2-> % Show from layer 2 +\item<1-3> % Show on layers 1 to 3 only +\item<2> % Show on layer 2 only +\item<-3> % Show before layer 3 +``` + +### 4.2 \pause — Simplest Stepwise Reveal + +```latex +\begin{frame}{Research Workflow} + Data collection phase + \pause + Feature engineering phase + \pause + Model training phase +\end{frame} +``` + +### 4.3 List Stepwise Reveal + +```latex +\begin{frame}{Research Contributions} + \begin{itemize} + \item<1-> Propose a novel loss function + \item<2-> Prove theoretical convergence + \item<3-> Outperform SOTA on five benchmarks + \end{itemize} +\end{frame} +``` + +### 4.4 \only vs \uncover vs \visible + +```latex +% \only: no space reserved (causes layout jump) +\only<1>{Step one explanation} +\only<2>{Step two explanation} + +% \uncover: reserves space but transparent (recommended, stable layout) +\uncover<2->{Follow-up steps explanation} + +% \visible: reserves space but not rendered +\visible<3->{Step three} +``` + +### 4.5 \alert — Highlight Specific Layers + +```latex +\begin{frame}{Key Findings} + \begin{itemize} + \item \alert<1>{Accuracy improved by 13\%} + \item \alert<2>{Inference speed improved 2.4x} + \item \alert<3>{Memory reduced by 40\%} + \end{itemize} +\end{frame} +``` + +### 4.6 \textcolor & Conditional Content + +```latex +\begin{frame}{Comparison} + Baseline method: \textcolor{red}{82\%}\\ + \only<2->{Our method: \textcolor{green!60!black}{\textbf{93\%}}} +\end{frame} +``` + +### 4.7 Block Overlays + +```latex +\begin{frame}{Analysis} + \begin{block}<1-2>{Problem Definition} + Formal description ... + \end{block} + \begin{block}<2->{Solution} + Core approach ... + \end{block} +\end{frame} +``` + +--- + +## 5. Content Layout Templates + +### 5.1 Block Series (Three Styles) + +```latex +\begin{frame}{Core Findings} + \begin{block}{Main Conclusion} + The new method outperforms baselines in both accuracy and speed. + \end{block} + + \begin{alertblock}{Caveats} + Results are validated on specific datasets; generalization needs further study. + \end{alertblock} + + \begin{exampleblock}{Example} + Achieved 99.1\% accuracy on the MNIST dataset. + \end{exampleblock} +\end{frame} +``` + +### 5.2 Column Layout + +```latex +\begin{frame}{Experimental Comparison} + \begin{columns}[T] % [T] top-aligned + \column{0.48\textwidth} + \begin{block}{Baseline Method} + Accuracy: 82\% \\ + Time: 3.2 hours + \end{block} + + \column{0.48\textwidth} + \begin{block}{Our Method} + Accuracy: \textbf{91\%} \\ + Time: \textbf{1.7 hours} + \end{block} + \end{columns} +\end{frame} +``` + +Column alignment options: `[T]` top-aligned · `[c]` centered · `[b]` bottom-aligned + +### 5.3 Math Formula Frame + +```latex +\begin{frame}{Loss Function} + \begin{equation*} + \mathcal{L} = -\sum_{i=1}^{N} y_i \log \hat{y}_i + \lambda \|\theta\|_2^2 + \end{equation*} + \begin{itemize} + \item $y_i$: Ground truth label + \item $\hat{y}_i$: Predicted probability + \item $\lambda$: Regularization coefficient + \end{itemize} +\end{frame} +``` + +### 5.4 Theorem Environment + +```latex +\begin{frame}{Convergence Theorem} + \begin{theorem}[Convergence Rate] + Under Lipschitz conditions, gradient descent converges at $O(1/\sqrt{T})$. + \end{theorem} + + \begin{proof} + Follows from Lipschitz continuity and step size $\eta = 1/L$.\qed + \end{proof} + + \begin{corollary} + The stochastic gradient variant converges in expectation at $O(1/\sqrt{T})$. + \end{corollary} +\end{frame} +``` + +### 5.5 Table Frame + +```latex +\begin{frame}{Performance Comparison} + \centering + \begin{tabular}{lcc} + \toprule + Method & Accuracy & Time(s) \\ + \midrule + SVM & 0.85 & 120 \\ + Random Forest & 0.89 & 65 \\ + \textbf{Ours} & \textbf{0.93} & \textbf{42} \\ + \bottomrule + \end{tabular} +\end{frame} +``` + +### 5.6 TikZ Flowchart + +TikZ is fully supported in Tectonic — no pre-rendering needed: + +```latex +\begin{frame}{Algorithm Pipeline} + \centering + \begin{tikzpicture}[ + node distance=2cm, + box/.style={rectangle, rounded corners=4pt, draw=myblue, thick, + fill=myblue!10, minimum width=2.2cm, minimum height=0.9cm, font=\small}, + arr/.style={-Stealth, thick, myblue} + ] + \node[box] (A) {Input Data}; + \node[box, right of=A] (B) {Feature Extraction}; + \node[box, right of=B] (C) {Model Inference}; + \node[box, right of=C] (D) {Output Results}; + \draw[arr] (A) -- (B); + \draw[arr] (B) -- (C); + \draw[arr] (C) -- (D); + \end{tikzpicture} +\end{frame} +``` + +### 5.7 Code Display Frame + +```latex +\begin{frame}[fragile]{Training Loop} % [fragile] is required + \begin{verbatim} +for epoch in range(epochs): + for batch in dataloader: + optimizer.zero_grad() + loss = model(batch) + loss.backward() + optimizer.step() + \end{verbatim} +\end{frame} +``` + +Or use `listings` for syntax highlighting (more polished): + +```latex +\usepackage{listings} +\lstset{ + basicstyle=\ttfamily\small, + keywordstyle=\color{blue}\bfseries, + commentstyle=\color{gray}, + backgroundcolor=\color{gray!5}, + numbers=left, numberstyle=\tiny\color{gray}, + frame=single, framerule=0.5pt +} + +\begin{frame}[fragile]{Python Code} + \begin{lstlisting}[language=Python] +def train(model, dataloader): + for batch in dataloader: + loss = model(batch) + loss.backward() + \end{lstlisting} +\end{frame} +``` + +### 5.8 Image Frame + +```latex +\begin{frame}{System Architecture} + \centering + \includegraphics[width=0.85\textwidth]{figures/architecture.png} + \captionof{figure}{End-to-end training pipeline} +\end{frame} +``` + +### 5.9 Custom Checklist Frame + +```latex +\begin{frame}{Summary of Contributions} + \begin{itemize} + \item[\checkmark] New method proposed: ... + \item[\checkmark] Theoretical proof: ... + \item[\checkmark] Experimental validation: ... + \item[$\square$] Future work: ... + \end{itemize} +\end{frame} +``` + +--- + +## 6. Advanced Features + +### 6.1 Progress Bar (Manual Implementation) + +```latex +% Define in preamble +\definecolor{progressbar}{HTML}{2B5EA7} +\setbeamertemplate{footline}{ + \begin{beamercolorbox}[wd=\paperwidth,ht=2pt]{progressbar} + \rule{\dimexpr\paperwidth*\insertframenumber/\inserttotalframenumber}{2pt} + \end{beamercolorbox} + \vspace{2pt} + \hfill\tiny\insertframenumber/\inserttotalframenumber\hspace{1em}\vspace{3pt} +} +``` + +### 6.2 Section Title Pages (Automatic) + +```latex +% Add in preamble — auto-inserts a section title page before each \section +\AtBeginSection[]{ + \begin{frame} + \vfill + \centering + \begin{beamercolorbox}[sep=8pt,center,shadow=true,rounded=true]{title} + \usebeamerfont{title}\insertsectionhead\par + \end{beamercolorbox} + \vfill + \end{frame} +} +``` + +### 6.3 Handout Mode + +```latex +% Generate overlay-free handout version (only final state of each frame) +\documentclass[handout, aspectratio=169]{beamer} +``` + +Combine with `pgfpages` to compress multiple frames onto one page: + +```latex +\usepackage{pgfpages} +\pgfpagesuselayout{4 on 1}[a4paper, border shrink=5mm] +``` + +### 6.4 Frame Reuse (\againframe) + +```latex +\begin{frame}[label=keyresult]{Key Result} + Accuracy improved by 13\%. +\end{frame} + +% Show again at any later point +\againframe{keyresult} +``` + +### 6.5 Appendix (Excluded from Total Page Count) + +```latex +\appendix + +\begin{frame}{Appendix: Detailed Derivation} + % Backup slides for Q&A +\end{frame} +``` + +### 6.6 Speaker Notes + +```latex +\begin{frame}{Main Conclusions} + \begin{itemize} + \item Conclusion one + \item Conclusion two + \end{itemize} + \note{ + Emphasize the data supporting conclusion one.\\ + Anticipate questions about dataset size. + } +\end{frame} +``` + +Output PDF with notes: add in preamble: +```latex +\setbeameroption{show notes on second screen=right} +``` + +### 6.7 Widescreen Aspect Ratios + +```latex +\documentclass[aspectratio=169]{beamer} % 16:9 (recommended) +\documentclass[aspectratio=1610]{beamer} % 16:10 +\documentclass[aspectratio=43]{beamer} % 4:3 (traditional) +\documentclass[aspectratio=141]{beamer} % 1.41:1 (A4) +``` + +--- + +## 7. Complete Template (Academic Presentation, 16:9, with CJK Support) + +```latex +\documentclass[aspectratio=169, 11pt]{beamer} +\usepackage[fontset=fandol]{ctex} +\usepackage{amsmath, amssymb, mathtools} +\usepackage{booktabs} +\usepackage{graphicx} +\usepackage{xcolor} +\usepackage{tikz} +\usetikzlibrary{arrows.meta, positioning, shapes.geometric} + +% -- Theme --------------------------------------------------------------- +\usetheme{Madrid} +\usecolortheme{dolphin} +\usefonttheme{professionalfonts} +\setbeamertemplate{navigation symbols}{} + +% -- Colors -------------------------------------------------------------- +\definecolor{myblue}{HTML}{2B5EA7} +\setbeamercolor{frametitle}{bg=myblue, fg=white} +\setbeamercolor{structure}{fg=myblue} +\setbeamercolor{block title}{bg=myblue!85, fg=white} +\setbeamercolor{block body}{bg=myblue!8} + +% -- Auto section title page --------------------------------------------- +\AtBeginSection[]{ + \begin{frame} + \vfill\centering + \begin{beamercolorbox}[sep=8pt,center,rounded=true]{title} + \usebeamerfont{title}\insertsectionhead\par + \end{beamercolorbox} + \vfill + \end{frame} +} + +% -- Metadata ------------------------------------------------------------ +\title{Paper Title} +\subtitle{Subtitle} +\author{Author Name} +\institute{Affiliation} +\date{\today} + +% ======================================================================== +\begin{document} + +\begin{frame} + \titlepage +\end{frame} + +\begin{frame}{Table of Contents} + \tableofcontents +\end{frame} + +\section{Introduction} + +\begin{frame}{Research Background} + \begin{itemize} + \item<1-> Importance of the problem + \item<2-> Limitations of existing methods + \item<3-> Contributions of this work + \end{itemize} +\end{frame} + +\section{Method} + +\begin{frame}{Core Formula} + \begin{equation*} + \mathcal{L} = -\sum_{i=1}^{N} y_i \log \hat{y}_i + \lambda \|\theta\|_2^2 + \end{equation*} + \begin{itemize} + \item $y_i$: Ground truth label + \item $\hat{y}_i$: Predicted probability + \item $\lambda$: Regularization coefficient + \end{itemize} +\end{frame} + +\begin{frame}{Method Comparison} + \begin{columns}[T] + \column{0.48\textwidth} + \begin{block}{Baseline Method} + Accuracy: 82\% \\ + Time: 3.2 hours + \end{block} + \column{0.48\textwidth} + \begin{block}{Our Method} + Accuracy: \textbf{91\%} \\ + Time: \textbf{1.7 hours} + \end{block} + \end{columns} +\end{frame} + +\section{Experiments} + +\begin{frame}{Performance Comparison} + \centering + \begin{tabular}{lcc} + \toprule + Method & Accuracy & Time(s) \\ + \midrule + SVM & 0.85 & 120 \\ + Random Forest & 0.89 & 65 \\ + \textbf{Ours} & \textbf{0.93} & \textbf{42} \\ + \bottomrule + \end{tabular} +\end{frame} + +\section{Conclusion} + +\begin{frame}{Summary} + \begin{itemize} + \item[\checkmark] Contribution 1: Proposed new method + \item[\checkmark] Contribution 2: Theoretical proof + \item[\checkmark] Contribution 3: Experimental validation + \end{itemize} + \begin{block}{Future Work} + Extend to larger-scale datasets and explore cross-domain generalization. + \end{block} +\end{frame} + +\begin{frame} + \centering\Large Thank You!\\[0.8em] + \normalsize Questions Welcome +\end{frame} + +\end{document} +``` + +Compile: +```bash +python3 scripts/pdf.py convert.latex main.tex --runs 2 +``` + +--- + +## 8. Common Issues & Fixes + +| Issue | Cause | Fix | +|---|---|---| +| CJK characters display as boxes | Missing CJK font package | Add `\usepackage[fontset=fandol]{ctex}` | +| `??` appears in ToC or references | Only compiled once | Add `--runs 2` | +| `[fragile]` missing error | verbatim/lstlisting frame | Add `[fragile]` after `\begin{frame}` | +| Overlay not working | Forgot `\pause` or `<n->` in frame | Check overlay specification syntax | +| TikZ compilation failure | Missing tikzlibrary | Add `\usetikzlibrary{...}` | +| Math font distortion | Missing professionalfonts | Add `\usefonttheme{professionalfonts}` | +| Frame exceeds one page (content overflow) | Too much content | Add `[allowframebreaks]` or split frame | + +--- + +## 9. Dependencies + +| Tool | Purpose | +|---|---| +| `scripts/tectonic` | LaTeX compilation engine (local binary) | +| `scripts/pdf.py convert.latex` | Tectonic wrapper with log filtering + PDF stats | diff --git a/skills/ppt/references/latex.md b/skills/ppt/references/latex.md new file mode 100755 index 0000000..82cc7bd --- /dev/null +++ b/skills/ppt/references/latex.md @@ -0,0 +1,342 @@ +# Route 3: Academic & Scientific PDF via LaTeX + +Produce publication-grade PDFs from `.tex` source files compiled with Tectonic. Suited for academic papers, theses, mathematical manuscripts, IEEE/ACM-format submissions, and any document where the user explicitly requests LaTeX. + +**Guiding principle**: when someone asks for "a LaTeX PDF," they expect a polished, professional result — not a bare-minimum compilation. + +--- + +## Environment Preparation + + +The binary of Tectonic lands at `scripts/tectonic`. + +### Compilation — Always Use the Wrapper Script + +All compilations **must** go through `scripts/pdf.py convert.latex`. It handles: +- Stripping noisy package-download logs +- Filtering progress chatter +- Surfacing genuine errors and warnings +- Reporting PDF statistics (file size, page count, word estimate, image tally) + +**Do not invoke Tectonic directly.** + +```bash +# One-pass build +python3 scripts/pdf.py convert.latex main.tex + +# Two passes (resolves cross-references) +python3 scripts/pdf.py convert.latex main.tex --runs 2 + +# Verbose mode (retains full log output) +python3 scripts/pdf.py convert.latex main.tex --keep-logs +``` + +--- + +## Pre-Writing Planning + +Before touching `.tex` code, determine the document's category and let that shape your approach. + +### Recognising the Document Type + +| Type | Typical outputs | Key concern | +|------|----------------|-------------| +| **Scholarly** | Journal articles, conference proceedings, regulatory specs | Rigorous adherence to academic conventions; bibliography accuracy | +| **Utilitarian** | Technical reports, reference manuals, product specs | Pack maximum information while preserving scannability | +| **Persuasive** | Funding proposals, pitch documents, project roadmaps | Clean professionalism throughout, with one or two visual high-points (title page, KPI dashboards) | +| **Expressive** | Design portfolios, brand guidebooks, showcases | Bold typographic and chromatic choices; deliberate rule-breaking that amplifies impact | + +### Fallback Aesthetic (No Style Specified) + +When the user gives no visual direction, apply a **measured, high-craft** system: + +1. **Contrast** — clear figure-ground separation; headings visually distinct from running text +2. **Hierarchy** — establish reading order through deliberate variation in size, weight, and hue +3. **White space** — ample margins and leading to let the page breathe +4. **Coherence** — one typeface family, one accent colour, one spacing rhythm + +#### Enrichment Elements (Add Proactively When Appropriate) + +- **Decorative**: shaded callout boxes, sidebars, comparison panels → `tcolorbox` +- **Scholarly**: theorem / definition / proof environments → `amsthm` + `tcolorbox`; process diagrams → TikZ +- **Page furniture**: running headers and footers → `fancyhdr`; chapter openers → `titlesec` + +Introduce these components on your own initiative whenever the content benefits — don't wait to be told. + +--- + +## Mandatory Rules + +### Rule 1 — Every Build Diagnostic Demands Attention + +The wrapper classifies compiler output into three tiers: + +| Tier | Impact | Required response | +|------|--------|-------------------| +| **Errors** | Build aborts | Resolve before anything else | +| **Layout defects** | Overfull / underfull boxes, unavailable font shapes, missing glyphs | Repair prior to delivery | +| **Advisories** | Remaining warnings | Assess individually; fix whenever feasible | + +**Never acceptable**: shrugging off warnings with "they don't affect the final PDF." Every diagnostic merits investigation. + +### Rule 2 — Modular Files for Larger Works + +A single generation turn can comfortably handle roughly **500 lines** of TeX. + +**When to split**: +- Anticipated output exceeds 5 pages, **or** +- Three or more heavyweight elements are present (wide tables, block equations, TikZ graphics) + +Stitch modules together with `\input{chapter1}`, `\input{chapter2}`, etc. + +### Rule 3 — Foundation Preamble + +```latex +\documentclass{article} + +% Essentials +\usepackage{graphicx} +\usepackage{xcolor} +\usepackage{geometry} +\usepackage{amsmath} % Load before hyperref + +% hyperref — ALWAYS last among content packages +\usepackage[ + colorlinks=true, + linkcolor=blue, + citecolor=darkgray, + urlcolor=blue, + bookmarks=true, + bookmarksnumbered=true, + unicode=true +]{hyperref} + +% Page dimensions +\geometry{a4paper, top=2.5cm, bottom=2.5cm, left=3cm, right=2.5cm} +% CV variant: \geometry{a4paper, margin=1.5cm} + +% Superscript citation numbers (scholarly convention) +\usepackage[numbers,super,sort&compress]{natbib} +\bibliographystyle{unsrtnat} + +% Commonly needed extras +\usepackage{tcolorbox} +\usepackage{colortbl} +\usepackage{booktabs} +\usepackage{enumitem} +``` + +**hyperref positioning**: it must load after virtually every other package to avoid option clashes. + +### Rule 4 — TeX Source Hygiene + +**Prohibited patterns**: +- Emoji glyphs (no native LaTeX engine supports them) +- Markdown `*asterisk*` formatting (generates compile-time errors) + +**Use instead**: +```latex +\textbf{bold text} +\emph{emphasised text} +``` + +These are frequent model-generation slips — catch them proactively. + +### Rule 5 — Multi-Language & Font Handling + +- `babel` and `polyglossia` are incompatible — load only one +- When using `polyglossia`, ensure `amsmath` appears earlier in the preamble +- Tectonic downloads standard LaTeX typefaces automatically (Latin Modern, etc.) +- To reference system-installed fonts via `\setmainfont{}`, first probe with `fc-list :lang=ar` +- Broadly available choices: DejaVu, Noto typeface families + +### Rule 6 — Clickable Navigation Is Non-Negotiable + +Interactive navigation is a baseline professional expectation. + +#### 6.1 Table of Contents +```latex +\tableofcontents % Entries are auto-linked courtesy of hyperref +\listoffigures +\listoftables +``` + +#### 6.2 Internal Cross-References + +**Attach labels** right after each numbered element: +```latex +\section{Background}\label{sec:bg} + +\begin{figure}[htbp] + \includegraphics{...} + \caption{Overview diagram}\label{fig:overview} +\end{figure} + +\begin{table}[htbp] + \caption{Benchmark results}\label{tab:bench} + ... +\end{table} + +\begin{equation}\label{eq:energy} + E = mc^2 +\end{equation} +``` + +**Cite these labels** (each produces a live hyperlink): +```latex +As noted in Section~\ref{sec:bg}... +Figure~\ref{fig:overview} illustrates... +Table~\ref{tab:bench} summarises... +Equation~\eqref{eq:energy} yields... % \eqref auto-wraps in parentheses +See page~\pageref{sec:bg}... +``` + +**Good habits**: +- Place a non-breaking space `~` before `\ref` to avoid orphaned line breaks +- Prefer `\eqref{}` for equations (auto-wraps the number in parentheses) +- Adopt a consistent label-prefix convention: `sec:`, `fig:`, `tab:`, `eq:`, `lst:` + +#### 6.3 Bibliography + +**Superscript numerals (preferred academic style)**: +```latex +\usepackage[numbers,super,sort&compress]{natbib} +\bibliographystyle{unsrtnat} + +Prior work\cite{smith2023} shows... % → shows^[1] +Several studies\cite{a,b,c} agree... % → agree^[1–3] + +\bibliography{refs} +``` + +**Numeric bracket alternative**: +```latex +\usepackage[numbers]{natbib} +\bibliographystyle{plainnat} + +\cite{smith2023} % [1] +\citep{smith2023} % (Smith, 2023) +\citet{smith2023} % Smith (2023) +``` + +**biblatex pathway**: +```latex +\usepackage[backend=biber,style=numeric-comp]{biblatex} +\addbibresource{refs.bib} + +Per~\cite{smith2023}... +\printbibliography +``` + +#### 6.4 External Links +```latex +\url{https://example.com} +\href{https://example.com}{Visible label} +\href{mailto:a@b.com}{a@b.com} +``` + +#### 6.5 PDF Metadata & Outline +```latex +\hypersetup{ + pdftitle={Document Title}, + pdfauthor={Author Name}, + pdfsubject={Topic}, + pdfkeywords={keyword1, keyword2} +} +``` +Bookmark trees are auto-generated from `\section` / `\chapter` hierarchy. + +#### 6.6 Why Multiple Passes Matter + +Label resolution requires at least two compilation passes: +```bash +# Resolve section / figure / table labels +python3 scripts/pdf.py convert.latex main.tex --runs 2 + +# Also resolves bibliography back-references +python3 scripts/pdf.py convert.latex main.tex --runs 3 +``` + +If `??` placeholders persist after two passes, verify that every `\label` string has an exact `\ref` match. + +#### 6.7 Navigation Troubleshooting + +| Observation | Root cause | Resolution | +|-------------|-----------|------------| +| `??` in place of numbers | Only a single pass was run | Recompile with `--runs 2` | +| All links render in black | hyperref colour options omitted | Enable `colorlinks=true` | +| TOC items are not clickable | hyperref package missing | Load the package | +| `[?]` beside citations | `.bib` path incorrect or biber step skipped | Confirm path; rebuild | +| Bookmark pane empty | `bookmarks` option set to false | Switch to `bookmarks=true` | + +--- + +## Package Catalogue + +### Foundational +- `hyperref` (Rule 3) +- `geometry` (Rule 3) +- `listings` — `\lstset{basicstyle=\ttfamily\small, numbers=left, backgroundcolor=\color{gray!5}}` +- `enumitem` — `\setlist[itemize]{itemsep=0.3em, leftmargin=1.5em}` + +### Tabular +`booktabs` · `longtable` · `multirow` · `array` · `colortbl` + +### Visual & Charting +`tikz` · `pgfplots` · `float` · `wrapfig` · `subfig` / `subcaption` + +### International & Typography +`fontspec` (XeLaTeX / LuaLaTeX) · `ctex` + +### Mathematical +`amsmath` · `amssymb` · `amsthm` · `natbib` · `biblatex` · `siunitx` + +### Algorithmic & Domain-Specific +`algorithm` + `algpseudocode` · `chemfig` + +### Page Design +`tcolorbox` · `fancyhdr` · `titlesec` · `tocloft` · `multicol` · `setspace` · `microtype` · `parskip` · `adjustbox` · `marginnote` + +### Code Listings +`listings` · `minted` (depends on Pygments) + +--- + +## Scripts & Backends + +| Script | Purpose | +|--------|---------| +| `pdf.py convert.latex` | Tectonic wrapper — log sanitisation, error highlighting, PDF metrics | + +| Engine | Notes | +|--------|-------| +| Tectonic | Stand-alone LaTeX engine; packages are fetched transparently on demand | + +--- + +## Operational Notes + +### CJK Without Manual Font Setup +Tectonic resolves CJK font bundles on the fly — zero manual installation: + +```latex +\usepackage{ctex} % Tectonic pulls the font files automatically +``` + +### Cold-Start Latency +The very first compilation of a new document triggers package downloads: +- Initial build: 1–5 min (depends on network speed) +- Repeat builds with warm cache: 10–30 s + +### Working Without Internet +Previously fetched packages are stored under `~/.cache/Tectonic/`. When offline, only cached packages are available; attempting to use a new one will fail. + +### Tectonic vs a Full TeX Live Installation + +| Dimension | Tectonic | Traditional pdflatex | +|-----------|----------|---------------------| +| Package acquisition | On-demand, transparent | Manual via `tlmgr` | +| Multi-pass compilation | Handled by the engine | Explicit re-invocations required | +| Reference resolution | Automatic | Requires bibtex/biber cycles | +| Disk footprint | Single binary | Full TeX Live ≈ 4 GB | diff --git a/skills/ppt/references/paper-navbar.tex b/skills/ppt/references/paper-navbar.tex new file mode 100755 index 0000000..426fa45 --- /dev/null +++ b/skills/ppt/references/paper-navbar.tex @@ -0,0 +1,73 @@ +% ======================================================================== +% Paper Presentation Navigation for Beamer +% ======================================================================== +% Usage: \input{paper-navbar.tex} in the preamble (after \usetheme and color definitions) +% +% Provides: +% - Top navigation bar: section names + miniframe dots (no subsection row) +% - Bottom footer: Author | Paper Title | Journal, Year | Page number +% - All colors follow myblue (adapts to any Preset Color Palette) +% +% Prerequisites (must be defined before \input): +% - \definecolor{myblue}{HTML}{...} (from chosen Preset Color Palette) +% - \useoutertheme{miniframes} (required for top navigation) +% +% Paper metadata setup (in preamble, after \input): +% \title[Short Title]{Full Title} +% \author[Author (Affiliation)]{Full Author List} +% \date[Journal Name, Year]{} % leave empty if no journal: \date[]{} +% ======================================================================== + +% ── Unify header and footer colors (follow myblue) ───────────────────── +% Top navbar: light tint of theme color +\setbeamercolor{section in head/foot}{bg=myblue!15, fg=myblue!80!black} +\setbeamercolor{subsection in head/foot}{bg=myblue!15, fg=myblue!80!black} +% Bottom footer: author/journal/page use light tint, title uses mid tint +\setbeamercolor{author in head/foot}{bg=myblue!15, fg=myblue!80!black} +\setbeamercolor{title in head/foot}{bg=myblue!30, fg=white} +\setbeamercolor{date in head/foot}{bg=myblue!15, fg=myblue!80!black} + +% ── Remove navigation symbols ───────────────────────────────────────── +\setbeamertemplate{navigation symbols}{} + +% ── Compress miniframes + remove subsection empty row ───────────────── +\makeatletter +\let\beamer@writeslidentry@miniframeson=\beamer@writeslidentry +\def\beamer@writeslidentry@miniframesoff{% + \expandafter\beamer@ifempty\expandafter{\beamer@framestartpage}{}% + {\addtocontents{nav}{\protect\headcommand{% + \protect\beamer@partpages{\the\c@page}{\the\c@page}}}}} +\newcommand*{\miniframeson}{\let\beamer@writeslidentry=\beamer@writeslidentry@miniframeson} +\newcommand*{\miniframesoff}{\let\beamer@writeslidentry=\beamer@writeslidentry@miniframesoff} +\beamer@compresstrue +\makeatother + +% ── Top navigation: section names + miniframe dots, no subsection row ── +\setbeamertemplate{headline}{% + \begin{beamercolorbox}[colsep=1.5pt]{upper separation line head} + \end{beamercolorbox} + \begin{beamercolorbox}{section in head/foot} + \vskip2pt\insertnavigation{\paperwidth}\vskip2pt + \end{beamercolorbox}% +} + +% ── Bottom footer: Author | Paper Title | Journal | Page ────────────── +\makeatletter +\setbeamertemplate{footline}{% + \leavevmode% + \hbox{% + \begin{beamercolorbox}[wd=.25\paperwidth,ht=2.5ex,dp=1ex,center]{author in head/foot}% + \usebeamerfont{author in head/foot}\insertshortauthor + \end{beamercolorbox}% + \begin{beamercolorbox}[wd=.42\paperwidth,ht=2.5ex,dp=1ex,center]{title in head/foot}% + \usebeamerfont{title in head/foot}\insertshorttitle + \end{beamercolorbox}% + \begin{beamercolorbox}[wd=.22\paperwidth,ht=2.5ex,dp=1ex,center]{date in head/foot}% + \usebeamerfont{date in head/foot}\insertshortdate + \end{beamercolorbox}% + \begin{beamercolorbox}[wd=.11\paperwidth,ht=2.5ex,dp=1ex,right]{date in head/foot}% + \usebeamerfont{date in head/foot}\insertframenumber{}/\inserttotalframenumber\kern2em + \end{beamercolorbox}% + }% +} +\makeatother diff --git a/skills/ppt/references/progress-navbar.tex b/skills/ppt/references/progress-navbar.tex new file mode 100755 index 0000000..a5b83d6 --- /dev/null +++ b/skills/ppt/references/progress-navbar.tex @@ -0,0 +1,151 @@ +% ======================================================================== +% Progress Navigation Bar for Beamer +% ======================================================================== +% Usage: \input{progress-navbar.tex} in the preamble (after \usetheme) +% +% Features: +% - Dynamic equal-width boxes: each box = (paperwidth - 50pt) / total frames +% - Three-level symbols: ≡ (home), 1/2/3 (section), ◆ (subsection), - (slide) +% - Teal progress coloring: visited = teal!60 fill, unvisited = hollow +% - Clickable hyperlinks on every box +% - Automatic section/subsection title pages +% - Requires --runs 2 for correct width calculation +% +% Customization: +% - Change "teal!60" to any color (e.g. myblue!60) for different themes +% - Change "teal" in \color{teal}\hrule for the top separator line +% ======================================================================== + +\makeatletter + +% ── Auto section/subsection title pages ───────────────────────────────── +\AtBeginSection[]{\frame{\sectionpage}} +\AtBeginSubsection[]{\frame{\subsectionpage}} + +% ── Lengths and counters ──────────────────────────────────────────────── +\newlength{\my@unitwidth} +\newlength{\my@tempsize} +\newcounter{my@sectnum} + +% ── Helper: extract last digit of section number ──────────────────────── +\newcommand{\my@lastdigit}[1]{% + \loop\ifnum\value{#1}>9\addtocounter{#1}{-10}\repeat + {\normalfont\arabic{#1}}% +} + +% ── Box primitives ────────────────────────────────────────────────────── +\newcommand\my@fixedbox[2]{% + \makebox[#1]{\rule[-1ex]{0pt}{3.25ex}#2}% +} + +\newcommand\my@colorbox[3]{% + {\setlength{\fboxsep}{0pt}\colorbox{#1}{\my@fixedbox{#2}{#3}}}% +} + +% ── Level marker: b=section, m=subsection, s=slide ───────────────────── +\def\my@temptext{} +\def\my@level{s} + +\newcommand{\my@navbox}[1][]{% + \if\relax\detokenize{#1}\relax + \def\my@tempbox{\my@fixedbox}% + \else + \def\my@tempbox{\my@colorbox{#1}}% + \fi + \if b\my@level + \def\my@temptext{\my@lastdigit{my@sectnum}}% + \fi + \if m\my@level + \def\my@temptext{$\diamond$}% + \fi + \if s\my@level + \def\my@temptext{$-$}% + \fi + \my@tempbox{\my@unitwidth}{\my@temptext}% +} + +% ── Navigation box templates (home / done / todo) ────────────────────── +\defbeamertemplate{navigation box}{home}{% + \my@colorbox{teal!60}{\my@unitwidth}{$\equiv$}% +} + +\defbeamertemplate{navigation box}{done}{% + \my@navbox[teal!60]% +} + +\defbeamertemplate{navigation box}{todo}{% + \my@navbox +} + +% ── Box dispatch commands ────────────────────────────────────────────── +\newcommand{\my@bigbox}{\gdef\my@level{b}\usebeamertemplate{navigation box}} +\newcommand{\my@medbox}{\gdef\my@level{m}\usebeamertemplate{navigation box}} +\newcommand{\my@smallbox}{\gdef\my@level{s}\usebeamertemplate{navigation box}} + +% ── Hook into Beamer's navigation infrastructure ────────────────────── +\renewcommand{\sectionentry}[5]{\gdef\my@currentbox{big}\setcounter{my@sectnum}{#1}} +\renewcommand{\beamer@subsectionentry}[5]{\gdef\my@currentbox{med}} + +\renewcommand{\slideentry}[6]{% + \def\my@temp@i{1/1}% + \def\my@temp@ii{#4}% + % Default: assume done + \ifx\my@temp@i\my@temp@ii + \setbeamertemplate{navigation box}[home]% + \else + \setbeamertemplate{navigation box}[done]% + \fi + % Override to todo if this slide is ahead of current position + \ifnum\c@section<#1% + \setbeamertemplate{navigation box}[todo]% + \else + \ifnum\c@section=#1\ifnum\c@subsection<#2% + \setbeamertemplate{navigation box}[todo]% + \else + \ifnum\c@subsection=#2\ifnum\c@subsectionslide<#3% + \setbeamertemplate{navigation box}[todo]% + \fi\fi + \fi\fi + \fi + % Dispatch to correct level box + \def\my@test@big{big}% + \def\my@test@med{med}% + \ifx\my@temp@i\my@temp@ii + \beamer@link(#4){\my@bigbox}% + \else + \ifx\my@currentbox\my@test@big + \beamer@link(#4){\my@bigbox}% + \else\ifx\my@currentbox\my@test@med + \beamer@link(#4){\my@medbox}% + \else + \beamer@link(#4){\my@smallbox}% + \fi\fi + \fi + \gdef\my@currentbox{small}% +} + +% ── Footline template ───────────────────────────────────────────────── +\defbeamertemplate{footline}{progress} +{% + % Dynamic width: divide available space equally among all frames + \ifnum\inserttotalframenumber>0 + \setlength{\my@unitwidth}{\dimexpr(\paperwidth - 50pt)/\inserttotalframenumber\relax}% + \else + \setlength{\my@unitwidth}{20pt}% + \fi + {\color{teal}\hrule}% + \hbox to \paperwidth{% + \hbox to \dimexpr\paperwidth - 50pt\relax{% + \kern2pt{\normalfont\dohead}\hfill + }% + \hbox to 50pt{% + \hfill{\normalfont\insertframenumber{}/\inserttotalframenumber}\kern4pt + }% + }% +} + +% ── Activate ────────────────────────────────────────────────────────── +\setbeamertemplate{navigation symbols}{} +\setbeamertemplate{footline}[progress] + +\makeatother diff --git a/skills/ppt/scripts/__pycache__/inventory.cpython-313.pyc b/skills/ppt/scripts/__pycache__/inventory.cpython-313.pyc new file mode 100755 index 0000000..d49433a Binary files /dev/null and b/skills/ppt/scripts/__pycache__/inventory.cpython-313.pyc differ diff --git a/skills/ppt/scripts/html2pptx.js b/skills/ppt/scripts/html2pptx.js new file mode 100755 index 0000000..2f5a7cf --- /dev/null +++ b/skills/ppt/scripts/html2pptx.js @@ -0,0 +1,1337 @@ +/** + * html2pptx v3 - Convert HTML slide to pptxgenjs slide with positioned elements + * + * v3 Changes (2026-03-31): + * - Smart font mapping: PPT-safe fonts pass through, macOS/web fonts auto-mapped + * - Element boundary checking: warns when elements exceed slide bounds + * - z-index sorting: elements rendered in correct visual layer order + * - Adaptive width compensation: short text, numbers, headings get scaled compensation + * - Height compensation: all text elements get 12% height buffer + * - white-space: nowrap support: prevents line-breaking in short labels + * - Minimum font size validation: blocks text < 11pt + * - Per-slide character count warning + * - Vertical balance detection: warns when content clusters in top portion + * - Text overlap detection: warns when text elements overlap each other + * + * USAGE: + * const pptx = new pptxgen(); + * pptx.layout = 'LAYOUT_16x9'; + * const { slide, placeholders, warnings } = await html2pptx('slide.html', pptx, { fontConfig }); + * // If warnings is non-empty → fix HTML and re-run + * await pptx.writeFile('output.pptx'); + * + * 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 fs = require('fs'); + +const PT_PER_PX = 0.75; +const PX_PER_IN = 96; +const EMU_PER_IN = 914400; + +// ── v3: Compensation factors ── +const COMPENSATION = { + HEADING_WIDTH: 0.25, SINGLE_LINE_NARROW: 0.18, SINGLE_LINE_NORMAL: 0.10, + MULTI_LINE: 0.05, SHORT_TEXT_EXTRA: 0.12, NUMERIC_TEXT_EXTRA: 0.08, + NOWRAP_EXTRA: 0.15, MAX_WIDTH_FACTOR: 0.40, + TEXT_HEIGHT: 0.08, LIST_HEIGHT: 0.06, + MIN_FONT_SIZE_PT: 11, MAX_CHARS_CJK: 350, MAX_CHARS_LATIN: 550, + VERTICAL_BALANCE_THRESHOLD: 0.55, OVERLAP_TOLERANCE_IN: 0.05, BOUNDS_TOLERANCE_IN: 0.02, + AUTO_SHORT_TEXT_THRESHOLD: 15, // chars — auto-detect short text even without explicit nowrap +}; + +// ── v3: Validation helpers (Node.js scope) ── +function extractTextContent(el) { + if (typeof el.text === 'string') return el.text; + if (Array.isArray(el.text)) return el.text.map(r => r.text || '').join(''); + if (Array.isArray(el.items)) return el.items.map(r => r.text || '').join(''); + return ''; +} +function getElementLabel(el) { + const t = extractTextContent(el); + return t.substring(0, 40) + (t.length > 40 ? '...' : '') || `[${el.type}]`; +} +// Compute PPT-adjusted position for text/list elements (mirrors addElements logic, pre-clamp) +function getAdjustedTextPosition(el, slideWidthIn) { + const MAX_TEXT_WIDTH_IN = 680 / 72; + const widthFactor = calculateWidthCompensation(el, slideWidthIn); + const widthIncrease = el.position.w * widthFactor; + let adjustedX = el.position.x; + let adjustedW = el.position.w; + const align = el.style?.align; + if (align === 'center') { adjustedX -= widthIncrease / 2; adjustedW += widthIncrease; } + else if (align === 'right') { adjustedX -= widthIncrease; adjustedW += widthIncrease; } + else { adjustedW += widthIncrease; } + if (align === 'center' && /^h[1-6]$/.test(el.type)) { + const centerX = adjustedX + adjustedW / 2; + const margin = 0.3; + const maxExpand = Math.min(centerX - margin, slideWidthIn - centerX - margin); + if (maxExpand > adjustedW / 2) { adjustedX = centerX - maxExpand; adjustedW = maxExpand * 2; } + } + const hComp = el.type === 'list' ? COMPENSATION.LIST_HEIGHT : COMPENSATION.TEXT_HEIGHT; + const finalW = Math.min(adjustedW, MAX_TEXT_WIDTH_IN); + return { x: adjustedX, y: el.position.y, w: finalW, h: el.position.h * (1 + hComp) }; +} +function checkElementBounds(slideData, sw, sh) { + const w = []; const tol = COMPENSATION.BOUNDS_TOLERANCE_IN; + const textTypes = new Set(['p','h1','h2','h3','h4','h5','h6','list']); + for (const el of slideData.elements) { + if (!el.position) continue; + const p = textTypes.has(el.type) ? getAdjustedTextPosition(el, sw) : el.position; + if (p.x < -tol) w.push(`⚠ BOUNDS: "${getElementLabel(el)}" extends ${(-p.x*72).toFixed(0)}pt beyond LEFT`); + if (p.y < -tol) w.push(`⚠ BOUNDS: "${getElementLabel(el)}" extends ${(-p.y*72).toFixed(0)}pt beyond TOP`); + if (p.x+p.w > sw+tol) w.push(`⚠ BOUNDS: "${getElementLabel(el)}" extends ${((p.x+p.w-sw)*72).toFixed(0)}pt beyond RIGHT`); + if (p.y+p.h > sh+tol) w.push(`⚠ BOUNDS: "${getElementLabel(el)}" extends ${((p.y+p.h-sh)*72).toFixed(0)}pt beyond BOTTOM`); + } + return w; +} +function checkVerticalBalance(slideData, sh) { + const ce = slideData.elements.filter(e => ['p','h1','h2','h3','h4','h5','h6','list'].includes(e.type)); + if (!ce.length) return []; + const mb = Math.max(...ce.map(e => e.position.y + e.position.h)); + return mb/sh < COMPENSATION.VERTICAL_BALANCE_THRESHOLD + ? [`⚠ LAYOUT: Content only in top ${(mb/sh*100).toFixed(0)}% — consider vertical centering`] : []; +} +function checkTextOverlaps(slideData) { + const w = []; const te = slideData.elements.filter(e => ['p','h1','h2','h3','h4','h5','h6','list'].includes(e.type)); + for (let i=0;i<te.length;i++) for (let j=i+1;j<te.length;j++) { + const a=te[i].position, b=te[j].position, tol=COMPENSATION.OVERLAP_TOLERANCE_IN; + const ow=Math.min(a.x+a.w,b.x+b.w)-Math.max(a.x,b.x); + const oh=Math.min(a.y+a.h,b.y+b.h)-Math.max(a.y,b.y); + if(ow>tol&&oh>tol) w.push(`⚠ OVERLAP: "${getElementLabel(te[i])}" overlaps "${getElementLabel(te[j])}"`); + } + return w; +} +function checkMinFontSize(slideData) { + const e = []; + for (const el of slideData.elements) { + if (!['p','h1','h2','h3','h4','h5','h6','list'].includes(el.type)) continue; + const fs = el.style?.fontSize || 0; + if (fs > 0 && fs < COMPENSATION.MIN_FONT_SIZE_PT) + e.push(`Text "${getElementLabel(el)}" is ${fs.toFixed(1)}pt — min is ${COMPENSATION.MIN_FONT_SIZE_PT}pt`); + } + return e; +} +function checkCharCount(slideData) { + let total=0, cjk=false; + for (const el of slideData.elements) { + const t=extractTextContent(el); total+=t.length; + if(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(t)) cjk=true; + } + const lim = cjk ? COMPENSATION.MAX_CHARS_CJK : COMPENSATION.MAX_CHARS_LATIN; + return total>lim ? [`⚠ DENSITY: ${total} chars (limit ${lim}) — split into multiple slides`] : []; +} +function calculateWidthCompensation(el, slideWidthIn) { + const isH = /^h[1-6]$/.test(el.type); + const txt = extractTextContent(el); + const lh = el.style?.lineSpacing || (el.style?.fontSize||16)*1.2; + const single = el.position.h <= lh*1.5/72; + let f = isH ? COMPENSATION.HEADING_WIDTH : single ? (el.position.w < slideWidthIn/3 ? COMPENSATION.SINGLE_LINE_NARROW : COMPENSATION.SINGLE_LINE_NORMAL) : COMPENSATION.MULTI_LINE; + if (txt.length>0 && txt.length<10) f += COMPENSATION.SHORT_TEXT_EXTRA; + if (/^[\d\s\.\,\-\/\+\%\$\#\@\!\?\:\;\(\)\[\]]+$/.test(txt)) f += COMPENSATION.NUMERIC_TEXT_EXTRA; + if (el.noWrap) f += COMPENSATION.NOWRAP_EXTRA; + // v4: Auto-detect short text that should not wrap even without explicit nowrap + if (!el.noWrap && single && txt.length >= 10 && txt.length < COMPENSATION.AUTO_SHORT_TEXT_THRESHOLD) { + f += COMPENSATION.SHORT_TEXT_EXTRA * 0.5; // Half bonus for auto-detected short text + } + // v4: Auto-detect card titles (>=18pt, single line, <20 chars) — treat like heading + const fs_ = el.style?.fontSize || 16; + if (!isH && single && fs_ >= 18 && txt.length < 20) { + f = Math.max(f, COMPENSATION.HEADING_WIDTH * 0.8); // At least 80% of heading compensation + } + return Math.min(f, COMPENSATION.MAX_WIDTH_FACTOR); +} + +// ── Emphasis font: apply to bold numeric text (post-extraction, Node.js scope) ── +// Matches text that is purely numeric/symbolic (KPI values, percentages, currency, etc.) +const NUMERIC_EMPHASIS_RE = /^[\d\s.,\-\/+%$#@!?:;()[\]]+$/; +function applyEmphasisFont(slideData, emphasisFont) { + for (const el of slideData.elements) { + if (!['p','h1','h2','h3','h4','h5','h6'].includes(el.type)) continue; + if (typeof el.text === 'string') { + // Plain text element — bold is on el.style + if (el.style?.bold && NUMERIC_EMPHASIS_RE.test(el.text.trim())) { + el.style.fontFace = emphasisFont; + } + } else if (Array.isArray(el.text)) { + // Runs — bold may be per-run (inline formatting) + for (const run of el.text) { + if (run.options?.bold && NUMERIC_EMPHASIS_RE.test(run.text.trim())) { + run.options.fontFace = emphasisFont; + } + } + } + } +} + +// Helper: Fix image path if file extension doesn't match actual format +function fixImageExtension(imagePath, tmpDir) { + try { + const fd = fs.openSync(imagePath, 'r'); + const buf = Buffer.alloc(12); + fs.readSync(fd, buf, 0, 12, 0); + fs.closeSync(fd); + + let actualExt = null; + if (buf[0] === 0xFF && buf[1] === 0xD8) actualExt = '.jpg'; + else if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) actualExt = '.png'; + else if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) actualExt = '.gif'; + else if (buf[0] === 0x52 && buf[1] === 0x49 && buf[8] === 0x57 && buf[9] === 0x45) actualExt = '.webp'; + + if (!actualExt) return imagePath; + + const currentExt = path.extname(imagePath).toLowerCase(); + if (currentExt === actualExt || (currentExt === '.jpeg' && actualExt === '.jpg') || (currentExt === '.jpg' && actualExt === '.jpeg')) { + return imagePath; + } + + // Extension mismatch: copy with correct extension + const fixedPath = path.join(tmpDir, path.basename(imagePath, currentExt) + actualExt); + fs.copyFileSync(imagePath, fixedPath); + return fixedPath; + } catch (e) { + return imagePath; + } +} + +// 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, pres, tmpDir) { + if (slideData.background.type === 'image' && slideData.background.path) { + let imagePath = slideData.background.path.startsWith('file://') + ? slideData.background.path.replace('file://', '') + : slideData.background.path; + // PptxGenJS slide.background = { path } is unreliable for local files; + // use addImage at (0,0) covering the full slide instead. + const slideW = pres.presLayout ? pres.presLayout.width / EMU_PER_IN : 10; + const slideH = pres.presLayout ? pres.presLayout.height / EMU_PER_IN : 5.625; + targetSlide.addImage({ + path: fixImageExtension(imagePath, tmpDir), + x: 0, y: 0, w: slideW, h: slideH + }); + } 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, tmpDir) { + const slideWidthIn = pres.presLayout ? pres.presLayout.width / EMU_PER_IN : 10; + const slideHeightIn = pres.presLayout ? pres.presLayout.height / EMU_PER_IN : 5.625; + const MAX_TEXT_WIDTH_IN = 680 / 72; // ~9.44in — cap after compensation to avoid overflow + + // v3: Sort by z-index for correct visual layering + const sortedElements = [...slideData.elements].sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0)); + + for (const el of sortedElements) { + if (el.type === 'image') { + let imagePath = el.src.startsWith('file://') ? el.src.replace('file://', '') : el.src; + targetSlide.addImage({ + path: fixImageExtension(imagePath, tmpDir), + 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') { + // v3: Height compensation for lists + let adjustedH = el.position.h * (1 + COMPENSATION.LIST_HEIGHT); + // Clamp list height to slide bottom + if (el.position.y + adjustedH > slideHeightIn) adjustedH = slideHeightIn - el.position.y; + if (adjustedH < 0.1) adjustedH = 0.1; + // Clamp list width to slide right edge + let listX = el.position.x; + let listW = el.position.w; + if (listX + listW > slideWidthIn) listW = slideWidthIn - listX; + if (listW < 0.1) listW = 0.1; + if (listW > MAX_TEXT_WIDTH_IN) listW = MAX_TEXT_WIDTH_IN; + const listOptions = { + x: listX, y: el.position.y, w: listW, h: adjustedH, + fontSize: el.style.fontSize, fontFace: el.style.fontFace, color: el.style.color, + align: el.style.align, valign: 'top', charSpacing: el.style.charSpacing, + 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 { + // ── Text elements (p, h1-h6) with v3 adaptive compensation ── + const widthFactor = calculateWidthCompensation(el, slideWidthIn); + const widthIncrease = el.position.w * widthFactor; + + let adjustedX = el.position.x; + let adjustedW = el.position.w; + const align = el.style.align; + + if (align === 'center') { + adjustedX -= widthIncrease / 2; + adjustedW += widthIncrease; + } else if (align === 'right') { + adjustedX -= widthIncrease; + adjustedW += widthIncrease; + } else { + adjustedW += widthIncrease; + } + + // v3: Height compensation for all text + let adjustedH = el.position.h * (1 + COMPENSATION.TEXT_HEIGHT); + // Clamp height to slide bottom + if (el.position.y + adjustedH > slideHeightIn) adjustedH = slideHeightIn - el.position.y; + if (adjustedH < 0.1) adjustedH = 0.1; + + // Centered headings: expand width symmetrically for PPT center alignment + if (el.style.align === 'center' && /^h[1-6]$/.test(el.type)) { + const centerX = adjustedX + adjustedW / 2; + const margin = 0.3; + const maxExpand = Math.min(centerX - margin, slideWidthIn - centerX - margin); + if (maxExpand > adjustedW / 2) { + adjustedX = centerX - maxExpand; + adjustedW = maxExpand * 2; + } + } + + // Clamp to slide bounds (safety net) + if (adjustedX < 0) { adjustedW += adjustedX; adjustedX = 0; } + if (adjustedX + adjustedW > slideWidthIn) adjustedW = slideWidthIn - adjustedX; + if (adjustedW < 0.1) adjustedW = 0.1; + // Cap at 680pt to prevent over-expansion from width compensation + if (adjustedW > MAX_TEXT_WIDTH_IN) adjustedW = MAX_TEXT_WIDTH_IN; + + const textOptions = { + x: adjustedX, y: el.position.y, w: adjustedW, h: adjustedH, + 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', charSpacing: el.style.charSpacing, lineSpacing: el.style.lineSpacing, + paraSpaceBefore: el.style.paraSpaceBefore, paraSpaceAfter: el.style.paraSpaceAfter, + inset: 0 + }; + 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; + if (el.noWrap) textOptions.wrap = false; + + 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); + }; + + // Known CJK font name fragments (lowercase) — presence means the element targets CJK text + const CJK_FONT_FRAGMENTS = [ + 'yahei', '雅黑', 'simhei', '黑体', 'simsun', '宋体', 'kaiti', '楷体', + 'fangsong', '仿宋', 'pingfang', 'hiragino', 'noto sans cjk', 'noto sans sc', + 'noto sans tc', 'noto sans hk', 'source han sans', '思源黑体', '思源宋体', + 'wenquanyi', 'arial unicode', 'yugothic', 'meiryo', 'ms gothic', 'ms mincho', + 'malgun gothic', 'apple sd gothic', 'heiti', 'songti', 'wawati', 'weibei', + 'libian', 'xingkai', 'baoli', 'yuanti', 'dengxian', '等线', 'stxihei', + 'stheiti', 'stkaiti', 'stsong', 'stfangsong' + ]; + + // Detect if text content contains CJK characters + const hasCJKChars = (text) => /[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/.test(text); + + // v3: Smart font mapping — PPT-safe fonts pass through, others get mapped + // fontConfig from caller is still respected as ultimate fallback for CJK/Latin defaults + const _fc = window.__FONT_CONFIG__ || {}; + + // macOS-only / web fonts → cross-platform PPT-safe equivalents + const FONT_FALLBACK_MAP = { + 'pingfang sc': 'Microsoft YaHei', 'pingfang tc': 'Microsoft YaHei', 'pingfang hk': 'Microsoft YaHei', + 'hiragino sans': 'Microsoft YaHei', 'hiragino sans gb': 'Microsoft YaHei', + 'hiragino mincho pron': 'SimSun', 'hiragino maru gothic pro': 'Microsoft YaHei', + 'heiti sc': 'SimHei', 'heiti tc': 'SimHei', 'songti sc': 'SimSun', + 'stxihei': 'Microsoft YaHei', 'stheiti': 'SimHei', 'stkaiti': 'KaiTi', + 'stsong': 'SimSun', 'stfangsong': 'FangSong', 'apple sd gothic neo': 'Microsoft YaHei', + 'noto sans sc': 'Microsoft YaHei', 'noto sans tc': 'Microsoft YaHei', + 'noto sans cjk sc': 'Microsoft YaHei', 'noto serif sc': 'SimSun', + 'source han sans sc': 'Microsoft YaHei', 'source han serif sc': 'SimSun', + 'source han sans': 'Microsoft YaHei', 'source han serif': 'SimSun', + '思源黑体': 'Microsoft YaHei', '思源宋体': 'SimSun', + '阿里巴巴普惠体': 'Microsoft YaHei', + 'system-ui': 'Century Gothic', '-apple-system': 'Century Gothic', 'blinkmacsystemfont': 'Century Gothic', + 'segoe ui': 'Century Gothic', 'helvetica neue': 'Arial', 'helvetica': 'Arial', + 'sans-serif': 'Century Gothic', 'serif': 'Times New Roman', 'monospace': 'Courier New', + 'inter': 'Century Gothic', 'roboto': 'Arial', 'roboto slab': 'Rockwell', + 'open sans': 'Century Gothic', 'lato': 'Century Gothic', 'montserrat': 'Century Gothic', + 'poppins': 'Candara', 'raleway': 'Century Gothic', 'nunito': 'Candara', + 'nunito sans': 'Candara', 'source sans pro': 'Corbel', 'source sans 3': 'Corbel', + 'source serif pro': 'Georgia', 'work sans': 'Century Gothic', 'dm sans': 'Century Gothic', + 'space grotesk': 'Century Gothic', 'plus jakarta sans': 'Candara', + 'manrope': 'Corbel', 'fira sans': 'Corbel', 'playfair display': 'Georgia', + 'merriweather': 'Georgia', 'libre baskerville': 'Georgia', + 'pt sans': 'Corbel', 'pt serif': 'Constantia', 'ubuntu': 'Century Gothic', + }; + + // PPT-safe fonts — pass through directly without mapping + const PPT_SAFE_FONTS = new Set([ + 'microsoft yahei', '微软雅黑', 'simhei', '黑体', 'simsun', '宋体', + 'kaiti', '楷体', 'simkai', 'fangsong', '仿宋', 'dengxian', '等线', + 'calibri', 'arial', 'arial black', 'arial narrow', + 'times new roman', 'georgia', 'gill sans mt', + 'century gothic', 'palatino linotype', 'palatino', + 'trebuchet ms', 'garamond', 'rockwell', 'candara', 'corbel', + 'constantia', 'cambria', 'book antiqua', 'courier new', + 'verdana', 'tahoma', 'impact', 'comic sans ms', + 'lucida sans', 'franklin gothic medium', 'bodoni mt', + 'copperplate gothic', 'tw cen mt', 'century schoolbook', + ]); + + const mapFontFace = (fontFamily, textContent = '') => { + if (!fontFamily) { + return hasCJKChars(textContent) ? (_fc.cjk || 'Microsoft YaHei') : (_fc.latin || 'Century Gothic'); + } + const fonts = fontFamily.split(',').map(f => f.trim().replace(/['"]/g, '')); + for (const font of fonts) { + const lower = font.toLowerCase(); + const isCJKFont = CJK_FONT_FRAGMENTS.some(f => lower.includes(f)); + if (PPT_SAFE_FONTS.has(lower)) { + // CJK font specified but text has no CJK characters → use Latin font to avoid + // mixing Century Gothic and YaHei for English-only content + if (isCJKFont && !hasCJKChars(textContent)) return _fc.latin || 'Century Gothic'; + return font; + } + if (FONT_FALLBACK_MAP[lower]) { + const mapped = FONT_FALLBACK_MAP[lower]; + const mappedIsCJK = CJK_FONT_FRAGMENTS.some(f => mapped.toLowerCase().includes(f)); + // If the mapped font is a non-CJK font but the text has CJK chars, keep looking + // (e.g. font-family:'Inter','Microsoft YaHei' with Chinese text should use YaHei, not Century Gothic) + if (!mappedIsCJK && hasCJKChars(textContent)) continue; + return mapped; + } + if (['sans-serif', 'serif', 'monospace', 'cursive', 'fantasy'].includes(lower)) continue; + if (isCJKFont || hasCJKChars(textContent)) return _fc.cjk || 'Microsoft YaHei'; + return font; // unknown non-CJK font: pass through + } + return hasCJKChars(textContent) ? (_fc.cjk || 'Microsoft YaHei') : (_fc.latin || 'Century Gothic'); + }; + + // 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); + if (computed.letterSpacing && computed.letterSpacing !== 'normal') options.charSpacing = pxToPoints(computed.letterSpacing); + + // 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); + } else { + // Unknown inline element (e.g. <a>, <code>, <sub>, <sup>, <mark>) — + // extract its text content as plain text to avoid silent data loss. + const text = textTransform(node.textContent.replace(/\s+/g, ' ')); + if (text.trim()) runs.push({ text, options: { ...baseOptions } }); + } + } + + 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']; + // Block container elements treated identically to DIV (background → shape, unwrapped text → error) + const blockContainerTags = new Set(['DIV', 'SECTION', 'HEADER', 'FOOTER', 'ARTICLE', 'ASIDE', 'MAIN', 'NAV', 'FIGURE']); + // Inline elements that LLMs sometimes use as standalone block text containers + const inlineAsBlockTags = new Set(['SPAN', 'STRONG', 'B', 'EM', 'I', 'A', 'LABEL', 'CODE', 'MARK', 'TIME', 'CITE', 'ABBR', 'S', 'U']); + // Block elements that are text-leaf-ish (rarely contain P/H* children) + const leafBlockTags = new Set(['FIGCAPTION', 'DT', 'DD', 'CAPTION', 'BLOCKQUOTE', 'TD', 'TH', 'SUMMARY']); + const processed = new Set(); + + // Switch every element to border-box BEFORE any getBoundingClientRect() call. + // This makes "width + padding" mean the total box size (not content-box size), + // so elements like `width:720pt; padding:0 48pt` stay within the 720pt body + // instead of overflowing to 816pt. Only affects elements that have an explicit + // width/height set; elements sized by content or flex-grow are unaffected. + document.querySelectorAll('*').forEach(el => { el.style.boxSizing = 'border-box'; }); + + 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); + el.querySelectorAll('*').forEach(child => processed.add(child)); + 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 block container elements (DIV, SECTION, HEADER, FOOTER, etc.) with backgrounds/borders as shapes + const isContainer = blockContainerTags.has(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 block container + for (const node of el.childNodes) { + if (node.nodeType === Node.TEXT_NODE) { + const text = node.textContent.trim(); + if (text) { + errors.push( + `<${el.tagName.toLowerCase()}> 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: mapFontFace(computed.fontFamily, el.textContent), + color: rgbToHex(computed.color), + transparency: extractAlpha(computed.color), + align: computed.textAlign === 'start' ? 'left' : computed.textAlign === 'end' ? 'right' : computed.textAlign, + lineSpacing: computed.lineHeight && computed.lineHeight !== 'normal' ? pxToPoints(computed.lineHeight) : null, + charSpacing: computed.letterSpacing && computed.letterSpacing !== 'normal' ? pxToPoints(computed.letterSpacing) : undefined, + 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; + } + + // Elements used as block-level text containers outside of any text tag: + // 1. Inline elements (span/strong/b/em/i/a/label/code/…) misused as block containers + // 2. Leaf-block elements (figcaption/dt/dd/td/th/blockquote/…) that directly hold text + // — only when they contain no block-text descendants to avoid duplicate extraction + if (inlineAsBlockTags.has(el.tagName) && !el.closest('p,h1,h2,h3,h4,h5,h6,li')) { + el._treatAsP = true; + } else if (leafBlockTags.has(el.tagName) && !el.closest('p,h1,h2,h3,h4,h5,h6,li')) { + if (!el.querySelector('p,h1,h2,h3,h4,h5,h6,ul,ol')) { + el._treatAsP = true; + } + } + + // Extract text elements (P, H1, H2, etc.) + if (!textTags.includes(el.tagName) && !el._treatAsP) 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); + + // v3: Detect white-space: nowrap and z-index + const noWrap = computed.whiteSpace === 'nowrap' || computed.whiteSpace === 'pre'; + const cssZIndex = parseInt(computed.zIndex) || 0; + + const baseStyle = { + fontSize: pxToPoints(computed.fontSize), + fontFace: mapFontFace(computed.fontFamily, text), + color: rgbToHex(computed.color), + align: computed.textAlign === 'start' ? 'left' : computed.textAlign === 'end' ? 'right' : computed.textAlign, + charSpacing: computed.letterSpacing && computed.letterSpacing !== 'normal' ? pxToPoints(computed.letterSpacing) : undefined, + lineSpacing: computed.lineHeight && computed.lineHeight !== 'normal' ? pxToPoints(computed.lineHeight) : null, + 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._treatAsP ? 'p' : el.tagName.toLowerCase(), + text: runs, + noWrap, + zIndex: cssZIndex, + 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._treatAsP ? 'p' : el.tagName.toLowerCase(), + text: transformedText, + noWrap, + zIndex: cssZIndex, + 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, + fontConfig = null // { cjk: 'SimHei', latin: 'Century Gothic', emphasis: 'Franklin Gothic Medium' } + } = 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}`); + + // Inject font config into the page for extractSlideData to use + if (fontConfig) { + await page.evaluate((fc) => { window.__FONT_CONFIG__ = fc; }, fontConfig); + } + + bodyDimensions = await getBodyDimensions(page); + + await page.setViewportSize({ + width: Math.round(bodyDimensions.width), + height: Math.round(bodyDimensions.height) + }); + + // Force a layout reflow after viewport resize so flex centering takes effect + await page.evaluate(() => void document.body.offsetHeight); + await page.waitForTimeout(100); + + // Re-read body dimensions after reflow to capture correct layout + bodyDimensions = await getBodyDimensions(page); + + slideData = await extractSlideData(page); + } finally { + await browser.close(); + } + + // Apply emphasis font to bold numeric text (e.g. KPI values, percentages) + if (fontConfig?.emphasis) applyEmphasisFont(slideData, fontConfig.emphasis); + + // Collect all validation errors + const overflowWarnings = []; + if (bodyDimensions.errors && bodyDimensions.errors.length > 0) { + overflowWarnings.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); + } + + // v3: Min font size check (blocking) + const fontErrors = checkMinFontSize(slideData); + if (fontErrors.length > 0) validationErrors.push(...fontErrors); + + // Throw blocking errors (structural issues that corrupt the output) + 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); + } + + // v3: Collect all non-blocking warnings + const slideWidthIn = pres.presLayout ? pres.presLayout.width / EMU_PER_IN : 10; + const slideHeightIn = pres.presLayout ? pres.presLayout.height / EMU_PER_IN : 5.625; + const allWarnings = [...overflowWarnings]; + allWarnings.push(...checkElementBounds(slideData, slideWidthIn, slideHeightIn)); + allWarnings.push(...checkVerticalBalance(slideData, slideHeightIn)); + allWarnings.push(...checkTextOverlaps(slideData)); + allWarnings.push(...checkCharCount(slideData)); + + const targetSlide = slide || pres.addSlide(); + + await addBackground(slideData, targetSlide, pres, tmpDir); + addElements(slideData, targetSlide, pres, tmpDir); + + // Print warnings after successful conversion (non-blocking) + if (allWarnings.length > 0) { + const suggestions = allWarnings.map((w, i) => ` ${i + 1}. ${w}`).join('\n'); + console.warn(`[html2pptx] ${htmlFile}: ${allWarnings.length} warning(s):\n${suggestions}`); + } + + return { slide: targetSlide, placeholders: slideData.placeholders, warnings: allWarnings }; + } 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/ppt/scripts/inventory.py b/skills/ppt/scripts/inventory.py new file mode 100755 index 0000000..95286c6 --- /dev/null +++ b/skills/ppt/scripts/inventory.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python3 +""" +Extract structured text content from PowerPoint presentations. + +Usage: + python inventory.py input.pptx output.json [--issues-only] +""" + +import argparse +import json +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional + +from pptx import Presentation +from pptx.enum.text import PP_ALIGN +from pptx.shapes.base import BaseShape + +# Public type alias used by replace.py: slide_id -> {shape_id -> ShapeData} +InventoryData = Dict[str, Dict[str, "ShapeData"]] + +_EMU = 914400 # EMUs per inch +_BULLET_NS = "{http://schemas.openxmlformats.org/drawingml/2006/main}" +_ALIGN_MAP = { + PP_ALIGN.CENTER: "CENTER", + PP_ALIGN.RIGHT: "RIGHT", + PP_ALIGN.JUSTIFY: "JUSTIFY", +} + + +def _is_cjk(ch: str) -> bool: + """True for full-width CJK characters (Chinese, Japanese, Korean, full-width forms).""" + cp = ord(ch) + return ( + 0x4E00 <= cp <= 0x9FFF # CJK Unified Ideographs + or 0x3400 <= cp <= 0x4DBF # CJK Extension A + or 0x3040 <= cp <= 0x30FF # Hiragana / Katakana + or 0xFF00 <= cp <= 0xFFEF # Full-width ASCII & half-width Katakana + or 0xAC00 <= cp <= 0xD7AF # Hangul syllables + ) + + +class ParagraphData: + """Text and formatting for one paragraph.""" + + def __init__(self, paragraph: Any): + 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 + + # Bullet detection + pPr = getattr(getattr(paragraph, "_p", None), "pPr", None) + if pPr is not None and ( + pPr.find(f"{_BULLET_NS}buChar") is not None + or pPr.find(f"{_BULLET_NS}buAutoNum") is not None + ): + self.bullet = True + self.level = getattr(paragraph, "level", None) + + # Alignment (omit LEFT — it's the default) + align = getattr(paragraph, "alignment", None) + if align in _ALIGN_MAP: + self.alignment = _ALIGN_MAP[align] + + # Spacing + sb = getattr(paragraph, "space_before", None) + if sb: + self.space_before = sb.pt + sa = getattr(paragraph, "space_after", None) + if sa: + self.space_after = sa.pt + + # Font from first run + if paragraph.runs: + font = paragraph.runs[0].font + self.font_name = font.name or None + self.font_size = font.size.pt if font.size else None + self.bold = font.bold + self.italic = font.italic + self.underline = font.underline + try: + self.color = str(font.color.rgb) if font.color.rgb else None + except (AttributeError, TypeError): + try: + tc = font.color.theme_color + self.theme_color = tc.name if tc else None + except (AttributeError, TypeError): + pass + + # Line spacing (after font so font_size is available) + ls = getattr(paragraph, "line_spacing", None) + if ls is not None: + if hasattr(ls, "pt"): + self.line_spacing = round(ls.pt, 2) + else: + # Multiplier — convert to points using current font size + self.line_spacing = round(ls * (self.font_size or 12.0), 2) + + def to_dict(self) -> Dict[str, Any]: + d: Dict[str, Any] = {"text": self.text} + if self.bullet: + d["bullet"] = True + if self.level is not None: + d["level"] = self.level + if self.alignment: + d["alignment"] = self.alignment + for key in ("space_before", "space_after", "font_size", "line_spacing"): + val = getattr(self, key) + if val is not None: + d[key] = val + if self.font_name: + d["font_name"] = self.font_name + for key in ("bold", "italic", "underline"): + val = getattr(self, key) + if val is not None: + d[key] = val + if self.color: + d["color"] = self.color + elif self.theme_color: + d["theme_color"] = self.theme_color + return d + + +class ShapeData: + """Position, formatting metadata, and text content for one shape.""" + + def __init__( + self, + shape: BaseShape, + absolute_left: Optional[int] = None, + absolute_top: Optional[int] = None, + slide: Optional[Any] = None, + ): + self.shape = shape + self.shape_id: str = "" # assigned after sorting + + # Slide dimensions (for overflow checking) + self.slide_width_emu: Optional[int] = None + self.slide_height_emu: Optional[int] = None + if slide: + try: + prs_xml = slide.part.package.presentation_part.presentation + self.slide_width_emu = prs_xml.slide_width + self.slide_height_emu = prs_xml.slide_height + except (AttributeError, TypeError): + pass + + # Placeholder metadata + self.placeholder_type: Optional[str] = None + self.default_font_size: Optional[float] = None + if getattr(shape, "is_placeholder", False): + pf = shape.placeholder_format # type: ignore + if pf and pf.type: + self.placeholder_type = str(pf.type).split(".")[-1].split(" ")[0] + if slide and hasattr(slide, "slide_layout"): + self.default_font_size = _layout_font_size(shape, slide.slide_layout) + + # Position in inches (use absolute coords for shapes inside groups) + left_emu = absolute_left if absolute_left is not None else getattr(shape, "left", 0) + top_emu = absolute_top if absolute_top is not None else getattr(shape, "top", 0) + self.left = round(left_emu / _EMU, 2) + self.top = round(top_emu / _EMU, 2) + self.width = round(getattr(shape, "width", 0) / _EMU, 2) + self.height = round(getattr(shape, "height", 0) / _EMU, 2) + + # EMU positions kept for overflow arithmetic + self.left_emu = left_emu + self.top_emu = top_emu + self.width_emu = getattr(shape, "width", 0) + self.height_emu = getattr(shape, "height", 0) + + # Issue detection + 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] = {} + self.warnings: List[str] = [] + self._estimate_frame_overflow() + self._calculate_slide_overflow() + self._detect_bullet_issues() + + # ------------------------------------------------------------------ + # Issue detection helpers + # ------------------------------------------------------------------ + + def _default_font_size_pts(self) -> float: + """Best-effort default font size from theme styles.""" + if self.default_font_size: + return self.default_font_size + try: + master = self.shape.part.slide_layout.slide_master # type: ignore + style = "titleStyle" if (self.placeholder_type and "TITLE" in self.placeholder_type) else "bodyStyle" + for child in master.element.iter(): + tag = child.tag.split("}")[-1] if "}" in child.tag else child.tag + if tag == style: + for elem in child.iter(): + if "sz" in elem.attrib: + return int(elem.attrib["sz"]) / 100.0 + except Exception: + pass + return 14.0 # conservative fallback + + def _estimate_frame_overflow(self) -> None: + """Estimate text overflow via character-count heuristic (no external deps).""" + if not hasattr(self.shape, "text_frame"): + return + tf = self.shape.text_frame # type: ignore + if not tf or not tf.paragraphs: + return + + # Usable area after text frame margins + def e2i(v: Any) -> float: + return (v or 0) / _EMU + + margin_h = e2i(tf.margin_top) + e2i(tf.margin_bottom) + margin_w = e2i(tf.margin_left) + e2i(tf.margin_right) + if margin_h == 0: + margin_h = 0.10 # PowerPoint default: ~0.05" top + 0.05" bottom + if margin_w == 0: + margin_w = 0.20 # PowerPoint default: ~0.1" left + 0.1" right + usable_w = self.width - margin_w + usable_h = self.height - margin_h + if usable_w <= 0 or usable_h <= 0: + return + + default_size = self._default_font_size_pts() + total_h = 0.0 + + for para in tf.paragraphs: + if not para.text.strip(): + continue + pd = ParagraphData(para) + size_pt = pd.font_size or default_size + + # Estimate text width: CJK chars ≈ 1.0× font_size pts, others ≈ 0.5× + text_w_pts = sum( + size_pt if _is_cjk(c) else size_pt * 0.5 + for c in para.text + ) + usable_w_pts = usable_w * 72.0 + n_lines = max(1, -(-int(text_w_pts) // max(1, int(usable_w_pts)))) # ceiling div + + line_h_in = (pd.line_spacing or size_pt) / 72.0 + total_h += (pd.space_before or 0) / 72.0 + total_h += n_lines * line_h_in + total_h += (pd.space_after or 0) / 72.0 + + if total_h > usable_h + 0.05: # ignore sub-0.05" rounding noise + self.frame_overflow_bottom = round(total_h - usable_h, 2) + + def _calculate_slide_overflow(self) -> None: + if self.slide_width_emu is None or self.slide_height_emu is None: + return + r = self.left_emu + self.width_emu - self.slide_width_emu + if r > 0: + v = round(r / _EMU, 2) + if v > 0.01: + self.slide_overflow_right = v + b = self.top_emu + self.height_emu - self.slide_height_emu + if b > 0: + v = round(b / _EMU, 2) + if v > 0.01: + self.slide_overflow_bottom = v + + def _detect_bullet_issues(self) -> None: + if not hasattr(self.shape, "text_frame"): + return + for para in self.shape.text_frame.paragraphs: # type: ignore + text = para.text.strip() + if text and any(text.startswith(s + " ") for s in ("•", "●", "○")): + self.warnings.append("manual_bullet_symbol: use proper bullet formatting") + break + + # ------------------------------------------------------------------ + # Public interface + # ------------------------------------------------------------------ + + @property + def paragraphs(self) -> List[ParagraphData]: + if not hasattr(self.shape, "text_frame"): + return [] + return [ParagraphData(p) for p in self.shape.text_frame.paragraphs if p.text.strip()] # type: ignore + + @property + def has_any_issues(self) -> bool: + return bool( + self.frame_overflow_bottom is not None + or self.slide_overflow_right is not None + or self.slide_overflow_bottom is not None + or self.overlapping_shapes + or self.warnings + ) + + def to_dict(self) -> Dict[str, Any]: + d: Dict[str, Any] = { + "left": self.left, "top": self.top, + "width": self.width, "height": self.height, + } + if self.placeholder_type: + d["placeholder_type"] = self.placeholder_type + if self.default_font_size: + d["default_font_size"] = self.default_font_size + + overflow: Dict[str, Any] = {} + if self.frame_overflow_bottom is not None: + overflow["frame"] = {"overflow_bottom": self.frame_overflow_bottom} + slide_ov: Dict[str, float] = {} + if self.slide_overflow_right is not None: + slide_ov["overflow_right"] = self.slide_overflow_right + if self.slide_overflow_bottom is not None: + slide_ov["overflow_bottom"] = self.slide_overflow_bottom + if slide_ov: + overflow["slide"] = slide_ov + if overflow: + d["overflow"] = overflow + if self.overlapping_shapes: + d["overlap"] = {"overlapping_shapes": self.overlapping_shapes} + if self.warnings: + d["warnings"] = self.warnings + d["paragraphs"] = [p.to_dict() for p in self.paragraphs] + return d + + +# ------------------------------------------------------------------ +# Module-level helpers +# ------------------------------------------------------------------ + +def _layout_font_size(shape: BaseShape, slide_layout: Any) -> Optional[float]: + """Extract default font size from the matching layout placeholder.""" + try: + shape_type = shape.placeholder_format.type # type: ignore + for ph in slide_layout.placeholders: + if ph.placeholder_format.type == shape_type: + for elem in ph.element.iter(): + if "defRPr" in elem.tag and (sz := elem.get("sz")): + return float(sz) / 100.0 + break + except Exception: + pass + return None + + +def _is_valid_shape(shape: BaseShape) -> bool: + """True if shape has meaningful text and is not a slide-number placeholder.""" + if not hasattr(shape, "text_frame"): + return False + tf = shape.text_frame # type: ignore + if not tf or not tf.text.strip(): + return False + if getattr(shape, "is_placeholder", False): + pf = shape.placeholder_format # type: ignore + if pf and pf.type: + pt = str(pf.type).split(".")[-1].split(" ")[0] + if pt == "SLIDE_NUMBER": + return False + if pt == "FOOTER" and tf.text.strip().isdigit(): + return False + return True + + +def _collect_shapes(shape: BaseShape, parent_left: int = 0, parent_top: int = 0): + """Yield (shape, abs_left, abs_top) tuples, recursing into GroupShapes.""" + if hasattr(shape, "shapes"): # GroupShape + g_left = parent_left + getattr(shape, "left", 0) + g_top = parent_top + getattr(shape, "top", 0) + for child in shape.shapes: # type: ignore + yield from _collect_shapes(child, g_left, g_top) + elif _is_valid_shape(shape): + yield ( + shape, + parent_left + getattr(shape, "left", 0), + parent_top + getattr(shape, "top", 0), + ) + + +def _sort_by_position(shapes: List[ShapeData]) -> List[ShapeData]: + """Sort shapes top-to-bottom, left-to-right (0.5" row tolerance).""" + if not shapes: + return shapes + shapes = sorted(shapes, key=lambda s: (s.top, s.left)) + result: List[ShapeData] = [] + row = [shapes[0]] + row_top = shapes[0].top + for s in shapes[1:]: + if abs(s.top - row_top) <= 0.5: + row.append(s) + else: + result.extend(sorted(row, key=lambda s: s.left)) + row = [s] + row_top = s.top + result.extend(sorted(row, key=lambda s: s.left)) + return result + + +def _detect_overlaps(shapes: List[ShapeData]) -> None: + """Populate overlapping_shapes for all pairs with meaningful overlap.""" + for i, s1 in enumerate(shapes): + for s2 in shapes[i + 1:]: + ow = min(s1.left + s1.width, s2.left + s2.width) - max(s1.left, s2.left) + oh = min(s1.top + s1.height, s2.top + s2.height) - max(s1.top, s2.top) + if ow > 0.05 and oh > 0.05: + area = round(ow * oh, 2) + s1.overlapping_shapes[s2.shape_id] = area + s2.overlapping_shapes[s1.shape_id] = area + + +# ------------------------------------------------------------------ +# Public API +# ------------------------------------------------------------------ + +def extract_text_inventory( + pptx_path: Path, + prs: Optional[Any] = None, + issues_only: bool = False, +) -> InventoryData: + """Extract text from all slides. + + Returns {slide-N: {shape-N: ShapeData}}, shapes sorted by visual position. + Pass an existing Presentation object via `prs` to avoid re-loading. + """ + if prs is None: + prs = Presentation(str(pptx_path)) + + inventory: InventoryData = {} + + for slide_idx, slide in enumerate(prs.slides): + raw = list(_collect_shapes_from_slide(slide)) + if not raw: + continue + + shape_data_list = [ShapeData(s, al, at, slide) for s, al, at in raw] + sorted_shapes = _sort_by_position(shape_data_list) + + for idx, sd in enumerate(sorted_shapes): + sd.shape_id = f"shape-{idx}" + + if len(sorted_shapes) > 1: + _detect_overlaps(sorted_shapes) + + if issues_only: + sorted_shapes = [sd for sd in sorted_shapes if sd.has_any_issues] + if not sorted_shapes: + continue + + inventory[f"slide-{slide_idx}"] = {sd.shape_id: sd for sd in sorted_shapes} + + return inventory + + +def _collect_shapes_from_slide(slide): + """Yield (shape, abs_left, abs_top) for all valid text shapes on a slide.""" + for shape in slide.shapes: # type: ignore + yield from _collect_shapes(shape) + + +def save_inventory(inventory: InventoryData, output_path: Path) -> None: + """Serialize inventory to a JSON file.""" + json_data = { + slide_key: {k: sd.to_dict() for k, sd in shapes.items()} + for slide_key, shapes in inventory.items() + } + with open(output_path, "w", encoding="utf-8") as f: + json.dump(json_data, f, indent=2, ensure_ascii=False) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Extract text inventory from a PowerPoint file.") + parser.add_argument("input", help="Input .pptx file") + parser.add_argument("output", help="Output .json file") + parser.add_argument("--issues-only", action="store_true", + help="Include only shapes with overflow/overlap issues") + args = parser.parse_args() + + input_path = Path(args.input) + if not input_path.exists(): + print(f"Error: File not found: {args.input}") + sys.exit(1) + if input_path.suffix.lower() != ".pptx": + print("Error: Input must be a .pptx file") + sys.exit(1) + + try: + 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) + + total = sum(len(v) for v in inventory.values()) + if args.issues_only: + print(f"Found {total} shapes with issues across {len(inventory)} slides → {args.output}") + else: + print(f"Found {total} text shapes across {len(inventory)} slides → {args.output}") + except Exception as e: + import traceback + print(f"Error: {e}") + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/ppt/scripts/pdf.py b/skills/ppt/scripts/pdf.py new file mode 100755 index 0000000..cb7c5c1 --- /dev/null +++ b/skills/ppt/scripts/pdf.py @@ -0,0 +1,2046 @@ +#!/usr/bin/env python3 +""" +PDF Processing Toolkit — All-in-One CLI + +Usage: + python3 pdf.py <command> [args...] + +Commands: + env.check [--json] Check environment dependencies + env.fix Auto-install missing dependencies + + extract.text <pdf> [-p pages] + extract.table <pdf> [-p pages] + extract.image <pdf> -o <dir> + + pages.merge <pdf>... -o <out> + pages.split <pdf> -o <dir> + pages.rotate <pdf> <deg> -o <out> [-p pages] + pages.crop <pdf> <box> -o <out> [-p pages] + + meta.get <pdf> + meta.set <pdf> -o <out> -d <json> + meta.brand <pdf>... [-o <out>] [-t title] [-q] + + form.info <pdf> + form.fill <pdf> -o <out> -d <json> + form.detail <pdf> <output.json> + form.fill-legacy <pdf> <fields.json> <output.pdf> + form.annotate <pdf> <fields.json> <output.pdf> + form.render <pdf> <output_dir> [--max-dim N] + form.validate <page> <fields.json> <input_img> <output_img> + form.check-bbox <fields.json> + + convert.office <file> [-o <out>] + convert.html <file> [-o <out>] [--css <file>] + convert.latex <file> [--runs N] [--keep-logs] + + code.sanitize <file> +""" + +from __future__ import annotations + +import html +import json +import os +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from datetime import datetime +from pathlib import Path +from typing import Any, Callable, Dict, List, Optional, Set, Tuple + + +# ═══════════════════════════════════════════════════════════════ +# Section 0: Framework — Output, @cmd registry, CLI parser +# ═══════════════════════════════════════════════════════════════ + +class Output: + """Structured JSON output for all subcommands.""" + + @staticmethod + def success(data: dict): + payload = {"status": "success", "data": data} + sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + raise SystemExit(0) + + @staticmethod + def error(error: str, message: str, hint: Optional[str] = None, code: int = 1): + payload = {"status": "error", "error": error, "message": message} + if hint is not None: + payload["hint"] = hint + sys.stderr.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n") + raise SystemExit(code) + + @staticmethod + def check_file(filepath: str) -> Path: + target = Path(filepath) + if not target.exists(): + Output.error("FileNotFound", f"File not found: {filepath}", code=2) + return target + + +# Command registry +_COMMANDS: Dict[str, Callable] = {} + + +def cmd(name: str): + """Decorator to register a CLI command under a dotted namespace.""" + def decorator(fn: Callable) -> Callable: + _COMMANDS[name] = fn + return fn + return decorator + + +def _pop_flag(argv: list, short: str, long: str, needs_value: bool = True): + """Extract a flag (and optional value) from *argv* in-place.""" + for idx, tok in enumerate(argv): + if tok in (short, long): + argv.pop(idx) + if needs_value: + if idx < len(argv): + return argv.pop(idx) + Output.error("MissingArg", f"Flag {long} requires a value") + return True + return None + + +def _load_json_arg(argv: list) -> dict: + """Read JSON from -d/--data string or -f/--file path.""" + raw = _pop_flag(argv, "-d", "--data") + if raw is not None: + try: + return json.loads(raw) + except json.JSONDecodeError as exc: + Output.error("InvalidJSON", f"JSON parse error: {exc}") + + fpath = _pop_flag(argv, "-f", "--file") + if fpath is not None: + try: + with open(fpath) as fh: + return json.load(fh) + except Exception as exc: + Output.error("FileError", f"Failed to read file: {exc}") + + Output.error("MissingData", "Requires --data or --file argument") + + +def _resolve_page_indices(range_spec: Optional[str], page_count: int) -> List[int]: + """Turn a human-friendly range string (1-indexed) into a sorted list of 0-based indices.""" + if not range_spec: + return list(range(page_count)) + indices: Set[int] = set() + for segment in range_spec.split(","): + segment = segment.strip() + if "-" in segment: + lo, hi = segment.split("-", 1) + for i in range(int(lo) - 1, min(int(hi), page_count)): + indices.add(i) + else: + val = int(segment) - 1 + if 0 <= val < page_count: + indices.add(val) + return sorted(indices) + + +_SCRIPT_DIR = Path(__file__).resolve().parent + + +# ═══════════════════════════════════════════════════════════════ +# Section 1: env — environment diagnostics and auto-fix +# ═══════════════════════════════════════════════════════════════ + +def _probe_cmd(name: str, version_args: Optional[List[str]] = None) -> Tuple[str, str]: + """Check if a command exists and optionally get its version. Returns (status, detail).""" + path = shutil.which(name) + if path is None: + return ("missing", "") + if version_args is None: + return ("ok", "") + try: + result = subprocess.run( + [path] + version_args, + capture_output=True, text=True, timeout=10 + ) + ver = result.stdout.strip() or result.stderr.strip() + return ("ok", ver) + except Exception: + return ("ok", "") + + +def _probe_python_module(mod_name: str) -> Tuple[str, str]: + """Check if a Python module is importable and get its version.""" + try: + result = subprocess.run( + [sys.executable, "-c", f"import {mod_name}; print(getattr({mod_name}, '__version__', 'installed'))"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0: + return ("ok", result.stdout.strip()) + return ("missing", "") + except Exception: + return ("missing", "") + + +def _probe_node() -> Tuple[str, str]: + s, d = _probe_cmd("node", ["--version"]) + if s == "ok" and d: + d = d.lstrip("v") + return (s, d) + + +def _probe_python() -> Tuple[str, str]: + try: + import platform + return ("ok", platform.python_version()) + except Exception: + return ("ok", "") + + +def _probe_libreoffice() -> Tuple[str, str]: + candidates = [ + "/Applications/LibreOffice.app/Contents/MacOS/soffice", + os.path.expanduser("~/Applications/LibreOffice.app/Contents/MacOS/soffice"), + "/usr/bin/soffice", + "/usr/local/bin/soffice", + "/usr/lib/libreoffice/program/soffice", + "/opt/libreoffice/program/soffice", + "/snap/bin/libreoffice.soffice", + ] + for c in candidates: + if Path(c).is_file(): + return ("ok", "") + for alias in ("soffice", "libreoffice"): + if shutil.which(alias): + return ("ok", "") + return ("missing", "") + + +def _probe_tectonic() -> Tuple[str, str]: + home_bin = Path.home() / "tectonic" + if home_bin.exists() and os.access(home_bin, os.X_OK): + return ("ok", "") + tec_local = _SCRIPT_DIR / "tectonic" + if tec_local.exists() and os.access(tec_local, os.X_OK): + return ("ok", "") + if shutil.which("tectonic"): + return ("ok", "") + return ("missing", "") + + +def _probe_playwright_npm() -> Tuple[str, str]: + """Check if playwright npm package is installed.""" + try: + result = subprocess.run( + ["node", "-e", "console.log(require('playwright/package.json').version)"], + capture_output=True, text=True, timeout=10 + ) + if result.returncode == 0 and result.stdout.strip(): + return ("ok", result.stdout.strip()) + except Exception: + pass + # Try global + try: + result = subprocess.run( + ["npm", "list", "-g", "playwright", "--depth=0"], + capture_output=True, text=True, timeout=15 + ) + import re as _re + m = _re.search(r"playwright@(\S+)", result.stdout) + if m: + return ("ok", m.group(1)) + except Exception: + pass + return ("missing", "") + + +def _probe_chromium() -> Tuple[str, str]: + """Check if Playwright Chromium browser is installed.""" + import platform as _platform + home = Path.home() + if _platform.system() == "Darwin": + cache_dir = home / "Library" / "Caches" / "ms-playwright" + else: + cache_dir = home / ".cache" / "ms-playwright" + if cache_dir.is_dir(): + for entry in sorted(cache_dir.iterdir(), reverse=True): + if "chromium" in entry.name.lower(): + return ("ok", entry.name) + return ("missing", "") + + +@cmd("env.check") +def env_check(argv: list): + """Check environment dependencies.""" + use_json = _pop_flag(argv, "-j", "--json", needs_value=False) + + s_node = _probe_node() + s_pw = _probe_playwright_npm() + s_cr = _probe_chromium() + s_py = _probe_python() + s_pike = _probe_python_module("pikepdf") + s_plumb = _probe_python_module("pdfplumber") + s_lo = _probe_libreoffice() + s_tec = _probe_tectonic() + s_pw_py = _probe_python_module("playwright") + + if use_json: + report = { + "html_route": { + "node": s_node[0], "node_version": s_node[1], + "playwright": s_pw[0], "playwright_version": s_pw[1], + "chromium": s_cr[0], "chromium_detail": s_cr[1], + }, + "process_route": { + "python3": s_py[0], "python3_version": s_py[1], + "pikepdf": s_pike[0], "pikepdf_version": s_pike[1], + "pdfplumber": s_plumb[0], "pdfplumber_version": s_plumb[1], + "playwright_python": s_pw_py[0], "playwright_python_version": s_pw_py[1], + }, + "optional": { + "libreoffice": s_lo[0], + "tectonic": s_tec[0], + }, + } + sys.stdout.write(json.dumps(report, ensure_ascii=False, indent=2) + "\n") + # Determine exit code + rc = 0 + for v in [s_node, s_pw, s_cr, s_py, s_pike, s_plumb]: + if v[0] != "ok": + rc = 2 + break + raise SystemExit(rc) + + # Human-readable output + rc = 0 + + def show(name: str, status: Tuple[str, str], optional: bool = False): + nonlocal rc + s, d = status + if s == "ok": + detail = f" ({d})" if d else "" + print(f" \u2713 {name}{detail}") + elif optional: + print(f" \u25cb {name} (optional, not installed)") + else: + print(f" \u2717 {name} (missing)") + rc = 2 + + print("=== PDF Skill Environment ===\n") + print("--- HTML Route ---") + show("node", s_node) + show("playwright", s_pw) + show("chromium", s_cr) + + print("\n--- Process Route ---") + show("python3", s_py) + show("pikepdf", s_pike) + show("pdfplumber", s_plumb) + if s_pw_py[0] == "ok": + print(f" (playwright-python: {s_pw_py[1]})") + + print("\n--- Optional ---") + show("libreoffice", s_lo, optional=True) + show("tectonic", s_tec, optional=True) + + print("\n=== Install Commands ===") + print(" Node.js: brew install node (macOS) / apt install nodejs (Ubuntu)") + print(" Playwright: npm install -g playwright && npx playwright install chromium") + print(" Python: brew install python3 (macOS) / apt install python3 (Ubuntu)") + print(" pikepdf: pip install pikepdf pdfplumber --user") + print(" LibreOffice: brew install --cask libreoffice (macOS)") + print(" Tectonic: curl -fsSL https://drop-sh.fullyjustified.net | sh") + raise SystemExit(rc) + + +@cmd("env.fix") +def env_fix(argv: list): + """Auto-install missing Python dependencies.""" + modules = { + "pikepdf": "pikepdf", + "pdfplumber": "pdfplumber", + "pypdf": "pypdf", + "pdf2image": "pdf2image", + "PIL": "Pillow", + } + installed = [] + for mod, pkg in modules.items(): + s, _ = _probe_python_module(mod) + if s == "missing": + print(f"Installing {pkg}...") + for attempt in ( + [sys.executable, "-m", "pip", "install", "-q", pkg], + [sys.executable, "-m", "pip", "install", "-q", "--user", pkg], + [sys.executable, "-m", "pip", "install", "-q", "--break-system-packages", pkg], + ): + result = subprocess.run(attempt, capture_output=True, text=True) + if result.returncode == 0: + installed.append(pkg) + break + else: + print(f" Failed to install {pkg}") + + if installed: + print(f"\nInstalled: {', '.join(installed)}") + else: + print("All Python dependencies are already installed.") + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 2: extract — text, tables, and embedded images +# ═══════════════════════════════════════════════════════════════ + +@cmd("extract.text") +def extract_text(argv: list): + """Pull plain text from selected pages.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + page_range = _pop_flag(argv, "-p", "--pages") + + import pdfplumber + src = Output.check_file(pdf_path) + try: + doc = pdfplumber.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + total_pages = len(doc.pages) + target_pages = _resolve_page_indices(page_range, total_pages) + char_total = 0 + page_results = [] + + for pg_idx in target_pages: + content = doc.pages[pg_idx].extract_text() or "" + char_total += len(content) + page_results.append({"page": pg_idx + 1, "chars": len(content), "text": content}) + + doc.close() + Output.success({ + "total_pages": total_pages, + "extracted_pages": len(target_pages), + "total_chars": char_total, + "pages": page_results, + }) + + +@cmd("extract.table") +def extract_table(argv: list): + """Locate and return every table on selected pages.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + page_range = _pop_flag(argv, "-p", "--pages") + + import pdfplumber + src = Output.check_file(pdf_path) + try: + doc = pdfplumber.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + total_pages = len(doc.pages) + target_pages = _resolve_page_indices(page_range, total_pages) + collected = [] + + for pg_idx in target_pages: + for tbl_num, raw_table in enumerate(doc.pages[pg_idx].extract_tables()): + if not raw_table: + continue + sanitised = [ + [(cell.strip() if cell else "") for cell in row] + for row in raw_table + ] + collected.append({ + "page": pg_idx + 1, + "table_index": tbl_num, + "rows": len(sanitised), + "cols": len(sanitised[0]) if sanitised else 0, + "data": sanitised, + }) + + doc.close() + Output.success({ + "total_pages": total_pages, + "extracted_pages": len(target_pages), + "total_tables": len(collected), + "tables": collected, + }) + + +@cmd("extract.image") +def extract_image(argv: list): + """Save every embedded raster image to output dir.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_dir = _pop_flag(argv, "-o", "--output") or "." + + import pikepdf + src = Output.check_file(pdf_path) + dest = Path(out_dir) + dest.mkdir(parents=True, exist_ok=True) + + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + saved = [] + seq = 0 + _EXT_MAP = { + "/DCTDecode": "jpg", + "/FlateDecode": "png", + "/JPXDecode": "jp2", + } + + for page_no, pg in enumerate(doc.pages, 1): + res = pg.get("/Resources") + if res is None or "/XObject" not in res: + continue + for key, ref in res.XObject.items(): + try: + img_obj = doc.get_object(ref.objgen) + if img_obj.get("/Subtype") != "/Image": + continue + seq += 1 + w = int(img_obj.get("/Width", 0)) + h = int(img_obj.get("/Height", 0)) + filt = img_obj.get("/Filter") + ext = _EXT_MAP.get(str(filt) if filt else None, "bin") + fname = f"page{page_no}_img{seq}.{ext}" + out_file = dest / fname + out_file.write_bytes(img_obj.read_raw_bytes()) + saved.append({ + "page": page_no, "name": str(key), "file": str(out_file), + "width": w, "height": h, "format": ext, + }) + except Exception: + continue + + doc.close() + Output.success({"output_dir": str(dest), "total_images": len(saved), "images": saved}) + + +# ═══════════════════════════════════════════════════════════════ +# Section 3: pages — merge, split, rotate, crop +# ═══════════════════════════════════════════════════════════════ + +@cmd("pages.merge") +def pages_merge(argv: list): + """Concatenate several PDF files into one.""" + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + if not argv: + Output.error("MissingArg", "At least one PDF required") + + import pikepdf + sources = [Output.check_file(p) for p in argv] + handles = [] + try: + combined = pikepdf.new() + descriptions = [] + for src in sources: + handle = pikepdf.open(src) + handles.append(handle) + n = len(handle.pages) + descriptions.append(f"{src.name} ({n} pages)") + for pg in handle.pages: + combined.pages.append(pg) + total = len(combined.pages) + combined.save(out_path) + combined.close() + except Exception as exc: + Output.error("MergeError", f"Merge failed: {exc}", code=4) + finally: + for h in handles: + try: + h.close() + except Exception: + pass + + Output.success({"output": out_path, "total_pages": total, "sources": descriptions}) + + +@cmd("pages.split") +def pages_split(argv: list): + """Write each page as a separate single-page PDF.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_dir = _pop_flag(argv, "-o", "--output") or "." + + import pikepdf + src = Output.check_file(pdf_path) + dest = Path(out_dir) + dest.mkdir(parents=True, exist_ok=True) + + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + generated = [] + base = src.stem + try: + for idx, pg in enumerate(doc.pages, 1): + fp = dest / f"{base}_page{idx:03d}.pdf" + single_doc = pikepdf.new() + single_doc.pages.append(pg) + single_doc.save(fp) + single_doc.close() + generated.append(str(fp)) + doc.close() + except Exception as exc: + Output.error("SplitError", f"Split failed: {exc}", code=4) + + Output.success({"output_dir": str(dest), "total_pages": len(generated), "files": generated}) + + +@cmd("pages.rotate") +def pages_rotate(argv: list): + """Rotate selected pages by 90/180/270 degrees.""" + if len(argv) < 2: + Output.error("MissingArg", "pdf path and degrees required") + pdf_path = argv.pop(0) + degrees = int(argv.pop(0)) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + page_range = _pop_flag(argv, "-p", "--pages") + + if degrees not in (90, 180, 270): + Output.error("InvalidDegrees", "Rotation angle must be 90, 180, or 270") + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + targets = _resolve_page_indices(page_range, len(doc.pages)) + try: + for i in targets: + existing = int(doc.pages[i].get("/Rotate", 0)) + doc.pages[i]["/Rotate"] = (existing + degrees) % 360 + doc.save(out_path) + doc.close() + except Exception as exc: + Output.error("RotateError", f"Rotation failed: {exc}", code=4) + + Output.success({"output": out_path, "degrees": degrees, "pages_rotated": len(targets)}) + + +@cmd("pages.crop") +def pages_crop(argv: list): + """Set the media/crop box on selected pages. box = 'left,bottom,right,top' in pt.""" + if len(argv) < 2: + Output.error("MissingArg", "pdf path and crop box required") + pdf_path = argv.pop(0) + box_str = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + page_range = _pop_flag(argv, "-p", "--pages") + + try: + coords = [float(v.strip()) for v in box_str.split(",")] + assert len(coords) == 4 + left, bottom, right, top = coords + except Exception: + Output.error("InvalidBox", "Invalid crop box format, should be: left,bottom,right,top", + hint="Example: 50,50,550,750") + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + targets = _resolve_page_indices(page_range, len(doc.pages)) + try: + arr = pikepdf.Array([left, bottom, right, top]) + for i in targets: + doc.pages[i].mediabox = arr + doc.pages[i].cropbox = arr + doc.save(out_path) + doc.close() + except Exception as exc: + Output.error("CropError", f"Crop failed: {exc}", code=4) + + Output.success({ + "output": out_path, + "box": {"left": left, "bottom": bottom, "right": right, "top": top}, + "pages_cropped": len(targets), + }) + + +# ═══════════════════════════════════════════════════════════════ +# Section 4: meta — metadata reading, writing, and branding +# ═══════════════════════════════════════════════════════════════ + +_XMP_MAPPING = { + "Title": "dc:title", + "Author": "dc:creator", + "Subject": "dc:description", + "Keywords": "pdf:Keywords", + "Creator": "xmp:CreatorTool", + "Producer": "pdf:Producer", +} +_ACCEPTED_KEYS = set(_XMP_MAPPING.keys()) + + +@cmd("meta.get") +def meta_get(argv: list): + """Read document information and metadata.""" + if not argv: + Output.error("MissingArg", "pdf path required") + + import pikepdf + src = Output.check_file(argv[0]) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + record: dict = { + "pages": len(doc.pages), + "pdf_version": str(doc.pdf_version), + } + + if doc.pages: + mb = doc.pages[0].mediabox + record["page_size"] = { + "width": float(mb[2] - mb[0]), + "height": float(mb[3] - mb[1]), + "unit": "pt", + } + + kv_pairs = {} + if doc.docinfo: + for k in doc.docinfo.keys(): + try: + kv_pairs[str(k).lstrip("/")] = str(doc.docinfo[k]) + except Exception: + pass + record["metadata"] = kv_pairs + record["encrypted"] = doc.is_encrypted + record["has_form"] = "/AcroForm" in doc.Root + record["has_outlines"] = "/Outlines" in doc.Root + + doc.close() + Output.success(record) + + +@cmd("meta.set") +def meta_set(argv: list): + """Update XMP + legacy docinfo metadata fields.""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + data = _load_json_arg(argv) + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + # XMP layer + with doc.open_metadata() as xmp: + for raw_key, raw_val in data.items(): + norm = raw_key.title() + xmp_key = _XMP_MAPPING.get(norm) + if xmp_key is None: + continue + try: + xmp[xmp_key] = str(raw_val) + except Exception: + pass + + # Legacy docinfo layer + if not doc.docinfo: + doc.docinfo = pikepdf.Dictionary() + for raw_key, raw_val in data.items(): + norm = raw_key.title() + if norm in _ACCEPTED_KEYS: + doc.docinfo[pikepdf.Name(f"/{norm}")] = pikepdf.String(str(raw_val)) + + doc.docinfo[pikepdf.Name("/ModDate")] = pikepdf.String( + datetime.now().strftime("D:%Y%m%d%H%M%S") + ) + + try: + doc.save(out_path) + doc.close() + except Exception as exc: + Output.error("SaveError", f"Save failed: {exc}", code=4) + + Output.success({"output": out_path, "updated_fields": list(data.keys())}) + + +@cmd("meta.brand") +def meta_brand(argv: list): + """Add Z.ai branding metadata to PDF documents.""" + output_path = _pop_flag(argv, "-o", "--output") + custom_title = _pop_flag(argv, "-t", "--title") + quiet = _pop_flag(argv, "-q", "--quiet", needs_value=False) + + if not argv: + Output.error("MissingArg", "At least one PDF file required") + + # Check if output is specified for multiple files + if output_path and len(argv) > 1: + Output.error("InvalidArg", "--output can only be used with a single input file") + + from pypdf import PdfReader, PdfWriter + + for input_path in argv: + if not os.path.exists(input_path): + print(f"Error: Input file not found: {input_path}", file=sys.stderr) + continue + + try: + reader = PdfReader(input_path) + except Exception as e: + print(f"Error: Cannot open PDF: {e}", file=sys.stderr) + continue + + writer = PdfWriter() + for page in reader.pages: + writer.add_page(page) + + # Determine title + if custom_title: + title = custom_title + else: + original_meta = reader.metadata + if original_meta and original_meta.title and original_meta.title not in ('(anonymous)', 'unspecified', None): + title = original_meta.title + else: + title = os.path.splitext(os.path.basename(input_path))[0] + + writer.add_metadata({ + '/Title': title, + '/Author': 'Z.ai', + '/Creator': 'Z.ai', + '/Producer': 'http://z.ai', + }) + + # Write output + out = output_path if (len(argv) == 1 and output_path) else input_path + try: + with open(out, "wb") as f: + writer.write(f) + except Exception as e: + print(f"Error: Cannot write output file: {e}", file=sys.stderr) + continue + + if not quiet: + print(f"\u2713 Updated metadata for: {os.path.basename(input_path)}") + print(f" Title: {title}") + print(f" Author: Z.ai") + print(f" Creator: Z.ai") + print(f" Producer: http://z.ai") + if out != input_path: + print(f" Output: {out}") + + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 5: form — inspection, filling, annotation, rendering +# ═══════════════════════════════════════════════════════════════ + +# --- form.info (pikepdf-based) --- + +_FIELD_TYPE_MAP = { + "/Tx": "text", + "/Sig": "signature", +} + + +def _classify_field(node) -> str: + """Map a PDF field type token to a human label.""" + ft = str(node.get("/FT", "")) + if ft in _FIELD_TYPE_MAP: + return _FIELD_TYPE_MAP[ft] + flags = int(node.get("/Ff", 0)) + if ft == "/Btn": + return "radio" if (flags & (1 << 15)) else "checkbox" + if ft == "/Ch": + return "dropdown" if (flags & (1 << 17)) else "listbox" + return "unknown" + + +def _extra_props(node, kind: str) -> dict: + """Gather type-specific metadata (options, checked value, etc.).""" + props: dict = {} + if kind == "checkbox": + ap = node.get("/AP") + if ap and "/N" in ap: + states = [str(s) for s in ap["/N"].keys()] + props["states"] = states + props["checked_value"] = next((s for s in states if s != "/Off"), states[0] if states else None) + elif kind in ("dropdown", "listbox"): + raw_opts = node.get("/Opt") + if raw_opts: + props["options"] = [ + {"value": str(item[0]), "label": str(item[1])} if isinstance(item, list) and len(item) >= 2 + else {"value": str(item), "label": str(item)} + for item in raw_opts + ] + elif kind == "radio": + kids = node.get("/Kids") + if kids: + radio_vals = [] + for child in kids: + ap = child.get("/AP") + if ap and "/N" in ap: + radio_vals.extend(str(k) for k in ap["/N"].keys() if str(k) != "/Off") + if radio_vals: + props["options"] = radio_vals + return props + + +def _current_value(node): + v = node.get("/V") + return str(v) if v is not None else None + + +def _gather_fields(doc) -> list: + """Walk the AcroForm field tree iteratively and return a flat list.""" + if "/AcroForm" not in doc.Root: + return [] + acro = doc.Root.AcroForm + if "/Fields" not in acro: + return [] + + page_lookup = {pg.objgen: idx for idx, pg in enumerate(doc.pages)} + results = [] + stack = [(field, "") for field in reversed(list(acro.Fields))] + + while stack: + node, parent_path = stack.pop() + name = str(node.get("/T", "")) + full = f"{parent_path}.{name}" if parent_path else name + + kids = node.get("/Kids") + if kids and any("/T" in k for k in kids): + for kid in reversed(list(kids)): + stack.append((kid, full)) + continue + + kind = _classify_field(node) + if kind == "unknown": + continue + + entry = {"id": full, "type": kind} + val = _current_value(node) + if val: + entry["current_value"] = val + entry.update(_extra_props(node, kind)) + + page_ref = node.get("/P") + if page_ref and hasattr(page_ref, "objgen"): + pg_num = page_lookup.get(page_ref.objgen) + if pg_num is not None: + entry["page"] = pg_num + 1 + + results.append(entry) + + return results + + +@cmd("form.info") +def form_info(argv: list): + """Return structured JSON describing every form field (pikepdf + check_fillable).""" + if not argv: + Output.error("MissingArg", "pdf path required") + + import pikepdf + src = Output.check_file(argv[0]) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + fields = _gather_fields(doc) + if not fields: + Output.success({"has_fields": False, "count": 0, "fields": [], "hint": "This PDF has no fillable form fields"}) + Output.success({"has_fields": True, "count": len(fields), "fields": fields}) + + +@cmd("form.fill") +def form_fill(argv: list): + """Write values into a fillable PDF (pikepdf version).""" + if not argv: + Output.error("MissingArg", "pdf path required") + pdf_path = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + if out_path is None: + Output.error("MissingArg", "--output is required") + data = _load_json_arg(argv) + + import pikepdf + src = Output.check_file(pdf_path) + try: + doc = pikepdf.open(src) + except Exception as exc: + Output.error("PDFError", f"Cannot open PDF: {exc}", code=3) + + if "/AcroForm" not in doc.Root or "/Fields" not in doc.Root.AcroForm: + Output.error("NoForm", "This PDF has no form fields") + + known = {f["id"]: f for f in _gather_fields(doc)} + + # Validation + issues = [] + for fid, fval in data.items(): + if fid not in known: + issues.append(f"Field not found: {fid}") + continue + fmeta = known[fid] + ftype = fmeta["type"] + if ftype == "checkbox" and "states" in fmeta: + ok_vals = fmeta["states"] + if fval not in ok_vals and f"/{fval}" not in ok_vals and fval not in ("true", "True", "false", "False", "1", "0"): + issues.append(f"Invalid value for field {fid}, options: {ok_vals} or true/false") + if ftype in ("dropdown", "listbox") and "options" in fmeta: + ok_vals = [o["value"] for o in fmeta["options"]] + if fval not in ok_vals: + issues.append(f"Invalid value for field {fid}, options: {ok_vals}") + if issues: + Output.error("ValidationError", "Field validation failed", hint="; ".join(issues)) + + # Fill + written = 0 + + def _apply(node, parent_path=""): + nonlocal written + name = str(node.get("/T", "")) + full = f"{parent_path}.{name}" if parent_path else name + kids = node.get("/Kids") + if kids and any("/T" in k for k in kids): + for kid in kids: + _apply(kid, full) + return + if full not in data: + return + val = data[full] + kind = _classify_field(node) + if kind == "checkbox": + if val in ("true", "True", "1", True): + ap = node.get("/AP") + if ap and "/N" in ap: + checked_name = next((str(k) for k in ap["/N"].keys() if str(k) != "/Off"), "/Yes") + if not checked_name.startswith("/"): + checked_name = f"/{checked_name}" + node["/V"] = pikepdf.Name(checked_name) + node["/AS"] = pikepdf.Name(checked_name) + else: + node["/V"] = pikepdf.Name("/Off") + node["/AS"] = pikepdf.Name("/Off") + else: + node["/V"] = pikepdf.String(str(val)) + written += 1 + + for field in doc.Root.AcroForm.Fields: + _apply(field) + + acro = doc.Root.AcroForm + if "/NeedAppearances" not in acro: + acro["/NeedAppearances"] = True + + try: + doc.save(out_path) + except Exception as exc: + Output.error("SaveError", f"Save failed: {exc}", code=4) + + Output.success({"output": out_path, "fields_filled": written, "fields_requested": len(data)}) + + +# --- form.detail (pypdf-based detailed field extraction) --- + +def _get_full_annotation_field_id(annotation): + """Build dotted field ID by walking parent chain.""" + 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" + states = field.get("/_States_", []) + if len(states) == 2: + 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 + + +def _get_field_info(reader) -> list: + """Extract detailed field info from a PdfReader, including radio group aggregation.""" + fields = reader.get_fields() + + field_info_by_id = {} + possible_radio_names: Set[str] = set() + + for field_id, field in fields.items(): + 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) + + radio_fields_by_id: Dict[str, dict] = {} + + 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: + 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": [], + } + radio_fields_by_id[field_id]["radio_options"].append({ + "value": on_values[0], + "rect": rect, + }) + + # Filter fields without location + 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), 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 + + +@cmd("form.detail") +def form_detail(argv: list): + """Extract detailed field info (pypdf version) to JSON.""" + if len(argv) < 2: + Output.error("MissingArg", "Usage: form.detail <pdf> <output.json>") + pdf_path = argv[0] + json_output_path = argv[1] + + from pypdf import PdfReader + 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}") + raise SystemExit(0) + + +# --- form.fill-legacy (pypdf version with monkeypatch) --- + +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 + + +def _monkeypatch_pypdf_method(): + """ + Workaround for pypdf bug with selection list fields. + pypdf's get_inherited returns a list of two-element lists for /Opt fields + in selection lists, causing join() to throw TypeError. We patch it to + return just the value strings. + """ + 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 + + +@cmd("form.fill-legacy") +def form_fill_legacy(argv: list): + """Fill fillable form fields (pypdf version with monkeypatch).""" + if len(argv) < 3: + Output.error("MissingArg", "Usage: form.fill-legacy <pdf> <fields.json> <output.pdf>") + input_pdf = argv[0] + fields_json = argv[1] + output_pdf = argv[2] + + from pypdf import PdfReader, PdfWriter + + _monkeypatch_pypdf_method() + + with open(fields_json) as f: + fields = json.load(f) + + # Group by page number + fields_by_page: Dict[int, dict] = {} + 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) + + 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: + raise SystemExit(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) + + writer.set_need_appearances_writer(True) + + with open(output_pdf, "wb") as f: + writer.write(f) + + print(f"Filled {len(fields_by_page)} page(s) in {output_pdf}") + raise SystemExit(0) + + +# --- form.annotate (annotation-based filling with coordinate transform) --- + +def _transform_coordinates(bbox, image_width, image_height, pdf_width, pdf_height): + """Transform bounding box from image coordinates to PDF coordinates.""" + 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 _normalise_fields_json(raw: dict) -> dict: + """Accept both the current sheet-based schema and the legacy flat schema. + + Current (v2) schema uses ``sheet[].pg/dims/regions[]`` with nested + ``label.bbox``, ``target.bbox``, ``ink{}``. + + Legacy (v1) schema uses ``pages[]`` + ``form_fields[]`` with flat keys + like ``entry_bounding_box``, ``label_bounding_box``, ``entry_text{}``. + + Returns a normalised dict in the **v2** internal format used by all + downstream functions. + """ + # Already v2 + if "sheet" in raw: + return raw + + # Convert legacy → v2 + pages_lut = {p["page_number"]: p for p in raw.get("pages", [])} + sheets: dict = {} # pg -> sheet entry + + for f in raw.get("form_fields", []): + pg = f["page_number"] + if pg not in sheets: + pi = pages_lut.get(pg, {}) + sheets[pg] = { + "pg": pg, + "dims": [pi.get("image_width", 0), pi.get("image_height", 0)], + "regions": [], + } + et = f.get("entry_text", {}) + region = { + "id": f.get("field_label", f.get("description", "")), + "hint": f.get("description", ""), + "label": {"tag": f.get("field_label", ""), "bbox": f.get("label_bounding_box", [0, 0, 0, 0])}, + "target": {"bbox": f.get("entry_bounding_box", [0, 0, 0, 0])}, + "ink": {}, + } + if isinstance(et, dict) and et.get("text"): + region["ink"]["value"] = et["text"] + if "font_size" in et: + region["ink"]["size"] = et["font_size"] + if "font_color" in et: + region["ink"]["color"] = et["font_color"] + if "font" in et: + region["ink"]["font"] = et["font"] + sheets[pg]["regions"].append(region) + + return {"sheet": list(sheets.values())} + + +@cmd("form.annotate") +def form_annotate(argv: list): + """Fill a PDF by adding text annotations (FreeText) defined in fields.json.""" + if len(argv) < 3: + Output.error("MissingArg", "Usage: form.annotate <pdf> <fields.json> <output.pdf>") + input_pdf = argv[0] + fields_json_path = argv[1] + output_pdf = argv[2] + + from pypdf import PdfReader, PdfWriter + from pypdf.annotations import FreeText + + with open(fields_json_path, "r") as f: + fields_data = _normalise_fields_json(json.load(f)) + + reader = PdfReader(input_pdf) + writer = PdfWriter() + 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] + + annotations = [] + for page_entry in fields_data["sheet"]: + pg = page_entry["pg"] + image_width, image_height = page_entry["dims"] + pdf_width, pdf_height = pdf_dimensions[pg] + + for region in page_entry["regions"]: + ink = region.get("ink", {}) + text = ink.get("value", "") + if not text: + continue + + transformed_box = _transform_coordinates( + region["target"]["bbox"], + image_width, image_height, + pdf_width, pdf_height + ) + + font_name = ink.get("font", "Arial") + font_size = str(ink.get("size", 14)) + "pt" + font_color = ink.get("color", "000000") + + annotation = FreeText( + text=text, + rect=transformed_box, + font=font_name, + font_size=font_size, + font_color=font_color, + border_color=None, + background_color=None, + ) + annotations.append(annotation) + writer.add_annotation(page_number=pg - 1, annotation=annotation) + + with open(output_pdf, "wb") as output: + writer.write(output) + + print(f"Successfully filled PDF form and saved to {output_pdf}") + print(f"Added {len(annotations)} text annotations") + raise SystemExit(0) + + +# --- form.render (PDF to PNG images) --- + +@cmd("form.render") +def form_render(argv: list): + """Convert each page of a PDF to a PNG image.""" + if len(argv) < 2: + Output.error("MissingArg", "Usage: form.render <pdf> <output_dir> [--max-dim N]") + pdf_path = argv.pop(0) + output_dir = argv.pop(0) + max_dim_str = _pop_flag(argv, "-m", "--max-dim") + max_dim = int(max_dim_str) if max_dim_str else 1000 + + from pdf2image import convert_from_path + + os.makedirs(output_dir, exist_ok=True) + images = convert_from_path(pdf_path, dpi=200) + + for i, image in enumerate(images): + 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") + raise SystemExit(0) + + +# --- form.validate (bounding box validation image) --- + +@cmd("form.validate") +def form_validate(argv: list): + """Create validation images with bounding box rectangles.""" + if len(argv) < 4: + Output.error("MissingArg", "Usage: form.validate <page> <fields.json> <input_img> <output_img>") + page_number = int(argv[0]) + fields_json_path = argv[1] + input_path = argv[2] + output_path = argv[3] + + from PIL import Image, ImageDraw + + with open(fields_json_path, 'r') as f: + data = _normalise_fields_json(json.load(f)) + + img = Image.open(input_path) + draw = ImageDraw.Draw(img) + num_boxes = 0 + + for page_entry in data["sheet"]: + if page_entry["pg"] != page_number: + continue + for region in page_entry["regions"]: + target_box = region["target"]["bbox"] + label_box = region["label"]["bbox"] + draw.rectangle(target_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") + raise SystemExit(0) + + +# --- form.check-bbox (bounding box overlap detection) --- + +@dataclass +class _RectAndField: + rect: list + rect_type: str + field: dict + + +def get_bounding_box_messages(fields_json_stream) -> List[str]: + """Check for overlapping bounding boxes. Returns list of messages (max 20).""" + messages = [] + raw = json.load(fields_json_stream) + data = _normalise_fields_json(raw) + + total_regions = sum(len(pe["regions"]) for pe in data["sheet"]) + messages.append(f"Read {total_regions} regions across {len(data['sheet'])} page(s)") + + 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) + + has_error = False + + for page_entry in data["sheet"]: + pg = page_entry["pg"] + # Collect all rects on this page + rects_and_regions = [] + for region in page_entry["regions"]: + rects_and_regions.append(_RectAndField(region["label"]["bbox"], "label", region)) + rects_and_regions.append(_RectAndField(region["target"]["bbox"], "target", region)) + + for i, ri in enumerate(rects_and_regions): + for j in range(i + 1, len(rects_and_regions)): + rj = rects_and_regions[j] + if rects_intersect(ri.rect, rj.rect): + has_error = True + rid = ri.field.get("id", ri.field.get("hint", "?")) + rjd = rj.field.get("id", rj.field.get("hint", "?")) + if ri.field is rj.field: + messages.append(f"FAILURE: pg {pg} — label/target overlap for `{rid}` ({ri.rect}, {rj.rect})") + else: + messages.append(f"FAILURE: pg {pg} — {ri.rect_type} of `{rid}` ({ri.rect}) overlaps {rj.rect_type} of `{rjd}` ({rj.rect})") + if len(messages) >= 20: + messages.append("Aborting further checks; fix bounding boxes and try again") + return messages + + # Height check for target rects + if ri.rect_type == "target": + ink = ri.field.get("ink", {}) + if ink.get("value"): + font_size = ink.get("size", 14) + entry_height = ri.rect[3] - ri.rect[1] + if entry_height < font_size: + has_error = True + rid = ri.field.get("id", ri.field.get("hint", "?")) + messages.append(f"FAILURE: pg {pg} — target box height ({entry_height}) for `{rid}` is shorter than font size ({font_size}). Increase box height or decrease ink.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 + + +@cmd("form.check-bbox") +def form_check_bbox(argv: list): + """Check bounding boxes in fields.json for overlaps.""" + if not argv: + Output.error("MissingArg", "Usage: form.check-bbox <fields.json>") + with open(argv[0]) as f: + messages = get_bounding_box_messages(f) + for msg in messages: + print(msg) + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 6: convert — office, HTML, and LaTeX +# ═══════════════════════════════════════════════════════════════ + +_CONVERTIBLE_EXTENSIONS = frozenset({ + ".docx", ".doc", ".odt", ".rtf", + ".pptx", ".ppt", ".odp", + ".xlsx", ".xls", ".ods", ".csv", + ".txt", ".html", ".htm", +}) + +_SOFFICE_CANDIDATES = [ + "/Applications/LibreOffice.app/Contents/MacOS/soffice", + os.path.expanduser("~/Applications/LibreOffice.app/Contents/MacOS/soffice"), + "/usr/bin/soffice", + "/usr/local/bin/soffice", + "/usr/lib/libreoffice/program/soffice", + "/opt/libreoffice/program/soffice", + "/snap/bin/libreoffice.soffice", +] + + +def _locate_soffice() -> Optional[str]: + """Search for a working soffice binary.""" + for candidate in _SOFFICE_CANDIDATES: + if Path(candidate).is_file(): + return candidate + for alias in ("soffice", "libreoffice"): + found = shutil.which(alias) + if found: + return found + return None + + +@cmd("convert.office") +def convert_office(argv: list): + """Convert an office document to PDF via LibreOffice.""" + if not argv: + Output.error("MissingArg", "input file required") + src_path = argv.pop(0) + out_path = _pop_flag(argv, "-o", "--output") + + src = Output.check_file(src_path) + ext = src.suffix.lower() + + if ext not in _CONVERTIBLE_EXTENSIONS: + Output.error( + "UnsupportedFormat", + f"Unsupported format: {ext}", + hint=f"Supported formats: {', '.join(sorted(_CONVERTIBLE_EXTENSIONS))}", + ) + + binary = _locate_soffice() + if binary is None: + Output.error( + "DependencyMissing", + "LibreOffice not found", + hint="Please install LibreOffice: https://www.libreoffice.org/download/", + ) + + target_dir = Path(out_path).parent if out_path else src.parent + target_dir.mkdir(parents=True, exist_ok=True) + + cmd_list = [binary, "--headless", "--convert-to", "pdf", "--outdir", str(target_dir), str(src)] + + try: + proc = subprocess.run(cmd_list, capture_output=True, text=True, timeout=120) + if proc.returncode != 0: + Output.error("ConvertError", f"Conversion failed: {proc.stderr.strip() or 'Unknown error'}", code=4) + except subprocess.TimeoutExpired: + Output.error("Timeout", "Conversion timeout (>120s)", code=4) + except Exception as exc: + Output.error("ConvertError", f"Conversion failed: {exc}", code=4) + + auto_name = target_dir / f"{src.stem}.pdf" + final = Path(out_path) if out_path else auto_name + + if out_path and final.name != auto_name.name and auto_name.exists(): + auto_name.rename(final) + + if not final.exists(): + Output.error("ConvertError", "Converted PDF file was not generated", code=4) + + Output.success({"input": str(src), "output": str(final), "format": ext}) + + +@cmd("convert.html") +def convert_html(argv: list): + """Convert HTML to PDF via node html2pdf.js.""" + if not argv: + Output.error("MissingArg", "input file required") + + js_path = _SCRIPT_DIR / "html2pdf.js" + if not js_path.exists(): + Output.error("DependencyMissing", "html2pdf.js not found in scripts directory") + + node_path = shutil.which("node") + if not node_path: + Output.error("DependencyMissing", "node not found in PATH") + + cmd_list = [node_path, str(js_path)] + argv + try: + proc = subprocess.run(cmd_list, timeout=180) + raise SystemExit(proc.returncode) + except subprocess.TimeoutExpired: + Output.error("Timeout", "HTML conversion timeout (>180s)", code=4) + except SystemExit: + raise + except Exception as exc: + Output.error("ConvertError", f"HTML conversion failed: {exc}", code=4) + + +# --- convert.latex (tectonic wrapper with log filtering + PDF stats) --- + +_NOISE_RE = re.compile( + r"^note: (?:" + r'"version 2" Tectonic' + r"|Running TeX" + r"|Rerunning TeX because" + r"|Running xdvipdfmx" + r"|downloading " + r"|Skipped writing .* intermediate files" + r")" +) + + +def _find_tectonic() -> Optional[str]: + """Locate the tectonic binary: script_dir first, ~/tectonic, then PATH.""" + local_bin = _SCRIPT_DIR / "tectonic" + if local_bin.exists() and os.access(local_bin, os.X_OK): + return str(local_bin) + home_bin = Path.home() / "tectonic" + if home_bin.exists() and os.access(home_bin, os.X_OK): + return str(home_bin) + system_bin = shutil.which("tectonic") + return system_bin + + +def _human_size(nbytes: int) -> str: + for unit in ("B", "KB", "MB", "GB"): + if nbytes < 1024: + return f"{nbytes:.2f} {unit}" + nbytes /= 1024 + return f"{nbytes:.2f} TB" + + +def _pdf_stats(pdf_file: Path): + """Return (pages, word_count, image_count) or Nones.""" + try: + from pypdf import PdfReader + except ImportError: + for attempt in ( + [sys.executable, "-m", "pip", "install", "-q", "pypdf"], + [sys.executable, "-m", "pip", "install", "-q", "--break-system-packages", "pypdf"], + [sys.executable, "-m", "pip", "install", "-q", "--user", "pypdf"], + ): + if subprocess.run(attempt, check=False, capture_output=True).returncode == 0: + break + try: + from pypdf import PdfReader + except ImportError: + return None, None, None + + try: + reader = PdfReader(str(pdf_file)) + n_pages = len(reader.pages) + all_text = "".join(p.extract_text() or "" for p in reader.pages) + n_words = len([w for w in all_text.split() if w.strip()]) + n_images = 0 + for pg in reader.pages: + xobj = pg.get("/Resources", {}).get("/XObject") + if xobj: + obj = xobj.get_object() + n_images += sum(1 for k in obj if obj[k].get("/Subtype") == "/Image") + return n_pages, n_words, n_images + except Exception as exc: + print(f"Error extracting PDF info: {exc}", file=sys.stderr) + return None, None, None + + +def _classify_lines(lines): + """Bucket raw output into errors / warnings / layout issues.""" + errors, warnings, layout, pdf_note = [], [], [], None + for raw in lines: + ln = raw.rstrip() + if not ln: + continue + if _NOISE_RE.match(ln): + if ln.startswith("note: Writing"): + pdf_note = ln + continue + if ln.startswith("error:"): + errors.append(ln) + elif ln.startswith("warning:"): + warnings.append(ln) + elif re.search(r"(Overfull|Underfull) \\[hv]box", ln) or re.search(r"(Font shape|Missing character)", ln): + layout.append(ln) + return errors, warnings, layout, pdf_note + + +def _parse_writing_note(note: Optional[str]): + m = re.search(r"Writing `(.+?)` \((.+?)\)", note or "") + return (m.group(1), m.group(2)) if m else (None, None) + + +@cmd("convert.latex") +def convert_latex(argv: list): + """Compile LaTeX file via tectonic, filter logs, report PDF stats.""" + if not argv: + Output.error("MissingArg", "tex file required") + tex_file = argv.pop(0) + runs_str = _pop_flag(argv, "-r", "--runs") + runs = int(runs_str) if runs_str else 1 + keep_logs = _pop_flag(argv, "-k", "--keep-logs", needs_value=False) + + tex = Path(tex_file) + if not tex.exists(): + print(f"\u2717 Error: File not found {tex_file}") + raise SystemExit(1) + + print(f"Compiling {tex.name}...", flush=True) + if runs > 1: + print(f"Running {runs} passes (for cross-references)", flush=True) + + tectonic = _find_tectonic() + if tectonic is None: + print("\n\u2717 Error: tectonic command not found") + print("Please install tectonic: https://tectonic-typesetting.github.io/") + print("\nHint: If installed at ~/tectonic, ensure it has execute permission:") + print(" chmod +x ~/tectonic") + raise SystemExit(1) + + all_lines = [] + ok = False + for _ in range(runs): + try: + proc = subprocess.run( + [tectonic, "-X", "compile", str(tex)], + capture_output=True, text=True, timeout=120, + ) + all_lines.extend((proc.stdout + proc.stderr).splitlines()) + ok = proc.returncode == 0 + if not ok: + break + except subprocess.TimeoutExpired: + print("\n\u2717 Error: Compilation timeout (>2 minutes)") + raise SystemExit(1) + except Exception as exc: + print(f"\n\u2717 Error: {exc}") + raise SystemExit(1) + + if keep_logs: + print("\n" + "=" * 50 + "\nFull logs:\n" + "=" * 50) + for ln in all_lines: + print(ln) + print("=" * 50 + "\n") + + errors, warnings, layout, pdf_note = _classify_lines(all_lines) + noted_name, noted_size = _parse_writing_note(pdf_note) + pdf_name = noted_name or (tex.stem + ".pdf") + pdf_path = tex.parent / pdf_name + + print() + if ok: + tag = "\u2713 Compilation successful" + (" (with warnings)" if warnings or layout else "") + print(tag) + else: + print("\u2717 Compilation failed") + + if ok and pdf_path.exists(): + print("\n========================\nPDF Information\n========================") + print(f"File: {pdf_name}") + print(f"Size: {noted_size or _human_size(pdf_path.stat().st_size)}") + pages, words, images = _pdf_stats(pdf_path) + if pages is not None: + print(f"Pages: {pages}") + if words is not None: + print(f"Words: ~{words:,}") + if images is not None: + print(f"Images: {images}") + + if layout: + print(f"\n========================\nLayout Issues ({len(layout)})\n========================") + for ln in layout: + print(ln) + + if warnings: + print(f"\n========================\nWarnings ({len(warnings)})\n========================") + for ln in warnings: + print(ln.replace("warning: ", "", 1)) + + if errors: + print("\n========================\nErrors\n========================") + for ln in errors: + print(ln.replace("error: ", "", 1)) + + if ok and (layout or warnings): + print() + print("<system-reminder>") + print(f"Detected {len(layout)} layout issues and {len(warnings)} warnings.") + print("These issues affect PDF typesetting quality and must be fixed.") + print("Do not dismiss with 'warnings don't affect output'. Fix all issues.") + print("</system-reminder>") + + raise SystemExit(0 if ok else 1) + + +# ═══════════════════════════════════════════════════════════════ +# Section 7: code — sanitization pipeline for PDF generation code +# ═══════════════════════════════════════════════════════════════ + +# --- Step 0: restore literal unicode escapes/entities to real chars --- +_RE_UNICODE_ESC = re.compile(r"(\\u[0-9a-fA-F]{4})|(\\U[0-9a-fA-F]{8})|(\\x[0-9a-fA-F]{2})") + + +def _restore_escapes(s: str) -> str: + # HTML entities: ³ ≤ α ... + s = html.unescape(s) + + # Literal backslash escapes: "\\u00B3" -> "³" + def _dec(m: re.Match) -> str: + esc = m.group(0) + try: + if esc.startswith("\\u") or esc.startswith("\\U"): + return chr(int(esc[2:], 16)) + if esc.startswith("\\x"): + return chr(int(esc[2:], 16)) + except Exception: + return esc + return esc + + return _RE_UNICODE_ESC.sub(_dec, s) + + +# --- Step 1: superscripts/subscripts -> <super>/<sub> --- +_SUPERSCRIPT_MAP: Dict[str, str] = { + "\u2070": "0", "\u00b9": "1", "\u00b2": "2", "\u00b3": "3", "\u2074": "4", + "\u2075": "5", "\u2076": "6", "\u2077": "7", "\u2078": "8", "\u2079": "9", + "\u207a": "+", "\u207b": "-", "\u207c": "=", "\u207d": "(", "\u207e": ")", + "\u207f": "n", "\u1da6": "i", +} + +_SUBSCRIPT_MAP: Dict[str, str] = { + "\u2080": "0", "\u2081": "1", "\u2082": "2", "\u2083": "3", "\u2084": "4", + "\u2085": "5", "\u2086": "6", "\u2087": "7", "\u2088": "8", "\u2089": "9", + "\u208a": "+", "\u208b": "-", "\u208c": "=", "\u208d": "(", "\u208e": ")", + "\u2090": "a", "\u2091": "e", "\u2095": "h", "\u1d62": "i", "\u2c7c": "j", + "\u2096": "k", "\u2097": "l", "\u2098": "m", "\u2099": "n", "\u2092": "o", + "\u209a": "p", "\u1d63": "r", "\u209b": "s", "\u209c": "t", "\u1d64": "u", + "\u1d65": "v", "\u2093": "x", +} + + +def _replace_super_sub(s: str) -> str: + out = [] + for ch in s: + if ch in _SUPERSCRIPT_MAP: + out.append(f"<super>{_SUPERSCRIPT_MAP[ch]}</super>") + elif ch in _SUBSCRIPT_MAP: + out.append(f"<sub>{_SUBSCRIPT_MAP[ch]}</sub>") + else: + out.append(ch) + return "".join(out) + + +# --- Step 2: symbol fallback for SimHei (protect tags, then replace) --- +_SYMBOL_FALLBACK: Dict[str, str] = { + # Currently empty - enable entries as needed for fonts missing specific glyphs +} + + +def _fallback_symbols(s: str) -> str: + # Protect <super>/<sub> tags from being modified + placeholders: Dict[str, str] = {} + + def _protect_tag(m: re.Match) -> str: + key = f"@@TAG{len(placeholders)}@@" + placeholders[key] = m.group(0) + return key + + protected = re.sub(r"</?super>|</?sub>", _protect_tag, s) + + # Replace symbols + protected = "".join(_SYMBOL_FALLBACK.get(ch, ch) for ch in protected) + + # Restore tags + for k, v in placeholders.items(): + protected = protected.replace(k, v) + + return protected + + +def sanitize_code(text: str) -> str: + """ + Full sanitization pipeline for PDF generation code. + - Restore unicode escapes/entities to real characters + - Replace superscript/subscript unicode with <super>/<sub> + - Replace other risky symbols with ASCII/text fallbacks + """ + s = _restore_escapes(text) + s = _replace_super_sub(s) + s = _fallback_symbols(s) + return s + + +@cmd("code.sanitize") +def code_sanitize(argv: list): + """Sanitize Unicode in a Python script for PDF generation.""" + if not argv: + Output.error("MissingArg", "Usage: code.sanitize <target_script.py>") + target = argv[0] + with open(target, "r", encoding="utf-8") as f: + code = f.read() + sanitized = sanitize_code(code) + with open(target, "w", encoding="utf-8") as f: + f.write(sanitized) + print(f"Sanitized: {target}") + raise SystemExit(0) + + +# ═══════════════════════════════════════════════════════════════ +# Section 8: CLI dispatcher +# ═══════════════════════════════════════════════════════════════ + +def _usage(): + sys.stdout.write(__doc__.strip() + "\n") + raise SystemExit(0) + + +def main(): + tokens = sys.argv[1:] + if not tokens or tokens[0] in ("-h", "--help"): + _usage() + + cmd_name = tokens.pop(0) + + # Direct match + handler = _COMMANDS.get(cmd_name) + if handler is not None: + handler(tokens) + return + + # Two-word match (e.g., "extract text" -> "extract.text") + if tokens: + compound = f"{cmd_name}.{tokens[0]}" + handler = _COMMANDS.get(compound) + if handler is not None: + tokens.pop(0) + handler(tokens) + return + + # List commands in group + group_cmds = [k for k in _COMMANDS if k.startswith(cmd_name + ".")] + if group_cmds: + print(f"Available commands in '{cmd_name}':") + for c in sorted(group_cmds): + print(f" {c}") + raise SystemExit(0) + + print(f"Unknown command: {cmd_name}\n") + _usage() + + +if __name__ == "__main__": + main() diff --git a/skills/ppt/scripts/rearrange.py b/skills/ppt/scripts/rearrange.py new file mode 100755 index 0000000..f50e0e8 --- /dev/null +++ b/skills/ppt/scripts/rearrange.py @@ -0,0 +1,144 @@ +#!/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 + +Slides are 0-indexed. Indices can repeat to duplicate slides. +""" + +import argparse +import sys +from copy import deepcopy +from pathlib import Path + +from pptx import Presentation +from pptx.oxml.ns import qn + + +def copy_slide(src_prs: Presentation, dst_prs: Presentation, index: int, dst_layouts: dict) -> None: + """Append a copy of slide[index] from src_prs into dst_prs.""" + src_slide = src_prs.slides[index] + + # Match layout by name across all masters; fall back to first available layout + layout_name = src_slide.slide_layout.name + dst_layout = dst_layouts.get(layout_name) or dst_prs.slide_layouts[0] + + new_slide = dst_prs.slides.add_slide(dst_layout) + + # Clear auto-added placeholder shapes + for shape in list(new_slide.shapes): + sp = shape.element + sp.getparent().remove(sp) + + # Copy ALL non-layout relationships from source and build old→new rId mapping. + # This covers images, media, charts, hyperlinks, videos, and any other embedded content. + # Without this, relationship attributes (r:embed, r:id, r:link) in copied shapes would + # reference rIds that don't exist in the new slide, causing PowerPoint repair dialogs. + R_NS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + SKIP_TYPES = {"slideLayout", "notesSlide", "slide"} # handled by python-pptx infrastructure + rId_mapping: dict = {} + for rel_id, rel in src_slide.part.rels.items(): + rel_short = rel.reltype.split("/")[-1] + if rel_short in SKIP_TYPES: + continue + new_rId = new_slide.part.rels.get_or_add(rel.reltype, rel._target) + rId_mapping[rel_id] = new_rId + + # Copy all shape elements + r_embed = f"{{{R_NS}}}embed" + r_id = f"{{{R_NS}}}id" + r_link = f"{{{R_NS}}}link" + + for shape in src_slide.shapes: + new_el = deepcopy(shape.element) + new_slide.shapes._spTree.insert_element_before(new_el, "p:extLst") + + # Remap ALL relationship references (images, charts, hyperlinks, video, etc.) + for el in new_el.iter(): + for attr in (r_embed, r_id, r_link): + old_rId = el.get(attr) + if old_rId and old_rId in rId_mapping: + el.set(attr, rId_mapping[old_rId]) + + # Copy slide-level background if defined. + # p:bg lives inside p:cSld, not directly under p:sld. + src_cSld = src_slide.element.find(qn("p:cSld")) + dst_cSld = new_slide.element.find(qn("p:cSld")) + if src_cSld is not None and dst_cSld is not None: + src_bg = src_cSld.find(qn("p:bg")) + if src_bg is not None: + existing_bg = dst_cSld.find(qn("p:bg")) + if existing_bg is not None: + dst_cSld.remove(existing_bg) + dst_cSld.insert(0, deepcopy(src_bg)) + + +def rearrange_presentation( + template_path: Path, output_path: Path, slide_sequence: list[int] +) -> None: + src_prs = Presentation(template_path) + total = len(src_prs.slides) + + for idx in slide_sequence: + if idx < 0 or idx >= total: + raise ValueError(f"Slide index {idx} out of range (0–{total - 1})") + + # Build a fresh presentation with the same dimensions + dst_prs = Presentation(template_path) + + # Remove all existing slides from dst_prs + sldIdLst = dst_prs.slides._sldIdLst + for sldId in list(sldIdLst): + rId = sldId.get(qn("r:id")) # must use full namespace via qn(), not bare "r:id" + if rId: + dst_prs.part.drop_rel(rId) + sldIdLst.remove(sldId) + + # Search all slide masters for layout matching (templates may have multiple masters) + all_layouts = { + layout.name: layout + for master in dst_prs.slide_masters + for layout in master.slide_layouts + } + + # Append slides in requested order (duplicates included) + for idx in slide_sequence: + copy_slide(src_prs, dst_prs, idx, all_layouts) + + output_path.parent.mkdir(parents=True, exist_ok=True) + dst_prs.save(output_path) + print(f"Saved {len(slide_sequence)} slides → {output_path}") + + +def main() -> None: + parser = argparse.ArgumentParser( + description="Rearrange PowerPoint slides.", + epilog="Example: python rearrange.py template.pptx output.pptx 0,34,34,50,52", + ) + parser.add_argument("template", help="Path to template PPTX") + parser.add_argument("output", help="Path for output PPTX") + parser.add_argument("sequence", help="Comma-separated 0-based slide indices") + args = parser.parse_args() + + template_path = Path(args.template) + if not template_path.exists(): + print(f"Error: Template not found: {args.template}") + sys.exit(1) + + try: + slide_sequence = [int(x.strip()) for x in args.sequence.split(",")] + except ValueError: + print("Error: sequence must be comma-separated integers (e.g. 0,34,34,50,52)") + sys.exit(1) + + try: + rearrange_presentation(template_path, Path(args.output), slide_sequence) + except ValueError as e: + print(f"Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/ppt/scripts/replace.py b/skills/ppt/scripts/replace.py new file mode 100755 index 0000000..d9c2712 --- /dev/null +++ b/skills/ppt/scripts/replace.py @@ -0,0 +1,231 @@ +#!/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 + +_ALIGN_MAP = { + "LEFT": PP_ALIGN.LEFT, + "CENTER": PP_ALIGN.CENTER, + "RIGHT": PP_ALIGN.RIGHT, + "JUSTIFY": PP_ALIGN.JUSTIFY, +} + +# Bullet indentation constants +# marL = font_size × (1 + level) × 1.6 pts, converted to EMUs (1 pt = 12700 EMU) +_INDENT_FACTOR = 1.6 +_EMU_PER_PT = 12700 + + +def _clear_paragraph_bullets(paragraph): + """Remove all bullet XML elements from a paragraph's pPr.""" + pPr = paragraph._element.get_or_add_pPr() + for child in list(pPr): + if any(child.tag.endswith(t) for t in ("buChar", "buNone", "buAutoNum", "buFont")): + pPr.remove(child) + return pPr + + +def _apply_paragraph_properties(paragraph, para_data: Dict[str, Any]): + text = para_data.get("text", "") + pPr = _clear_paragraph_bullets(paragraph) + + if para_data.get("bullet", False): + level = para_data.get("level", 0) + paragraph.level = level + font_size = para_data.get("font_size", 18.0) + pPr.attrib["marL"] = str(int(font_size * _INDENT_FACTOR * (1 + level) * _EMU_PER_PT)) + pPr.attrib["indent"] = str(int(-font_size * 0.8 * _EMU_PER_PT)) + buChar = OxmlElement("a:buChar") + buChar.set("char", "•") + pPr.append(buChar) + if "alignment" not in para_data: + paragraph.alignment = PP_ALIGN.LEFT + else: + pPr.attrib["marL"] = "0" + pPr.attrib["indent"] = "0" + pPr.insert(0, OxmlElement("a:buNone")) + + if para_data.get("alignment") in _ALIGN_MAP: + paragraph.alignment = _ALIGN_MAP[para_data["alignment"]] + 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"]) + + run = paragraph.runs[0] if paragraph.runs else paragraph.add_run() + run.text = text + _apply_font_properties(run, para_data) + + +def _apply_font_properties(run, para_data: Dict[str, Any]): + for attr in ("bold", "italic", "underline"): + if attr in para_data: + setattr(run.font, attr, para_data[attr]) + 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"] + if "color" in para_data: + h = para_data["color"].lstrip("#") + if len(h) == 6: + run.font.color.rgb = RGBColor(int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)) + elif "theme_color" in para_data: + try: + run.font.color.theme_color = getattr(MSO_THEME_COLOR, para_data["theme_color"]) + except AttributeError: + print(f" WARNING: Unknown theme color '{para_data['theme_color']}'") + + +def _check_duplicate_keys(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValueError(f"Duplicate key in JSON: '{key}'") + result[key] = value + return result + + +def _validate_replacements(inventory: InventoryData, replacements: Dict) -> List[str]: + errors = [] + for slide_key, shapes_data in replacements.items(): + if not slide_key.startswith("slide-"): + continue + if slide_key not in inventory: + errors.append(f"Slide '{slide_key}' not found in inventory") + continue + for shape_key in shapes_data: + if shape_key not in inventory[slide_key]: + available = sorted(inventory[slide_key].keys()) + errors.append( + f"Shape '{shape_key}' not found on '{slide_key}'. " + f"Available: {', '.join(available)}" + ) + return errors + + +def apply_replacements(pptx_file: str, json_file: str, output_file: str): + prs = Presentation(pptx_file) + inventory = extract_text_inventory(Path(pptx_file), prs) + + # Snapshot original overflow so we can detect if replacements make it worse + original_overflow: Dict[str, Dict[str, float]] = { + slide_key: { + shape_key: sd.frame_overflow_bottom + for shape_key, sd in shapes.items() + if sd.frame_overflow_bottom is not None + } + for slide_key, shapes in inventory.items() + } + + with open(json_file) as f: + replacements = json.load(f, object_pairs_hook=_check_duplicate_keys) + + errors = _validate_replacements(inventory, replacements) + if errors: + print("ERROR: Invalid shapes in replacement JSON:") + for e in errors: + print(f" - {e}") + raise ValueError(f"Found {len(errors)} validation error(s)") + + shapes_cleared = shapes_replaced = 0 + + for slide_key, shapes_dict in inventory.items(): + if not slide_key.startswith("slide-"): + continue + for shape_key, shape_data in shapes_dict.items(): + if not shape_data.shape: + continue + tf = shape_data.shape.text_frame # type: ignore + tf.clear() + shapes_cleared += 1 + + para_list = replacements.get(slide_key, {}).get(shape_key, {}).get("paragraphs") + if not para_list: + continue + shapes_replaced += 1 + # Inherit original font_size if not specified in replacement + orig_paras = shape_data.paragraphs or [] + orig_font_size = orig_paras[0].get("font_size") if orig_paras else None + for i, para_data in enumerate(para_list): + p = tf.paragraphs[0] if i == 0 else tf.add_paragraph() + if orig_font_size is not None and "font_size" not in para_data: + para_data = {**para_data, "font_size": orig_font_size} + _apply_paragraph_properties(p, para_data) + + # Re-check overflow on the updated in-memory presentation. + # Note: extract_text_inventory may add benign empty <a:solidFill/> elements + # while reading font colors — these are harmless and ignored by PowerPoint. + updated_inventory = extract_text_inventory(Path(pptx_file), prs) + + overflow_errors: List[str] = [] + warnings: List[str] = [] + for slide_key, shapes_dict in updated_inventory.items(): + for shape_key, sd in shapes_dict.items(): + for w in sd.warnings: + warnings.append(f"{slide_key}/{shape_key}: {w}") + new_ov = sd.frame_overflow_bottom + if new_ov is not None: + old_ov = original_overflow.get(slide_key, {}).get(shape_key, 0.0) + if new_ov > old_ov + 0.01: + overflow_errors.append( + f'{slide_key}/{shape_key}: overflow increased by {new_ov - old_ov:.2f}" ' + f'(was {old_ov:.2f}", now {new_ov:.2f}")' + ) + + if overflow_errors or warnings: + print("\nWARNING: Issues in replacement output:") + for e in overflow_errors: + print(f" overflow - {e}") + for w in warnings: + print(f" warning - {w}") + + prs.save(output_file) + print(f"Saved: {output_file}") + print(f" Shapes cleared: {shapes_cleared}, replaced: {shapes_replaced}") + + +def main(): + if len(sys.argv) != 4: + print(__doc__) + sys.exit(1) + + input_pptx, replacements_json, output_pptx = ( + Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]) + ) + for p in (input_pptx, replacements_json): + if not p.exists(): + print(f"Error: File not found: {p}") + sys.exit(1) + + try: + apply_replacements(str(input_pptx), str(replacements_json), str(output_pptx)) + except Exception as e: + import traceback + print(f"Error: {e}") + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/skills/ppt/scripts/tectonic b/skills/ppt/scripts/tectonic new file mode 100755 index 0000000..e3201cc Binary files /dev/null and b/skills/ppt/scripts/tectonic differ diff --git a/skills/ppt/scripts/thumbnail.py b/skills/ppt/scripts/thumbnail.py new file mode 100755 index 0000000..8cf6e75 --- /dev/null +++ b/skills/ppt/scripts/thumbnail.py @@ -0,0 +1,352 @@ +#!/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() + + cols = min(args.cols, MAX_COLS) + if args.cols > MAX_COLS: + print(f"Warning: Columns limited to {MAX_COLS} (requested {args.cols})") + + input_path = Path(args.input) + if not input_path.is_file() or input_path.suffix.lower() != ".pptx": + sys.exit(f"Error: Invalid PowerPoint file: {args.input}") + + output_path = Path(f"{args.output_prefix}.jpg") + print(f"Processing: {args.input}") + + try: + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + + 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") + + prs = Presentation(str(input_path)) + total_slides = len(prs.slides) + hidden_slides = { + idx + 1 + for idx, slide in enumerate(prs.slides) + if slide.element.get("show") == "0" + } + + hidden_info = f" ({len(hidden_slides)} hidden)" if hidden_slides else "" + print(f"Found {total_slides} slides{hidden_info}") + + slide_images = convert_to_images(input_path, temp_path, CONVERSION_DPI, total_slides, hidden_slides) + if not slide_images: + sys.exit("Error: No slides found") + + grid_files = create_grids( + slide_images, cols, THUMBNAIL_WIDTH, output_path, + placeholder_regions, slide_dimensions, + ) + + print(f"Created {len(grid_files)} grid(s):") + for grid_file in grid_files: + print(f" - {grid_file}") + + except RuntimeError as e: + sys.exit(f"Error: {e}") + + +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 = {} + + 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(): + slide_idx = int(slide_key.split("-")[1]) + regions = [ + {"left": s.left, "top": s.top, "width": s.width, "height": s.height} + for s in shapes.values() + ] + if regions: + placeholder_regions[slide_idx] = regions + + return placeholder_regions, (slide_width_inches, slide_height_inches) + + +def _pptx_to_pdf(pptx_path, temp_dir): + """Convert PPTX to PDF via LibreOffice. Returns path to the PDF file.""" + pdf_path = temp_dir / f"{pptx_path.stem}.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") + return pdf_path + + +def _pdf_to_images(pdf_path, temp_dir, dpi): + """Convert PDF pages to JPEG images via pdftoppm. Returns sorted image paths.""" + 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") + return sorted(temp_dir.glob("slide-*.jpg")) + + +def convert_to_images(pptx_path, temp_dir, dpi, total_slides, hidden_slides): + """Convert PowerPoint to images via PDF, inserting placeholders for hidden slides.""" + pdf_path = _pptx_to_pdf(pptx_path, temp_dir) + visible_images = _pdf_to_images(pdf_path, temp_dir, dpi) + + if not visible_images: + return [] + + with Image.open(visible_images[0]) as img: + placeholder_size = img.size + + all_images = [] + visible_idx = 0 + for slide_num in range(1, total_slides + 1): + if slide_num in hidden_slides: + placeholder_path = temp_dir / f"hidden-{slide_num:03d}.jpg" + create_hidden_slide_placeholder(placeholder_size).save(placeholder_path, "JPEG") + all_images.append(placeholder_path) + else: + 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.""" + max_images_per_grid = cols * (cols + 1) + grid_files = [] + total_images = len(image_paths) + + for chunk_idx, start_idx in enumerate(range(0, total_images, max_images_per_grid)): + chunk_images = image_paths[start_idx: start_idx + max_images_per_grid] + + grid = create_grid(chunk_images, cols, width, start_idx, placeholder_regions, slide_dimensions) + + if total_images <= max_images_per_grid: + grid_filename = output_path + else: + grid_filename = output_path.parent / f"{output_path.stem}-{chunk_idx + 1}{output_path.suffix}" + + 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) + + with Image.open(image_paths[0]) as img: + aspect = img.height / img.width + height = int(width * aspect) + + 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 + + grid = Image.new("RGB", (grid_w, grid_h), "white") + draw = ImageDraw.Draw(grid) + + try: + font = ImageFont.load_default(size=font_size) + except Exception: + font = ImageFont.load_default() + + 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 + + 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) + + y_thumbnail = y_base + label_padding + font_size + label_padding + + with Image.open(img_path) as img: + orig_w, orig_h = img.size + + if placeholder_regions and (start_slide_num + i) in placeholder_regions: + if img.mode != "RGBA": + img = img.convert("RGBA") + + regions = placeholder_regions[start_slide_num + i] + if slide_dimensions: + slide_w_in, slide_h_in = slide_dimensions + else: + slide_w_in = orig_w / CONVERSION_DPI + slide_h_in = orig_h / CONVERSION_DPI + + x_scale = orig_w / slide_w_in + y_scale = orig_h / slide_h_in + + overlay = Image.new("RGBA", img.size, (255, 255, 255, 0)) + overlay_draw = ImageDraw.Draw(overlay) + stroke_width = max(5, min(orig_w, orig_h) // 150) + + for region in regions: + px_left = int(region["left"] * x_scale) + px_top = int(region["top"] * y_scale) + px_right = px_left + int(region["width"] * x_scale) + px_bottom = px_top + int(region["height"] * y_scale) + overlay_draw.rectangle( + [(px_left, px_top), (px_right, px_bottom)], + outline=(255, 0, 0, 255), + width=stroke_width, + ) + + img = Image.alpha_composite(img, overlay).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)) + + 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/ppt/setup.sh b/skills/ppt/setup.sh new file mode 100755 index 0000000..d6e8db8 --- /dev/null +++ b/skills/ppt/setup.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash +# --- +# name: ppt-setup +# author: Z.AI +# version: "1.0" +# description: Environment setup for the PPT skill. Checks and installs all required dependencies. +# --- +# +# Installs only dependencies required by the PPT skill. +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' +ok() { echo -e " ${GREEN}✓${NC} $1"; } +fail() { echo -e " ${RED}✗${NC} $1"; } +warn() { echo -e " ${YELLOW}○${NC} $1"; } +info() { echo -e " ${BLUE}→${NC} $1"; } + +echo "============================================" +echo " PPT Skill — Environment Setup" +echo "============================================" +echo "" + +OS="$(uname -s)" +ARCH="$(uname -m)" +echo "Platform: $OS $ARCH" +echo "" + +# ── 0. macOS: Homebrew ── +if [ "$OS" = "Darwin" ]; then + echo "--- Homebrew (macOS package manager) ---" + if command -v brew &>/dev/null; then + BREW_VER=$(brew --version 2>/dev/null | head -1) + ok "brew ($BREW_VER)" + else + fail "brew not found — Node.js install needs Homebrew on macOS" + info "Install: /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" + fi + echo "" +fi + +# ── 1. Node.js (PptxGenJS runs on Node) ── +echo "--- Node.js ---" +if command -v node &>/dev/null; then + NODE_VER=$(node --version) + ok "node ($NODE_VER)" +else + fail "node not found (required — PPTX generation uses PptxGenJS on Node)" + case "$OS" in + Darwin) info "Install: brew install node" ;; + Linux) info "Install: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -" + info " sudo apt install -y nodejs" ;; + *) info "Install: https://nodejs.org/" ;; + esac +fi + +# ── 2. npm ── +echo "" +echo "--- npm ---" +if command -v npm &>/dev/null; then + NPM_VER=$(npm --version 2>/dev/null) + ok "npm ($NPM_VER)" +else + fail "npm not found" + case "$OS" in + Darwin) info "Install: brew install node (includes npm)" ;; + Linux) info "Install: comes with nodejs" ;; + *) info "Install: https://nodejs.org/" ;; + esac +fi + +# ── 3. npm package: pptxgenjs ── +echo "" +echo "--- npm Packages ---" +if node -e "require('pptxgenjs')" 2>/dev/null || npm list -g pptxgenjs &>/dev/null; then + PPTX_VER=$(node -e "try{console.log(require('pptxgenjs/package.json').version)}catch(e){console.log('installed')}" 2>/dev/null) + ok "pptxgenjs ($PPTX_VER)" +else + fail "pptxgenjs not installed" + info "Install: npm install -g pptxgenjs" + echo "" + if [ -t 0 ]; then + read -p " Install now? [Y/n] " -n 1 -r REPLY + echo "" + REPLY=${REPLY:-Y} + else + warn "Non-interactive mode — skipping auto-install." + REPLY=N + fi + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + npm install -g pptxgenjs 2>/dev/null && ok "Installed: pptxgenjs" || fail "npm install failed" + fi +fi + +# ── 4. Python 3 (OOXML validation scripts) ── +echo "" +echo "--- Python (OOXML validation) ---" +if command -v python3 &>/dev/null; then + PY_VER=$(python3 --version 2>&1) + ok "python3 ($PY_VER)" + if [ "$OS" = "Darwin" ]; then + PY_PATH=$(which python3 2>/dev/null) + if [[ "$PY_PATH" == "/usr/bin/python3" ]]; then + warn "Using macOS system Python (limited). Recommend: brew install python3" + fi + fi +else + fail "python3 not found" + case "$OS" in + Darwin) info "Install: brew install python3" ;; + Linux) info "Install: sudo apt install python3 python3-pip (Debian/Ubuntu)" + info " sudo dnf install python3 python3-pip (Fedora/RHEL)" ;; + *) info "Install: https://www.python.org/downloads/" ;; + esac +fi + +# ── 5. pip ── +echo "" +echo "--- pip ---" +if python3 -m pip --version &>/dev/null 2>&1; then + PIP_VER=$(python3 -m pip --version 2>/dev/null | head -1) + ok "pip ($PIP_VER)" +else + fail "pip not found" + case "$OS" in + Darwin) info "Install: python3 -m ensurepip --upgrade" + info " or: brew install python3 (includes pip)" ;; + Linux) info "Install: sudo apt install python3-pip (Debian/Ubuntu)" ;; + *) info "Install: python3 -m ensurepip --upgrade" ;; + esac +fi + +# ── 6. Python packages ── +echo "" +echo "--- Python Packages ---" +PY_PKGS=( + "lxml:lxml" + "defusedxml:defusedxml" +) + +MISSING_PY=() +for entry in "${PY_PKGS[@]}"; do + mod="${entry%%:*}" + pkg="${entry##*:}" + if python3 -c "import $mod" 2>/dev/null; then + ver=$(python3 -c "import $mod; print(getattr($mod, '__version__', 'installed'))" 2>/dev/null) + ok "$pkg ($ver)" + else + fail "$pkg not installed" + MISSING_PY+=("$pkg") + fi +done + +if [ ${#MISSING_PY[@]} -gt 0 ]; then + echo "" + if [ -t 0 ]; then + read -p " Install missing Python packages? [Y/n] " -n 1 -r REPLY + echo "" + REPLY=${REPLY:-Y} + else + warn "Non-interactive mode — skipping auto-install. Run interactively or install manually." + REPLY=N + fi + if [[ ! $REPLY =~ ^[Nn]$ ]]; then + python3 -m pip install -q "${MISSING_PY[@]}" 2>/dev/null \ + || python3 -m pip install -q --user "${MISSING_PY[@]}" 2>/dev/null \ + || python3 -m pip install -q --break-system-packages "${MISSING_PY[@]}" 2>/dev/null \ + || { fail "pip install failed. Try manually: pip install ${MISSING_PY[*]}"; } + ok "Installed: ${MISSING_PY[*]}" + fi +fi + +# ── 7. Tectonic (optional, for Beamer/LaTeX slides) ── +echo "" +echo "--- Tectonic (optional, Beamer slides only) ---" +if command -v tectonic &>/dev/null; then + TEC_VER=$(tectonic --version 2>&1 | head -1) + ok "tectonic ($TEC_VER)" +elif [ -x "$HOME/tectonic" ]; then + ok "tectonic (~/tectonic)" +else + warn "tectonic not installed (needed only for Beamer/LaTeX slide PDFs)" + case "$OS" in + Darwin) info "Install: brew install tectonic" ;; + Linux) info "Install: conda install -c conda-forge tectonic" + info " or: curl -fsSL https://drop-sh.fullyjustified.net | sh" ;; + MINGW*|MSYS*|CYGWIN*) info "Install: scoop install tectonic" ;; + esac +fi + +# ── Summary ── +echo "" +echo "============================================" +echo " Setup complete." +echo " Core: Node.js + pptxgenjs" +echo " Validation: Python + lxml + defusedxml" +echo " Beamer slides: tectonic (optional)" +echo "============================================" diff --git a/skills/ppt/themes.md b/skills/ppt/themes.md new file mode 100755 index 0000000..f0f95d5 --- /dev/null +++ b/skills/ppt/themes.md @@ -0,0 +1,581 @@ +# Themes — Preset Theme Configuration + +Each theme = a set of colors + a set of fonts + mood tags. Once a theme is selected, all colors are automatically derived according to the color scale rules in `design-system.md`. + +--- + +## Theme Selection Rules + +1. AI selects the theme based on scene classification and content tone +2. Each PPT deck uses only one theme, **no switching throughout** +3. If the user specifies a color or style preference, select the closest theme or customize accordingly (still must follow color scale rules) + +--- + +## Preset Themes + +### 1. Ocean (Deep Sea Blue) + +| Property | Value | +|------|-----| +| **Mood** | Professional, trustworthy | +| **Suitable for** | Work reports, business proposals, company introductions | +| **Primary** | `#1B2A4A` | +| **Accent (A — default)** | `#2A9D8F` (teal) | +| **Accent B** | `#D7A460` (warm gold) | +| **Accent C** | `#6C92E0` (bright blue) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Gill Sans MT Bold | +| **Latin Body Font** | Gill Sans MT | +| **Image Keywords** | ocean, deep blue, corporate, skyline, technology, water, aerial city | +| **Mask Color** | `rgba(18,32,64,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | Usage Example | +|------|------|---------| +| primary-100 | `#0D1525` | Dark mode background | +| primary-90 | `#122040` | Title bar background | +| primary-80 | `#1B2A4A` | Title text | +| primary-60 | `#3D5A80` | Subtitle / icons | +| primary-40 | `#6B8AB0` | Secondary text | +| primary-20 | `#B0C4DE` | Light decoration | +| primary-10 | `#DFE8F2` | Card background / border | +| primary-5 | `#F0F4F8` | Surface base color | + +--- + +### 2. Graphite (Graphite Gray) + +| Property | Value | +|------|-----| +| **Mood** | Modern, restrained, clean and sharp | +| **Suitable for** | Tech products, weekly/monthly reports, technical sharing | +| **Primary** | `#2D3436` | +| **Accent (A — default)** | `#D77860` (coral red) | +| **Accent B** | `#6BCB77` (emerald green) | +| **Accent C** | `#9B8EC4` (light purple) | +| **CJK Title Font** | SimHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Century Gothic Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | architecture, minimal, concrete, technology, geometric, monochrome, server room | +| **Mask Color** | `rgba(34,40,41,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | +|------|------| +| primary-100 | `#161A1B` | +| primary-90 | `#222829` | +| primary-80 | `#2D3436` | +| primary-60 | `#576163` | +| primary-40 | `#8D9496` | +| primary-20 | `#C3C8CA` | +| primary-10 | `#E4E7E8` | +| primary-5 | `#F3F4F5` | + +--- + +### 3. Forest (Forest Green) + +| Property | Value | +|------|-----| +| **Mood** | Natural, serene, organic and gentle | +| **Suitable for** | Educational courseware, humanities/liberal arts academic, ESG/sustainability | +| **Primary** | `#1B4332` | +| **Accent (A — default)** | `#DCBD74` (golden) | +| **Accent B** | `#E18C70` (coral) | +| **Accent C** | `#7EC8B0` (mint) | +| **CJK Title Font** | SimSun Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Palatino Linotype Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | forest, nature, leaves, green, organic, sustainability, botanical, meadow | +| **Mask Color** | `rgba(18,45,31,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | +|------|------| +| primary-100 | `#0B1E14` | +| primary-90 | `#122D1F` | +| primary-80 | `#1B4332` | +| primary-60 | `#2E6B4F` | +| primary-40 | `#60A882` | +| primary-20 | `#A8D4BC` | +| primary-10 | `#D8EEE2` | +| primary-5 | `#EEF7F1` | + +--- + +### 4. Twilight (Dark Night Purple) + +| Property | Value | +|------|-----| +| **Mood** | Creative, unique, personality-driven | +| **Suitable for** | Creative proposals, brand planning, design sharing | +| **Primary** | `#2D2B55` | +| **Accent (A — default)** | `#9B50F5` (electric purple) | +| **Accent B** | `#E3B27D` (warm orange) | +| **Accent C** | `#5EC4D4` (sky blue) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Candara Bold | +| **Latin Body Font** | Candara | +| **Image Keywords** | galaxy, night sky, aurora, creative, neon, abstract art, purple gradient | +| **Mask Color** | `rgba(33,31,64,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | +|------|------| +| primary-100 | `#15142A` | +| primary-90 | `#211F40` | +| primary-80 | `#2D2B55` | +| primary-60 | `#555380` | +| primary-40 | `#8886AB` | +| primary-20 | `#BFBED6` | +| primary-10 | `#E2E1ED` | +| primary-5 | `#F2F2F7` | + +--- + +### 5. Sandstone (Warm Sand) + +| Property | Value | +|------|-----| +| **Mood** | Premium, warm, composed and grand | +| **Suitable for** | Investment roadshows, real estate, luxury goods, finance | +| **Primary** | `#3C2415` | +| **Accent (A — default)** | `#C09E30` (gold) | +| **Accent B** | `#D4766A` (ochre red) | +| **Accent C** | `#8BB174` (olive) | +| **CJK Title Font** | SimSun Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Palatino Linotype Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | luxury, gold, marble, architecture, warm light, classical, elegant interior | +| **Mask Color** | `rgba(45,27,16,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | +|------|------| +| primary-100 | `#1E120A` | +| primary-90 | `#2D1B10` | +| primary-80 | `#3C2415` | +| primary-60 | `#6B5443` | +| primary-40 | `#9E8A7A` | +| primary-20 | `#CEBFB5` | +| primary-10 | `#E9E2DC` | +| primary-5 | `#F5F2F0` | + +--- + +### 6. Mono (Minimalist Black) + +| Property | Value | +|------|-----| +| **Mood** | Sharp, minimalist, visual impact | +| **Suitable for** | Product launches, minimalist style, photography showcases | +| **Primary** | `#1A1A1A` | +| **Accent (A — default)** | `#E6754C` (burnt orange) | +| **Accent B** | `#4ECDC4` (cyan) | +| **Accent C** | `#C44569` (rose) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Arial Bold | +| **Latin Body Font** | Arial | +| **Image Keywords** | minimal, black white, contrast, product, clean, abstract, studio lighting | +| **Mask Color** | `rgba(20,20,20,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | +|------|------| +| primary-100 | `#0A0A0A` | +| primary-90 | `#141414` | +| primary-80 | `#1A1A1A` | +| primary-60 | `#4A4A4A` | +| primary-40 | `#808080` | +| primary-20 | `#B8B8B8` | +| primary-10 | `#E0E0E0` | +| primary-5 | `#F2F2F2` | + +--- + +### 7. Deep Mineral (Dark Night Blue) + +| Property | Value | +|------|-----| +| **Mood** | High-end, immersive, cinematic depth | +| **Suitable for** | Tech launches, keynotes, data dashboards, night-mode presentations | +| **Primary** | `#FFFFFF` | +| **Accent (A — default)** | `#44D1E4` (electric cyan) | +| **Accent B** | `#EDCD82` (warm yellow) | +| **Accent C** | `#E65174` (rose) | +| **Background Override** | `#162235` (replaces default `#FFFFFF`) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Trebuchet MS Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | dark blue, night city, deep ocean, space, data visualization, neon glow | +| **Mask Color** | `rgba(16,26,40,0.70)` (primary-100 based) | + +**Dark theme notes**: This is a dark-background theme — the color logic is inverted: +- `background` = `#162235` (dark), `surface` = `#1E2D45` (slightly lighter), `surface-card` = `#243550` (card layer) +- `text-primary` = `#FFFFFF`, `text-secondary` = `rgba(255,255,255,0.75)`, `text-muted` = `rgba(255,255,255,0.45)` +- Cards use `surface-card` with no shadow (shadows invisible on dark backgrounds) +- Accent (`#44D1E4`) is used for highlights, KPI numbers, accent bars, and emphasis elements + +Color Scale (inverted — lighter values are "darker" in visual weight): + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#0D1525` | Deepest background | +| primary-90 | `#122040` | Title bar background | +| primary-80 | `#FFFFFF` | Title text / primary elements | +| primary-60 | `rgba(255,255,255,0.75)` | Subtitle / secondary text | +| primary-40 | `rgba(255,255,255,0.45)` | Muted text / annotations | +| primary-20 | `#2A3F5F` | Card borders / subtle decorations | +| primary-10 | `#1E2D45` | Surface / content area base | +| primary-5 | `#162235` | Page background | + +--- + +### 8. Warm Retro (Vintage Warmth) + +| Property | Value | +|------|-----| +| **Mood** | Vintage, cultured, warm and grounded | +| **Suitable for** | Cultural events, humanities, food/lifestyle, brand storytelling | +| **Primary** | `#2A4A3A` | +| **Accent (A — default)** | `#C89F62` (antique gold) | +| **Accent B** | `#C06C58` (copper red) | +| **Accent C** | `#7BA68C` (moss green) | +| **Background Override** | `#F4F1E9` (warm off-white, replaces default `#FFFFFF`) | +| **CJK Title Font** | SimSun Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Palatino Linotype Bold | +| **Latin Body Font** | Garamond | +| **Image Keywords** | vintage, warm light, craft, leather, coffee, botanical, old book, linen texture | +| **Mask Color** | `rgba(30,40,34,0.75)` (primary-90 based) | + +**Warm background notes**: This theme uses a warm off-white (`#F4F1E9`) instead of pure white: +- `background` = `#F4F1E9`, `surface` = `#EDE9DF` (slightly darker warm), `surface-card` = `#FEFDFB` (near-white card) +- The warm base gives a cozy, analog feel — avoid pure white elements that would look jarring + +Color Scale: + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#0F1C15` | Darkest (rarely used) | +| primary-90 | `#1E2822` | Title bar background | +| primary-80 | `#2A4A3A` | Title text / primary elements | +| primary-60 | `#4A7A62` | Subtitle / icons | +| primary-40 | `#7AAA90` | Secondary text | +| primary-20 | `#B5D4C2` | Light decoration | +| primary-10 | `#DCE9E0` | Card background / border | +| primary-5 | `#F0F5F1` | Surface base (use `#EDE9DF` for warm surface) | + +--- + +### 9. Deep Forest (Dark Green) + +| Property | Value | +|------|-----| +| **Mood** | Calm, grounded, nature-tech fusion | +| **Suitable for** | ESG reports, sustainability, ecology, green tech | +| **Primary** | `#FFFFFF` | +| **Accent (A — default)** | `#D7D968` (electric lime) | +| **Accent B** | `#E18C70` (coral orange) | +| **Accent C** | `#85C5E0` (sky blue) | +| **Background Override** | `#193328` (deep forest green, replaces default `#FFFFFF`) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Gill Sans MT Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | dark forest, moss, fern, green nature, sustainability, eco technology, rain forest | +| **Mask Color** | `rgba(18,38,30,0.70)` (primary-100 based) | + +**Dark theme notes**: Same inverted logic as Deep Mineral — see its notes above. +- `background` = `#193328`, `surface` = `#1F3F32`, `surface-card` = `#264A3C` +- `text-primary` = `#FFFFFF`, `text-secondary` = `rgba(255,255,255,0.75)`, `text-muted` = `rgba(255,255,255,0.45)` + +Color Scale (inverted): + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#0D1A14` | Deepest background | +| primary-90 | `#12261E` | Title bar background | +| primary-80 | `#FFFFFF` | Title text / primary elements | +| primary-60 | `rgba(255,255,255,0.75)` | Subtitle / secondary text | +| primary-40 | `rgba(255,255,255,0.45)` | Muted text / annotations | +| primary-20 | `#2A5040` | Card borders / subtle decorations | +| primary-10 | `#1F3F32` | Surface / content area base | +| primary-5 | `#193328` | Page background | + +--- + +### 10. Coral (Coral Pink) + +| Property | Value | +|------|-----| +| **Mood** | Warm, vibrant, youthful | +| **Suitable for** | Brand marketing, consumer products, lifestyle, female-audience presentations | +| **Primary** | `#8B3A4A` | +| **Accent (A — default)** | `#E2A574` (warm peach) | +| **Accent B** | `#DC755B` (burnt sienna) | +| **Accent C** | `#6BC5B8` (mint contrast) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Candara Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | pink, flowers, beauty, lifestyle, warm light, cafe, fashion, soft texture | +| **Mask Color** | `rgba(100,42,54,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#3D1A22` | Deepest background | +| primary-90 | `#642A36` | Title bar background | +| primary-80 | `#8B3A4A` | Title text / primary elements | +| primary-60 | `#B06070` | Subtitle / icons | +| primary-40 | `#D08A96` | Secondary text | +| primary-20 | `#E8BAC2` | Light decoration | +| primary-10 | `#F3DDE1` | Card background / border | +| primary-5 | `#FAF0F2` | Surface base color | + +--- + +### 11. Mint (Mint Green) + +| Property | Value | +|------|-----| +| **Mood** | Fresh, clean, health-oriented | +| **Suitable for** | Healthcare, wellness, eco-friendly, food & beverage | +| **Primary** | `#1A6B5A` | +| **Accent (A — default)** | `#E3987D` (warm coral) | +| **Accent B** | `#DEC363` (golden yellow) | +| **Accent C** | `#8574E2` (medium slate blue) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Gill Sans MT Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | mint, fresh, health, botanical, clean, green smoothie, spa, herbal | +| **Mask Color** | `rgba(18,78,64,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#0A3028` | Deepest background | +| primary-90 | `#124E40` | Title bar background | +| primary-80 | `#1A6B5A` | Title text / primary elements | +| primary-60 | `#3D9B86` | Subtitle / icons | +| primary-40 | `#6DBFAB` | Secondary text | +| primary-20 | `#A8DACE` | Light decoration | +| primary-10 | `#D6EDE7` | Card background / border | +| primary-5 | `#EFF8F5` | Surface base color | + +--- + +### 12. Azure (Sky Blue) + +| Property | Value | +|------|-----| +| **Mood** | Open, lightweight, approachable technology | +| **Suitable for** | SaaS products, AI/tech, onboarding, training materials | +| **Primary** | `#1E5F8C` | +| **Accent (A — default)** | `#FF6B2B` (electric orange) | +| **Accent B** | `#DD5F5F` (red) | +| **Accent C** | `#9270E1` (violet) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Trebuchet MS Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | sky, cloud, technology, light blue, software, digital, clean interface | +| **Mask Color** | `rgba(20,68,104,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#0C2A40` | Deepest background | +| primary-90 | `#144468` | Title bar background | +| primary-80 | `#1E5F8C` | Title text / primary elements | +| primary-60 | `#4085B0` | Subtitle / icons | +| primary-40 | `#70AAD0` | Secondary text | +| primary-20 | `#A8CEE5` | Light decoration | +| primary-10 | `#D8E9F3` | Card background / border | +| primary-5 | `#F0F6FA` | Surface base color | + +--- + +### 13. Ember (Amber Red) + +| Property | Value | +|------|-----| +| **Mood** | Bold, energetic, high-impact | +| **Suitable for** | Annual events, product launches, sales campaigns, motivational presentations | +| **Primary** | `#8B2500` | +| **Accent (A — default)** | `#F04828` (fire orange) | +| **Accent B** | `#2EC4B6` (teal contrast) | +| **Accent C** | `#EC7979` (coral red) | +| **CJK Title Font** | SimHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Arial Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | fire, red, energy, stage light, celebration, bold, dynamic, explosion | +| **Mask Color** | `rgba(100,26,0,0.75)` (primary-90 based) | + +Color Scale: + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#3E1000` | Deepest background | +| primary-90 | `#641A00` | Title bar background | +| primary-80 | `#8B2500` | Title text / primary elements | +| primary-60 | `#B85030` | Subtitle / icons | +| primary-40 | `#D88060` | Secondary text | +| primary-20 | `#EBB8A0` | Light decoration | +| primary-10 | `#F5DCD0` | Card background / border | +| primary-5 | `#FBF0EB` | Surface base color | + +--- + +### 14. Volt (Electric / Startup Dark) + +| Property | Value | +|------|-----| +| **Mood** | Electric, aggressive, high-energy | +| **Suitable for** | AI/tech product launches, startup pitches, youth tech brands, digital innovation | +| **Primary** | `#FFFFFF` | +| **Accent (A — default)** | `#00F5A0` (electric mint-green) | +| **Accent B** | `#FF5E3A` (electric orange) | +| **Accent C** | `#A855F7` (electric purple) | +| **Background Override** | `#080B14` (near-black with blue tint, replaces default `#FFFFFF`) | +| **CJK Title Font** | Microsoft YaHei Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Trebuchet MS Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | neon, circuit board, futuristic, dark ui, electric glow, startup, code, data stream | +| **Mask Color** | `rgba(5,7,16,0.70)` (primary-100 based) | + +**Dark theme notes**: Same inverted logic as Deep Mineral — see its notes above. +- `background` = `#080B14`, `surface` = `#0F1424`, `surface-card` = `#162033` +- `text-primary` = `#FFFFFF`, `text-secondary` = `rgba(255,255,255,0.75)`, `text-muted` = `rgba(255,255,255,0.45)` +- Accents are extremely vivid by design — use sparingly (bars, numbers, key labels only) + +Color Scale (inverted): + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#050710` | Deepest background | +| primary-90 | `#0D1320` | Title bar background | +| primary-80 | `#FFFFFF` | Title text / primary elements | +| primary-60 | `rgba(255,255,255,0.75)` | Subtitle / secondary text | +| primary-40 | `rgba(255,255,255,0.45)` | Muted text / annotations | +| primary-20 | `#1E2E48` | Card borders / subtle decorations | +| primary-10 | `#0F1424` | Surface / content area base | +| primary-5 | `#080B14` | Page background | + +--- + +### 15. Vermillion (Chinese Red) + +| Property | Value | +|------|-----| +| **Mood** | Ceremonial, festive, culturally resonant | +| **Suitable for** | Chinese cultural events, Spring Festival, traditional ceremonies, brand storytelling with Chinese aesthetics | +| **Primary** | `#8B1A1A` | +| **Accent (A — default)** | `#D4AF37` (Chinese gold) | +| **Accent B** | `#C4762A` (copper bronze) | +| **Accent C** | `#2D6B8A` (cool blue contrast) | +| **Background Override** | `#FDF8F2` (warm rice-paper white, replaces default `#FFFFFF`) | +| **CJK Title Font** | SimSun Bold | +| **CJK Body Font** | Microsoft YaHei | +| **Latin Title Font** | Palatino Linotype Bold | +| **Latin Body Font** | Corbel | +| **Image Keywords** | red lantern, Chinese architecture, traditional culture, festival, gold ornament, calligraphy, temple, Spring Festival | +| **Mask Color** | `rgba(107,20,20,0.75)` (primary-90 based) | + +**Warm background notes**: Uses warm rice-paper white `#FDF8F2` instead of pure white: +- `background` = `#FDF8F2`, `surface` = `#F5EDE0` (warm tint), `surface-card` = `#FEFDFB` (near-white card) +- Avoid pure white elements that would look jarring against the warm base + +Color Scale: + +| Scale | Color Value | Usage | +|------|------|---------| +| primary-100 | `#450D0D` | Deepest background / dramatic pages | +| primary-90 | `#6B1414` | Title bar background | +| primary-80 | `#8B1A1A` | Title text / primary elements | +| primary-60 | `#B84040` | Subtitle / icons | +| primary-40 | `#D57070` | Secondary text | +| primary-20 | `#EDB0B0` | Light decoration | +| primary-10 | `#F8E0E0` | Card background / border | +| primary-5 | `#FDF4F4` | Surface base color | + +--- + +## Custom Themes + +If the user specifies a particular color, create a custom theme following these steps: + +1. Use the user-specified color as `primary-80` +2. Derive the full 8-level color scale according to the color scale generation rules in section 3.1 of `design-system.md` +3. Select an Accent color with a hue difference of ≥ 30° from the Primary (to ensure visual distinction). Recommended range: 60°–180°. Provide 3 accent variants (A/B/C) for variety. +4. Choose the most suitable font pairing from the presets, or use the user-specified fonts +5. Record the complete configuration in comments within the generated code to ensure consistency across the entire PPT deck + +## Font Pairing Notes + +Each theme has **4 font roles**: + +| Role | HTML font-family Syntax | Usage | +|------|----------------------|------| +| CJK Title | Write PPT font name directly: `font-family:'SimHei','Microsoft YaHei',sans-serif` | h1, h2, cover title | +| CJK Body | Write PPT font name directly: `font-family:'Microsoft YaHei',sans-serif` | body, lists, annotations | +| Latin Title | Write PPT font name directly: `font-family:'Century Gothic','Corbel',sans-serif` | Pure Latin character titles | +| Latin Body | Write PPT font name directly: `font-family:'Corbel',sans-serif` | Pure Latin character body text | + +**v3 Smart Font Mapping**: html2pptx.js automatically handles fonts: +- **PPT-safe fonts** (SimHei, Corbel, Palatino Linotype, etc. 40+) → Passed through directly, preserved as-is +- **macOS-exclusive fonts** (PingFang, Hiragino) → Automatically mapped to cross-platform equivalent fonts +- **Web fonts** (Roboto, Montserrat, Inter, etc.) → Automatically mapped to the visually closest PPT-safe font +- **fontConfig still available**: Serves as the final fallback default for CJK/Latin + +**fontConfig can be passed when calling html2pptx()** (optional, used to override default fallback): +```javascript +const fontConfig = { cjk: 'SimHei', latin: 'Corbel' }; +``` + +**Recommended practice**: Write the target PPT font name directly in HTML; do not rely on fontConfig for switching. + +**PPT-safe Chinese fonts**: Microsoft YaHei / SimHei / SimSun / KaiTi / FangSong / DengXian +**PPT-safe English fonts**: Corbel / Arial / Times New Roman / Palatino Linotype / Gill Sans MT / Century Gothic / Garamond / Rockwell / Candara / Constantia / Cambria / Trebuchet MS + +--- + +## Dark Mode Semantic Color Mapping + +For themes that need a full dark-mode deck (not just individual dark slides), use these semantic overrides: + +| Semantic Token | Light Mode Value | Dark Mode Value | +|---------------|-----------------|-----------------| +| `background` | `#FFFFFF` | `primary-100` | +| `surface` | `primary-5` | `primary-90` | +| `surface-card` | `#FFFFFF` | `primary-80` | +| `text-primary` | `primary-80` | `#FFFFFF` | +| `text-secondary` | `primary-60` | `rgba(255,255,255,0.75)` | +| `text-muted` | `primary-40` | `rgba(255,255,255,0.45)` | +| `border` | `primary-10` | `primary-60` | +| `on-dark` | `#FFFFFF` | `#FFFFFF` (unchanged) | + +Deep Mineral and Deep Forest themes already use dark mode by default. For any other theme, apply this mapping to create a dark variant. + +Cards in dark mode use `surface-card` background with **no shadow** (shadows invisible on dark backgrounds). Use subtle `border: 1pt solid primary-60` instead for card definition. diff --git a/skills/qingyan-research/SKILL.md b/skills/qingyan-research/SKILL.md new file mode 100755 index 0000000..8008354 --- /dev/null +++ b/skills/qingyan-research/SKILL.md @@ -0,0 +1,294 @@ +--- +name: qingyan_research_report +description: "Deep web research and HTML report generation. When GLM needs to conduct systematic information gathering and analysis for: (1) Exploring open-ended questions through multi-step search, deep reading, and logical reasoning, (2) Applying critical thinking and dynamic reflection to optimize search strategies and ensure information coverage, (3) Generating publication-quality HTML research reports with specific UI/UX standards (typography, colors, layout), (4) Creating interactive data visualizations (Chart.js) based on extracted statistical data, (5) Producing structured documents with automatic Table of Contents and responsive design." +--- +你是 **GLM**,一位具备**批判性思维、系统性探索能力与结构化表达能力**的高级网络研究智能体。你的任务是围绕通用开放性问题,通过搜索、深度阅读与逐步推理,开展系统化信息收集与分析,最终产出一篇**结构清晰、语义深刻、表达专业且视觉美观的 HTML 研究报告**。 + + +--- + + +### 一、思考准则 + +#### 1. 思考驱动的信息探索 + + +在执行每一轮信息收集行动(如发起搜索、访问网页等)之前,你必须首先进行深入的任务分析与策略制定。你的思考内容需包括: + + +* 对当前信息状态的完整性、权威性与时效性评估 +* 将用户问题拆解为多层次子问题,并识别缺失的关键信息 +* 明确接下来应聚焦的关键主题与相应关键词,并给出搜索与访问策略 +* 制定探索路径,说明哪些页面需要优先访问、哪些部分需重点提取 +* 在此基础上,结合反思机制动态调整任务推进方向 + + +#### 2. 动态反思与策略修正 + + +在任务推进过程中,应适时在思考中进行反思与策略调整,以确保信息探索的深度与方向持续优化。反思内容可聚焦以下任一方面: + + +* **问题覆盖检查(Question Coverage)**:当前是否已全面回应用户关切的核心问题?是否仍有未触及的关键角度或遗漏的子问题? +* **内容深度评估(Content Depth Reflection)**:现有信息是否具备足够的逻辑深度、数据支持与推理展开?是否存在内容空洞或片面性? +* **信息拓展建议(Information Supplementation)**:是否存在虽未被显式提出,但对理解问题具有价值的潜在方向、边界扩展或补充数据? + + +--- + + +### 二、搜索工具 + + +你可以使用加载外部skills中的搜索工具来系统性地获取信息,支持研究任务的深入推进: + + +- search:用于发起单轮全面精准的网页检索,以获取覆盖核心问题的权威来源。 + + +- visit:访问指定网页,提取首页的主要内容以供后续分析。 + +--- + + +### 三、HTML 报告生成规范 + + +最终,当收集到足够充分的信息后,调用`generate_html`工具,输出一份具备出版级品质的 HTML 研究报告。 + +generate_html工具使用说明: +python3 generate_html.py --title "Report Title" <<'EOF' +<!DOCTYPE html> +<html> +...[Full HTML Content]... +</html> +EOF + +Parameters Description: +Report Title: The level-1 heading of the report, also used as the filename. +Full HTML Content: The complete, self-contained HTML source code (including embedded CSS). + + +HTML格式需满足以下要求: + +#### 1. 主题化设计与风格要求 + + +**1. 总体布局与氛围:** + * **页面背景:** 纯白 (`#FFFFFF`), 页面背景必须覆盖整个页面。 + * **内容区域:** 纯白 (`#FFFFFF`),确保与文本的最大对比度。 + * **主文字色:** 近黑色 (`#212529`)。 + * **文本强调色A:** 用于目录、链接、使用蓝色 (`#0D6EFD`)。 + * **文本强调色B:** 用于关键高亮以及文本中加粗字体、使用黑色(`#212529`) + * **文本强调色C:** 用于标题装饰、使用黑色(`#212529`) + * **body设置:** 不要用display: flex设定。 + + +**2. 字体与排版:** + 标题 (Headings): "Alibaba PuHuiTi 3.0", "Noto Sans SC", "Noto Serif SC", sans-serif + 正文 (Body): "Alibaba PuHuiTi 3.0", "Noto Serif SC", serif + 代码 (Code): "Source Code Pro", monospace + 字号: + 正文: `16px` + H1 标题: font-size: 28px;margin-top: 24px;margin-bottom: 20px + H2 标题: font-size: 22px;padding-bottom: 0.4em; + H3 标题: font-size: 20px; + H4 标题: font-size: 18px; + 脚注/图表说明: margin-bottom: 1.2em; + + +**3. 其他元素:** + 当进行列举具体示例和行程安排时,适当用组件对示例和安排进行分组。正常文本不需要单独增加模块分组。 + + +1. **标题:** + * `<h1>` 居中;`<h2>` 标题前添加装饰元素,样式为:14px圆形,颜色使用: 文本强调色A(`#0D6EFD`)。 + + +2. **表格:** + * 摒弃传统边框。 + * `thead` 下方 `2px` 主题强调色。 + * `tbody tr:hover` 背景取主题明度 +5%。 + + +3. **引用:** + * 左侧竖条使用主题强调色。 + + +4. **文本主题背景:** + * 设置页面container,包含所有文本避免文本内容超出页面容器。 + * 确保背景长度可以包含所有文本,不要出现文本超出背景的情况。 + + +5. **分隔线:** + * 使用主题强调色。 + + +6. **目录生成:** + 在第一层 `<h1>` 标题 **之后** 自动插入 `Table of Contents`(名称为目录(保持和文本语种一致))模块,其生成规则如下: + 1. **范围与层级:** 仅收集文档中出现的所有 `<h2>` 及其紧随其后的 `<h3>` 子标题(直到下一个 `<h2>` 前)。 + 2. **结构:** + ```html + <nav class="toc"> + <ul class="toc-level-2"> + <li><a href="#section-1">H2 标题文本</a> + <ul class="toc-level-3"> + <li><a href="#section-1-1">H3 标题文本</a></li> + ... + </ul> + </li> + ... + </ul> + </nav> + ``` + * **所有目录级别 (`<li>`) 的标题文本必须包裹在 `<a>` 标签中,确保点击即可跳转到对应的 `<h2>` 或 `<h3>`。** + 3. **锚点生成:** 给每个 `<h2>`、`<h3>` 添加唯一 `id`(可使用标题文本的 slug 形式,全部小写,去除特殊字符)。目录中的 `href` 指向对应的 `#id`,实现点击跳转。 + 4. **样式要求:** + * 目录整体放在纯白内容区域中,与正文保持 `margin-bottom: 2em`。 + * `.toc-level-2 > li` 使用数字或圆点标识;嵌套 `.toc-level-3` 使用缩进列表。 + * 全部目录(序号和标题)颜色使用 **文本强调色** `#0D6EFD`,悬停时下划线, 适当添加缩进。 + 5. **序号格式:** + * 先检原文本标题是否包含序号(阿拉伯数字、中文数字、第一、第二、第三等),若包含则直接使用原文本标题中的序号。 + * 若不包含序号,则根据文档主要语言为中文(根据 `<h1>`/`<h2>` 含有中文字符判断),则在目录中为每个 `<h2>` 添加中文序号前缀:`一、`、`二、`、`三、`……;其对应的 `<h3>` 列表项不再重复序号,仅作为缩进子项显示。 + * 若不包含序号,则根据文档主要语言为非中文(根据 `<h1>`/`<h2>` 含有中文字符判断),则在目录中为每个 `<h2>` 添加阿拉伯数字加点形式:`1.`、`2.`、`3.` ……;其对应的 `<h3>` 列表项不再重复序号,仅作为缩进子项显示。 + * 序号仅在目录中显示,不修改正文标题本身。 + 6. **可折叠(可选):** 如目录过长,可为每个 `<li>` 添加 `details/summary` 结构,实现折叠展开,但默认展开即可。 + + +7. **智能图表生成** + + + * **图表生成要求** + * 数据多时,采用组合图,在一张图表中展现出全面的数据。 + * 图表种类尽量多元化,不要大量重复使用一种图表格式。 + + + * **触发条件:** + * **数据比较:** 文本中包含多组数据的直接对比(如 "A组的结果是25%,而B组是40%")。 + * **趋势描述:** 描述了某个变量随时间的变化(如 "2024年时,A组25%,2023年时20%")。 + * **分布或构成:** 展示一个整体中各个部分的百分比构成(如 "30%为男性,70%为女性")。 + * **数据密集的表格:** 表格展示了精确数据,但趋势或比较更适合用图表表达。 + + + * **解析需求** + * 图表类型(柱状、折线、条形图、组合图等) + * 比较主体、时间范围及指标 + * 禁止生成环形图 + + + * **数据处理** + * 依据解析结果,根据上下文进行数据搜集处理。 + + + * **生成 Chart.js 图表(保持与主题语种一致)** + * 使用 Chart.js 绘制图表(防止打印 PDF 被截断) + + + * **坐标轴 / 文字** + * 文字使用主文字色 `#212529`,指定字体 + * 调整 x/y 轴名称字体及标题字体,避免超出图表空间 + * y 轴最大值应为数据最大值的 1.2 倍 + * 网格线使用辅助色 `#E9ECEF`,虚线显示 + * 图表宽高自适应,节点不超边界 + * 节点间自动计算间距,避免重叠 + * 长文本自动换行或缩小字体 + * 柱状图应从下向上绘制 + + + * **数据元素绘制** + * 元素尺寸和位置必须精确计算 + + + * **图例绘制** + * 图例图标与文本保持间距,避免重叠 + * 除组合图外,不允许任何元素重叠(如 x/y 轴标题与数据名称重叠) + + + * **颜色规范** + * 图形使用主题强调色 `#0D6EFD` + * 多图形并存使用对比色(如绿色、橙色),颜色带透明度 + * 所有文字使用主文字色 `#212529` + + + * **图表注释** + * 注释清晰、具体 + * 注释与主题语种一致 + * 图表与注释文字用不同容器 + * 示例:图2:2021年主要石化仓储上市公司毛利率对比 + + + * **图表互动模块生成要求** + * 添加交互提示(鼠标悬停显示信息) + + + * **代码示例:** + ``` + function createChart(ctx, config) { + if (ctx) { + new Chart(ctx, config); + } + } + + + createChart(growthCtx, { + type: 'bar', + data: { + labels: growthData.years, + datasets: [ + { + label: '', + data: , + yAxisID: 'y', + backgroundColor: 'rgba(59, 130, 246, 0.5)', + borderColor: 'rgba(59, 130, 246, 1)', + borderWidth: 1 + } + // ... + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + scales: { + y: { + type: 'logarithmic', + position: 'left', + title: { + display: true, + text: '...' + } + } + }, + plugins: { + tooltip: { + mode: 'index', + intersect: false + }, + title: { + display: false + } + } + } + }); + ``` + + + * **背景 / 网格线** + * 使用页面背景色或辅助色:`#F8F9FA`、`#E9ECEF` + + + * **嵌入 HTML** + * 使用 `<figure class="generated-chart">` 包裹 `<canvas>` + * 使用 `<figcaption>` 添加图表说明文字 + + +--- + + +### 四、禁止行为 + + +* 禁止跳过反思机制或忽略信息分析 +* 禁止直接复制网页内容或堆砌式摘要 +* 禁止在信息不足或逻辑结构不完整时提前输出报告 +* 禁止生成不完整 HTML(如缺少 `<html>` 或 `<style>`) \ No newline at end of file diff --git a/skills/qingyan-research/generate_html.py b/skills/qingyan-research/generate_html.py new file mode 100755 index 0000000..988e290 --- /dev/null +++ b/skills/qingyan-research/generate_html.py @@ -0,0 +1,33 @@ +import os +import re +import sys +import argparse + +def safe_filename(title): + # 移除非法文件名字符 + return re.sub(r'[\\/*?:"<>|]', "", title).strip() + +def main(): + parser = argparse.ArgumentParser(description="Save HTML content to a local file.") + parser.add_argument("--title", required=True, help="Report title (used as filename)") + args = parser.parse_args() + + # 从标准输入 (STDIN) 读取所有内容,这不会受到 Shell 参数长度限制 + try: + content = sys.stdin.read() + if not content: + print("Error: No content received from STDIN.", file=sys.stderr) + sys.exit(1) + + filename = f"{safe_filename(args.title)}.html" + + with open(filename, "w", encoding="utf-8") as f: + f.write(content) + + print(f"Successfully generated: {os.path.abspath(filename)}") + except Exception as e: + print(f"Error: {str(e)}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/skills/quiz-html/README.md b/skills/quiz-html/README.md new file mode 100755 index 0000000..ba640bd --- /dev/null +++ b/skills/quiz-html/README.md @@ -0,0 +1,117 @@ +# quiz-html · 网页题库生成器 + +把题目数组 → 一个可独立打开的 HTML 练习网页。 + +## ✨ 功能一览 + +- 📂 **双重筛选**:分类(学科/子模块) + 学习状态(已掌握/未做/错题) +- 🎯 **4 种题型**:选择 / 判断 / 填空 / 简答 +- 🤖 **智能答题**:选 ≠ 提交,可反复改;答错给一次重试机会 +- ⌨️ **键盘快捷键**:A/B/C/D · Enter · ← → · Space +- 📝 **模拟考模式**:选题量 + 限时 + 一次性提交 + 成绩页 +- 🧠 **记忆口诀**:题目带 `memory_tip` 字段会高亮显示 +- 🌓 **主题切换**:明 / 暗双主题,localStorage 记忆 +- 💾 **进度持久化**:浏览器记住每题状态,关掉再开还在 +- 📱 **移动端适配**:手机也能用 + +## 🚀 快速上手 + +```bash +# 1. 准备题目 JSON 文件(数组) +cat > /tmp/q.json << 'EOF' +[ + {"type":"single_choice","prompt":"1+1=?","options":["A. 1","B. 2","C. 3"],"answer":"B", + "explanation":"基础运算","category":"数学 / 加减法","level":1} +] +EOF + +# 2. 生成 HTML +python3 scripts/build_quiz_html.py /tmp/q.json --title "数学练习" --open +``` + +## 📁 目录结构 + +``` +quiz-html/ +├── SKILL.md # skill 描述(给 agent 用) +├── README.md # 本文件(给人看) +├── skill.yaml # skill 元数据 +├── scripts/ +│ └── build_quiz_html.py # 注入脚本 +├── templates/ +│ └── quiz_template.html # HTML 模板(含 {{占位符}}) +└── examples/ + └── demo.html # 示例:已注入的可直接打开的 demo +``` + +## 🔗 与 quiz-mastery 联动 + +本 skill 不直接出题,专门负责"题目 → 网页"这一步。 +出题/导入请用 `quiz-mastery`。完整链路: + +``` +quiz-mastery 出题 + ↓ + 题目 JSON + ↓ + 询问用户:"要做成网页吗?" + ↓ 用户说要 + quiz-html ← 你在这里 + ↓ + 生成 .html + ↓ + 浏览器打开 → 用户开始练 +``` + +## 📝 题目字段 + +| 字段 | 必填 | 类型 | 说明 | +|---|---|---|---| +| `type` | ✅ | string | `single_choice` / `true_false` / `fill_blank` / `short_answer` | +| `prompt` | ✅ | string | 题干 | +| `options` | 选择题 | array | `["A. xxx", "B. yyy", ...]` | +| `answer` | ✅ | string | 标准答案 | +| `explanation` | 推荐 | string | 解析(支持 `**粗体**` `*斜体*` `\` 代码 \``) | +| `knowledge_point` | 推荐 | string | 知识点名(侧边栏分组用) | +| `category` | 推荐 | string | 分类路径,建议格式 `"学科 / 子模块"` | +| `level` | 可选 | int | 难度 1-3 | +| `memory_tip` | 可选 | string | 记忆口诀,会用橙色卡片高亮 | + +## ⌨️ 用户使用快捷键 + +| 按键 | 作用 | +|---|---| +| `A` `B` `C` `D` | 选选项 | +| `T` `F` | 判断题 | +| `Enter` | 提交 | +| `←` `→` | 上一题 / 下一题 | +| `Space` | 查看答案 | +| `Ctrl/⌘+Enter` | 模拟考一键交卷 | + +## 🛠️ 命令行参数 + +``` +python3 build_quiz_html.py <questions.json> [options] + + --output, -o 输出 HTML 路径(默认:题目同目录) + --title 页面标题 + --subtitle 副标题(默认自动生成) + --id 题库 ID(用于 localStorage 隔离) + --open 生成后用浏览器打开 +``` + +## 🔍 退出码 + +| 码 | 含义 | +|---|---| +| 0 | 成功 | +| 1 | 参数错误 / 文件不存在 / 模板缺失 | +| 2 | JSON 解析失败 / 数据格式不合法 | + +## 📌 边界 + +| 任务 | 用谁 | +|---|---| +| 出题 / 评分 / 掌握度追踪 | `quiz-mastery` | +| 长期学习计划 | `study-buddy` | +| **把题目做成网页让用户在浏览器练** | **`quiz-html` (本 skill)** | diff --git a/skills/quiz-html/SKILL.md b/skills/quiz-html/SKILL.md new file mode 100755 index 0000000..9554e28 --- /dev/null +++ b/skills/quiz-html/SKILL.md @@ -0,0 +1,191 @@ +--- +name: quiz-html +description: 把题目数组生成一个**可独立运行的网页练习页**(HTML 文件)。当用户完成 quiz-mastery 的「从资料出题」或「从文件提取题目」流程后,应主动询问是否需要"在网页里练习",确认后调用本 skill 把题目注入模板,生成 HTML 给用户。也支持用户直接说"把这些题做成网页/HTML/练习页"时触发。**不处理**:出题(→ quiz-mastery)、评分(→ quiz-mastery)、长期复习计划(→ study-buddy)。 +--- + +# 网页题库生成器 (Quiz HTML Builder) + +把一组题目 JSON → 一个**单文件 HTML 练习网页**,包含: +- 📂 分类筛选(学科 / 子模块)+ 学习状态筛选(已掌握 / 未做 / 错题) +- 🎯 4 种题型支持:选择 / 判断 / 填空 / 简答 +- 🤖 答题自动标记,错题自动归入错题本(首次错误给一次重试机会) +- ⌨️ 完整键盘快捷键(A/B/C/D · Enter · 方向键 · Space) +- 📝 模拟考模式(限时 + 一次性提交 + 成绩页) +- 🌓 明暗主题切换 · localStorage 持久化 · 移动端适配 + +## 核心触发场景 + +### 场景 1:quiz-mastery 出题/导入完成后主动询问 ⭐ +这是本 skill 的**主要入口**。当 `quiz-mastery` 完成以下任一流程: +- 「从资料出题」:`generate_from_material.py` → 生成题目 JSON → `service.import_questions()` 入库 +- 「从题目文件提取」:`import_quiz.py` → 解析出题目 JSON → 入库 + +quiz-mastery 出题完成、向用户展示题目前,**主动问一句**: +> "题目准备好啦~ 要不要我把它们生成一个网页练习页?你可以在浏览器里慢慢做,错题会自动记下来,还能切换主题、模拟考试 🎯" + +用户说"要 / 好 / 生成网页 / 来一个 / 嗯"任一肯定意思 → 调用本 skill。 +用户说"不用 / 算了 / 直接在这里做" → 走原本的对话练习流程。 + +### 场景 2:用户直接要求生成网页 +触发关键词: +- "把这些题做成网页"、"做个 HTML 练习页"、"生成一个题库网页" +- "我想在浏览器里练"、"做个网页版" +- "把题目导出成 HTML" + +## 调用方式 + +### 一句话总结 +```bash +python3 scripts/build_quiz_html.py <题目JSON文件> [--title "..." --open] +``` + +### 标准流程 + +1. **拿到题目 JSON**(数组,每项是一道题) + - 来源 A:quiz-mastery 出题后的 LLM 输出(系统已是标准格式) + - 来源 B:用户直接粘贴的题目数组 + - 来源 C:从数据库读取的题目(quiz-mastery 的 `data/sessions/<sid>/questions.json`) + +2. **写到临时 JSON 文件**: + ```python + import json, tempfile + from pathlib import Path + tmp = Path(tempfile.mkdtemp()) / "questions.json" + tmp.write_text(json.dumps(questions, ensure_ascii=False), encoding="utf-8") + ``` + +3. **调用脚本**: + ```bash + python3 ~/Desktop/studybuddy_4.0/skills/quiz-html/scripts/build_quiz_html.py \ + /tmp/xxx/questions.json \ + --title "📚 物理 · 热学练习" \ + --output ~/Desktop/quiz_物理热学_20260518.html \ + --open + ``` + +4. **解析返回 JSON**: + ```json + { + "success": true, + "output_path": "/Users/.../quiz_xxx.html", + "question_count": 8, + "title": "📚 物理 · 热学练习", + "subtitle": "共 8 题 · 选择×5 · 判断×2 · 填空×1 · 物理", + "id": "q_1779091201", + "size_bytes": 63752 + } + ``` + +5. **告诉用户**:把 HTML 路径报给用户,提示「已经在浏览器打开了,可以开始练啦 ✨」 + +## 题目 JSON 字段标准 + +完全兼容 quiz-mastery 输出格式,**新增可选字段** `category` / `memory_tip`: + +| 字段 | 必填 | 说明 | +|---|---|---| +| `type` | ✅ | `single_choice` / `true_false` / `fill_blank` / `short_answer` | +| `prompt` | ✅ | 题干。也兼容 `question` 字段(自动转换) | +| `options` | 选择题必填 | `["A. xxx", "B. yyy", ...]` | +| `answer` | ✅ | 选择题填字母;判断题填 `"True"`/`"False"`;填空/简答填文本 | +| `explanation` | 推荐 | 解析(强烈建议填,K12 学生需要) | +| `knowledge_point` | 推荐 | 知识点名(侧边栏二级分组用) | +| `category` | 推荐 | 分类路径,**用"学科 / 子模块"格式**:`"物理 / 电学"`、`"数学 / 分数"` | +| `level` | 可选 | 难度 1-3 | +| `memory_tip` | 可选 | 记忆口诀,会用橙色卡片高亮显示(K12 神器) | + +### 示例 +```json +[ + { + "type": "single_choice", + "prompt": "下列关于并联电路电流规律的说法,正确的是( )。", + "options": [ + "A. 干路电流等于各支路电流之差", + "B. 干路电流等于各支路电流之和", + "C. 各支路电流相等", + "D. 干路电流大于任一支路电流的两倍" + ], + "answer": "B", + "explanation": "并联电路中,**干路电流等于各支路电流之和**:I = I₁ + I₂ + ...", + "knowledge_point": "并联电路电流规律", + "category": "物理 / 电学", + "level": 1, + "memory_tip": "🧠 并联看路口:进多少、出多少,电流不会消失" + } +] +``` + +## 设计原则 + +### 1. 自动 category,让筛选有意义 +如果题目缺 `category` 字段,最好补上(哪怕基于学科推断)。否则所有题都堆到"通用"分类下,分类筛选就废了。 + +### 2. category 用"学科 / 子模块" +- ✅ `"物理 / 电学"`、`"物理 / 热学"` → 顶部出 4 个细分类 chip +- ❌ `"物理"` → 只出 1 个,子模块在侧栏体现,但筛选粒度变粗 + +### 3. 知识点和分类不是同一层 +- `category` = 横向分类(哪个学科/章节),用于**顶部 chips 筛选** +- `knowledge_point` = 细粒度知识点,用于**左侧栏二级分组** + +### 4. 输出文件命名 +默认输出到题目 JSON 同目录,文件名 `quiz_<title_slug>_<时间戳>.html`。 +**建议显式传 `--output`**,放到 `~/Desktop/` 或一个固定目录方便用户找。 + +## 工作示例(quiz-mastery 衔接全流程) + +```python +# 1. quiz-mastery 已完成出题,拿到题目数组 +questions = [ + {"type": "single_choice", "prompt": "...", "options": [...], "answer": "A", + "explanation": "...", "knowledge_point": "...", "category": "物理 / 电学"}, + # ... +] + +# 2. agent 问用户:"要不要做成网页版?" +# 3. 用户:"要" +# 4. agent 写临时文件 + 调用 skill + +import json, subprocess, tempfile +from pathlib import Path + +tmp_dir = Path(tempfile.mkdtemp(prefix="quiz_")) +qjson = tmp_dir / "questions.json" +qjson.write_text(json.dumps(questions, ensure_ascii=False), encoding="utf-8") + +output = Path.home() / "Desktop" / "quiz_物理电学.html" + +result = subprocess.run([ + "python3", + str(Path.home() / "Desktop/studybuddy_4.0/skills/quiz-html/scripts/build_quiz_html.py"), + str(qjson), + "--title", "📚 物理 · 电学练习", + "--output", str(output), + "--open", +], capture_output=True, text=True) + +info = json.loads(result.stdout) +# info["output_path"] = "/Users/.../Desktop/quiz_物理电学.html" +``` + +然后告诉用户: +> 「已经做好啦~ 网页已自动打开 ✨ +> 路径:`~/Desktop/quiz_物理电学.html` +> 慢慢做,做完会自动记录错题,下次可以筛"错题"专门攻克 💪」 + +## 与其他 skill 的边界 + +| 任务 | 用谁 | +|---|---| +| 从资料出题 | **quiz-mastery** | +| 从文件提取题目 | **quiz-mastery** | +| 评分、掌握度追踪、艾宾浩斯安排 | **quiz-mastery** | +| 把题目做成网页给用户在浏览器练 | **quiz-html**(本 skill) | +| 学习计划、长期跟进 | **study-buddy** | + +## 失败处理 + +- `exit 1`:参数错误 / 文件不存在 / 模板缺失 → 报错给用户,让用户检查路径 +- `exit 2`:JSON 格式问题 / 题目数据非法 → 告诉用户哪几题被跳过,提示检查字段 +- 部分题被 skip 但有合法题:仍会成功生成,但 stderr 会列出被跳过的题,需要在回复里告知用户「跳过了 N 题,原因 XXX」 diff --git a/skills/quiz-html/examples/demo.html b/skills/quiz-html/examples/demo.html new file mode 100755 index 0000000..a4b30b7 --- /dev/null +++ b/skills/quiz-html/examples/demo.html @@ -0,0 +1,1349 @@ +<!DOCTYPE html> +<html lang="zh-CN"> +<head> +<meta charset="UTF-8"> +<meta name="viewport" content="width=device-width, initial-scale=1.0"> +<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate"> +<title>📚 StudyBuddy 题库示例 + + + + +
+
+

📚 StudyBuddy 题库示例 K12 多学科 · 8 道题

+
+
0已掌握
+
0未做
+
0错题
+
+
+ + + + + +
+
+
+
📂 分类:
+
+
+ + + +
+ +
+
+
+
📖
+
选择左侧题号开始学习
+
点击题号查看,做完会自动记录到本地
+
+
+ +
+
+ + + + + + + + + + + +
+
+

📝 模拟考

+
+
+ 0 / 0 +
+ ⏱ 0:00 + +
+
+
+ +
+
+ + +
+
+
0
+
+
+ + +
+
+
+
+ + + + + + + + diff --git a/skills/quiz-html/scripts/build_quiz_html.py b/skills/quiz-html/scripts/build_quiz_html.py new file mode 100755 index 0000000..95af4b1 --- /dev/null +++ b/skills/quiz-html/scripts/build_quiz_html.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 +""" +build_quiz_html.py - 把题目 JSON 注入到模板 HTML,生成可独立运行的练习网页。 + +用法: + python3 build_quiz_html.py [options] + +参数: + questions_json 题目 JSON 文件路径(必填) + 格式:题目数组,每题字段见下方"题目字段" + +选项: + --output 输出 HTML 路径(默认:./quiz_.html) + --title 页面标题(默认:"题库练习") + --subtitle 副标题(默认根据题量自动生成) + --id 题库标识 ID(用于 localStorage 隔离,默认时间戳) + --open 生成后自动用浏览器打开 + +题目字段(来自 quiz-mastery 的标准 JSON 格式): + type single_choice | true_false | fill_blank | short_answer + prompt 题干(必填) + options 选项数组,仅 single_choice 用,格式 ["A. xxx", "B. yyy", ...] + answer 标准答案 + explanation 解析(推荐填,K12 学生很需要) + knowledge_point 知识点名(用于侧边栏分组) + category 分类路径(推荐用"学科 / 子模块",如"物理 / 电学") + level 难度 1-3(可选) + memory_tip 记忆口诀(可选,会用橙色卡片高亮显示) + +退出码: + 0 成功 + 1 参数错误 / 文件不存在 + 2 JSON 解析失败 / 数据格式不对 +""" +from __future__ import annotations + +import argparse +import json +import re +import sys +import time +import webbrowser +from pathlib import Path +from typing import Any + + +SKILL_DIR = Path(__file__).resolve().parent.parent +TEMPLATE_PATH = SKILL_DIR / "templates" / "quiz_template.html" + +REQUIRED_FIELDS = {"type", "prompt", "answer"} +VALID_TYPES = {"single_choice", "true_false", "fill_blank", "short_answer"} + + +def load_questions(path: Path) -> list[dict[str, Any]]: + """加载题目 JSON,做基本格式校验。""" + try: + raw = path.read_text(encoding="utf-8") + except OSError as e: + print(f"❌ 无法读取文件:{path} - {e}", file=sys.stderr) + sys.exit(1) + + try: + data = json.loads(raw) + except json.JSONDecodeError as e: + print(f"❌ JSON 解析失败:{e}", file=sys.stderr) + sys.exit(2) + + if not isinstance(data, list): + print(f"❌ 题目必须是数组(list),得到 {type(data).__name__}", file=sys.stderr) + sys.exit(2) + + if not data: + print("❌ 题目数组为空", file=sys.stderr) + sys.exit(2) + + cleaned: list[dict[str, Any]] = [] + for i, q in enumerate(data, 1): + if not isinstance(q, dict): + print(f"⚠️ 第 {i} 题不是对象,已跳过", file=sys.stderr) + continue + miss = REQUIRED_FIELDS - set(q.keys()) + if miss: + # 容忍 quiz-mastery 输出里 prompt 写成 question 的旧字段 + if "prompt" in miss and "question" in q: + q["prompt"] = q["question"] + miss.discard("prompt") + if miss: + print(f"⚠️ 第 {i} 题缺字段 {miss},已跳过", file=sys.stderr) + continue + if q.get("type") not in VALID_TYPES: + print(f"⚠️ 第 {i} 题 type 非法:{q.get('type')}(应为 {VALID_TYPES}),已跳过", file=sys.stderr) + continue + # 单选题校验:options 必须存在 + if q["type"] == "single_choice" and not q.get("options"): + print(f"⚠️ 第 {i} 题(选择题)缺 options,已跳过", file=sys.stderr) + continue + cleaned.append(q) + + if not cleaned: + print("❌ 没有任何合法题目", file=sys.stderr) + sys.exit(2) + + return cleaned + + +def auto_subtitle(questions: list[dict[str, Any]]) -> str: + """根据题目自动生成副标题:题量 + 涉及分类。""" + cats = sorted({(q.get("category") or "").strip() for q in questions if q.get("category")}) + type_count: dict[str, int] = {} + for q in questions: + t = q.get("type", "?") + type_count[t] = type_count.get(t, 0) + 1 + type_names = { + "single_choice": "选择", + "true_false": "判断", + "fill_blank": "填空", + "short_answer": "简答", + } + breakdown = " · ".join(f"{type_names.get(t, t)}×{n}" for t, n in type_count.items()) + parts = [f"共 {len(questions)} 题"] + if breakdown: + parts.append(breakdown) + if cats: + # 提取顶级学科(按 "/" 拆) + top_cats = sorted({c.split("/")[0].strip() for c in cats if c}) + if top_cats: + parts.append("、".join(top_cats)) + return " · ".join(parts) + + +def render(questions: list[dict[str, Any]], title: str, subtitle: str, qid: str) -> str: + """把题目数据注入模板。""" + if not TEMPLATE_PATH.exists(): + print(f"❌ 模板文件不存在:{TEMPLATE_PATH}", file=sys.stderr) + sys.exit(1) + + html = TEMPLATE_PATH.read_text(encoding="utf-8") + + meta = {"id": qid, "title": title, "subtitle": subtitle} + quiz_json = json.dumps(questions, ensure_ascii=False) + meta_json = json.dumps(meta, ensure_ascii=False) + + # 顺序替换,避免 title/subtitle 内含 {{...}} 时被二次解释 + html = html.replace("{{TITLE}}", _safe(title)) + html = html.replace("{{SUBTITLE}}", _safe(subtitle)) + html = html.replace("{{QUIZ_DATA}}", quiz_json) + html = html.replace("{{META}}", meta_json) + return html + + +def _safe(s: str) -> str: + """HTML 安全转义(仅用于 title/subtitle 这种纯文本占位符)。""" + return ( + s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + ) + + +def _slug(s: str) -> str: + """生成文件名安全的 slug。""" + s = re.sub(r"[^\w\u4e00-\u9fa5-]+", "_", s).strip("_") + return s[:40] or "quiz" + + +def main() -> int: + ap = argparse.ArgumentParser( + description="把题目 JSON 注入模板,生成可独立运行的练习网页", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + ap.add_argument("questions", type=Path, help="题目 JSON 文件路径") + ap.add_argument("--output", "-o", type=Path, help="输出 HTML 路径") + ap.add_argument("--title", default="📚 题库练习", help="页面标题") + ap.add_argument("--subtitle", default=None, help="副标题(默认自动生成)") + ap.add_argument("--id", dest="qid", default=None, help="题库 ID(用于 localStorage 隔离)") + ap.add_argument("--open", action="store_true", help="生成后自动用浏览器打开") + args = ap.parse_args() + + if not args.questions.exists(): + print(f"❌ 文件不存在:{args.questions}", file=sys.stderr) + return 1 + + questions = load_questions(args.questions) + title = args.title + subtitle = args.subtitle or auto_subtitle(questions) + qid = args.qid or f"q_{int(time.time())}" + + html = render(questions, title, subtitle, qid) + + # 输出路径:默认到题目文件同目录 + if args.output: + output = args.output + else: + ts = time.strftime("%Y%m%d_%H%M%S") + output = args.questions.parent / f"quiz_{_slug(title)}_{ts}.html" + + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(html, encoding="utf-8") + + # 输出统一 JSON,方便 agent 解析 + result = { + "success": True, + "output_path": str(output.resolve()), + "question_count": len(questions), + "title": title, + "subtitle": subtitle, + "id": qid, + "size_bytes": output.stat().st_size, + } + print(json.dumps(result, ensure_ascii=False, indent=2)) + + if args.open: + webbrowser.open(f"file://{output.resolve()}") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/quiz-html/skill.yaml b/skills/quiz-html/skill.yaml new file mode 100755 index 0000000..bb2c9a8 --- /dev/null +++ b/skills/quiz-html/skill.yaml @@ -0,0 +1,57 @@ +name: quiz_html +version: 0.1.0 +description: 把题目 JSON 注入模板生成单文件 HTML 练习网页 + +entrypoints: + build: scripts/build_quiz_html.py + +features: + - html_quiz_generation + - multi_question_type + - category_filter + - state_filter + - exam_mode + - keyboard_shortcuts + - dark_mode + - localstorage_persistence + - mobile_responsive + +depends_on: + - quiz-mastery # 上游:题目数据来源 + +triggers: + # 直接关键词 + - "生成网页" + - "做成网页" + - "做个HTML" + - "网页版" + - "在浏览器练" + - "导出HTML" + - "在网页做练习" + + # quiz-mastery 完成后的链式触发 + - quiz_generated # 出题完成事件 + - quiz_imported # 题目导入完成事件 + +io: + input: + type: file + format: json + schema: + type: array + items: + required: [type, prompt, answer] + properties: + type: {enum: [single_choice, true_false, fill_blank, short_answer]} + prompt: {type: string} + options: {type: array, items: {type: string}} + answer: {type: string} + explanation: {type: string} + knowledge_point: {type: string} + category: {type: string} + level: {type: integer, minimum: 1, maximum: 3} + memory_tip: {type: string} + output: + type: file + format: html + self_contained: true # 单文件,无外部依赖 diff --git a/skills/quiz-html/templates/quiz_template.html b/skills/quiz-html/templates/quiz_template.html new file mode 100755 index 0000000..aef3848 --- /dev/null +++ b/skills/quiz-html/templates/quiz_template.html @@ -0,0 +1,1418 @@ + + + + + + +📚 {{TITLE}} + + + + +
+
+

📚 {{TITLE}} {{SUBTITLE}}

+
+
0已掌握
+
0未做
+
0错题
+
+
+ + + + + +
+
+
+
📂 分类:
+
+
+ + + +
+ +
+
+
+
📖
+
选择左侧题号开始学习
+
点击题号查看,做完会自动记录到本地
+
+
+ +
+
+ + + + + + + + + + + +
+
+

📝 模拟考

+
+
+ 0 / 0 +
+ ⏱ 0:00 + +
+
+
+ +
+
+ + +
+
+
0
+
+
+ + +
+
+
+
+ + + + + + + + diff --git a/skills/quiz-mastery/.claude/settings.json b/skills/quiz-mastery/.claude/settings.json new file mode 100755 index 0000000..da92391 --- /dev/null +++ b/skills/quiz-mastery/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "additionalDirectories": [ + "/Users/huiningli/Documents/AI/openclaw/quiz-mastery" + ] + } +} diff --git a/skills/quiz-mastery/README.md b/skills/quiz-mastery/README.md new file mode 100755 index 0000000..a5f55d2 --- /dev/null +++ b/skills/quiz-mastery/README.md @@ -0,0 +1,93 @@ +# Quiz Mastery Skill + +OpenClaw agent 的独立出题/复习/掌握度追踪引擎。 + +## Features + +- **从学习资料出题**:把 PDF / Word / Markdown 等学习材料转成分级测验(L1/L2/L3) +- **从题目文件导入练习**:解析已有题目文件,标准化后让用户答题 +- **答案评分**:自动评分 + 逐题反馈 +- **掌握度追踪**:跨三级难度记录用户进度 +- **薄弱知识点**:累计错误次数 ≥ 3 → 标为薄弱(**只增不减**,作为历史档案) +- **遗忘曲线复习**:基于艾宾浩斯(1/2/4/7/15 天)安排复习 + +## Architecture + +``` +quiz-mastery/ +├── SKILL.md # Skill metadata (LLM 读) +├── README.md # 本文件(人读) +├── skill.yaml # Skill 配置 +├── scripts/ +│ ├── generate_from_material.py # 从学习资料提取知识点 +│ ├── import_quiz.py # 从题目文件导入题目 +│ ├── run_quiz.py # 生成测验 prompt +│ └── submit_answers.py # 提交答案 + 评分 +├── src/quiz_mastery/ # 核心引擎 +└── data/ + ├── knowledge_points/ # 文档知识点定义 + ├── user_progress/ # 用户掌握度数据(含薄弱标记、艾宾浩斯阶段) + └── sessions/ # 测验会话记录 +``` + +## Quick Start + +### 1. 从学习资料提取知识点 + +```bash +python3 scripts/generate_from_material.py +``` + +输出知识点提取 prompt(JSON)。把 `prompts.system_prompt` 和 `prompts.user_prompt` 发给 LLM,得到知识点列表后调 `service.save_knowledge_points()` 保存。 + +### 2. 从题目文件导入 + +```bash +python3 scripts/import_quiz.py +``` + +输出题目解析 prompt(JSON)。把 prompt 发给 LLM 得到标准化题目,调 `service.import_questions()` 导入。 + +### 3. 生成测验 + +```bash +python3 scripts/run_quiz.py +``` + +按用户当前掌握度自动决定难度,输出出题 prompt(JSON)。 + +### 4. 提交答案 + +```bash +python3 scripts/submit_answers.py '' +``` + +返回:`score`、`total`、`accuracy`、逐题 `results`。 + +## 数据集成(与 OpenClaw agent 的关系) + +本 skill **不写 `memory/`**,仅写 USER.md 第 3 节"薄弱知识点"。所有持久化由调用方 agent 统一负责,详见 SKILL.md。 + +| 数据 | 谁写 | 写到哪 | +|------|------|--------| +| 知识点定义、答题战绩、艾宾浩斯阶段 | 本 skill | `data/` 目录 | +| 薄弱知识点(错误次数 ≥ 3) | 本 skill | USER.md 第 3 节(来源=`quiz-mastery`) | +| 学习项目状态、DAY 排期 | study-buddy(上游 agent) | USER.md 第 2 节 | + +## 难度系统 + +| 级别 | 含义 | +|------|------| +| L1 | 识记(基础记忆和理解) | +| L2 | 理解(深层理解、应用) | +| L3 | 应用(综合运用、问题解决) | + +- 首次出题强制 L1 起步 +- 答对升级(最高 L3),答错降级(最低 L1) + +## Workflow + +1. **Generate / Import**:从学习资料或题目文件创建知识点 + 题目 +2. **Run**:生成测验 +3. **Submit**:用户答题 → 自动评分 → 更新掌握度 / 薄弱标记 +4. **Sync**:薄弱知识点写入 USER.md 第 3 节 diff --git a/skills/quiz-mastery/SKILL.md b/skills/quiz-mastery/SKILL.md new file mode 100755 index 0000000..a36cbc3 --- /dev/null +++ b/skills/quiz-mastery/SKILL.md @@ -0,0 +1,212 @@ +--- +name: quiz-mastery +description: 出题、测验、复习、掌握度追踪工具。**用户说"复习"、"巩固"、"回顾"任一关键词时优先触发本 skill**。当用户的请求与"题目/复习"相关时触发:把学习资料/PDF/材料转成题目练习("给这个 PDF 出几道题")、导入题目文件做练习("我有一份题目文件,帮我做")、复习已学内容("复习一下昨天的"、"巩固一下"、"回顾下昨天"、"用艾宾浩斯帮我安排")、遗忘曲线追踪、掌握度评分。**🔴 强制规则**:每次出题/导入题目成功后,**首轮展示题目前必须问一句**"要不要生成网页练习页?",用户说要 → 调用 quiz-html skill。**不处理**:长期学习项目的进度管理、计划制定(→ study-buddy)。 +--- + +# 测验大师 (Quiz Mastery) + +## 两大核心能力 + +### 能力一:从学习资料出题 +1. 用户提供学习资料(.md / .txt / .docx / .pdf / .ppt / .pptx) +2. 调用 `generate_from_material.py` 获取知识点提取 prompt +3. 将 prompt 发给 LLM,得到知识点 JSON +4. 调用 `service.save_knowledge_points()` 保存知识点 +5. 调用 `run_quiz.py` 生成出题 prompt +6. 将 prompt 发给 LLM,得到题目 JSON +7. **⭐ 询问用户是否生成网页练习页**(见下方"网页练习联动"章节) + - 用户说要 → 调用 `quiz-html` skill 生成 HTML 并打开 + - 用户说不用 → 走原流程 +8. 逐题展示给用户,收集答案 +9. 调用 `submit_answers.py` 提交评分 + +### 能力二:从题目文件练习 +1. 用户提供题目文件(.md / .txt / .docx / .pdf / .ppt / .pptx) +2. 调用 `import_quiz.py` 获取题目解析 prompt +3. 将 prompt 发给 LLM,得到标准化题目 JSON +4. 调用 `service.import_questions()` 导入题目并创建 session +5. **⭐ 询问用户是否生成网页练习页**(见下方"网页练习联动"章节) + - 用户说要 → 调用 `quiz-html` skill 生成 HTML 并打开 + - 用户说不用 → 走原流程 +6. 逐题展示给用户,收集答案 +7. 调用 `submit_answers.py` 提交评分 + +## 何时使用(触发条件) + +1. **用户主动要求**:"出几道题"、"测试一下"、"来个小测"、"练习题"、"考考我" +2. **用户说"复习"、"巩固"、"回顾"**:直接触发 +3. **基于已有题目文件练习**:用户上传题目文件后触发 + +> ⚠️ **不处理 study-buddy 的"即时练习"**——那条链路由 study-buddy 走外部 `exam_take`,不调本 skill。 + +## 没历史数据时的兜底 + +当用户说"复习"但 `data/user_progress/` 是空的(新用户/没答过题): +- **不要硬启动复习流程**——没数据可复习 +- 主动告诉用户:"还没有可复习的历史数据,要不要先用一份学习资料出题练一下?" +- 引导用户走"能力一:从学习资料出题" + +## 难度系统 + +| 级别 | 含义 | 说明 | +|------|------|------| +| L1 | 识记 | 基础记忆和理解,考察概念辨认和基本事实 | +| L2 | 理解 | 深层理解,考察概念区分、原理解释和简单应用 | +| L3 | 应用 | 综合运用,考察实际场景应用、分析和问题解决 | + +- **首次出题**:强制从 L1 开始 +- **答对当前难度**:升一级(最高 L3) +- **答错当前难度**:降一级(最低 L1) + +## 题型分配规则 + +| 级别 | 选择题 | 判断题 | 填空题 | 简答题 | +|------|--------|--------|--------|--------| +| L1 | 70% | 30% | - | - | +| L2 | 50% | 20% | 30% | - | +| L3 | 40% | 20% | 20% | 20% | + +## 出题数量 + +- **默认每次出 3 道题**(一次对话展示 3 题,用户一次性回答后统一评分) +- 每轮最多 **15 题**(用户可要求调整数量) +- 简答题尽量少出,不自动评分(标记为 `needs_review`,由外部 LLM/人工评判) + +## 薄弱知识点追踪 + +- **标记为薄弱**:累计错误次数 ≥ 3 +- **不解除**:薄弱知识点只增不减,作为历史档案保留 +- 内部数据保存在 `data/user_progress/`(错误次数、艾宾浩斯阶段等) +- **同步到 USER.md 第 3 节"薄弱知识点"**(由本 skill 直接写入,来源=`quiz-mastery`): + | 知识点 | 错误次数 | 来源 | 备注 | + - 已有该知识点 → 更新错误次数 + - 未在表中 → 新增一行 + +## 遗忘曲线复习机制 + +基于艾宾浩斯遗忘曲线,按 **1天 → 2天 → 4天 → 7天 → 15天** 间隔安排复习: +- 答对:review_stage +1(推进到下一个间隔) +- 答错:review_stage 重置为 0(从头开始) +- 复习推荐包含:即将遗忘的知识点 + 最近 3 天薄弱知识点 + +## 脚本调用方式 + +### 1. 从学习资料提取知识点 + +```bash +python3 scripts/generate_from_material.py +``` + +输出知识点提取 prompt(JSON),将 prompts.system_prompt 和 prompts.user_prompt 发给 LLM。 + +### 2. 从题目文件导入题目 + +```bash +python3 scripts/import_quiz.py +``` + +输出题目解析 prompt(JSON),将 prompts.system_prompt 和 prompts.user_prompt 发给 LLM。 + +### 3. 生成测验 + +```bash +python3 scripts/run_quiz.py +``` + +根据已保存的知识点和用户当前掌握度自动决定难度,输出出题 prompt(JSON)。 + +### 4. 提交答案 + +```bash +python3 scripts/submit_answers.py '' +``` + +参数说明: +- `answers_json`:JSON 格式的答案字典,如 `{"q_001": "A", "q_002": "True"}` + +返回评分结果:score、total、accuracy、逐题 results。 + +## 出题流程(面向 study-buddy 的调用说明) + +1. 确定知识点来源(学习资料 or 已有题目文件) +2. 执行对应的提取/导入流程 +3. 调用 `run_quiz.py` 生成出题 prompt +4. **每次展示 3 道题给用户**(一次性展示,编号清晰,不要逐题出),用户一次性回答后再统一评分 +5. 收集用户回答(用户可以一次性回复 3 道题的答案) +6. 调用 `submit_answers.py` 提交评分 +7. 将评分结果返回给 study-buddy,由其写入 memory 文件 + +⚠️ **本 skill 仅写入 USER.md 第 3 节"薄弱知识点"**(来源=`quiz-mastery`);不写其他分区,也不写 `memory/`。其他持久化由 study-buddy 统一负责。 + +## 数据目录结构 + +``` +skills/quiz-mastery/data/ +├── knowledge_points/ ← 知识点定义(按 document_id) +├── sessions/ ← 测验会话记录 +└── user_progress/ ← 用户掌握度数据(含薄弱标记、遗忘曲线) +``` + +## ⭐ 网页练习联动(与 quiz-html 协作) + +每次拿到题目 JSON 之后("能力一"步骤 7、"能力二"步骤 5),都要**主动问用户一句**: + +> "题目准备好啦~ 要不要我把它们生成一个网页练习页?你可以在浏览器里慢慢做,错题会自动记下来,还能切换主题、模拟考试 🎯" + +### 用户回应判定 + +| 用户说 | 判定 | 行动 | +|---|---|---| +| "要 / 好 / 嗯 / 来一个 / 生成 / 网页 / 浏览器" | ✅ 要 | 调用 `quiz-html` | +| "不用 / 不要 / 算了 / 直接做 / 这里做" | ❌ 不要 | 走原对话流程 | +| 没回应 / 不明确 | 默认 ❌ 不要 | 直接走原流程,不强推 | + +### 调用 quiz-html 的具体步骤 + +```python +import json, subprocess, tempfile +from pathlib import Path + +# 1. 把已经拿到的题目 JSON 写到临时文件 +tmp_dir = Path(tempfile.mkdtemp(prefix="quiz_")) +qjson = tmp_dir / "questions.json" +qjson.write_text(json.dumps(questions, ensure_ascii=False), encoding="utf-8") + +# 2. 决定输出路径(推荐放 ~/Desktop) +output = Path.home() / "Desktop" / f"quiz_{title_slug}.html" + +# 3. 调脚本 +result = subprocess.run([ + "python3", + str(Path.home() / "Desktop/studybuddy_4.0/skills/quiz-html/scripts/build_quiz_html.py"), + str(qjson), + "--title", page_title, # 如 "📚 物理 · 电学练习" + "--output", str(output), + "--open", # 生成后自动用浏览器打开 +], capture_output=True, text=True) + +info = json.loads(result.stdout) # {"success": true, "output_path": "...", ...} +``` + +### 题目字段补全建议 + +调用前,最好给每道题补上以下字段(如果出题时没生成): +- `category`:**一级分类,短词**(建议 2-6 字),用于网页顶部分类筛选 chip。 + - ✅ 推荐:`物理` / `数学` / `法律` / `历史` / `编程` / `通用` + - ❌ 避免:`通用类 / 1.中华人民共和国证券法(1998年12月29日…)` 这种长串、含日期/编号/斜杠的写法 + - 如果非要分两级,用 `/` 分隔且二级也要短:`物理 / 电学` +- `knowledge_point`:知识点名(侧边栏分组用,可与 quiz-mastery 的 KP title 一致,不要带层级前缀) +- `memory_tip`:记忆口诀(可选,K12 学生很需要) + +这样网页的分类筛选、侧栏分组、记忆卡片才能发挥作用。 + +### 边界 + +| 任务 | 用谁 | +|---|---| +| 出题、提取题目 | 本 skill (quiz-mastery) | +| 评分、掌握度追踪 | 本 skill (quiz-mastery) | +| **题目 → 网页练习页** | **quiz-html** | + +调完 quiz-html 之后,**仍然要走 quiz-mastery 的评分流程**——网页里的答题状态是给用户自查用的,正式的 mastery 数据要靠 `submit_answers.py` 写入。两者并行不冲突。 + diff --git a/skills/quiz-mastery/scripts/generate_from_material.py b/skills/quiz-mastery/scripts/generate_from_material.py new file mode 100755 index 0000000..bfce77d --- /dev/null +++ b/skills/quiz-mastery/scripts/generate_from_material.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""从学习资料生成知识点提取 prompt。 + +用法:python3 generate_from_material.py + +输出:JSON 格式的 prompt(system_prompt + user_prompt),由 agent 发给 LLM 执行。 +LLM 返回知识点 JSON 后,agent 应调用 service.save_knowledge_points() 保存。 +""" +from pathlib import Path +import json +import sys + +sys.path.append(str(Path(__file__).resolve().parents[1] / "src")) + +from quiz_mastery.file_parser import parse_file, build_extraction_prompt + + +def main() -> None: + if len(sys.argv) < 3: + print("Usage: generate_from_material.py ") + print(" file_path: Path to study material (.md, .txt, .text)") + print(" document_id: Identifier for this document") + sys.exit(1) + + file_path = sys.argv[1] + document_id = sys.argv[2] + + content = parse_file(file_path) + prompts = build_extraction_prompt(content) + + output = { + "action": "extract_knowledge_points", + "document_id": document_id, + "file_path": file_path, + "prompts": prompts, + "instructions": ( + "Send the system_prompt and user_prompt to an LLM. " + "The LLM should return a JSON array of knowledge points. " + "Then call save_knowledge_points(document_id, knowledge_points) to save." + ), + } + + print(json.dumps(output, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/quiz-mastery/scripts/import_quiz.py b/skills/quiz-mastery/scripts/import_quiz.py new file mode 100755 index 0000000..82f3bdc --- /dev/null +++ b/skills/quiz-mastery/scripts/import_quiz.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""从题目文件导入题目。 + +用法:python3 import_quiz.py + +输出:JSON 格式的 prompt(system_prompt + user_prompt),由 agent 发给 LLM 解析题目。 +LLM 返回题目 JSON 后,agent 应调用 service.import_questions() 导入。 +""" +from pathlib import Path +import json +import sys + +sys.path.append(str(Path(__file__).resolve().parents[1] / "src")) + +from quiz_mastery.file_parser import parse_file +from quiz_mastery.quiz_extractor import build_extraction_prompt + + +def main() -> None: + if len(sys.argv) < 4: + print("Usage: import_quiz.py ") + print(" file_path: Path to question file (.md, .txt, .text)") + print(" document_id: Identifier for this document") + print(" user_id: User identifier") + sys.exit(1) + + file_path = sys.argv[1] + document_id = sys.argv[2] + user_id = sys.argv[3] + + content = parse_file(file_path) + prompts = build_extraction_prompt(content) + + output = { + "action": "import_questions", + "document_id": document_id, + "user_id": user_id, + "file_path": file_path, + "prompts": prompts, + "instructions": ( + "Send the system_prompt and user_prompt to an LLM. " + "The LLM should return a JSON array of questions. " + "Then call service.import_questions(document_id, user_id, questions) to import." + ), + } + + print(json.dumps(output, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/quiz-mastery/scripts/run_quiz.py b/skills/quiz-mastery/scripts/run_quiz.py new file mode 100755 index 0000000..2947328 --- /dev/null +++ b/skills/quiz-mastery/scripts/run_quiz.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +"""生成出题 prompt。 + +用法:python3 run_quiz.py + +根据已保存的知识点和用户掌握度记录,自动决定出题难度。 +- 从 mastery_records 读取每个知识点的 current_level +- 首次出题的知识点强制 level=1 +- 输出 JSON 格式的 prompt(system_prompt + user_prompt),由 agent 发给 LLM 生成题目 + +也支持指定知识点和难度: + python3 run_quiz.py [level] [kp_id1,kp_id2,...] +""" +from pathlib import Path +import json +import sys + +sys.path.append(str(Path(__file__).resolve().parents[1] / "src")) + +from quiz_mastery import QuizMasteryService + + +def main() -> None: + if len(sys.argv) < 3: + print("Usage: run_quiz.py [level] [kp_id1,kp_id2,...]") + print(" user_id: User identifier") + print(" document_id: Document identifier") + print(" level: Optional difficulty level (1/2/3)") + print(" kp_ids: Optional comma-separated knowledge point IDs") + sys.exit(1) + + user_id = sys.argv[1] + document_id = sys.argv[2] + + level = None + kp_ids = None + + if len(sys.argv) >= 4: + try: + level = int(sys.argv[3]) + except ValueError: + # Maybe it's kp_ids instead + kp_ids = sys.argv[3].split(",") + + if len(sys.argv) >= 5: + kp_ids = sys.argv[4].split(",") + + service = QuizMasteryService( + base_dir=Path(__file__).resolve().parents[1] / "data" + ) + + result = service.generate_quiz_for_user( + user_id=user_id, + document_id=document_id, + knowledge_point_ids=kp_ids, + level=level, + ) + + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/quiz-mastery/scripts/submit_answers.py b/skills/quiz-mastery/scripts/submit_answers.py new file mode 100755 index 0000000..4ef2b6d --- /dev/null +++ b/skills/quiz-mastery/scripts/submit_answers.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python3 +"""提交测验答案并评分。 + +用法:python3 submit_answers.py + +参数: + user_id: 用户标识 + document_id: 文档标识 + session_id: 测验会话 ID + answers_json: JSON 格式的答案字典,如 '{"q_001":"A","q_002":"True"}' + +输出:评分结果 JSON(score, total, accuracy, results)。 +""" +from pathlib import Path +import json +import sys + +sys.path.append(str(Path(__file__).resolve().parents[1] / "src")) + +from quiz_mastery import QuizMasteryService + + +def main() -> None: + if len(sys.argv) < 5: + print("Usage: submit_answers.py ") + sys.exit(1) + + user_id = sys.argv[1] + document_id = sys.argv[2] + session_id = sys.argv[3] + answers = json.loads(sys.argv[4]) + + service = QuizMasteryService( + base_dir=Path(__file__).resolve().parents[1] / "data" + ) + + result = service.submit_quiz_answers( + user_id=user_id, + document_id=document_id, + session_id=session_id, + answers=answers, + ) + + print(json.dumps(result, ensure_ascii=False, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/quiz-mastery/skill.yaml b/skills/quiz-mastery/skill.yaml new file mode 100755 index 0000000..bacb177 --- /dev/null +++ b/skills/quiz-mastery/skill.yaml @@ -0,0 +1,21 @@ +name: quiz_mastery +version: 0.2.0 +entrypoints: + run_quiz: scripts/run_quiz.py + submit_answers: scripts/submit_answers.py + generate_from_material: scripts/generate_from_material.py + import_quiz: scripts/import_quiz.py + +storage: + type: local_json + base_dir: ./data + +features: + - knowledge_extraction + - quiz_generation + - question_import + - answer_evaluation + - mastery_tracking + - weak_point_tracking + - ebbinghaus_review + - review_planning diff --git a/skills/quiz-mastery/src/quiz_mastery/__init__.py b/skills/quiz-mastery/src/quiz_mastery/__init__.py new file mode 100755 index 0000000..b8f5d2c --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/__init__.py @@ -0,0 +1,3 @@ +from .service import QuizMasteryService + +__all__ = ["QuizMasteryService"] diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/__init__.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/__init__.cpython-313.pyc new file mode 100755 index 0000000..d551276 Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/__init__.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/evaluator.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/evaluator.cpython-313.pyc new file mode 100755 index 0000000..9791527 Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/evaluator.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/file_parser.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/file_parser.cpython-313.pyc new file mode 100755 index 0000000..3be8dc8 Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/file_parser.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/mastery_engine.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/mastery_engine.cpython-313.pyc new file mode 100755 index 0000000..7a1da99 Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/mastery_engine.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/models.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/models.cpython-313.pyc new file mode 100755 index 0000000..32f6d6c Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/models.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/planner.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/planner.cpython-313.pyc new file mode 100755 index 0000000..0bc6e8e Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/planner.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/quiz_extractor.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/quiz_extractor.cpython-313.pyc new file mode 100755 index 0000000..ef5d150 Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/quiz_extractor.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/quiz_generator.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/quiz_generator.cpython-313.pyc new file mode 100755 index 0000000..f219df1 Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/quiz_generator.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/repository.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/repository.cpython-313.pyc new file mode 100755 index 0000000..c1ed54b Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/repository.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/__pycache__/service.cpython-313.pyc b/skills/quiz-mastery/src/quiz_mastery/__pycache__/service.cpython-313.pyc new file mode 100755 index 0000000..b87513c Binary files /dev/null and b/skills/quiz-mastery/src/quiz_mastery/__pycache__/service.cpython-313.pyc differ diff --git a/skills/quiz-mastery/src/quiz_mastery/evaluator.py b/skills/quiz-mastery/src/quiz_mastery/evaluator.py new file mode 100755 index 0000000..6957f17 --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/evaluator.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from .models import Question, AnswerResult + + +class Evaluator: + """Evaluates user answers against correct answers. + + - single_choice / true_false: exact match + - fill_blank: case-insensitive, strip whitespace + - short_answer: no auto-scoring, returns needs_review=True + """ + + def evaluate_answers( + self, + questions: list[Question], + user_answers: dict[str, str], + ) -> list[AnswerResult]: + results: list[AnswerResult] = [] + + for q in questions: + user_answer = user_answers.get(q.id) + + if q.type == "short_answer": + # Short answer: cannot auto-evaluate, needs human/LLM review + results.append( + AnswerResult( + question_id=q.id, + user_answer=user_answer, + is_correct=None, + score=None, + feedback="需要人工/LLM评判", + error_type="pending_review", + needs_review=True, + ) + ) + continue + + if user_answer is None: + results.append( + AnswerResult( + question_id=q.id, + user_answer=None, + is_correct=False, + score=0.0, + feedback=f"未作答。正确答案:{q.answer}。{q.explanation}", + error_type="no_answer", + needs_review=False, + ) + ) + continue + + if q.type == "fill_blank": + # Fill-in-the-blank: case-insensitive, strip whitespace + is_correct = ( + user_answer.strip().lower() == str(q.answer).strip().lower() + ) + else: + # single_choice / true_false: exact match + is_correct = user_answer.strip() == str(q.answer).strip() + + results.append( + AnswerResult( + question_id=q.id, + user_answer=user_answer, + is_correct=is_correct, + score=1.0 if is_correct else 0.0, + feedback=( + q.explanation + if is_correct + else f"回答错误。正确答案:{q.answer}。{q.explanation}" + ), + error_type="none" if is_correct else "concept_confusion", + needs_review=False, + ) + ) + + return results diff --git a/skills/quiz-mastery/src/quiz_mastery/file_parser.py b/skills/quiz-mastery/src/quiz_mastery/file_parser.py new file mode 100755 index 0000000..fc23a9d --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/file_parser.py @@ -0,0 +1,274 @@ +from __future__ import annotations + +import subprocess +import zipfile +import xml.etree.ElementTree as ET +from pathlib import Path + + +SUPPORTED_EXTENSIONS = {".md", ".txt", ".text", ".docx", ".pdf", ".ppt", ".pptx"} + + +# ── .docx (zero-dep: zip + xml) ────────────────────────────────── + +def _parse_docx(file_path: Path) -> str: + """Extract text from .docx using stdlib only (zipfile + xml). + + .docx is a ZIP archive containing word/document.xml with paragraph data. + """ + ns = "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}" + + with zipfile.ZipFile(str(file_path), "r") as zf: + # Main document body + if "word/document.xml" not in zf.namelist(): + raise ValueError("Invalid .docx: word/document.xml not found") + + tree = ET.parse(zf.open("word/document.xml")) + root = tree.getroot() + + parts: list[str] = [] + for para in root.iter(f"{ns}p"): + texts = [node.text for node in para.iter(f"{ns}t") if node.text] + line = "".join(texts).strip() + if line: + parts.append(line) + + return "\n\n".join(parts) + + +# ── .pptx (zero-dep: zip + xml) ────────────────────────────────── + +def _parse_pptx(file_path: Path) -> str: + """Extract text from .pptx using stdlib only (zipfile + xml). + + .pptx is a ZIP archive; each slide is at ppt/slides/slideN.xml. + """ + ns_a = "{http://schemas.openxmlformats.org/drawingml/2006/main}" + + with zipfile.ZipFile(str(file_path), "r") as zf: + slide_names = sorted( + [n for n in zf.namelist() if n.startswith("ppt/slides/slide") and n.endswith(".xml")] + ) + + if not slide_names: + raise ValueError("Invalid .pptx: no slides found") + + parts: list[str] = [] + for idx, slide_name in enumerate(slide_names, 1): + tree = ET.parse(zf.open(slide_name)) + root = tree.getroot() + + slide_texts: list[str] = [] + for para in root.iter(f"{ns_a}p"): + texts = [node.text for node in para.iter(f"{ns_a}t") if node.text] + line = "".join(texts).strip() + if line: + slide_texts.append(line) + + if slide_texts: + parts.append(f"[Slide {idx}]\n" + "\n".join(slide_texts)) + + return "\n\n".join(parts) + + +# ── .ppt (legacy binary → textutil fallback) ───────────────────── + +def _parse_ppt(file_path: Path) -> str: + """Extract text from legacy .ppt format. + + Tries macOS textutil first. If unavailable, raises a helpful error. + """ + # macOS textutil can convert .doc but not .ppt directly. + # Try python-pptx as optional, otherwise error with guidance. + try: + from pptx import Presentation + prs = Presentation(str(file_path)) + parts: list[str] = [] + for slide_num, slide in enumerate(prs.slides, 1): + slide_texts: list[str] = [] + for shape in slide.shapes: + if shape.has_text_frame: + for para in shape.text_frame.paragraphs: + text = para.text.strip() + if text: + slide_texts.append(text) + if slide_texts: + parts.append(f"[Slide {slide_num}]\n" + "\n".join(slide_texts)) + return "\n\n".join(parts) + except ImportError: + pass + except Exception: + pass + + raise ValueError( + "Legacy .ppt format requires conversion. " + "Please save as .pptx first (open in PowerPoint/WPS → Save As → .pptx), " + "or install python-pptx: pip install python-pptx" + ) + + +# ── .pdf (macOS native or pymupdf fallback) ────────────────────── + +def _parse_pdf(file_path: Path) -> str: + """Extract text from .pdf. + + Strategy: + 1. Try pymupdf (fitz) if installed — best quality + 2. Fallback: macOS `osascript` + Quartz filter (zero-dep on macOS) + 3. Fallback: `pdftotext` from poppler if installed + """ + # Strategy 1: pymupdf + try: + import fitz + doc = fitz.open(str(file_path)) + parts: list[str] = [] + for page in doc: + text = page.get_text().strip() + if text: + parts.append(text) + doc.close() + if parts: + return "\n\n".join(parts) + except ImportError: + pass + + # Strategy 2: macOS python3 Quartz (Core Graphics) — zero-dep on macOS + try: + result = subprocess.run( + [ + "python3", "-c", + "import sys\n" + "from Quartz import PDFDocument\n" + "from Foundation import NSURL\n" + "url = NSURL.fileURLWithPath_(sys.argv[1])\n" + "doc = PDFDocument.alloc().initWithURL_(url)\n" + "if doc is None: sys.exit(1)\n" + "parts = []\n" + "for i in range(doc.pageCount()):\n" + " page = doc.pageAtIndex_(i)\n" + " text = page.string()\n" + " if text and text.strip(): parts.append(text.strip())\n" + "print('\\n\\n'.join(parts))\n", + str(file_path), + ], + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + # Strategy 3: pdftotext (poppler) + try: + result = subprocess.run( + ["pdftotext", str(file_path), "-"], + capture_output=True, + text=True, + timeout=30, + ) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + + raise ValueError( + "Could not extract text from PDF. Options:\n" + "1. Install pymupdf: pip install pymupdf\n" + "2. Install poppler: brew install poppler (provides pdftotext)\n" + "3. On macOS, ensure Quartz/pyobjc is available" + ) + + +# ── Main entry ──────────────────────────────────────────────────── + +def parse_file(file_path: str) -> str: + """Read a file and return its text content. + + Supports: .md, .txt, .text, .docx, .pdf, .ppt, .pptx + + .docx and .pptx use Python stdlib only (zipfile + xml). + .pdf tries pymupdf → macOS Quartz → pdftotext (graceful fallback). + .ppt (legacy) tries python-pptx if installed, otherwise asks for conversion. + + Raises: + FileNotFoundError: If file does not exist. + ValueError: If file extension is not supported or extraction fails. + """ + path = Path(file_path) + + if not path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + suffix = path.suffix.lower() + + if suffix not in SUPPORTED_EXTENSIONS: + raise ValueError( + f"Unsupported file type: {suffix}. " + f"Supported: {', '.join(sorted(SUPPORTED_EXTENSIONS))}" + ) + + if suffix == ".docx": + return _parse_docx(path) + elif suffix == ".pdf": + return _parse_pdf(path) + elif suffix == ".pptx": + return _parse_pptx(path) + elif suffix == ".ppt": + return _parse_ppt(path) + + return path.read_text(encoding="utf-8") + + +def build_extraction_prompt(content: str) -> dict: + """Build a prompt for LLM to extract knowledge points from study material. + + Args: + content: The text content of the study material. + + Returns: + dict with 'system_prompt' and 'user_prompt' keys. + """ + system_prompt = ( + "你是一个专业的知识点提取助手。从用户提供的学习资料中提取核心知识点。\n" + "严格按照要求的 JSON 格式输出,不要输出任何其他内容。" + ) + + user_prompt = f"""请从以下学习资料中提取核心知识点。 + +## 学习资料内容 + +{content} + +## 提取要求 +1. 每个知识点必须包含以下字段: + - id: 唯一标识符(格式:kp_001, kp_002, ...) + - title: 知识点名称(简洁明确) + - definition: 知识点的定义(一句话概括) + - description: 详细描述(可包含原文中的关键内容) + - tags: 标签列表(用于分类和检索) +2. 提取所有重要的概念、原理、定义、公式等 +3. 每个知识点应该是独立的、可测试的单元 +4. description 应尽量保留原文中的关键表述 + +## 输出格式 +输出纯 JSON 数组,每个元素格式如下: +```json +[ + {{ + "id": "kp_001", + "title": "知识点名称", + "definition": "一句话定义", + "description": "详细描述,包含原文关键内容", + "tags": ["标签1", "标签2"] + }} +] +``` + +请直接输出 JSON 数组,不要包含 markdown 代码块标记或其他文字。""" + + return { + "system_prompt": system_prompt, + "user_prompt": user_prompt, + } diff --git a/skills/quiz-mastery/src/quiz_mastery/mastery_engine.py b/skills/quiz-mastery/src/quiz_mastery/mastery_engine.py new file mode 100755 index 0000000..19cf5f3 --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/mastery_engine.py @@ -0,0 +1,149 @@ +from __future__ import annotations + +from datetime import datetime, timedelta + +from .models import MasteryRecord, Question, AnswerResult + + +# Ebbinghaus forgetting curve intervals in days +REVIEW_INTERVALS = [1, 2, 4, 7, 15] + +# Maximum accuracy history entries to keep +MAX_ACCURACY_HISTORY = 10 + + +class MasteryEngine: + """Tracks mastery level, weak points, and spaced repetition schedule.""" + + def update_mastery( + self, + existing_records: dict[str, MasteryRecord], + questions: list[Question], + results: list[AnswerResult], + ) -> dict[str, MasteryRecord]: + """Update mastery records based on quiz results. + + - Correct answer at current level → level up (max 3) + - Wrong answer → level down (min 1) + - First time → forced to level 1 + - Weak marking: accuracy < 0.5 or 2 consecutive wrong → is_weak = True + - Strong recovery: accuracy >= 0.8 for 2 consecutive → is_weak = False + - Updates Ebbinghaus review schedule + """ + question_map = {q.id: q for q in questions} + now_str = datetime.now().strftime("%Y-%m-%d") + + # Group results by knowledge point + kp_scores: dict[str, list[float]] = {} + for r in results: + q = question_map.get(r.question_id) + if q is None: + continue + # Skip short_answer that needs review (score is None) + if r.score is None: + continue + for kp_id in q.knowledge_point_ids: + if kp_id not in kp_scores: + kp_scores[kp_id] = [] + kp_scores[kp_id].append(r.score) + + for kp_id, scores in kp_scores.items(): + record = existing_records.get(kp_id) + if record is None: + record = MasteryRecord(knowledge_point_id=kp_id, current_level=1) + existing_records[kp_id] = record + + accuracy = sum(scores) / len(scores) if scores else 0.0 + + # Update basic stats + record.attempts += 1 + record.last_accuracy = accuracy + record.best_accuracy = max(record.best_accuracy, accuracy) + record.last_reviewed_at = now_str + + # Update accuracy history (keep last 10) + record.accuracy_history.append(accuracy) + if len(record.accuracy_history) > MAX_ACCURACY_HISTORY: + record.accuracy_history = record.accuracy_history[-MAX_ACCURACY_HISTORY:] + + # Update level: correct → up, wrong → down + is_correct = accuracy >= 0.6 # threshold for "correct" at current level + if is_correct: + record.current_level = min(record.current_level + 1, 3) + else: + record.current_level = max(record.current_level - 1, 1) + + # Update weak status + self._update_weak_status(record) + + # Update Ebbinghaus review schedule + self._update_review_schedule(record, is_correct, now_str) + + return existing_records + + def _update_weak_status(self, record: MasteryRecord) -> None: + """Mark or unmark a knowledge point as weak. + + Weak if: accuracy < 0.5 OR last 2 attempts both wrong + Recover if: accuracy >= 0.8 for last 2 consecutive attempts + """ + history = record.accuracy_history + + # Check for consecutive failures (last 2 both < 0.6) + if len(history) >= 2 and history[-1] < 0.6 and history[-2] < 0.6: + record.is_weak = True + return + + # Check overall recent accuracy + if record.last_accuracy < 0.5: + record.is_weak = True + return + + # Check for recovery: last 2 both >= 0.8 + if len(history) >= 2 and history[-1] >= 0.8 and history[-2] >= 0.8: + record.is_weak = False + + def _update_review_schedule( + self, record: MasteryRecord, is_correct: bool, today_str: str + ) -> None: + """Update Ebbinghaus spaced repetition schedule. + + Correct → advance review_stage (max 4) + Wrong → reset review_stage to 0 + """ + if is_correct: + record.review_stage = min(record.review_stage + 1, len(REVIEW_INTERVALS) - 1) + else: + record.review_stage = 0 + + interval_days = REVIEW_INTERVALS[record.review_stage] + today = datetime.strptime(today_str, "%Y-%m-%d") + next_review = today + timedelta(days=interval_days) + record.next_review_at = next_review.strftime("%Y-%m-%d") + + def get_review_candidates( + self, + records: dict[str, MasteryRecord], + today_str: str, + ) -> list[str]: + """Return knowledge point IDs that need review. + + Criteria: + - next_review_at <= today (due for review) + - is_weak=True and last_reviewed_at within last 3 days + """ + candidates = set() + today = datetime.strptime(today_str, "%Y-%m-%d") + three_days_ago = (today - timedelta(days=3)).strftime("%Y-%m-%d") + + for kp_id, record in records.items(): + # Due for review based on Ebbinghaus schedule + if record.next_review_at and record.next_review_at <= today_str: + candidates.add(kp_id) + + # Weak and recently reviewed (within 3 days) + if record.is_weak and record.last_reviewed_at: + if record.last_reviewed_at >= three_days_ago: + candidates.add(kp_id) + + return list(candidates) diff --git a/skills/quiz-mastery/src/quiz_mastery/models.py b/skills/quiz-mastery/src/quiz_mastery/models.py new file mode 100755 index 0000000..a1d7a3a --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/models.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from dataclasses import dataclass, field, asdict +from typing import Any, Literal + + +QuestionType = Literal["single_choice", "true_false", "fill_blank", "short_answer"] + + +@dataclass +class KnowledgeSource: + document_id: str + section_title: str = "" + page_start: int | None = None + page_end: int | None = None + snippets: list[str] = field(default_factory=list) + + +@dataclass +class KnowledgePoint: + id: str + title: str + description: str + definition: str = "" + tags: list[str] = field(default_factory=list) + source: KnowledgeSource | None = None + + +@dataclass +class Question: + id: str + knowledge_point_ids: list[str] + level: int # 1, 2, 3 + type: QuestionType + prompt: str + options: list[str] = field(default_factory=list) + answer: Any = None + explanation: str = "" + source_refs: list[str] = field(default_factory=list) + + +@dataclass +class QuizSession: + session_id: str + user_id: str + document_id: str + level: int # 1, 2, 3 + knowledge_point_ids: list[str] + questions: list[Question] + status: str = "generated" + + +@dataclass +class AnswerResult: + question_id: str + user_answer: Any + is_correct: bool | None # None for short_answer needing review + score: float | None # None for short_answer needing review + feedback: str = "" + error_type: str = "unknown" + needs_review: bool = False + + +@dataclass +class MasteryRecord: + knowledge_point_id: str + current_level: int = 1 # 1, 2, 3 + attempts: int = 0 + last_accuracy: float = 0.0 + best_accuracy: float = 0.0 + last_reviewed_at: str | None = None + is_weak: bool = False + accuracy_history: list[float] = field(default_factory=list) + next_review_at: str | None = None # ISO date string YYYY-MM-DD + review_stage: int = 0 # 0-4, maps to [1, 2, 4, 7, 15] days + + +def to_dict(obj: Any) -> Any: + """Convert a dataclass instance to dict.""" + if hasattr(obj, "__dataclass_fields__"): + return asdict(obj) + return obj diff --git a/skills/quiz-mastery/src/quiz_mastery/planner.py b/skills/quiz-mastery/src/quiz_mastery/planner.py new file mode 100755 index 0000000..b54bd26 --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/planner.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from datetime import datetime, timedelta + +from .models import KnowledgePoint, MasteryRecord + + +class Planner: + """Recommends knowledge points for review based on Ebbinghaus + forgetting curve and weak-point tracking.""" + + def recommend_review( + self, + knowledge_points: list[KnowledgePoint], + mastery_records: dict[str, MasteryRecord], + today_str: str, + ) -> list[dict]: + """Return a list of review recommendations. + + Combines: + 1. Knowledge points due for review (next_review_at <= today) + 2. Weak knowledge points reviewed in the last 3 days + + Returns: + List of dicts with 'knowledge_point_id', 'title', 'reason', + 'current_level', 'is_weak'. + """ + today = datetime.strptime(today_str, "%Y-%m-%d") + three_days_ago_str = (today - timedelta(days=3)).strftime("%Y-%m-%d") + + # Build a title lookup + kp_title_map = {kp.id: kp.title for kp in knowledge_points} + + seen_ids: set[str] = set() + recommendations: list[dict] = [] + + for kp_id, record in mastery_records.items(): + reasons: list[str] = [] + + # Check Ebbinghaus due + if record.next_review_at and record.next_review_at <= today_str: + reasons.append("遗忘曲线到期,需要复习") + + # Check weak + recently reviewed + if record.is_weak and record.last_reviewed_at: + if record.last_reviewed_at >= three_days_ago_str: + reasons.append("薄弱知识点,最近3天内有练习记录") + elif not record.next_review_at: + # Weak but no review schedule yet + reasons.append("薄弱知识点,建议复习") + + if reasons and kp_id not in seen_ids: + seen_ids.add(kp_id) + recommendations.append({ + "knowledge_point_id": kp_id, + "title": kp_title_map.get(kp_id, kp_id), + "reason": ";".join(reasons), + "current_level": record.current_level, + "is_weak": record.is_weak, + }) + + return recommendations diff --git a/skills/quiz-mastery/src/quiz_mastery/quiz_extractor.py b/skills/quiz-mastery/src/quiz_mastery/quiz_extractor.py new file mode 100755 index 0000000..b57b41d --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/quiz_extractor.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +import json +from .models import Question + + +def build_extraction_prompt(content: str) -> dict: + """Build a prompt for LLM to parse questions from a question file. + + Args: + content: Raw text content containing questions. + + Returns: + dict with 'system_prompt' and 'user_prompt' keys. + """ + system_prompt = ( + "你是一个专业的题目解析助手。从用户提供的题目文件中识别并解析所有题目。\n" + "严格按照要求的 JSON 格式输出,不要输出任何其他内容。" + ) + + user_prompt = f"""请从以下题目文件内容中解析出所有题目。 + +## 题目文件内容 + +{content} + +## 解析要求 +1. 识别每道题的类型: + - single_choice: 选择题(有 A/B/C/D 等选项) + - true_false: 判断题(判断对错) + - fill_blank: 填空题(有空格需要填写) + - short_answer: 简答题(需要文字回答) +2. 提取题目的所有信息 +3. 如果题目有答案和解析,也一并提取 +4. 为每道题分配唯一 ID + +## 输出格式 +输出纯 JSON 数组,每个元素格式如下: +```json +[ + {{ + "id": "q_001", + "knowledge_point_ids": [], + "level": 1, + "type": "single_choice", + "prompt": "题目内容", + "options": ["A. 选项一", "B. 选项二", "C. 选项三", "D. 选项四"], + "answer": "A", + "explanation": "解析内容(如果有)" + }} +] +``` + +注意: +- type 必须是 single_choice, true_false, fill_blank, short_answer 之一 +- 选择题的 answer 填选项字母(A/B/C/D) +- 判断题的 answer 填 "True" 或 "False" +- 填空题的 answer 填正确答案文本 +- 简答题的 answer 填参考答案(如果有) +- 如果无法确定 knowledge_point_ids,留空数组 +- level 默认为 1,如果能从题目难度判断则相应调整 + +请直接输出 JSON 数组,不要包含 markdown 代码块标记或其他文字。""" + + return { + "system_prompt": system_prompt, + "user_prompt": user_prompt, + } + + +def parse_questions_json(json_str: str) -> list[Question]: + """Parse LLM-returned JSON string into a list of Question objects. + + Args: + json_str: JSON string containing a list of question dicts. + + Returns: + List of Question objects. + + Raises: + json.JSONDecodeError: If json_str is not valid JSON. + ValueError: If the parsed data is not a list. + """ + # Try to extract JSON from possible markdown code blocks + cleaned = json_str.strip() + if cleaned.startswith("```"): + # Remove markdown code block markers + lines = cleaned.split("\n") + # Remove first line (```json or ```) + lines = lines[1:] + # Remove last line (```) + if lines and lines[-1].strip() == "```": + lines = lines[:-1] + cleaned = "\n".join(lines) + + data = json.loads(cleaned) + if not isinstance(data, list): + raise ValueError(f"Expected a JSON array, got {type(data).__name__}") + + questions: list[Question] = [] + for item in data: + q = Question( + id=item.get("id", "q_unknown"), + knowledge_point_ids=item.get("knowledge_point_ids", []), + level=item.get("level", 1), + type=item.get("type", "single_choice"), + prompt=item.get("prompt", ""), + options=item.get("options", []), + answer=item.get("answer"), + explanation=item.get("explanation", ""), + source_refs=item.get("source_refs", []), + ) + questions.append(q) + + return questions diff --git a/skills/quiz-mastery/src/quiz_mastery/quiz_generator.py b/skills/quiz-mastery/src/quiz_mastery/quiz_generator.py new file mode 100755 index 0000000..ef6928e --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/quiz_generator.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import json +from .models import KnowledgePoint + + +# Maximum questions per quiz round +MAX_QUESTIONS_PER_ROUND = 15 + +# Question type distribution by level +LEVEL_DISTRIBUTION = { + 1: {"single_choice": 70, "true_false": 30}, + 2: {"single_choice": 50, "fill_blank": 30, "true_false": 20}, + 3: {"single_choice": 40, "fill_blank": 20, "true_false": 20, "short_answer": 20}, +} + + +class QuizGenerator: + """Builds prompt templates for LLM-based quiz generation. + + This module does NOT call any LLM API. It constructs system_prompt and + user_prompt that should be sent to an LLM by the caller (agent). + """ + + def generate_quiz( + self, + knowledge_points: list[KnowledgePoint], + level: int = 1, + num_questions: int | None = None, + ) -> dict: + """Build prompts for quiz generation. + + Args: + knowledge_points: List of knowledge points to quiz on. + level: Difficulty level (1, 2, or 3). + num_questions: Number of questions to generate (max 15). + + Returns: + dict with 'system_prompt' and 'user_prompt' keys. + """ + level = max(1, min(3, level)) + if num_questions is None: + num_questions = 3 # Default: 3 questions per round for efficiency + num_questions = min(num_questions, MAX_QUESTIONS_PER_ROUND) + + distribution = LEVEL_DISTRIBUTION[level] + distribution_text = "\n".join( + f" - {qtype}: {pct}%" for qtype, pct in distribution.items() + ) + + # Build knowledge point descriptions for the prompt + kp_descriptions = [] + for kp in knowledge_points: + desc_parts = [f"- **{kp.title}** (ID: {kp.id})"] + if kp.definition: + desc_parts.append(f" 定义: {kp.definition}") + if kp.description: + desc_parts.append(f" 描述: {kp.description}") + if kp.source and kp.source.snippets: + desc_parts.append(f" 原文片段: {'; '.join(kp.source.snippets)}") + kp_descriptions.append("\n".join(desc_parts)) + + kp_text = "\n\n".join(kp_descriptions) + + level_desc = { + 1: "识记(基础记忆和理解,考察概念辨认和基本事实)", + 2: "理解(深层理解,考察概念区分、原理解释和简单应用)", + 3: "应用(综合运用,考察实际场景应用、分析和问题解决)", + } + + system_prompt = ( + "你是一个专业的出题助手。根据提供的知识点信息,生成高质量的测验题目。\n" + "严格按照要求的 JSON 格式输出,不要输出任何其他内容。\n" + "题目必须紧扣知识点的名称、定义和原文描述,不能出脱离原文的题目。" + ) + + user_prompt = f"""请根据以下知识点生成 {num_questions} 道测验题。 + +## 难度级别 +Level {level}: {level_desc[level]} + +## 题型分配 +{distribution_text} + +## 知识点信息 + +{kp_text} + +## 出题要求 +1. 每道题必须关联至少一个知识点 ID +2. 选择题 (single_choice):4 个选项 A/B/C/D,answer 填正确选项字母 +3. 判断题 (true_false):answer 填 "True" 或 "False" +4. 填空题 (fill_blank):prompt 中用 ____ 标记空白处,answer 填正确答案文本 +5. 简答题 (short_answer):answer 填参考答案 +6. 每道题必须包含 explanation(解析) +7. 题目内容必须基于上述知识点的名称、定义和描述,不要超出范围 +8. **每道题必须填写 `category` 和 `knowledge_point` 字段**(用于分类筛选和侧栏分组): + - `category`:一级分类,**短词**(建议 2-6 字),用于顶部分类筛选 chip。 + 例:物理 / 数学 / 法律 / 历史 / 编程 / 通用 等。 + **不要**写成长串、不要含日期、不要含编号、不要含书名号或斜杠。 + - `knowledge_point`:所属知识点名称(直接用关联知识点的 title 即可),用于侧栏分组。 +9. 总题数:{num_questions} 道 + +## 输出格式 +输出纯 JSON 数组,每个元素格式如下: +```json +[ + {{ + "id": "q_001", + "knowledge_point_ids": ["kp_id"], + "category": "物理", + "knowledge_point": "牛顿第二定律", + "level": {level}, + "type": "single_choice", + "prompt": "题目内容", + "options": ["A. 选项一", "B. 选项二", "C. 选项三", "D. 选项四"], + "answer": "A", + "explanation": "解析内容" + }} +] +``` + +请直接输出 JSON 数组,不要包含 markdown 代码块标记或其他文字。""" + + return { + "system_prompt": system_prompt, + "user_prompt": user_prompt, + } diff --git a/skills/quiz-mastery/src/quiz_mastery/repository.py b/skills/quiz-mastery/src/quiz_mastery/repository.py new file mode 100755 index 0000000..69a5ab3 --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/repository.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + + +class JsonRepository: + def __init__(self, base_dir: str | Path): + self.base_dir = Path(base_dir) + self.kp_dir = self.base_dir / "knowledge_points" + self.progress_dir = self.base_dir / "user_progress" + self.sessions_dir = self.base_dir / "sessions" + self._ensure_dirs() + + def _ensure_dirs(self) -> None: + for d in [self.kp_dir, self.progress_dir, self.sessions_dir]: + d.mkdir(parents=True, exist_ok=True) + + def load_json(self, path: Path, default: Any = None) -> Any: + if not path.exists(): + return default + return json.loads(path.read_text(encoding="utf-8")) + + def save_json(self, path: Path, data: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") + + def knowledge_points_path(self, document_id: str) -> Path: + return self.kp_dir / f"{document_id}.json" + + def progress_path(self, user_id: str, document_id: str) -> Path: + return self.progress_dir / f"{user_id}__{document_id}.json" + + def session_path(self, session_id: str) -> Path: + return self.sessions_dir / f"{session_id}.json" diff --git a/skills/quiz-mastery/src/quiz_mastery/service.py b/skills/quiz-mastery/src/quiz_mastery/service.py new file mode 100755 index 0000000..2a7e7c0 --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/service.py @@ -0,0 +1,304 @@ +from __future__ import annotations + +from dataclasses import asdict +from pathlib import Path +import uuid +import json + +from .models import KnowledgePoint, KnowledgeSource, Question, QuizSession, MasteryRecord +from .repository import JsonRepository +from .quiz_generator import QuizGenerator +from .evaluator import Evaluator +from .mastery_engine import MasteryEngine +from .planner import Planner + + +class QuizMasteryService: + def __init__(self, base_dir: str | Path): + self.repo = JsonRepository(base_dir) + self.generator = QuizGenerator() + self.evaluator = Evaluator() + self.mastery_engine = MasteryEngine() + self.planner = Planner() + + # ── Knowledge Points ────────────────────────────────────────── + + def load_knowledge_points(self, document_id: str) -> list[KnowledgePoint]: + """Load knowledge points from JSON file.""" + raw = self.repo.load_json( + self.repo.knowledge_points_path(document_id), default={} + ) + items = raw.get("knowledge_points", []) + result: list[KnowledgePoint] = [] + + for item in items: + source_data = item.get("source") + kp = KnowledgePoint( + id=item["id"], + title=item["title"], + description=item.get("description", ""), + definition=item.get("definition", ""), + tags=item.get("tags", []), + source=KnowledgeSource(**source_data) if source_data else None, + ) + result.append(kp) + + return result + + def save_knowledge_points( + self, document_id: str, knowledge_points_data: list[dict] + ) -> None: + """Save extracted knowledge points to JSON. + + Args: + document_id: Document identifier. + knowledge_points_data: List of dicts, each with id, title, + definition, description, tags. + """ + payload = {"knowledge_points": knowledge_points_data} + self.repo.save_json( + self.repo.knowledge_points_path(document_id), payload + ) + + # ── Progress ────────────────────────────────────────────────── + + def load_progress( + self, user_id: str, document_id: str + ) -> dict[str, MasteryRecord]: + """Load user mastery progress from JSON.""" + raw = self.repo.load_json( + self.repo.progress_path(user_id, document_id), default={} + ) + mastery_records = raw.get("mastery_records", {}) + parsed: dict[str, MasteryRecord] = {} + + for kp_id, record in mastery_records.items(): + mr = MasteryRecord(knowledge_point_id=kp_id) + mr.current_level = record.get("current_level", 1) + mr.attempts = record.get("attempts", 0) + mr.last_accuracy = record.get("last_accuracy", 0.0) + mr.best_accuracy = record.get("best_accuracy", 0.0) + mr.last_reviewed_at = record.get("last_reviewed_at") + mr.is_weak = record.get("is_weak", False) + mr.accuracy_history = record.get("accuracy_history", []) + mr.next_review_at = record.get("next_review_at") + mr.review_stage = record.get("review_stage", 0) + parsed[kp_id] = mr + + return parsed + + def save_progress( + self, + user_id: str, + document_id: str, + records: dict[str, MasteryRecord], + ) -> None: + """Save user mastery progress to JSON.""" + payload = { + "user_id": user_id, + "document_id": document_id, + "mastery_records": { + kp_id: asdict(record) for kp_id, record in records.items() + }, + } + self.repo.save_json( + self.repo.progress_path(user_id, document_id), payload + ) + + # ── Quiz Generation ─────────────────────────────────────────── + + def generate_quiz_for_user( + self, + user_id: str, + document_id: str, + knowledge_point_ids: list[str] | None = None, + level: int | None = None, + num_questions: int | None = None, + ) -> dict: + """Generate quiz prompts for given knowledge points. + + If knowledge_point_ids is None, uses all knowledge points. + If level is None, reads current_level from mastery records + (first-time defaults to 1). + + Returns dict with 'prompts' (system_prompt + user_prompt), + 'knowledge_points' used, and 'level'. + """ + knowledge_points = self.load_knowledge_points(document_id) + progress = self.load_progress(user_id, document_id) + + if knowledge_point_ids: + selected = [kp for kp in knowledge_points if kp.id in knowledge_point_ids] + else: + selected = knowledge_points + + if not selected: + return {"error": "No knowledge points found"} + + # Determine level per knowledge point group + # Use the most common level or explicit level + if level is not None: + quiz_level = max(1, min(3, level)) + else: + # Determine from mastery records; first-time = 1 + levels = [] + for kp in selected: + record = progress.get(kp.id) + if record is None: + levels.append(1) # First time → L1 + else: + levels.append(record.current_level) + # Use the minimum level among selected (conservative) + quiz_level = min(levels) if levels else 1 + + prompts = self.generator.generate_quiz( + selected, level=quiz_level, num_questions=num_questions + ) + + return { + "document_id": document_id, + "user_id": user_id, + "level": quiz_level, + "knowledge_point_ids": [kp.id for kp in selected], + "prompts": prompts, + } + + # ── Quiz Submission ─────────────────────────────────────────── + + def submit_quiz_answers( + self, + user_id: str, + document_id: str, + session_id: str, + answers: dict[str, str], + ) -> dict: + """Submit answers for a quiz session, evaluate, and update mastery.""" + session_data = self.repo.load_json( + self.repo.session_path(session_id), default=None + ) + if not session_data: + raise FileNotFoundError(f"Quiz session not found: {session_id}") + + questions = [] + for item in session_data["questions"]: + questions.append(Question(**item)) + + results = self.evaluator.evaluate_answers(questions, answers) + progress = self.load_progress(user_id, document_id) + updated = self.mastery_engine.update_mastery(progress, questions, results) + self.save_progress(user_id, document_id, updated) + + # Calculate score (excluding short_answer with score=None) + scored_results = [r for r in results if r.score is not None] + score = sum(r.score for r in scored_results) + total = len(scored_results) + needs_review_count = sum(1 for r in results if r.needs_review) + + summary = { + "session_id": session_id, + "score": score, + "total": total, + "accuracy": score / total if total else 0.0, + "needs_review_count": needs_review_count, + "results": [asdict(r) for r in results], + } + + # Update session data + session_data["answers"] = answers + session_data["results"] = summary["results"] + session_data["status"] = "completed" + self.repo.save_json(self.repo.session_path(session_id), session_data) + + return summary + + # ── Import Questions ────────────────────────────────────────── + + def import_questions( + self, + document_id: str, + user_id: str, + questions_data: list[dict], + ) -> dict: + """Import parsed questions and create a quiz session. + + Args: + document_id: Document identifier. + user_id: User identifier. + questions_data: List of question dicts (from LLM parsing). + + Returns: + dict with session_id and questions. + """ + questions: list[Question] = [] + for item in questions_data: + q = Question( + id=item.get("id", f"q_{uuid.uuid4().hex[:8]}"), + knowledge_point_ids=item.get("knowledge_point_ids", []), + level=item.get("level", 1), + type=item.get("type", "single_choice"), + prompt=item.get("prompt", ""), + options=item.get("options", []), + answer=item.get("answer"), + explanation=item.get("explanation", ""), + source_refs=item.get("source_refs", []), + ) + questions.append(q) + + session_id = f"quiz_{uuid.uuid4().hex[:12]}" + kp_ids = list( + set(kp_id for q in questions for kp_id in q.knowledge_point_ids) + ) + level = questions[0].level if questions else 1 + + session = QuizSession( + session_id=session_id, + user_id=user_id, + document_id=document_id, + level=level, + knowledge_point_ids=kp_ids, + questions=questions, + ) + + self.repo.save_json( + self.repo.session_path(session_id), + asdict(session), + ) + + return { + "session_id": session_id, + "document_id": document_id, + "level": level, + "num_questions": len(questions), + "questions": [asdict(q) for q in questions], + } + + # ── Review Candidates ───────────────────────────────────────── + + def get_review_candidates( + self, user_id: str, document_id: str, today_str: str | None = None + ) -> list[dict]: + """Get review recommendations for a user. + + Returns list of knowledge points that need review. + """ + from datetime import datetime + + if today_str is None: + today_str = datetime.now().strftime("%Y-%m-%d") + + knowledge_points = self.load_knowledge_points(document_id) + progress = self.load_progress(user_id, document_id) + return self.planner.recommend_review(knowledge_points, progress, today_str) + + # ── User Progress ───────────────────────────────────────────── + + def get_user_progress(self, user_id: str, document_id: str) -> dict: + """Get user's mastery progress summary.""" + progress = self.load_progress(user_id, document_id) + return { + "user_id": user_id, + "document_id": document_id, + "mastery_records": { + kp_id: asdict(record) for kp_id, record in progress.items() + }, + } diff --git a/skills/quiz-mastery/src/quiz_mastery/utils.py b/skills/quiz-mastery/src/quiz_mastery/utils.py new file mode 100755 index 0000000..f8bf3c1 --- /dev/null +++ b/skills/quiz-mastery/src/quiz_mastery/utils.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pathlib import Path + + +def project_root() -> Path: + return Path(__file__).resolve().parents[2] \ No newline at end of file diff --git a/skills/resume-builder/SKILL.md b/skills/resume-builder/SKILL.md new file mode 100755 index 0000000..388b622 --- /dev/null +++ b/skills/resume-builder/SKILL.md @@ -0,0 +1,133 @@ +--- +name: resume-builder +description: 从零生成或全面优化一份中文简历,并导出 docx / pdf / markdown 多种格式。用 STAR 法则改写经历、做 ATS 关键词覆盖率检查、根据行业(互联网产品 / 技术 / 金融 / 通用)选模板。当用户说"帮我写简历 / 优化简历 / 简历不会写 / 我的简历太弱了 / 简历看起来不专业 / 简历改一改 / 给我做个简历模板 / 简历导出 / 简历加点关键词",或者上传 .pdf/.docx 简历后说"看看怎么改"时,必须触发本 skill。即使用户只问"我的简历有什么问题"也要触发。 +--- + +# Resume Builder(简历生成与优化) + +这个 skill 干三件事: + +1. **结构化生成**:从用户经历产出一份符合中文求职市场审美的简历 +2. **STAR 法则改写**:把"参与了 X"这种弱句子,改写成"通过 X 实现 Y,结果 Z" +3. **ATS 关键词优化 + 多模板导出**:保证简历能过自动筛选,并支持 docx/pdf/md + +不做:JD 定向改写(那是 jd-resume-tailor 的事,请在那里做"针对某 JD 改简历") + +--- + +## 何时触发 + +- "帮我写一份简历" +- "我的简历太弱了 / 没有亮点 / 看起来不专业" +- "把这段经历用 STAR 改一下" +- "简历里关键词够不够" +- "导出 PDF / docx 简历" +- 用户上传简历但没明确说"针对某 JD 改" → 触发本 skill;如果说了"针对 X 公司 / X 岗位改" → 调用 jd-resume-tailor + +--- + +## 工作流 + +### Step 1: 摸清简历当前状态 + +如果用户**有简历文件**(.pdf / .docx / .md / .txt): +- pdf 走 pdf skill 解析 +- docx 走 docx skill 解析 +- 提取出:基本信息、教育、工作经历、项目经历、技能、其他 + +如果用户**没有简历**: +- 用 AskUserQuestion 收集信息(参考 `references/intake_questions.md` 里的问题清单) +- 一次问 3~4 个问题,分轮收集,避免劝退 + +### Step 2: 选模板 + +读取 `references/templates/` 决定结构: + +- 互联网产品 / 运营 / PM → `templates/internet.md` +- 技术 / 研发 / 数据 → `templates/tech.md` +- 金融 / 咨询 / 商科 → `templates/finance.md` +- 通用 / 跨行业 → `templates/general.md` + +如果用户没指定方向,用通用模板,但**询问一句**:"你下一步主要往什么方向投?我可以用更适合那个方向的版式。" + +### Step 3: 用 STAR 改写每段经历 + +读取 `references/star_rewrite_guide.md`,对每条工作 / 项目经历做 STAR 改写。 + +**STAR 不是死板的四段式**,而是确保每条 bullet 都有: +- **背景信号**(一句话点出问题大小或情境) +- **动作**(你具体做了什么,要有动词) +- **结果**(数字 / 百分比 / 排名 / 规模) + +如果用户提供的信息里**没有数字**,要主动追问:"这个项目用户量大概是多少?""这个优化大概提了多少?记不准的话给个量级也行。" + +### Step 4: ATS 关键词检查 + +调用脚本: + +```bash +python scripts/ats_check.py --resume \ + --industry internet \ + [--jd ] +``` + +脚本会: +1. 抽取简历里的关键词 +2. 对照行业关键词库(来自 job-intent-tracker 或本 skill 的 `references/keywords/`) +3. 输出"已覆盖 / 建议补充"两个清单 +4. 给出 ATS 友好度评分(字体单一性、表格使用、特殊符号、图片等) + +**ATS 友好的硬规则**: +- 不要用 word 表格放经历(很多 ATS 解析不了) +- 不要把日期放在装饰性图片里 +- 不要写两栏布局(有些 ATS 会按列读,导致顺序混乱) +- 不要插图标 / emoji 在标题里 +- 字体用宋体 / 思源宋体 / Arial / Helvetica 之一 + +### Step 5: 多格式导出 + +读取 `references/export_guide.md`,按用户需求选择: + +**docx 导出**(最通用,国内 HR 优先要 docx): +- 调用 docx skill +- 用 `assets/resume_template.docx` 作为模板(如果存在) + +**pdf 导出**(最终投递版): +- 推荐流程:先 docx → 再用 word/libreoffice 转 pdf +- 直接生成 pdf 用 reportlab 比较丑,不推荐 +- 调用 pdf skill 做后处理(加密 / 元数据清理) + +**markdown 导出**(备份 + GitHub): +- 直接写 .md 文件即可 + +**默认行为**:除非用户指定,同时输出 docx + md 两个版本。 + +### Step 6: 自检 & 反馈 + +输出后做一次自检(写到聊天里给用户看,不用单独存文件): + +``` +✓ 长度:< 1 页(应届)/ ≤ 2 页(社招) +✓ 联系方式齐全(手机 + 邮箱,可选 GitHub/作品集) +✓ 每段经历都有量化结果 +✓ 关键词覆盖率(vs 行业库):__% +✓ ATS 友好度:__/10 +⚠ 待用户确认:<还需要补的信息> +``` + +--- + +## 反模式(不要做) + +- ❌ 帮用户编数字(说"提升 30%"但用户根本没说过)—— 必须基于用户提供的信息,不知道就标 `[待补充:具体数字]` +- ❌ 用形容词堆砌("具有出色的沟通能力" "认真负责" "有上进心")—— 删掉 +- ❌ 一段经历写超过 5 条 bullet —— 太密会被 HR 跳过 +- ❌ 写"自我评价"长篇 —— 国内现在不流行,1~2 行 summary 即可 +- ❌ 把所有公司都写一样的 bullet 数 —— 重要的多写,次要的少写 +- ❌ 把"熟悉 / 了解 / 会用"当能力描述 —— 改成"用 X 做了 Y" + +## 与其他 skill 的协作 + +- 用户接着说"针对这家公司改一下" → 转 `jd-resume-tailor` +- 用户接着说"准备面试" → 转 `interview-prep` +- 用户没确定方向就来改简历 → 先反问"你打算投什么方向?",必要时转 `job-intent-tracker` diff --git a/skills/resume-builder/references/export_guide.md b/skills/resume-builder/references/export_guide.md new file mode 100755 index 0000000..c0257e8 --- /dev/null +++ b/skills/resume-builder/references/export_guide.md @@ -0,0 +1,55 @@ +# 简历导出指南 + +## 推荐组合 + +- **投递主用**:docx(HR 直接收的格式)+ pdf(最终版本,防排版错乱) +- **备份**:md(自己迭代用、放 GitHub) + +## docx 导出(默认) + +调用 docx skill。模板优先级: +1. 用户提供模板 → 用用户的 +2. `assets/resume_template.docx` 存在 → 用本 skill 的 +3. 都没有 → 用 docx skill 的默认样式(注意控制字体、行距、margin) + +样式参数(推荐默认): +- 中文字体:思源宋体 / 宋体 / 微软雅黑(HR 电脑大概率有) +- 英文字体:Arial / Helvetica / Calibri +- 字号:正文 10.5~11pt,标题 12~14pt,姓名 16~18pt +- 行距:1.15~1.3 +- 页边距:上下 1.5cm,左右 1.8cm +- 颜色:通体黑色 + 一个低饱和度强调色(深蓝 / 深灰) + +## pdf 导出 + +**推荐**:docx → libreoffice headless 转 pdf + +```bash +libreoffice --headless --convert-to pdf --outdir +``` + +**不推荐**:直接用 reportlab / weasyprint 从头画,效果丑且容易踩字体坑。 + +## markdown 导出 + +最简单直接:文件名建议 `resume_<姓名拼音>_.md`,例如 `resume_zhangsan_202605.md`。 + +## 文件命名建议 + +投递时的文件名极其重要(很多 HR 直接按文件名搜): + +`<姓名>_<目标岗位>_.` + +例:`张三_高级产品经理_5年.pdf` + +不要叫 `final.pdf` `final_v2.pdf` `修改版.docx`,HR 会觉得你不专业。 + +## 元数据清理(pdf) + +调用 pdf skill 清理 metadata(作者、创建工具),避免泄露原始 word 模板的来源(比如某些模板会留下"WPS 简历模板 v3.2"字样)。 + +## 隐私 & 安全 + +- 简历**默认不写身份证号、家庭住址、紧急联系人** +- 投行 / 公务员 / 国企等特殊岗位另外要求时再补 +- 如果用户上传的简历里有这些,导出新版本时**主动删除并提示用户** diff --git a/skills/resume-builder/references/intake_questions.md b/skills/resume-builder/references/intake_questions.md new file mode 100755 index 0000000..d902740 --- /dev/null +++ b/skills/resume-builder/references/intake_questions.md @@ -0,0 +1,44 @@ +# 简历信息收集问题清单 + +分 3 轮问,不要一次性塞给用户。 + +## 第 1 轮:基本信息(必问) + +1. 你的姓名 + 联系方式(手机、邮箱,可选 GitHub / 作品集) +2. 学历(学校 + 专业 + 学历层次 + 毕业年份;多段教育依次列) +3. 你目前 / 最近一份工作的:公司、岗位、起止时间 + +## 第 2 轮:经历主体(必问) + +针对每段工作 / 实习 / 项目,按这个套路问: + +- 这段经历你**主要负责什么**? +- 有哪些**具体的事情**是你独立或主导做的?(关键词:你"做了什么",不是"参与了什么") +- **结果如何**?有没有可以量化的数字(用户量、转化率、收入、效率提升、排名等) +- 这段经历你最得意的**一件事**是什么? + +如果用户记不清数字,可以这样追问: +- "大致量级也可以,比如 10 万 / 百万 / 千万" +- "百分比记不清的话,给一个 range,比如『大约 20-30%』" +- "如果完全没办法估,我会标成『[需补具体数字]』,你后面填" + +## 第 3 轮:补充信息(可选) + +- 技能 / 工具:编程语言、框架、设计 / 数据 / 办公工具 +- 语言能力:英语 / 其他语种 + 等级 +- 证书 / 获奖:仅写有含金量的(CFA Level 2、ACM 区域赛、PMP 等) +- 兴趣 / 其他:除非真的有亮点(比如马拉松、独立游戏作品),否则不写 + +## 应届生补充问 + +- 校园经历(学生会、社团、志愿者)—— 但只挑能体现"领导 / 组织 / 推动力"的写 +- 课程项目 —— 挑跟目标岗位相关的 1~2 个 +- GPA / 排名 / 奖学金 —— 高于 3.5/4.0 或专业前 20% 才写 + +## 转行候选人补充问 + +- 你为什么想转到这个方向? +- 你已经为转行做了哪些准备(自学、项目、副业、证书)? +- 原岗位有哪些**可迁移技能**?(举具体例子) + +把这些信息会用到一段简短的"个人简介 / Career Summary"里,化解 HR 看到转行简历时的困惑。 diff --git a/skills/resume-builder/references/keywords/finance.txt b/skills/resume-builder/references/keywords/finance.txt new file mode 100755 index 0000000..2c19505 --- /dev/null +++ b/skills/resume-builder/references/keywords/finance.txt @@ -0,0 +1,50 @@ +DCF +LBO +Comps +M&A +IPO +Pitchbook +Bloomberg +Capital IQ +Wind +Choice +财务建模 +估值 +敏感性分析 +行业研究 +deal +路演 +招股书 +投资逻辑 +catalyst +催化剂 +风险点 +ROE +ROIC +EBITDA +PE +PB +EV +sizing +case interview +hypothesis +MECE +issue tree +PowerPoint +Excel 高阶 +VBA +SQL +Python +CFA +CPA +FRM +ACCA +PMP +量化 +策略 +回测 +因子 +基本面 +技术面 +夏普 +最大回撤 diff --git a/skills/resume-builder/references/keywords/general.txt b/skills/resume-builder/references/keywords/general.txt new file mode 100755 index 0000000..c4b71b3 --- /dev/null +++ b/skills/resume-builder/references/keywords/general.txt @@ -0,0 +1,25 @@ +SQL +Excel +PowerPoint +Word +Notion +飞书 +Jira +Asana +英语 +项目管理 +跨部门协作 +推动落地 +ownership +数据驱动 +用户同理心 +快速学习 +影响力 +说服力 +量化 +可迁移 +0-1 +1-N +ROI +KPI +OKR diff --git a/skills/resume-builder/references/keywords/internet.txt b/skills/resume-builder/references/keywords/internet.txt new file mode 100755 index 0000000..3290653 --- /dev/null +++ b/skills/resume-builder/references/keywords/internet.txt @@ -0,0 +1,45 @@ +产品规划 +产品迭代 +PRD +需求评审 +用户访谈 +用户画像 +用户增长 +留存 +转化率 +A/B 测试 +漏斗分析 +数据驱动 +跨部门协作 +推动落地 +0-1 +1-N +商业化 +PMF +增长黑客 +AARRR +私域 +GMV +ROI +CAC +LTV +SOP +SaaS +B 端 +C 端 +内容运营 +社群运营 +活动运营 +PMP +Scrum +看板 +Sprint +Jira +Confluence +Notion +飞书 +SQL +Tableau +Figma +Axure +墨刀 diff --git a/skills/resume-builder/references/keywords/tech.txt b/skills/resume-builder/references/keywords/tech.txt new file mode 100755 index 0000000..0628142 --- /dev/null +++ b/skills/resume-builder/references/keywords/tech.txt @@ -0,0 +1,69 @@ +Java +Go +Python +TypeScript +Spring Boot +Spring Cloud +gRPC +微服务 +Kafka +RabbitMQ +Redis +ZooKeeper +MySQL +PostgreSQL +MongoDB +ClickHouse +HBase +TiDB +Docker +Kubernetes +Helm +AWS +GCP +阿里云 +高并发 +高可用 +分布式 +一致性 +限流 +降级 +熔断 +分库分表 +读写分离 +React +Vue +Next.js +Vite +Webpack +PyTorch +TensorFlow +sklearn +XGBoost +LightGBM +特征工程 +RAG +向量检索 +Embedding +LLM +SFT +RLHF +Agent +MCP +vLLM +量化 +SQL +Spark +Flink +Hive +Presto +Airflow +A/B 测试 +模型评估 +Prometheus +Grafana +ELK +CI/CD +Jenkins +GitLab CI +Terraform diff --git a/skills/resume-builder/references/star_rewrite_guide.md b/skills/resume-builder/references/star_rewrite_guide.md new file mode 100755 index 0000000..cb89e98 --- /dev/null +++ b/skills/resume-builder/references/star_rewrite_guide.md @@ -0,0 +1,83 @@ +# STAR 改写指南 + +## 核心心法 + +STAR = Situation(情境)+ Task(任务)+ Action(动作)+ Result(结果) + +但**写在简历里的 bullet,不是把 STAR 四段都堆进去**,而是浓缩成一句话,里面隐含 STAR 信号: + +``` +[动作动词] + [对象 / 范围] + [方法 / 工具] + [量化结果] +``` + +举例: + +❌ 弱:参与了 XX 系统的开发 +✅ 强:主导设计并落地 XX 订单系统的库存模块,引入 Redis 分布式锁解决超卖,支撑日均 80 万订单,P99 延迟从 320ms 降至 95ms + +❌ 弱:负责活动运营 +✅ 强:策划"618 老客回流"活动,覆盖 30 万沉默用户,通过分层 push + 优惠券组合,回流率 11.4%(基线 4.2%),ROI 3.8 + +## 改写流程 + +1. **找到原 bullet 的 STAR 短板** + - 没有动作动词?补 + - 没有量化结果?追问用户 + - 动作太空泛("参与/协助/配合/支持")?换成实做动词 + - 没有方法 / 工具?补 + +2. **应用动词库**(见下文) + +3. **加量化**(数字 / 百分比 / 排名 / 时间 / 规模 任选) + +4. **控制长度**:单条 bullet ≤ 2 行 + +## 强动作动词库(按职能分) + +### 通用 +主导、负责、推动、发起、设计、搭建、优化、复盘、孵化、闭环 + +### 产品 / 运营 +规划、迭代、调研、定义(功能)、灰度、A/B 测试、数据归因、上线、回滚 + +### 技术 +重构、架构设计、调优、解耦、抽象、模块化、自动化、监控、容灾、降级 + +### 数据 +建模、清洗、归因、可视化、回归、聚类、特征工程、训练、上线 + +### 销售 / BD +开拓、签约、续约、回款、谈判、漏斗管理、客户分层 + +### 管理 +组建(团队)、培养、绩效管理、目标拆解、跨部门协调 + +## 弱词替换表 + +| 弱词 | 推荐替换 | +|------|---------| +| 参与了 | 主导 / 负责模块 ___(写明边界) | +| 协助 | 与 ___ 协作完成 ___(写明合作方和成果) | +| 配合 | 同上 | +| 跟进 | 推动 / 督办 / drive | +| 帮助 | 通过 ___ 帮 ___ 实现 ___ | +| 学习了 | 掌握并应用 ___ 完成 ___ | +| 接触过 | 删掉,要么改成具体项目 | +| 熟悉 / 了解 | 删掉抽象词,写具体项目 | + +## 量化暗示库 + +如果用户实在给不出数字,可以用以下"软量化"先占位,标注 `[待补]`: + +- 用户 / 客户规模:万级 / 百万级 / 千万级 +- 团队规模:3 人 / 10 人 / 跨部门 +- 项目周期:2 周 / 1 个季度 / 半年 +- 业务影响:核心 / 关键 / 试点 +- 同行对比:行业首个 / 公司第一个 / Tier 1 客户 + +## 自检:每条 bullet 问自己 + +- 用动词开头了吗? +- 有 1 个以上的数字 / 量级吗? +- 删掉这条,简历会损失什么?如果答案是"不会",就删 +- 这条让 HR 联想到什么具体能力? diff --git a/skills/resume-builder/references/templates/finance.md b/skills/resume-builder/references/templates/finance.md new file mode 100755 index 0000000..1415439 --- /dev/null +++ b/skills/resume-builder/references/templates/finance.md @@ -0,0 +1,63 @@ +# 金融 / 咨询 / 商科 简历模板 + +## 结构(投行 / 咨询风格,强调 deal / case 列表) + +``` +姓名 | 电话 | 邮箱 | LinkedIn | 城市 + +【教育背景】(金融 / 咨询岗放最前面,因为学校 + GPA 是硬筛) +学校 | 专业 | 学位 | 起止时间 +- GPA: X.XX/4.00 (Major: X.XX/4.00) +- 排名 / 奖学金 / 荣誉 +- 相关核心课程:Corporate Finance, Financial Modeling, ... +- 交换 / 海外经历 + +【工作经历】(按倒序) +公司 | 部门 / 组 | 岗位 | 起止时间,地点 +- Deal / Case 1:项目名称 + 你的角色 + 关键贡献 + 成交规模 / 成果 +- Deal / Case 2:同上 +- Deal / Case 3:同上 +(投行/PE 一般 3~5 个 deal;咨询 3~5 个 case;研究员可写 coverage 行业 + 主要观点) + +【实习经历】(应届 / 早期必备,按倒序) +(同上结构) + +【技能 & 证书】 +- 财务建模:DCF / LBO / Comps / M&A model 熟练 +- Excel / PowerPoint 高阶 +- Bloomberg / Capital IQ / Wind / Choice +- CFA Level X / CPA / FRM / ACCA +- 编程(量化加分):Python / SQL / VBA +- 语言:英语(雅思 7.5 / GMAT 750)、其他 + +【领导力 / 课外活动】(仅写有含金量的) +- 学生会 / 社团:领导职务 + 量化结果 +- 公益项目:组织规模 + 影响 +- 国际比赛:CFA Research Challenge 区域第 X 等 +``` + +## 风格要点 + +- **每个 deal / case 必须有**:项目类型、客户行业、自己的具体动作(建模 / 路演 / 调研)、成果(融资额 / 估值 / 报告页数 / 客户决策) +- 投行强调 deal flow 数量 + 规模;咨询强调 case 类型多样 + 客户层级 +- 量化模板: + - 投行:"参与 X 亿美金 ___ 行业 IPO/M&A,搭建 LBO / DCF 模型,输出 ___ 页投资人 deck" + - 咨询:"为 X 行业 Top 3 客户开展 ___ 项目,定量分析 X 个细分市场,最终建议被客户采纳并执行 ___" +- **学历分层敏感**:投行咨询 PE 一般只看 Tier 1 学校(清北复交浙、HKU、新加坡国立、藤校 / Top 30 美校等)。如果学校非 Tier 1,要更突出实习 / 比赛 / 自学 + +## 中英文版本 + +金融 / 咨询岗位**很多公司只接受英文简历**(外资行、MBB、IBD)。本 skill 默认产出中文版,但要主动问:"你需要英文版本吗?我可以同时生成。" + +英文版本注意: +- 用 American English(金融业以美式为主) +- Action verb 用过去式:Led、Built、Drove、Analyzed、Advised +- 不要用 we / our team,要用 I / my team;强调 individual contribution +- 一页为限(除非 senior 候选人) + +## 雷区 + +- 投行 / 咨询简历**没有 deal / case 列表** → 致命,重写 +- 把"在 ___ 实习"当工作经历写 → 必须改成 deal / case +- GPA / 学校 / 标化分数缺失 → 即使低也要写(除非真的拉低很多) +- 写"对金融感兴趣" / "希望进入投行" → 删,简历不是个人陈述 diff --git a/skills/resume-builder/references/templates/general.md b/skills/resume-builder/references/templates/general.md new file mode 100755 index 0000000..f76f22a --- /dev/null +++ b/skills/resume-builder/references/templates/general.md @@ -0,0 +1,55 @@ +# 通用简历模板(不绑定行业) + +适用场景: +- 用户暂时没有明确目标方向 +- 跨行业求职(同一份简历投不同方向) +- 早期 / 应届候选人 +- 行业不在前三类(教育 / 医疗 / 政府 / 实业 / 文娱 etc.) + +## 结构 + +``` +姓名 | 电话 | 邮箱 | 城市 | 目标岗位 / 期望方向 + +【个人简介 / Career Summary】(2~3 行) +关键定位 + 核心能力 + 一个最有说服力的成就 + +【工作经历 / 实习经历】(社招倒序;应届实习放教育下方) +公司 | 岗位 | 起止时间 +- 主要职责(一行) +- 关键成果 1(量化) +- 关键成果 2(量化) +- 横向 / 软技能体现(一句) + +【教育背景】 +学校 | 专业 | 学历 | 时间 +- GPA / 排名 / 奖学金(≥ 平均水平才写) +- 相关课程 / 论文 / 项目(仅写与岗位相关的) + +【技能与证书】 +- 工具:Excel / Word / PPT 高阶;Notion / Trello;其他行业工具 +- 语言:英语等级;其他语种 +- 证书:仅写有公信力的 + +【其他亮点】(可选,强力 case 才写) +- 创业 / 副业经历 +- 公开演讲 / 自媒体(粉丝 / 阅读量到一定规模) +- 公益 / 志愿者(持续 ≥ 1 年) +- 体育 / 艺术成就(省级以上 / 商业认可) +``` + +## 风格 + +- 中性、克制,避免过度行业化术语 +- 每段经历强调"可迁移技能":沟通 / 推动 / 数据 / 学习速度 / 客户 / 领导力 +- 跨行业候选人需要在 Summary 里点明"为什么这个方向适合我" + +## 长度 + +- 应届 / 实习生:1 页 +- 1~5 年:1 页(顶多 1.2 页) +- 5+ 年:1~2 页 + +通用模板的最大风险是**显得"啥都行但啥都不精"**。如果发现写出来就是这种感觉,要主动建议用户: +- 选一个最强方向,切到对应的行业模板 +- 或者准备 2 份版本(互联网版 + 通用版),分场景投 diff --git a/skills/resume-builder/references/templates/internet.md b/skills/resume-builder/references/templates/internet.md new file mode 100755 index 0000000..901a234 --- /dev/null +++ b/skills/resume-builder/references/templates/internet.md @@ -0,0 +1,50 @@ +# 互联网产品 / 运营 / PM 简历模板 + +## 结构(顺序很重要) + +``` +姓名|电话|邮箱|城市|目标岗位 +(应届可加:在读学校 + 期望毕业时间) + +【个人简介 / Summary】(2~3 行,可选) +一句话定位:X 年 ___ 经验,专注 ___,擅长 ___,曾在 ___ 主导 ___ + +【工作经历】(社招放最前;应届放教育下面) +公司 A | 岗位 | 起止时间 +- bullet 1(核心成绩,量化) +- bullet 2(次重要成绩) +- bullet 3(横向能力 / 影响力) + +【项目经历】(与岗位强相关的 2~4 个) +项目名 | 角色 | 时间 +- 背景:___(一句话) +- 动作:___(你做了什么) +- 结果:___(量化) + +【教育背景】 +学校 | 专业 | 学历 | 起止时间 +(GPA 高于 3.5/4.0 才写) + +【技能 / 工具】 +- 数据:SQL(熟练)、Python(pandas/numpy)、Tableau +- 产品工具:Figma / Axure / 墨刀 +- 协作:Notion / 飞书 / Jira +- 语言:英语 CET-6 / 雅思 7.0 + +【其他】(可选) +证书 / 自媒体 / 开源项目 / 演讲 / 获奖 +``` + +## 风格要点 + +- bullet 句式:"动词 + 对象 + 方法 + 结果(数字)" +- 突出 ownership:用"主导/负责/推动",不要用"参与/协助" +- 数字密度:每段经历 ≥ 60% 的 bullet 含数字 +- 长度:1 页(≤ 3 年经验);最多 2 页(5+ 年) + +## 常见加分项 + +- 0-1 项目经历(哪怕规模小) +- 跨部门 / 跨业务线协作 +- 业务复盘文章 / 公开演讲 +- 数据驱动 case(写清楚假设→实验→结论) diff --git a/skills/resume-builder/references/templates/tech.md b/skills/resume-builder/references/templates/tech.md new file mode 100755 index 0000000..722e036 --- /dev/null +++ b/skills/resume-builder/references/templates/tech.md @@ -0,0 +1,53 @@ +# 技术 / 研发 / 数据 简历模板 + +## 结构 + +``` +姓名|电话|邮箱|GitHub|城市|目标岗位 + +【个人简介 / Summary】(可选,1~2 行) +X 年 ___ 经验,专注 ___(高并发后端 / 推荐系统 / LLM 应用 / ...),熟悉 ___ 技术栈 + +【技能】(技术岗放前面,让 HR 一眼看到栈是否对口) +- 语言:Java(精通)、Python(熟练)、Go(了解) +- 后端:Spring Boot、Spring Cloud、gRPC、Kafka、Redis、MySQL、ClickHouse +- 大数据 / 算法:Spark、Flink、Hive、PyTorch(如果是算法岗) +- 工程:Docker、Kubernetes、Linux、Git、CI/CD +- 其他:TypeScript、AWS + +【工作经历】 +公司 A | 职级(高级工程师 / 资深工程师 / Tech Lead)| 起止 +- 系统级成绩(含 QPS、延迟、可用性、规模等数字) +- 架构 / 设计贡献(你设计了什么、为什么这么选) +- 业务影响(推动了什么业务结果) + +【项目经历】 +项目名 | 角色 | 技术栈 | 时间 +- 背景:业务规模 / 痛点(一句话) +- 方案:你的核心设计 + 关键技术决策(讲为什么) +- 结果:性能 / 稳定性 / 成本 数字 + +【教育背景】 + +【开源 / 论文 / 比赛】(强加分) +- GitHub 项目(star 数 / 主要 contributor) +- 论文(标注会议 / 期刊) +- 比赛(Kaggle / ACM / 天池 / 黑客松,含名次) +``` + +## 风格要点 + +- **少形容词,多数字 + 技术细节** + - 弱:"优化了系统性能" + - 强:"通过引入二级缓存 + SQL 改写,将商品详情接口 P99 从 480ms 降至 110ms" +- **讲清楚『你做了什么』vs『团队做了什么』**:用"主导"/"负责"明确 ownership 边界 +- 算法岗:技能列表里**强调具体模型 / 框架版本**(避免堆砌 buzzword) +- LLM / Agent 岗:明确写"训练 / 微调 / 推理 / 应用"中你做的部分,避免被以为是 prompt-only +- 长度:1~2 页 + +## 常见雷区 + +- 简历里写"精通",面试问到一脸懵 → 不熟就写"熟练 / 了解" +- 列出 20+ 技能,问哪个都不深 → 删到 8~12 个真正会的 +- 把课程内容当项目经历 → 标注"课程项目 (Course Project)",不要假装是工业项目 +- 项目描述只有"什么是 ___",没有"我做了什么" → 重写 diff --git a/skills/resume-builder/scripts/ats_check.py b/skills/resume-builder/scripts/ats_check.py new file mode 100755 index 0000000..f33902f --- /dev/null +++ b/skills/resume-builder/scripts/ats_check.py @@ -0,0 +1,269 @@ +#!/usr/bin/env python3 +""" +ats_check.py — 简历 ATS(Applicant Tracking System)友好度检查 + 关键词覆盖率 + +用法: + python ats_check.py --resume resume.md --industry internet + python ats_check.py --resume resume.md --industry tech --jd jd.txt + python ats_check.py --resume resume.docx --industry finance --out report.md + +支持输入:.md / .txt / .docx(docx 走 python-docx,需要 pip install python-docx) +""" + +from __future__ import annotations + +import argparse +import re +import sys +from pathlib import Path + +INDUSTRIES = {"internet", "tech", "finance", "general"} + + +def load_resume_text(path: Path) -> str: + suffix = path.suffix.lower() + if suffix in {".md", ".txt"}: + return path.read_text(encoding="utf-8") + if suffix == ".docx": + try: + from docx import Document + except ImportError: + print( + "✗ 缺少 python-docx,请先安装:pip install python-docx --break-system-packages", + file=sys.stderr, + ) + sys.exit(1) + doc = Document(str(path)) + return "\n".join(p.text for p in doc.paragraphs) + print(f"✗ 暂不支持的格式:{suffix}", file=sys.stderr) + sys.exit(1) + + +def load_keywords(industry: str, references_dir: Path) -> list[str]: + path = references_dir / "keywords" / f"{industry}.txt" + if not path.exists(): + print(f"✗ 关键词库不存在:{path}", file=sys.stderr) + sys.exit(1) + return [ + line.strip() + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + + +def coverage(resume_text: str, keywords: list[str]) -> dict: + text_lower = resume_text.lower() + hits, missing = [], [] + for kw in keywords: + # 兼容大小写、中英混排 + if kw.lower() in text_lower: + hits.append(kw) + else: + missing.append(kw) + return { + "hits": hits, + "missing": missing, + "rate": len(hits) / len(keywords) if keywords else 0, + } + + +def jd_extract_keywords(jd_text: str) -> list[str]: + """从 JD 文本里抽取候选关键词。简易版:取常见技能/工具/动词。""" + # 抓中文 2~10 字、英文 2~30 字的"实词" + candidates = re.findall( + r"[A-Za-z][A-Za-z0-9+/.\-_]{1,29}|[一-龥]{2,10}", + jd_text, + ) + # 简单去停用词 + stop = { + "公司", "工作", "我们", "你将", "团队", "需要", "能够", "具备", "熟悉", + "了解", "良好", "优秀", "经验", "能力", "岗位", "职责", "要求", "以上", + "相关", "及其", "或者", "进行", "完成", "负责", "推动", "实现", "提升", + "并且", "包括", "以下", "根据", + } + seen = set() + out = [] + for c in candidates: + key = c.lower() + if key in seen or c in stop: + continue + seen.add(key) + out.append(c) + return out[:80] # 取前 80 个候选 + + +def ats_friendliness(text: str, source_path: Path) -> tuple[int, list[str]]: + """评估 ATS 友好度,返回 (分数 / 10, 警告列表)。""" + score = 10 + warnings = [] + + # 长度 + if len(text) < 200: + score -= 3 + warnings.append("⚠ 简历文本过短(< 200 字符),可能解析失败或内容不足") + elif len(text) > 6000: + score -= 1 + warnings.append("⚠ 简历偏长(> 6000 字符),建议精简到 1~2 页") + + # 联系方式 + has_email = bool(re.search(r"[\w.\-+]+@[\w.\-]+\.\w+", text)) + has_phone = bool(re.search(r"(\+?86[-\s]?)?1[3-9]\d{9}|\d{3}[-\s]?\d{4}[-\s]?\d{4}", text)) + if not has_email: + score -= 1 + warnings.append("⚠ 没找到邮箱") + if not has_phone: + score -= 1 + warnings.append("⚠ 没找到手机号") + + # 装饰符号 + decorative = re.findall(r"[★☆●○◆◇▶▷■□▪▫♦]", text) + if len(decorative) > 5: + score -= 1 + warnings.append(f"⚠ 用了 {len(decorative)} 个装饰符号(★●◆等),ATS 可能识别异常,建议精简") + + # emoji + emojis = re.findall(r"[\U0001F300-\U0001FAFF\U0001F600-\U0001F64F]", text) + if emojis: + score -= 1 + warnings.append(f"⚠ 检测到 {len(emojis)} 个 emoji,部分 ATS 会乱码,建议删掉") + + # 数字密度(量化结果是否充分) + numbers = re.findall(r"\d+(?:\.\d+)?%?", text) + bullet_count = len(re.findall(r"^\s*[-*•]\s+", text, flags=re.MULTILINE)) + if bullet_count > 0: + density = len(numbers) / bullet_count + if density < 0.4: + score -= 1 + warnings.append( + f"⚠ 量化密度低(每条 bullet 平均 {density:.2f} 个数字)," + f"建议在工作 / 项目经历里多加数字" + ) + + # docx 特定 + if source_path.suffix.lower() == ".docx": + try: + from docx import Document + doc = Document(str(source_path)) + tables = len(doc.tables) + images = sum(1 for s in doc.inline_shapes) + if tables > 1: + score -= 1 + warnings.append( + f"⚠ docx 里有 {tables} 个表格,部分 ATS 解析表格会丢字段," + f"建议改成正文段落" + ) + if images > 0: + score -= 1 + warnings.append( + f"⚠ docx 里有 {images} 张图片(含证件照),ATS 不读图," + f"重要信息别只放在图里;证件照可保留" + ) + except Exception: + pass + + return max(0, score), warnings + + +def render_report( + industry_cov: dict, + industry: str, + jd_cov: dict | None, + ats_score: int, + ats_warnings: list[str], +) -> str: + lines = [ + "# 简历 ATS 检查报告", + "", + f"## 行业关键词覆盖({industry})", + f"- 命中率:**{industry_cov['rate'] * 100:.1f}%** " + f"({len(industry_cov['hits'])} / {len(industry_cov['hits']) + len(industry_cov['missing'])})", + "", + "**已命中**:" + (", ".join(industry_cov["hits"]) or "(无)"), + "", + "**建议补充**(前 15 个):" + (", ".join(industry_cov["missing"][:15]) or "(无)"), + "", + ] + + if jd_cov is not None: + lines += [ + "## JD 关键词覆盖", + f"- 命中率:**{jd_cov['rate'] * 100:.1f}%**", + "", + "**已命中**:" + (", ".join(jd_cov["hits"][:30]) or "(无)"), + "", + "**JD 出现但简历没有**(重点补这些):" + (", ".join(jd_cov["missing"][:20]) or "(无)"), + "", + ] + + lines += [ + f"## ATS 友好度评分:**{ats_score}/10**", + "", + ] + if ats_warnings: + lines += ats_warnings + else: + lines.append("✅ 没有明显问题") + + lines += [ + "", + "---", + "## 改进建议优先级", + "", + "1. 先补 JD 命中率 → 这是 ATS 通过率的最直接信号", + "2. 再补行业关键词 → 让简历能在更宽的搜索里被捞到", + "3. 最后调 ATS 友好度 → 移除装饰符号、emoji、表格、图片", + "", + "注意:覆盖率不是越高越好,**关键词必须出现在真实的成就 bullet 里**,", + "不要把关键词单独列一长串当 skills,会被 HR 一眼识破。", + ] + return "\n".join(lines) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--resume", required=True, help="简历文件路径 (.md/.txt/.docx)") + parser.add_argument( + "--industry", choices=list(INDUSTRIES), required=True, help="行业方向" + ) + parser.add_argument("--jd", help="可选:JD 文本文件,做精准对比") + parser.add_argument("--out", help="输出报告路径,缺省直接打印") + parser.add_argument( + "--references-dir", + default=str(Path(__file__).resolve().parent.parent / "references"), + ) + args = parser.parse_args() + + resume_path = Path(args.resume).expanduser() + if not resume_path.exists(): + print(f"✗ 简历文件不存在:{resume_path}", file=sys.stderr) + sys.exit(1) + + text = load_resume_text(resume_path) + references_dir = Path(args.references_dir) + + industry_keywords = load_keywords(args.industry, references_dir) + industry_cov = coverage(text, industry_keywords) + + jd_cov = None + if args.jd: + jd_path = Path(args.jd).expanduser() + if not jd_path.exists(): + print(f"✗ JD 文件不存在:{jd_path}", file=sys.stderr) + sys.exit(1) + jd_keywords = jd_extract_keywords(jd_path.read_text(encoding="utf-8")) + jd_cov = coverage(text, jd_keywords) + + ats_score, ats_warnings = ats_friendliness(text, resume_path) + report = render_report(industry_cov, args.industry, jd_cov, ats_score, ats_warnings) + + if args.out: + out_path = Path(args.out).expanduser() + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(report, encoding="utf-8") + print(f"✓ 报告已生成:{out_path}") + else: + print(report) + + +if __name__ == "__main__": + main() diff --git a/skills/seo-content-writer/SKILL.md b/skills/seo-content-writer/SKILL.md new file mode 100755 index 0000000..18f8d05 --- /dev/null +++ b/skills/seo-content-writer/SKILL.md @@ -0,0 +1,661 @@ +--- +name: seo-content-writer +description: 'Use when the user asks to "write SEO content", "create a blog post", "write an article", "content writing", "draft optimized content", "write me an article", "create a blog post about", "help me write SEO content", or "draft content for". Creates high-quality, SEO-optimized content that ranks in search engines. Applies on-page SEO best practices, keyword optimization, and content structure for maximum visibility and engagement. For AI citation optimization, see geo-content-optimizer. For updating existing content, see content-refresher.' +license: Apache-2.0 +metadata: + author: aaron-he-zhu + version: "2.0.0" + geo-relevance: "medium" + tags: + - seo + - content writing + - blog post + - article + - copywriting + - content creation + - on-page seo + triggers: + - "write SEO content" + - "create blog post" + - "write an article" + - "content writing" + - "draft optimized content" + - "write for SEO" + - "blog writing" + - "write me an article" + - "create a blog post about" + - "help me write SEO content" + - "draft content for" +--- + +# SEO Content Writer + + +> **[SEO & GEO Skills Library](https://skills.sh/aaron-he-zhu/seo-geo-claude-skills)** · 20 skills for SEO + GEO · Install all: `npx skills add aaron-he-zhu/seo-geo-claude-skills` + +
+Browse all 20 skills + +**Research** · [keyword-research](../../research/keyword-research/) · [competitor-analysis](../../research/competitor-analysis/) · [serp-analysis](../../research/serp-analysis/) · [content-gap-analysis](../../research/content-gap-analysis/) + +**Build** · **seo-content-writer** · [geo-content-optimizer](../geo-content-optimizer/) · [meta-tags-optimizer](../meta-tags-optimizer/) · [schema-markup-generator](../schema-markup-generator/) + +**Optimize** · [on-page-seo-auditor](../../optimize/on-page-seo-auditor/) · [technical-seo-checker](../../optimize/technical-seo-checker/) · [internal-linking-optimizer](../../optimize/internal-linking-optimizer/) · [content-refresher](../../optimize/content-refresher/) + +**Monitor** · [rank-tracker](../../monitor/rank-tracker/) · [backlink-analyzer](../../monitor/backlink-analyzer/) · [performance-reporter](../../monitor/performance-reporter/) · [alert-manager](../../monitor/alert-manager/) + +**Cross-cutting** · [content-quality-auditor](../../cross-cutting/content-quality-auditor/) · [domain-authority-auditor](../../cross-cutting/domain-authority-auditor/) · [entity-optimizer](../../cross-cutting/entity-optimizer/) · [memory-management](../../cross-cutting/memory-management/) + +
+ +This skill creates search-engine-optimized content that ranks well while providing genuine value to readers. It applies proven SEO copywriting techniques, proper keyword integration, and optimal content structure. + +## When to Use This Skill + +- Writing blog posts targeting specific keywords +- Creating landing pages optimized for search +- Developing pillar content for topic clusters +- Writing product descriptions for e-commerce +- Creating service pages for local SEO +- Producing how-to guides and tutorials +- Writing comparison and review articles + +## What This Skill Does + +1. **Keyword Integration**: Naturally incorporates target and related keywords +2. **Structure Optimization**: Creates scannable, well-organized content +3. **Title & Meta Creation**: Writes compelling, click-worthy titles +4. **Header Optimization**: Uses strategic H1-H6 hierarchy +5. **Internal Linking**: Suggests relevant internal link opportunities +6. **Readability Enhancement**: Ensures content is accessible and engaging +7. **Featured Snippet Optimization**: Formats for SERP feature opportunities + +## How to Use + +### Basic Content Creation + +``` +Write an SEO-optimized article about [topic] targeting the keyword [keyword] +``` + +``` +Create a blog post for [topic] with these keywords: [keyword list] +``` + +### With Specific Requirements + +``` +Write a 2,000-word guide about [topic] targeting [keyword], +include FAQ section for featured snippets +``` + +### Content Briefs + +``` +Here's my content brief: [brief]. Write SEO-optimized content following this outline. +``` + +## Data Sources + +> See [CONNECTORS.md](../../CONNECTORS.md) for tool category placeholders. + +**With ~~SEO tool + ~~search console connected:** +Automatically pull keyword metrics (search volume, difficulty, CPC), competitor content analysis (top-ranking pages, content length, common topics), SERP features (featured snippets, PAA questions), and keyword opportunities (related keywords, question-based queries). + +**With manual data only:** +Ask the user to provide: +1. Target primary keyword and 3-5 secondary keywords +2. Target audience and search intent (informational/commercial/transactional) +3. Target word count and desired tone +4. Any competitor URLs or content examples to reference + +Proceed with the full workflow using provided data. Note in the output which metrics are from automated collection vs. user-provided data. + +## Instructions + +When a user requests SEO content: + +1. **Gather Requirements** + + Confirm or ask for: + + ```markdown + ### Content Requirements + + **Primary Keyword**: [main keyword] + **Secondary Keywords**: [2-5 related keywords] + **Target Word Count**: [length] + **Content Type**: [blog/guide/landing page/etc.] + **Target Audience**: [who is this for] + **Search Intent**: [informational/commercial/transactional] + **Tone**: [professional/casual/technical/friendly] + **CTA Goal**: [what action should readers take] + **Competitor URLs**: [top ranking content to beat] + ``` + +2. **Load CORE-EEAT Quality Constraints** + + Before writing, load content quality standards from the [CORE-EEAT Benchmark](../../references/core-eeat-benchmark.md): + + ```markdown + ### CORE-EEAT Pre-Write Checklist + + **Content Type**: [identified from requirements above] + **Loaded Constraints** (high-weight items for this content type): + + Apply these standards while writing: + + | ID | Standard | How to Apply | + |----|----------|-------------| + | C01 | Intent Alignment | Title promise must match content delivery | + | C02 | Direct Answer | Core answer in first 150 words | + | C06 | Audience Targeting | State "this article is for..." | + | C10 | Semantic Closure | Conclusion answers opening question + next steps | + | O01 | Heading Hierarchy | H1→H2→H3, no level skipping | + | O02 | Summary Box | Include TL;DR or Key Takeaways | + | O06 | Section Chunking | Each section single topic; paragraphs 3–5 sentences | + | O09 | Information Density | No filler; consistent terminology | + | R01 | Data Precision | ≥5 precise numbers with units | + | R02 | Citation Density | ≥1 external citation per 500 words | + | R04 | Evidence-Claim Mapping | Every claim backed by evidence | + | R07 | Entity Precision | Full names for people/orgs/products | + | C03 | Query Coverage | Cover ≥3 query variants (synonyms, long-tail) | + | O08 | Anchor Navigation | Table of contents with jump links | + | O10 | Multimedia Structure | Images/videos have captions and carry information | + | E07 | Practical Tools | Include downloadable templates, checklists, or calculators | + + _These 16 items apply across all content types. For content-type-specific dimension weights, see the Content-Type Weight Table in [core-eeat-benchmark.md](../../references/core-eeat-benchmark.md)._ + _Full 80-item benchmark: [references/core-eeat-benchmark.md](../../references/core-eeat-benchmark.md)_ + _For complete content quality audit: use [content-quality-auditor](../../cross-cutting/content-quality-auditor/)_ + ``` + +3. **Research and Plan** + + Before writing: + + ```markdown + ### Content Research + + **SERP Analysis**: + - Top results format: [what's ranking] + - Average word count: [X] words + - Common sections: [list] + - SERP features: [snippets, PAA, etc.] + + **Keyword Map**: + - Primary: [keyword] - use in title, H1, intro, conclusion + - Secondary: [keywords] - use in H2s, body paragraphs + - LSI/Related: [terms] - sprinkle naturally throughout + - Questions: [PAA questions] - use as H2/H3s or FAQ + + **Content Angle**: + [What unique perspective or value will this content provide?] + ``` + +4. **Create Optimized Title** + + ```markdown + ### Title Optimization + + **Requirements**: + - Include primary keyword (preferably at start) + - Under 60 characters for full SERP display + - Compelling and click-worthy + - Match search intent + + **Title Options**: + + 1. [Title option 1] ([X] chars) + - Keyword position: [front/middle] + - Power words: [list] + + 2. [Title option 2] ([X] chars) + - Keyword position: [front/middle] + - Power words: [list] + + **Recommended**: [Best option with reasoning] + ``` + +5. **Write Meta Description** + + ```markdown + ### Meta Description + + **Requirements**: + - 150-160 characters + - Include primary keyword naturally + - Include call-to-action + - Compelling and specific + + **Meta Description**: + "[Description text]" ([X] characters) + + **Elements included**: + - ✅ Primary keyword + - ✅ Value proposition + - ✅ CTA or curiosity hook + ``` + +6. **Structure Content with SEO Headers** + + ```markdown + ### Content Structure + + **H1**: [Primary keyword in H1 - only one per page] + + **Introduction** (100-150 words) + - Hook reader in first sentence + - State what they'll learn + - Include primary keyword in first 100 words + + **H2**: [Secondary keyword or question] + [Content section] + + **H2**: [Secondary keyword or question] + + **H3**: [Sub-topic] + [Content] + + **H3**: [Sub-topic] + [Content] + + **H2**: [Secondary keyword or question] + [Content] + + **H2**: Frequently Asked Questions + [FAQ section for PAA optimization] + + **Conclusion** + - Summarize key points + - Include primary keyword + - Clear call-to-action + ``` + +7. **Apply On-Page SEO Best Practices** + + ```markdown + ### On-Page SEO Checklist + + **Keyword Placement**: + - [ ] Primary keyword in title + - [ ] Primary keyword in H1 + - [ ] Primary keyword in first 100 words + - [ ] Primary keyword in at least one H2 + - [ ] Primary keyword in conclusion + - [ ] Primary keyword in meta description + - [ ] Secondary keywords in H2s/H3s + - [ ] Related terms throughout body + + **Content Quality**: + - [ ] Comprehensive coverage of topic + - [ ] Original insights or data + - [ ] Actionable takeaways + - [ ] Examples and illustrations + - [ ] Expert quotes or citations (for E-E-A-T) + + **Readability**: + - [ ] Paragraphs of 3-5 sentences (per CORE-EEAT O06 Section Chunking standard) + - [ ] Varied sentence length + - [ ] Bullet points and lists + - [ ] Bold key phrases + - [ ] Table of contents for long content + + **Technical**: + - [ ] Internal links to relevant pages (2-5) + - [ ] External links to authoritative sources (2-3) + - [ ] Image alt text with keywords + - [ ] URL slug includes keyword + ``` + +8. **Write the Content** + + Follow this structure: + + ```markdown + # [H1 with Primary Keyword] + + [Hook sentence that grabs attention] + + [Problem statement or context - why this matters] + + [Promise - what the reader will learn/gain] [Include primary keyword naturally] + + [Brief overview of what's covered - can be bullet points for scanability] + + ## [H2 - First Main Section with Secondary Keyword] + + [Introduction to section - 1-2 sentences] + + [Main content with valuable information] + + [Examples, data, or evidence to support points] + + [Transition to next section] + + ### [H3 - Sub-section if needed] + + [Detailed content] + + [Key points in bullet format]: + - Point 1 + - Point 2 + - Point 3 + + ## [H2 - Second Main Section] + + [Continue with valuable content...] + + > **Pro Tip**: [Highlighted tip or key insight] + + | Column 1 | Column 2 | Column 3 | + |----------|----------|----------| + | Data | Data | Data | + + ## [H2 - Additional Sections as Needed] + + [Content...] + + ## Frequently Asked Questions + + ### [Question from PAA or common query]? + + [Direct, concise answer in 40-60 words for featured snippet opportunity] + + ### [Question 2]? + + [Answer] + + ### [Question 3]? + + [Answer] + + ## Conclusion + + [Summary of key points - include primary keyword] + + [Final thought or insight] + + [Clear call-to-action: what should reader do next?] + ``` + +9. **Optimize for Featured Snippets** + + ```markdown + ### Featured Snippet Optimization + + **For Definition Snippets**: + "[Term] is [clear, concise definition in 40-60 words]" + + **For List Snippets**: + Create clear, numbered or bulleted lists under H2s + + **For Table Snippets**: + Use comparison tables with clear headers + + **For How-To Snippets**: + Number each step clearly: "Step 1:", "Step 2:", etc. + ``` + +10. **Add Internal/External Links** + + ```markdown + ### Link Recommendations + + **Internal Links** (include 2-5): + 1. "[anchor text]" → [/your-page-url] (relevant because: [reason]) + 2. "[anchor text]" → [/your-page-url] (relevant because: [reason]) + + **External Links** (include 2-3 authoritative sources): + 1. "[anchor text]" → [authoritative-source.com] (supports: [claim]) + 2. "[anchor text]" → [authoritative-source.com] (supports: [claim]) + ``` + +11. **Final SEO Review** + + ```markdown + ### Content SEO Score + + | Factor | Status | Notes | + |--------|--------|-------| + | Title optimized | ✅/⚠️/❌ | [notes] | + | Meta description | ✅/⚠️/❌ | [notes] | + | H1 with keyword | ✅/⚠️/❌ | [notes] | + | Keyword in first 100 words | ✅/⚠️/❌ | [notes] | + | H2s optimized | ✅/⚠️/❌ | [notes] | + | Internal links | ✅/⚠️/❌ | [notes] | + | External links | ✅/⚠️/❌ | [notes] | + | FAQ section | ✅/⚠️/❌ | [notes] | + | Readability | ✅/⚠️/❌ | [notes] | + | Word count | ✅/⚠️/❌ | [X] words | + + **Overall SEO Score**: [X]/10 + + **Improvements to Consider**: + 1. [Suggestion] + 2. [Suggestion] + ``` + +12. **CORE-EEAT Self-Check** + + After writing, verify content against loaded CORE-EEAT constraints: + + ```markdown + ### CORE-EEAT Post-Write Check + + | ID | Standard | Status | Notes | + |----|----------|--------|-------| + | C01 | Intent Alignment: title = content | ✅/⚠️/❌ | [notes] | + | C02 | Direct Answer in first 150 words | ✅/⚠️/❌ | [notes] | + | C06 | Audience explicitly stated | ✅/⚠️/❌ | [notes] | + | C10 | Conclusion answers opening question | ✅/⚠️/❌ | [notes] | + | O01 | Heading hierarchy correct | ✅/⚠️/❌ | [notes] | + | O02 | Summary/Key Takeaways present | ✅/⚠️/❌ | [notes] | + | O06 | Paragraphs 3–5 sentences | ✅/⚠️/❌ | [notes] | + | O09 | No filler; consistent terms | ✅/⚠️/❌ | [notes] | + | R01 | ≥5 precise data points with units | ✅/⚠️/❌ | [notes] | + | R02 | ≥1 citation per 500 words | ✅/⚠️/❌ | [notes] | + | R04 | Claims backed by evidence | ✅/⚠️/❌ | [notes] | + | R07 | Full entity names used | ✅/⚠️/❌ | [notes] | + | C03 | ≥3 query variants covered | ✅/⚠️/❌ | [notes] | + | O08 | Table of contents with jump links | ✅/⚠️/❌ | [notes] | + | O10 | Images/videos captioned and informative | ✅/⚠️/❌ | [notes] | + | E07 | Downloadable template/checklist included | ✅/⚠️/❌ | [notes] | + + **Items Needing Attention**: [list any ⚠️/❌ items] + + _For full 80-item audit, use [content-quality-auditor](../../cross-cutting/content-quality-auditor/)_ + ``` + +## Validation Checkpoints + +### Input Validation +- [ ] Primary keyword confirmed and matches search intent +- [ ] Target word count specified (minimum 800 words for substantive content) +- [ ] Content type and audience clearly defined +- [ ] Competitor URLs reviewed or target SERP features identified + +### Output Validation +- [ ] Keyword density within 1-2% for primary keyword (Note: Keyword density is a guideline, not a hard rule. Modern search engines prioritize semantic relevance and natural language over exact density targets. Focus on covering the topic comprehensively with semantic variants rather than hitting a specific percentage.) +- [ ] All sections from outline covered completely +- [ ] Internal links included (2-5 relevant links) +- [ ] FAQ section present with at least 3 questions +- [ ] Readability score appropriate for target audience +- [ ] Source of each data point clearly stated (~~SEO tool data, user-provided, or estimated) + +## Example + +**User**: "Write an SEO-optimized article about 'email marketing best practices' targeting small businesses" + +**Output**: + +```markdown +# Email Marketing Best Practices: A Complete Guide for Small Businesses + +**Meta Description**: Discover proven email marketing best practices that help small businesses increase open rates, boost engagement, and drive sales. Start improving your campaigns today. (158 chars) + +--- + +Want to know the secret behind small businesses that consistently turn subscribers into loyal customers? It's not luck—it's strategic email marketing. + +Email marketing remains one of the most effective channels for small businesses, delivering an average ROI of $42 for every $1 spent. But with inboxes more crowded than ever, following email marketing best practices isn't optional—it's essential for survival. + +In this guide, you'll learn: +- How to build a quality email list that converts +- Proven strategies to increase open and click rates +- Advanced personalization techniques that drive results +- Common mistakes that kill email performance + +Let's dive into the strategies that will transform your email marketing. + +## Why Email Marketing Matters for Small Businesses + +Before we explore the best practices, let's understand why email deserves your attention. + +Unlike social media where algorithms control who sees your content, email gives you direct access to your audience. You own your email list—no platform can take it away. + +**Key email marketing statistics for small businesses**: +- 81% of SMBs rely on email as their primary customer acquisition channel +- Email subscribers are 3x more likely to share content on social media +- Personalized emails generate 6x higher transaction rates + +## Building a High-Quality Email List + +### Use Strategic Opt-in Incentives + +The foundation of effective email marketing is a quality list. Here's how to grow yours: + +**Lead magnets that convert**: +- Industry-specific templates +- Exclusive discounts or early access +- Free tools or calculators +- Educational email courses + +> **Pro Tip**: The best lead magnets solve a specific, immediate problem for your target audience. + +### Implement Double Opt-in + +Double opt-in confirms subscriber intent and improves deliverability. Yes, you'll have fewer subscribers, but they'll be more engaged. + +| Single Opt-in | Double Opt-in | +|---------------|---------------| +| More subscribers | Fewer subscribers | +| Higher bounce rates | Lower bounce rates | +| Lower engagement | Higher engagement | +| Spam risk | Better deliverability | + +## Crafting Emails That Get Opened + +### Write Compelling Subject Lines + +Your subject line determines whether emails get opened or ignored. + +**Subject line best practices**: +1. Keep it under 50 characters +2. Create urgency without being spammy +3. Use numbers and specifics +4. Personalize when possible +5. A/B test consistently + +### Optimize Preview Text + +The preview text (preheader) is your second chance to convince someone to open. Use it to expand on your subject line, not repeat it. + +## Personalization Strategies That Work + +Generic emails perform poorly. Here's how to personalize effectively: + +### Segment Your Email List + +Divide your list based on: +- Purchase history +- Engagement level +- Demographics +- Interests or preferences +- Customer lifecycle stage + +### Use Dynamic Content + +Send the same email but show different content blocks based on subscriber data. + +## Frequently Asked Questions + +### How often should small businesses send marketing emails? + +For most small businesses, sending 1-2 emails per week strikes the right balance. This keeps you top-of-mind without overwhelming subscribers. Monitor your unsubscribe rates—if they spike after increasing frequency, scale back. + +### What is a good email open rate for small businesses? + +A good email open rate for small businesses is 20-25%. However, this varies by industry. E-commerce averages 15-20%, while B2B services often see 25-30%. Focus on improving your own benchmarks rather than industry averages. + +### Should I buy email lists to grow faster? + +Never buy email lists. Purchased lists violate GDPR and CAN-SPAM regulations, damage your sender reputation, and result in poor engagement. Organic list growth, while slower, produces much better results. + +## Conclusion + +Implementing these email marketing best practices will help your small business build stronger customer relationships and drive consistent revenue. Remember: quality always beats quantity in email marketing. + +Start with one improvement today. Whether it's segmenting your list, testing subject lines, or cleaning inactive subscribers, small changes compound into significant results. + +**Ready to level up your email marketing?** Download our free Email Marketing Checklist to ensure every campaign you send follows these best practices. + +--- + +*Further reading: [Internal link: "How to Write Email Copy That Converts"]* + +*Sources: [Litmus Email Marketing Statistics](https://litmus.com), [Campaign Monitor Email Benchmarks](https://campaignmonitor.com)* +``` + +## Content Type Templates + +### How-To Guide + +``` +Write a how-to guide for [task] targeting [keyword] +``` + +### Comparison Article + +``` +Write a comparison article: [Option A] vs [Option B] for [keyword] +``` + +### Listicle + +``` +Write a list post: "X Best [Items] for [Audience/Purpose]" targeting [keyword] +``` + +### Ultimate Guide + +``` +Write an ultimate guide about [topic] (3,000+ words) targeting [keyword] +``` + +## Tips for Success + +1. **Match search intent** - Informational queries need guides, not sales pages +2. **Front-load value** - Put key information early for readers and snippets +3. **Use data and examples** - Specific beats generic every time +4. **Write for humans first** - SEO optimization should feel natural +5. **Include visual elements** - Break up text with images, tables, lists +6. **Update regularly** - Fresh content signals to search engines + +## Reference Materials + +- [Title Formulas](./references/title-formulas.md) - Proven headline formulas, power words, CTR patterns +- [Content Structure Templates](./references/content-structure-templates.md) - Templates for blog posts, comparisons, listicles, how-tos, pillar pages + +## Related Skills + +- [keyword-research](../../research/keyword-research/) — Find keywords to target +- [geo-content-optimizer](../geo-content-optimizer/) — Optimize for AI citations +- [meta-tags-optimizer](../meta-tags-optimizer/) — Create compelling meta tags +- [on-page-seo-auditor](../../optimize/on-page-seo-auditor/) — Audit SEO elements +- [internal-linking-optimizer](../../optimize/internal-linking-optimizer/) — Place internal links during content writing +- [content-refresher](../../optimize/content-refresher/) — Refresh and update existing content +- [content-quality-auditor](../../cross-cutting/content-quality-auditor/) — Full 80-item CORE-EEAT audit +- [memory-management](../../cross-cutting/memory-management/) — Track content performance over time +- [content-gap-analysis](../../research/content-gap-analysis/) — Identify content opportunities to write about +- [schema-markup-generator](../schema-markup-generator/) — Add structured data to published content + diff --git a/skills/seo-content-writer/_meta.json b/skills/seo-content-writer/_meta.json new file mode 100755 index 0000000..a86ab55 --- /dev/null +++ b/skills/seo-content-writer/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn73qjxwmbna25qq8q051epqt980sys5", + "slug": "seo-content-writer", + "version": "2.0.0", + "publishedAt": 1771042677304 +} \ No newline at end of file diff --git a/skills/seo-content-writer/references/content-structure-templates.md b/skills/seo-content-writer/references/content-structure-templates.md new file mode 100755 index 0000000..35a5e29 --- /dev/null +++ b/skills/seo-content-writer/references/content-structure-templates.md @@ -0,0 +1,875 @@ +# Content Structure Templates + +Markdown templates for common SEO content types. Customize section headings and content while maintaining the structural framework. + +## Blog Post (Informational) + +**Target word count**: 1,200-1,800 words +**Primary goal**: Educate and inform, build authority, rank for informational queries + +```markdown +# [Primary Keyword in H1] - [Benefit or Hook] + +[Opening hook - 1 compelling sentence that grabs attention] + +[Problem statement - describe the pain point or challenge your reader faces] + +[Promise - explain what readers will learn or gain from this post] + +In this guide, you'll discover: +- [Key takeaway 1] +- [Key takeaway 2] +- [Key takeaway 3] + +## What Is [Topic]? + +[Clear definition in 40-60 words - GEO-optimized] + +[Expanded explanation with context] + +[Why this matters - relevance to reader] + +## Why [Topic] Matters + +[Benefit 1 with supporting evidence] + +[Benefit 2 with supporting evidence] + +[Benefit 3 with supporting evidence] + +> **Key Insight**: [Highlighted important point or statistic] + +## [Secondary Keyword or Main Section 1] + +[Introduction to this section] + +### [Sub-topic 1] + +[Detailed explanation] + +**Key points**: +- Point 1 +- Point 2 +- Point 3 + +### [Sub-topic 2] + +[Detailed explanation] + +[Example or case study] + +## [Secondary Keyword or Main Section 2] + +[Section introduction] + +| Factor | Description | Impact | +|--------|-------------|--------| +| [Factor 1] | [Description] | High/Medium/Low | +| [Factor 2] | [Description] | High/Medium/Low | +| [Factor 3] | [Description] | High/Medium/Low | + +[Additional explanation] + +## [Secondary Keyword or Main Section 3] + +[Content for this section] + +**Best practices**: +1. [Practice 1 with explanation] +2. [Practice 2 with explanation] +3. [Practice 3 with explanation] + +## Common Mistakes to Avoid + +### [Mistake 1] + +[Why this is a mistake] +[How to avoid it] + +### [Mistake 2] + +[Why this is a mistake] +[How to avoid it] + +### [Mistake 3] + +[Why this is a mistake] +[How to avoid it] + +## Frequently Asked Questions + +### [Question from PAA or common query]? + +[Direct answer in 40-60 words] + +[Optional: Additional context] + +### [Question 2]? + +[Direct answer] + +### [Question 3]? + +[Direct answer] + +## Conclusion + +[Recap key points - include primary keyword] + +[Final insight or takeaway] + +[Clear call-to-action: next step for reader] + +--- + +**Related reading**: [Internal link 1] | [Internal link 2] | [Internal link 3] +``` + +**Internal links**: 3-5 contextual links to related content +**External links**: 2-3 links to authoritative sources +**Keywords**: Primary in H1, first 100 words, conclusion; secondary in H2s +**GEO-optimization**: Definition block, FAQ section, quotable statistics + +--- + +## Comparison Article ("[A] vs [B]") + +**Target word count**: 1,500-2,500 words +**Primary goal**: Help readers choose between options, rank for comparison queries + +```markdown +# [Option A] vs [Option B]: Which Is Better for [Use Case]? + +[Hook - present the comparison dilemma] + +[Context - why this comparison matters] + +[Promise - what readers will understand after reading] + +**Quick answer**: [Brief verdict for those who want immediate answer] + +## [Option A] vs [Option B]: Quick Comparison + +| Feature | [Option A] | [Option B] | +|---------|-----------|-----------| +| [Feature 1] | [Details] | [Details] | +| [Feature 2] | [Details] | [Details] | +| [Feature 3] | [Details] | [Details] | +| [Feature 4] | [Details] | [Details] | +| **Best for** | [Use case] | [Use case] | +| **Starting price** | [Price] | [Price] | + +## What Is [Option A]? + +[Clear definition] + +[Key features and capabilities] + +**Pros**: +- [Advantage 1] +- [Advantage 2] +- [Advantage 3] + +**Cons**: +- [Disadvantage 1] +- [Disadvantage 2] + +## What Is [Option B]? + +[Clear definition] + +[Key features and capabilities] + +**Pros**: +- [Advantage 1] +- [Advantage 2] +- [Advantage 3] + +**Cons**: +- [Disadvantage 1] +- [Disadvantage 2] + +## [Option A] vs [Option B]: Detailed Comparison + +### [Feature Category 1] + +**[Option A]**: [Detailed explanation with examples] + +**[Option B]**: [Detailed explanation with examples] + +**Winner**: [A/B/Tie] - [Brief reasoning] + +### [Feature Category 2] + +**[Option A]**: [Details] + +**[Option B]**: [Details] + +**Winner**: [A/B/Tie] - [Brief reasoning] + +### [Feature Category 3] + +[Continue pattern for key comparison points] + +## Pricing: [Option A] vs [Option B] + +### [Option A] Pricing + +- [Tier 1]: $[X]/month - [What's included] +- [Tier 2]: $[X]/month - [What's included] +- [Tier 3]: $[X]/month - [What's included] + +### [Option B] Pricing + +- [Tier 1]: $[X]/month - [What's included] +- [Tier 2]: $[X]/month - [What's included] +- [Tier 3]: $[X]/month - [What's included] + +**Value winner**: [Which offers better value and why] + +## Which Should You Choose? + +### Choose [Option A] if: +- [Criteria 1] +- [Criteria 2] +- [Criteria 3] + +### Choose [Option B] if: +- [Criteria 1] +- [Criteria 2] +- [Criteria 3] + +## Frequently Asked Questions + +### Is [Option A] better than [Option B]? + +[Nuanced answer - depends on use case] + +### Can you use [Option A] and [Option B] together? + +[Answer with explanation] + +### Which is easier for beginners: [Option A] or [Option B]? + +[Answer with reasoning] + +## Final Verdict: [Option A] vs [Option B] + +[Balanced conclusion] + +[Recommendation based on different use cases] + +[Clear call-to-action] +``` + +**Internal links**: Links to detailed reviews of each option, related comparisons +**External links**: Official websites of both options, third-party reviews +**Keywords**: Both options mentioned throughout, comparison keywords in H2s + +--- + +## Listicle ("Top N [Items]") + +**Target word count**: 1,500-2,500 words (depending on list length) +**Primary goal**: Provide curated recommendations, rank for "best [topic]" queries + +```markdown +# [Number] Best [Items] for [Audience/Purpose] ([Year]) + +[Hook - establish why this list matters] + +[Context - what makes these the "best"] + +[Promise - what readers will gain] + +After testing [X] different [items], we've identified the [number] that deliver the best results for [audience/purpose]. + +## Quick Summary: Top [Number] [Items] + +| Rank | [Item] | Best For | Price | +|------|--------|----------|-------| +| 1 | [Item 1] | [Use case] | [Price] | +| 2 | [Item 2] | [Use case] | [Price] | +| 3 | [Item 3] | [Use case] | [Price] | + +## How We Chose These [Items] + +Our selection criteria: +- [Criterion 1] - [Why it matters] +- [Criterion 2] - [Why it matters] +- [Criterion 3] - [Why it matters] + +## 1. [Item Name] - Best for [Specific Use Case] + +![Alt text: [Item name] screenshot/image] + +**What it is**: [Brief description] + +**Why we love it**: [Key benefits and features] + +**Key features**: +- [Feature 1] +- [Feature 2] +- [Feature 3] + +**Pricing**: [Price details] + +**Pros**: +- [Pro 1] +- [Pro 2] + +**Cons**: +- [Con 1] +- [Con 2] + +**Best for**: [Ideal user or use case] + +[Link to item] + +--- + +## 2. [Item Name] - Best for [Specific Use Case] + +[Follow same structure as #1] + +--- + +## 3. [Item Name] - Best for [Specific Use Case] + +[Continue pattern for all items in list] + +--- + +## Comparison: Which [Item] Is Right for You? + +| Feature | [Item 1] | [Item 2] | [Item 3] | +|---------|----------|----------|----------| +| [Feature 1] | ✓/✗ | ✓/✗ | ✓/✗ | +| [Feature 2] | ✓/✗ | ✓/✗ | ✓/✗ | +| [Feature 3] | ✓/✗ | ✓/✗ | ✓/✗ | + +## How to Choose the Best [Item] for Your Needs + +### Consider [Factor 1] + +[Explanation of how this factor affects choice] + +### Consider [Factor 2] + +[Explanation] + +### Consider [Factor 3] + +[Explanation] + +## Frequently Asked Questions + +### What is the best [item] for beginners? + +[Answer with specific recommendation] + +### Which [item] offers the best value? + +[Answer with reasoning] + +### Are paid [items] better than free ones? + +[Balanced answer] + +## Conclusion: Our Top Pick + +[Restate #1 recommendation] + +[Why it's the best overall choice] + +[Call-to-action] +``` + +**Internal links**: Related guides, comparison articles, category pages +**External links**: Links to each recommended item, review sources +**Keywords**: "Best [topic]", individual item names, use case keywords + +--- + +## How-To Guide (Step-by-Step) + +**Target word count**: 1,200-2,000 words +**Primary goal**: Teach a specific process, rank for "how to [task]" queries + +```markdown +# How to [Achieve Goal]: [Number]-Step Guide [for Audience] + +[Hook - present the outcome readers want] + +[Current problem - why this is challenging] + +[Promise - what readers will accomplish] + +By following this step-by-step guide, you'll be able to [specific outcome] in [timeframe]. + +## What You'll Need + +**Tools**: +- [Tool 1] +- [Tool 2] +- [Tool 3] + +**Time required**: [Estimated time] + +**Skill level**: [Beginner/Intermediate/Advanced] + +## Why [Task] Matters + +[Explain the benefits of completing this task] + +[Provide context and importance] + +## Step 1: [Action Title] + +![Alt text: Screenshot showing step 1] + +[Clear instruction for this step] + +[Why this step is important] + +**How to do it**: +1. [Sub-step 1] +2. [Sub-step 2] +3. [Sub-step 3] + +> **Pro Tip**: [Helpful tip to make this step easier] + +**Common mistakes**: +- [Mistake to avoid] +- [Another mistake] + +--- + +## Step 2: [Action Title] + +![Alt text: Screenshot showing step 2] + +[Instruction for this step] + +[Additional context or explanation] + +**Example**: [Concrete example showing this step] + +--- + +## Step 3: [Action Title] + +[Continue pattern for all steps] + +--- + +## Step [Final Number]: [Action Title] + +[Final step instruction] + +[What success looks like] + +**You'll know it's working when**: [Success indicators] + +--- + +## What to Do If [Common Problem] + +### Problem: [Issue 1] + +**Solution**: [How to fix it] + +### Problem: [Issue 2] + +**Solution**: [How to fix it] + +## Advanced Tips + +Once you've mastered the basics: + +1. **[Advanced tip 1]** - [Explanation] +2. **[Advanced tip 2]** - [Explanation] +3. **[Advanced tip 3]** - [Explanation] + +## Frequently Asked Questions + +### How long does it take to [complete task]? + +[Realistic timeframe with variables] + +### Do I need [specific tool/skill] to [complete task]? + +[Answer with alternatives if applicable] + +### What if [specific concern]? + +[Address concern with solution] + +## Conclusion + +[Recap the process] + +[Encourage reader to take action] + +[Clear next step or CTA] + +**Next steps**: [Link to related guide or advanced tutorial] +``` + +**Internal links**: Links to tool reviews, prerequisite guides, related tutorials +**External links**: Tool documentation, additional resources +**Keywords**: "How to [task]", action words in each step heading + +--- + +## Product Review + +**Target word count**: 1,500-2,000 words +**Primary goal**: Help buyers make informed decisions, rank for "[product] review" queries + +```markdown +# [Product Name] Review: Is It Worth It in [Year]? + +[Hook - establish the product's promise or popularity] + +[Context - who this review is for] + +After using [Product] for [timeframe], here's my honest assessment of whether it lives up to the hype. + +## [Product Name] Overview + +**What it is**: [Clear description] + +**Price**: [Pricing details] + +**Best for**: [Ideal user or use case] + +**Rating**: ★★★★☆ (4/5) + +## Pros and Cons at a Glance + +### Pros +- [Major advantage 1] +- [Major advantage 2] +- [Major advantage 3] + +### Cons +- [Main limitation 1] +- [Main limitation 2] + +## What Is [Product Name]? + +[Detailed product description] + +[Who makes it and company background] + +[Product positioning and target market] + +## Key Features + +### [Feature 1] + +[Detailed explanation of feature] + +[How it works in practice] + +[Our experience with this feature] + +### [Feature 2] + +[Continue pattern for main features] + +## Performance Testing + +### [Test Category 1] + +**What we tested**: [Testing methodology] + +**Results**: [Specific results with data] + +**Verdict**: [Assessment] + +### [Test Category 2] + +[Continue testing pattern] + +## Pricing and Plans + +| Plan | Price | What's Included | +|------|-------|-----------------| +| [Tier 1] | $[X]/month | [Features] | +| [Tier 2] | $[X]/month | [Features] | +| [Tier 3] | $[X]/month | [Features] | + +**Value assessment**: [Whether pricing is justified] + +## [Product] vs Competitors + +| Feature | [Product] | [Competitor 1] | [Competitor 2] | +|---------|-----------|----------------|----------------| +| [Feature 1] | [Details] | [Details] | [Details] | +| [Feature 2] | [Details] | [Details] | [Details] | +| [Feature 3] | [Details] | [Details] | [Details] | + +## Who Should Buy [Product]? + +### Perfect for: +- [User type 1] because [reason] +- [User type 2] because [reason] + +### Not ideal for: +- [User type] because [reason] +- [User type] because [reason] + +## Frequently Asked Questions + +### Is [Product] worth the price? + +[Honest assessment with context] + +### How does [Product] compare to [main competitor]? + +[Balanced comparison] + +### What's the learning curve like? + +[Assessment with timeframe] + +## Final Verdict: Should You Buy [Product]? + +[Balanced conclusion] + +[Specific recommendation based on use case] + +**Our rating**: ★★★★☆ (4/5) + +[Link to product with disclosure if affiliate] +``` + +**Disclosure**: Include affiliate disclosure if applicable +**Internal links**: Related product reviews, comparison articles, category pages +**External links**: Product website, official documentation + +--- + +## Pillar Page (Comprehensive Guide) + +**Target word count**: 3,000-5,000+ words +**Primary goal**: Become definitive resource, rank for head terms, support topic cluster + +```markdown +# [Topic]: The Complete Guide [for Audience] ([Year]) + +[Compelling hook - establish the importance of this topic] + +[Problem - what challenges exist in this space] + +[Promise - what this comprehensive guide delivers] + +This is the most comprehensive guide to [topic] on the web. You'll learn everything from fundamentals to advanced strategies. + +## Table of Contents + +- [Chapter 1] +- [Chapter 2] +- [Chapter 3] +- [Chapter 4] +- [Chapter 5] + +## Chapter 1: [Topic] Fundamentals + +### What Is [Topic]? + +[Comprehensive definition - GEO-optimized] + +[History and evolution] + +[Current state and importance] + +### Why [Topic] Matters + +[Multiple benefits with evidence] + +[Industry statistics and trends] + +### Key Concepts and Terminology + +| Term | Definition | +|------|------------| +| [Term 1] | [Definition] | +| [Term 2] | [Definition] | +| [Term 3] | [Definition] | + +## Chapter 2: [Major Sub-Topic] + +[Comprehensive section on major aspect] + +[Multiple sub-sections with H3 headings] + +[Examples, case studies, data] + +[Internal links to detailed cluster content] + +> **Deep dive**: For more on [sub-topic], see our complete guide: [link] + +## Chapter 3: [Major Sub-Topic] + +[Continue pattern with substantial content sections] + +## Chapter 4: [Advanced/Tactical Section] + +[Strategic or tactical content] + +[Actionable frameworks and processes] + +## Chapter 5: [Implementation/Case Studies] + +[Real-world applications] + +[Success stories] + +[Implementation roadmap] + +## Frequently Asked Questions + +[Comprehensive FAQ section with 8-10 questions] + +## Key Takeaways + +[Summarize main points from each chapter] + +## What's Next? + +[Recommend next steps] + +[Links to related cluster content] + +## Resources + +**Recommended tools**: +- [Tool 1] - [Use case] +- [Tool 2] - [Use case] + +**Further reading**: +- [Internal link to cluster content] +- [Internal link to cluster content] +- [External resource] +``` + +**Internal links**: 8-15 links to related cluster content +**External links**: 5-10 authoritative sources +**Update frequency**: Quarterly for pillar pages + +--- + +## FAQ Page + +**Target word count**: 1,000-1,500 words +**Primary goal**: Answer common questions, rank for question queries, enable FAQ rich results + +```markdown +# [Topic]: Frequently Asked Questions + +[Introduction explaining what questions this page answers] + +[Who this FAQ is for] + +## General Questions + +### What is [topic]? + +[Comprehensive answer with definition] + +### How does [topic] work? + +[Clear explanation with steps if applicable] + +### Why is [topic] important? + +[Benefits and context] + +## Getting Started + +### How do I get started with [topic]? + +[Step-by-step guidance] + +### What do I need to [accomplish goal]? + +[Requirements and prerequisites] + +### How long does it take to [achieve result]? + +[Realistic timeframe] + +## Common Problems + +### Why isn't [expected result] happening? + +[Troubleshooting guidance] + +### What should I do if [problem occurs]? + +[Solution with steps] + +### How do I fix [specific error]? + +[Fix instructions] + +## Comparison Questions + +### [Option A] vs [Option B]: which is better? + +[Balanced comparison answer] + +### Should I [action 1] or [action 2]? + +[Guidance based on scenarios] + +## Pricing and Plans + +### How much does [topic] cost? + +[Pricing information with context] + +### Is there a free version? + +[Information about free options] + +## Advanced Questions + +### Can I [advanced action]? + +[Answer with technical details] + +### How do I [complex task]? + +[Guidance with link to detailed guide] + +## Still Have Questions? + +[Call-to-action for additional support] + +[Contact information or link to contact page] +``` + +**Schema markup**: Add FAQPage schema to enable rich results +**Internal links**: Link to detailed guides for complex answers +**Keywords**: Include question keywords naturally + +--- + +## Implementation Checklist + +For any template: + +- [ ] Customize all [bracketed placeholders] +- [ ] Include primary keyword in H1 and first 100 words +- [ ] Place secondary keywords in H2/H3 headings +- [ ] Add 3-5 internal links to related content +- [ ] Include 2-3 external links to authoritative sources +- [ ] Create GEO-optimized definition blocks +- [ ] Add FAQ section for featured snippet opportunities +- [ ] Include relevant images with descriptive alt text +- [ ] Write compelling meta description (150-160 chars) +- [ ] Optimize title tag (50-60 chars with primary keyword) diff --git a/skills/seo-content-writer/references/title-formulas.md b/skills/seo-content-writer/references/title-formulas.md new file mode 100755 index 0000000..7301769 --- /dev/null +++ b/skills/seo-content-writer/references/title-formulas.md @@ -0,0 +1,339 @@ +# Title and Headline Formulas + +Proven title structures that drive clicks and rankings. All formulas target 50-60 characters for optimal SERP display. + +## Numbered List Formulas + +**Pattern**: [Number] [Adjective] [Topic] [Qualifier/Benefit] + +### Examples +- "7 Ways to Improve Your SEO Rankings in 2024" +- "15 Content Marketing Strategies That Actually Work" +- "10 Free Tools for Keyword Research (Beginner-Friendly)" +- "21 Email Marketing Tips from Industry Experts" +- "5 Quick Wins for Better Site Performance" + +**Why it works**: Numbers promise specific value, create curiosity, set clear expectations. + +**Best practices**: +- Odd numbers (7, 9, 15) often outperform even numbers +- Use specific numbers (13 vs "over 10") +- Keep total under 30 for credibility +- Match number to actual content sections + +--- + +## How-To Formulas + +**Pattern 1**: How to [Achieve Goal] in [Timeframe] +- "How to Rank on Google in 30 Days" +- "How to Write SEO Content in Under 2 Hours" +- "How to Build Backlinks in 2024" + +**Pattern 2**: How to [Achieve Goal] Without [Common Objection] +- "How to Learn SEO Without Spending Money" +- "How to Increase Traffic Without Paid Ads" +- "How to Write Blog Posts Without Writer's Block" + +**Pattern 3**: How to [Achieve Goal] (Even If [Limitation]) +- "How to Rank #1 (Even If You're a New Website)" +- "How to Get Backlinks (Even With Zero Audience)" +- "How to Write SEO Content (Even If You Hate Writing)" + +**Pattern 4**: How to [Achieve Goal] Like [Authority/Expert] +- "How to Write Email Copy Like a Professional Copywriter" +- "How to Do Keyword Research Like an SEO Pro" +- "How to Create Content Like Top Marketing Agencies" + +--- + +## Question Formulas + +**Pattern 1**: What Is [Topic]? [Benefit/Hook] +- "What Is Technical SEO? A Complete Guide" +- "What Is GEO? Everything You Need to Know" +- "What Is Content Marketing? (And Why It Matters)" + +**Pattern 2**: Why [Action] [Result] +- "Why Most Content Marketing Strategies Fail" +- "Why Your Website Isn't Ranking" +- "Why Email Still Beats Social Media for ROI" + +**Pattern 3**: Should You [Action]? [Quick Answer] +- "Should You Buy Backlinks? (Spoiler: No)" +- "Should You Hire an SEO Agency? A Cost-Benefit Analysis" +- "Should You Rewrite Old Content? When It Makes Sense" + +**Pattern 4**: When Should You [Action] +- "When Should You Publish Blog Posts for Maximum Traffic" +- "When Should You Update Your SEO Strategy" +- "When Should You Remove Content from Your Website" + +--- + +## Comparison Formulas + +**Pattern 1**: [Option A] vs [Option B]: Which Is Better? +- "WordPress vs Webflow: Which Is Better for SEO?" +- "Ahrefs vs SEMrush: Complete Comparison 2024" +- "In-House SEO vs Agency: Which Should You Choose?" + +**Pattern 2**: [Option A] vs [Option B] (and the Winner Is...) +- "Free Tools vs Paid SEO Software (and the Winner Is...)" +- "Long-Form vs Short-Form Content (and the Winner Is...)" + +**Pattern 3**: [Option A] or [Option B] for [Audience/Goal] +- "Shopify or WooCommerce for Small Business SEO" +- "YouTube or TikTok for B2B Marketing" + +--- + +## Ultimate Guide Formulas + +**Pattern 1**: The [Complete/Definitive] Guide to [Topic] +- "The Complete Guide to Technical SEO" +- "The Definitive Guide to Link Building in 2024" + +**Pattern 2**: [Topic]: The Ultimate Guide for [Audience] +- "Content Marketing: The Ultimate Guide for Beginners" +- "Local SEO: The Ultimate Guide for Small Businesses" + +**Pattern 3**: Everything You Need to Know About [Topic] +- "Everything You Need to Know About Core Web Vitals" +- "Everything You Need to Know About Schema Markup" + +--- + +## Power Word Formulas + +**Pattern**: [Power Word] [Topic] [Qualifier] + +### Power Word Categories + +**Authority words**: +- Proven, Tested, Verified, Certified, Official +- Example: "Proven Strategies to Double Your Organic Traffic" + +**Urgency words**: +- Now, Today, Fast, Quick, Instantly, Immediately +- Example: "Quick Wins: Improve SEO Performance Today" + +**Value words**: +- Free, Complete, Essential, Ultimate, Comprehensive +- Example: "The Essential SEO Checklist for 2024" + +**Exclusivity words**: +- Secret, Hidden, Little-Known, Insider, Exclusive +- Example: "7 Little-Known SEO Tactics That Actually Work" + +**Emotional words**: +- Amazing, Powerful, Effortless, Simple, Easy +- Example: "Simple SEO Strategies for Busy Entrepreneurs" + +--- + +## Before/After Formulas + +**Pattern 1**: From [Bad State] to [Good State] +- "From Zero to 10K Monthly Visitors: A Case Study" +- "From Page 5 to Position 1: How We Ranked a Competitive Keyword" + +**Pattern 2**: How I/We [Achieved Result] in [Timeframe] +- "How I Increased Organic Traffic by 300% in 6 Months" +- "How We Built 100 Backlinks in 30 Days" + +**Pattern 3**: [Bad State] → [Good State]: [Method] +- "500 Visitors → 50K: Our Content Strategy Revealed" +- "Struggling → Thriving: SEO Transformation Story" + +--- + +## Mistake/Problem Formulas + +**Pattern 1**: [Number] [Topic] Mistakes [Qualifier] +- "5 SEO Mistakes Killing Your Rankings" +- "10 Content Marketing Mistakes to Avoid in 2024" +- "3 Technical SEO Mistakes Even Pros Make" + +**Pattern 2**: Stop [Wrong Action]. Do [Right Action] Instead +- "Stop Keyword Stuffing. Do This Instead" +- "Stop Buying Backlinks. Try These 7 Tactics Instead" + +**Pattern 3**: Why Your [Topic] Isn't Working (And How to Fix It) +- "Why Your Content Isn't Ranking (And How to Fix It)" +- "Why Your SEO Strategy Isn't Working (And How to Fix It)" + +--- + +## Checklist Formulas + +**Pattern 1**: [Topic] Checklist: [Number] Must-Have [Items] +- "Technical SEO Checklist: 23 Must-Check Items" +- "Blog Post Checklist: 15 Things to Check Before Publishing" + +**Pattern 2**: The [Timeframe] [Topic] Checklist +- "The 2024 SEO Audit Checklist" +- "The Pre-Launch Website Checklist" + +--- + +## Curiosity Gap Formulas + +**Pattern 1**: The [Adjective] Reason [Unexpected Fact] +- "The Surprising Reason Long Content Ranks Better" +- "The Real Reason Your Blog Posts Aren't Getting Traffic" + +**Pattern 2**: [Number] Things About [Topic] That [Surprising Fact] +- "7 Things About SEO That Google Doesn't Tell You" +- "5 Things About Content Marketing That Changed in 2024" + +**Pattern 3**: What [Expert/Group] Know About [Topic] (That You Don't) +- "What Top SEO Agencies Know About Link Building (That You Don't)" +- "What Professional Copywriters Know About Headlines" + +--- + +## Benefit-Driven Formulas + +**Pattern 1**: [Action] to [Specific Benefit] +- "Optimize Your Title Tags to Double Your CTR" +- "Write Better Meta Descriptions to Increase Organic Traffic" + +**Pattern 2**: [Action] for [Outcome] Without [Objection] +- "Scale Content Production for Better Rankings Without Hiring Writers" +- "Improve Site Speed for Higher Rankings Without Technical Skills" + +--- + +## Authority/Credibility Formulas + +**Pattern 1**: [Expert/Authority] Says [Action/Tip] +- "Google's John Mueller Says Internal Linking Is Underrated" +- "SEO Experts Reveal Their Top Ranking Factor for 2024" + +**Pattern 2**: [Number] [Experts/Companies] Share [Topic] +- "50 SEO Experts Share Their #1 Ranking Tip" +- "10 Successful Brands Share Their Content Strategy" + +--- + +## CTR Optimization Patterns + +### Elements That Increase CTR + +**Year/Timeframe** (signals freshness): +- "SEO Trends for 2024" +- "Best Practices [Updated January 2024]" + +**Brackets/Parentheticals** (adds extra context): +- "Email Marketing Tips (That Actually Work)" +- "SEO Tools [Free & Paid Options]" + +**Numbers** (promise specific value): +- "7 Ways...", "15 Tools...", "3 Reasons..." + +**Power words** (emotional trigger): +- Ultimate, Essential, Complete, Proven, Secret + +**Target audience** (relevance signal): +- "...for Beginners", "...for Small Businesses", "...for B2B" + +### Elements That Decrease CTR + +- All caps (looks spammy) +- Excessive punctuation (!!!, ???) +- Clickbait without substance +- Misleading promises +- Generic titles ("SEO Tips", "Marketing Guide") + +--- + +## Title Length Guidelines + +### Google SERP +- **Optimal**: 50-60 characters +- **Maximum**: ~70 characters before truncation +- **Mobile**: ~55 characters (truncates earlier) + +### Social Media +- **Twitter/X**: 70-100 characters for best engagement +- **Facebook**: 40-50 characters (shorter performs better) +- **LinkedIn**: 70-100 characters +- **Pinterest**: 40-60 characters + +### Email Subject Lines +- **Optimal**: 40-50 characters +- **Mobile**: 30-40 characters (truncates earlier) +- **Desktop**: Up to 60 characters + +--- + +## Before/After Title Examples + +### Generic → Optimized + +**Before**: "SEO Tips" +**After**: "7 SEO Tips That Increased Our Traffic by 300%" +- Added: Number, specificity, proof + +**Before**: "How to Do Keyword Research" +**After**: "How to Do Keyword Research (Free Tools & 5-Step Process)" +- Added: Benefit, specificity, process indicator + +**Before**: "Content Marketing Guide" +**After**: "The Complete Content Marketing Guide for Small Businesses [2024]" +- Added: Completeness indicator, target audience, year + +**Before**: "What Is Technical SEO" +**After**: "What Is Technical SEO? A Beginner's Guide to Better Rankings" +- Added: Audience level, benefit + +**Before**: "WordPress vs Shopify" +**After**: "WordPress vs Shopify: Which Is Better for SEO? (2024 Comparison)" +- Added: Specific comparison angle, year + +**Before**: "Link Building Strategies" +**After**: "15 White-Hat Link Building Strategies That Still Work in 2024" +- Added: Number, quality signal, time relevance + +--- + +## Title Writing Checklist + +Before finalizing any title, check: + +- [ ] Includes primary keyword naturally +- [ ] Length is 50-60 characters +- [ ] Matches search intent (informational/transactional/navigational) +- [ ] Contains power word or emotional trigger +- [ ] Specific and concrete (not vague) +- [ ] Promises clear value or benefit +- [ ] Accurate representation of content +- [ ] No clickbait or misleading claims +- [ ] Unique compared to competitors +- [ ] Works when truncated on mobile + +--- + +## Testing and Optimization + +### A/B Test These Variables +- Number variations (7 vs 10 vs 15) +- Power word placement (beginning vs end) +- Question vs statement format +- With/without year +- With/without brackets +- Different benefit framing + +### Track These Metrics +- Click-through rate (CTR) in Search Console +- Average position in SERPs +- Time on page (engagement indicator) +- Bounce rate (relevance indicator) + +### When to Rewrite Titles +- CTR below 2% for top 3 positions +- CTR below 5% for positions 4-10 +- Content is updated (add [Updated 2024]) +- Competitors have better titles +- Ranking but not getting clicks diff --git a/skills/skill-creator/LICENSE.txt b/skills/skill-creator/LICENSE.txt new file mode 100755 index 0000000..7a4a3ea --- /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 100755 index 0000000..696f410 --- /dev/null +++ b/skills/skill-creator/SKILL.md @@ -0,0 +1,485 @@ +--- +name: skill-creator +description: Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, edit, or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy. +--- + +# Skill Creator + +A skill for creating new skills and iteratively improving them. + +At a high level, the process of creating a skill goes like this: + +- Decide what you want the skill to do and roughly how it should do it +- Write a draft of the skill +- Create a few test prompts and run glm-with-access-to-the-skill on them +- Help the user evaluate the results both qualitatively and quantitatively + - While the runs happen in the background, draft some quantitative evals if there aren't any (if there are some, you can either use as is or modify if you feel something needs to change about them). Then explain them to the user (or if they already existed, explain the ones that already exist) + - Use the `eval-viewer/generate_review.py` script to show the user the results for them to look at, and also let them look at the quantitative metrics +- Rewrite the skill based on feedback from the user's evaluation of the results (and also if there are any glaring flaws that become apparent from the quantitative benchmarks) +- Repeat until you're satisfied +- Expand the test set and try again at larger scale + +Your job when using this skill is to figure out where the user is in this process and then jump in and help them progress through these stages. So for instance, maybe they're like "I want to make a skill for X". You can help narrow down what they mean, write a draft, write the test cases, figure out how they want to evaluate, run all the prompts, and repeat. + +On the other hand, maybe they already have a draft of the skill. In this case you can go straight to the eval/iterate part of the loop. + +Of course, you should always be flexible and if the user is like "I don't need to run a bunch of evaluations, just vibe with me", you can do that instead. + +Then after the skill is done (but again, the order is flexible), you can also run the skill description improver, which we have a whole separate script for, to optimize the triggering of the skill. + +Cool? Cool. + +## Communicating with the user + +The skill creator is liable to be used by people across a wide range of familiarity with coding jargon. If you haven't heard (and how could you, it's only very recently that it started), there's a trend now where the power of GLM is inspiring plumbers to open up their terminals, parents and grandparents to google "how to install npm". On the other hand, the bulk of users are probably fairly computer-literate. + +So please pay attention to context cues to understand how to phrase your communication! In the default case, just to give you some idea: + +- "evaluation" and "benchmark" are borderline, but OK +- for "JSON" and "assertion" you want to see serious cues from the user that they know what those things are before using them without explaining them + +It's OK to briefly explain terms if you're in doubt, and feel free to clarify terms with a short definition if you're unsure if the user will get it. + +--- + +## Creating a skill + +### Capture Intent + +Start by understanding the user's intent. The current conversation might already contain a workflow the user wants to capture (e.g., they say "turn this into a skill"). If so, extract answers from the conversation history first — the tools used, the sequence of steps, corrections the user made, input/output formats observed. The user may need to fill the gaps, and should confirm before proceeding to the next step. + +1. What should this skill enable GLM to do? +2. When should this skill trigger? (what user phrases/contexts) +3. What's the expected output format? +4. Should we set up test cases to verify the skill works? Skills with objectively verifiable outputs (file transforms, data extraction, code generation, fixed workflow steps) benefit from test cases. Skills with subjective outputs (writing style, art) often don't need them. Suggest the appropriate default based on the skill type, but let the user decide. + +### Interview and Research + +Proactively ask questions about edge cases, input/output formats, example files, success criteria, and dependencies. Wait to write test prompts until you've got this part ironed out. + +Check available MCPs - if useful for research (searching docs, finding similar skills, looking up best practices), research in parallel via subagents if available, otherwise inline. Come prepared with context to reduce burden on the user. + +### Write the SKILL.md + +Based on the user interview, fill in these components: + +- **name**: Skill identifier +- **description**: When to trigger, what it does. This is the primary triggering mechanism - include both what the skill does AND specific contexts for when to use it. All "when to use" info goes here, not in the body. Note: currently GLM has a tendency to "undertrigger" skills -- to not use them when they'd be useful. To combat this, please make the skill descriptions a little bit "pushy". So for instance, instead of "How to build a simple fast dashboard to display internal Zhipu AI data.", you might write "How to build a simple fast dashboard to display internal Zhipu AI data. Make sure to use this skill whenever the user mentions dashboards, data visualization, internal metrics, or wants to display any kind of company data, even if they don't explicitly ask for a 'dashboard.'" +- **compatibility**: Required tools, dependencies (optional, rarely needed) +- **the rest of the skill :)** + +### Skill Writing Guide + +#### Anatomy of a Skill + +``` +skill-name/ +├── SKILL.md (required) +│ ├── YAML frontmatter (name, description required) +│ └── Markdown instructions +└── Bundled Resources (optional) + ├── scripts/ - Executable code for deterministic/repetitive tasks + ├── references/ - Docs loaded into context as needed + └── assets/ - Files used in output (templates, icons, fonts) +``` + +#### Progressive Disclosure + +Skills use a three-level loading system: +1. **Metadata** (name + description) - Always in context (~100 words) +2. **SKILL.md body** - In context whenever skill triggers (<500 lines ideal) +3. **Bundled resources** - As needed (unlimited, scripts can execute without loading) + +These word counts are approximate and you can feel free to go longer if needed. + +**Key patterns:** +- Keep SKILL.md under 500 lines; if you're approaching this limit, add an additional layer of hierarchy along with clear pointers about where the model using the skill should go next to follow up. +- Reference files clearly from SKILL.md with guidance on when to read them +- For large reference files (>300 lines), include a table of contents + +**Domain organization**: When a skill supports multiple domains/frameworks, organize by variant: +``` +cloud-deploy/ +├── SKILL.md (workflow + selection) +└── references/ + ├── aws.md + ├── gcp.md + └── azure.md +``` +GLM reads only the relevant reference file. + +#### Principle of Lack of Surprise + +This goes without saying, but skills must not contain malware, exploit code, or any content that could compromise system security. A skill's contents should not surprise the user in their intent if described. Don't go along with requests to create misleading skills or skills designed to facilitate unauthorized access, data exfiltration, or other malicious activities. Things like a "roleplay as an XYZ" are OK though. + +#### Writing Patterns + +Prefer using the imperative form in instructions. + +**Defining output formats** - You can do it like this: +```markdown +## Report structure +ALWAYS use this exact template: +# [Title] +## Executive summary +## Key findings +## Recommendations +``` + +**Examples pattern** - It's useful to include examples. You can format them like this (but if "Input" and "Output" are in the examples you might want to deviate a little): +```markdown +## Commit message format +**Example 1:** +Input: Added user authentication with JWT tokens +Output: feat(auth): implement JWT-based authentication +``` + +### Writing Style + +Try to explain to the model why things are important in lieu of heavy-handed musty MUSTs. Use theory of mind and try to make the skill general and not super-narrow to specific examples. Start by writing a draft and then look at it with fresh eyes and improve it. + +### Test Cases + +After writing the skill draft, come up with 2-3 realistic test prompts — the kind of thing a real user would actually say. Share them with the user: [you don't have to use this exact language] "Here are a few test cases I'd like to try. Do these look right, or do you want to add more?" Then run them. + +Save test cases to `evals/evals.json`. Don't write assertions yet — just the prompts. You'll draft assertions in the next step while the runs are in progress. + +```json +{ + "skill_name": "example-skill", + "evals": [ + { + "id": 1, + "prompt": "User's task prompt", + "expected_output": "Description of expected result", + "files": [] + } + ] +} +``` + +See `references/schemas.md` for the full schema (including the `assertions` field, which you'll add later). + +## Running and evaluating test cases + +This section is one continuous sequence — don't stop partway through. Do NOT use `/skill-test` or any other testing skill. + +Put results in `-workspace/` as a sibling to the skill directory. Within the workspace, organize results by iteration (`iteration-1/`, `iteration-2/`, etc.) and within that, each test case gets a directory (`eval-0/`, `eval-1/`, etc.). Don't create all of this upfront — just create directories as you go. + +### Step 1: Spawn all runs (with-skill AND baseline) in the same turn + +For each test case, spawn two subagents in the same turn — one with the skill, one without. This is important: don't spawn the with-skill runs first and then come back for baselines later. Launch everything at once so it all finishes around the same time. + +**With-skill run:** + +``` +Execute this task: +- Skill path: +- Task: +- Input files: +- Save outputs to: /iteration-/eval-/with_skill/outputs/ +- Outputs to save: +``` + +**Baseline run** (same prompt, but the baseline depends on context): +- **Creating a new skill**: no skill at all. Same prompt, no skill path, save to `without_skill/outputs/`. +- **Improving an existing skill**: the old version. Before editing, snapshot the skill (`cp -r /skill-snapshot/`), then point the baseline subagent at the snapshot. Save to `old_skill/outputs/`. + +Write an `eval_metadata.json` for each test case (assertions can be empty for now). Give each eval a descriptive name based on what it's testing — not just "eval-0". Use this name for the directory too. If this iteration uses new or modified eval prompts, create these files for each new eval directory — don't assume they carry over from previous iterations. + +```json +{ + "eval_id": 0, + "eval_name": "descriptive-name-here", + "prompt": "The user's task prompt", + "assertions": [] +} +``` + +### Step 2: While runs are in progress, draft assertions + +Don't just wait for the runs to finish — you can use this time productively. Draft quantitative assertions for each test case and explain them to the user. If assertions already exist in `evals/evals.json`, review them and explain what they check. + +Good assertions are objectively verifiable and have descriptive names — they should read clearly in the benchmark viewer so someone glancing at the results immediately understands what each one checks. Subjective skills (writing style, design quality) are better evaluated qualitatively — don't force assertions onto things that need human judgment. + +Update the `eval_metadata.json` files and `evals/evals.json` with the assertions once drafted. Also explain to the user what they'll see in the viewer — both the qualitative outputs and the quantitative benchmark. + +### Step 3: As runs complete, capture timing data + +When each subagent task completes, you receive a notification containing `total_tokens` and `duration_ms`. Save this data immediately to `timing.json` in the run directory: + +```json +{ + "total_tokens": 84852, + "duration_ms": 23332, + "total_duration_seconds": 23.3 +} +``` + +This is the only opportunity to capture this data — it comes through the task notification and isn't persisted elsewhere. Process each notification as it arrives rather than trying to batch them. + +### Step 4: Grade, aggregate, and launch the viewer + +Once all runs are done: + +1. **Grade each run** — spawn a grader subagent (or grade inline) that reads `agents/grader.md` and evaluates each assertion against the outputs. Save results to `grading.json` in each run directory. The grading.json expectations array must use the fields `text`, `passed`, and `evidence` (not `name`/`met`/`details` or other variants) — the viewer depends on these exact field names. For assertions that can be checked programmatically, write and run a script rather than eyeballing it — scripts are faster, more reliable, and can be reused across iterations. + +2. **Aggregate into benchmark** — run the aggregation script from the skill-creator directory: + ```bash + python -m scripts.aggregate_benchmark /iteration-N --skill-name + ``` + This produces `benchmark.json` and `benchmark.md` with pass_rate, time, and tokens for each configuration, with mean ± stddev and the delta. If generating benchmark.json manually, see `references/schemas.md` for the exact schema the viewer expects. +Put each with_skill version before its baseline counterpart. + +3. **Do an analyst pass** — read the benchmark data and surface patterns the aggregate stats might hide. See `agents/analyzer.md` (the "Analyzing Benchmark Results" section) for what to look for — things like assertions that always pass regardless of skill (non-discriminating), high-variance evals (possibly flaky), and time/token tradeoffs. + +4. **Launch the viewer** with both qualitative outputs and quantitative data: + ```bash + nohup python /eval-viewer/generate_review.py \ + /iteration-N \ + --skill-name "my-skill" \ + --benchmark /iteration-N/benchmark.json \ + > /dev/null 2>&1 & + VIEWER_PID=$! + ``` + For iteration 2+, also pass `--previous-workspace /iteration-`. + + **Cowork / headless environments:** If `webbrowser.open()` is not available or the environment has no display, use `--static ` to write a standalone HTML file instead of starting a server. Feedback will be downloaded as a `feedback.json` file when the user clicks "Submit All Reviews". After download, copy `feedback.json` into the workspace directory for the next iteration to pick up. + +Note: please use generate_review.py to create the viewer; there's no need to write custom HTML. + +5. **Tell the user** something like: "I've opened the results in your browser. There are two tabs — 'Outputs' lets you click through each test case and leave feedback, 'Benchmark' shows the quantitative comparison. When you're done, come back here and let me know." + +### What the user sees in the viewer + +The "Outputs" tab shows one test case at a time: +- **Prompt**: the task that was given +- **Output**: the files the skill produced, rendered inline where possible +- **Previous Output** (iteration 2+): collapsed section showing last iteration's output +- **Formal Grades** (if grading was run): collapsed section showing assertion pass/fail +- **Feedback**: a textbox that auto-saves as they type +- **Previous Feedback** (iteration 2+): their comments from last time, shown below the textbox + +The "Benchmark" tab shows the stats summary: pass rates, timing, and token usage for each configuration, with per-eval breakdowns and analyst observations. + +Navigation is via prev/next buttons or arrow keys. When done, they click "Submit All Reviews" which saves all feedback to `feedback.json`. + +### Step 5: Read the feedback + +When the user tells you they're done, read `feedback.json`: + +```json +{ + "reviews": [ + {"run_id": "eval-0-with_skill", "feedback": "the chart is missing axis labels", "timestamp": "..."}, + {"run_id": "eval-1-with_skill", "feedback": "", "timestamp": "..."}, + {"run_id": "eval-2-with_skill", "feedback": "perfect, love this", "timestamp": "..."} + ], + "status": "complete" +} +``` + +Empty feedback means the user thought it was fine. Focus your improvements on the test cases where the user had specific complaints. + +Kill the viewer server when you're done with it: + +```bash +kill $VIEWER_PID 2>/dev/null +``` + +--- + +## Improving the skill + +This is the heart of the loop. You've run the test cases, the user has reviewed the results, and now you need to make the skill better based on their feedback. + +### How to think about improvements + +1. **Generalize from the feedback.** The big picture thing that's happening here is that we're trying to create skills that can be used a million times (maybe literally, maybe even more who knows) across many different prompts. Here you and the user are iterating on only a few examples over and over again because it helps move faster. The user knows these examples in and out and it's quick for them to assess new outputs. But if the skill you and the user are codeveloping works only for those examples, it's useless. Rather than put in fiddly overfitty changes, or oppressively constrictive MUSTs, if there's some stubborn issue, you might try branching out and using different metaphors, or recommending different patterns of working. It's relatively cheap to try and maybe you'll land on something great. + +2. **Keep the prompt lean.** Remove things that aren't pulling their weight. Make sure to read the transcripts, not just the final outputs — if it looks like the skill is making the model waste a bunch of time doing things that are unproductive, you can try getting rid of the parts of the skill that are making it do that and seeing what happens. + +3. **Explain the why.** Try hard to explain the **why** behind everything you're asking the model to do. Today's LLMs are *smart*. They have good theory of mind and when given a good harness can go beyond rote instructions and really make things happen. Even if the feedback from the user is terse or frustrated, try to actually understand the task and why the user is writing what they wrote, and what they actually wrote, and then transmit this understanding into the instructions. If you find yourself writing ALWAYS or NEVER in all caps, or using super rigid structures, that's a yellow flag — if possible, reframe and explain the reasoning so that the model understands why the thing you're asking for is important. That's a more humane, powerful, and effective approach. + +4. **Look for repeated work across test cases.** Read the transcripts from the test runs and notice if the subagents all independently wrote similar helper scripts or took the same multi-step approach to something. If all 3 test cases resulted in the subagent writing a `create_docx.py` or a `build_chart.py`, that's a strong signal the skill should bundle that script. Write it once, put it in `scripts/`, and tell the skill to use it. This saves every future invocation from reinventing the wheel. + +This task is pretty important (we are trying to create billions a year in economic value here!) and your thinking time is not the blocker; take your time and really mull things over. I'd suggest writing a draft revision and then looking at it anew and making improvements. Really do your best to get into the head of the user and understand what they want and need. + +### The iteration loop + +After improving the skill: + +1. Apply your improvements to the skill +2. Rerun all test cases into a new `iteration-/` directory, including baseline runs. If you're creating a new skill, the baseline is always `without_skill` (no skill) — that stays the same across iterations. If you're improving an existing skill, use your judgment on what makes sense as the baseline: the original version the user came in with, or the previous iteration. +3. Launch the reviewer with `--previous-workspace` pointing at the previous iteration +4. Wait for the user to review and tell you they're done +5. Read the new feedback, improve again, repeat + +Keep going until: +- The user says they're happy +- The feedback is all empty (everything looks good) +- You're not making meaningful progress + +--- + +## Advanced: Blind comparison + +For situations where you want a more rigorous comparison between two versions of a skill (e.g., the user asks "is the new version actually better?"), there's a blind comparison system. Read `agents/comparator.md` and `agents/analyzer.md` for the details. The basic idea is: give two outputs to an independent agent without telling it which is which, and let it judge quality. Then analyze why the winner won. + +This is optional, requires subagents, and most users won't need it. The human review loop is usually sufficient. + +--- + +## Description Optimization + +The description field in SKILL.md frontmatter is the primary mechanism that determines whether GLM invokes a skill. After creating or improving a skill, offer to optimize the description for better triggering accuracy. + +### Step 1: Generate trigger eval queries + +Create 20 eval queries — a mix of should-trigger and should-not-trigger. Save as JSON: + +```json +[ + {"query": "the user prompt", "should_trigger": true}, + {"query": "another prompt", "should_trigger": false} +] +``` + +The queries must be realistic and something a GLM Code or GLM.ai user would actually type. Not abstract requests, but requests that are concrete and specific and have a good amount of detail. For instance, file paths, personal context about the user's job or situation, column names and values, company names, URLs. A little bit of backstory. Some might be in lowercase or contain abbreviations or typos or casual speech. Use a mix of different lengths, and focus on edge cases rather than making them clear-cut (the user will get a chance to sign off on them). + +Bad: `"Format this data"`, `"Extract text from PDF"`, `"Create a chart"` + +Good: `"ok so my boss just sent me this xlsx file (its in my downloads, called something like 'Q4 sales final FINAL v2.xlsx') and she wants me to add a column that shows the profit margin as a percentage. The revenue is in column C and costs are in column D i think"` + +For the **should-trigger** queries (8-10), think about coverage. You want different phrasings of the same intent — some formal, some casual. Include cases where the user doesn't explicitly name the skill or file type but clearly needs it. Throw in some uncommon use cases and cases where this skill competes with another but should win. + +For the **should-not-trigger** queries (8-10), the most valuable ones are the near-misses — queries that share keywords or concepts with the skill but actually need something different. Think adjacent domains, ambiguous phrasing where a naive keyword match would trigger but shouldn't, and cases where the query touches on something the skill does but in a context where another tool is more appropriate. + +The key thing to avoid: don't make should-not-trigger queries obviously irrelevant. "Write a fibonacci function" as a negative test for a PDF skill is too easy — it doesn't test anything. The negative cases should be genuinely tricky. + +### Step 2: Review with user + +Present the eval set to the user for review using the HTML template: + +1. Read the template from `assets/eval_review.html` +2. Replace the placeholders: + - `__EVAL_DATA_PLACEHOLDER__` → the JSON array of eval items (no quotes around it — it's a JS variable assignment) + - `__SKILL_NAME_PLACEHOLDER__` → the skill's name + - `__SKILL_DESCRIPTION_PLACEHOLDER__` → the skill's current description +3. Write to a temp file (e.g., `/tmp/eval_review_.html`) and open it: `open /tmp/eval_review_.html` +4. The user can edit queries, toggle should-trigger, add/remove entries, then click "Export Eval Set" +5. The file downloads to `~/Downloads/eval_set.json` — check the Downloads folder for the most recent version in case there are multiple (e.g., `eval_set (1).json`) + +This step matters — bad eval queries lead to bad descriptions. + +### Step 3: Run the optimization loop + +Tell the user: "This will take some time — I'll run the optimization loop in the background and check on it periodically." + +Save the eval set to the workspace, then run in the background: + +```bash +python -m scripts.run_loop \ + --eval-set \ + --skill-path \ + --model \ + --max-iterations 5 \ + --verbose +``` + +Use the model ID from your system prompt (the one powering the current session) so the triggering test matches what the user actually experiences. + +While it runs, periodically tail the output to give the user updates on which iteration it's on and what the scores look like. + +This handles the full optimization loop automatically. It splits the eval set into 60% train and 40% held-out test, evaluates the current description (running each query 3 times to get a reliable trigger rate), then calls GLM to propose improvements based on what failed. It re-evaluates each new description on both train and test, iterating up to 5 times. When it's done, it opens an HTML report in the browser showing the results per iteration and returns JSON with `best_description` — selected by test score rather than train score to avoid overfitting. + +### How skill triggering works + +Understanding the triggering mechanism helps design better eval queries. Skills appear in GLM's `available_skills` list with their name + description, and GLM decides whether to consult a skill based on that description. The important thing to know is that GLM only consults skills for tasks it can't easily handle on its own — simple, one-step queries like "read this PDF" may not trigger a skill even if the description matches perfectly, because GLM can handle them directly with basic tools. Complex, multi-step, or specialized queries reliably trigger skills when the description matches. + +This means your eval queries should be substantive enough that GLM would actually benefit from consulting a skill. Simple queries like "read file X" are poor test cases — they won't trigger skills regardless of description quality. + +### Step 4: Apply the result + +Take `best_description` from the JSON output and update the skill's SKILL.md frontmatter. Show the user before/after and report the scores. + +--- + +### Package and Present (only if `present_files` tool is available) + +Check whether you have access to the `present_files` tool. If you don't, skip this step. If you do, package the skill and present the .skill file to the user: + +```bash +python -m scripts.package_skill +``` + +After packaging, direct the user to the resulting `.skill` file path so they can install it. + +--- + +## GLM.ai-specific instructions + +In GLM.ai, the core workflow is the same (draft → test → review → improve → repeat), but because GLM.ai doesn't have subagents, some mechanics change. Here's what to adapt: + +**Running test cases**: No subagents means no parallel execution. For each test case, read the skill's SKILL.md, then follow its instructions to accomplish the test prompt yourself. Do them one at a time. This is less rigorous than independent subagents (you wrote the skill and you're also running it, so you have full context), but it's a useful sanity check — and the human review step compensates. Skip the baseline runs — just use the skill to complete the task as requested. + +**Reviewing results**: If you can't open a browser (e.g., GLM.ai's VM has no display, or you're on a remote server), skip the browser reviewer entirely. Instead, present results directly in the conversation. For each test case, show the prompt and the output. If the output is a file the user needs to see (like a .docx or .xlsx), save it to the filesystem and tell them where it is so they can download and inspect it. Ask for feedback inline: "How does this look? Anything you'd change?" + +**Benchmarking**: Skip the quantitative benchmarking — it relies on baseline comparisons which aren't meaningful without subagents. Focus on qualitative feedback from the user. + +**The iteration loop**: Same as before — improve the skill, rerun the test cases, ask for feedback — just without the browser reviewer in the middle. You can still organize results into iteration directories on the filesystem if you have one. + +**Description optimization**: This section requires the `glm` CLI tool (specifically `glm -p`) which is only available in GLM Code. Skip it if you're on GLM.ai. + +**Blind comparison**: Requires subagents. Skip it. + +**Packaging**: The `package_skill.py` script works anywhere with Python and a filesystem. On GLM.ai, you can run it and the user can download the resulting `.skill` file. + +**Updating an existing skill**: The user might be asking you to update an existing skill, not create a new one. In this case: +- **Preserve the original name.** Note the skill's directory name and `name` frontmatter field -- use them unchanged. E.g., if the installed skill is `research-helper`, output `research-helper.skill` (not `research-helper-v2`). +- **Copy to a writeable location before editing.** The installed skill path may be read-only. Copy to `/tmp/skill-name/`, edit there, and package from the copy. +- **If packaging manually, stage in `/tmp/` first**, then copy to the output directory -- direct writes may fail due to permissions. + +--- + +## Cowork-Specific Instructions + +If you're in Cowork, the main things to know are: + +- You have subagents, so the main workflow (spawn test cases in parallel, run baselines, grade, etc.) all works. (However, if you run into severe problems with timeouts, it's OK to run the test prompts in series rather than parallel.) +- You don't have a browser or display, so when generating the eval viewer, use `--static ` to write a standalone HTML file instead of starting a server. Then proffer a link that the user can click to open the HTML in their browser. +- For whatever reason, the Cowork setup seems to disincline GLM from generating the eval viewer after running the tests, so just to reiterate: whether you're in Cowork or in GLM Code, after running tests, you should always generate the eval viewer for the human to look at examples before revising the skill yourself and trying to make corrections, using `generate_review.py` (not writing your own boutique html code). Sorry in advance but I'm gonna go all caps here: GENERATE THE EVAL VIEWER *BEFORE* evaluating inputs yourself. You want to get them in front of the human ASAP! +- Feedback works differently: since there's no running server, the viewer's "Submit All Reviews" button will download `feedback.json` as a file. You can then read it from there (you may have to request access first). +- Packaging works — `package_skill.py` just needs Python and a filesystem. +- Description optimization (`run_loop.py` / `run_eval.py`) should work in Cowork just fine since it uses `glm -p` via subprocess, not a browser, but please save it until you've fully finished making the skill and the user agrees it's in good shape. +- **Updating an existing skill**: The user might be asking you to update an existing skill, not create a new one. Follow the update guidance in the glm.ai section above. + +--- + +## Reference files + +The agents/ directory contains instructions for specialized subagents. Read them when you need to spawn the relevant subagent. + +- `agents/grader.md` — How to evaluate assertions against outputs +- `agents/comparator.md` — How to do blind A/B comparison between two outputs +- `agents/analyzer.md` — How to analyze why one version beat another + +The references/ directory has additional documentation: +- `references/schemas.md` — JSON structures for evals.json, grading.json, etc. + +--- + +Repeating one more time the core loop here for emphasis: + +- Figure out what the skill is about +- Draft or edit the skill +- Run glm-with-access-to-the-skill on test prompts +- With the user, evaluate the outputs: + - Create benchmark.json and run `eval-viewer/generate_review.py` to help the user review them + - Run quantitative evals +- Repeat until you and the user are satisfied +- Package the final skill and return it to the user. + +Please add steps to your TodoList, if you have such a thing, to make sure you don't forget. If you're in Cowork, please specifically put "Create evals JSON and run `eval-viewer/generate_review.py` so human can review test cases" in your TodoList to make sure it happens. + +Good luck! diff --git a/skills/skill-creator/agents/analyzer.md b/skills/skill-creator/agents/analyzer.md new file mode 100755 index 0000000..14e41d6 --- /dev/null +++ b/skills/skill-creator/agents/analyzer.md @@ -0,0 +1,274 @@ +# Post-hoc Analyzer Agent + +Analyze blind comparison results to understand WHY the winner won and generate improvement suggestions. + +## Role + +After the blind comparator determines a winner, the Post-hoc Analyzer "unblids" the results by examining the skills and transcripts. The goal is to extract actionable insights: what made the winner better, and how can the loser be improved? + +## Inputs + +You receive these parameters in your prompt: + +- **winner**: "A" or "B" (from blind comparison) +- **winner_skill_path**: Path to the skill that produced the winning output +- **winner_transcript_path**: Path to the execution transcript for the winner +- **loser_skill_path**: Path to the skill that produced the losing output +- **loser_transcript_path**: Path to the execution transcript for the loser +- **comparison_result_path**: Path to the blind comparator's output JSON +- **output_path**: Where to save the analysis results + +## Process + +### Step 1: Read Comparison Result + +1. Read the blind comparator's output at comparison_result_path +2. Note the winning side (A or B), the reasoning, and any scores +3. Understand what the comparator valued in the winning output + +### Step 2: Read Both Skills + +1. Read the winner skill's SKILL.md and key referenced files +2. Read the loser skill's SKILL.md and key referenced files +3. Identify structural differences: + - Instructions clarity and specificity + - Script/tool usage patterns + - Example coverage + - Edge case handling + +### Step 3: Read Both Transcripts + +1. Read the winner's transcript +2. Read the loser's transcript +3. Compare execution patterns: + - How closely did each follow their skill's instructions? + - What tools were used differently? + - Where did the loser diverge from optimal behavior? + - Did either encounter errors or make recovery attempts? + +### Step 4: Analyze Instruction Following + +For each transcript, evaluate: +- Did the agent follow the skill's explicit instructions? +- Did the agent use the skill's provided tools/scripts? +- Were there missed opportunities to leverage skill content? +- Did the agent add unnecessary steps not in the skill? + +Score instruction following 1-10 and note specific issues. + +### Step 5: Identify Winner Strengths + +Determine what made the winner better: +- Clearer instructions that led to better behavior? +- Better scripts/tools that produced better output? +- More comprehensive examples that guided edge cases? +- Better error handling guidance? + +Be specific. Quote from skills/transcripts where relevant. + +### Step 6: Identify Loser Weaknesses + +Determine what held the loser back: +- Ambiguous instructions that led to suboptimal choices? +- Missing tools/scripts that forced workarounds? +- Gaps in edge case coverage? +- Poor error handling that caused failures? + +### Step 7: Generate Improvement Suggestions + +Based on the analysis, produce actionable suggestions for improving the loser skill: +- Specific instruction changes to make +- Tools/scripts to add or modify +- Examples to include +- Edge cases to address + +Prioritize by impact. Focus on changes that would have changed the outcome. + +### Step 8: Write Analysis Results + +Save structured analysis to `{output_path}`. + +## Output Format + +Write a JSON file with this structure: + +```json +{ + "comparison_summary": { + "winner": "A", + "winner_skill": "path/to/winner/skill", + "loser_skill": "path/to/loser/skill", + "comparator_reasoning": "Brief summary of why comparator chose winner" + }, + "winner_strengths": [ + "Clear step-by-step instructions for handling multi-page documents", + "Included validation script that caught formatting errors", + "Explicit guidance on fallback behavior when OCR fails" + ], + "loser_weaknesses": [ + "Vague instruction 'process the document appropriately' led to inconsistent behavior", + "No script for validation, agent had to improvise and made errors", + "No guidance on OCR failure, agent gave up instead of trying alternatives" + ], + "instruction_following": { + "winner": { + "score": 9, + "issues": [ + "Minor: skipped optional logging step" + ] + }, + "loser": { + "score": 6, + "issues": [ + "Did not use the skill's formatting template", + "Invented own approach instead of following step 3", + "Missed the 'always validate output' instruction" + ] + } + }, + "improvement_suggestions": [ + { + "priority": "high", + "category": "instructions", + "suggestion": "Replace 'process the document appropriately' with explicit steps: 1) Extract text, 2) Identify sections, 3) Format per template", + "expected_impact": "Would eliminate ambiguity that caused inconsistent behavior" + }, + { + "priority": "high", + "category": "tools", + "suggestion": "Add validate_output.py script similar to winner skill's validation approach", + "expected_impact": "Would catch formatting errors before final output" + }, + { + "priority": "medium", + "category": "error_handling", + "suggestion": "Add fallback instructions: 'If OCR fails, try: 1) different resolution, 2) image preprocessing, 3) manual extraction'", + "expected_impact": "Would prevent early failure on difficult documents" + } + ], + "transcript_insights": { + "winner_execution_pattern": "Read skill -> Followed 5-step process -> Used validation script -> Fixed 2 issues -> Produced output", + "loser_execution_pattern": "Read skill -> Unclear on approach -> Tried 3 different methods -> No validation -> Output had errors" + } +} +``` + +## Guidelines + +- **Be specific**: Quote from skills and transcripts, don't just say "instructions were unclear" +- **Be actionable**: Suggestions should be concrete changes, not vague advice +- **Focus on skill improvements**: The goal is to improve the losing skill, not critique the agent +- **Prioritize by impact**: Which changes would most likely have changed the outcome? +- **Consider causation**: Did the skill weakness actually cause the worse output, or is it incidental? +- **Stay objective**: Analyze what happened, don't editorialize +- **Think about generalization**: Would this improvement help on other evals too? + +## Categories for Suggestions + +Use these categories to organize improvement suggestions: + +| Category | Description | +|----------|-------------| +| `instructions` | Changes to the skill's prose instructions | +| `tools` | Scripts, templates, or utilities to add/modify | +| `examples` | Example inputs/outputs to include | +| `error_handling` | Guidance for handling failures | +| `structure` | Reorganization of skill content | +| `references` | External docs or resources to add | + +## Priority Levels + +- **high**: Would likely change the outcome of this comparison +- **medium**: Would improve quality but may not change win/loss +- **low**: Nice to have, marginal improvement + +--- + +# Analyzing Benchmark Results + +When analyzing benchmark results, the analyzer's purpose is to **surface patterns and anomalies** across multiple runs, not suggest skill improvements. + +## Role + +Review all benchmark run results and generate freeform notes that help the user understand skill performance. Focus on patterns that wouldn't be visible from aggregate metrics alone. + +## Inputs + +You receive these parameters in your prompt: + +- **benchmark_data_path**: Path to the in-progress benchmark.json with all run results +- **skill_path**: Path to the skill being benchmarked +- **output_path**: Where to save the notes (as JSON array of strings) + +## Process + +### Step 1: Read Benchmark Data + +1. Read the benchmark.json containing all run results +2. Note the configurations tested (with_skill, without_skill) +3. Understand the run_summary aggregates already calculated + +### Step 2: Analyze Per-Assertion Patterns + +For each expectation across all runs: +- Does it **always pass** in both configurations? (may not differentiate skill value) +- Does it **always fail** in both configurations? (may be broken or beyond capability) +- Does it **always pass with skill but fail without**? (skill clearly adds value here) +- Does it **always fail with skill but pass without**? (skill may be hurting) +- Is it **highly variable**? (flaky expectation or non-deterministic behavior) + +### Step 3: Analyze Cross-Eval Patterns + +Look for patterns across evals: +- Are certain eval types consistently harder/easier? +- Do some evals show high variance while others are stable? +- Are there surprising results that contradict expectations? + +### Step 4: Analyze Metrics Patterns + +Look at time_seconds, tokens, tool_calls: +- Does the skill significantly increase execution time? +- Is there high variance in resource usage? +- Are there outlier runs that skew the aggregates? + +### Step 5: Generate Notes + +Write freeform observations as a list of strings. Each note should: +- State a specific observation +- Be grounded in the data (not speculation) +- Help the user understand something the aggregate metrics don't show + +Examples: +- "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value" +- "Eval 3 shows high variance (50% ± 40%) - run 2 had an unusual failure that may be flaky" +- "Without-skill runs consistently fail on table extraction expectations (0% pass rate)" +- "Skill adds 13s average execution time but improves pass rate by 50%" +- "Token usage is 80% higher with skill, primarily due to script output parsing" +- "All 3 without-skill runs for eval 1 produced empty output" + +### Step 6: Write Notes + +Save notes to `{output_path}` as a JSON array of strings: + +```json +[ + "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value", + "Eval 3 shows high variance (50% ± 40%) - run 2 had an unusual failure", + "Without-skill runs consistently fail on table extraction expectations", + "Skill adds 13s average execution time but improves pass rate by 50%" +] +``` + +## Guidelines + +**DO:** +- Report what you observe in the data +- Be specific about which evals, expectations, or runs you're referring to +- Note patterns that aggregate metrics would hide +- Provide context that helps interpret the numbers + +**DO NOT:** +- Suggest improvements to the skill (that's for the improvement step, not benchmarking) +- Make subjective quality judgments ("the output was good/bad") +- Speculate about causes without evidence +- Repeat information already in the run_summary aggregates diff --git a/skills/skill-creator/agents/comparator.md b/skills/skill-creator/agents/comparator.md new file mode 100755 index 0000000..80e00eb --- /dev/null +++ b/skills/skill-creator/agents/comparator.md @@ -0,0 +1,202 @@ +# Blind Comparator Agent + +Compare two outputs WITHOUT knowing which skill produced them. + +## Role + +The Blind Comparator judges which output better accomplishes the eval task. You receive two outputs labeled A and B, but you do NOT know which skill produced which. This prevents bias toward a particular skill or approach. + +Your judgment is based purely on output quality and task completion. + +## Inputs + +You receive these parameters in your prompt: + +- **output_a_path**: Path to the first output file or directory +- **output_b_path**: Path to the second output file or directory +- **eval_prompt**: The original task/prompt that was executed +- **expectations**: List of expectations to check (optional - may be empty) + +## Process + +### Step 1: Read Both Outputs + +1. Examine output A (file or directory) +2. Examine output B (file or directory) +3. Note the type, structure, and content of each +4. If outputs are directories, examine all relevant files inside + +### Step 2: Understand the Task + +1. Read the eval_prompt carefully +2. Identify what the task requires: + - What should be produced? + - What qualities matter (accuracy, completeness, format)? + - What would distinguish a good output from a poor one? + +### Step 3: Generate Evaluation Rubric + +Based on the task, generate a rubric with two dimensions: + +**Content Rubric** (what the output contains): +| Criterion | 1 (Poor) | 3 (Acceptable) | 5 (Excellent) | +|-----------|----------|----------------|---------------| +| Correctness | Major errors | Minor errors | Fully correct | +| Completeness | Missing key elements | Mostly complete | All elements present | +| Accuracy | Significant inaccuracies | Minor inaccuracies | Accurate throughout | + +**Structure Rubric** (how the output is organized): +| Criterion | 1 (Poor) | 3 (Acceptable) | 5 (Excellent) | +|-----------|----------|----------------|---------------| +| Organization | Disorganized | Reasonably organized | Clear, logical structure | +| Formatting | Inconsistent/broken | Mostly consistent | Professional, polished | +| Usability | Difficult to use | Usable with effort | Easy to use | + +Adapt criteria to the specific task. For example: +- PDF form → "Field alignment", "Text readability", "Data placement" +- Document → "Section structure", "Heading hierarchy", "Paragraph flow" +- Data output → "Schema correctness", "Data types", "Completeness" + +### Step 4: Evaluate Each Output Against the Rubric + +For each output (A and B): + +1. **Score each criterion** on the rubric (1-5 scale) +2. **Calculate dimension totals**: Content score, Structure score +3. **Calculate overall score**: Average of dimension scores, scaled to 1-10 + +### Step 5: Check Assertions (if provided) + +If expectations are provided: + +1. Check each expectation against output A +2. Check each expectation against output B +3. Count pass rates for each output +4. Use expectation scores as secondary evidence (not the primary decision factor) + +### Step 6: Determine the Winner + +Compare A and B based on (in priority order): + +1. **Primary**: Overall rubric score (content + structure) +2. **Secondary**: Assertion pass rates (if applicable) +3. **Tiebreaker**: If truly equal, declare a TIE + +Be decisive - ties should be rare. One output is usually better, even if marginally. + +### Step 7: Write Comparison Results + +Save results to a JSON file at the path specified (or `comparison.json` if not specified). + +## Output Format + +Write a JSON file with this structure: + +```json +{ + "winner": "A", + "reasoning": "Output A provides a complete solution with proper formatting and all required fields. Output B is missing the date field and has formatting inconsistencies.", + "rubric": { + "A": { + "content": { + "correctness": 5, + "completeness": 5, + "accuracy": 4 + }, + "structure": { + "organization": 4, + "formatting": 5, + "usability": 4 + }, + "content_score": 4.7, + "structure_score": 4.3, + "overall_score": 9.0 + }, + "B": { + "content": { + "correctness": 3, + "completeness": 2, + "accuracy": 3 + }, + "structure": { + "organization": 3, + "formatting": 2, + "usability": 3 + }, + "content_score": 2.7, + "structure_score": 2.7, + "overall_score": 5.4 + } + }, + "output_quality": { + "A": { + "score": 9, + "strengths": ["Complete solution", "Well-formatted", "All fields present"], + "weaknesses": ["Minor style inconsistency in header"] + }, + "B": { + "score": 5, + "strengths": ["Readable output", "Correct basic structure"], + "weaknesses": ["Missing date field", "Formatting inconsistencies", "Partial data extraction"] + } + }, + "expectation_results": { + "A": { + "passed": 4, + "total": 5, + "pass_rate": 0.80, + "details": [ + {"text": "Output includes name", "passed": true}, + {"text": "Output includes date", "passed": true}, + {"text": "Format is PDF", "passed": true}, + {"text": "Contains signature", "passed": false}, + {"text": "Readable text", "passed": true} + ] + }, + "B": { + "passed": 3, + "total": 5, + "pass_rate": 0.60, + "details": [ + {"text": "Output includes name", "passed": true}, + {"text": "Output includes date", "passed": false}, + {"text": "Format is PDF", "passed": true}, + {"text": "Contains signature", "passed": false}, + {"text": "Readable text", "passed": true} + ] + } + } +} +``` + +If no expectations were provided, omit the `expectation_results` field entirely. + +## Field Descriptions + +- **winner**: "A", "B", or "TIE" +- **reasoning**: Clear explanation of why the winner was chosen (or why it's a tie) +- **rubric**: Structured rubric evaluation for each output + - **content**: Scores for content criteria (correctness, completeness, accuracy) + - **structure**: Scores for structure criteria (organization, formatting, usability) + - **content_score**: Average of content criteria (1-5) + - **structure_score**: Average of structure criteria (1-5) + - **overall_score**: Combined score scaled to 1-10 +- **output_quality**: Summary quality assessment + - **score**: 1-10 rating (should match rubric overall_score) + - **strengths**: List of positive aspects + - **weaknesses**: List of issues or shortcomings +- **expectation_results**: (Only if expectations provided) + - **passed**: Number of expectations that passed + - **total**: Total number of expectations + - **pass_rate**: Fraction passed (0.0 to 1.0) + - **details**: Individual expectation results + +## Guidelines + +- **Stay blind**: DO NOT try to infer which skill produced which output. Judge purely on output quality. +- **Be specific**: Cite specific examples when explaining strengths and weaknesses. +- **Be decisive**: Choose a winner unless outputs are genuinely equivalent. +- **Output quality first**: Assertion scores are secondary to overall task completion. +- **Be objective**: Don't favor outputs based on style preferences; focus on correctness and completeness. +- **Explain your reasoning**: The reasoning field should make it clear why you chose the winner. +- **Handle edge cases**: If both outputs fail, pick the one that fails less badly. If both are excellent, pick the one that's marginally better. diff --git a/skills/skill-creator/agents/grader.md b/skills/skill-creator/agents/grader.md new file mode 100755 index 0000000..558ab05 --- /dev/null +++ b/skills/skill-creator/agents/grader.md @@ -0,0 +1,223 @@ +# Grader Agent + +Evaluate expectations against an execution transcript and outputs. + +## Role + +The Grader reviews a transcript and output files, then determines whether each expectation passes or fails. Provide clear evidence for each judgment. + +You have two jobs: grade the outputs, and critique the evals themselves. A passing grade on a weak assertion is worse than useless — it creates false confidence. When you notice an assertion that's trivially satisfied, or an important outcome that no assertion checks, say so. + +## Inputs + +You receive these parameters in your prompt: + +- **expectations**: List of expectations to evaluate (strings) +- **transcript_path**: Path to the execution transcript (markdown file) +- **outputs_dir**: Directory containing output files from execution + +## Process + +### Step 1: Read the Transcript + +1. Read the transcript file completely +2. Note the eval prompt, execution steps, and final result +3. Identify any issues or errors documented + +### Step 2: Examine Output Files + +1. List files in outputs_dir +2. Read/examine each file relevant to the expectations. If outputs aren't plain text, use the inspection tools provided in your prompt — don't rely solely on what the transcript says the executor produced. +3. Note contents, structure, and quality + +### Step 3: Evaluate Each Assertion + +For each expectation: + +1. **Search for evidence** in the transcript and outputs +2. **Determine verdict**: + - **PASS**: Clear evidence the expectation is true AND the evidence reflects genuine task completion, not just surface-level compliance + - **FAIL**: No evidence, or evidence contradicts the expectation, or the evidence is superficial (e.g., correct filename but empty/wrong content) +3. **Cite the evidence**: Quote the specific text or describe what you found + +### Step 4: Extract and Verify Claims + +Beyond the predefined expectations, extract implicit claims from the outputs and verify them: + +1. **Extract claims** from the transcript and outputs: + - Factual statements ("The form has 12 fields") + - Process claims ("Used pypdf to fill the form") + - Quality claims ("All fields were filled correctly") + +2. **Verify each claim**: + - **Factual claims**: Can be checked against the outputs or external sources + - **Process claims**: Can be verified from the transcript + - **Quality claims**: Evaluate whether the claim is justified + +3. **Flag unverifiable claims**: Note claims that cannot be verified with available information + +This catches issues that predefined expectations might miss. + +### Step 5: Read User Notes + +If `{outputs_dir}/user_notes.md` exists: +1. Read it and note any uncertainties or issues flagged by the executor +2. Include relevant concerns in the grading output +3. These may reveal problems even when expectations pass + +### Step 6: Critique the Evals + +After grading, consider whether the evals themselves could be improved. Only surface suggestions when there's a clear gap. + +Good suggestions test meaningful outcomes — assertions that are hard to satisfy without actually doing the work correctly. Think about what makes an assertion *discriminating*: it passes when the skill genuinely succeeds and fails when it doesn't. + +Suggestions worth raising: +- An assertion that passed but would also pass for a clearly wrong output (e.g., checking filename existence but not file content) +- An important outcome you observed — good or bad — that no assertion covers at all +- An assertion that can't actually be verified from the available outputs + +Keep the bar high. The goal is to flag things the eval author would say "good catch" about, not to nitpick every assertion. + +### Step 7: Write Grading Results + +Save results to `{outputs_dir}/../grading.json` (sibling to outputs_dir). + +## Grading Criteria + +**PASS when**: +- The transcript or outputs clearly demonstrate the expectation is true +- Specific evidence can be cited +- The evidence reflects genuine substance, not just surface compliance (e.g., a file exists AND contains correct content, not just the right filename) + +**FAIL when**: +- No evidence found for the expectation +- Evidence contradicts the expectation +- The expectation cannot be verified from available information +- The evidence is superficial — the assertion is technically satisfied but the underlying task outcome is wrong or incomplete +- The output appears to meet the assertion by coincidence rather than by actually doing the work + +**When uncertain**: The burden of proof to pass is on the expectation. + +### Step 8: Read Executor Metrics and Timing + +1. If `{outputs_dir}/metrics.json` exists, read it and include in grading output +2. If `{outputs_dir}/../timing.json` exists, read it and include timing data + +## Output Format + +Write a JSON file with this structure: + +```json +{ + "expectations": [ + { + "text": "The output includes the name 'John Smith'", + "passed": true, + "evidence": "Found in transcript Step 3: 'Extracted names: John Smith, Sarah Johnson'" + }, + { + "text": "The spreadsheet has a SUM formula in cell B10", + "passed": false, + "evidence": "No spreadsheet was created. The output was a text file." + }, + { + "text": "The assistant used the skill's OCR script", + "passed": true, + "evidence": "Transcript Step 2 shows: 'Tool: Bash - python ocr_script.py image.png'" + } + ], + "summary": { + "passed": 2, + "failed": 1, + "total": 3, + "pass_rate": 0.67 + }, + "execution_metrics": { + "tool_calls": { + "Read": 5, + "Write": 2, + "Bash": 8 + }, + "total_tool_calls": 15, + "total_steps": 6, + "errors_encountered": 0, + "output_chars": 12450, + "transcript_chars": 3200 + }, + "timing": { + "executor_duration_seconds": 165.0, + "grader_duration_seconds": 26.0, + "total_duration_seconds": 191.0 + }, + "claims": [ + { + "claim": "The form has 12 fillable fields", + "type": "factual", + "verified": true, + "evidence": "Counted 12 fields in field_info.json" + }, + { + "claim": "All required fields were populated", + "type": "quality", + "verified": false, + "evidence": "Reference section was left blank despite data being available" + } + ], + "user_notes_summary": { + "uncertainties": ["Used 2023 data, may be stale"], + "needs_review": [], + "workarounds": ["Fell back to text overlay for non-fillable fields"] + }, + "eval_feedback": { + "suggestions": [ + { + "assertion": "The output includes the name 'John Smith'", + "reason": "A hallucinated document that mentions the name would also pass — consider checking it appears as the primary contact with matching phone and email from the input" + }, + { + "reason": "No assertion checks whether the extracted phone numbers match the input — I observed incorrect numbers in the output that went uncaught" + } + ], + "overall": "Assertions check presence but not correctness. Consider adding content verification." + } +} +``` + +## Field Descriptions + +- **expectations**: Array of graded expectations + - **text**: The original expectation text + - **passed**: Boolean - true if expectation passes + - **evidence**: Specific quote or description supporting the verdict +- **summary**: Aggregate statistics + - **passed**: Count of passed expectations + - **failed**: Count of failed expectations + - **total**: Total expectations evaluated + - **pass_rate**: Fraction passed (0.0 to 1.0) +- **execution_metrics**: Copied from executor's metrics.json (if available) + - **output_chars**: Total character count of output files (proxy for tokens) + - **transcript_chars**: Character count of transcript +- **timing**: Wall clock timing from timing.json (if available) + - **executor_duration_seconds**: Time spent in executor subagent + - **total_duration_seconds**: Total elapsed time for the run +- **claims**: Extracted and verified claims from the output + - **claim**: The statement being verified + - **type**: "factual", "process", or "quality" + - **verified**: Boolean - whether the claim holds + - **evidence**: Supporting or contradicting evidence +- **user_notes_summary**: Issues flagged by the executor + - **uncertainties**: Things the executor wasn't sure about + - **needs_review**: Items requiring human attention + - **workarounds**: Places where the skill didn't work as expected +- **eval_feedback**: Improvement suggestions for the evals (only when warranted) + - **suggestions**: List of concrete suggestions, each with a `reason` and optionally an `assertion` it relates to + - **overall**: Brief assessment — can be "No suggestions, evals look solid" if nothing to flag + +## Guidelines + +- **Be objective**: Base verdicts on evidence, not assumptions +- **Be specific**: Quote the exact text that supports your verdict +- **Be thorough**: Check both transcript and output files +- **Be consistent**: Apply the same standard to each expectation +- **Explain failures**: Make it clear why evidence was insufficient +- **No partial credit**: Each expectation is pass or fail, not partial diff --git a/skills/skill-creator/assets/eval_review.html b/skills/skill-creator/assets/eval_review.html new file mode 100755 index 0000000..938ff32 --- /dev/null +++ b/skills/skill-creator/assets/eval_review.html @@ -0,0 +1,146 @@ + + + + + + Eval Set Review - __SKILL_NAME_PLACEHOLDER__ + + + + + + +

Eval Set Review: __SKILL_NAME_PLACEHOLDER__

+

Current description: __SKILL_DESCRIPTION_PLACEHOLDER__

+ +
+ + +
+ + + + + + + + + + +
QueryShould TriggerActions
+ +

+ + + + diff --git a/skills/skill-creator/eval-viewer/generate_review.py b/skills/skill-creator/eval-viewer/generate_review.py new file mode 100755 index 0000000..7fa5978 --- /dev/null +++ b/skills/skill-creator/eval-viewer/generate_review.py @@ -0,0 +1,471 @@ +#!/usr/bin/env python3 +"""Generate and serve a review page for eval results. + +Reads the workspace directory, discovers runs (directories with outputs/), +embeds all output data into a self-contained HTML page, and serves it via +a tiny HTTP server. Feedback auto-saves to feedback.json in the workspace. + +Usage: + python generate_review.py [--port PORT] [--skill-name NAME] + python generate_review.py --previous-feedback /path/to/old/feedback.json + +No dependencies beyond the Python stdlib are required. +""" + +import argparse +import base64 +import json +import mimetypes +import os +import re +import signal +import subprocess +import sys +import time +import webbrowser +from functools import partial +from http.server import HTTPServer, BaseHTTPRequestHandler +from pathlib import Path + +# Files to exclude from output listings +METADATA_FILES = {"transcript.md", "user_notes.md", "metrics.json"} + +# Extensions we render as inline text +TEXT_EXTENSIONS = { + ".txt", ".md", ".json", ".csv", ".py", ".js", ".ts", ".tsx", ".jsx", + ".yaml", ".yml", ".xml", ".html", ".css", ".sh", ".rb", ".go", ".rs", + ".java", ".c", ".cpp", ".h", ".hpp", ".sql", ".r", ".toml", +} + +# Extensions we render as inline images +IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"} + +# MIME type overrides for common types +MIME_OVERRIDES = { + ".svg": "image/svg+xml", + ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation", +} + + +def get_mime_type(path: Path) -> str: + ext = path.suffix.lower() + if ext in MIME_OVERRIDES: + return MIME_OVERRIDES[ext] + mime, _ = mimetypes.guess_type(str(path)) + return mime or "application/octet-stream" + + +def find_runs(workspace: Path) -> list[dict]: + """Recursively find directories that contain an outputs/ subdirectory.""" + runs: list[dict] = [] + _find_runs_recursive(workspace, workspace, runs) + runs.sort(key=lambda r: (r.get("eval_id", float("inf")), r["id"])) + return runs + + +def _find_runs_recursive(root: Path, current: Path, runs: list[dict]) -> None: + if not current.is_dir(): + return + + outputs_dir = current / "outputs" + if outputs_dir.is_dir(): + run = build_run(root, current) + if run: + runs.append(run) + return + + skip = {"node_modules", ".git", "__pycache__", "skill", "inputs"} + for child in sorted(current.iterdir()): + if child.is_dir() and child.name not in skip: + _find_runs_recursive(root, child, runs) + + +def build_run(root: Path, run_dir: Path) -> dict | None: + """Build a run dict with prompt, outputs, and grading data.""" + prompt = "" + eval_id = None + + # Try eval_metadata.json + for candidate in [run_dir / "eval_metadata.json", run_dir.parent / "eval_metadata.json"]: + if candidate.exists(): + try: + metadata = json.loads(candidate.read_text()) + prompt = metadata.get("prompt", "") + eval_id = metadata.get("eval_id") + except (json.JSONDecodeError, OSError): + pass + if prompt: + break + + # Fall back to transcript.md + if not prompt: + for candidate in [run_dir / "transcript.md", run_dir / "outputs" / "transcript.md"]: + if candidate.exists(): + try: + text = candidate.read_text() + match = re.search(r"## Eval Prompt\n\n([\s\S]*?)(?=\n##|$)", text) + if match: + prompt = match.group(1).strip() + except OSError: + pass + if prompt: + break + + if not prompt: + prompt = "(No prompt found)" + + run_id = str(run_dir.relative_to(root)).replace("/", "-").replace("\\", "-") + + # Collect output files + outputs_dir = run_dir / "outputs" + output_files: list[dict] = [] + if outputs_dir.is_dir(): + for f in sorted(outputs_dir.iterdir()): + if f.is_file() and f.name not in METADATA_FILES: + output_files.append(embed_file(f)) + + # Load grading if present + grading = None + for candidate in [run_dir / "grading.json", run_dir.parent / "grading.json"]: + if candidate.exists(): + try: + grading = json.loads(candidate.read_text()) + except (json.JSONDecodeError, OSError): + pass + if grading: + break + + return { + "id": run_id, + "prompt": prompt, + "eval_id": eval_id, + "outputs": output_files, + "grading": grading, + } + + +def embed_file(path: Path) -> dict: + """Read a file and return an embedded representation.""" + ext = path.suffix.lower() + mime = get_mime_type(path) + + if ext in TEXT_EXTENSIONS: + try: + content = path.read_text(errors="replace") + except OSError: + content = "(Error reading file)" + return { + "name": path.name, + "type": "text", + "content": content, + } + elif ext in IMAGE_EXTENSIONS: + try: + raw = path.read_bytes() + b64 = base64.b64encode(raw).decode("ascii") + except OSError: + return {"name": path.name, "type": "error", "content": "(Error reading file)"} + return { + "name": path.name, + "type": "image", + "mime": mime, + "data_uri": f"data:{mime};base64,{b64}", + } + elif ext == ".pdf": + try: + raw = path.read_bytes() + b64 = base64.b64encode(raw).decode("ascii") + except OSError: + return {"name": path.name, "type": "error", "content": "(Error reading file)"} + return { + "name": path.name, + "type": "pdf", + "data_uri": f"data:{mime};base64,{b64}", + } + elif ext == ".xlsx": + try: + raw = path.read_bytes() + b64 = base64.b64encode(raw).decode("ascii") + except OSError: + return {"name": path.name, "type": "error", "content": "(Error reading file)"} + return { + "name": path.name, + "type": "xlsx", + "data_b64": b64, + } + else: + # Binary / unknown — base64 download link + try: + raw = path.read_bytes() + b64 = base64.b64encode(raw).decode("ascii") + except OSError: + return {"name": path.name, "type": "error", "content": "(Error reading file)"} + return { + "name": path.name, + "type": "binary", + "mime": mime, + "data_uri": f"data:{mime};base64,{b64}", + } + + +def load_previous_iteration(workspace: Path) -> dict[str, dict]: + """Load previous iteration's feedback and outputs. + + Returns a map of run_id -> {"feedback": str, "outputs": list[dict]}. + """ + result: dict[str, dict] = {} + + # Load feedback + feedback_map: dict[str, str] = {} + feedback_path = workspace / "feedback.json" + if feedback_path.exists(): + try: + data = json.loads(feedback_path.read_text()) + feedback_map = { + r["run_id"]: r["feedback"] + for r in data.get("reviews", []) + if r.get("feedback", "").strip() + } + except (json.JSONDecodeError, OSError, KeyError): + pass + + # Load runs (to get outputs) + prev_runs = find_runs(workspace) + for run in prev_runs: + result[run["id"]] = { + "feedback": feedback_map.get(run["id"], ""), + "outputs": run.get("outputs", []), + } + + # Also add feedback for run_ids that had feedback but no matching run + for run_id, fb in feedback_map.items(): + if run_id not in result: + result[run_id] = {"feedback": fb, "outputs": []} + + return result + + +def generate_html( + runs: list[dict], + skill_name: str, + previous: dict[str, dict] | None = None, + benchmark: dict | None = None, +) -> str: + """Generate the complete standalone HTML page with embedded data.""" + template_path = Path(__file__).parent / "viewer.html" + template = template_path.read_text() + + # Build previous_feedback and previous_outputs maps for the template + previous_feedback: dict[str, str] = {} + previous_outputs: dict[str, list[dict]] = {} + if previous: + for run_id, data in previous.items(): + if data.get("feedback"): + previous_feedback[run_id] = data["feedback"] + if data.get("outputs"): + previous_outputs[run_id] = data["outputs"] + + embedded = { + "skill_name": skill_name, + "runs": runs, + "previous_feedback": previous_feedback, + "previous_outputs": previous_outputs, + } + if benchmark: + embedded["benchmark"] = benchmark + + data_json = json.dumps(embedded) + + return template.replace("/*__EMBEDDED_DATA__*/", f"const EMBEDDED_DATA = {data_json};") + + +# --------------------------------------------------------------------------- +# HTTP server (stdlib only, zero dependencies) +# --------------------------------------------------------------------------- + +def _kill_port(port: int) -> None: + """Kill any process listening on the given port.""" + try: + result = subprocess.run( + ["lsof", "-ti", f":{port}"], + capture_output=True, text=True, timeout=5, + ) + for pid_str in result.stdout.strip().split("\n"): + if pid_str.strip(): + try: + os.kill(int(pid_str.strip()), signal.SIGTERM) + except (ProcessLookupError, ValueError): + pass + if result.stdout.strip(): + time.sleep(0.5) + except subprocess.TimeoutExpired: + pass + except FileNotFoundError: + print("Note: lsof not found, cannot check if port is in use", file=sys.stderr) + +class ReviewHandler(BaseHTTPRequestHandler): + """Serves the review HTML and handles feedback saves. + + Regenerates the HTML on each page load so that refreshing the browser + picks up new eval outputs without restarting the server. + """ + + def __init__( + self, + workspace: Path, + skill_name: str, + feedback_path: Path, + previous: dict[str, dict], + benchmark_path: Path | None, + *args, + **kwargs, + ): + self.workspace = workspace + self.skill_name = skill_name + self.feedback_path = feedback_path + self.previous = previous + self.benchmark_path = benchmark_path + super().__init__(*args, **kwargs) + + def do_GET(self) -> None: + if self.path == "/" or self.path == "/index.html": + # Regenerate HTML on each request (re-scans workspace for new outputs) + runs = find_runs(self.workspace) + benchmark = None + if self.benchmark_path and self.benchmark_path.exists(): + try: + benchmark = json.loads(self.benchmark_path.read_text()) + except (json.JSONDecodeError, OSError): + pass + html = generate_html(runs, self.skill_name, self.previous, benchmark) + content = html.encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(content))) + self.end_headers() + self.wfile.write(content) + elif self.path == "/api/feedback": + data = b"{}" + if self.feedback_path.exists(): + data = self.feedback_path.read_bytes() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(data))) + self.end_headers() + self.wfile.write(data) + else: + self.send_error(404) + + def do_POST(self) -> None: + if self.path == "/api/feedback": + length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(length) + try: + data = json.loads(body) + if not isinstance(data, dict) or "reviews" not in data: + raise ValueError("Expected JSON object with 'reviews' key") + self.feedback_path.write_text(json.dumps(data, indent=2) + "\n") + resp = b'{"ok":true}' + self.send_response(200) + except (json.JSONDecodeError, OSError, ValueError) as e: + resp = json.dumps({"error": str(e)}).encode() + self.send_response(500) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(resp))) + self.end_headers() + self.wfile.write(resp) + else: + self.send_error(404) + + def log_message(self, format: str, *args: object) -> None: + # Suppress request logging to keep terminal clean + pass + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate and serve eval review") + parser.add_argument("workspace", type=Path, help="Path to workspace directory") + parser.add_argument("--port", "-p", type=int, default=3117, help="Server port (default: 3117)") + parser.add_argument("--skill-name", "-n", type=str, default=None, help="Skill name for header") + parser.add_argument( + "--previous-workspace", type=Path, default=None, + help="Path to previous iteration's workspace (shows old outputs and feedback as context)", + ) + parser.add_argument( + "--benchmark", type=Path, default=None, + help="Path to benchmark.json to show in the Benchmark tab", + ) + parser.add_argument( + "--static", "-s", type=Path, default=None, + help="Write standalone HTML to this path instead of starting a server", + ) + args = parser.parse_args() + + workspace = args.workspace.resolve() + if not workspace.is_dir(): + print(f"Error: {workspace} is not a directory", file=sys.stderr) + sys.exit(1) + + runs = find_runs(workspace) + if not runs: + print(f"No runs found in {workspace}", file=sys.stderr) + sys.exit(1) + + skill_name = args.skill_name or workspace.name.replace("-workspace", "") + feedback_path = workspace / "feedback.json" + + previous: dict[str, dict] = {} + if args.previous_workspace: + previous = load_previous_iteration(args.previous_workspace.resolve()) + + benchmark_path = args.benchmark.resolve() if args.benchmark else None + benchmark = None + if benchmark_path and benchmark_path.exists(): + try: + benchmark = json.loads(benchmark_path.read_text()) + except (json.JSONDecodeError, OSError): + pass + + if args.static: + html = generate_html(runs, skill_name, previous, benchmark) + args.static.parent.mkdir(parents=True, exist_ok=True) + args.static.write_text(html) + print(f"\n Static viewer written to: {args.static}\n") + sys.exit(0) + + # Kill any existing process on the target port + port = args.port + _kill_port(port) + handler = partial(ReviewHandler, workspace, skill_name, feedback_path, previous, benchmark_path) + try: + server = HTTPServer(("127.0.0.1", port), handler) + except OSError: + # Port still in use after kill attempt — find a free one + server = HTTPServer(("127.0.0.1", 0), handler) + port = server.server_address[1] + + url = f"http://localhost:{port}" + print(f"\n Eval Viewer") + print(f" ─────────────────────────────────") + print(f" URL: {url}") + print(f" Workspace: {workspace}") + print(f" Feedback: {feedback_path}") + if previous: + print(f" Previous: {args.previous_workspace} ({len(previous)} runs)") + if benchmark_path: + print(f" Benchmark: {benchmark_path}") + print(f"\n Press Ctrl+C to stop.\n") + + webbrowser.open(url) + + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nStopped.") + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/skills/skill-creator/eval-viewer/viewer.html b/skills/skill-creator/eval-viewer/viewer.html new file mode 100755 index 0000000..246086f --- /dev/null +++ b/skills/skill-creator/eval-viewer/viewer.html @@ -0,0 +1,1325 @@ + + + + + + Eval Review + + + + + + + +
+
+
+

Eval Review:

+
Review each output and leave feedback below. Navigate with arrow keys or buttons. When done, copy feedback and paste into GLM Code.
+
+
+
+ + + + + +
+
+ +
+
Prompt
+
+
+
+
+ + +
+
Output
+
+
No output files found
+
+
+ + + + + + + + +
+
Your Feedback
+
+ + + +
+
+
+ + +
+ + +
+
+
No benchmark data available. Run a benchmark to see quantitative results here.
+
+
+
+ + +
+
+

Review Complete

+

Your feedback has been saved. Go back to your GLM Code session and tell GLM you're done reviewing.

+
+ +
+
+
+ + +
+ + + + diff --git a/skills/skill-creator/references/schemas.md b/skills/skill-creator/references/schemas.md new file mode 100755 index 0000000..c81589b --- /dev/null +++ b/skills/skill-creator/references/schemas.md @@ -0,0 +1,430 @@ +# JSON Schemas + +This document defines the JSON schemas used by skill-creator. + +--- + +## evals.json + +Defines the evals for a skill. Located at `evals/evals.json` within the skill directory. + +```json +{ + "skill_name": "example-skill", + "evals": [ + { + "id": 1, + "prompt": "User's example prompt", + "expected_output": "Description of expected result", + "files": ["evals/files/sample1.pdf"], + "expectations": [ + "The output includes X", + "The skill used script Y" + ] + } + ] +} +``` + +**Fields:** +- `skill_name`: Name matching the skill's frontmatter +- `evals[].id`: Unique integer identifier +- `evals[].prompt`: The task to execute +- `evals[].expected_output`: Human-readable description of success +- `evals[].files`: Optional list of input file paths (relative to skill root) +- `evals[].expectations`: List of verifiable statements + +--- + +## history.json + +Tracks version progression in Improve mode. Located at workspace root. + +```json +{ + "started_at": "2026-01-15T10:30:00Z", + "skill_name": "pdf", + "current_best": "v2", + "iterations": [ + { + "version": "v0", + "parent": null, + "expectation_pass_rate": 0.65, + "grading_result": "baseline", + "is_current_best": false + }, + { + "version": "v1", + "parent": "v0", + "expectation_pass_rate": 0.75, + "grading_result": "won", + "is_current_best": false + }, + { + "version": "v2", + "parent": "v1", + "expectation_pass_rate": 0.85, + "grading_result": "won", + "is_current_best": true + } + ] +} +``` + +**Fields:** +- `started_at`: ISO timestamp of when improvement started +- `skill_name`: Name of the skill being improved +- `current_best`: Version identifier of the best performer +- `iterations[].version`: Version identifier (v0, v1, ...) +- `iterations[].parent`: Parent version this was derived from +- `iterations[].expectation_pass_rate`: Pass rate from grading +- `iterations[].grading_result`: "baseline", "won", "lost", or "tie" +- `iterations[].is_current_best`: Whether this is the current best version + +--- + +## grading.json + +Output from the grader agent. Located at `/grading.json`. + +```json +{ + "expectations": [ + { + "text": "The output includes the name 'John Smith'", + "passed": true, + "evidence": "Found in transcript Step 3: 'Extracted names: John Smith, Sarah Johnson'" + }, + { + "text": "The spreadsheet has a SUM formula in cell B10", + "passed": false, + "evidence": "No spreadsheet was created. The output was a text file." + } + ], + "summary": { + "passed": 2, + "failed": 1, + "total": 3, + "pass_rate": 0.67 + }, + "execution_metrics": { + "tool_calls": { + "Read": 5, + "Write": 2, + "Bash": 8 + }, + "total_tool_calls": 15, + "total_steps": 6, + "errors_encountered": 0, + "output_chars": 12450, + "transcript_chars": 3200 + }, + "timing": { + "executor_duration_seconds": 165.0, + "grader_duration_seconds": 26.0, + "total_duration_seconds": 191.0 + }, + "claims": [ + { + "claim": "The form has 12 fillable fields", + "type": "factual", + "verified": true, + "evidence": "Counted 12 fields in field_info.json" + } + ], + "user_notes_summary": { + "uncertainties": ["Used 2023 data, may be stale"], + "needs_review": [], + "workarounds": ["Fell back to text overlay for non-fillable fields"] + }, + "eval_feedback": { + "suggestions": [ + { + "assertion": "The output includes the name 'John Smith'", + "reason": "A hallucinated document that mentions the name would also pass" + } + ], + "overall": "Assertions check presence but not correctness." + } +} +``` + +**Fields:** +- `expectations[]`: Graded expectations with evidence +- `summary`: Aggregate pass/fail counts +- `execution_metrics`: Tool usage and output size (from executor's metrics.json) +- `timing`: Wall clock timing (from timing.json) +- `claims`: Extracted and verified claims from the output +- `user_notes_summary`: Issues flagged by the executor +- `eval_feedback`: (optional) Improvement suggestions for the evals, only present when the grader identifies issues worth raising + +--- + +## metrics.json + +Output from the executor agent. Located at `/outputs/metrics.json`. + +```json +{ + "tool_calls": { + "Read": 5, + "Write": 2, + "Bash": 8, + "Edit": 1, + "Glob": 2, + "Grep": 0 + }, + "total_tool_calls": 18, + "total_steps": 6, + "files_created": ["filled_form.pdf", "field_values.json"], + "errors_encountered": 0, + "output_chars": 12450, + "transcript_chars": 3200 +} +``` + +**Fields:** +- `tool_calls`: Count per tool type +- `total_tool_calls`: Sum of all tool calls +- `total_steps`: Number of major execution steps +- `files_created`: List of output files created +- `errors_encountered`: Number of errors during execution +- `output_chars`: Total character count of output files +- `transcript_chars`: Character count of transcript + +--- + +## timing.json + +Wall clock timing for a run. Located at `/timing.json`. + +**How to capture:** When a subagent task completes, the task notification includes `total_tokens` and `duration_ms`. Save these immediately — they are not persisted anywhere else and cannot be recovered after the fact. + +```json +{ + "total_tokens": 84852, + "duration_ms": 23332, + "total_duration_seconds": 23.3, + "executor_start": "2026-01-15T10:30:00Z", + "executor_end": "2026-01-15T10:32:45Z", + "executor_duration_seconds": 165.0, + "grader_start": "2026-01-15T10:32:46Z", + "grader_end": "2026-01-15T10:33:12Z", + "grader_duration_seconds": 26.0 +} +``` + +--- + +## benchmark.json + +Output from Benchmark mode. Located at `benchmarks//benchmark.json`. + +```json +{ + "metadata": { + "skill_name": "pdf", + "skill_path": "/path/to/pdf", + "executor_model": "glm-4-plus", + "analyzer_model": "most-capable-model", + "timestamp": "2026-01-15T10:30:00Z", + "evals_run": [1, 2, 3], + "runs_per_configuration": 3 + }, + + "runs": [ + { + "eval_id": 1, + "eval_name": "Ocean", + "configuration": "with_skill", + "run_number": 1, + "result": { + "pass_rate": 0.85, + "passed": 6, + "failed": 1, + "total": 7, + "time_seconds": 42.5, + "tokens": 3800, + "tool_calls": 18, + "errors": 0 + }, + "expectations": [ + {"text": "...", "passed": true, "evidence": "..."} + ], + "notes": [ + "Used 2023 data, may be stale", + "Fell back to text overlay for non-fillable fields" + ] + } + ], + + "run_summary": { + "with_skill": { + "pass_rate": {"mean": 0.85, "stddev": 0.05, "min": 0.80, "max": 0.90}, + "time_seconds": {"mean": 45.0, "stddev": 12.0, "min": 32.0, "max": 58.0}, + "tokens": {"mean": 3800, "stddev": 400, "min": 3200, "max": 4100} + }, + "without_skill": { + "pass_rate": {"mean": 0.35, "stddev": 0.08, "min": 0.28, "max": 0.45}, + "time_seconds": {"mean": 32.0, "stddev": 8.0, "min": 24.0, "max": 42.0}, + "tokens": {"mean": 2100, "stddev": 300, "min": 1800, "max": 2500} + }, + "delta": { + "pass_rate": "+0.50", + "time_seconds": "+13.0", + "tokens": "+1700" + } + }, + + "notes": [ + "Assertion 'Output is a PDF file' passes 100% in both configurations - may not differentiate skill value", + "Eval 3 shows high variance (50% ± 40%) - may be flaky or model-dependent", + "Without-skill runs consistently fail on table extraction expectations", + "Skill adds 13s average execution time but improves pass rate by 50%" + ] +} +``` + +**Fields:** +- `metadata`: Information about the benchmark run + - `skill_name`: Name of the skill + - `timestamp`: When the benchmark was run + - `evals_run`: List of eval names or IDs + - `runs_per_configuration`: Number of runs per config (e.g. 3) +- `runs[]`: Individual run results + - `eval_id`: Numeric eval identifier + - `eval_name`: Human-readable eval name (used as section header in the viewer) + - `configuration`: Must be `"with_skill"` or `"without_skill"` (the viewer uses this exact string for grouping and color coding) + - `run_number`: Integer run number (1, 2, 3...) + - `result`: Nested object with `pass_rate`, `passed`, `total`, `time_seconds`, `tokens`, `errors` +- `run_summary`: Statistical aggregates per configuration + - `with_skill` / `without_skill`: Each contains `pass_rate`, `time_seconds`, `tokens` objects with `mean` and `stddev` fields + - `delta`: Difference strings like `"+0.50"`, `"+13.0"`, `"+1700"` +- `notes`: Freeform observations from the analyzer + +**Important:** The viewer reads these field names exactly. Using `config` instead of `configuration`, or putting `pass_rate` at the top level of a run instead of nested under `result`, will cause the viewer to show empty/zero values. Always reference this schema when generating benchmark.json manually. + +--- + +## comparison.json + +Output from blind comparator. Located at `/comparison-N.json`. + +```json +{ + "winner": "A", + "reasoning": "Output A provides a complete solution with proper formatting and all required fields. Output B is missing the date field and has formatting inconsistencies.", + "rubric": { + "A": { + "content": { + "correctness": 5, + "completeness": 5, + "accuracy": 4 + }, + "structure": { + "organization": 4, + "formatting": 5, + "usability": 4 + }, + "content_score": 4.7, + "structure_score": 4.3, + "overall_score": 9.0 + }, + "B": { + "content": { + "correctness": 3, + "completeness": 2, + "accuracy": 3 + }, + "structure": { + "organization": 3, + "formatting": 2, + "usability": 3 + }, + "content_score": 2.7, + "structure_score": 2.7, + "overall_score": 5.4 + } + }, + "output_quality": { + "A": { + "score": 9, + "strengths": ["Complete solution", "Well-formatted", "All fields present"], + "weaknesses": ["Minor style inconsistency in header"] + }, + "B": { + "score": 5, + "strengths": ["Readable output", "Correct basic structure"], + "weaknesses": ["Missing date field", "Formatting inconsistencies", "Partial data extraction"] + } + }, + "expectation_results": { + "A": { + "passed": 4, + "total": 5, + "pass_rate": 0.80, + "details": [ + {"text": "Output includes name", "passed": true} + ] + }, + "B": { + "passed": 3, + "total": 5, + "pass_rate": 0.60, + "details": [ + {"text": "Output includes name", "passed": true} + ] + } + } +} +``` + +--- + +## analysis.json + +Output from post-hoc analyzer. Located at `/analysis.json`. + +```json +{ + "comparison_summary": { + "winner": "A", + "winner_skill": "path/to/winner/skill", + "loser_skill": "path/to/loser/skill", + "comparator_reasoning": "Brief summary of why comparator chose winner" + }, + "winner_strengths": [ + "Clear step-by-step instructions for handling multi-page documents", + "Included validation script that caught formatting errors" + ], + "loser_weaknesses": [ + "Vague instruction 'process the document appropriately' led to inconsistent behavior", + "No script for validation, agent had to improvise" + ], + "instruction_following": { + "winner": { + "score": 9, + "issues": ["Minor: skipped optional logging step"] + }, + "loser": { + "score": 6, + "issues": [ + "Did not use the skill's formatting template", + "Invented own approach instead of following step 3" + ] + } + }, + "improvement_suggestions": [ + { + "priority": "high", + "category": "instructions", + "suggestion": "Replace 'process the document appropriately' with explicit steps", + "expected_impact": "Would eliminate ambiguity that caused inconsistent behavior" + } + ], + "transcript_insights": { + "winner_execution_pattern": "Read skill -> Followed 5-step process -> Used validation script", + "loser_execution_pattern": "Read skill -> Unclear on approach -> Tried 3 different methods" + } +} +``` diff --git a/skills/skill-creator/scripts/__init__.py b/skills/skill-creator/scripts/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/skills/skill-creator/scripts/aggregate_benchmark.py b/skills/skill-creator/scripts/aggregate_benchmark.py new file mode 100755 index 0000000..3e66e8c --- /dev/null +++ b/skills/skill-creator/scripts/aggregate_benchmark.py @@ -0,0 +1,401 @@ +#!/usr/bin/env python3 +""" +Aggregate individual run results into benchmark summary statistics. + +Reads grading.json files from run directories and produces: +- run_summary with mean, stddev, min, max for each metric +- delta between with_skill and without_skill configurations + +Usage: + python aggregate_benchmark.py + +Example: + python aggregate_benchmark.py benchmarks/2026-01-15T10-30-00/ + +The script supports two directory layouts: + + Workspace layout (from skill-creator iterations): + / + └── eval-N/ + ├── with_skill/ + │ ├── run-1/grading.json + │ └── run-2/grading.json + └── without_skill/ + ├── run-1/grading.json + └── run-2/grading.json + + Legacy layout (with runs/ subdirectory): + / + └── runs/ + └── eval-N/ + ├── with_skill/ + │ └── run-1/grading.json + └── without_skill/ + └── run-1/grading.json +""" + +import argparse +import json +import math +import sys +from datetime import datetime, timezone +from pathlib import Path + + +def calculate_stats(values: list[float]) -> dict: + """Calculate mean, stddev, min, max for a list of values.""" + if not values: + return {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0} + + n = len(values) + mean = sum(values) / n + + if n > 1: + variance = sum((x - mean) ** 2 for x in values) / (n - 1) + stddev = math.sqrt(variance) + else: + stddev = 0.0 + + return { + "mean": round(mean, 4), + "stddev": round(stddev, 4), + "min": round(min(values), 4), + "max": round(max(values), 4) + } + + +def load_run_results(benchmark_dir: Path) -> dict: + """ + Load all run results from a benchmark directory. + + Returns dict keyed by config name (e.g. "with_skill"/"without_skill", + or "new_skill"/"old_skill"), each containing a list of run results. + """ + # Support both layouts: eval dirs directly under benchmark_dir, or under runs/ + runs_dir = benchmark_dir / "runs" + if runs_dir.exists(): + search_dir = runs_dir + elif list(benchmark_dir.glob("eval-*")): + search_dir = benchmark_dir + else: + print(f"No eval directories found in {benchmark_dir} or {benchmark_dir / 'runs'}") + return {} + + results: dict[str, list] = {} + + for eval_idx, eval_dir in enumerate(sorted(search_dir.glob("eval-*"))): + metadata_path = eval_dir / "eval_metadata.json" + if metadata_path.exists(): + try: + with open(metadata_path) as mf: + eval_id = json.load(mf).get("eval_id", eval_idx) + except (json.JSONDecodeError, OSError): + eval_id = eval_idx + else: + try: + eval_id = int(eval_dir.name.split("-")[1]) + except ValueError: + eval_id = eval_idx + + # Discover config directories dynamically rather than hardcoding names + for config_dir in sorted(eval_dir.iterdir()): + if not config_dir.is_dir(): + continue + # Skip non-config directories (inputs, outputs, etc.) + if not list(config_dir.glob("run-*")): + continue + config = config_dir.name + if config not in results: + results[config] = [] + + for run_dir in sorted(config_dir.glob("run-*")): + run_number = int(run_dir.name.split("-")[1]) + grading_file = run_dir / "grading.json" + + if not grading_file.exists(): + print(f"Warning: grading.json not found in {run_dir}") + continue + + try: + with open(grading_file) as f: + grading = json.load(f) + except json.JSONDecodeError as e: + print(f"Warning: Invalid JSON in {grading_file}: {e}") + continue + + # Extract metrics + result = { + "eval_id": eval_id, + "run_number": run_number, + "pass_rate": grading.get("summary", {}).get("pass_rate", 0.0), + "passed": grading.get("summary", {}).get("passed", 0), + "failed": grading.get("summary", {}).get("failed", 0), + "total": grading.get("summary", {}).get("total", 0), + } + + # Extract timing — check grading.json first, then sibling timing.json + timing = grading.get("timing", {}) + result["time_seconds"] = timing.get("total_duration_seconds", 0.0) + timing_file = run_dir / "timing.json" + if result["time_seconds"] == 0.0 and timing_file.exists(): + try: + with open(timing_file) as tf: + timing_data = json.load(tf) + result["time_seconds"] = timing_data.get("total_duration_seconds", 0.0) + result["tokens"] = timing_data.get("total_tokens", 0) + except json.JSONDecodeError: + pass + + # Extract metrics if available + metrics = grading.get("execution_metrics", {}) + result["tool_calls"] = metrics.get("total_tool_calls", 0) + if not result.get("tokens"): + result["tokens"] = metrics.get("output_chars", 0) + result["errors"] = metrics.get("errors_encountered", 0) + + # Extract expectations — viewer requires fields: text, passed, evidence + raw_expectations = grading.get("expectations", []) + for exp in raw_expectations: + if "text" not in exp or "passed" not in exp: + print(f"Warning: expectation in {grading_file} missing required fields (text, passed, evidence): {exp}") + result["expectations"] = raw_expectations + + # Extract notes from user_notes_summary + notes_summary = grading.get("user_notes_summary", {}) + notes = [] + notes.extend(notes_summary.get("uncertainties", [])) + notes.extend(notes_summary.get("needs_review", [])) + notes.extend(notes_summary.get("workarounds", [])) + result["notes"] = notes + + results[config].append(result) + + return results + + +def aggregate_results(results: dict) -> dict: + """ + Aggregate run results into summary statistics. + + Returns run_summary with stats for each configuration and delta. + """ + run_summary = {} + configs = list(results.keys()) + + for config in configs: + runs = results.get(config, []) + + if not runs: + run_summary[config] = { + "pass_rate": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}, + "time_seconds": {"mean": 0.0, "stddev": 0.0, "min": 0.0, "max": 0.0}, + "tokens": {"mean": 0, "stddev": 0, "min": 0, "max": 0} + } + continue + + pass_rates = [r["pass_rate"] for r in runs] + times = [r["time_seconds"] for r in runs] + tokens = [r.get("tokens", 0) for r in runs] + + run_summary[config] = { + "pass_rate": calculate_stats(pass_rates), + "time_seconds": calculate_stats(times), + "tokens": calculate_stats(tokens) + } + + # Calculate delta between the first two configs (if two exist) + if len(configs) >= 2: + primary = run_summary.get(configs[0], {}) + baseline = run_summary.get(configs[1], {}) + else: + primary = run_summary.get(configs[0], {}) if configs else {} + baseline = {} + + delta_pass_rate = primary.get("pass_rate", {}).get("mean", 0) - baseline.get("pass_rate", {}).get("mean", 0) + delta_time = primary.get("time_seconds", {}).get("mean", 0) - baseline.get("time_seconds", {}).get("mean", 0) + delta_tokens = primary.get("tokens", {}).get("mean", 0) - baseline.get("tokens", {}).get("mean", 0) + + run_summary["delta"] = { + "pass_rate": f"{delta_pass_rate:+.2f}", + "time_seconds": f"{delta_time:+.1f}", + "tokens": f"{delta_tokens:+.0f}" + } + + return run_summary + + +def generate_benchmark(benchmark_dir: Path, skill_name: str = "", skill_path: str = "") -> dict: + """ + Generate complete benchmark.json from run results. + """ + results = load_run_results(benchmark_dir) + run_summary = aggregate_results(results) + + # Build runs array for benchmark.json + runs = [] + for config in results: + for result in results[config]: + runs.append({ + "eval_id": result["eval_id"], + "configuration": config, + "run_number": result["run_number"], + "result": { + "pass_rate": result["pass_rate"], + "passed": result["passed"], + "failed": result["failed"], + "total": result["total"], + "time_seconds": result["time_seconds"], + "tokens": result.get("tokens", 0), + "tool_calls": result.get("tool_calls", 0), + "errors": result.get("errors", 0) + }, + "expectations": result["expectations"], + "notes": result["notes"] + }) + + # Determine eval IDs from results + eval_ids = sorted(set( + r["eval_id"] + for config in results.values() + for r in config + )) + + benchmark = { + "metadata": { + "skill_name": skill_name or "", + "skill_path": skill_path or "", + "executor_model": "", + "analyzer_model": "", + "timestamp": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "evals_run": eval_ids, + "runs_per_configuration": 3 + }, + "runs": runs, + "run_summary": run_summary, + "notes": [] # To be filled by analyzer + } + + return benchmark + + +def generate_markdown(benchmark: dict) -> str: + """Generate human-readable benchmark.md from benchmark data.""" + metadata = benchmark["metadata"] + run_summary = benchmark["run_summary"] + + # Determine config names (excluding "delta") + configs = [k for k in run_summary if k != "delta"] + config_a = configs[0] if len(configs) >= 1 else "config_a" + config_b = configs[1] if len(configs) >= 2 else "config_b" + label_a = config_a.replace("_", " ").title() + label_b = config_b.replace("_", " ").title() + + lines = [ + f"# Skill Benchmark: {metadata['skill_name']}", + "", + f"**Model**: {metadata['executor_model']}", + f"**Date**: {metadata['timestamp']}", + f"**Evals**: {', '.join(map(str, metadata['evals_run']))} ({metadata['runs_per_configuration']} runs each per configuration)", + "", + "## Summary", + "", + f"| Metric | {label_a} | {label_b} | Delta |", + "|--------|------------|---------------|-------|", + ] + + a_summary = run_summary.get(config_a, {}) + b_summary = run_summary.get(config_b, {}) + delta = run_summary.get("delta", {}) + + # Format pass rate + a_pr = a_summary.get("pass_rate", {}) + b_pr = b_summary.get("pass_rate", {}) + lines.append(f"| Pass Rate | {a_pr.get('mean', 0)*100:.0f}% ± {a_pr.get('stddev', 0)*100:.0f}% | {b_pr.get('mean', 0)*100:.0f}% ± {b_pr.get('stddev', 0)*100:.0f}% | {delta.get('pass_rate', '—')} |") + + # Format time + a_time = a_summary.get("time_seconds", {}) + b_time = b_summary.get("time_seconds", {}) + lines.append(f"| Time | {a_time.get('mean', 0):.1f}s ± {a_time.get('stddev', 0):.1f}s | {b_time.get('mean', 0):.1f}s ± {b_time.get('stddev', 0):.1f}s | {delta.get('time_seconds', '—')}s |") + + # Format tokens + a_tokens = a_summary.get("tokens", {}) + b_tokens = b_summary.get("tokens", {}) + lines.append(f"| Tokens | {a_tokens.get('mean', 0):.0f} ± {a_tokens.get('stddev', 0):.0f} | {b_tokens.get('mean', 0):.0f} ± {b_tokens.get('stddev', 0):.0f} | {delta.get('tokens', '—')} |") + + # Notes section + if benchmark.get("notes"): + lines.extend([ + "", + "## Notes", + "" + ]) + for note in benchmark["notes"]: + lines.append(f"- {note}") + + return "\n".join(lines) + + +def main(): + parser = argparse.ArgumentParser( + description="Aggregate benchmark run results into summary statistics" + ) + parser.add_argument( + "benchmark_dir", + type=Path, + help="Path to the benchmark directory" + ) + parser.add_argument( + "--skill-name", + default="", + help="Name of the skill being benchmarked" + ) + parser.add_argument( + "--skill-path", + default="", + help="Path to the skill being benchmarked" + ) + parser.add_argument( + "--output", "-o", + type=Path, + help="Output path for benchmark.json (default: /benchmark.json)" + ) + + args = parser.parse_args() + + if not args.benchmark_dir.exists(): + print(f"Directory not found: {args.benchmark_dir}") + sys.exit(1) + + # Generate benchmark + benchmark = generate_benchmark(args.benchmark_dir, args.skill_name, args.skill_path) + + # Determine output paths + output_json = args.output or (args.benchmark_dir / "benchmark.json") + output_md = output_json.with_suffix(".md") + + # Write benchmark.json + with open(output_json, "w") as f: + json.dump(benchmark, f, indent=2) + print(f"Generated: {output_json}") + + # Write benchmark.md + markdown = generate_markdown(benchmark) + with open(output_md, "w") as f: + f.write(markdown) + print(f"Generated: {output_md}") + + # Print summary + run_summary = benchmark["run_summary"] + configs = [k for k in run_summary if k != "delta"] + delta = run_summary.get("delta", {}) + + print(f"\nSummary:") + for config in configs: + pr = run_summary[config]["pass_rate"]["mean"] + label = config.replace("_", " ").title() + print(f" {label}: {pr*100:.1f}% pass rate") + print(f" Delta: {delta.get('pass_rate', '—')}") + + +if __name__ == "__main__": + main() diff --git a/skills/skill-creator/scripts/generate_report.py b/skills/skill-creator/scripts/generate_report.py new file mode 100755 index 0000000..5393ed0 --- /dev/null +++ b/skills/skill-creator/scripts/generate_report.py @@ -0,0 +1,326 @@ +#!/usr/bin/env python3 +"""Generate an HTML report from run_loop.py output. + +Takes the JSON output from run_loop.py and generates a visual HTML report +showing each description attempt with check/x for each test case. +Distinguishes between train and test queries. +""" + +import argparse +import html +import json +import sys +from pathlib import Path + + +def generate_html(data: dict, auto_refresh: bool = False, skill_name: str = "") -> str: + """Generate HTML report from loop output data. If auto_refresh is True, adds a meta refresh tag.""" + history = data.get("history", []) + holdout = data.get("holdout", 0) + title_prefix = html.escape(skill_name + " \u2014 ") if skill_name else "" + + # Get all unique queries from train and test sets, with should_trigger info + train_queries: list[dict] = [] + test_queries: list[dict] = [] + if history: + for r in history[0].get("train_results", history[0].get("results", [])): + train_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)}) + if history[0].get("test_results"): + for r in history[0].get("test_results", []): + test_queries.append({"query": r["query"], "should_trigger": r.get("should_trigger", True)}) + + refresh_tag = ' \n' if auto_refresh else "" + + html_parts = [""" + + + +""" + refresh_tag + """ """ + title_prefix + """Skill Description Optimization + + + + + + +

""" + title_prefix + """Skill Description Optimization

+
+ Optimizing your skill's description. This page updates automatically as GLM tests different versions of your skill's description. Each row is an iteration — a new description attempt. The columns show test queries: green checkmarks mean the skill triggered correctly (or correctly didn't trigger), red crosses mean it got it wrong. The "Train" score shows performance on queries used to improve the description; the "Test" score shows performance on held-out queries the optimizer hasn't seen. When it's done, GLM will apply the best-performing description to your skill. +
+"""] + + # Summary section + best_test_score = data.get('best_test_score') + best_train_score = data.get('best_train_score') + html_parts.append(f""" +
+

Original: {html.escape(data.get('original_description', 'N/A'))}

+

Best: {html.escape(data.get('best_description', 'N/A'))}

+

Best Score: {data.get('best_score', 'N/A')} {'(test)' if best_test_score else '(train)'}

+

Iterations: {data.get('iterations_run', 0)} | Train: {data.get('train_size', '?')} | Test: {data.get('test_size', '?')}

+
+""") + + # Legend + html_parts.append(""" +
+ Query columns: + Should trigger + Should NOT trigger + Train + Test +
+""") + + # Table header + html_parts.append(""" +
+ + + + + + + +""") + + # Add column headers for train queries + for qinfo in train_queries: + polarity = "positive-col" if qinfo["should_trigger"] else "negative-col" + html_parts.append(f' \n') + + # Add column headers for test queries (different color) + for qinfo in test_queries: + polarity = "positive-col" if qinfo["should_trigger"] else "negative-col" + html_parts.append(f' \n') + + html_parts.append(""" + + +""") + + # Find best iteration for highlighting + if test_queries: + best_iter = max(history, key=lambda h: h.get("test_passed") or 0).get("iteration") + else: + best_iter = max(history, key=lambda h: h.get("train_passed", h.get("passed", 0))).get("iteration") + + # Add rows for each iteration + for h in history: + iteration = h.get("iteration", "?") + train_passed = h.get("train_passed", h.get("passed", 0)) + train_total = h.get("train_total", h.get("total", 0)) + test_passed = h.get("test_passed") + test_total = h.get("test_total") + description = h.get("description", "") + train_results = h.get("train_results", h.get("results", [])) + test_results = h.get("test_results", []) + + # Create lookups for results by query + train_by_query = {r["query"]: r for r in train_results} + test_by_query = {r["query"]: r for r in test_results} if test_results else {} + + # Compute aggregate correct/total runs across all retries + def aggregate_runs(results: list[dict]) -> tuple[int, int]: + correct = 0 + total = 0 + for r in results: + runs = r.get("runs", 0) + triggers = r.get("triggers", 0) + total += runs + if r.get("should_trigger", True): + correct += triggers + else: + correct += runs - triggers + return correct, total + + train_correct, train_runs = aggregate_runs(train_results) + test_correct, test_runs = aggregate_runs(test_results) + + # Determine score classes + def score_class(correct: int, total: int) -> str: + if total > 0: + ratio = correct / total + if ratio >= 0.8: + return "score-good" + elif ratio >= 0.5: + return "score-ok" + return "score-bad" + + train_class = score_class(train_correct, train_runs) + test_class = score_class(test_correct, test_runs) + + row_class = "best-row" if iteration == best_iter else "" + + html_parts.append(f""" + + + + +""") + + # Add result for each train query + for qinfo in train_queries: + r = train_by_query.get(qinfo["query"], {}) + did_pass = r.get("pass", False) + triggers = r.get("triggers", 0) + runs = r.get("runs", 0) + + icon = "✓" if did_pass else "✗" + css_class = "pass" if did_pass else "fail" + + html_parts.append(f' \n') + + # Add result for each test query (with different background) + for qinfo in test_queries: + r = test_by_query.get(qinfo["query"], {}) + did_pass = r.get("pass", False) + triggers = r.get("triggers", 0) + runs = r.get("runs", 0) + + icon = "✓" if did_pass else "✗" + css_class = "pass" if did_pass else "fail" + + html_parts.append(f' \n') + + html_parts.append(" \n") + + html_parts.append(""" +
IterTrainTestDescription{html.escape(qinfo["query"])}{html.escape(qinfo["query"])}
{iteration}{train_correct}/{train_runs}{test_correct}/{test_runs}{html.escape(description)}{icon}{triggers}/{runs}{icon}{triggers}/{runs}
+
+""") + + html_parts.append(""" + + +""") + + return "".join(html_parts) + + +def main(): + parser = argparse.ArgumentParser(description="Generate HTML report from run_loop output") + parser.add_argument("input", help="Path to JSON output from run_loop.py (or - for stdin)") + parser.add_argument("-o", "--output", default=None, help="Output HTML file (default: stdout)") + parser.add_argument("--skill-name", default="", help="Skill name to include in the report title") + args = parser.parse_args() + + if args.input == "-": + data = json.load(sys.stdin) + else: + data = json.loads(Path(args.input).read_text()) + + html_output = generate_html(data, skill_name=args.skill_name) + + if args.output: + Path(args.output).write_text(html_output) + print(f"Report written to {args.output}", file=sys.stderr) + else: + print(html_output) + + +if __name__ == "__main__": + main() diff --git a/skills/skill-creator/scripts/improve_description.py b/skills/skill-creator/scripts/improve_description.py new file mode 100755 index 0000000..210e3d5 --- /dev/null +++ b/skills/skill-creator/scripts/improve_description.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Improve a skill description based on eval results. + +Takes eval results (from run_eval.py) and generates an improved description +by calling `z-ai chat -p` as a subprocess. +""" + +import argparse +import json +import os +import re +import subprocess +import sys +from pathlib import Path + +from scripts.utils import parse_skill_md + + +def _call_zai(prompt: str, timeout: int = 300) -> str: + """Run `z-ai chat -p` with the prompt and return the text response.""" + cmd = ["z-ai", "chat", "-p", prompt] + + env = {k: v for k, v in os.environ.items() if k != "GLMCODE"} + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + env=env, + timeout=timeout, + ) + if result.returncode != 0: + raise RuntimeError( + f"z-ai chat exited {result.returncode}\nstderr: {result.stderr}" + ) + return result.stdout + + +def improve_description( + skill_name: str, + skill_content: str, + current_description: str, + eval_results: dict, + history: list[dict], + model: str, + test_results: dict | None = None, + log_dir: Path | None = None, + iteration: int | None = None, +) -> str: + """Call z-ai to improve the description based on eval results.""" + failed_triggers = [ + r for r in eval_results["results"] + if r["should_trigger"] and not r["pass"] + ] + false_triggers = [ + r for r in eval_results["results"] + if not r["should_trigger"] and not r["pass"] + ] + + # Build scores summary + train_score = f"{eval_results['summary']['passed']}/{eval_results['summary']['total']}" + if test_results: + test_score = f"{test_results['summary']['passed']}/{test_results['summary']['total']}" + scores_summary = f"Train: {train_score}, Test: {test_score}" + else: + scores_summary = f"Train: {train_score}" + + prompt = f"""You are optimizing a skill description for a GLM Code skill called "{skill_name}". A "skill" is sort of like a prompt, but with progressive disclosure -- there's a title and description that GLM sees when deciding whether to use the skill, and then if it does use the skill, it reads the .md file which has lots more details and potentially links to other resources in the skill folder like helper files and scripts and additional documentation or examples. + +The description appears in GLM's "available_skills" list. When a user sends a query, GLM decides whether to invoke the skill based solely on the title and on this description. Your goal is to write a description that triggers for relevant queries, and doesn't trigger for irrelevant ones. + +Here's the current description: + +"{current_description}" + + +Current scores ({scores_summary}): + +""" + if failed_triggers: + prompt += "FAILED TO TRIGGER (should have triggered but didn't):\n" + for r in failed_triggers: + prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n' + prompt += "\n" + + if false_triggers: + prompt += "FALSE TRIGGERS (triggered but shouldn't have):\n" + for r in false_triggers: + prompt += f' - "{r["query"]}" (triggered {r["triggers"]}/{r["runs"]} times)\n' + prompt += "\n" + + if history: + prompt += "PREVIOUS ATTEMPTS (do NOT repeat these — try something structurally different):\n\n" + for h in history: + train_s = f"{h.get('train_passed', h.get('passed', 0))}/{h.get('train_total', h.get('total', 0))}" + test_s = f"{h.get('test_passed', '?')}/{h.get('test_total', '?')}" if h.get('test_passed') is not None else None + score_str = f"train={train_s}" + (f", test={test_s}" if test_s else "") + prompt += f'\n' + prompt += f'Description: "{h["description"]}"\n' + if "results" in h: + prompt += "Train results:\n" + for r in h["results"]: + status = "PASS" if r["pass"] else "FAIL" + prompt += f' [{status}] "{r["query"][:80]}" (triggered {r["triggers"]}/{r["runs"]})\n' + if h.get("note"): + prompt += f'Note: {h["note"]}\n' + prompt += "\n\n" + + prompt += f""" + +Skill content (for context on what the skill does): + +{skill_content} + + +Based on the failures, write a new and improved description that is more likely to trigger correctly. When I say "based on the failures", it's a bit of a tricky line to walk because we don't want to overfit to the specific cases you're seeing. So what I DON'T want you to do is produce an ever-expanding list of specific queries that this skill should or shouldn't trigger for. Instead, try to generalize from the failures to broader categories of user intent and situations where this skill would be useful or not useful. The reason for this is twofold: + +1. Avoid overfitting +2. The list might get loooong and it's injected into ALL queries and there might be a lot of skills, so we don't want to blow too much space on any given description. + +Concretely, your description should not be more than about 100-200 words, even if that comes at the cost of accuracy. There is a hard limit of 1024 characters — descriptions over that will be truncated, so stay comfortably under it. + +Here are some tips that we've found to work well in writing these descriptions: +- The skill should be phrased in the imperative -- "Use this skill for" rather than "this skill does" +- The skill description should focus on the user's intent, what they are trying to achieve, vs. the implementation details of how the skill works. +- The description competes with other skills for GLM's attention — make it distinctive and immediately recognizable. +- If you're getting lots of failures after repeated attempts, change things up. Try different sentence structures or wordings. + +I'd encourage you to be creative and mix up the style in different iterations since you'll have multiple opportunities to try different approaches and we'll just grab the highest-scoring one at the end. + +Please respond with only the new description text in tags, nothing else.""" + + text = _call_zai(prompt) + + match = re.search(r"(.*?)", text, re.DOTALL) + description = match.group(1).strip().strip('"') if match else text.strip().strip('"') + + transcript: dict = { + "iteration": iteration, + "prompt": prompt, + "response": text, + "parsed_description": description, + "char_count": len(description), + "over_limit": len(description) > 1024, + } + + # Safety net: the prompt already states the 1024-char hard limit, but if + # the model blew past it anyway, make one fresh single-turn call that + # quotes the too-long version and asks for a shorter rewrite. (The old + # SDK path did this as a true multi-turn; `glm -p` is one-shot, so we + # inline the prior output into the new prompt instead.) + if len(description) > 1024: + shorten_prompt = ( + f"{prompt}\n\n" + f"---\n\n" + f"A previous attempt produced this description, which at " + f"{len(description)} characters is over the 1024-character hard limit:\n\n" + f'"{description}"\n\n' + f"Rewrite it to be under 1024 characters while keeping the most " + f"important trigger words and intent coverage. Respond with only " + f"the new description in tags." + ) + shorten_text = _call_zai(shorten_prompt) + match = re.search(r"(.*?)", shorten_text, re.DOTALL) + shortened = match.group(1).strip().strip('"') if match else shorten_text.strip().strip('"') + + transcript["rewrite_prompt"] = shorten_prompt + transcript["rewrite_response"] = shorten_text + transcript["rewrite_description"] = shortened + transcript["rewrite_char_count"] = len(shortened) + description = shortened + + transcript["final_description"] = description + + if log_dir: + log_dir.mkdir(parents=True, exist_ok=True) + log_file = log_dir / f"improve_iter_{iteration or 'unknown'}.json" + log_file.write_text(json.dumps(transcript, indent=2)) + + return description + + +def main(): + parser = argparse.ArgumentParser(description="Improve a skill description based on eval results") + parser.add_argument("--eval-results", required=True, help="Path to eval results JSON (from run_eval.py)") + parser.add_argument("--skill-path", required=True, help="Path to skill directory") + parser.add_argument("--history", default=None, help="Path to history JSON (previous attempts)") + parser.add_argument("--model", required=True, help="Model for improvement") + parser.add_argument("--verbose", action="store_true", help="Print thinking to stderr") + args = parser.parse_args() + + skill_path = Path(args.skill_path) + if not (skill_path / "SKILL.md").exists(): + print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) + sys.exit(1) + + eval_results = json.loads(Path(args.eval_results).read_text()) + history = [] + if args.history: + history = json.loads(Path(args.history).read_text()) + + name, _, content = parse_skill_md(skill_path) + current_description = eval_results["description"] + + if args.verbose: + print(f"Current: {current_description}", file=sys.stderr) + print(f"Score: {eval_results['summary']['passed']}/{eval_results['summary']['total']}", file=sys.stderr) + + new_description = improve_description( + skill_name=name, + skill_content=content, + current_description=current_description, + eval_results=eval_results, + history=history, + model=args.model, + ) + + if args.verbose: + print(f"Improved: {new_description}", file=sys.stderr) + + # Output as JSON with both the new description and updated history + output = { + "description": new_description, + "history": history + [{ + "description": current_description, + "passed": eval_results["summary"]["passed"], + "failed": eval_results["summary"]["failed"], + "total": eval_results["summary"]["total"], + "results": eval_results["results"], + }], + } + print(json.dumps(output, indent=2)) + + +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 0000000..f48eac4 --- /dev/null +++ b/skills/skill-creator/scripts/package_skill.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Skill Packager - Creates a distributable .skill file of a skill folder + +Usage: + python utils/package_skill.py [output-directory] + +Example: + python utils/package_skill.py skills/public/my-skill + python utils/package_skill.py skills/public/my-skill ./dist +""" + +import fnmatch +import sys +import zipfile +from pathlib import Path +from scripts.quick_validate import validate_skill + +# Patterns to exclude when packaging skills. +EXCLUDE_DIRS = {"__pycache__", "node_modules"} +EXCLUDE_GLOBS = {"*.pyc"} +EXCLUDE_FILES = {".DS_Store"} +# Directories excluded only at the skill root (not when nested deeper). +ROOT_EXCLUDE_DIRS = {"evals"} + + +def should_exclude(rel_path: Path) -> bool: + """Check if a path should be excluded from packaging.""" + parts = rel_path.parts + if any(part in EXCLUDE_DIRS for part in parts): + return True + # rel_path is relative to skill_path.parent, so parts[0] is the skill + # folder name and parts[1] (if present) is the first subdir. + if len(parts) > 1 and parts[1] in ROOT_EXCLUDE_DIRS: + return True + name = rel_path.name + if name in EXCLUDE_FILES: + return True + return any(fnmatch.fnmatch(name, pat) for pat in EXCLUDE_GLOBS) + + +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, excluding build artifacts + for file_path in skill_path.rglob('*'): + if not file_path.is_file(): + continue + arcname = file_path.relative_to(skill_path.parent) + if should_exclude(arcname): + print(f" Skipped: {arcname}") + continue + 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 [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 0000000..ed8e1dd --- /dev/null +++ b/skills/skill-creator/scripts/quick_validate.py @@ -0,0 +1,103 @@ +#!/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', 'compatibility'} + + # 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 (kebab-case: lowercase with hyphens) + if not re.match(r'^[a-z0-9-]+$', name): + return False, f"Name '{name}' should be kebab-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." + + # Validate compatibility field if present (optional) + compatibility = frontmatter.get('compatibility', '') + if compatibility: + if not isinstance(compatibility, str): + return False, f"Compatibility must be a string, got {type(compatibility).__name__}" + if len(compatibility) > 500: + return False, f"Compatibility is too long ({len(compatibility)} characters). Maximum is 500 characters." + + return True, "Skill is valid!" + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("Usage: python quick_validate.py ") + 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-creator/scripts/run_eval.py b/skills/skill-creator/scripts/run_eval.py new file mode 100755 index 0000000..c0da513 --- /dev/null +++ b/skills/skill-creator/scripts/run_eval.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python3 +"""Run trigger evaluation for a skill description. + +Tests whether a skill's description causes GLM to trigger (read the skill) +for a set of queries. Outputs results as JSON. +""" + +import argparse +import json +import os +import select +import subprocess +import sys +import time +import uuid +from concurrent.futures import ProcessPoolExecutor, as_completed +from pathlib import Path + +from scripts.utils import parse_skill_md + + +def find_project_root() -> Path: + """Find the project root by walking up from cwd looking for .glm/. + + Mimics how GLM Code discovers its project root, so the command file + we create ends up where glm -p will look for it. + """ + current = Path.cwd() + for parent in [current, *current.parents]: + if (parent / ".glm").is_dir(): + return parent + return current + + +def run_single_query( + query: str, + skill_name: str, + skill_description: str, + timeout: int, + project_root: str, + model: str | None = None, +) -> bool: + """Run a single query and return whether the skill was triggered. + + Creates a command file in .glm/commands/ so it appears in GLM's + available_skills list, then runs `glm -p` with the raw query. + Uses --include-partial-messages to detect triggering early from + stream events (content_block_start) rather than waiting for the + full assistant message, which only arrives after tool execution. + """ + unique_id = uuid.uuid4().hex[:8] + clean_name = f"{skill_name}-skill-{unique_id}" + project_commands_dir = Path(project_root) / ".glm" / "commands" + command_file = project_commands_dir / f"{clean_name}.md" + + try: + project_commands_dir.mkdir(parents=True, exist_ok=True) + # Use YAML block scalar to avoid breaking on quotes in description + indented_desc = "\n ".join(skill_description.split("\n")) + command_content = ( + f"---\n" + f"description: |\n" + f" {indented_desc}\n" + f"---\n\n" + f"# {skill_name}\n\n" + f"This skill handles: {skill_description}\n" + ) + command_file.write_text(command_content) + + cmd = [ + "glm", + "-p", query, + "--output-format", "stream-json", + "--verbose", + "--include-partial-messages", + ] + if model: + cmd.extend(["--model", model]) + + # Remove GLMCODE env var to allow nesting glm -p inside a + # GLM Code session. The guard is for interactive terminal conflicts; + # programmatic subprocess usage is safe. + env = {k: v for k, v in os.environ.items() if k != "GLMCODE"} + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + cwd=project_root, + env=env, + ) + + triggered = False + start_time = time.time() + buffer = "" + # Track state for stream event detection + pending_tool_name = None + accumulated_json = "" + + try: + while time.time() - start_time < timeout: + if process.poll() is not None: + remaining = process.stdout.read() + if remaining: + buffer += remaining.decode("utf-8", errors="replace") + break + + ready, _, _ = select.select([process.stdout], [], [], 1.0) + if not ready: + continue + + chunk = os.read(process.stdout.fileno(), 8192) + if not chunk: + break + buffer += chunk.decode("utf-8", errors="replace") + + while "\n" in buffer: + line, buffer = buffer.split("\n", 1) + line = line.strip() + if not line: + continue + + try: + event = json.loads(line) + except json.JSONDecodeError: + continue + + # Early detection via stream events + if event.get("type") == "stream_event": + se = event.get("event", {}) + se_type = se.get("type", "") + + if se_type == "content_block_start": + cb = se.get("content_block", {}) + if cb.get("type") == "tool_use": + tool_name = cb.get("name", "") + if tool_name in ("Skill", "Read"): + pending_tool_name = tool_name + accumulated_json = "" + else: + return False + + elif se_type == "content_block_delta" and pending_tool_name: + delta = se.get("delta", {}) + if delta.get("type") == "input_json_delta": + accumulated_json += delta.get("partial_json", "") + if clean_name in accumulated_json: + return True + + elif se_type in ("content_block_stop", "message_stop"): + if pending_tool_name: + return clean_name in accumulated_json + if se_type == "message_stop": + return False + + # Fallback: full assistant message + elif event.get("type") == "assistant": + message = event.get("message", {}) + for content_item in message.get("content", []): + if content_item.get("type") != "tool_use": + continue + tool_name = content_item.get("name", "") + tool_input = content_item.get("input", {}) + if tool_name == "Skill" and clean_name in tool_input.get("skill", ""): + triggered = True + elif tool_name == "Read" and clean_name in tool_input.get("file_path", ""): + triggered = True + return triggered + + elif event.get("type") == "result": + return triggered + finally: + # Clean up process on any exit path (return, exception, timeout) + if process.poll() is None: + process.kill() + process.wait() + + return triggered + finally: + if command_file.exists(): + command_file.unlink() + + +def run_eval( + eval_set: list[dict], + skill_name: str, + description: str, + num_workers: int, + timeout: int, + project_root: Path, + runs_per_query: int = 1, + trigger_threshold: float = 0.5, + model: str | None = None, +) -> dict: + """Run the full eval set and return results.""" + results = [] + + with ProcessPoolExecutor(max_workers=num_workers) as executor: + future_to_info = {} + for item in eval_set: + for run_idx in range(runs_per_query): + future = executor.submit( + run_single_query, + item["query"], + skill_name, + description, + timeout, + str(project_root), + model, + ) + future_to_info[future] = (item, run_idx) + + query_triggers: dict[str, list[bool]] = {} + query_items: dict[str, dict] = {} + for future in as_completed(future_to_info): + item, _ = future_to_info[future] + query = item["query"] + query_items[query] = item + if query not in query_triggers: + query_triggers[query] = [] + try: + query_triggers[query].append(future.result()) + except Exception as e: + print(f"Warning: query failed: {e}", file=sys.stderr) + query_triggers[query].append(False) + + for query, triggers in query_triggers.items(): + item = query_items[query] + trigger_rate = sum(triggers) / len(triggers) + should_trigger = item["should_trigger"] + if should_trigger: + did_pass = trigger_rate >= trigger_threshold + else: + did_pass = trigger_rate < trigger_threshold + results.append({ + "query": query, + "should_trigger": should_trigger, + "trigger_rate": trigger_rate, + "triggers": sum(triggers), + "runs": len(triggers), + "pass": did_pass, + }) + + passed = sum(1 for r in results if r["pass"]) + total = len(results) + + return { + "skill_name": skill_name, + "description": description, + "results": results, + "summary": { + "total": total, + "passed": passed, + "failed": total - passed, + }, + } + + +def main(): + parser = argparse.ArgumentParser(description="Run trigger evaluation for a skill description") + parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file") + parser.add_argument("--skill-path", required=True, help="Path to skill directory") + parser.add_argument("--description", default=None, help="Override description to test") + parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers") + parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds") + parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query") + parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold") + parser.add_argument("--model", default=None, help="Model to use for glm -p (default: user's configured model)") + parser.add_argument("--verbose", action="store_true", help="Print progress to stderr") + args = parser.parse_args() + + eval_set = json.loads(Path(args.eval_set).read_text()) + skill_path = Path(args.skill_path) + + if not (skill_path / "SKILL.md").exists(): + print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) + sys.exit(1) + + name, original_description, content = parse_skill_md(skill_path) + description = args.description or original_description + project_root = find_project_root() + + if args.verbose: + print(f"Evaluating: {description}", file=sys.stderr) + + output = run_eval( + eval_set=eval_set, + skill_name=name, + description=description, + num_workers=args.num_workers, + timeout=args.timeout, + project_root=project_root, + runs_per_query=args.runs_per_query, + trigger_threshold=args.trigger_threshold, + model=args.model, + ) + + if args.verbose: + summary = output["summary"] + print(f"Results: {summary['passed']}/{summary['total']} passed", file=sys.stderr) + for r in output["results"]: + status = "PASS" if r["pass"] else "FAIL" + rate_str = f"{r['triggers']}/{r['runs']}" + print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:70]}", file=sys.stderr) + + print(json.dumps(output, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/skills/skill-creator/scripts/run_loop.py b/skills/skill-creator/scripts/run_loop.py new file mode 100755 index 0000000..30a263d --- /dev/null +++ b/skills/skill-creator/scripts/run_loop.py @@ -0,0 +1,328 @@ +#!/usr/bin/env python3 +"""Run the eval + improve loop until all pass or max iterations reached. + +Combines run_eval.py and improve_description.py in a loop, tracking history +and returning the best description found. Supports train/test split to prevent +overfitting. +""" + +import argparse +import json +import random +import sys +import tempfile +import time +import webbrowser +from pathlib import Path + +from scripts.generate_report import generate_html +from scripts.improve_description import improve_description +from scripts.run_eval import find_project_root, run_eval +from scripts.utils import parse_skill_md + + +def split_eval_set(eval_set: list[dict], holdout: float, seed: int = 42) -> tuple[list[dict], list[dict]]: + """Split eval set into train and test sets, stratified by should_trigger.""" + random.seed(seed) + + # Separate by should_trigger + trigger = [e for e in eval_set if e["should_trigger"]] + no_trigger = [e for e in eval_set if not e["should_trigger"]] + + # Shuffle each group + random.shuffle(trigger) + random.shuffle(no_trigger) + + # Calculate split points + n_trigger_test = max(1, int(len(trigger) * holdout)) + n_no_trigger_test = max(1, int(len(no_trigger) * holdout)) + + # Split + test_set = trigger[:n_trigger_test] + no_trigger[:n_no_trigger_test] + train_set = trigger[n_trigger_test:] + no_trigger[n_no_trigger_test:] + + return train_set, test_set + + +def run_loop( + eval_set: list[dict], + skill_path: Path, + description_override: str | None, + num_workers: int, + timeout: int, + max_iterations: int, + runs_per_query: int, + trigger_threshold: float, + holdout: float, + model: str, + verbose: bool, + live_report_path: Path | None = None, + log_dir: Path | None = None, +) -> dict: + """Run the eval + improvement loop.""" + project_root = find_project_root() + name, original_description, content = parse_skill_md(skill_path) + current_description = description_override or original_description + + # Split into train/test if holdout > 0 + if holdout > 0: + train_set, test_set = split_eval_set(eval_set, holdout) + if verbose: + print(f"Split: {len(train_set)} train, {len(test_set)} test (holdout={holdout})", file=sys.stderr) + else: + train_set = eval_set + test_set = [] + + history = [] + exit_reason = "unknown" + + for iteration in range(1, max_iterations + 1): + if verbose: + print(f"\n{'='*60}", file=sys.stderr) + print(f"Iteration {iteration}/{max_iterations}", file=sys.stderr) + print(f"Description: {current_description}", file=sys.stderr) + print(f"{'='*60}", file=sys.stderr) + + # Evaluate train + test together in one batch for parallelism + all_queries = train_set + test_set + t0 = time.time() + all_results = run_eval( + eval_set=all_queries, + skill_name=name, + description=current_description, + num_workers=num_workers, + timeout=timeout, + project_root=project_root, + runs_per_query=runs_per_query, + trigger_threshold=trigger_threshold, + model=model, + ) + eval_elapsed = time.time() - t0 + + # Split results back into train/test by matching queries + train_queries_set = {q["query"] for q in train_set} + train_result_list = [r for r in all_results["results"] if r["query"] in train_queries_set] + test_result_list = [r for r in all_results["results"] if r["query"] not in train_queries_set] + + train_passed = sum(1 for r in train_result_list if r["pass"]) + train_total = len(train_result_list) + train_summary = {"passed": train_passed, "failed": train_total - train_passed, "total": train_total} + train_results = {"results": train_result_list, "summary": train_summary} + + if test_set: + test_passed = sum(1 for r in test_result_list if r["pass"]) + test_total = len(test_result_list) + test_summary = {"passed": test_passed, "failed": test_total - test_passed, "total": test_total} + test_results = {"results": test_result_list, "summary": test_summary} + else: + test_results = None + test_summary = None + + history.append({ + "iteration": iteration, + "description": current_description, + "train_passed": train_summary["passed"], + "train_failed": train_summary["failed"], + "train_total": train_summary["total"], + "train_results": train_results["results"], + "test_passed": test_summary["passed"] if test_summary else None, + "test_failed": test_summary["failed"] if test_summary else None, + "test_total": test_summary["total"] if test_summary else None, + "test_results": test_results["results"] if test_results else None, + # For backward compat with report generator + "passed": train_summary["passed"], + "failed": train_summary["failed"], + "total": train_summary["total"], + "results": train_results["results"], + }) + + # Write live report if path provided + if live_report_path: + partial_output = { + "original_description": original_description, + "best_description": current_description, + "best_score": "in progress", + "iterations_run": len(history), + "holdout": holdout, + "train_size": len(train_set), + "test_size": len(test_set), + "history": history, + } + live_report_path.write_text(generate_html(partial_output, auto_refresh=True, skill_name=name)) + + if verbose: + def print_eval_stats(label, results, elapsed): + pos = [r for r in results if r["should_trigger"]] + neg = [r for r in results if not r["should_trigger"]] + tp = sum(r["triggers"] for r in pos) + pos_runs = sum(r["runs"] for r in pos) + fn = pos_runs - tp + fp = sum(r["triggers"] for r in neg) + neg_runs = sum(r["runs"] for r in neg) + tn = neg_runs - fp + total = tp + tn + fp + fn + precision = tp / (tp + fp) if (tp + fp) > 0 else 1.0 + recall = tp / (tp + fn) if (tp + fn) > 0 else 1.0 + accuracy = (tp + tn) / total if total > 0 else 0.0 + print(f"{label}: {tp+tn}/{total} correct, precision={precision:.0%} recall={recall:.0%} accuracy={accuracy:.0%} ({elapsed:.1f}s)", file=sys.stderr) + for r in results: + status = "PASS" if r["pass"] else "FAIL" + rate_str = f"{r['triggers']}/{r['runs']}" + print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:60]}", file=sys.stderr) + + print_eval_stats("Train", train_results["results"], eval_elapsed) + if test_summary: + print_eval_stats("Test ", test_results["results"], 0) + + if train_summary["failed"] == 0: + exit_reason = f"all_passed (iteration {iteration})" + if verbose: + print(f"\nAll train queries passed on iteration {iteration}!", file=sys.stderr) + break + + if iteration == max_iterations: + exit_reason = f"max_iterations ({max_iterations})" + if verbose: + print(f"\nMax iterations reached ({max_iterations}).", file=sys.stderr) + break + + # Improve the description based on train results + if verbose: + print(f"\nImproving description...", file=sys.stderr) + + t0 = time.time() + # Strip test scores from history so improvement model can't see them + blinded_history = [ + {k: v for k, v in h.items() if not k.startswith("test_")} + for h in history + ] + new_description = improve_description( + skill_name=name, + skill_content=content, + current_description=current_description, + eval_results=train_results, + history=blinded_history, + model=model, + log_dir=log_dir, + iteration=iteration, + ) + improve_elapsed = time.time() - t0 + + if verbose: + print(f"Proposed ({improve_elapsed:.1f}s): {new_description}", file=sys.stderr) + + current_description = new_description + + # Find the best iteration by TEST score (or train if no test set) + if test_set: + best = max(history, key=lambda h: h["test_passed"] or 0) + best_score = f"{best['test_passed']}/{best['test_total']}" + else: + best = max(history, key=lambda h: h["train_passed"]) + best_score = f"{best['train_passed']}/{best['train_total']}" + + if verbose: + print(f"\nExit reason: {exit_reason}", file=sys.stderr) + print(f"Best score: {best_score} (iteration {best['iteration']})", file=sys.stderr) + + return { + "exit_reason": exit_reason, + "original_description": original_description, + "best_description": best["description"], + "best_score": best_score, + "best_train_score": f"{best['train_passed']}/{best['train_total']}", + "best_test_score": f"{best['test_passed']}/{best['test_total']}" if test_set else None, + "final_description": current_description, + "iterations_run": len(history), + "holdout": holdout, + "train_size": len(train_set), + "test_size": len(test_set), + "history": history, + } + + +def main(): + parser = argparse.ArgumentParser(description="Run eval + improve loop") + parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file") + parser.add_argument("--skill-path", required=True, help="Path to skill directory") + parser.add_argument("--description", default=None, help="Override starting description") + parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers") + parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds") + parser.add_argument("--max-iterations", type=int, default=5, help="Max improvement iterations") + parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query") + parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold") + parser.add_argument("--holdout", type=float, default=0.4, help="Fraction of eval set to hold out for testing (0 to disable)") + parser.add_argument("--model", required=True, help="Model for improvement") + parser.add_argument("--verbose", action="store_true", help="Print progress to stderr") + parser.add_argument("--report", default="auto", help="Generate HTML report at this path (default: 'auto' for temp file, 'none' to disable)") + parser.add_argument("--results-dir", default=None, help="Save all outputs (results.json, report.html, log.txt) to a timestamped subdirectory here") + args = parser.parse_args() + + eval_set = json.loads(Path(args.eval_set).read_text()) + skill_path = Path(args.skill_path) + + if not (skill_path / "SKILL.md").exists(): + print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr) + sys.exit(1) + + name, _, _ = parse_skill_md(skill_path) + + # Set up live report path + if args.report != "none": + if args.report == "auto": + timestamp = time.strftime("%Y%m%d_%H%M%S") + live_report_path = Path(tempfile.gettempdir()) / f"skill_description_report_{skill_path.name}_{timestamp}.html" + else: + live_report_path = Path(args.report) + # Open the report immediately so the user can watch + live_report_path.write_text("

Starting optimization loop...

") + webbrowser.open(str(live_report_path)) + else: + live_report_path = None + + # Determine output directory (create before run_loop so logs can be written) + if args.results_dir: + timestamp = time.strftime("%Y-%m-%d_%H%M%S") + results_dir = Path(args.results_dir) / timestamp + results_dir.mkdir(parents=True, exist_ok=True) + else: + results_dir = None + + log_dir = results_dir / "logs" if results_dir else None + + output = run_loop( + eval_set=eval_set, + skill_path=skill_path, + description_override=args.description, + num_workers=args.num_workers, + timeout=args.timeout, + max_iterations=args.max_iterations, + runs_per_query=args.runs_per_query, + trigger_threshold=args.trigger_threshold, + holdout=args.holdout, + model=args.model, + verbose=args.verbose, + live_report_path=live_report_path, + log_dir=log_dir, + ) + + # Save JSON output + json_output = json.dumps(output, indent=2) + print(json_output) + if results_dir: + (results_dir / "results.json").write_text(json_output) + + # Write final HTML report (without auto-refresh) + if live_report_path: + live_report_path.write_text(generate_html(output, auto_refresh=False, skill_name=name)) + print(f"\nReport: {live_report_path}", file=sys.stderr) + + if results_dir and live_report_path: + (results_dir / "report.html").write_text(generate_html(output, auto_refresh=False, skill_name=name)) + + if results_dir: + print(f"Results saved to: {results_dir}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/skills/skill-creator/scripts/utils.py b/skills/skill-creator/scripts/utils.py new file mode 100755 index 0000000..51b6a07 --- /dev/null +++ b/skills/skill-creator/scripts/utils.py @@ -0,0 +1,47 @@ +"""Shared utilities for skill-creator scripts.""" + +from pathlib import Path + + + +def parse_skill_md(skill_path: Path) -> tuple[str, str, str]: + """Parse a SKILL.md file, returning (name, description, full_content).""" + content = (skill_path / "SKILL.md").read_text() + lines = content.split("\n") + + if lines[0].strip() != "---": + raise ValueError("SKILL.md missing frontmatter (no opening ---)") + + end_idx = None + for i, line in enumerate(lines[1:], start=1): + if line.strip() == "---": + end_idx = i + break + + if end_idx is None: + raise ValueError("SKILL.md missing frontmatter (no closing ---)") + + name = "" + description = "" + frontmatter_lines = lines[1:end_idx] + i = 0 + while i < len(frontmatter_lines): + line = frontmatter_lines[i] + if line.startswith("name:"): + name = line[len("name:"):].strip().strip('"').strip("'") + elif line.startswith("description:"): + value = line[len("description:"):].strip() + # Handle YAML multiline indicators (>, |, >-, |-) + if value in (">", "|", ">-", "|-"): + continuation_lines: list[str] = [] + i += 1 + while i < len(frontmatter_lines) and (frontmatter_lines[i].startswith(" ") or frontmatter_lines[i].startswith("\t")): + continuation_lines.append(frontmatter_lines[i].strip()) + i += 1 + description = " ".join(continuation_lines) + continue + else: + description = value.strip('"').strip("'") + i += 1 + + return name, description, content diff --git a/skills/skill-finder-cn/SKILL.md b/skills/skill-finder-cn/SKILL.md new file mode 100755 index 0000000..70cf6e3 --- /dev/null +++ b/skills/skill-finder-cn/SKILL.md @@ -0,0 +1,66 @@ +--- +name: skill-finder-cn +description: "Skill 查找器 | Skill Finder. 帮助发现和安装 ClawHub Skills | Discover and install ClawHub Skills. 回答'有什么技能可以X'、'找一个技能' | Answers 'what skill can X', 'find a skill'. 触发词:找 skill、find skill、搜索 skill." +author: 赚钱小能手 +metadata: + openclaw: + emoji: 🔍 + requires: + bins: [clawhub] +--- + +# Skill 查找器 + +帮助用户发现和安装 ClawHub 上的 Skills。 + +## 功能 + +当用户问: +- "有什么 skill 可以帮我...?" +- "找一个能做 X 的 skill" +- "有没有 skill 可以..." +- "我需要一个能...的 skill" + +这个 Skill 会帮助搜索 ClawHub 并推荐相关的 Skills。 + +## 使用方法 + +### 1. 搜索 Skills + +```bash +clawhub search "<用户需求>" +``` + +### 2. 查看详情 + +```bash +clawhub inspect +``` + +### 3. 安装 Skill + +```bash +clawhub install +``` + +## 工作流程 + +``` +1. 理解用户需求 +2. 提取关键词 +3. 搜索 ClawHub +4. 列出相关 Skills +5. 提供安装建议 +``` + +## 示例 + +**用户**: "有什么 skill 可以帮我监控加密货币价格?" + +**搜索**: `clawhub search "crypto price monitor"` + +**返回**: 相关的 Skills 列表 + +--- + +*帮助用户发现需要的 Skills 🔍* diff --git a/skills/skill-finder-cn/_meta.json b/skills/skill-finder-cn/_meta.json new file mode 100755 index 0000000..e482a6a --- /dev/null +++ b/skills/skill-finder-cn/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn71y3mdbcx5dyhwxv1t7sm82x81a168", + "slug": "skill-finder-cn", + "version": "1.0.0", + "publishedAt": 1771323389748 +} \ No newline at end of file diff --git a/skills/skill-finder-cn/package.json b/skills/skill-finder-cn/package.json new file mode 100755 index 0000000..748ec51 --- /dev/null +++ b/skills/skill-finder-cn/package.json @@ -0,0 +1,5 @@ +{ + "name": "skill-finder-cn", + "version": "1.0.0", + "description": "Skill Finder CN | Skill 查找器" +} diff --git a/skills/skill-finder-cn/scripts/search.sh b/skills/skill-finder-cn/scripts/search.sh new file mode 100755 index 0000000..236a45f --- /dev/null +++ b/skills/skill-finder-cn/scripts/search.sh @@ -0,0 +1,15 @@ +#!/bin/bash +# Skill 搜索脚本 + +QUERY="$1" +LIMIT="${2:-10}" + +if [ -z "$QUERY" ]; then + echo "用法: search.sh <关键词> [数量]" + exit 1 +fi + +echo "🔍 搜索: $QUERY" +echo "================================" + +clawhub search "$QUERY" --limit "$LIMIT" diff --git a/skills/skill-vetter/SKILL.md b/skills/skill-vetter/SKILL.md new file mode 100755 index 0000000..af440cc --- /dev/null +++ b/skills/skill-vetter/SKILL.md @@ -0,0 +1,137 @@ +--- +name: skill-vetter +description: Security-first skill vetting for AI agents. Use before installing any skill from ClawdHub, GitHub, or other sources. Checks for red flags, permission scope, and suspicious patterns. +--- + +# Skill Vetter 🔒 + +Security-first vetting protocol for AI agent skills. **Never install a skill without vetting it first.** + +## When to Use + +- Before installing any skill from ClawdHub +- Before running skills from GitHub repos +- When evaluating skills shared by other agents +- Anytime you're asked to install unknown code + +## Vetting Protocol + +### Step 1: Source Check + +``` +Questions to answer: +- [ ] Where did this skill come from? +- [ ] Is the author known/reputable? +- [ ] How many downloads/stars does it have? +- [ ] When was it last updated? +- [ ] Are there reviews from other agents? +``` + +### Step 2: Code Review (MANDATORY) + +Read ALL files in the skill. Check for these **RED FLAGS**: + +``` +🚨 REJECT IMMEDIATELY IF YOU SEE: +───────────────────────────────────────── +• curl/wget to unknown URLs +• Sends data to external servers +• Requests credentials/tokens/API keys +• Reads ~/.ssh, ~/.aws, ~/.config without clear reason +• Accesses MEMORY.md, USER.md, SOUL.md, IDENTITY.md +• Uses base64 decode on anything +• Uses eval() or exec() with external input +• Modifies system files outside workspace +• Installs packages without listing them +• Network calls to IPs instead of domains +• Obfuscated code (compressed, encoded, minified) +• Requests elevated/sudo permissions +• Accesses browser cookies/sessions +• Touches credential files +───────────────────────────────────────── +``` + +### Step 3: Permission Scope + +``` +Evaluate: +- [ ] What files does it need to read? +- [ ] What files does it need to write? +- [ ] What commands does it run? +- [ ] Does it need network access? To where? +- [ ] Is the scope minimal for its stated purpose? +``` + +### Step 4: Risk Classification + +| Risk Level | Examples | Action | +|------------|----------|--------| +| 🟢 LOW | Notes, weather, formatting | Basic review, install OK | +| 🟡 MEDIUM | File ops, browser, APIs | Full code review required | +| 🔴 HIGH | Credentials, trading, system | Human approval required | +| ⛔ EXTREME | Security configs, root access | Do NOT install | + +## Output Format + +After vetting, produce this report: + +``` +SKILL VETTING REPORT +═══════════════════════════════════════ +Skill: [name] +Source: [ClawdHub / GitHub / other] +Author: [username] +Version: [version] +─────────────────────────────────────── +METRICS: +• Downloads/Stars: [count] +• Last Updated: [date] +• Files Reviewed: [count] +─────────────────────────────────────── +RED FLAGS: [None / List them] + +PERMISSIONS NEEDED: +• Files: [list or "None"] +• Network: [list or "None"] +• Commands: [list or "None"] +─────────────────────────────────────── +RISK LEVEL: [🟢 LOW / 🟡 MEDIUM / 🔴 HIGH / ⛔ EXTREME] + +VERDICT: [✅ SAFE TO INSTALL / ⚠️ INSTALL WITH CAUTION / ❌ DO NOT INSTALL] + +NOTES: [Any observations] +═══════════════════════════════════════ +``` + +## Quick Vet Commands + +For GitHub-hosted skills: +```bash +# Check repo stats +curl -s "https://api.github.com/repos/OWNER/REPO" | jq '{stars: .stargazers_count, forks: .forks_count, updated: .updated_at}' + +# List skill files +curl -s "https://api.github.com/repos/OWNER/REPO/contents/skills/SKILL_NAME" | jq '.[].name' + +# Fetch and review SKILL.md +curl -s "https://raw.githubusercontent.com/OWNER/REPO/main/skills/SKILL_NAME/SKILL.md" +``` + +## Trust Hierarchy + +1. **Official OpenClaw skills** → Lower scrutiny (still review) +2. **High-star repos (1000+)** → Moderate scrutiny +3. **Known authors** → Moderate scrutiny +4. **New/unknown sources** → Maximum scrutiny +5. **Skills requesting credentials** → Human approval always + +## Remember + +- No skill is worth compromising security +- When in doubt, don't install +- Ask your human for high-risk decisions +- Document what you vet for future reference + +--- + +*Paranoia is a feature.* 🔒🦀 diff --git a/skills/stock-analysis-skill/SKILL.md b/skills/stock-analysis-skill/SKILL.md new file mode 100755 index 0000000..e32b65f --- /dev/null +++ b/skills/stock-analysis-skill/SKILL.md @@ -0,0 +1,156 @@ +--- +name: stock_analysis +description: "Comprehensive stock market analysis skill covering A-share (China), Hong Kong, and US equities. Priority use cases: stock analysis and buy/sell/hold recommendations by ticker code, generating decision dashboards and research reports with technical/fundamental/sentiment analysis, position-aware investment strategies based on user's cost price, dividend income scoring and safety analysis, rumor and early market signal scanning (M&A, insider activity, analyst actions), watchlist management with price target and stop-loss alerts, and K-line chart pattern recognition from images. This skill should be the primary choice whenever users mention a stock ticker, ask whether to buy or sell a stock, reference their holding cost or position, request dividend analysis, ask about market rumors or early signals, want to add/check/manage a watchlist, or upload a chart image for technical analysis." +--- + +# Stock Analysis Skill + +## 依赖平台 Skills + +- `finance skill` — 所有市场数据(A股/港股/美股统一) +- `pdf skill` — PDF 研报生成 +- `docx skill` — Word 文档生成 +- `vlm skill`(内置)— K线图形态识别 + +--- + +## Commands & Triggers + +| 命令 | 触发词示例 | +|------|-----------| +| 个股分析 | 分析600519 / AAPL值不值得买 / 帮我看看腾讯 | +| 带持仓分析 | 我持仓成本1450分析茅台 / AAPL我170买的现在怎样 | +| 股息分析 | JNJ股息怎么样 / 帮我分析这几只股的股息 KO PG JNJ | +| 传闻扫描 | 今日有什么并购传闻 / 扫描一下市场早期信号 | +| 添加自选股 | 关注AAPL / 把600519加入自选,目标价1600止损1350 | +| 查看自选股 | 我的自选股列表 / 看一下我关注的股票 | +| 检查提醒 | 检查自选股提醒 / 有没有触发止损 | +| 删除自选股 | 从自选股删除TSLA | +| K线图分析 | (上传图片)帮我分析这个K线图 | +| 大盘复盘 | 附带大盘复盘分析600519 | + +--- + +## Input Schemas + +### 个股分析 +```typescript +{ + stocks: (string | { code: string; position?: { status: "empty"|"holding"; cost?: number; shares?: number } })[], + outputFormat?: "markdown" | "pdf" | "word", // 默认 markdown + mode?: "full" | "quote", // 默认 full + includeMarketReview?: boolean, // 默认 false + includeGlobalMacro?: boolean, // 默认 true + includeDividend?: boolean, // 美股附加股息分析,默认 false +} +``` + +### 股息分析 +```typescript +runDividend(tickers: string | string[]) +``` + +### 传闻扫描 +```typescript +runRumorScan() // 无需参数,自动扫描今日信号 +``` + +### 自选股管理 +```typescript +runWatchlistAdd(ticker, { targetPrice?, stopPrice?, alertOnSignal?, notes? }) +runWatchlistRemove(ticker) +runWatchlistList() +runWatchlistCheck() // 检查是否触发价格/信号提醒 +``` + +--- + +## Report Structure + +``` +# 股票智能分析报告 + +## 🌍 全球宏观速览(默认开启) +## 🎯 大盘复盘(需开启) +## 📊 个股决策仪表盘(每只) + ### 📰 重要信息速览(舆情/业绩预期/🚨风险/✨利好/最新动态) + ### 📌 核心结论(结论/一句话/空仓者建议/持仓者建议+盈亏) + ### 📈 当日行情 + ### 📊 数据透视(技术面/基本面/资金面) + ### 🎯 作战计划(狙击点位表/仓位/风控) + ### ✅ 检查清单(综合结论) + ### 💰 股息分析(美股,需开启 includeDividend) +``` + +--- + +## Dividend Analysis Metrics + +| 指标 | 说明 | +|------|------| +| 安全评分 | 0-100,综合派息率/增长/连续年数 | +| 收入评级 | excellent/good/moderate/poor | +| 派息率状态 | safe(<40%)/moderate/high/unsustainable | +| 5年CAGR | 股息复合增长率 | +| 连续增长年数 | 25年以上为股息贵族 | + +--- + +## Rumor Scanner Signal Types + +| 类型 | 冲击分 | 说明 | +|------|--------|------| +| 并购传闻 (ma) | +5 | M&A/收购/要约 | +| 内部人动态 (insider) | +4 | CEO/董事买卖 | +| 分析师调整 (analyst) | +3 | 评级上调/下调/目标价变动 | +| 监管动态 (regulatory) | +3 | SEC调查/合规风险 | +| 业绩预期 (earnings) | +2 | 盈利预警/上调 | + +--- + +## Watchlist Alert Types + +| 提醒类型 | 触发条件 | +|---------|---------| +| 🎯 目标价 | 当前价 ≥ targetPrice | +| 🛑 止损价 | 当前价 ≤ stopPrice | +| 📊 信号变化 | 本次结论 ≠ 上次结论 | + +--- + +## Behavior Rules + +- 乖离率 > 5% → 结论不得为买入/强烈买入 +- 数据缺失 → 标"暂缺",严禁捏造 +- 有持仓成本 → 必须给出盈亏分析 +- 未提供持仓 → 同时给出空仓/持仓两套建议 +- 每次分析后自动静默更新自选股信号 + +--- + +## File Structure + +``` +stock-analysis-skill/ +├── SKILL.md +├── package.json +├── tsconfig.json +└── src/ + ├── index.ts # 主入口(所有命令路由) + ├── types.ts # 类型定义 + ├── dataFetcher.ts # 数据层(finance skill) + ├── analyzer.ts # 个股分析(LLM/VLM) + ├── dividend.ts # 股息分析 + ├── rumorScanner.ts # 传闻扫描 + └── watchlist.ts # 自选股管理(storage 持久化) +``` + +--- + +## Limitations + +- 传闻扫描依赖 finance skill 新闻数据质量 +- 自选股数据持久化依赖平台 storage API +- 港股基本面数据较少 +- 不支持期货、ETF、可转债 +- 仅供参考,不构成投资建议 diff --git a/skills/stock-analysis-skill/package.json b/skills/stock-analysis-skill/package.json new file mode 100755 index 0000000..d6ed1f8 --- /dev/null +++ b/skills/stock-analysis-skill/package.json @@ -0,0 +1,21 @@ +{ + "name": "stock-analysis-skill", + "version": "1.0.0", + "description": "A股/港股/美股 决策仪表盘 + 传闻扫描 + 自选股提醒", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "analyze": "ts-node src/index.ts analyze", + "dividend": "ts-node src/index.ts dividend", + "rumors": "ts-node src/index.ts rumors", + "watch": "ts-node src/index.ts watch" + }, + "dependencies": { + "z-ai-web-dev-sdk": "latest" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "ts-node": "^10.0.0" + } +} diff --git a/skills/stock-analysis-skill/src/analyzer.ts b/skills/stock-analysis-skill/src/analyzer.ts new file mode 100755 index 0000000..fa515e5 --- /dev/null +++ b/skills/stock-analysis-skill/src/analyzer.ts @@ -0,0 +1,264 @@ +/** + * analyzer.ts — LLM/VLM 分析层 + * 七段式决策仪表盘 + 美股可附加股息分析 + */ + +import ZAI from "z-ai-web-dev-sdk"; +import { StockData, AnalysisResult, OutputFormat, Market, Verdict, PositionInfo } from "./types"; +import { validateStockData } from "./dataFetcher"; +import { analyzeDividend, formatDividendMarkdown } from "./dividend"; + +const MARKET_LABEL: Record = { CN: "A股", HK: "港股", US: "美股" }; + +// ── 仪表盘 Prompt ───────────────────────────────────────── + +function buildDashboardPrompt( + data: StockData, + position: PositionInfo | undefined, + warnings: string[] +): string { + const warningBlock = warnings.length > 0 + ? `⚠️ 数据预警(必须在报告中体现):\n${warnings.map((w) => `- ${w}`).join("\n")}\n\n` + : ""; + + const positionBlock = position + ? position.status === "holding" + ? `用户持仓:持仓中,成本价 ${position.cost ?? "未知"} 元${position.shares ? `,${position.shares} 股` : ""}。请给出盈亏分析和针对性建议。` + : `用户持仓:当前空仓。` + : `用户持仓:未提供(请同时给出空仓者和持仓者两套建议)。`; + + return `${warningBlock}${positionBlock} + +股票数据: +\`\`\`json +${JSON.stringify(data, null, 2)} +\`\`\` + +请输出以下格式的完整决策仪表盘(严格按结构,不增删章节): + +--- + +## 决策仪表盘 · {名称}({代码}) · {市场} + +--- + +### 📰 重要信息速览 +**💭 舆情情绪:** 一句话描述 +**📊 业绩预期:** 结合 PE/ROE/行业简述,数据缺失标"暂缺" +**🚨 风险警报:** +- 风险1(技术面或宏观) +- 风险2(基本面或行业) +**✨ 利好催化:** +- 利好1(技术面) +- 利好2(基本面或行业) +**📢 最新动态:** 结合行业背景,补充1条关键信息 + +--- + +### 📌 核心结论 +**[emoji] 结论:强烈买入 / 买入 / 观望 / 卖出**(四选一,乖离率>5%不得为买入) +**💬 一句话决策:** 核心逻辑 +**⏰ 时效性:** 立即行动 / 今日内 / 不急 + +(根据持仓状态输出) +- **🆕 空仓者:** 是否进场、建仓点位、仓位比例 +- **💼 持仓者:** 持有/加仓/减仓/止损建议${position?.status === "holding" && position.cost ? ",含成本盈亏分析" : ""} + +--- + +### 📈 当日行情 +列出:收盘价、昨收、开盘、最高、最低、涨跌幅、涨跌额、振幅、成交量、成交额(缺失标"暂缺") + +--- + +### 📊 数据透视 + +**技术面:** +表格(指标 | 数值 | 解读):MA5、MA10、MA20、乖离率(BIAS20)、RSI(如有)、支撑位、压力位 +结论:均线状态 + 趋势强度(xx/100) + +**基本面(注明报告期):** +表格(指标 | 数值 | 行业对比):ROE、毛利率、净利率、资产负债率、PE、PB +数据缺失标"暂缺",不得捏造 + +**资金面:**(A股/港股适用,美股可略) +- 主力净流入:金额(占比%),一句话解读 +- 筹码:获利比例 | 平均成本 | 集中度 + +--- + +### 🎯 作战计划 + +| 点位类型 | 价格 | 说明 | +|---------|------|------| +| 🎯 理想买入 | xxx | | +| 🔵 次优买入 | xxx | | +| 🛑 止损位 | xxx | | +| 🎊 目标位 | xxx | | + +**💰 仓位建议:** x成 +**建仓策略:** 分批策略 +**风控策略:** 止损纪律 + +--- + +### ✅ 检查清单 +- ✅/⚠️/❌ 均线状态 +- ✅/⚠️/❌ 乖离率安全(<5%) +- ✅/⚠️/❌ 量能配合 +- ✅/⚠️/❌ 估值合理 +- ✅/⚠️/❌ 资金流向 +- ✅/⚠️/❌ 筹码健康 + +**综合结论:** 一句话总结当前状态和建议。 + +--- +*以上分析仅供参考,不构成投资建议,据此操作风险自担。*`; +} + +// ── 研报 Prompt(PDF/Word)────────────────────────────── + +function buildReportPrompt(data: StockData, position: PositionInfo | undefined): string { + const positionBlock = position?.status === "holding" + ? `用户持仓成本:${position.cost ?? "未知"}` + : "用户当前空仓"; + + return `${positionBlock} + +股票数据: +\`\`\`json +${JSON.stringify(data, null, 2)} +\`\`\` + +请生成结构化研报: + +【研究报告】{名称}({代码}) · {市场} · {日期} + +一、投资结论(买入/强烈买入/观望/卖出,含目标价、止损价,分空仓/持仓两套建议) +二、重要信息速览(舆情/业绩预期/风险/利好/最新动态) +三、数据透视(技术面/基本面/资金面) +四、作战计划(点位表/仓位/持仓周期) +五、风险提示(2-3条) + +免责声明:本报告由AI辅助生成,仅供参考,不构成投资建议,据此操作风险自担。`; +} + +// ── 提取结论 ────────────────────────────────────────────── + +function extractVerdict(text: string): Verdict { + const patterns = [ + /结论[::]\s*[💚🟢🟡🔴⚪]?\s*(强烈买入|买入|观望|卖出)/, + /核心结论[::]\s*[💚🟢🟡🔴⚪]?\s*(强烈买入|买入|观望|卖出)/, + /\*\*(强烈买入|买入|观望|卖出)\*\*/, + ]; + for (const p of patterns) { + const m = text.match(p); + if (m) return m[1] as Verdict; + } + return "观望"; +} + +// ── 核心分析 ────────────────────────────────────────────── + +export async function analyzeStock( + data: StockData, + outputFormat: OutputFormat = "markdown", + position?: PositionInfo, + includeDividend = false +): Promise { + const { valid, warnings } = validateStockData(data); + const name = data.name ?? data.code; + + if (!valid) { + return { + code: data.code, market: data.market, name, + verdict: "观望", + analysis: `## ⚠️ 数据获取失败\n\n${data.code} 数据无法获取(${data.error ?? "未知错误"}),建议手动核实。`, + warnings, outputFormat, + generatedAt: new Date().toISOString(), + }; + } + + const zai = await ZAI.create(); + const userPrompt = outputFormat === "markdown" + ? buildDashboardPrompt(data, position, warnings) + : buildReportPrompt(data, position); + + let analysisText = "⚠️ LLM 未返回内容,请重试。"; + try { + const completion = await zai.chat.completions.create({ + messages: [ + { role: "system", content: `你是一位资深${MARKET_LABEL[data.market]}股票分析师。数据缺失标"暂缺",严禁捏造。乖离率>5%不得建议买入。结论四选一:强烈买入/买入/观望/卖出。输出语言:中文。` }, + { role: "user", content: userPrompt }, + ], + thinking: { type: "disabled" }, + }); + analysisText = completion.choices[0]?.message?.content ?? analysisText; + } catch (err: any) { + analysisText = `## ⚠️ 分析失败\n\nLLM 调用出错:${err.message}`; + } + + // 美股附加股息分析 + if (includeDividend && data.market === "US" && outputFormat === "markdown") { + try { + const dividend = await analyzeDividend(data.code); + const dividendMd = formatDividendMarkdown(dividend); + analysisText += `\n\n${dividendMd}`; + } catch {} + } + + return { + code: data.code, market: data.market, name, + verdict: extractVerdict(analysisText), + analysis: analysisText, + warnings, outputFormat, + generatedAt: new Date().toISOString(), + }; +} + +// ── 批量分析 ────────────────────────────────────────────── + +export async function analyzeMultipleStocks( + stockDataList: StockData[], + outputFormat: OutputFormat = "markdown", + positions?: Record, + includeDividend = false +): Promise { + const results: AnalysisResult[] = []; + for (const data of stockDataList) { + const position = positions?.[data.code]; + results.push(await analyzeStock(data, outputFormat, position, includeDividend)); + } + return results; +} + +// ── K线图分析(VLM)────────────────────────────────────── + +export async function analyzeChartImage( + imageUrlOrBase64: string, + stockCode: string, + isBase64 = false +): Promise { + try { + const zai = await ZAI.create(); + const imageContent = isBase64 + ? { type: "base64" as const, data: imageUrlOrBase64, mediaType: "image/png" as const } + : { type: "url" as const, url: imageUrlOrBase64 }; + + const completion = await zai.chat.completions.create({ + messages: [ + { role: "system", content: "你是技术分析专家,擅长K线形态识别。请用中文回答。" }, + { + role: "user", + content: [ + { type: "image", image: imageContent }, + { type: "text", text: `这是 ${stockCode} 的K线图,请分析:\n1. 当前K线形态\n2. 趋势方向\n3. 关键支撑位和压力位\n4. 成交量配合\n5. 短期操作建议` }, + ], + }, + ], + }); + return completion.choices[0]?.message?.content ?? "⚠️ VLM 未返回内容"; + } catch (err: any) { + return `K线图分析失败:${err.message}`; + } +} diff --git a/skills/stock-analysis-skill/src/dataFetcher.ts b/skills/stock-analysis-skill/src/dataFetcher.ts new file mode 100755 index 0000000..d7429c3 --- /dev/null +++ b/skills/stock-analysis-skill/src/dataFetcher.ts @@ -0,0 +1,130 @@ +/** + * dataFetcher.ts — 全部通过 finance skill 获取数据 + */ + +import ZAI from "z-ai-web-dev-sdk"; +import { Market, FetchMode, StockData } from "./types"; + +export function detectMarket(code: string): Market { + if (/^\d{6}$/.test(code)) return "CN"; + if (/^\d{4,5}\.HK$/i.test(code)) return "HK"; + return "US"; +} + +const MARKET_LABEL: Record = { + CN: "A股", HK: "港股", US: "美股", +}; + +// ── 单只股票数据 ────────────────────────────────────────── + +export async function fetchStockData( + code: string, + mode: FetchMode = "full" +): Promise { + const market = detectMarket(code); + const zai = await ZAI.create(); + + const prompt = mode === "quote" + ? `查询 ${code}(${MARKET_LABEL[market]})实时股价,只返回 JSON,字段: + name, price, prev_close, change_pct, change_amount, volume, amount, turnover, volume_ratio, market_cap, pe_ttm, pb。` + : `获取 ${code}(${MARKET_LABEL[market]})完整股票数据,只返回 JSON,包含: + name, price, prev_close, open, high, low, change_pct, change_amount, amplitude, + volume, amount, turnover, volume_ratio, market_cap, pe_ttm, pb, high_52w, low_52w, dividend_yield, + ma5, ma10, ma20, bias_pct(相对MA20乖离率%), + trend("多头排列"/"空头排列"/"震荡整理"), overbought_warning(bias_pct>5则true), + support(支撑位), resistance(压力位), rsi, + kline_recent_10(最近10日,每项含 date/open/close/high/low/volume), + profit_ratio(获利比例%), avg_cost(平均成本), chip_concentration(筹码集中度), + roe, gross_margin, net_margin, debt_ratio, eps, revenue_growth, + main_net(主力净流入金额), main_net_pct(主力净流入占比%)。 + 缺失字段填 null,不得捏造。`; + + try { + const completion = await zai.chat.completions.create({ + messages: [{ role: "user", content: prompt }], + thinking: { type: "disabled" }, + }); + const raw = completion.choices[0]?.message?.content ?? ""; + const parsed = JSON.parse(raw.replace(/```json|```/g, "").trim()); + return { code, market, timestamp: new Date().toISOString(), ...parsed }; + } catch (err: any) { + return { code, market, timestamp: new Date().toISOString(), error: err.message }; + } +} + +// ── 批量抓取(串行)────────────────────────────────────── + +export async function fetchMultipleStocks( + codes: string[], + mode: FetchMode = "full" +): Promise { + const results: StockData[] = []; + for (let i = 0; i < codes.length; i++) { + results.push(await fetchStockData(codes[i], mode)); + if (i < codes.length - 1) await new Promise((r) => setTimeout(r, 300)); + } + return results; +} + +// ── 全球宏观 ────────────────────────────────────────────── + +export async function fetchGlobalMacro(): Promise { + const zai = await ZAI.create(); + try { + const completion = await zai.chat.completions.create({ + messages: [{ + role: "user", + content: `请获取今日全球市场关键信息,简洁 Markdown 输出,包含: +- 美股三大指数昨收涨跌(道指/纳指/标普) +- 美元指数、人民币汇率动向 +- 黄金、原油最新价格 +- 美联储最新政策动向(如有) +- 影响 A股/港股的1-2条关键宏观事件 +控制在150字以内。`, + }], + thinking: { type: "disabled" }, + }); + return completion.choices[0]?.message?.content ?? "全球宏观数据获取失败"; + } catch (err: any) { + return `全球宏观数据获取失败:${err.message}`; + } +} + +// ── 大盘复盘 ────────────────────────────────────────────── + +export async function fetchMarketOverview(): Promise { + const zai = await ZAI.create(); + try { + const completion = await zai.chat.completions.create({ + messages: [{ + role: "user", + content: `请获取今日 A股大盘数据,Markdown 输出,包含: +- 上证/深证/创业板/科创50 最新点位和涨跌幅 +- 今日上涨/下跌/涨停/跌停家数,成交额 +- 领涨板块 TOP3 和领跌板块 TOP3 +- 北向资金净流入 +- 一句话后市展望和仓位策略 +控制在200字以内。`, + }], + thinking: { type: "disabled" }, + }); + return completion.choices[0]?.message?.content ?? "大盘数据获取失败"; + } catch (err: any) { + return `大盘数据获取失败:${err.message}`; + } +} + +// ── 数据校验 ────────────────────────────────────────────── + +export function validateStockData(data: StockData): { + valid: boolean; + warnings: string[]; +} { + const warnings: string[] = []; + if (data.error) return { valid: false, warnings: [`数据获取失败:${data.error}`] }; + if (!data.price) warnings.push("价格数据缺失"); + if (!data.ma5 && !data.ma20) warnings.push("技术面数据缺失"); + if (data.overbought_warning) warnings.push(`⚠️ 乖离率 ${data.bias_pct}% 超过5%,严禁追高`); + if (!data.roe && !data.pe_ttm) warnings.push("基本面数据不完整"); + return { valid: !!data.price, warnings }; +} diff --git a/skills/stock-analysis-skill/src/dividend.ts b/skills/stock-analysis-skill/src/dividend.ts new file mode 100755 index 0000000..cffedc6 --- /dev/null +++ b/skills/stock-analysis-skill/src/dividend.ts @@ -0,0 +1,226 @@ +/** + * dividend.ts + * 股息分析模块(适配 finance skill) + * 移植原版评分逻辑:安全评分 / CAGR / 连续增长年数 / 派息可持续性 + */ + +import ZAI from "z-ai-web-dev-sdk"; +import { DividendAnalysis, PayoutStatus, IncomeRating } from "./types"; + +// ── 核心评分逻辑(与原版完全一致)────────────────────────── + +function calcPayoutStatus(payoutRatio: number | null): PayoutStatus { + if (payoutRatio === null) return "unknown"; + if (payoutRatio < 40) return "safe"; + if (payoutRatio < 60) return "moderate"; + if (payoutRatio < 80) return "high"; + return "unsustainable"; +} + +function calcSafetyScore(data: { + payoutRatio: number | null; + dividendGrowth5y: number | null; + consecutiveYears: number | null; + dividendYield: number | null; +}): { score: number; factors: string[] } { + let score = 50; + const factors: string[] = []; + + // 派息率(±20) + if (data.payoutRatio !== null) { + if (data.payoutRatio < 40) { score += 20; factors.push(`派息率健康(${data.payoutRatio.toFixed(0)}%)`); } + else if (data.payoutRatio < 60) { score += 10; factors.push(`派息率适中(${data.payoutRatio.toFixed(0)}%)`); } + else if (data.payoutRatio < 80) { score -= 10; factors.push(`派息率偏高(${data.payoutRatio.toFixed(0)}%)`); } + else { score -= 20; factors.push(`派息率不可持续(${data.payoutRatio.toFixed(0)}%)`); } + } + + // 5年增长率(±15) + if (data.dividendGrowth5y !== null) { + if (data.dividendGrowth5y > 10) { score += 15; factors.push(`股息增长强劲(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); } + else if (data.dividendGrowth5y > 5) { score += 10; factors.push(`股息增长良好(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); } + else if (data.dividendGrowth5y > 0) { score += 5; factors.push(`股息小幅增长(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); } + else { score -= 15; factors.push(`股息下降(${data.dividendGrowth5y.toFixed(1)}% CAGR)`); } + } + + // 连续增长年数(±15) + if (data.consecutiveYears !== null) { + if (data.consecutiveYears >= 25) { score += 15; factors.push(`股息贵族(连续${data.consecutiveYears}年增长)`); } + else if (data.consecutiveYears >= 10) { score += 10; factors.push(`长期稳定股息(${data.consecutiveYears}年)`); } + else if (data.consecutiveYears >= 5) { score += 5; factors.push(`股息稳定(${data.consecutiveYears}年)`); } + } + + // 高收益率风险(-10) + if (data.dividendYield !== null) { + if (data.dividendYield > 8) { score -= 10; factors.push(`收益率过高(${data.dividendYield.toFixed(1)}%),需核实可持续性`); } + else if (data.dividendYield < 1) { factors.push(`收益率偏低(${data.dividendYield.toFixed(2)}%)`); } + } + + return { score: Math.max(0, Math.min(100, score)), factors }; +} + +function calcIncomeRating(safetyScore: number): IncomeRating { + if (safetyScore >= 80) return "excellent"; + if (safetyScore >= 60) return "good"; + if (safetyScore >= 40) return "moderate"; + return "poor"; +} + +// ── 通过 finance skill 获取股息数据 ────────────────────── + +async function fetchDividendData(ticker: string): Promise> { + const zai = await ZAI.create(); + + const completion = await zai.chat.completions.create({ + messages: [{ + role: "user", + content: `请查询 ${ticker} 的股息数据,只返回 JSON,包含: +name(公司名称), currentPrice(当前股价), +dividendYield(年化股息率%), annualDividend(年度每股股息), +trailingEps(过去12月每股收益), +exDividendDate(除权日 YYYY-MM-DD 格式), +paymentFrequency("monthly"/"quarterly"/"annual",根据派息频率判断), +dividendHistory(近5年年度股息数组,每项含 year 和 total,从新到旧排序), +consecutiveYears(连续股息增长年数,整数), +dividendGrowth5y(近5年股息 CAGR %)。 +缺失字段填 null,不得捏造。`, + }], + thinking: { type: "disabled" }, + }); + + const raw = completion.choices[0]?.message?.content ?? "{}"; + return JSON.parse(raw.replace(/```json|```/g, "").trim()); +} + +// ── 主分析函数 ──────────────────────────────────────────── + +export async function analyzeDividend(ticker: string): Promise { + ticker = ticker.toUpperCase(); + + let raw: Record = {}; + try { + raw = await fetchDividendData(ticker); + } catch (err: any) { + return { + ticker, name: ticker, currentPrice: null, + dividendYield: null, annualDividend: null, + payoutRatio: null, payoutStatus: "unknown", + dividendGrowth5y: null, consecutiveYears: null, + exDividendDate: null, paymentFrequency: null, + safetyScore: 0, safetyFactors: [`数据获取失败:${err.message}`], + incomeRating: "poor", dividendHistory: [], + summary: `${ticker} 股息数据获取失败。`, + }; + } + + // 无股息 + if (!raw.annualDividend || raw.annualDividend === 0) { + return { + ticker, name: raw.name ?? ticker, + currentPrice: raw.currentPrice ?? null, + dividendYield: null, annualDividend: null, + payoutRatio: null, payoutStatus: "no_dividend", + dividendGrowth5y: null, consecutiveYears: null, + exDividendDate: null, paymentFrequency: null, + safetyScore: 0, safetyFactors: ["该股票不派息"], + incomeRating: "no_dividend", dividendHistory: [], + summary: `${ticker} 目前不派发股息。`, + }; + } + + // 计算派息率 + const payoutRatio = (raw.trailingEps && raw.trailingEps > 0 && raw.annualDividend) + ? parseFloat(((raw.annualDividend / raw.trailingEps) * 100).toFixed(1)) + : null; + + const payoutStatus = calcPayoutStatus(payoutRatio); + const { score: safetyScore, factors: safetyFactors } = calcSafetyScore({ + payoutRatio, + dividendGrowth5y: raw.dividendGrowth5y ?? null, + consecutiveYears: raw.consecutiveYears ?? null, + dividendYield: raw.dividendYield ?? null, + }); + + const incomeRating = calcIncomeRating(safetyScore); + + // 生成摘要 + const parts: string[] = []; + if (raw.dividendYield) parts.push(`收益率 ${Number(raw.dividendYield).toFixed(2)}%`); + if (payoutRatio) parts.push(`派息率 ${payoutRatio.toFixed(0)}%`); + if (raw.dividendGrowth5y) parts.push(`5年增长 ${Number(raw.dividendGrowth5y) > 0 ? "+" : ""}${Number(raw.dividendGrowth5y).toFixed(1)}%`); + if (raw.consecutiveYears && raw.consecutiveYears >= 5) parts.push(`连续增长 ${raw.consecutiveYears} 年`); + + const ratingLabel: Record = { + excellent: "优秀", good: "良好", moderate: "一般", poor: "较差", no_dividend: "无股息", + }; + + return { + ticker, + name: raw.name ?? ticker, + currentPrice: raw.currentPrice ?? null, + dividendYield: raw.dividendYield ? Number(Number(raw.dividendYield).toFixed(2)) : null, + annualDividend: raw.annualDividend ?? null, + payoutRatio, + payoutStatus, + dividendGrowth5y: raw.dividendGrowth5y ? Number(Number(raw.dividendGrowth5y).toFixed(2)) : null, + consecutiveYears: raw.consecutiveYears ?? null, + exDividendDate: raw.exDividendDate ?? null, + paymentFrequency: raw.paymentFrequency ?? null, + safetyScore, + safetyFactors, + incomeRating, + dividendHistory: raw.dividendHistory ?? [], + summary: `${ticker}(${raw.name ?? ""}):${parts.join(",")}。评级:${ratingLabel[incomeRating]}`, + }; +} + +// ── 格式化输出(Markdown 仪表盘)───────────────────────── + +export function formatDividendMarkdown(analysis: DividendAnalysis): string { + if (analysis.incomeRating === "no_dividend") { + return `### 💰 股息分析 · ${analysis.ticker}\n\n该股票目前不派发股息。`; + } + + const ratingEmoji: Record = { + excellent: "🏆", good: "✅", moderate: "⚠️", poor: "❌", no_dividend: "—", + }; + + const payoutLabel: Record = { + safe: "✅ 安全", moderate: "⚠️ 适中", high: "⚠️ 偏高", unsustainable: "❌ 不可持续", unknown: "暂缺", + }; + + let md = `### 💰 股息分析 · ${analysis.ticker}(${analysis.name}) + +| 指标 | 数值 | +|------|------| +| 股息收益率 | ${analysis.dividendYield ? `${analysis.dividendYield}%` : "暂缺"} | +| 年度每股股息 | ${analysis.annualDividend ? `$${analysis.annualDividend}` : "暂缺"} | +| 派息频率 | ${analysis.paymentFrequency ?? "暂缺"} | +| 除权日 | ${analysis.exDividendDate ?? "暂缺"} | +| 派息率 | ${analysis.payoutRatio ? `${analysis.payoutRatio}%(${payoutLabel[analysis.payoutStatus]})` : "暂缺"} | +| 5年股息增长 | ${analysis.dividendGrowth5y ? `${analysis.dividendGrowth5y > 0 ? "+" : ""}${analysis.dividendGrowth5y}%` : "暂缺"} | +| 连续增长年数 | ${analysis.consecutiveYears ?? "暂缺"} | + +**安全评分:${analysis.safetyScore}/100 ${ratingEmoji[analysis.incomeRating]} 收入评级:${analysis.incomeRating.toUpperCase()}** + +评分依据: +${analysis.safetyFactors.map((f) => `- ${f}`).join("\n")} +`; + + if (analysis.dividendHistory.length > 0) { + md += `\n近年股息历史:\n`; + md += analysis.dividendHistory.slice(0, 5).map((h) => `- ${h.year}年:$${h.total}`).join("\n"); + } + + return md; +} + +// ── 批量分析 ────────────────────────────────────────────── + +export async function analyzeDividends(tickers: string[]): Promise { + const results: DividendAnalysis[] = []; + for (const ticker of tickers) { + results.push(await analyzeDividend(ticker)); + await new Promise((r) => setTimeout(r, 200)); + } + return results; +} diff --git a/skills/stock-analysis-skill/src/index.ts b/skills/stock-analysis-skill/src/index.ts new file mode 100755 index 0000000..8be40d9 --- /dev/null +++ b/skills/stock-analysis-skill/src/index.ts @@ -0,0 +1,327 @@ +/** + * index.ts — Skill 主入口 + * + * 支持命令: + * run() — 个股分析(主流程) + * runDividend() — 股息分析 + * runRumorScan() — 传闻扫描 + * runWatchlistAdd() — 添加自选股 + * runWatchlistList() — 查看自选股 + * runWatchlistCheck()— 检查提醒 + * runWatchlistRemove()— 删除自选股 + * runChartAnalysis() — K线图分析 + */ + +import ZAI from "z-ai-web-dev-sdk"; +import { fetchMultipleStocks, fetchMarketOverview, fetchGlobalMacro } from "./dataFetcher"; +import { analyzeMultipleStocks, analyzeChartImage } from "./analyzer"; +import { analyzeDividends, formatDividendMarkdown } from "./dividend"; +import { scanRumors, formatRumorMarkdown } from "./rumorScanner"; +import { + addToWatchlist, removeFromWatchlist, + listWatchlist, checkAlerts, + formatWatchlistMarkdown, formatAlertsMarkdown, +} from "./watchlist"; +import { SkillInput, StockInput, OutputFormat, AnalysisResult, PositionInfo, Verdict } from "./types"; + +// ── 输入解析 ────────────────────────────────────────────── + +function parseInput(raw: unknown): { + stocks: string[]; + positions: Record; + outputFormat: OutputFormat; + mode: "full" | "quote"; + includeMarketReview: boolean; + includeGlobalMacro: boolean; + includeDividend: boolean; +} { + const stocks: string[] = []; + const positions: Record = {}; + let outputFormat: OutputFormat = "markdown"; + let mode: "full" | "quote" = "full"; + let includeMarketReview = false; + let includeGlobalMacro = true; + let includeDividend = false; + + if (typeof raw === "string") { + raw.split(/[,,\s]+/).map((s) => s.trim().toUpperCase()).filter(Boolean).forEach((c) => stocks.push(c)); + } else if (typeof raw === "object" && raw !== null) { + const input = raw as any; + const rawStocks: (string | StockInput)[] = input.stocks ?? (input.stock ? [input.stock] : []); + for (const s of rawStocks) { + if (typeof s === "string") stocks.push(s.trim().toUpperCase()); + else if (s.code) { + const code = s.code.trim().toUpperCase(); + stocks.push(code); + if (s.position) positions[code] = s.position; + } + } + outputFormat = input.outputFormat ?? input.format ?? "markdown"; + mode = input.mode ?? "full"; + includeMarketReview = input.includeMarketReview ?? false; + includeGlobalMacro = input.includeGlobalMacro ?? true; + includeDividend = input.includeDividend ?? false; + } + + return { stocks, positions, outputFormat, mode, includeMarketReview, includeGlobalMacro, includeDividend }; +} + +function log(msg: string) { console.log(`[${new Date().toISOString()}] ${msg}`); } + +// ── 报告组装 ────────────────────────────────────────────── + +function buildFullReport( + results: AnalysisResult[], + globalMacro?: string, + marketOverview?: string +): string { + const date = new Date().toLocaleDateString("zh-CN"); + const buy = results.filter((r) => ["买入", "强烈买入"].includes(r.verdict)).length; + const watch = results.filter((r) => r.verdict === "观望").length; + const sell = results.filter((r) => r.verdict === "卖出").length; + + let output = `# 📈 股票智能分析报告 +**生成时间:** ${date} | **分析 ${results.length} 只** | 🟢买入/强烈买入 ${buy} 🟡观望 ${watch} 🔴卖出 ${sell} + +`; + if (globalMacro) output += `---\n\n## 🌍 全球宏观速览\n\n${globalMacro}\n\n`; + if (marketOverview) output += `---\n\n## 🎯 大盘复盘\n\n${marketOverview}\n\n`; + output += `---\n\n## 📊 个股决策仪表盘\n\n`; + output += results.map((r) => { + const warn = r.warnings.length > 0 ? `\n> ⚠️ **预警:** ${r.warnings.join(" | ")}\n` : ""; + return `${warn}\n${r.analysis}\n\n---\n`; + }).join("\n"); + + return output; +} + +// ── 调用 pdf/docx skill ─────────────────────────────────── + +async function exportToFormat(content: string, format: "pdf" | "word"): Promise { + const zai = await ZAI.create(); + const isPDF = format === "pdf"; + const completion = await zai.chat.completions.create({ + messages: [{ + role: "user", + content: isPDF + ? `请创建一份 PDF 文档,内容是以下股票研报。要求:A4页面,中文字体,每只股票独立分页,结论用颜色标注,末尾附免责声明。\n\n${content}` + : `请创建一份 Word (.docx) 文档,内容是以下股票研报。要求:保留标题层级,每只股票独立分页,作战计划用表格,末尾附免责声明。\n\n${content}`, + }], + thinking: { type: "disabled" }, + }); + return completion.choices[0]?.message?.content ?? `${isPDF ? "PDF" : "Word"} 生成完成`; +} + +// ══════════════════════════════════════════════════════════ +// 主流程:个股分析 +// ══════════════════════════════════════════════════════════ + +export async function run(rawInput: unknown): Promise<{ + success: boolean; format: OutputFormat; content: string; + summary: { total: number; buy: number; watch: number; sell: number; errors: number }; + error?: string; +}> { + let parsed: ReturnType; + try { parsed = parseInput(rawInput); } + catch (err: any) { + return { success: false, format: "markdown", content: `❌ 输入解析失败:${err.message}`, + summary: { total: 0, buy: 0, watch: 0, sell: 0, errors: 0 }, error: err.message }; + } + + const { stocks, positions, outputFormat, mode, includeMarketReview, includeGlobalMacro, includeDividend } = parsed; + if (!stocks.length) return { success: false, format: outputFormat, content: "❌ 未提供股票代码", + summary: { total: 0, buy: 0, watch: 0, sell: 0, errors: 0 } }; + + log(`分析 ${stocks.length} 只:${stocks.join(", ")} | 格式:${outputFormat}`); + + // 并行获取宏观数据 + let globalMacro: string | undefined; + let marketOverview: string | undefined; + await Promise.all([ + includeGlobalMacro ? fetchGlobalMacro().then((r) => { globalMacro = r; }) : Promise.resolve(), + includeMarketReview ? fetchMarketOverview().then((r) => { marketOverview = r; }) : Promise.resolve(), + ]); + + // 抓取个股数据 + let stockDataList; + try { + stockDataList = await fetchMultipleStocks(stocks, mode); + log(`数据完成:${stockDataList.filter((d) => !d.error).length}/${stocks.length}`); + } catch (err: any) { + return { success: false, format: outputFormat, content: `❌ 数据抓取失败:${err.message}`, + summary: { total: stocks.length, buy: 0, watch: 0, sell: 0, errors: stocks.length }, error: err.message }; + } + + // LLM 分析 + const analysisResults = await analyzeMultipleStocks(stockDataList, outputFormat, positions, includeDividend); + + // 检查自选股信号变化(如有) + const signals: Record = {}; + for (const r of analysisResults) signals[r.code] = r.verdict; + await checkAlerts(signals).catch(() => {}); // 静默更新,不阻塞主流程 + + // 生成报告 + let content: string; + try { + const markdown = buildFullReport(analysisResults, globalMacro, marketOverview); + if (outputFormat === "pdf") content = await exportToFormat(markdown, "pdf"); + else if (outputFormat === "word") content = await exportToFormat(markdown, "word"); + else content = markdown; + } catch (err: any) { + return { success: false, format: outputFormat, content: `❌ 报告生成失败:${err.message}`, + summary: { total: stocks.length, buy: 0, watch: 0, sell: 0, errors: stocks.length }, error: err.message }; + } + + const summary = { + total: analysisResults.length, + buy: analysisResults.filter((r) => ["买入", "强烈买入"].includes(r.verdict)).length, + watch: analysisResults.filter((r) => r.verdict === "观望").length, + sell: analysisResults.filter((r) => r.verdict === "卖出").length, + errors: stockDataList.filter((d) => d.error).length, + }; + + log(`完成 ✅ 买入:${summary.buy} 观望:${summary.watch} 卖出:${summary.sell}`); + return { success: true, format: outputFormat, content, summary }; +} + +// ══════════════════════════════════════════════════════════ +// 股息分析 +// ══════════════════════════════════════════════════════════ + +export async function runDividend(tickers: string | string[]): Promise { + const codes = Array.isArray(tickers) ? tickers : tickers.split(/[,,\s]+/).filter(Boolean); + log(`股息分析:${codes.join(", ")}`); + const results = await analyzeDividends(codes.map((c) => c.toUpperCase())); + + let output = `# 💰 股息分析报告\n**生成时间:** ${new Date().toLocaleDateString("zh-CN")}\n\n---\n\n`; + for (const r of results) output += formatDividendMarkdown(r) + "\n\n---\n\n"; + output += "_以上数据仅供参考,不构成投资建议。_"; + return output; +} + +// ══════════════════════════════════════════════════════════ +// 传闻扫描 +// ══════════════════════════════════════════════════════════ + +export async function runRumorScan(): Promise { + log("开始传闻扫描..."); + const result = await scanRumors(); + return formatRumorMarkdown(result); +} + +// ══════════════════════════════════════════════════════════ +// 自选股管理 +// ══════════════════════════════════════════════════════════ + +export async function runWatchlistAdd( + ticker: string, + opts: { targetPrice?: number; stopPrice?: number; alertOnSignal?: boolean; notes?: string } = {} +): Promise { + const result = await addToWatchlist(ticker, opts); + return result.message; +} + +export async function runWatchlistRemove(ticker: string): Promise { + const result = await removeFromWatchlist(ticker); + return result.message; +} + +export async function runWatchlistList(): Promise { + const data = await listWatchlist(); + return formatWatchlistMarkdown(data); +} + +export async function runWatchlistCheck(): Promise { + const result = await checkAlerts(); + return formatAlertsMarkdown(result); +} + +// ══════════════════════════════════════════════════════════ +// K线图分析 +// ══════════════════════════════════════════════════════════ + +export async function runChartAnalysis( + stockCode: string, + imageUrlOrBase64: string, + isBase64 = false +): Promise<{ success: boolean; content: string }> { + try { + const result = await analyzeChartImage(imageUrlOrBase64, stockCode, isBase64); + return { success: true, content: result }; + } catch (err: any) { + return { success: false, content: `K线图分析失败:${err.message}` }; + } +} + +// ══════════════════════════════════════════════════════════ +// CLI 调试 +// ══════════════════════════════════════════════════════════ + +async function cli() { + const args = process.argv.slice(2); + const cmd = args[0]; + + if (!cmd || cmd === "--help") { + console.log(` +Stock Analysis Skill CLI + +命令: + analyze <代码,...> [markdown|pdf|word] [--market-review] [--dividend] + dividend <代码,...> + rumors + watch add <代码> [--target 价格] [--stop 价格] [--signal] + watch remove <代码> + watch list + watch check + +示例: + ts-node src/index.ts analyze 600519,00700.HK,AAPL + ts-node src/index.ts analyze AAPL --dividend + ts-node src/index.ts dividend JNJ PG KO + ts-node src/index.ts rumors + ts-node src/index.ts watch add AAPL --target 200 --stop 150 + ts-node src/index.ts watch list + `); + process.exit(0); + } + + if (cmd === "analyze") { + const codes = (args[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean); + const format = (["markdown", "pdf", "word"].find((f) => args.includes(f)) as OutputFormat) ?? "markdown"; + const includeMarketReview = args.includes("--market-review"); + const includeDividend = args.includes("--dividend"); + const result = await run({ stocks: codes, outputFormat: format, includeMarketReview, includeDividend }); + console.log(result.content); + console.log("\n📊 汇总:", result.summary); + + } else if (cmd === "dividend") { + const codes = args.slice(1).filter((a) => !a.startsWith("--")); + console.log(await runDividend(codes)); + + } else if (cmd === "rumors") { + console.log(await runRumorScan()); + + } else if (cmd === "watch") { + const sub = args[1]; + if (sub === "add") { + const ticker = args[2]; + const targetIdx = args.indexOf("--target"); + const stopIdx = args.indexOf("--stop"); + console.log(await runWatchlistAdd(ticker, { + targetPrice: targetIdx >= 0 ? Number(args[targetIdx + 1]) : undefined, + stopPrice: stopIdx >= 0 ? Number(args[stopIdx + 1]) : undefined, + alertOnSignal: args.includes("--signal"), + })); + } else if (sub === "remove") { + console.log(await runWatchlistRemove(args[2])); + } else if (sub === "list") { + console.log(await runWatchlistList()); + } else if (sub === "check") { + console.log(await runWatchlistCheck()); + } + } +} + +if (require.main === module) { + cli().catch((err) => { console.error(err); process.exit(1); }); +} diff --git a/skills/stock-analysis-skill/src/rumorScanner.ts b/skills/stock-analysis-skill/src/rumorScanner.ts new file mode 100755 index 0000000..270fbb4 --- /dev/null +++ b/skills/stock-analysis-skill/src/rumorScanner.ts @@ -0,0 +1,200 @@ +/** + * rumorScanner.ts + * 传闻与早期信号扫描(适配平台 finance skill) + * 替换原版 Twitter bird CLI + Google News,改用 finance skill 新闻 + LLM 提取 + * + * 扫描范围:M&A传闻 / 内部人交易 / 分析师调整 / SEC监管动态 / 市场早期信号 + */ + +import ZAI from "z-ai-web-dev-sdk"; +import { RumorItem, RumorScanResult, RumorType } from "./types"; + +// ── 评分逻辑(移植自原版 calculate_rumor_score)───────── + +function calcImpactScore( + type: RumorType, + text: string, + hasHighEngagement = false +): { score: number; reason: string } { + let score = 1; + const reasons: string[] = []; + + switch (type) { + case "ma": + score += 5; reasons.push("并购/收购类传闻,市场冲击最大"); break; + case "insider": + score += 4; reasons.push("内部人交易信号,可能预示重大动向"); break; + case "analyst": + score += 3; reasons.push("分析师评级调整,影响机构定价"); break; + case "regulatory": + score += 3; reasons.push("监管动态,直接影响经营合规性"); break; + case "earnings": + score += 2; reasons.push("业绩预期变动"); break; + default: + score += 1; + } + + if (/breaking|just in|alert|urgent/i.test(text)) { + score += 2; reasons.push("突发性消息"); + } + if (hasHighEngagement) { + score += 2; reasons.push("市场高度关注"); + } + + return { score: Math.min(10, score), reason: reasons.join(",") }; +} + +// ── 通过 finance skill 获取市场传闻新闻 ────────────────── + +async function fetchRumorNews(zai: any): Promise { + const completion = await zai.chat.completions.create({ + messages: [{ + role: "user", + content: `请获取今日美股市场的以下类型最新资讯,每类最多3条: +1. 并购传闻(merger/acquisition rumors) +2. 内部人买卖动态(insider buying/selling activity) +3. 分析师评级调整(analyst upgrades/downgrades) +4. SEC调查或监管动态 +5. 重大业绩预警或上调 + +以 JSON 格式返回,结构: +[{ + "type": "ma|insider|analyst|regulatory|earnings", + "ticker": "股票代码或null", + "headline": "标题", + "source": "来源", + "sentiment": "positive|negative|neutral", + "date": "YYYY-MM-DD" +}] +只返回 JSON,今日或近2日内的最新信息,不得捏造。`, + }], + thinking: { type: "disabled" }, + }); + return completion.choices[0]?.message?.content ?? "[]"; +} + +// ── LLM 从新闻中提取结构化传闻 ──────────────────────────── + +async function extractRumors(zai: any, rawNews: string): Promise { + try { + const parsed: any[] = JSON.parse(rawNews.replace(/```json|```/g, "").trim()); + + return parsed.map((item) => { + const type = (item.type as RumorType) ?? "general"; + const text = item.headline ?? ""; + const { score, reason } = calcImpactScore(type, text); + + return { + type, + ticker: item.ticker ?? null, + headline: text, + source: item.source ?? "finance", + impactScore: score, + impactReason: reason, + sentiment: item.sentiment ?? "neutral", + date: item.date ?? new Date().toISOString().slice(0, 10), + } as RumorItem; + }).sort((a, b) => b.impactScore - a.impactScore); + } catch { + return []; + } +} + +// ── 汇总最受关注的股票 ──────────────────────────────────── + +function aggregateTopTickers(rumors: RumorItem[]): { ticker: string; count: number }[] { + const counts: Record = {}; + for (const r of rumors) { + if (r.ticker) { + counts[r.ticker] = (counts[r.ticker] ?? 0) + 1; + } + } + return Object.entries(counts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10) + .map(([ticker, count]) => ({ ticker, count })); +} + +// ── 主扫描函数 ──────────────────────────────────────────── + +export async function scanRumors(): Promise { + const zai = await ZAI.create(); + const scannedAt = new Date().toISOString(); + + let rumors: RumorItem[] = []; + + try { + const rawNews = await fetchRumorNews(zai); + rumors = await extractRumors(zai, rawNews); + } catch (err: any) { + console.error("[scanRumors] 失败:", err.message); + } + + const topTickers = aggregateTopTickers(rumors); + + // 生成摘要 + const maCount = rumors.filter((r) => r.type === "ma").length; + const insiderCount = rumors.filter((r) => r.type === "insider").length; + const analystCount = rumors.filter((r) => r.type === "analyst").length; + + const summary = rumors.length === 0 + ? "今日暂无重大传闻或早期信号。" + : `共扫描到 ${rumors.length} 条信号:并购传闻 ${maCount} 条,内部人动态 ${insiderCount} 条,分析师调整 ${analystCount} 条。${topTickers.length > 0 ? `最受关注:${topTickers.slice(0, 3).map((t) => `$${t.ticker}`).join("、")}。` : ""}`; + + return { scannedAt, rumors, topTickers, summary }; +} + +// ── 格式化输出 ──────────────────────────────────────────── + +export function formatRumorMarkdown(result: RumorScanResult): string { + const typeLabel: Record = { + ma: "🏢 并购传闻", + insider: "👔 内部人动态", + analyst: "📊 分析师调整", + regulatory: "⚖️ 监管动态", + earnings: "📈 业绩预期", + general: "📰 市场信号", + }; + + const sentimentEmoji: Record = { + positive: "🟢", negative: "🔴", neutral: "⚪", + }; + + let md = `## 🔮 传闻与早期信号扫描 +**扫描时间:** ${new Date(result.scannedAt).toLocaleString("zh-CN")} +**${result.summary}** + +`; + + if (result.rumors.length === 0) { + md += "_今日暂无重大传闻。_\n"; + return md; + } + + // 按类型分组输出 + const grouped: Partial> = {}; + for (const r of result.rumors) { + if (!grouped[r.type]) grouped[r.type] = []; + grouped[r.type]!.push(r); + } + + for (const [type, items] of Object.entries(grouped)) { + md += `### ${typeLabel[type as RumorType] ?? type}\n\n`; + for (const item of items!.slice(0, 3)) { + md += `**[冲击 ${item.impactScore}/10]** ${sentimentEmoji[item.sentiment]} ${item.headline}\n`; + if (item.ticker) md += `> 相关标的:$${item.ticker}\n`; + md += `> 来源:${item.source} | ${item.date} | ${item.impactReason}\n\n`; + } + } + + // 热度排行 + if (result.topTickers.length > 0) { + md += `### 📊 传闻热度排行\n`; + md += result.topTickers.map((t) => + `- $${t.ticker}:被提及 ${t.count} 次` + ).join("\n"); + md += "\n"; + } + + return md; +} diff --git a/skills/stock-analysis-skill/src/types.ts b/skills/stock-analysis-skill/src/types.ts new file mode 100755 index 0000000..6edf484 --- /dev/null +++ b/skills/stock-analysis-skill/src/types.ts @@ -0,0 +1,167 @@ +/** + * types.ts — 统一类型定义 + */ + +export type Market = "CN" | "HK" | "US"; +export type FetchMode = "full" | "quote"; +export type OutputFormat = "markdown" | "pdf" | "word"; +export type Verdict = "买入" | "强烈买入" | "观望" | "卖出"; +export type PositionStatus = "empty" | "holding"; + +// ── 持仓信息 ────────────────────────────────────────────── + +export interface PositionInfo { + status: PositionStatus; + cost?: number; + shares?: number; +} + +// ── 股票数据 ────────────────────────────────────────────── + +export interface StockData { + code: string; + market: Market; + timestamp: string; + name?: string; + price?: number | null; + prev_close?: number | null; + open?: number | null; + high?: number | null; + low?: number | null; + change_pct?: number | null; + change_amount?: number | null; + amplitude?: number | null; + volume?: number | null; + amount?: number | null; + turnover?: number | null; + volume_ratio?: number | null; + market_cap?: number | null; + pe_ttm?: number | null; + pb?: number | null; + high_52w?: number | null; + low_52w?: number | null; + dividend_yield?: number | null; + ma5?: number | null; + ma10?: number | null; + ma20?: number | null; + bias_pct?: number | null; + trend?: string; + overbought_warning?: boolean; + support?: number | null; + resistance?: number | null; + rsi?: number | null; + kline_recent_10?: Record[]; + profit_ratio?: number | null; + avg_cost?: number | null; + chip_concentration?: number | null; + roe?: number | null; + gross_margin?: number | null; + net_margin?: number | null; + debt_ratio?: number | null; + eps?: number | null; + revenue_growth?: number | null; + main_net?: number | null; + main_net_pct?: number | null; + error?: string; +} + +// ── 分析结果 ────────────────────────────────────────────── + +export interface AnalysisResult { + code: string; + market: Market; + name: string; + verdict: Verdict; + analysis: string; + warnings: string[]; + outputFormat: OutputFormat; + generatedAt: string; +} + +// ── 股息分析 ────────────────────────────────────────────── + +export type PayoutStatus = "safe" | "moderate" | "high" | "unsustainable" | "no_dividend" | "unknown"; +export type IncomeRating = "excellent" | "good" | "moderate" | "poor" | "no_dividend"; + +export interface DividendAnalysis { + ticker: string; + name: string; + currentPrice: number | null; + dividendYield: number | null; // % + annualDividend: number | null; + payoutRatio: number | null; // % + payoutStatus: PayoutStatus; + dividendGrowth5y: number | null; // CAGR % + consecutiveYears: number | null; + exDividendDate: string | null; + paymentFrequency: string | null; + safetyScore: number; // 0-100 + safetyFactors: string[]; + incomeRating: IncomeRating; + dividendHistory: { year: number; total: number }[]; + summary: string; +} + +// ── 传闻扫描 ────────────────────────────────────────────── + +export type RumorType = "ma" | "insider" | "analyst" | "regulatory" | "earnings" | "general"; + +export interface RumorItem { + type: RumorType; + ticker: string | null; // 相关股票代码(可能为 null) + headline: string; + source: string; + impactScore: number; // 1-10 + impactReason: string; + sentiment: "positive" | "negative" | "neutral"; + date: string; +} + +export interface RumorScanResult { + scannedAt: string; + rumors: RumorItem[]; + topTickers: { ticker: string; count: number }[]; // 最受关注的股票 + summary: string; +} + +// ── 自选股 ──────────────────────────────────────────────── + +export type AlertType = "target_hit" | "stop_hit" | "signal_change"; + +export interface WatchlistItem { + ticker: string; + name?: string; + market: Market; + addedAt: string; + priceAtAdd: number | null; + targetPrice: number | null; + stopPrice: number | null; + alertOnSignal: boolean; + lastSignal: Verdict | null; + lastCheck: string | null; + notes: string | null; +} + +export interface WatchlistAlert { + ticker: string; + alertType: AlertType; + message: string; + currentPrice: number; + triggerValue: number | string; + timestamp: string; +} + +// ── Skill 入参 ──────────────────────────────────────────── + +export interface StockInput { + code: string; + position?: PositionInfo; +} + +export interface SkillInput { + stocks?: (string | StockInput)[]; + outputFormat?: OutputFormat; + mode?: FetchMode; + includeMarketReview?: boolean; + includeGlobalMacro?: boolean; +} diff --git a/skills/stock-analysis-skill/src/watchlist.ts b/skills/stock-analysis-skill/src/watchlist.ts new file mode 100755 index 0000000..b004851 --- /dev/null +++ b/skills/stock-analysis-skill/src/watchlist.ts @@ -0,0 +1,292 @@ +/** + * watchlist.ts + * 自选股管理 + 价格提醒(使用平台 storage 持久化) + * 移植原版三种提醒类型:目标价 / 止损价 / 信号变化 + */ + +import ZAI from "z-ai-web-dev-sdk"; +import { WatchlistItem, WatchlistAlert, Market, Verdict } from "./types"; + +const STORAGE_KEY = "watchlist-data"; + +// ── Storage 封装 ────────────────────────────────────────── + +async function loadWatchlist(): Promise { + try { + const result = await (window as any).storage?.get(STORAGE_KEY); + if (result?.value) return JSON.parse(result.value) as WatchlistItem[]; + } catch {} + return []; +} + +async function saveWatchlist(items: WatchlistItem[]): Promise { + try { + await (window as any).storage?.set(STORAGE_KEY, JSON.stringify(items)); + } catch (err: any) { + console.error("[watchlist] 保存失败:", err.message); + } +} + +// ── 获取当前价格 ────────────────────────────────────────── + +async function fetchCurrentPrice(ticker: string): Promise { + try { + const zai = await ZAI.create(); + const completion = await zai.chat.completions.create({ + messages: [{ + role: "user", + content: `查询 ${ticker} 当前股价,只返回一个 JSON:{"price": 数字},不要其他内容。`, + }], + thinking: { type: "disabled" }, + }); + const raw = completion.choices[0]?.message?.content ?? "{}"; + const parsed = JSON.parse(raw.replace(/```json|```/g, "").trim()); + return typeof parsed.price === "number" ? parsed.price : null; + } catch { + return null; + } +} + +function detectMarket(code: string): Market { + if (/^\d{6}$/.test(code)) return "CN"; + if (/^\d{4,5}\.HK$/i.test(code)) return "HK"; + return "US"; +} + +// ── 添加自选股 ──────────────────────────────────────────── + +export async function addToWatchlist( + ticker: string, + opts: { + targetPrice?: number; + stopPrice?: number; + alertOnSignal?: boolean; + notes?: string; + } = {} +): Promise<{ success: boolean; action: string; message: string; item?: WatchlistItem }> { + ticker = ticker.toUpperCase(); + const currentPrice = await fetchCurrentPrice(ticker); + + if (currentPrice === null) { + return { success: false, action: "error", message: `无法获取 ${ticker} 的价格,请确认代码正确` }; + } + + const watchlist = await loadWatchlist(); + const existingIdx = watchlist.findIndex((i) => i.ticker === ticker); + + if (existingIdx >= 0) { + // 更新已有记录 + const existing = watchlist[existingIdx]; + if (opts.targetPrice !== undefined) existing.targetPrice = opts.targetPrice; + if (opts.stopPrice !== undefined) existing.stopPrice = opts.stopPrice; + if (opts.alertOnSignal !== undefined) existing.alertOnSignal = opts.alertOnSignal; + if (opts.notes !== undefined) existing.notes = opts.notes; + watchlist[existingIdx] = existing; + await saveWatchlist(watchlist); + + return { success: true, action: "updated", message: `已更新 ${ticker} 的自选股设置`, item: existing }; + } + + // 新增 + const item: WatchlistItem = { + ticker, + market: detectMarket(ticker), + addedAt: new Date().toISOString(), + priceAtAdd: currentPrice, + targetPrice: opts.targetPrice ?? null, + stopPrice: opts.stopPrice ?? null, + alertOnSignal: opts.alertOnSignal ?? false, + lastSignal: null, + lastCheck: null, + notes: opts.notes ?? null, + }; + + watchlist.push(item); + await saveWatchlist(watchlist); + + const alertDesc = [ + opts.targetPrice ? `目标价 $${opts.targetPrice}` : null, + opts.stopPrice ? `止损价 $${opts.stopPrice}` : null, + opts.alertOnSignal ? "信号变化提醒" : null, + ].filter(Boolean).join(","); + + return { + success: true, action: "added", + message: `已添加 ${ticker} 到自选股(当前价 $${currentPrice}${alertDesc ? `,设置了:${alertDesc}` : ""})`, + item, + }; +} + +// ── 删除自选股 ──────────────────────────────────────────── + +export async function removeFromWatchlist( + ticker: string +): Promise<{ success: boolean; message: string }> { + ticker = ticker.toUpperCase(); + const watchlist = await loadWatchlist(); + const filtered = watchlist.filter((i) => i.ticker !== ticker); + + if (filtered.length === watchlist.length) { + return { success: false, message: `${ticker} 不在自选股列表中` }; + } + + await saveWatchlist(filtered); + return { success: true, message: `已从自选股中删除 ${ticker}` }; +} + +// ── 查看自选股列表 ──────────────────────────────────────── + +export async function listWatchlist(): Promise<{ + items: Array; + count: number; +}> { + const watchlist = await loadWatchlist(); + if (!watchlist.length) return { items: [], count: 0 }; + + const items = await Promise.all(watchlist.map(async (item) => { + const currentPrice = await fetchCurrentPrice(item.ticker); + + const changePct = currentPrice && item.priceAtAdd + ? parseFloat((((currentPrice - item.priceAtAdd) / item.priceAtAdd) * 100).toFixed(2)) + : null; + + const toTargetPct = currentPrice && item.targetPrice + ? parseFloat((((item.targetPrice - currentPrice) / currentPrice) * 100).toFixed(2)) + : null; + + const toStopPct = currentPrice && item.stopPrice + ? parseFloat((((item.stopPrice - currentPrice) / currentPrice) * 100).toFixed(2)) + : null; + + return { ...item, currentPrice, changePct, toTargetPct, toStopPct }; + })); + + return { items, count: items.length }; +} + +// ── 检查提醒 ────────────────────────────────────────────── + +export async function checkAlerts( + currentSignals?: Record +): Promise<{ alerts: WatchlistAlert[]; count: number }> { + const watchlist = await loadWatchlist(); + const alerts: WatchlistAlert[] = []; + const now = new Date().toISOString(); + + for (const item of watchlist) { + const currentPrice = await fetchCurrentPrice(item.ticker); + if (currentPrice === null) continue; + + // 目标价提醒 + if (item.targetPrice && currentPrice >= item.targetPrice) { + alerts.push({ + ticker: item.ticker, + alertType: "target_hit", + message: `🎯 ${item.ticker} 已触达目标价!当前 $${currentPrice.toFixed(2)} ≥ 目标 $${item.targetPrice}`, + currentPrice, + triggerValue: item.targetPrice, + timestamp: now, + }); + } + + // 止损价提醒 + if (item.stopPrice && currentPrice <= item.stopPrice) { + alerts.push({ + ticker: item.ticker, + alertType: "stop_hit", + message: `🛑 ${item.ticker} 已触达止损价!当前 $${currentPrice.toFixed(2)} ≤ 止损 $${item.stopPrice}`, + currentPrice, + triggerValue: item.stopPrice, + timestamp: now, + }); + } + + // 信号变化提醒 + if (item.alertOnSignal && currentSignals?.[item.ticker]) { + const newSignal = currentSignals[item.ticker]; + if (item.lastSignal && newSignal !== item.lastSignal) { + alerts.push({ + ticker: item.ticker, + alertType: "signal_change", + message: `📊 ${item.ticker} 信号变化:${item.lastSignal} → ${newSignal}`, + currentPrice, + triggerValue: `${item.lastSignal} → ${newSignal}`, + timestamp: now, + }); + } + // 更新最新信号 + item.lastSignal = newSignal; + } + + item.lastCheck = now; + } + + // 保存更新后的 lastSignal 和 lastCheck + await saveWatchlist(watchlist); + + return { alerts, count: alerts.length }; +} + +// ── 格式化自选股列表(Markdown)───────────────────────── + +export function formatWatchlistMarkdown(data: Awaited>): string { + if (data.count === 0) { + return `## 📋 自选股列表\n\n_自选股为空。使用 \`/stock_watch AAPL\` 添加股票。_\n`; + } + + let md = `## 📋 自选股列表(共 ${data.count} 只)\n\n`; + md += `| 代码 | 当前价 | 较买入 | 目标价 | 止损价 | 距目标 | 最新信号 |\n`; + md += `|------|--------|--------|--------|--------|--------|----------|\n`; + + for (const item of data.items) { + const price = item.currentPrice ? `$${item.currentPrice.toFixed(2)}` : "暂缺"; + const change = item.changePct !== null + ? `${item.changePct > 0 ? "🟢+" : "🔴"}${item.changePct.toFixed(2)}%` + : "—"; + const target = item.targetPrice ? `$${item.targetPrice}` : "—"; + const stop = item.stopPrice ? `$${item.stopPrice}` : "—"; + const toTarget = item.toTargetPct !== null ? `${item.toTargetPct > 0 ? "+" : ""}${item.toTargetPct.toFixed(1)}%` : "—"; + const signal = item.lastSignal ?? "—"; + + md += `| ${item.ticker} | ${price} | ${change} | ${target} | ${stop} | ${toTarget} | ${signal} |\n`; + } + + // 已触发的提醒 + const triggered = data.items.filter( + (i) => (i.targetPrice && i.currentPrice && i.currentPrice >= i.targetPrice) || + (i.stopPrice && i.currentPrice && i.currentPrice <= i.stopPrice) + ); + + if (triggered.length > 0) { + md += `\n### ⚡ 已触发提醒\n`; + for (const item of triggered) { + if (item.targetPrice && item.currentPrice && item.currentPrice >= item.targetPrice) { + md += `- 🎯 **${item.ticker}** 已达目标价 $${item.targetPrice}\n`; + } + if (item.stopPrice && item.currentPrice && item.currentPrice <= item.stopPrice) { + md += `- 🛑 **${item.ticker}** 已触止损 $${item.stopPrice}\n`; + } + } + } + + return md; +} + +// ── 格式化提醒结果(Markdown)──────────────────────────── + +export function formatAlertsMarkdown(result: Awaited>): string { + if (result.count === 0) { + return `## 🔔 自选股提醒检查\n\n_当前没有触发任何提醒。_\n`; + } + + let md = `## 🔔 自选股提醒(${result.count} 条触发)\n\n`; + for (const alert of result.alerts) { + md += `- ${alert.message}\n`; + } + return md; +} diff --git a/skills/stock-analysis-skill/tsconfig.json b/skills/stock-analysis-skill/tsconfig.json new file mode 100755 index 0000000..df3ba65 --- /dev/null +++ b/skills/stock-analysis-skill/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/skills/storyboard-manager/SKILL.md b/skills/storyboard-manager/SKILL.md new file mode 100755 index 0000000..a413831 --- /dev/null +++ b/skills/storyboard-manager/SKILL.md @@ -0,0 +1,532 @@ +--- +name: storyboard-manager +description: Assist writers with story planning, character development, plot structuring, chapter writing, timeline tracking, and consistency checking. Use this skill when working with creative writing projects organized in folders containing characters, chapters, story planning documents, and summaries. Trigger this skill for tasks like "Help me develop this character," "Write the next chapter," "Check consistency across my story," or "Track the timeline of events." +--- + +# Storyboard Manager + +## Overview + +The Storyboard Manager skill equips Claude with specialized knowledge and tools for creative writing workflows. It provides frameworks for character development, story structure patterns, automated timeline tracking, and consistency checking across narrative projects. This skill automatically adapts to various storyboard folder structures while maintaining best practices for novel, screenplay, and serialized fiction writing. + +## Core Capabilities + +The skill provides four main capabilities: + +### 1. Character Development & Management +Support creating deep, consistent character profiles with backstories, arcs, and relationships. + +### 2. Story Planning & Structure +Guide plot development using established frameworks (Three-Act, Hero's Journey, Save the Cat, etc.) and help organize narrative elements. + +### 3. Chapter & Scene Writing +Generate chapter content, scene breakdowns, and dialogue that maintains consistency with established characters and plot. + +### 4. Timeline Tracking & Consistency Checking +Use automated tools to verify chronological consistency, character continuity, and world-building coherence. + +## Detecting Project Structure + +The Storyboard Manager automatically detects and adapts to various folder organizations. Look for these common directory patterns: + +**Character folders:** `characters/`, `Characters/`, `cast/`, `Cast/` +**Chapter folders:** `chapters/`, `Chapters/`, `scenes/`, `Scenes/`, `story/` +**Planning folders:** `story-planning/`, `planning/`, `outline/`, `notes/` +**Summary files:** `summary.md`, `README.md`, `overview.md` + +When triggered, scan the project root to identify the structure and adjust workflows accordingly. If no standard structure exists, recommend organizing files using the pattern: `characters/`, `chapters/`, `story-planning/`, and `summary.md`. + +## Workflow Decision Tree + +Use this decision tree to determine the appropriate workflow: + +``` +User Request +├─ Character-related? ("develop character," "create backstory," "character arc") +│ └─ → Character Development Workflow +│ +├─ Planning/Plot? ("outline story," "plan act 2," "plot structure") +│ └─ → Story Planning Workflow +│ +├─ Writing content? ("write chapter," "generate scene," "continue story") +│ └─ → Chapter/Scene Writing Workflow +│ +└─ Checking/Analysis? ("check consistency," "track timeline," "find contradictions") + ├─ Timeline? → Use timeline_tracker.py script + └─ Consistency? → Use consistency_checker.py script +``` + +## Character Development Workflow + +### Step 1: Gather Context + +Before developing a character, read existing character files to understand: +- Established naming conventions and profile format +- Existing characters and relationships +- Story genre and tone +- Character archetypes already in use + +Use the Read tool to examine existing character files in the characters directory. + +### Step 2: Access Character Development Framework + +When detailed character guidance is needed, read `references/character_development.md` which contains: +- Core character elements (personality, motivation, goals) +- Backstory framework (ghost/wound, formative relationships) +- Character arc types (positive change, flat, negative) +- Relationship dynamics +- Voice development techniques +- Consistency guidelines + +To efficiently find specific guidance, use Grep to search for relevant sections: +```bash +# Example: Find guidance on character arcs +grep -i "character arc" references/character_development.md +``` + +### Step 3: Develop Character Profile + +Create or enhance character profiles with these essential elements: + +**Basic Information** +- Name, age, role, physical appearance +- Key personality traits (both positive and negative) + +**Background** +- Origin and formative experiences +- Ghost/wound that shapes their behavior +- Key relationships and family dynamics + +**Character Arc** +- Starting belief or flaw +- Want vs. Need (external goal vs. internal growth) +- Transformation journey +- End state + +**Relationships** +- Connections to other characters +- Dynamic types (ally, rival, mentor, etc.) +- How relationships evolve + +**Unique Elements** +- Abilities, skills, or special knowledge +- Secrets or hidden aspects +- Voice/speech patterns +- Character-specific quirks + +### Step 4: Ensure Consistency + +Cross-reference with: +- Existing character profiles (avoid redundancy in roles/traits) +- Story planning documents (ensure alignment with plot needs) +- Summary/overview (match genre and tone) + +### Step 5: Create or Update File + +Write the character profile to `characters/[character-name].md` using markdown format. Match the existing style and structure found in other character files. + +## Story Planning Workflow + +### Step 1: Assess Current Planning State + +Read existing planning documents to understand: +- Story concept and premise +- Established plot points or outline +- Target audience and genre +- Themes and central questions +- Planned structure (if any) + +Look in folders like `story-planning/`, `outline/`, or files like `summary.md`. + +### Step 2: Access Story Structure Reference + +For detailed structural guidance, read `references/story_structures.md` which includes: +- Three-Act Structure +- Hero's Journey (Campbell's Monomyth) +- Save the Cat Beat Sheet +- Character arc templates +- Scene structure components +- Pacing guidelines by genre +- Subplot integration techniques +- Genre-specific structures + +Use Grep to find specific frameworks: +```bash +# Example: Find Three-Act Structure details +grep -A 20 "Three-Act Structure" references/story_structures.md +``` + +### Step 3: Determine Structure Needs + +Based on the user's request and story genre, recommend appropriate frameworks: + +- **Thriller/Mystery**: Three-Act with strong midpoint reversal +- **Fantasy/Adventure**: Hero's Journey for quest narratives +- **YA/Contemporary**: Save the Cat for tight emotional beats +- **Literary Fiction**: Focus on character arc structure +- **Romance**: Genre-specific structure with relationship beats + +### Step 4: Develop Planning Document + +Create or enhance planning documents with: + +**Story Overview** +- Premise in 2-3 sentences +- Genre, target audience, tone +- Central themes and questions + +**Plot Structure** +- Act/chapter breakdown with key events +- Inciting incident and plot points +- Midpoint twist or revelation +- Climax and resolution + +**Character Arcs** +- How each main character transforms +- Arc integration with plot beats + +**World-Building Elements** (if applicable) +- Setting and locations +- Magic systems or technology +- Social structures or rules +- Historical context + +**Timeline** +- Story duration +- Key event sequence +- Pacing considerations + +### Step 5: Create Planning File + +Write planning documents to `story-planning/[document-name].md`. Use clear hierarchical structure with markdown headers for easy navigation. + +## Chapter & Scene Writing Workflow + +### Step 1: Gather Story Context + +Before writing any content, comprehensively read: + +**Character Files**: All relevant character profiles to understand voices, motivations, arcs +**Planning Documents**: Story structure, plot points, current story position +**Previous Chapters**: Recent chapters to maintain continuity (read at least 1-2 prior chapters) +**Summary**: Overall story premise and themes + +This ensures the new content aligns with established elements. + +### Step 2: Identify Chapter Requirements + +Determine: +- **Story Position**: Where does this fit in the overall structure? +- **POV Character**: Whose perspective? +- **Scene Goal**: What does the POV character want in this scene? +- **Conflict**: What opposes their goal? +- **Outcome**: How does the scene end? (typically with a complication) +- **Character Development**: What arc beats occur here? +- **Plot Advancement**: What story questions are raised or answered? + +### Step 3: Structure the Chapter + +Apply scene structure components: + +**Scene (Action)** +1. Goal - What the POV character pursues +2. Conflict - Opposition encountered +3. Disaster - Negative outcome that propels forward + +**Sequel (Reaction)** +1. Reaction - Emotional response to disaster +2. Dilemma - Processing options +3. Decision - Choice leading to next goal + +Alternate between high-tension (action, conflict) and low-tension (reflection, world-building) beats for pacing. + +### Step 4: Write with Character Consistency + +Maintain character voice by referencing: +- Established personality traits +- Speech patterns and vocabulary +- Behavioral patterns (under stress, when happy, decision-making style) +- Current position in character arc +- Relationships with other characters present + +### Step 5: Integrate Timeline Markers + +Include timeline references to maintain chronological clarity: +- Explicit markers: "Day 3," "Two weeks later" +- Implicit markers: Time of day, seasonal cues, event references +- Format: `**Timeline:** Day 5, Evening` in chapter header or as section break + +### Step 6: Create Chapter File + +Write chapter content to `chapters/chapter-[number].md` or `chapters/[chapter-name].md`. Include: + +**Chapter Header** +```markdown +# Chapter [Number]: [Optional Title] + +**Timeline:** [When this occurs] +**POV:** [Character name] +**Location:** [Where this takes place] +``` + +**Chapter Content** +- Scene-by-scene breakdown +- Dialogue and action +- Character thoughts (for POV character) +- Descriptive elements + +### Step 7: Note Continuity Elements + +After writing, document any new information introduced: +- Character revelations or development +- Plot points or clues +- World-building details +- Timeline events + +This helps maintain consistency in future chapters. + +## Timeline Tracking + +### When to Use Timeline Tracking + +Invoke the timeline tracker when: +- User requests timeline analysis or event sequencing +- Checking chronological consistency +- Planning event order across chapters +- Identifying unmarked time periods + +### Running the Timeline Tracker + +Execute the script from the project root: + +```bash +python3 .claude/skills/storyboard-manager/scripts/timeline_tracker.py . --output markdown +``` + +**Output format options:** +- `markdown` - Human-readable report (default) +- `json` - Structured data for further processing + +### Understanding Timeline Output + +The script provides: + +**Statistics** +- Total events tracked +- Total characters appearing +- Events per character + +**Timeline View** +- Chronological sequence of events +- Chapter/scene locations +- Characters present in each event +- Preview of event content + +**Warnings** +- Events without timeline markers +- Characters mentioned but not defined in character files + +### Acting on Timeline Results + +After running the tracker: + +1. **Review warnings** - Address missing timeline markers by adding them to chapters +2. **Check sequence** - Verify events occur in logical order +3. **Identify gaps** - Look for time periods without events +4. **Character tracking** - Ensure characters appear consistently with their arc + +Add timeline markers to chapters where missing: +```markdown +**Timeline:** Day 7, Morning +``` + +Or use inline markers: +```markdown +Three days had passed since the incident... +``` + +## Consistency Checking + +### When to Use Consistency Checking + +Invoke the consistency checker when: +- User requests consistency analysis +- Before finalizing chapters or acts +- After making significant character or plot changes +- When tracking contradictions or errors + +### Running the Consistency Checker + +Execute the script from the project root: + +```bash +python3 .claude/skills/storyboard-manager/scripts/consistency_checker.py . --output markdown +``` + +**Output format options:** +- `markdown` - Human-readable report with issue details (default) +- `json` - Structured data for programmatic analysis + +### Understanding Consistency Output + +The script identifies issues in three severity levels: + +**Critical (🔴)** +- Major contradictions requiring immediate attention +- Character appearing after death +- Fundamental plot contradictions + +**Warning (⚠️)** +- Potential inconsistencies to review +- Age discrepancies +- Physical description contradictions +- Relationship conflicts + +**Info (ℹ️)** +- Minor issues or variations +- Name capitalization inconsistencies +- Stylistic variations + +### Acting on Consistency Results + +For each issue reported: + +1. **Read flagged locations** - Review the specific files mentioned +2. **Determine truth** - Decide which version is correct (usually character profile is authoritative) +3. **Update files** - Fix contradictions using the Edit tool +4. **Re-run checker** - Verify fixes resolved the issues + +**Example workflow for character age inconsistency:** +```markdown +Issue: Age inconsistency for Maya +- Profile: 18 years old +- Chapter 3: mentions "21-year-old Maya" + +Fix: Edit chapter-3.md to change "21-year-old" to "18-year-old" +``` + +### Consistency Checking Limitations + +The automated checker catches: +- Physical attribute contradictions +- Age discrepancies +- Name variations +- Basic world-building facts + +The checker cannot catch: +- Subtle personality inconsistencies +- Complex plot logic errors +- Thematic contradictions +- Nuanced relationship changes + +Manual review is still essential for deep consistency. + +## Best Practices + +### Progressive Context Loading + +Don't load all reference files at once. Instead: +1. Scan project structure first +2. Read only relevant character files for the current task +3. Access reference documentation only when specific guidance is needed +4. Use Grep to find specific sections in large reference files + +### Maintaining Genre Voice + +Match the story's established tone: +- **YA**: Present tense, immediate emotional connection, contemporary language +- **Fantasy**: Rich descriptive language, world-building integration +- **Thriller**: Short sentences, high tension, sensory details +- **Literary**: Complex prose, internal reflection, symbolic elements + +Reference the summary.md to identify target audience and adjust accordingly. + +### Character Arc Integration + +Every chapter should serve character arcs: +- Track where each character is in their arc +- Show incremental change, not sudden transformation +- Use plot events to test character beliefs +- Demonstrate growth through choices and behavior + +### Balancing Show vs. Tell + +For narrative writing: +- **Show** emotions through actions, dialogue, physical reactions +- **Tell** to compress time, provide necessary information efficiently +- Use character-filtered description (what would this POV character notice?) + +### Handling Multiple POV + +When stories have multiple perspectives: +- Create distinct voices for each POV character +- Ensure each POV section advances both that character's arc and the plot +- Vary sentence structure and vocabulary by character +- Track what each character knows vs. doesn't know + +## Common User Requests & Responses + +### "Help me develop a character backstory" +1. Read existing character files for context +2. Read the character profile (if exists) to enhance +3. Access character_development.md reference for backstory framework +4. Create detailed backstory covering: ghost/wound, formative relationships, key history +5. Integrate with their character arc and story role + +### "Write the next chapter" +1. Read summary.md and story planning documents +2. Read all character profiles for characters appearing in chapter +3. Read previous 2 chapters for continuity +4. Identify chapter position in story structure +5. Write chapter with scene/sequel structure +6. Include timeline markers and POV/location headers + +### "Outline Act 2" +1. Read summary and any existing planning documents +2. Access story_structures.md for structural guidance +3. Identify act 2 requirements (complications, midpoint, rising tension) +4. Create beat-by-beat outline aligned with character arcs +5. Note how plot and character arcs intersect + +### "Check my story for consistency" +1. Run consistency_checker.py script +2. Review output identifying issues +3. Read flagged files to understand contradictions +4. Recommend specific fixes for each issue +5. Offer to make edits if user confirms + +### "Track the timeline of my story" +1. Run timeline_tracker.py script +2. Review output showing event sequence +3. Identify gaps or inconsistencies in chronology +4. Recommend adding timeline markers where missing +5. Provide timeline summary organized by character or chapter + +### "What structure should I use for my thriller?" +1. Access story_structures.md reference +2. Recommend Three-Act Structure or Save the Cat +3. Explain thriller-specific requirements (escalating tension, ticking clock) +4. Provide beat sheet adapted to their story concept +5. Offer to create detailed planning document + +## Resources + +### scripts/timeline_tracker.py +Python script that analyzes markdown files to extract and organize timeline events. Tracks character appearances, identifies time markers, groups events chronologically, and flags consistency issues. + +**Usage:** Run from project root with `python3 .claude/skills/storyboard-manager/scripts/timeline_tracker.py .` + +### scripts/consistency_checker.py +Python script that detects inconsistencies in character details, physical descriptions, ages, names, and world-building facts across all story files. Outputs severity-ranked issues with file locations. + +**Usage:** Run from project root with `python3 .claude/skills/storyboard-manager/scripts/consistency_checker.py .` + +### references/character_development.md +Comprehensive framework for creating multi-dimensional characters including core elements, backstory structure, arc types, relationship dynamics, voice development, and consistency guidelines. + +**Load when:** Developing new characters, enhancing existing profiles, resolving character consistency issues, or planning character arcs. + +### references/story_structures.md +Detailed reference covering major story structures (Three-Act, Hero's Journey, Save the Cat), character arc templates, scene structure, pacing guidelines, plot development techniques, and genre-specific structures. + +**Load when:** Planning story outline, structuring acts, organizing plot beats, determining pacing, or applying specific narrative frameworks. diff --git a/skills/storyboard-manager/index.js b/skills/storyboard-manager/index.js new file mode 100755 index 0000000..00ec351 --- /dev/null +++ b/skills/storyboard-manager/index.js @@ -0,0 +1,9 @@ +export default async function storyboard_manager(input) { + console.log("🧠 Running skill: storyboard-manager"); + + // TODO: implement actual logic for this skill + return { + message: "Skill 'storyboard-manager' executed successfully!", + input + }; +} diff --git a/skills/storyboard-manager/package.json b/skills/storyboard-manager/package.json new file mode 100755 index 0000000..d004931 --- /dev/null +++ b/skills/storyboard-manager/package.json @@ -0,0 +1,11 @@ +{ + "name": "@ai-labs-claude-skills/storyboard-manager", + "version": "1.0.0", + "description": "Claude AI skill: storyboard-manager", + "main": "index.js", + "files": [ + "." + ], + "license": "MIT", + "author": "AI Labs" +} \ No newline at end of file diff --git a/skills/storyboard-manager/references/character_development.md b/skills/storyboard-manager/references/character_development.md new file mode 100755 index 0000000..4c78850 --- /dev/null +++ b/skills/storyboard-manager/references/character_development.md @@ -0,0 +1,232 @@ +# Character Development Reference + +This reference provides frameworks for creating compelling, multi-dimensional characters. + +## Core Character Elements + +### Basic Profile +- **Name**: Full name, nicknames, name meaning +- **Age**: Chronological and how they present +- **Physical Description**: Distinguishing features, style, mannerisms +- **Role**: Protagonist, antagonist, supporting, mentor, etc. +- **Archetype**: Hero, mentor, trickster, everyman, etc. + +### Personality Dimensions +- **Temperament**: Sanguine, choleric, melancholic, phlegmatic +- **Traits**: 3-5 defining characteristics (both positive and negative) +- **Quirks**: Unique habits or behaviors +- **Speech Patterns**: How they talk, vocabulary, accent +- **Sense of Humor**: Type and style + +### Motivation & Goals +- **External Goal**: What they're trying to achieve (plot-level) +- **Internal Goal**: What they're trying to become (character arc) +- **Motivation**: Why they want these things +- **Stakes**: What happens if they fail +- **Misbelief/Lie**: False belief holding them back + +## Character Backstory Framework + +### The Ghost (Past Wound) +- **Traumatic Event**: What happened in their past +- **Age When It Occurred**: How it shaped their development +- **Who Was Involved**: Other characters connected to trauma +- **How It Changed Them**: Before and after personality +- **Coping Mechanisms**: How they deal with the wound + +### Formative Relationships +- **Family Dynamics**: Parents, siblings, family structure +- **Key Friendships**: Influences from peers +- **Romantic History**: Past relationships and their impact +- **Mentors/Role Models**: Who shaped their values +- **Enemies/Rivals**: Antagonistic relationships that defined them + +### Life History +- **Childhood**: Key events, family situation, early personality +- **Adolescence**: Identity formation, major choices, first loves/losses +- **Young Adulthood**: Independence, career/path choices, relationships +- **Current Situation**: Where story finds them + +## Character Arc Types + +### Positive Change Arc +**Structure:** +1. Lie they believe +2. Want vs. Need established +3. First glimpse of truth +4. Rejection of truth (return to lie) +5. Moment of truth (crisis) +6. Choice to embrace truth +7. New worldview demonstrated + +**Markers:** +- Start: Incomplete, held back by misbelief +- Midpoint: Glimpse growth but not ready +- Climax: Must choose between lie and truth +- End: Transformed, living truth + +### Flat Arc +**Structure:** +1. Truth known from beginning +2. World believes lie +3. Character tested on their truth +4. Character demonstrates truth +5. World begins to change +6. Truth proven through action + +**Markers:** +- Start: Strong in beliefs +- Midpoint: Severely tested +- Climax: Greatest test of faith +- End: Changed the world, not themselves + +### Negative Arc +**Structure:** +1. Flaw/weakness established +2. Temptation introduced +3. Small compromises begin +4. Point of no return crossed +5. Descent accelerates +6. Rejection of redemption +7. Tragic conclusion + +**Markers:** +- Start: Flawed but sympathetic +- Midpoint: Questionable choices +- Climax: Beyond redemption +- End: Destroyed or becomes villain + +## Relationship Dynamics + +### Character Relationships Matrix +For each significant relationship, define: +- **Dynamic Type**: Mentor/student, rivals, allies, romance, family +- **Conflict Source**: What creates tension +- **Common Ground**: What bonds them +- **Influence**: How they change each other +- **Arc**: How relationship evolves + +### Protagonist-Antagonist Relationship +- **Opposition**: How antagonist blocks protagonist's goal +- **Mirror/Foil**: How they reflect/contrast each other +- **Personal Stakes**: Why this matters beyond plot +- **Symmetry**: Similar origins or opposite arc paths +- **Respect Level**: Do they understand each other? + +## Character Voice Development + +### Dialogue Markers +- **Vocabulary Level**: Formal, casual, slang, technical +- **Sentence Structure**: Short and punchy vs. long and flowing +- **Favorite Words/Phrases**: Repeated expressions +- **Topics They Discuss**: What they talk about most +- **What They Avoid**: Topics they don't address +- **Lying Tells**: How they behave when dishonest + +### Internal Voice (POV Characters) +- **Thought Patterns**: Analytical, emotional, scattered, focused +- **Biases**: How they interpret events +- **Blind Spots**: What they don't see about themselves +- **Metaphors**: Types of comparisons they make +- **Narrative Distance**: Close, intimate vs. distant, observational + +## Character Consistency + +### Behavioral Patterns +- **Under Stress**: How they react to pressure +- **When Happy**: How they express joy +- **When Angry**: Explosive, cold, passive-aggressive +- **Decision-Making**: Impulsive, analytical, avoidant +- **Trust**: Quick or slow to trust others + +### Core Values +- **Non-Negotiables**: Lines they won't cross +- **Flexible Areas**: Where they compromise +- **Value Hierarchy**: Ranking of priorities (family, honor, survival, etc.) +- **Values Testing**: Scenes where values conflict + +### Growth Indicators +- **Early Story**: How they handle situation type X +- **Mid Story**: How handling of X begins to shift +- **Late Story**: How they handle X after growth +- **Demonstration**: Parallel scenes showing change + +## Character Roles in Ensemble + +### Ensemble Balance +- **The Leader**: Drives action, makes decisions +- **The Heart**: Emotional center, unifies group +- **The Brain**: Strategy, knowledge, analysis +- **The Warrior**: Action, protection, physical strength +- **The Wildcard**: Unpredictable, challenges norms +- **The Conscience**: Moral compass, voice of reason + +### Avoiding Character Redundancy +- **Different Wants**: Each character pursuing different goals +- **Different Methods**: Varied approaches to problems +- **Different Worldviews**: Contrasting perspectives +- **Different Skills**: Complementary abilities +- **Different Arcs**: Each on unique journey + +## Character Development Questions + +### Surface Level +- What do they look like? +- How do they dress? +- What's their job/role? +- Where do they live? + +### Deeper Level +- What do they fear most? +- What do they desire more than anything? +- What's their greatest secret? +- What do they lie to themselves about? +- What would they sacrifice everything for? + +### Behavioral Level +- How do they treat people with less power? +- What makes them laugh? +- What makes them cry? +- When do they lie, and why? +- How do they handle failure? + +### Thematic Level +- What do they represent in the story? +- What question does their arc answer? +- How do they embody or challenge the theme? +- What truth do they discover? + +## Character Testing Scenarios + +To ensure character depth, test them against: + +1. **Moral Dilemma**: Force choice between two values +2. **Loss**: Take away something they depend on +3. **Temptation**: Offer something they want vs. need +4. **Betrayal**: Test their trust and forgiveness +5. **Sacrifice**: Force them to give up something important +6. **Revelation**: Expose a truth they've been avoiding +7. **Isolation**: Remove their support system +8. **Power**: Give them control and see how they use it + +## Red Flags for Weak Characters + +### Avoid: +- **Mary Sue/Gary Stu**: Too perfect, no real flaws +- **Inconsistent Behavior**: Acts differently for plot convenience +- **No Agency**: Things happen to them, they don't drive action +- **Single-Note**: Only one personality trait +- **No Growth**: Same at end as beginning (unless flat arc) +- **Reactive Only**: Never makes proactive choices +- **Exposition Puppet**: Exists to explain things +- **Token Diversity**: Defined only by identity marker + +### Fix By: +- Adding meaningful flaws and consequences +- Establishing behavioral patterns and motivations +- Giving them goals and plans they actively pursue +- Layering contradictory traits and complexity +- Planning clear arc with transformation +- Creating scenes where they initiate action +- Giving them purpose beyond information delivery +- Developing full personality, backstory, and individual arc diff --git a/skills/storyboard-manager/references/story_structures.md b/skills/storyboard-manager/references/story_structures.md new file mode 100755 index 0000000..906a0ed --- /dev/null +++ b/skills/storyboard-manager/references/story_structures.md @@ -0,0 +1,148 @@ +# Story Structure Reference + +This reference provides common story structures and frameworks for planning narratives. + +## Three-Act Structure + +### Act One: Setup (25% of story) +- **Hook**: Opening scene that grabs attention +- **Inciting Incident**: Event that disrupts the protagonist's normal world +- **First Plot Point**: Decision/event that propels protagonist into Act Two (typically at 25% mark) + +### Act Two: Confrontation (50% of story) +- **Rising Action**: Series of obstacles and complications +- **Midpoint**: Major revelation or reversal (at 50% mark) +- **Pinch Points**: Moments that increase pressure on protagonist +- **Second Plot Point**: Lowest point/crisis that leads into Act Three (at 75% mark) + +### Act Three: Resolution (25% of story) +- **Climax**: Final confrontation or decision +- **Falling Action**: Immediate consequences of climax +- **Resolution**: New normal/equilibrium established + +## Hero's Journey (Joseph Campbell) + +1. **Ordinary World**: Hero's normal life +2. **Call to Adventure**: Challenge or quest presented +3. **Refusal of the Call**: Initial hesitation or fear +4. **Meeting the Mentor**: Guidance or magical aid +5. **Crossing the Threshold**: Commitment to the journey +6. **Tests, Allies, and Enemies**: Learning the rules of the new world +7. **Approach to the Inmost Cave**: Preparation for major challenge +8. **Ordeal**: Greatest fear/challenge faced +9. **Reward**: Achievement of goal or new knowledge +10. **The Road Back**: Return journey begins +11. **Resurrection**: Final test with everything at stake +12. **Return with the Elixir**: Hero returns transformed + +## Save the Cat Beat Sheet (Blake Snyder) + +1. **Opening Image**: Snapshot of protagonist's world before change +2. **Theme Stated**: Central question or theme introduced +3. **Setup**: Establish protagonist's world, flaws, and stakes +4. **Catalyst**: Event that starts the story (at 10% mark) +5. **Debate**: Internal conflict about whether to act +6. **Break into Two**: Protagonist commits to journey (at 20-25% mark) +7. **B Story**: Subplot introduced (often romantic or thematic) +8. **Fun and Games**: Promise of the premise delivered +9. **Midpoint**: False victory or defeat (at 50% mark) +10. **Bad Guys Close In**: External and internal pressure increases +11. **All Is Lost**: Lowest point (at 75% mark) +12. **Dark Night of the Soul**: Protagonist processes loss +13. **Break into Three**: Solution discovered (at 80% mark) +14. **Finale**: Climax and resolution +15. **Final Image**: Parallel to opening showing change + +## Character Arc Templates + +### Positive Change Arc +- **Lie Believed**: Character starts believing something false about themselves/world +- **Want vs. Need**: What they think they want vs. what they actually need +- **Ghost/Wound**: Past trauma influencing present behavior +- **Moment of Truth**: Forced to choose between lie and truth +- **Resolution**: Embraces truth and grows + +### Flat Arc +- **Truth Known**: Character already knows the truth +- **World's Lie**: The world around them believes a lie +- **Testing**: Character's truth is challenged repeatedly +- **Impact**: Character changes the world around them +- **Affirmation**: Character's truth proven correct + +### Negative Arc +- **Initial Weakness**: Character has a flaw or belief +- **Escalation**: Flaw grows worse through choices +- **Point of No Return**: Character chooses darkness +- **Descent**: Consequences spiral +- **Tragic End**: Character destroyed or becomes antagonist + +## Scene Structure + +### Scene Components +1. **Goal**: What the POV character wants in this scene +2. **Conflict**: Opposition to achieving the goal +3. **Disaster**: Outcome (usually negative) that propels to next scene + +### Sequel Components (reaction to scene) +1. **Reaction**: Emotional response to disaster +2. **Dilemma**: Working through options +3. **Decision**: Choice that leads to next goal/scene + +## Pacing Guidelines + +### Chapter Length by Genre +- **Thriller/Mystery**: 2,000-3,000 words (faster pace) +- **Fantasy/Sci-Fi**: 3,000-5,000 words (world-building needs) +- **Romance**: 2,500-4,000 words (emotional beats) +- **Literary Fiction**: 2,000-6,000 words (varies widely) +- **YA**: 2,000-3,500 words (shorter attention span) + +### Tension Management +- **High-tension scenes**: Action, conflict, revelations (shorter, punchier) +- **Low-tension scenes**: Character development, world-building (can be longer) +- **Rhythm**: Alternate between high and low tension +- **Overall trend**: Tension should increase as story progresses + +## Plot Development + +### Conflict Types +1. **Character vs. Character**: Antagonist opposition +2. **Character vs. Self**: Internal struggle +3. **Character vs. Society**: Against norms/systems +4. **Character vs. Nature**: Environmental challenges +5. **Character vs. Technology**: Man vs. machine +6. **Character vs. Fate**: Against destiny/prophecy + +### Subplot Integration +- **Mirror subplots**: Reflect main theme differently +- **Contrast subplots**: Show opposite approach to theme +- **Complication subplots**: Add obstacles to main plot +- **Resolution rule**: Resolve minor subplots before climax, major ones during/after + +## Genre-Specific Structures + +### Mystery/Thriller +- Introduction of crime/mystery +- Investigation and clue discovery +- Red herrings and misdirection +- Escalating danger +- Revelation and confrontation +- Resolution and explanation + +### Romance +- Meet-cute or introduction +- Attraction develops +- Barrier/conflict introduced +- Relationship deepens despite obstacles +- Black moment/breakup +- Grand gesture/reconciliation +- Happy ending or HEA (Happily Ever After) + +### Fantasy/Sci-Fi +- Ordinary world establishment +- Introduction to magical/sci-fi elements +- Quest or mission defined +- Journey and world exploration +- Building towards prophesied/anticipated event +- Final battle or confrontation +- New world order established diff --git a/skills/storyboard-manager/scripts/consistency_checker.py b/skills/storyboard-manager/scripts/consistency_checker.py new file mode 100755 index 0000000..36999e0 --- /dev/null +++ b/skills/storyboard-manager/scripts/consistency_checker.py @@ -0,0 +1,391 @@ +#!/usr/bin/env python3 +""" +Consistency Checker for Storyboard Manager + +This script analyzes markdown files in a storyboard project to detect inconsistencies +in character details, plot elements, and world-building across the story. +""" + +import os +import re +import sys +import json +from pathlib import Path +from typing import List, Dict, Set, Tuple, Optional +from collections import defaultdict + + +class ConsistencyIssue: + """Represents a consistency issue found in the story""" + + def __init__(self, issue_type: str, severity: str, description: str, + locations: List[str], details: Dict = None): + self.issue_type = issue_type # character, plot, world, timeline + self.severity = severity # critical, warning, info + self.description = description + self.locations = locations + self.details = details or {} + + def __repr__(self): + return f"ConsistencyIssue({self.severity}: {self.description})" + + def to_dict(self): + return { + 'type': self.issue_type, + 'severity': self.severity, + 'description': self.description, + 'locations': self.locations, + 'details': self.details + } + + +class CharacterProfile: + """Stores character information from profile files""" + + def __init__(self, name: str, file_path: str): + self.name = name + self.file_path = file_path + self.attributes = {} + self.aliases = [] + self.relationships = {} + + def add_attribute(self, key: str, value: str): + """Add a character attribute""" + self.attributes[key.lower()] = value + + def get_attribute(self, key: str) -> Optional[str]: + """Get a character attribute""" + return self.attributes.get(key.lower()) + + +class ConsistencyChecker: + """Main consistency checking class""" + + # Patterns to extract character attributes + ATTRIBUTE_PATTERNS = { + 'age': r'\*\*Age:\*\*\s*(.+?)(?:\n|$)', + 'appearance': r'\*\*Appearance:\*\*\s*(.+?)(?:\n|$)', + 'hair': r'(?:hair|Hair)[\s:]+([^,\n]+)', + 'eyes': r'(?:eyes|Eyes)[\s:]+([^,\n]+)', + 'height': r'\*\*Height:\*\*\s*(.+?)(?:\n|$)', + 'role': r'\*\*Role:\*\*\s*(.+?)(?:\n|$)', + } + + def __init__(self, project_root: str): + self.project_root = Path(project_root) + self.characters: Dict[str, CharacterProfile] = {} + self.issues: List[ConsistencyIssue] = [] + self.world_facts: Dict[str, Tuple[str, str]] = {} # fact -> (value, location) + + def scan_directory(self, directory: Path) -> List[Path]: + """Recursively find all markdown files in directory""" + md_files = [] + if not directory.exists(): + return md_files + + for item in directory.iterdir(): + if item.is_file() and item.suffix == '.md': + md_files.append(item) + elif item.is_dir() and not item.name.startswith('.'): + md_files.extend(self.scan_directory(item)) + + return md_files + + def load_character_profile(self, file_path: Path) -> Optional[CharacterProfile]: + """Load character information from a profile file""" + try: + content = file_path.read_text(encoding='utf-8') + + # Extract character name from title + name_match = re.search(r'^#\s+(.+?)$', content, re.MULTILINE) + if not name_match: + return None + + name = name_match.group(1).strip() + profile = CharacterProfile(name, str(file_path.relative_to(self.project_root))) + + # Extract attributes + for attr_name, pattern in self.ATTRIBUTE_PATTERNS.items(): + match = re.search(pattern, content, re.IGNORECASE) + if match: + profile.add_attribute(attr_name, match.group(1).strip()) + + # Extract aliases/nicknames + alias_match = re.search( + r'\*\*(?:Nicknames?|Aliases?):\*\*\s*(.+?)(?:\n|$)', + content, re.IGNORECASE + ) + if alias_match: + aliases = re.split(r'[,;]', alias_match.group(1)) + profile.aliases = [a.strip() for a in aliases if a.strip()] + + return profile + + except Exception as e: + print(f"Warning: Could not read character profile {file_path}: {e}", + file=sys.stderr) + return None + + def load_all_characters(self): + """Load all character profiles from the project""" + char_dirs = ['characters', 'Characters', 'cast', 'Cast'] + + for dirname in char_dirs: + char_dir = self.project_root / dirname + if char_dir.exists(): + for char_file in self.scan_directory(char_dir): + profile = self.load_character_profile(char_file) + if profile: + self.characters[profile.name] = profile + + def check_character_mentions(self, file_path: Path): + """Check character mentions in content for inconsistencies""" + try: + content = file_path.read_text(encoding='utf-8') + location = str(file_path.relative_to(self.project_root)) + + for char_name, profile in self.characters.items(): + # Check if character is mentioned + if not re.search(r'\b' + re.escape(char_name) + r'\b', content, re.IGNORECASE): + continue + + # Check for attribute contradictions + for attr_name, attr_value in profile.attributes.items(): + # Look for contradicting descriptions + if attr_name == 'age': + age_mentions = re.finditer( + r'\b' + re.escape(char_name) + r'\b[^.!?]*\b(\d+)[\s-](?:year|yr)', + content, re.IGNORECASE + ) + for match in age_mentions: + mentioned_age = match.group(1) + profile_age = re.search(r'\d+', attr_value) + if profile_age and mentioned_age != profile_age.group(0): + self.issues.append(ConsistencyIssue( + issue_type='character', + severity='warning', + description=f"Age inconsistency for {char_name}", + locations=[location, profile.file_path], + details={ + 'character': char_name, + 'profile_age': attr_value, + 'mentioned_age': mentioned_age + } + )) + + elif attr_name in ['hair', 'eyes']: + # Check for contradicting physical descriptions + desc_pattern = rf'\b{re.escape(char_name)}\b[^.!?]*\b({attr_name})\b[^.!?]*' + desc_mentions = re.finditer(desc_pattern, content, re.IGNORECASE) + for match in desc_mentions: + context = match.group(0).lower() + # Simple check: if profile says "black hair" but text says "blonde" + profile_value_lower = attr_value.lower() + if profile_value_lower not in context: + # Extract the contradicting description + color_pattern = r'\b(black|brown|blonde|red|auburn|white|gray|grey|blue|green|hazel)\b' + colors = re.findall(color_pattern, context, re.IGNORECASE) + if colors: + self.issues.append(ConsistencyIssue( + issue_type='character', + severity='warning', + description=f"{attr_name.capitalize()} color inconsistency for {char_name}", + locations=[location, profile.file_path], + details={ + 'character': char_name, + 'profile': attr_value, + 'context': match.group(0)[:100] + } + )) + + except Exception as e: + print(f"Warning: Error checking {file_path}: {e}", file=sys.stderr) + + def check_character_relationships(self): + """Check for inconsistent character relationships""" + # This is a placeholder for more sophisticated relationship checking + # Would analyze relationship declarations in character files and compare + # with how relationships are portrayed in chapters + + relationship_keywords = ['friend', 'enemy', 'lover', 'sibling', 'parent', 'child'] + + for char_name, profile in self.characters.items(): + # Extract relationship info from profile + # Compare with relationships mentioned in story files + # Flag inconsistencies + pass + + def check_world_building(self, file_path: Path): + """Check for world-building inconsistencies""" + try: + content = file_path.read_text(encoding='utf-8') + location = str(file_path.relative_to(self.project_root)) + + # Look for world-building facts (places, magic systems, technology, etc.) + # This is a simplified version - would need more sophisticated pattern matching + + # Example: Check for location descriptions + location_pattern = r'\*\*Location:\*\*\s*(.+?)(?:\n|$)' + for match in re.finditer(location_pattern, content, re.IGNORECASE): + loc_name = match.group(1).strip() + + if loc_name in self.world_facts: + # Check if description is consistent + prev_value, prev_location = self.world_facts[loc_name] + # In a real implementation, would do semantic comparison + else: + self.world_facts[loc_name] = (match.group(0), location) + + except Exception as e: + print(f"Warning: Error checking world-building in {file_path}: {e}", + file=sys.stderr) + + def check_plot_consistency(self): + """Check for plot inconsistencies""" + # Placeholder for plot consistency checking + # Would track plot points, events, and check for contradictions + + # Examples to check: + # - Events happening out of order + # - Characters appearing after their death + # - Objects used before acquisition + # - Locations visited before discovery + pass + + def check_name_variations(self, file_path: Path): + """Check for inconsistent name usage""" + try: + content = file_path.read_text(encoding='utf-8') + location = str(file_path.relative_to(self.project_root)) + + # Check if character names are spelled consistently + for char_name, profile in self.characters.items(): + # Look for potential misspellings (Levenshtein distance) + # This is simplified - would use actual string distance algorithm + + # Check for variations in capitalization + variations = re.findall( + r'\b' + re.escape(char_name) + r'\b', + content, + re.IGNORECASE + ) + + inconsistent_caps = [v for v in variations if v != char_name] + if inconsistent_caps: + unique_variations = list(set(inconsistent_caps)) + if len(unique_variations) > 0: + self.issues.append(ConsistencyIssue( + issue_type='character', + severity='info', + description=f"Name capitalization variations for {char_name}", + locations=[location], + details={ + 'character': char_name, + 'variations': unique_variations + } + )) + + except Exception as e: + print(f"Warning: Error checking names in {file_path}: {e}", file=sys.stderr) + + def analyze_project(self) -> Dict: + """Run all consistency checks on the project""" + + # Load character profiles + self.load_all_characters() + + # Check all content files + content_dirs = ['chapters', 'Chapters', 'scenes', 'Scenes', 'story'] + content_files = [] + + for dirname in content_dirs: + content_dir = self.project_root / dirname + if content_dir.exists(): + content_files.extend(self.scan_directory(content_dir)) + + # Run checks on each file + for content_file in content_files: + self.check_character_mentions(content_file) + self.check_world_building(content_file) + self.check_name_variations(content_file) + + # Run project-wide checks + self.check_character_relationships() + self.check_plot_consistency() + + # Organize results + issues_by_severity = defaultdict(list) + for issue in self.issues: + issues_by_severity[issue.severity].append(issue.to_dict()) + + analysis = { + 'total_issues': len(self.issues), + 'critical_issues': len(issues_by_severity['critical']), + 'warnings': len(issues_by_severity['warning']), + 'info': len(issues_by_severity['info']), + 'characters_analyzed': len(self.characters), + 'issues_by_severity': dict(issues_by_severity), + 'all_issues': [issue.to_dict() for issue in self.issues] + } + + return analysis + + +def main(): + """Main entry point for consistency checker""" + + if len(sys.argv) < 2: + print("Usage: consistency_checker.py [--output json|markdown]") + sys.exit(1) + + project_dir = sys.argv[1] + output_format = 'markdown' + + if len(sys.argv) > 2 and sys.argv[2] == '--output': + output_format = sys.argv[3] if len(sys.argv) > 3 else 'markdown' + + checker = ConsistencyChecker(project_dir) + analysis = checker.analyze_project() + + if output_format == 'json': + print(json.dumps(analysis, indent=2)) + else: + # Markdown output + print("# Consistency Analysis\n") + print(f"**Total Issues Found:** {analysis['total_issues']}") + print(f"- Critical: {analysis['critical_issues']}") + print(f"- Warnings: {analysis['warnings']}") + print(f"- Info: {analysis['info']}\n") + print(f"**Characters Analyzed:** {analysis['characters_analyzed']}\n") + + if analysis['total_issues'] == 0: + print("✅ No consistency issues found!\n") + else: + # Display issues by severity + for severity in ['critical', 'warning', 'info']: + issues = analysis['issues_by_severity'].get(severity, []) + if issues: + severity_emoji = { + 'critical': '🔴', + 'warning': '⚠️', + 'info': 'ℹ️' + } + print(f"\n## {severity_emoji[severity]} {severity.upper()}\n") + + for issue in issues: + print(f"### {issue['description']}") + print(f"**Type:** {issue['type']}") + print(f"**Locations:**") + for loc in issue['locations']: + print(f"- {loc}") + + if issue['details']: + print(f"**Details:**") + for key, value in issue['details'].items(): + print(f"- {key}: {value}") + + print() + + +if __name__ == '__main__': + main() diff --git a/skills/storyboard-manager/scripts/timeline_tracker.py b/skills/storyboard-manager/scripts/timeline_tracker.py new file mode 100755 index 0000000..67f5153 --- /dev/null +++ b/skills/storyboard-manager/scripts/timeline_tracker.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +""" +Timeline Tracker for Storyboard Manager + +This script analyzes markdown files in a storyboard project to extract and organize +timeline events, helping writers maintain chronological consistency. +""" + +import os +import re +import json +import sys +from pathlib import Path +from typing import List, Dict, Tuple, Optional +from datetime import datetime, timedelta +from collections import defaultdict + + +class TimelineEvent: + """Represents a single event in the story timeline""" + + def __init__(self, content: str, location: str, chapter: str = None, + timepoint: str = None, characters: List[str] = None): + self.content = content + self.location = location # File path where event was found + self.chapter = chapter + self.timepoint = timepoint # Relative time (e.g., "Day 1", "3 weeks later") + self.characters = characters or [] + + def __repr__(self): + return f"TimelineEvent({self.timepoint}: {self.content[:50]}...)" + + +class TimelineTracker: + """Main timeline tracking and analysis class""" + + # Patterns to detect time markers in text + TIME_PATTERNS = [ + r'(?:Day|Night)\s+(\d+)', # Day 1, Night 3 + r'(\d+)\s+(?:days?|weeks?|months?|years?)\s+(?:later|ago|after|before)', + r'(?:Morning|Afternoon|Evening|Night)\s+of\s+(?:Day\s+)?(\d+)', + r'Chapter\s+(\d+)', # Chapter markers + r'\*\*(?:Timeline|Time|When):\*\*\s*(.+?)(?:\n|$)', # Explicit timeline markers + r'\*\*Date:\*\*\s*(.+?)(?:\n|$)', + ] + + # Patterns to detect character mentions + CHARACTER_PATTERNS = [ + r'\*\*Characters?:\*\*\s*(.+?)(?:\n|$)', + r'\*\*(?:POV|Perspective):\*\*\s*(.+?)(?:\n|$)', + ] + + def __init__(self, project_root: str): + self.project_root = Path(project_root) + self.events: List[TimelineEvent] = [] + self.characters: set = set() + + def scan_directory(self, directory: Path) -> List[Path]: + """Recursively find all markdown files in directory""" + md_files = [] + if not directory.exists(): + return md_files + + for item in directory.iterdir(): + if item.is_file() and item.suffix == '.md': + md_files.append(item) + elif item.is_dir() and not item.name.startswith('.'): + md_files.extend(self.scan_directory(item)) + + return md_files + + def extract_characters_from_file(self, file_path: Path) -> List[str]: + """Extract character names from character profile files""" + try: + content = file_path.read_text(encoding='utf-8') + + # Look for character name in title (# Character Name) + name_match = re.search(r'^#\s+(.+?)$', content, re.MULTILINE) + if name_match: + return [name_match.group(1).strip()] + + # Look for explicit name field + name_match = re.search(r'\*\*Name:\*\*\s*(.+?)(?:\n|$)', content) + if name_match: + return [name_match.group(1).strip()] + + except Exception as e: + print(f"Warning: Could not read {file_path}: {e}", file=sys.stderr) + + return [] + + def extract_timeline_markers(self, content: str) -> List[Tuple[str, int]]: + """Extract time markers from content, return list of (timepoint, position)""" + markers = [] + + for pattern in self.TIME_PATTERNS: + for match in re.finditer(pattern, content, re.IGNORECASE): + timepoint = match.group(1) if match.lastindex else match.group(0) + markers.append((timepoint.strip(), match.start())) + + return sorted(markers, key=lambda x: x[1]) + + def extract_character_mentions(self, content: str) -> List[str]: + """Extract character names from explicit character markers""" + characters = [] + + for pattern in self.CHARACTER_PATTERNS: + matches = re.finditer(pattern, content, re.IGNORECASE) + for match in matches: + char_text = match.group(1) + # Split by commas, 'and', '&' + names = re.split(r'[,&]|\sand\s', char_text) + characters.extend([name.strip() for name in names if name.strip()]) + + return characters + + def find_character_references(self, content: str, known_characters: set) -> List[str]: + """Find mentions of known characters in content""" + found = [] + for character in known_characters: + # Simple word boundary check + if re.search(r'\b' + re.escape(character) + r'\b', content, re.IGNORECASE): + found.append(character) + return found + + def parse_chapter_file(self, file_path: Path) -> List[TimelineEvent]: + """Parse a chapter/scene file for timeline events""" + events = [] + + try: + content = file_path.read_text(encoding='utf-8') + + # Get chapter number/name from filename or title + chapter = file_path.stem + title_match = re.search(r'^#\s+(.+?)$', content, re.MULTILINE) + if title_match: + chapter = title_match.group(1).strip() + + # Extract explicit character mentions + explicit_chars = self.extract_character_mentions(content) + + # Find timeline markers + markers = self.extract_timeline_markers(content) + + # Split content into sections based on markers + if markers: + sections = [] + for i, (timepoint, pos) in enumerate(markers): + start_pos = pos + end_pos = markers[i + 1][1] if i + 1 < len(markers) else len(content) + section_content = content[start_pos:end_pos] + + # Find characters in this section + section_chars = explicit_chars.copy() + section_chars.extend(self.find_character_references( + section_content, self.characters)) + + event = TimelineEvent( + content=section_content[:500], # First 500 chars as preview + location=str(file_path.relative_to(self.project_root)), + chapter=chapter, + timepoint=timepoint, + characters=list(set(section_chars)) + ) + events.append(event) + else: + # No explicit markers, treat whole file as one event + all_chars = explicit_chars.copy() + all_chars.extend(self.find_character_references(content, self.characters)) + + event = TimelineEvent( + content=content[:500], + location=str(file_path.relative_to(self.project_root)), + chapter=chapter, + timepoint=None, + characters=list(set(all_chars)) + ) + events.append(event) + + except Exception as e: + print(f"Warning: Error parsing {file_path}: {e}", file=sys.stderr) + + return events + + def analyze_project(self) -> Dict: + """Analyze entire project and build timeline""" + + # First, find all characters + char_dirs = ['characters', 'Characters', 'cast'] + for dirname in char_dirs: + char_dir = self.project_root / dirname + if char_dir.exists(): + for char_file in self.scan_directory(char_dir): + names = self.extract_characters_from_file(char_file) + self.characters.update(names) + + # Then scan chapters/scenes + content_dirs = ['chapters', 'Chapters', 'scenes', 'Scenes', 'story'] + for dirname in content_dirs: + content_dir = self.project_root / dirname + if content_dir.exists(): + for content_file in self.scan_directory(content_dir): + events = self.parse_chapter_file(content_file) + self.events.extend(events) + + # Build analysis + analysis = { + 'total_events': len(self.events), + 'total_characters': len(self.characters), + 'characters': sorted(list(self.characters)), + 'events_by_timepoint': self._group_events_by_time(), + 'events_by_character': self._group_events_by_character(), + 'events_by_chapter': self._group_events_by_chapter(), + 'timeline': self._build_timeline(), + 'warnings': self._check_consistency() + } + + return analysis + + def _group_events_by_time(self) -> Dict[str, List[Dict]]: + """Group events by their timepoint""" + grouped = defaultdict(list) + + for event in self.events: + timepoint = event.timepoint or "Unspecified" + grouped[timepoint].append({ + 'location': event.location, + 'chapter': event.chapter, + 'characters': event.characters, + 'preview': event.content[:200] + }) + + return dict(grouped) + + def _group_events_by_character(self) -> Dict[str, List[Dict]]: + """Group events by character appearance""" + grouped = defaultdict(list) + + for event in self.events: + for character in event.characters: + grouped[character].append({ + 'location': event.location, + 'chapter': event.chapter, + 'timepoint': event.timepoint, + 'preview': event.content[:200] + }) + + return dict(grouped) + + def _group_events_by_chapter(self) -> Dict[str, List[Dict]]: + """Group events by chapter""" + grouped = defaultdict(list) + + for event in self.events: + chapter = event.chapter or "Unknown" + grouped[chapter].append({ + 'location': event.location, + 'timepoint': event.timepoint, + 'characters': event.characters, + 'preview': event.content[:200] + }) + + return dict(grouped) + + def _build_timeline(self) -> List[Dict]: + """Build chronological timeline of events""" + # Sort events by timepoint (this is simplified, real implementation + # would need more sophisticated time parsing) + timeline = [] + + for event in self.events: + timeline.append({ + 'timepoint': event.timepoint or "Unknown", + 'chapter': event.chapter, + 'location': event.location, + 'characters': event.characters, + 'preview': event.content[:200] + }) + + return timeline + + def _check_consistency(self) -> List[str]: + """Check for potential timeline inconsistencies""" + warnings = [] + + # Check for events without time markers + unmarked_events = [e for e in self.events if not e.timepoint] + if unmarked_events: + warnings.append( + f"Found {len(unmarked_events)} events without timeline markers" + ) + + # Check for characters appearing in timeline without character files + mentioned_chars = set() + for event in self.events: + mentioned_chars.update(event.characters) + + undefined_chars = mentioned_chars - self.characters + if undefined_chars: + warnings.append( + f"Characters mentioned but not defined: {', '.join(sorted(undefined_chars))}" + ) + + return warnings + + +def main(): + """Main entry point for timeline tracker""" + + if len(sys.argv) < 2: + print("Usage: timeline_tracker.py [--output json|markdown]") + sys.exit(1) + + project_dir = sys.argv[1] + output_format = 'markdown' + + if len(sys.argv) > 2 and sys.argv[2] == '--output': + output_format = sys.argv[3] if len(sys.argv) > 3 else 'markdown' + + tracker = TimelineTracker(project_dir) + analysis = tracker.analyze_project() + + if output_format == 'json': + print(json.dumps(analysis, indent=2)) + else: + # Markdown output + print("# Timeline Analysis\n") + print(f"**Total Events:** {analysis['total_events']}") + print(f"**Total Characters:** {analysis['total_characters']}\n") + + print("## Characters") + for char in analysis['characters']: + appearances = len(analysis['events_by_character'].get(char, [])) + print(f"- {char} ({appearances} appearances)") + + print("\n## Timeline") + for event in analysis['timeline']: + print(f"\n### {event['timepoint']} - {event['chapter']}") + print(f"**Location:** {event['location']}") + if event['characters']: + print(f"**Characters:** {', '.join(event['characters'])}") + print(f"\n{event['preview']}...\n") + print("---") + + if analysis['warnings']: + print("\n## Warnings") + for warning in analysis['warnings']: + print(f"- ⚠️ {warning}") + + +if __name__ == '__main__': + main() diff --git a/skills/study-buddy/SKILL.md b/skills/study-buddy/SKILL.md new file mode 100755 index 0000000..5c4caae --- /dev/null +++ b/skills/study-buddy/SKILL.md @@ -0,0 +1,525 @@ +--- +name: study-buddy +description: 智能督学助手,管理用户的长期学习项目工作流。当用户表达学习项目相关意图时触发:创建/制定学习计划("我想学X"、"帮我制定计划")、汇报学习进度("学完了"、"今天搞定了"、"完成今日任务")、查询计划状态("我学到哪了"、"看下进度")、查看学习报告("日报""周报""月报""项目总结""5月10号到15号的报告")、晨间/晚间打卡复盘、督促、动态调整计划、抱怨学不下去时(情绪支持)。**🔴 项目生成流程铁律**:项目生成成功后**必须一口气走完"项目→知识点→计划表"**,禁止只汇报"项目已生成 / X 个知识点 / X 个模块"就停下,必须**立即**输出"DAY / 项目 / 知识点 / 时长 / 难度"表格供用户确认 DAY 安排,否则视为流程失败。**🔴 报告查询铁律**:用户表达查看学习报告意图时(日报/周报/月报/项目总结/任意时间段),必须从 USER.md 取**对应时间段**的 `project_id` + `knowledge_id`,传给 `study_buddy_supervise` 工具的 `study_check` action 拿原始数据,按"模块3 主动报告查询"输出,**全程只读不写 USER.md**。**不处理**:单次出题(→ quiz-mastery)、Cheatsheet 生成(→ cheat-sheet)、把题目文件导入做练习(→ quiz-mastery)。 +--- + +# 督学助手 (Study Buddy) + +## 你是谁 + +你是**学长**——不是老师,不是机器。你比用户早踩过所有坑,你真的在乎他有没有学到东西。 + +**基调:严格但不冷漠,直接但不刻薄,有温度但不滥情。** + +开口前先读: +- `USER.md`:基础信息、学习项目、薄弱知识点、学习兴趣偏好 +- `memory/YYYY-MM-DD.md`:今天的学习日记 + +--- + +## ⚠️ 四条铁律(违反即为执行失败) + +> 后续所有模块默认遵守这四条,不再重复声明。 + +### 铁律一:搜索必须主动 +用户想学的东西没有现成资料时,**立即调用 `web-search` + `web-reader` 搜,不要让用户自己去搜**。 + +❌ "你可以自己去搜一下" / 用文字描述代替实际调用 +✅ 二话不说直接搜,把搜索结果按下方格式展示出来 + +**搜索结果展示格式**(强制,**资料名必须是超链接**): + +| 资料 | 来源 | 说明 | +|------|------|------| +| [资料名 1](URL) | 域名/平台(如 GitHub / 知乎 / 官方文档) | 一句话说明这份资料讲什么、适合谁 | +| [资料名 2](URL) | ... | ... | +| [资料名 3](URL) | ... | ... | + +- **"资料"列**:必须是 `[资料名](URL)` 格式的 Markdown 超链接,URL 直接用 `web-search` 返回的链接 +- **"来源"列**:写域名或平台名,不写完整 URL +- **"说明"列**:一句话讲清这份资料**讲什么 + 适合谁**,不超过 20 字 + +### 铁律二:全部学完才能出题 +用户表达了"今天学完/搞完"的意图时(不限措辞,比如"学完了/搞定了/看完了/做完了/OK 了/结束了/今天就这样"等,或发笔记截图,凭语境判断),先确认范围——是单个知识点还是今天全部。 + +- **只学了部分(今天还有剩余知识点)**:更新 USER.md 学完字段,提醒用户剩余知识点,**不出练习**。 +- **当天计划全部学完**:**必须主动建议用户做练习**,不是可选。把当天所有知识点的 `knowledge_id` 一次性传给 `exam_take`,出一个覆盖全天的综合测试。 +- **例外:用户主动要求练习某个知识点**(如"帮我出个X的练习"、"我想测一下Y")→ 直接为该知识点单独出练习,不受"全部学完"的限制。 + +**话术原则**(不要写死一句模板,按当下语境自由发挥): +- 表达"趁热打铁、巩固一下"的意思,不要说"来几道题"这种冷冰冰的命令 +- 给一个**做练习的好处**钩子——比如"知道自己吃透没"、"趁记忆新鲜"、"晚上复盘有数据更准" +- 简短,不啰嗦,**一两句话** + +参考语气(不要照抄): +- "趁热打铁,做组练习巩固一下——这会儿刚学完,最容易吃透的时候。" +- "顺手来个练习吧,5 分钟,能看出哪里还有漏。" +- "做几道题感受一下,晚上复盘的时候有数据,我也能看清楚你哪儿要补。" + +**出题方法**: +- **全部学完时**:调 `study_buddy` 工具的 `exam_take` action,传入当天**所有**知识点的 `knowledge_id` 数组,获取测试链接,按铁律三输出 `**今日练习:[标题](链接)**` 给用户。 +- **用户主动要求练单个知识点时**:调 `exam_take`,仅传入该知识点的 `knowledge_id`,同样按铁律三输出。 + +战绩统一由晚间 `study_check` 拿。 + +### 铁律三:链接必须输出 +凡涉及课程/项目/资料,**必须输出可点击链接**,纯文字描述不算。 +格式:`**项目生成中:[URL]**` / `**项目已生成:[标题](链接)**` / `**今日待学:[标题](链接)**` / `**推荐项目:[标题](链接)**` + +### 铁律四:用户门控不可绕过 +以下时刻**必须停下等用户明确回复**,禁止自作主张往下走: +- 询问"是否有学习材料" → 等用户回复 +- 展示 DAY 排期表格 → 等用户说"可以/没问题/就这样" +- 询问晨间/晚间提醒时间 → 等用户给具体时间 + +--- + +## 说话风格 + +❌ "您好,根据您的学习计划,今日需完成以下知识点……" +✅ "早,今天要搞定这三个——不难,但得认真。" + +❌ "非常感谢您坚持完成了今日的学习任务。" +✅ "行,今天的活干完了。我记下来了。明天继续。" + +### 情绪刻度 + +| 刻度 | 场景 | 风格 | +|------|------|------| +| [1] 冷静播报 | 日常推送 | 平铺直叙 | +| [2] 温和鼓励 | 用户在努力中 | 给点力,不夸张 | +| [3] 认真表扬 | 真实达成目标 | 实打实地夸 | +| [4] 直接施压 | 拖延、找借口 | 点破,不绕弯 | +| [5] 严肃警告 | 连续多天滞后 | 说清后果 | +| [6] 情感共情 | 用户崩溃/放弃 | 先接情绪,再给最小行动 | + +**规则**:崩溃时绝不用 [4][5];摆烂时绝不用 [2][3]。 + +--- + +## 模块1:定时提醒(Cron) + +**所有定时提醒统一用 `create_cron_job`,不用 HEARTBEAT.md。** + +### 晨间推送 cron +- **任务名**:`StudyBuddy 晨间推送`(固定,删除时按此名定位) +- 时间:每天 `<用户确认的小时>:00`,时区 `Asia/Shanghai` +- 触发时下发 message: + ``` + 现在是晨间推送时间。 + 按"模块3 晨间推送格式"组装并输出给用户。 + ``` + +### 晚间复盘 cron +- **任务名**:`StudyBuddy 晚间复盘`(固定,删除时按此名定位) +- 时间:每天 `<用户确认的小时>:00`,时区 `Asia/Shanghai` +- 触发时下发 message: + ``` + 现在是晚间复盘时间。 + 从 `USER.md` "学习项目"分区取所有"状态:进行中"项目的 `project_id`(1 个或多个), + 一次性传给 `study_buddy_supervise` 工具的 `study_check` action 获取原始数据, + 按"模块3 晚间复盘格式"组装并输出给用户。 + + 同时执行以下软性规则检查(命中才介入): + 1. 今日计划未完成 → 介入督促并记录原因 + 2. 距截止日期≤3天且进度<60% → 主动预警 + 3. 项目完成检查:扫描 `USER.md` "学习项目"分区下所有"状态:进行中"的项目, + 若所有叶子都已打卡完成(全部 [x]),把状态改为"已完成",通知用户"恭喜完成 XXX 项目"。 + **不要主动问删 cron**——所有项目完成 ≠ 用户要停学,下个项目可能马上来。 + cron 默认保留。只有用户明确表达"不学了 / 暂停一下 / 休息" 等停学意图时, + 才主动建议:"要不要把每日提醒 cron 也关掉?" 用户同意 → + 调 `list_cron_jobs` 列出所有 cron → 按任务名 `StudyBuddy 晨间推送` / + `StudyBuddy 晚间复盘` 找到对应 cron 的 ID → 调 `delete_cron_job` 删除。 + ``` + +**注意**: +- 时间必须来自用户明确回复,不得用默认值 +- 计划调整 / 删除提醒时,同步更新对应 cron +- 修改晚间复盘的软性规则需同步更新 cron message + +--- + +## 模块2:学习项目生成 + +用户说"我想学X"、"帮我制定计划"时执行。 + +### 步骤1 — 确认学习资料 +- 问"手里有资料吗?" +- 用户说没有 → 立即搜索(铁律一),展示结果 +- **停在这里等用户确认**(铁律四) + +### 步骤1.5 — 链接资料预处理(仅链接类资料) + +> ⚠️ **仅当资料是链接(URL)时执行此步骤**,无论是用户提供的还是搜索来的。用户提供了文件的直接跳到步骤2。 + +1. 调用 `qingyan-research` skill 生成 HTML 研究报告 +2. 调用 `pdf` skill 把 HTML 转成 PDF(命令:`python3 "$PDF_SKILL_DIR/scripts/pdf.py" convert.html <报告.html> --output <报告.pdf>`) +3. **HTML 与 PDF 都不展示给用户,仅供工具消费** +4. 完成后**直接继续执行步骤2**,不等待用户消息 +5. 若当前回复已结束(步骤1.5耗尽输出),下一条消息自动进入步骤2 + +### 步骤2 — 触发项目生成 + 10 分钟回查 + +**当前回复内做**(顺序不可调换): + +1. 把学习资料传给 `study_buddy` 工具的 `create_project` action: + - 资料是**文件** → 直接把文件传过去 + - 资料是**搜索来的内容**(步骤1.5 已生成 PDF)→ 把 PDF 文件传过去 + - 资料有多份就一次性全部传过去 + - **务必抓住返回的 `project_ids`、`project_names`、`share_urls` 三个数组**(一一对应),下面几步都要用 + +2. 调用 `create_cron_job` 创建 10 分钟后的一次性任务(**任务类型:`at`**,**任务名:`StudyBuddy 项目生成回查`**),message: + ``` + 项目生成 10 分钟检查时间到。 + 调用 `study_buddy` 工具的 `project_status` action, + 检查 project_ids=<完整 id 数组> 的生成状态, + 并按"模块2 步骤2 回查"的规则逐个处理。 + ``` + - **cron 建失败** → 告诉用户:"任务排期出点小状况,没法及时喊你啦~你可随时咨询进度,或点击链接查看生成状态。" 然后继续第 3 步 + +3. 输出(铁律三)——为每个生成中的项目逐个输出链接: + ``` + **项目生成中:[share_url_1]** + **项目生成中:[share_url_2]** + ... + + 项目正在生成中,预计 10 分钟内同步结果。你可随时咨询进度,或点击链接查看生成状态。 + ``` +4. 当前会话**不继续进入步骤3**,等 cron 触发 + +**步骤2 回查**(10 分钟后 `at` cron 触发,新会话): +- 从 message 读取 `project_ids` 数组 +- 调 `study_buddy` 工具的 `project_status` action 检查 `project_ids` 的生成情况 +- 对每个项目分别处理: + - **成功** → 输出 `**项目已生成:[project_name](share_url)**` → 在 USER.md "学习项目"分区**为该项目新增一块**(子标题=`project_name`、状态=进行中、project_id、share_url、开始日期=今天,叶子列表留空) + - **失败** → 告诉用户"项目 [project_name] 生成失败,晚点再试,或者换个资料看看",**不写入 USER.md** +- 全部处理完后: + - 若**至少一个成功** → **对每个成功的项目分别执行步骤3**,全部完成后**合并进入步骤4** + - 若**全部失败** → 主动告诉用户:"这次几个项目都没生成成功,要不要重新试一下?" 终止流程,等用户回应回到步骤1 + +**步骤2 主动查询**(用户在 10 分钟内主动问"生成好了吗/进度怎么样"时): +- 调 `study_buddy` 工具的 `project_status` action 检查生成情况 +- 对每个项目分别处理(逻辑同"步骤2 回查"): + - **成功** → 正常输出 + 继续走步骤3、4,同时调 `delete_cron_job`(按任务名 `StudyBuddy 项目生成回查` 定位)删除回查 cron,避免重复执行 + - **失败/未完成** → 告诉用户当前状态,cron 保留,等 10 分钟回查 + +### 步骤3 — 获取知识点列表并写入 USER.md + +> ⚠️ 多项目场景下,**每个成功的项目都要独立跑一遍这一步** + +1. 调 `study_buddy` 工具的 `list_leaves` action(传入当前项目的 project_id),拿叶子列表,**抓住每个叶子的 `knowledge_id` 和 `name`** +2. 写入 USER.md 该项目的"知识点叶子列表",每项格式: + ``` + - [ ] (knowledge_id: , DAY: 未分配, 学完: 否, 练过: 否, 答对: 0/0) + ``` + - `学完`:用户白天口头表达"学完了"时由 skill 改为 `是` + - `练过`:晚间复盘调 `study_check` 后由 skill 判定,**`[x]` 与 `练过: 是` 同步** + - `答对`:该知识点累计答题战绩(答对题目数/答题总数),初始 0/0,晚间复盘时更新 + +3. **🔴 强制:写完 USER.md 后立即进入步骤4,不许停在这里。** + - ❌ 错误示范:"已为你生成 25 个知识点,分 3 大模块。"(汇报完就停 = 失败) + - ❌ 错误示范:"项目已生成成功,共 N 个知识点,需要我帮你制定计划吗?"(追问 = 失败) + - ✅ 正确做法:写完知识点 → 一口气合并所有项目叶子 → 按步骤4 排出 DAY 表 → **直到表格出现**才能等用户回应 + - 唯一停下时机:步骤 4 第 3 步"表格展示完,等用户确认 DAY 安排" + +### 步骤4 — 制定学习计划 + 启用提醒 + +> 多项目场景下:**所有项目的叶子合并成一张总表**统一排 DAY;**只建一套提醒 cron**(晨/晚各一个),服务全部进行中的项目(用户精力有限,不并行学多个项目) + +1. 把所有项目的叶子合并 + 用户学习周期/每日时长,给每个叶子分配 DAY 编号 +2. 表格展示:DAY / 项目 / 知识点 / 预估时长 / 难度(多项目时加"项目"列) +3. **在 USER.md 中将对应项目的 `计划确认` 设为 `待确认`**,展示完表格后**停下等用户确认**(铁律四)。在用户说"可以/没问题"前: + - 不改 USER.md 的 DAY 字段 + - 不建 cron + - 不推送第一天计划 +4. 用户提调整 → 修改表格 → 再等确认 +5. 用户确认后 → **将 USER.md 中对应项目的 `计划确认` 改为 `已确认`** → 问晨间/晚间提醒时间 → 拿到具体时间后**一次性完成**: + - 按模块1 建 2 个 cron(晨间/晚间各一个,服务所有进行中的项目) + - **将 USER.md 中对应项目的 `提醒设置` 改为 `已完成`** + - 把 USER.md 里每个叶子 `(DAY: 未分配)` 改成 `(DAY: N)` + - 进入模块3 + +### 🔴 步骤4 未完成检查(每次会话开始时必做) + +> 此规则解决跨会话状态丢失问题:用户确认计划后换了会话,模型忘记还没问提醒时间;或用户看了表格没回复就走了,下次会话也不知道表格还没确认。 + +每次会话开始(读 USER.md 后),扫描"学习项目"分区所有进行中的项目,按以下优先级处理: + +**① `计划确认: 待确认`** → 主动追问(最高优先级,阻断其他流程): +> "上次给你排的 DAY 计划还没确认呢,你看看那个安排行不行?可以的话我帮你设定晨间推送和晚间复盘。" +- 等用户回复,不要自顾自往下走。用户说"可以/没问题" → 按步骤4第5步继续 + +**② `计划确认: 已确认` 且 `提醒设置: 未完成`** → 主动追问: +> "计划确认了但还没设提醒时间,你希望每天几点收到晨间推送、几点做晚间复盘?" +- 拿到时间后按步骤4第5步继续,将 `提醒设置` 改为 `已完成` + +--- + +## 模块3:每日督学循环 + +> 晨间/晚间由模块1 cron 自动触发;本模块描述触发后的输出格式。 + +**一天的完整链路(重要)**: + +``` +白天(cron 触发起点 + 用户互动): + 晨间推送(cron 触发) → 用户学 → 用户表达"学完了"的意图(凭语境判断,不限措辞) + ↓ + **先跟用户确认范围**:是今天某个知识点 还是 今天全部知识点都学完了? + ↓ + 更新 USER.md:把用户确认的知识点 `学完: 否` → `学完: 是` + (单个 / 多个 / 今天全部 都按这个流程批量更新) + ↓ + 判断:今天还有剩余知识点未学? + ├── 是 → 提醒用户剩余知识点,结束本次交互(不出练习) + └── 否(全部学完)→ 即时练习(铁律二) + ↓ + (用户答应)→ 调 `study_buddy` 工具的 `exam_take` + 传入当天所有知识点 knowledge_id,拿测验链接给用户 +晚间(cron 触发): + 晚间复盘 → 调 `study_buddy_supervise` 工具的 `study_check` action 拿今日战绩 → 综合判断 → 更新 USER.md + (打勾 [x] 与 `练过: 是` 同步 / 答对 X/Y / 调整后续 DAY) +``` + +**关键原则**: +- **白天只更新"学完"字段**(用户口头表达),**不动"练过"和 `[x]`**——"嘴上说学完" ≠ "真练过" +- **打勾 `[x]`(与"练过"同步)、答对战绩、DAY 调整**统一在**晚间复盘**做(数据来源:`study_check`) +- 用户说"学完了"千万不要回一句"哦,记下来了"就结束——先判断是否全部学完,**全部学完时必须走即时练习**;部分学完则提醒剩余知识点 + +### 晨间推送格式 + +1. **读 USER.md**,找到今天对应的 DAY(按用户的学习节奏算),列出该 DAY 下的所有项目和知识点 + - 多项目场景:同一个 DAY 下可能跨多个项目,**都要列** +2. **拿"今日待学"链接**: + - 取今天**第一个**知识点的 `knowledge_id` + - 调 `study_buddy` 工具的 `list_leaves` action(传 `knowledge_id`),从返回中取该知识点的 `share_url` +3. **输出内容**: + - **今日待学知识点列表**(每行:项目名称 + 知识点名称) + - 一两句"为什么今天学这个"的背景说明,激发动机,自然口吻 + - **`**今日待学:[project_name](share_url)**`**(铁律三) + +### 晚间复盘格式 + +> 一天的状态结算**集中在这里完成**(打勾 `[x]`、`练过: 是`、`答对` 战绩、DAY 调整)。白天只动 `学完` 字段,其他状态字段都等晚间。 + +**第一步:拿数据 + 更新 USER.md** + +1. 从 USER.md "学习项目"分区取**所有"状态:进行中"项目的 `project_id`**(1 个或多个),一次性传给 `study_buddy_supervise` 工具的 `study_check` action 拿今日战绩数据 +2. **更新 USER.md**(按 `study_check` 返回): + - **`练过` + 打勾**:今天真正练过的知识点 → `练过: 否` 改为 `练过: 是`,**同时** `[ ]` 改成 `[x]`(两者必须同步) + - **答对战绩**:把每个涉及的知识点 `答对: X/Y` 累加更新 + - **DAY 调整**:超额完成 → 后续叶子 DAY 往前挪;落后 → 顺延重排 + - **薄弱知识点更新**:扫"学习项目"分区所有知识点的 `答对: X/Y`,**错误次数 = Y - X**。错误次数 ≥ 3 且**未在 USER.md 第 3 节"薄弱知识点"中**的 → 写入第 3 节(来源=`study-buddy`)。已在表中的更新错误次数。**只增不减,不解除**。 +3. 按 cron message 中的 3 条软性规则逐条执行(命中才介入:项目完成检查等) + +**第二步:输出复盘给用户**(自然口吻,有温度,不模板化) + +按以下顺序输出三块内容: + +**① 今日计划完成情况**(必输出,三选一) +- ✅ 完成 +- 🟡 部分完成 +- ⚪ 未开始 + +**② 今日知识点完成明细**(仅当"部分完成"或"完成"时输出;"未开始"时跳过) + +从 USER.md 取**今日计划学习的所有知识点**,按下表格式输出(按项目分组,多项目时也写在同一张表里): + +| 项目 | 知识点 | 学完 | 练过 | 答对 | +|------|--------|------|------|------| +| LLM 入门 | Transformer 架构 | 是 | 是 | 7/10 | +| LLM 入门 | 注意力机制 | 是 | 否 | 4/8 | +| 日语 N2 | 接续助词 | 否 | 否 | 0/0 | + +**③ 整体总结 + 建议**(必输出,2~4 句) +- 总结:今天做得怎么样(实打实,不夸张、不模板) +- 建议:根据完成度 / 战绩 / 节奏给出**1 条具体建议**(不要堆建议,挑最该说的那条) +- 情绪刻度按用户当下状态选:完成漂亮用 [3] 认真表扬;落后用 [4] 直接施压;崩溃用 [6] 情感共情 + +**④ 成就掉落检查**(可选,命中才输出) + +按"模块4 成就掉落"表格逐条检查触发条件(连续打卡天数、深夜学习、攻克难点、提前完成、项目首通、遗忘曲线全对等)。**命中任何一条** → 在复盘后追加一段: +``` +🎉 解锁成就:【勋章名】 +[只说给这个人听的一句话,不能套模板] +``` +> 没命中就别硬掉勋章——稀缺感是这套机制的灵魂。 + +### 进度追踪(白天) + +> ⚠️ **白天只更新"学完"字段,不动"练过"和 `[x]`**——"嘴上说学完" ≠ "真练过",后两者等晚间复盘结算。 + +白天做四件事(顺序): + +1. **确认范围**——用户的"学完了"可能指**单个知识点**也可能指**今天全部**。如果用户原话没说清,**主动问一句**: + - "是今天计划的全部学完了,还是某一个?" + - 用户回复后再动手,**不要替用户脑补** +2. **批量更新 USER.md**:把用户确认的知识点(1 个 / 多个 / 今天全部)逐个找到,把 `学完: 否` 改成 `学完: 是`(通过 `knowledge_id` 定位) +3. **追加写进日记** `memory/YYYY-MM-DD.md`(事件流水,**严禁覆盖**): + ```markdown + ## [YYYY-MM-DD] 学习记录 + + - [HH:MM] 今天学了 N 个知识点 + - [HH:MM] 今天学了 M 个知识点 + ``` + > 只记**事件**(什么时候说学完了几个),知识点明细 USER.md 已经有了,不在这里重复。 +4. **判断是否全部学完**(对照 USER.md 今日计划的知识点列表): + - **还有剩余知识点未学** → 提醒用户还剩哪些,**不出练习**,结束本次交互 + - **当天全部学完** → 走即时练习(铁律二) + +### 即时练习 +1. 按铁律二,**当天全部学完时**必须主动建议用户做练习(话术见铁律二:"趁热打铁 + 价值钩子") +2. 用户有意愿后: + - 从 USER.md 收集**当天计划学习的所有知识点**的 `knowledge_id`,组成数组 + - 调 `study_buddy` 工具的 `exam_take` action,传入该 `knowledge_id` 数组,获取一个覆盖全天的综合测试链接 + - 输出(铁律三):`**今日练习:[标题](链接)**` + - 告诉用户:"点链接去答,答完今晚复盘的时候我会一起看战绩。" +3. **不要原地等战绩**——`exam_take` 返回的是跳转链接,用户在外部平台答题,当下拿不到结果,战绩统一晚间调 `study_buddy_supervise` 工具的 `study_check` action 取 + +--- +### 主动报告查询 + +> 用户主动询问任意时间段的学习报告时执行(日报/周报/月报/项目总结/自定义范围)。晨间/晚间 cron 走各自模块,不走这里。 + +#### 时间解析提示 + +- 「上周/本月/X 月份」按**自然周/月**算(不是"近 N 天") +- 「国庆/春节/寒暑假」等暧昧节假日 → **必须反问**:"你说的是几号到几号?" +- 「最近 N 天」= 今天往前 N 天 +- 项目总结 = 项目开始日期 ~ 今天 + +**边界**:含未来日期 → 截到今天并告知;超出项目周期 → 截到项目起点并告知;范围内无数据 → "这段时间还没产生学习数据"。 + +#### 执行步骤 + +1. 解析时间范围(暧昧必反问) +2. 从 USER.md 取该时间段**对应的** `project_id` + `knowledge_id` +3. 调 `study_buddy_supervise` 工具的 `study_check` action,传入第 2 步的列表 +4. 按下方格式组装输出 + +#### 输出格式(复用"晚间复盘格式"的 ②③④) + +**① 完成情况**(按时间长度自适应) +- 单日 → `✅完成` / `🟡部分完成` / `⚪未开始` +- 多日 → `📊 完成 X/Y 天 · 知识点 A/B 个(XX%)` +- 项目总结 → `📊 项目完成 A/B 个知识点(XX%)· 已学 N 天` + +**②③④** 完全复用晚间复盘格式: +- ② 知识点完成明细表(表头改"今日" → "该时间段";练过/答对取自 `study_check`,学完取自 USER.md) +- ③ 整体总结 + 1 条建议 +- ④ 成就掉落(命中才出,禁止硬掉) + +#### 🚫 只读不写 + +全程**不写 USER.md**(练过、打勾、DAY 调整、薄弱知识点、答对 X/Y 都不动)——这些写动作由晚间复盘 cron 独占。 + +#### "看进度" ≠ "看报告" + +用户说"我学到哪了""看下进度"等**轻量意图**(不带"报告/复盘/总结"等词)→ **不走本节、不调 study_check**,从 USER.md 拼一句话即可: +> "你在 LLM 入门 DAY 5/14,今天还剩 2 个知识点要学。" + +#### 用户询问"完成了哪些知识点 / 学了多少 / 能看到进度吗"等 + +**🔴 铁律补充:不允许回复"看不到/无法查看"**——USER.md 里记录了每个知识点的 `学完`、`练过`、`答对` 字段,数据就在那里。 + +**处理方式**: +1. 从 USER.md 读取当前进行中项目的知识点叶子列表,**直接汇报**哪些已完成(`学完: 是`)、哪些未完成(`学完: 否`),以及练过/答对情况 +2. **同时告知用户**:"我每天晚间复盘的时候会调用 study_check 核对你的练习战绩,所以练过的和答对的更完整的数据会在晚上更新。"(用自己的话自然地说,不要照搬模板) +3. ⚠️ **不要调 `study_check`**——练习战绩的读取只在晚间复盘和主动报告查询时执行,白天问进度只读 USER.md + +--- + +## 模块4:情绪支持与成就 + +### 成就掉落 + +| 触发条件 | 勋章 | +|----------|------| +| 连续7天打卡 | 【七日不断更】🔥 | +| 深夜仍在学习 | 【深夜研究员】🌙 | +| 攻克卡了多天的难点 | 【硬骨头猎人】🦴 | +| 提前完成当日计划 | 【计划粉碎机】⚡ | +| 完成第一个学习项目 | 【开荒先锋】🗺️ | +| 遗忘曲线到期全答对 | 【遗忘曲线克星】🧠 | + +每次掉落必须配一句**只说给这个人**的话,不能有复制粘贴感。 + +### 崩溃/想放弃(刻度[6]) +1. 一句话共情,不过度 +2. 问清卡点 +3. 给最小行动:"现在只做这一件事:[一个具体步骤]" + +--- + +## 模块5:项目推荐 + +**触发时机**(满足任一即可): +- 用户完成一个项目 +- 用户主动表达需要推荐学习资料/项目的意图("接下来学啥""推荐个项目""有啥可以学的"等,凭语境判断,不限措辞) + +**流程**: + +1. **优先调** `study_buddy` 工具的 `recommend` action 拿推荐结果——根据用户意图决定推荐依据: + - 用户**没指定方向**(项目完成后被动触发 / 只说"接下来学啥") → 基于已有学习数据推荐 + - 用户**明确指定方向**("推荐个 LLM 相关的"/"想学日语"/"找点设计资料") → **基于该方向**调 `recommend`,把方向作为参数传进去 + - 从返回中取 **`name`**(项目名)和 **`share_url`**(项目链接),用于输出 +2. `recommend` **返回为空或没合适的** → 才 fallback 走搜索: + - 分析已有知识结构 + - 按"相关领域深化 > 互补技能扩展 > 兴趣方向延伸"的优先级搜 +3. 给有说服力的理由(不强迫) +4. **输出格式**(一次最多推荐 3 个,统一用铁律一的"资料 / 来源 / 说明"三列表格): + - **只有 1 个** → 单行输出 `**推荐项目:[name](share_url)**` + 简短的推荐理由(铁律三) + - **2 个或 3 个** → 用表格展示: + + | 资料 | 来源 | 说明 | + |------|------|------| + | [name1](share_url1) | 官方推荐 / 网络搜索 | 简短推荐理由 | + | [name2](share_url2) | 官方推荐 / 网络搜索 | 简短推荐理由 | + | [name3](share_url3) | 官方推荐 / 网络搜索 | 简短推荐理由 | + + - **"来源"列规则**: + - 来自 `recommend` action 的 → 写 **`官方推荐`** + - 来自 fallback 搜索的 → 写域名/平台名(同铁律一) +5. 两条路都没找到 → 告诉用户"暂时没找到合适的,你想往哪个方向发展?" + +--- + +## 模块6:日常问答 + +- 优先结合用户当前项目上下文作答 +- 超出知识范围 → 主动搜索(铁律一),别说"我不确定" +- 推荐资料附简短理由 + 链接(铁律三),不堆砌 +- 学不下去时:共情 → 卡点 → 最小行动 + +--- + +## 模块7:动态调整 + +- **计划太难/太轻松**:重新评估 → 调整计划 → 更新 USER.md → 同步修改 cron +- **用户换项目/学新东西**:回到模块2 完整跑一遍 → 更新 USER.md → 同步修改 cron + +--- + +## 底线规则 + +1. **不撒谎**:进度落后就说落后,不说"你做得很好" +2. **不过度共情**:接住情绪,但不陪沉浸在"太难了" +3. **不放弃用户**:消失三天回来照常对待,不翻旧账,但该说的说 +4. **不刷存在感**:没有推送任务时不主动发话 +5. **四条铁律**:违反即为执行失败 + +--- + +## 文件结构 + +``` +workspace/ +├── AGENTS.md / SOUL.md / USER.md / IDENTITY.md / TOOLS.md +├── memory/YYYY-MM-DD.md ← 每日学习日记 +└── skills/ + ├── study-buddy/SKILL.md ← 当前文件(学习项目主线) + ├── qingyan-research/SKILL.md ← 深度网研,把搜索内容生成 HTML 研究报告(步骤2 调用) + ├── pdf/SKILL.md ← HTML → PDF 转换,把研究报告转成 PDF 给 create_project(步骤2 调用) + ├── web-search/ ← 搜索工具(铁律一调用) + └── web-reader/ ← 网页读取工具(铁律一调用) +``` \ No newline at end of file diff --git a/skills/task-review/SKILL.md b/skills/task-review/SKILL.md new file mode 100755 index 0000000..84b1ec9 --- /dev/null +++ b/skills/task-review/SKILL.md @@ -0,0 +1,49 @@ +--- +name: task-review +description: 当用户指令为高复杂度任务时触发,用于将刚完成的任务路径保存为可复用技能,生成相关的SKILL.md文档。 +--- + +## 触发时机 + +每次任务完成、准备向用户交付结果时,先执行以下检查。不要告诉用户你在做这个检查。 + +## 执行步骤 + +1. 快速判断本次任务是否满足以下任一条件: + - 调用了 5 次及以上的工具 + - 执行过程中遇到了错误或死胡同,最终找到了正确路径 + - 任务包含 3 个及以上的明确步骤 + +2. 如果不满足以上任何条件,跳过,不做任何操作。 + +3. 如果满足,继续评估: + - 这个任务未来是否可能被重复或有类似变体? + - 这次执行过程中,是否有可复用的流程、结构、注意事项? + +4. 如果任意一个为"是",则沉淀为技能: + - 先检查 skills/ 目录下是否已有匹配的技能文件 + - 如果有:将新经验、踩坑记录**追加更新**到原文件,不要新建 + - 如果没有:新建 `skills/SKILL-{技能名称}/SKILL.md`,格式如下: + +``` +--- +name: 技能名称 +description: 一句话描述适用场景,要具体到能让 agent 自动匹配任务 +--- + +(执行步骤、质量标准、踩坑记录) +``` + +5. 沉淀了新技能时,必须在回复末尾简短提一句,例如"💡 本次经验已沉淀为新技能:{技能名称}"。 +6. 更新了已有技能时,必须在回复末尾简短提一句,例如"💡 本次经验已更新到技能中:{技能名称}"。 +7. 未沉淀或者更新时,不提及任何关于此检查的内容。 + +## 质量标准 + +- description 必须具体,写成用户会说的话。好的例子:"监测全球AI新闻并生成HTML简报"。坏的例子:"处理信息相关任务"。 +- 执行步骤必须具体到照着做就能复现的程度。 +- 踩坑记录只记真正踩过的坑,不要编造。 + +## 踩坑记录 + +(暂无,随使用积累) diff --git a/skills/ui-ux-pro-max/SKILL.md b/skills/ui-ux-pro-max/SKILL.md new file mode 100755 index 0000000..b94ef59 --- /dev/null +++ b/skills/ui-ux-pro-max/SKILL.md @@ -0,0 +1,43 @@ +--- +name: ui-ux-pro-max +description: UI/UX design intelligence and implementation guidance for building polished interfaces. Use when the user asks for UI design, UX flows, information architecture, visual style direction, design systems/tokens, component specs, copy/microcopy, accessibility, or to generate/critique/refine frontend UI (HTML/CSS/JS, React, Next.js, Vue, Svelte, Tailwind). Includes workflows for (1) generating new UI layouts and styling, (2) improving existing UI/UX, (3) producing design-system tokens and component guidelines, and (4) turning UX recommendations into concrete code changes. +--- + +Follow these steps to deliver high-quality UI/UX output with minimal back-and-forth. + +## 1) Triage +Ask only what you must to avoid wrong work: +- Target platform: web / iOS / Android / desktop +- Stack (if code changes): React/Next/Vue/Svelte, CSS/Tailwind, component library +- Goal and constraints: conversion, speed, brand vibe, accessibility level (WCAG AA?) +- What you have: screenshot, Figma, repo, URL, user journey + +If the user says "全部都要" (design + UX + code + design system), treat it as four deliverables and ship in that order. + +## 2) Produce Deliverables (pick what fits) +Always be concrete: name components, states, spacing, typography, and interactions. + +- **UI concept + layout**: Provide a clear visual direction, grid, typography, color system, key screens/sections. +- **UX flow**: Map the user journey, critical paths, error/empty/loading states, edge cases. +- **Design system**: Tokens (color/typography/spacing/radius/shadow), component rules, accessibility notes. +- **Implementation plan**: Exact file-level edits, component breakdown, and acceptance criteria. + +## 3) Use Bundled Assets +This skill bundles data you can cite for inspiration/standards. + +- **Design intelligence data**: Read from `skills/ui-ux-pro-max/assets/data/` when you need palettes, patterns, or UI/UX heuristics. +- **Upstream reference**: If you need more phrasing/examples, consult `skills/ui-ux-pro-max/references/upstream-skill-content.md`. + +## 4) Optional Script (Design System Generator) +If you need to quickly generate tokens and page-specific overrides, use the bundled script: + +```bash +python3 skills/ui-ux-pro-max/scripts/design_system.py --help +``` + +Prefer running it when the user wants a structured token output (ASCII-friendly). + +## Output Standards +- Default to ASCII-only tokens/variables unless the project already uses Unicode. +- Include: spacing scale, type scale, 2-3 font pair options, color tokens, component states. +- Always cover: empty/loading/error, keyboard navigation, focus states, contrast. diff --git a/skills/ui-ux-pro-max/_meta.json b/skills/ui-ux-pro-max/_meta.json new file mode 100755 index 0000000..71bf465 --- /dev/null +++ b/skills/ui-ux-pro-max/_meta.json @@ -0,0 +1,6 @@ +{ + "ownerId": "kn7dzk42mb9ceh8sm1zdx51sws803c9b", + "slug": "ui-ux-pro-max", + "version": "0.1.0", + "publishedAt": 1769616159343 +} \ No newline at end of file diff --git a/skills/ui-ux-pro-max/assets/data/charts.csv b/skills/ui-ux-pro-max/assets/data/charts.csv new file mode 100755 index 0000000..8b99663 --- /dev/null +++ b/skills/ui-ux-pro-max/assets/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 +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 diff --git a/skills/ui-ux-pro-max/assets/data/colors.csv b/skills/ui-ux-pro-max/assets/data/colors.csv new file mode 100755 index 0000000..d9fd043 --- /dev/null +++ b/skills/ui-ux-pro-max/assets/data/colors.csv @@ -0,0 +1,97 @@ +No,Product Type,Primary (Hex),Secondary (Hex),CTA (Hex),Background (Hex),Text (Hex),Border (Hex),Notes +1,SaaS (General),#2563EB,#3B82F6,#F97316,#F8FAFC,#1E293B,#E2E8F0,Trust blue + orange CTA contrast +2,Micro SaaS,#6366F1,#818CF8,#10B981,#F5F3FF,#1E1B4B,#E0E7FF,Indigo primary + emerald CTA +3,E-commerce,#059669,#10B981,#F97316,#ECFDF5,#064E3B,#A7F3D0,Success green + urgency orange +4,E-commerce Luxury,#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium dark + gold accent +5,Service Landing Page,#0EA5E9,#38BDF8,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Sky blue trust + warm CTA +6,B2B Service,#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,Professional navy + blue CTA +7,Financial Dashboard,#0F172A,#1E293B,#22C55E,#020617,#F8FAFC,#334155,Dark bg + green positive indicators +8,Analytics Dashboard,#1E40AF,#3B82F6,#F59E0B,#F8FAFC,#1E3A8A,#DBEAFE,Blue data + amber highlights +9,Healthcare App,#0891B2,#22D3EE,#059669,#ECFEFF,#164E63,#A5F3FC,Calm cyan + health green +10,Educational App,#4F46E5,#818CF8,#F97316,#EEF2FF,#1E1B4B,#C7D2FE,Playful indigo + energetic orange +11,Creative Agency,#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold pink + cyan accent +12,Portfolio/Personal,#18181B,#3F3F46,#2563EB,#FAFAFA,#09090B,#E4E4E7,Monochrome + blue accent +13,Gaming,#7C3AED,#A78BFA,#F43F5E,#0F0F23,#E2E8F0,#4C1D95,Neon purple + rose action +14,Government/Public Service,#0F172A,#334155,#0369A1,#F8FAFC,#020617,#E2E8F0,High contrast navy + blue +15,Fintech/Crypto,#F59E0B,#FBBF24,#8B5CF6,#0F172A,#F8FAFC,#334155,Gold trust + purple tech +16,Social Media App,#E11D48,#FB7185,#2563EB,#FFF1F2,#881337,#FECDD3,Vibrant rose + engagement blue +17,Productivity Tool,#0D9488,#14B8A6,#F97316,#F0FDFA,#134E4A,#99F6E4,Teal focus + action orange +18,Design System/Component Library,#4F46E5,#6366F1,#F97316,#EEF2FF,#312E81,#C7D2FE,Indigo brand + doc hierarchy +19,AI/Chatbot Platform,#7C3AED,#A78BFA,#06B6D4,#FAF5FF,#1E1B4B,#DDD6FE,AI purple + cyan interactions +20,NFT/Web3 Platform,#8B5CF6,#A78BFA,#FBBF24,#0F0F23,#F8FAFC,#4C1D95,Purple tech + gold value +21,Creator Economy Platform,#EC4899,#F472B6,#F97316,#FDF2F8,#831843,#FBCFE8,Creator pink + engagement orange +22,Sustainability/ESG Platform,#059669,#10B981,#0891B2,#ECFDF5,#064E3B,#A7F3D0,Nature green + ocean blue +23,Remote Work/Collaboration Tool,#6366F1,#818CF8,#10B981,#F5F3FF,#312E81,#E0E7FF,Calm indigo + success green +24,Mental Health App,#8B5CF6,#C4B5FD,#10B981,#FAF5FF,#4C1D95,#EDE9FE,Calming lavender + wellness green +25,Pet Tech App,#F97316,#FB923C,#2563EB,#FFF7ED,#9A3412,#FED7AA,Playful orange + trust blue +26,Smart Home/IoT Dashboard,#1E293B,#334155,#22C55E,#0F172A,#F8FAFC,#475569,Dark tech + status green +27,EV/Charging Ecosystem,#0891B2,#22D3EE,#22C55E,#ECFEFF,#164E63,#A5F3FC,Electric cyan + eco green +28,Subscription Box Service,#D946EF,#E879F9,#F97316,#FDF4FF,#86198F,#F5D0FE,Excitement purple + urgency orange +29,Podcast Platform,#1E1B4B,#312E81,#F97316,#0F0F23,#F8FAFC,#4338CA,Dark audio + warm accent +30,Dating App,#E11D48,#FB7185,#F97316,#FFF1F2,#881337,#FECDD3,Romantic rose + warm orange +31,Micro-Credentials/Badges Platform,#0369A1,#0EA5E9,#CA8A04,#F0F9FF,#0C4A6E,#BAE6FD,Trust blue + achievement gold +32,Knowledge Base/Documentation,#475569,#64748B,#2563EB,#F8FAFC,#1E293B,#E2E8F0,Neutral grey + link blue +33,Hyperlocal Services,#059669,#10B981,#F97316,#ECFDF5,#064E3B,#A7F3D0,Location green + action orange +34,Beauty/Spa/Wellness Service,#EC4899,#F9A8D4,#8B5CF6,#FDF2F8,#831843,#FBCFE8,Soft pink + lavender luxury +35,Luxury/Premium Brand,#1C1917,#44403C,#CA8A04,#FAFAF9,#0C0A09,#D6D3D1,Premium black + gold accent +36,Restaurant/Food Service,#DC2626,#F87171,#CA8A04,#FEF2F2,#450A0A,#FECACA,Appetizing red + warm gold +37,Fitness/Gym App,#F97316,#FB923C,#22C55E,#1F2937,#F8FAFC,#374151,Energy orange + success green +38,Real Estate/Property,#0F766E,#14B8A6,#0369A1,#F0FDFA,#134E4A,#99F6E4,Trust teal + professional blue +39,Travel/Tourism Agency,#0EA5E9,#38BDF8,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Sky blue + adventure orange +40,Hotel/Hospitality,#1E3A8A,#3B82F6,#CA8A04,#F8FAFC,#1E40AF,#BFDBFE,Luxury navy + gold service +41,Wedding/Event Planning,#DB2777,#F472B6,#CA8A04,#FDF2F8,#831843,#FBCFE8,Romantic pink + elegant gold +42,Legal Services,#1E3A8A,#1E40AF,#B45309,#F8FAFC,#0F172A,#CBD5E1,Authority navy + trust gold +43,Insurance Platform,#0369A1,#0EA5E9,#22C55E,#F0F9FF,#0C4A6E,#BAE6FD,Security blue + protected green +44,Banking/Traditional Finance,#0F172A,#1E3A8A,#CA8A04,#F8FAFC,#020617,#E2E8F0,Trust navy + premium gold +45,Online Course/E-learning,#0D9488,#2DD4BF,#F97316,#F0FDFA,#134E4A,#5EEAD4,Progress teal + achievement orange +46,Non-profit/Charity,#0891B2,#22D3EE,#F97316,#ECFEFF,#164E63,#A5F3FC,Compassion blue + action orange +47,Music Streaming,#1E1B4B,#4338CA,#22C55E,#0F0F23,#F8FAFC,#312E81,Dark audio + play green +48,Video Streaming/OTT,#0F0F23,#1E1B4B,#E11D48,#000000,#F8FAFC,#312E81,Cinema dark + play red +49,Job Board/Recruitment,#0369A1,#0EA5E9,#22C55E,#F0F9FF,#0C4A6E,#BAE6FD,Professional blue + success green +50,Marketplace (P2P),#7C3AED,#A78BFA,#22C55E,#FAF5FF,#4C1D95,#DDD6FE,Trust purple + transaction green +51,Logistics/Delivery,#2563EB,#3B82F6,#F97316,#EFF6FF,#1E40AF,#BFDBFE,Tracking blue + delivery orange +52,Agriculture/Farm Tech,#15803D,#22C55E,#CA8A04,#F0FDF4,#14532D,#BBF7D0,Earth green + harvest gold +53,Construction/Architecture,#64748B,#94A3B8,#F97316,#F8FAFC,#334155,#E2E8F0,Industrial grey + safety orange +54,Automotive/Car Dealership,#1E293B,#334155,#DC2626,#F8FAFC,#0F172A,#E2E8F0,Premium dark + action red +55,Photography Studio,#18181B,#27272A,#F8FAFC,#000000,#FAFAFA,#3F3F46,Pure black + white contrast +56,Coworking Space,#F59E0B,#FBBF24,#2563EB,#FFFBEB,#78350F,#FDE68A,Energetic amber + booking blue +57,Cleaning Service,#0891B2,#22D3EE,#22C55E,#ECFEFF,#164E63,#A5F3FC,Fresh cyan + clean green +58,Home Services (Plumber/Electrician),#1E40AF,#3B82F6,#F97316,#EFF6FF,#1E3A8A,#BFDBFE,Professional blue + urgent orange +59,Childcare/Daycare,#F472B6,#FBCFE8,#22C55E,#FDF2F8,#9D174D,#FCE7F3,Soft pink + safe green +60,Senior Care/Elderly,#0369A1,#38BDF8,#22C55E,#F0F9FF,#0C4A6E,#E0F2FE,Calm blue + reassuring green +61,Medical Clinic,#0891B2,#22D3EE,#22C55E,#F0FDFA,#134E4A,#CCFBF1,Medical teal + health green +62,Pharmacy/Drug Store,#15803D,#22C55E,#0369A1,#F0FDF4,#14532D,#BBF7D0,Pharmacy green + trust blue +63,Dental Practice,#0EA5E9,#38BDF8,#FBBF24,#F0F9FF,#0C4A6E,#BAE6FD,Fresh blue + smile yellow +64,Veterinary Clinic,#0D9488,#14B8A6,#F97316,#F0FDFA,#134E4A,#99F6E4,Caring teal + warm orange +65,Florist/Plant Shop,#15803D,#22C55E,#EC4899,#F0FDF4,#14532D,#BBF7D0,Natural green + floral pink +66,Bakery/Cafe,#92400E,#B45309,#F8FAFC,#FEF3C7,#78350F,#FDE68A,Warm brown + cream white +67,Coffee Shop,#78350F,#92400E,#FBBF24,#FEF3C7,#451A03,#FDE68A,Coffee brown + warm gold +68,Brewery/Winery,#7C2D12,#B91C1C,#CA8A04,#FEF2F2,#450A0A,#FECACA,Deep burgundy + craft gold +69,Airline,#1E3A8A,#3B82F6,#F97316,#EFF6FF,#1E40AF,#BFDBFE,Sky blue + booking orange +70,News/Media Platform,#DC2626,#EF4444,#1E40AF,#FEF2F2,#450A0A,#FECACA,Breaking red + link blue +71,Magazine/Blog,#18181B,#3F3F46,#EC4899,#FAFAFA,#09090B,#E4E4E7,Editorial black + accent pink +72,Freelancer Platform,#6366F1,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Creative indigo + hire green +73,Consulting Firm,#0F172A,#334155,#CA8A04,#F8FAFC,#020617,#E2E8F0,Authority navy + premium gold +74,Marketing Agency,#EC4899,#F472B6,#06B6D4,#FDF2F8,#831843,#FBCFE8,Bold pink + creative cyan +75,Event Management,#7C3AED,#A78BFA,#F97316,#FAF5FF,#4C1D95,#DDD6FE,Excitement purple + action orange +76,Conference/Webinar Platform,#1E40AF,#3B82F6,#22C55E,#EFF6FF,#1E3A8A,#BFDBFE,Professional blue + join green +77,Membership/Community,#7C3AED,#A78BFA,#22C55E,#FAF5FF,#4C1D95,#DDD6FE,Community purple + join green +78,Newsletter Platform,#0369A1,#0EA5E9,#F97316,#F0F9FF,#0C4A6E,#BAE6FD,Trust blue + subscribe orange +79,Digital Products/Downloads,#6366F1,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Digital indigo + buy green +80,Church/Religious Organization,#7C3AED,#A78BFA,#CA8A04,#FAF5FF,#4C1D95,#DDD6FE,Spiritual purple + warm gold +81,Sports Team/Club,#DC2626,#EF4444,#FBBF24,#FEF2F2,#7F1D1D,#FECACA,Team red + championship gold +82,Museum/Gallery,#18181B,#27272A,#F8FAFC,#FAFAFA,#09090B,#E4E4E7,Gallery black + white space +83,Theater/Cinema,#1E1B4B,#312E81,#CA8A04,#0F0F23,#F8FAFC,#4338CA,Dramatic dark + spotlight gold +84,Language Learning App,#4F46E5,#818CF8,#22C55E,#EEF2FF,#312E81,#C7D2FE,Learning indigo + progress green +85,Coding Bootcamp,#0F172A,#1E293B,#22C55E,#020617,#F8FAFC,#334155,Terminal dark + success green +86,Cybersecurity Platform,#00FF41,#0D0D0D,#FF3333,#000000,#E0E0E0,#1F1F1F,Matrix green + alert red +87,Developer Tool / IDE,#1E293B,#334155,#22C55E,#0F172A,#F8FAFC,#475569,Code dark + run green +88,Biotech / Life Sciences,#0EA5E9,#0284C7,#10B981,#F0F9FF,#0C4A6E,#BAE6FD,DNA blue + life green +89,Space Tech / Aerospace,#F8FAFC,#94A3B8,#3B82F6,#0B0B10,#F8FAFC,#1E293B,Star white + launch blue +90,Architecture / Interior,#171717,#404040,#D4AF37,#FFFFFF,#171717,#E5E5E5,Minimal black + accent gold +91,Quantum Computing,#00FFFF,#7B61FF,#FF00FF,#050510,#E0E0FF,#333344,Quantum cyan + interference purple +92,Biohacking / Longevity,#FF4D4D,#4D94FF,#00E676,#F5F5F7,#1C1C1E,#E5E5EA,Bio red/blue + vitality green +93,Autonomous Systems,#00FF41,#008F11,#FF3333,#0D1117,#E6EDF3,#30363D,Terminal green + alert red +94,Generative AI Art,#18181B,#3F3F46,#EC4899,#FAFAFA,#09090B,#E4E4E7,Canvas neutral + creative pink +95,Spatial / Vision OS,#FFFFFF,#E5E5E5,#007AFF,#888888,#000000,#CCCCCC,Glass white + system blue +96,Climate Tech,#059669,#10B981,#FBBF24,#ECFDF5,#064E3B,#A7F3D0,Nature green + solar gold diff --git a/skills/ui-ux-pro-max/assets/data/icons.csv b/skills/ui-ux-pro-max/assets/data/icons.csv new file mode 100755 index 0000000..a85e97f --- /dev/null +++ b/skills/ui-ux-pro-max/assets/data/icons.csv @@ -0,0 +1,101 @@ +No,Category,Icon Name,Keywords,Library,Import Code,Usage,Best For,Style +1,Navigation,menu,hamburger menu navigation toggle bars,Lucide,import { Menu } from 'lucide-react',,Mobile navigation drawer toggle sidebar,Outline +2,Navigation,arrow-left,back previous return navigate,Lucide,import { ArrowLeft } from 'lucide-react',,Back button breadcrumb navigation,Outline +3,Navigation,arrow-right,next forward continue navigate,Lucide,import { ArrowRight } from 'lucide-react',,Forward button next step CTA,Outline +4,Navigation,chevron-down,dropdown expand accordion select,Lucide,import { ChevronDown } from 'lucide-react',,Dropdown toggle accordion header,Outline +5,Navigation,chevron-up,collapse close accordion minimize,Lucide,import { ChevronUp } from 'lucide-react',,Accordion collapse minimize,Outline +6,Navigation,home,homepage main dashboard start,Lucide,import { Home } from 'lucide-react',,Home navigation main page,Outline +7,Navigation,x,close cancel dismiss remove exit,Lucide,import { X } from 'lucide-react',,Modal close dismiss button,Outline +8,Navigation,external-link,open new tab external link,Lucide,import { ExternalLink } from 'lucide-react',,External link indicator,Outline +9,Action,plus,add create new insert,Lucide,import { Plus } from 'lucide-react',,Add button create new item,Outline +10,Action,minus,remove subtract decrease delete,Lucide,import { Minus } from 'lucide-react',,Remove item quantity decrease,Outline +11,Action,trash-2,delete remove discard bin,Lucide,import { Trash2 } from 'lucide-react',,Delete action destructive,Outline +12,Action,edit,pencil modify change update,Lucide,import { Edit } from 'lucide-react',,Edit button modify content,Outline +13,Action,save,disk store persist save,Lucide,import { Save } from 'lucide-react',,Save button persist changes,Outline +14,Action,download,export save file download,Lucide,import { Download } from 'lucide-react',,Download file export,Outline +15,Action,upload,import file attach upload,Lucide,import { Upload } from 'lucide-react',,Upload file import,Outline +16,Action,copy,duplicate clipboard paste,Lucide,import { Copy } from 'lucide-react',,Copy to clipboard,Outline +17,Action,share,social distribute send,Lucide,import { Share } from 'lucide-react',,Share button social,Outline +18,Action,search,find lookup filter query,Lucide,import { Search } from 'lucide-react',,Search input bar,Outline +19,Action,filter,sort refine narrow options,Lucide,import { Filter } from 'lucide-react',,Filter dropdown sort,Outline +20,Action,settings,gear cog preferences config,Lucide,import { Settings } from 'lucide-react',,Settings page configuration,Outline +21,Status,check,success done complete verified,Lucide,import { Check } from 'lucide-react',,Success state checkmark,Outline +22,Status,check-circle,success verified approved complete,Lucide,import { CheckCircle } from 'lucide-react',,Success badge verified,Outline +23,Status,x-circle,error failed cancel rejected,Lucide,import { XCircle } from 'lucide-react',,Error state failed,Outline +24,Status,alert-triangle,warning caution attention danger,Lucide,import { AlertTriangle } from 'lucide-react',,Warning message caution,Outline +25,Status,alert-circle,info notice information help,Lucide,import { AlertCircle } from 'lucide-react',,Info notice alert,Outline +26,Status,info,information help tooltip details,Lucide,import { Info } from 'lucide-react',,Information tooltip help,Outline +27,Status,loader,loading spinner processing wait,Lucide,import { Loader } from 'lucide-react',,Loading state spinner,Outline +28,Status,clock,time schedule pending wait,Lucide,import { Clock } from 'lucide-react',,Pending time schedule,Outline +29,Communication,mail,email message inbox letter,Lucide,import { Mail } from 'lucide-react',,Email contact inbox,Outline +30,Communication,message-circle,chat comment bubble conversation,Lucide,import { MessageCircle } from 'lucide-react',,Chat comment message,Outline +31,Communication,phone,call mobile telephone contact,Lucide,import { Phone } from 'lucide-react',,Phone contact call,Outline +32,Communication,send,submit dispatch message airplane,Lucide,import { Send } from 'lucide-react',,Send message submit,Outline +33,Communication,bell,notification alert ring reminder,Lucide,import { Bell } from 'lucide-react',,Notification bell alert,Outline +34,User,user,profile account person avatar,Lucide,import { User } from 'lucide-react',,User profile account,Outline +35,User,users,team group people members,Lucide,import { Users } from 'lucide-react',,Team group members,Outline +36,User,user-plus,add invite new member,Lucide,import { UserPlus } from 'lucide-react',,Add user invite,Outline +37,User,log-in,signin authenticate enter,Lucide,import { LogIn } from 'lucide-react',,Login signin,Outline +38,User,log-out,signout exit leave logout,Lucide,import { LogOut } from 'lucide-react',,Logout signout,Outline +39,Media,image,photo picture gallery thumbnail,Lucide,import { Image } from 'lucide-react',,Image photo gallery,Outline +40,Media,video,movie film play record,Lucide,import { Video } from 'lucide-react',