Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a369bba4ec | ||
|
|
3377f4726b | ||
|
|
8f3814946d | ||
|
|
d5ebcf6db2 | ||
|
|
d4a0ad6ac1 | ||
|
|
57f74bbd8f | ||
|
|
ece228e8ed | ||
|
|
9727516926 | ||
|
|
83f5f59015 | ||
|
|
85376e1f1a | ||
|
|
dc4c398d51 | ||
|
|
ee66a4b381 | ||
|
|
6158723563 | ||
|
|
b48821309b | ||
|
|
80662d3d3b | ||
|
|
b493403fc3 | ||
|
|
3993122a4e | ||
|
|
ecbe117053 | ||
|
|
6ff4924780 | ||
|
|
de9caf6bc4 | ||
|
|
3c76930964 | ||
|
|
6480f4bf4e | ||
|
|
6ffeca07d9 | ||
|
|
ef37c26433 | ||
|
|
b4a13f5c3b | ||
|
|
f69fa354cc | ||
|
|
8883fe1f5f | ||
|
|
c9c8cead57 | ||
|
|
ab76e4ff50 | ||
|
|
074ba7afe4 | ||
|
|
17c94be327 | ||
|
|
eeaa147565 | ||
|
|
1750dd8a24 | ||
|
|
9fbd0598f1 | ||
|
|
5093809ea0 | ||
|
|
c5a756d707 | ||
|
|
0b576d5973 | ||
|
|
c4e934c9dc | ||
|
|
32ef9e84fe | ||
|
|
c0de3f968d | ||
|
|
84f93999c5 | ||
|
|
64a36bebd2 | ||
|
|
6ca431644d | ||
|
|
812ea23419 | ||
|
|
82f0b59355 | ||
|
|
3dd52110bb | ||
|
|
313a3c88a3 | ||
|
|
fb7341d82b | ||
|
|
f84bef1f91 | ||
|
|
695948271f | ||
|
|
286a95ba04 | ||
|
|
1b01919482 | ||
|
|
5b1fd81815 | ||
|
|
dbf80803cc | ||
|
|
870a1ad343 | ||
|
|
07b6403d94 | ||
|
|
59f389d0a6 | ||
|
|
8f6fd7cd29 | ||
|
|
4f4b5643d4 | ||
|
|
b577ec5453 | ||
|
|
0348897eb1 | ||
|
|
8337836199 | ||
|
|
27e277099f | ||
|
|
f5a655b2f9 | ||
|
|
fbafc5e53a | ||
|
|
88266eaaf4 | ||
|
|
3e657523ef | ||
|
|
00a7d1437c | ||
|
|
2b1343ab38 | ||
|
|
f01fdf3260 | ||
|
|
27abb31219 | ||
|
|
31ea0c9f52 | ||
|
|
354bd34c0c | ||
|
|
3b97056d01 | ||
|
|
320d5e4704 | ||
|
|
30f52990b1 | ||
|
|
3b29b5e9e7 | ||
|
|
1ff60b4681 | ||
|
|
5984eb4914 | ||
|
|
f24fda926a | ||
|
|
282f01e1f4 | ||
|
|
8ca291526c | ||
|
|
82c6999a63 | ||
|
|
2049998ed7 | ||
|
|
06714e5998 | ||
|
|
8ad17a4fe4 | ||
|
|
419dcf1ba2 | ||
|
|
c1ef6d6822 | ||
|
|
d30acd4fdd | ||
|
|
ae380b156b | ||
|
|
d6ae6f3256 | ||
|
|
03e379c3e7 | ||
|
|
cb7e719e92 | ||
|
|
42814ba6a6 | ||
|
|
d43ffe4f8a | ||
|
|
ecbd2b0bb9 | ||
|
|
683eb93bcc | ||
|
|
b81f0a3455 | ||
|
|
88ba42fd37 | ||
|
|
a572675900 | ||
|
|
3dc88472ee | ||
|
|
8c277f8320 | ||
|
|
d3c8709bd2 | ||
|
|
c1d044f91b | ||
|
|
c0c670b810 | ||
|
|
a19f53b2b4 | ||
|
|
6370c9d618 | ||
|
|
85329290f2 | ||
|
|
90828cef49 | ||
|
|
c7c4bd2cbe | ||
|
|
87b04cc86e | ||
|
|
e851432f4c | ||
|
|
472844e691 | ||
|
|
926486e37b | ||
|
|
d9fa314c20 | ||
|
|
168cad7470 | ||
|
|
ce28d95bcf | ||
|
|
e1fbb0ca36 | ||
|
|
46c95c7b59 | ||
|
|
70977bad84 | ||
|
|
fdd63b27a9 | ||
|
|
bdd7d5e866 | ||
|
|
6701503801 | ||
|
|
1c6ae6b2e2 | ||
|
|
d2d9fcd09e | ||
|
|
fc3673466d | ||
|
|
a65867fc39 | ||
|
|
8c564946ef | ||
|
|
7d65fa8754 | ||
|
|
dc131a311f | ||
|
|
769fc85e17 | ||
|
|
0795ef197c | ||
|
|
51c3ea0fde | ||
|
|
f8ce0f21ef | ||
|
|
25a4687611 | ||
|
|
2da091d6e9 | ||
|
|
674d4bd8c6 | ||
|
|
a50ca4b867 | ||
|
|
3bb28e5f29 | ||
|
|
b277ee3e0f | ||
|
|
9171a4f49c | ||
|
|
7fb9a1505c | ||
|
|
6112bd9ae6 | ||
|
|
0b77ede6ff | ||
|
|
3cc598ded5 | ||
|
|
55a6501a09 | ||
|
|
43d7ec5577 | ||
|
|
49181a1a8d | ||
|
|
a86a4622fb | ||
|
|
2b2cf79b9a | ||
|
|
c41c5f3132 | ||
|
|
6650f91eca | ||
|
|
d6e317c6fe | ||
|
|
76b91dc1e5 | ||
|
|
0a9dabfdc2 | ||
|
|
5cf4e589be | ||
|
|
671d2c8bb7 | ||
|
|
e8c81faad9 | ||
|
|
480a29aaea | ||
|
|
c549494488 | ||
|
|
74b0c65843 | ||
|
|
04037e98d6 | ||
|
|
36338b3b55 | ||
|
|
5c0425901c | ||
|
|
913b29efe5 | ||
|
|
1df95fdfdd | ||
|
|
8981456dfd | ||
|
|
8946209f98 | ||
|
|
624c91a437 | ||
|
|
24d1dda07d |
@@ -0,0 +1,266 @@
|
||||
---
|
||||
name: code-review-excellence
|
||||
description: Master effective code review practices to provide constructive feedback, catch bugs early, and foster knowledge sharing while maintaining team morale. Use when reviewing pull requests, establishing review standards, or mentoring developers.
|
||||
---
|
||||
|
||||
# Code Review Excellence
|
||||
|
||||
Transform code reviews from gatekeeping to knowledge sharing through constructive feedback, systematic analysis, and collaborative improvement.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Reviewing pull requests and code changes
|
||||
- Establishing code review standards for teams
|
||||
- Mentoring junior developers through reviews
|
||||
- Conducting architecture reviews
|
||||
- Creating review checklists and guidelines
|
||||
- Improving team collaboration
|
||||
- Reducing code review cycle time
|
||||
- Maintaining code quality standards
|
||||
|
||||
## Core Principles
|
||||
|
||||
### 1. The Review Mindset
|
||||
|
||||
**Goals of Code Review:**
|
||||
|
||||
- Catch bugs and edge cases
|
||||
- Ensure code maintainability
|
||||
- Share knowledge across team
|
||||
- Enforce coding standards
|
||||
- Improve design and architecture
|
||||
- Build team culture
|
||||
|
||||
**Not the Goals:**
|
||||
|
||||
- Show off knowledge
|
||||
- Nitpick formatting (use linters)
|
||||
- Block progress unnecessarily
|
||||
- Rewrite to your preference
|
||||
|
||||
### 2. Effective Feedback
|
||||
|
||||
**Good Feedback is:**
|
||||
|
||||
- Specific and actionable
|
||||
- Educational, not judgmental
|
||||
- Focused on the code, not the person
|
||||
- Balanced (praise good work too)
|
||||
- Prioritized (critical vs nice-to-have)
|
||||
|
||||
```markdown
|
||||
❌ Bad: "This is wrong."
|
||||
✅ Good: "This could cause a race condition when multiple users
|
||||
access simultaneously. Consider using a mutex here."
|
||||
|
||||
❌ Bad: "Why didn't you use X pattern?"
|
||||
✅ Good: "Have you considered the Repository pattern? It would
|
||||
make this easier to test. Here's an example: [link]"
|
||||
|
||||
❌ Bad: "Rename this variable."
|
||||
✅ Good: "[nit] Consider `userCount` instead of `uc` for
|
||||
clarity. Not blocking if you prefer to keep it."
|
||||
```
|
||||
|
||||
### 3. Review Scope
|
||||
|
||||
**What to Review:**
|
||||
|
||||
- Logic correctness and edge cases
|
||||
- Security vulnerabilities
|
||||
- Performance implications
|
||||
- Test coverage and quality
|
||||
- Error handling
|
||||
- Documentation and comments
|
||||
- API design and naming
|
||||
- Architectural fit
|
||||
|
||||
**What Not to Review Manually:**
|
||||
|
||||
- Code formatting (use Prettier, Black, etc.)
|
||||
- Import organization
|
||||
- Linting violations
|
||||
- Simple typos
|
||||
|
||||
## Review Process
|
||||
|
||||
### Phase 1: Context Gathering (2-3 minutes)
|
||||
|
||||
```markdown
|
||||
Before diving into code, understand:
|
||||
|
||||
1. Read PR description and linked issue
|
||||
2. Check PR size (>400 lines? Ask to split)
|
||||
3. Review CI/CD status (tests passing?)
|
||||
4. Understand the business requirement
|
||||
5. Note any relevant architectural decisions
|
||||
```
|
||||
|
||||
### Phase 2: High-Level Review (5-10 minutes)
|
||||
|
||||
```markdown
|
||||
1. **Architecture & Design**
|
||||
- Does the solution fit the problem?
|
||||
- Are there simpler approaches?
|
||||
- Is it consistent with existing patterns?
|
||||
- Will it scale?
|
||||
|
||||
2. **File Organization**
|
||||
- Are new files in the right places?
|
||||
- Is code grouped logically?
|
||||
- Are there duplicate files?
|
||||
|
||||
3. **Testing Strategy**
|
||||
- Are there tests?
|
||||
- Do tests cover edge cases?
|
||||
- Are tests readable?
|
||||
```
|
||||
|
||||
### Phase 3: Line-by-Line Review (10-20 minutes)
|
||||
|
||||
```markdown
|
||||
For each file:
|
||||
|
||||
1. **Logic & Correctness**
|
||||
- Edge cases handled?
|
||||
- Off-by-one errors?
|
||||
- Null/undefined checks?
|
||||
- Race conditions?
|
||||
|
||||
2. **Security**
|
||||
- Input validation?
|
||||
- SQL injection risks?
|
||||
- XSS vulnerabilities?
|
||||
- Sensitive data exposure?
|
||||
|
||||
3. **Performance**
|
||||
- N+1 queries?
|
||||
- Unnecessary loops?
|
||||
- Memory leaks?
|
||||
- Blocking operations?
|
||||
|
||||
4. **Maintainability**
|
||||
- Clear variable names?
|
||||
- Functions doing one thing?
|
||||
- Complex code commented?
|
||||
- Magic numbers extracted?
|
||||
```
|
||||
|
||||
### Phase 4: Summary & Decision (2-3 minutes)
|
||||
|
||||
```markdown
|
||||
1. Summarize key concerns
|
||||
2. Highlight what you liked
|
||||
3. Make clear decision:
|
||||
- ✅ Approve
|
||||
- 💬 Comment (minor suggestions)
|
||||
- 🔄 Request Changes (must address)
|
||||
4. Offer to pair if complex
|
||||
```
|
||||
|
||||
Before writing review feedback, read `references/review-techniques.md` for checklists, question framing, collaboration, and severity techniques.
|
||||
|
||||
Before language-specific, architectural, test-quality, or security review, read `references/review-patterns.md` for review patterns and checklists.
|
||||
|
||||
## Giving Difficult Feedback
|
||||
|
||||
### Pattern: The Sandwich Method (Modified)
|
||||
|
||||
```markdown
|
||||
Traditional: Praise + Criticism + Praise (feels fake)
|
||||
|
||||
Better: Context + Specific Issue + Helpful Solution
|
||||
|
||||
Example:
|
||||
"I noticed the payment processing logic is inline in the
|
||||
controller. This makes it harder to test and reuse.
|
||||
|
||||
[Specific Issue]
|
||||
The calculateTotal() function mixes tax calculation,
|
||||
discount logic, and database queries, making it difficult
|
||||
to unit test and reason about.
|
||||
|
||||
[Helpful Solution]
|
||||
Could we extract this into a PaymentService class? That
|
||||
would make it testable and reusable. I can pair with you
|
||||
on this if helpful."
|
||||
```
|
||||
|
||||
### Handling Disagreements
|
||||
|
||||
```markdown
|
||||
When author disagrees with your feedback:
|
||||
|
||||
1. **Seek to Understand**
|
||||
"Help me understand your approach. What led you to
|
||||
choose this pattern?"
|
||||
|
||||
2. **Acknowledge Valid Points**
|
||||
"That's a good point about X. I hadn't considered that."
|
||||
|
||||
3. **Provide Data**
|
||||
"I'm concerned about performance. Can we add a benchmark
|
||||
to validate the approach?"
|
||||
|
||||
4. **Escalate if Needed**
|
||||
"Let's get [architect/senior dev] to weigh in on this."
|
||||
|
||||
5. **Know When to Let Go**
|
||||
If it's working and not a critical issue, approve it.
|
||||
Perfection is the enemy of progress.
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Review Promptly**: Within 24 hours, ideally same day
|
||||
2. **Limit PR Size**: 200-400 lines max for effective review
|
||||
3. **Review in Time Blocks**: 60 minutes max, take breaks
|
||||
4. **Use Review Tools**: GitHub, GitLab, or dedicated tools
|
||||
5. **Automate What You Can**: Linters, formatters, security scans
|
||||
6. **Build Rapport**: Emoji, praise, and empathy matter
|
||||
7. **Be Available**: Offer to pair on complex issues
|
||||
8. **Learn from Others**: Review others' review comments
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Perfectionism**: Blocking PRs for minor style preferences
|
||||
- **Scope Creep**: "While you're at it, can you also..."
|
||||
- **Inconsistency**: Different standards for different people
|
||||
- **Delayed Reviews**: Letting PRs sit for days
|
||||
- **Ghosting**: Requesting changes then disappearing
|
||||
- **Rubber Stamping**: Approving without actually reviewing
|
||||
- **Bike Shedding**: Debating trivial details extensively
|
||||
|
||||
## Templates
|
||||
|
||||
### PR Review Comment Template
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
[Brief overview of what was reviewed]
|
||||
|
||||
## Strengths
|
||||
|
||||
- [What was done well]
|
||||
- [Good patterns or approaches]
|
||||
|
||||
## Required Changes
|
||||
|
||||
🔴 [Blocking issue 1]
|
||||
🔴 [Blocking issue 2]
|
||||
|
||||
## Suggestions
|
||||
|
||||
💡 [Improvement 1]
|
||||
💡 [Improvement 2]
|
||||
|
||||
## Questions
|
||||
|
||||
❓ [Clarification needed on X]
|
||||
❓ [Alternative approach consideration]
|
||||
|
||||
## Verdict
|
||||
|
||||
✅ Approve after addressing required changes
|
||||
```
|
||||
@@ -0,0 +1,179 @@
|
||||
## Language-Specific Patterns
|
||||
|
||||
### Python Code Review
|
||||
|
||||
```python
|
||||
# Check for Python-specific issues
|
||||
|
||||
# ❌ Mutable default arguments
|
||||
def add_item(item, items=[]): # Bug! Shared across calls
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
# ✅ Use None as default
|
||||
def add_item(item, items=None):
|
||||
if items is None:
|
||||
items = []
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
# ❌ Catching too broad
|
||||
try:
|
||||
result = risky_operation()
|
||||
except: # Catches everything, even KeyboardInterrupt!
|
||||
pass
|
||||
|
||||
# ✅ Catch specific exceptions
|
||||
try:
|
||||
result = risky_operation()
|
||||
except ValueError as e:
|
||||
logger.error(f"Invalid value: {e}")
|
||||
raise
|
||||
|
||||
# ❌ Using mutable class attributes
|
||||
class User:
|
||||
permissions = [] # Shared across all instances!
|
||||
|
||||
# ✅ Initialize in __init__
|
||||
class User:
|
||||
def __init__(self):
|
||||
self.permissions = []
|
||||
```
|
||||
|
||||
### TypeScript/JavaScript Code Review
|
||||
|
||||
```typescript
|
||||
// Check for TypeScript-specific issues
|
||||
|
||||
// ❌ Using any defeats type safety
|
||||
function processData(data: any) { // Avoid any
|
||||
return data.value;
|
||||
}
|
||||
|
||||
// ✅ Use proper types
|
||||
interface DataPayload {
|
||||
value: string;
|
||||
}
|
||||
function processData(data: DataPayload) {
|
||||
return data.value;
|
||||
}
|
||||
|
||||
// ❌ Not handling async errors
|
||||
async function fetchUser(id: string) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
return response.json(); // What if network fails?
|
||||
}
|
||||
|
||||
// ✅ Handle errors properly
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
try {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch user:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Mutation of props
|
||||
function UserProfile({ user }: Props) {
|
||||
user.lastViewed = new Date(); // Mutating prop!
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
|
||||
// ✅ Don't mutate props
|
||||
function UserProfile({ user, onView }: Props) {
|
||||
useEffect(() => {
|
||||
onView(user.id); // Notify parent to update
|
||||
}, [user.id, onView]);
|
||||
return <div>{user.name}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Review Patterns
|
||||
|
||||
### Pattern 1: Architectural Review
|
||||
|
||||
```markdown
|
||||
When reviewing significant changes:
|
||||
|
||||
1. **Design Document First**
|
||||
- For large features, request design doc before code
|
||||
- Review design with team before implementation
|
||||
- Agree on approach to avoid rework
|
||||
|
||||
2. **Review in Stages**
|
||||
- First PR: Core abstractions and interfaces
|
||||
- Second PR: Implementation
|
||||
- Third PR: Integration and tests
|
||||
- Easier to review, faster to iterate
|
||||
|
||||
3. **Consider Alternatives**
|
||||
- "Have we considered using [pattern/library]?"
|
||||
- "What's the tradeoff vs. the simpler approach?"
|
||||
- "How will this evolve as requirements change?"
|
||||
```
|
||||
|
||||
### Pattern 2: Test Quality Review
|
||||
|
||||
```typescript
|
||||
// ❌ Poor test: Implementation detail testing
|
||||
test('increments counter variable', () => {
|
||||
const component = render(<Counter />);
|
||||
const button = component.getByRole('button');
|
||||
fireEvent.click(button);
|
||||
expect(component.state.counter).toBe(1); // Testing internal state
|
||||
});
|
||||
|
||||
// ✅ Good test: Behavior testing
|
||||
test('displays incremented count when clicked', () => {
|
||||
render(<Counter />);
|
||||
const button = screen.getByRole('button', { name: /increment/i });
|
||||
fireEvent.click(button);
|
||||
expect(screen.getByText('Count: 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Review questions for tests:
|
||||
// - Do tests describe behavior, not implementation?
|
||||
// - Are test names clear and descriptive?
|
||||
// - Do tests cover edge cases?
|
||||
// - Are tests independent (no shared state)?
|
||||
// - Can tests run in any order?
|
||||
```
|
||||
|
||||
### Pattern 3: Security Review
|
||||
|
||||
```markdown
|
||||
## Security Review Checklist
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
- [ ] Is authentication required where needed?
|
||||
- [ ] Are authorization checks before every action?
|
||||
- [ ] Is JWT validation proper (signature, expiry)?
|
||||
- [ ] Are API keys/secrets properly secured?
|
||||
|
||||
### Input Validation
|
||||
|
||||
- [ ] All user inputs validated?
|
||||
- [ ] File uploads restricted (size, type)?
|
||||
- [ ] SQL queries parameterized?
|
||||
- [ ] XSS protection (escape output)?
|
||||
|
||||
### Data Protection
|
||||
|
||||
- [ ] Passwords hashed (bcrypt/argon2)?
|
||||
- [ ] Sensitive data encrypted at rest?
|
||||
- [ ] HTTPS enforced for sensitive data?
|
||||
- [ ] PII handled according to regulations?
|
||||
|
||||
### Common Vulnerabilities
|
||||
|
||||
- [ ] No eval() or similar dynamic execution?
|
||||
- [ ] No hardcoded secrets?
|
||||
- [ ] CSRF protection for state-changing operations?
|
||||
- [ ] Rate limiting on public endpoints?
|
||||
```
|
||||
@@ -0,0 +1,86 @@
|
||||
## Review Techniques
|
||||
|
||||
### Technique 1: The Checklist Method
|
||||
|
||||
```markdown
|
||||
## Security Checklist
|
||||
|
||||
- [ ] User input validated and sanitized
|
||||
- [ ] SQL queries use parameterization
|
||||
- [ ] Authentication/authorization checked
|
||||
- [ ] Secrets not hardcoded
|
||||
- [ ] Error messages don't leak info
|
||||
|
||||
## Performance Checklist
|
||||
|
||||
- [ ] No N+1 queries
|
||||
- [ ] Database queries indexed
|
||||
- [ ] Large lists paginated
|
||||
- [ ] Expensive operations cached
|
||||
- [ ] No blocking I/O in hot paths
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Happy path tested
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Error cases tested
|
||||
- [ ] Test names are descriptive
|
||||
- [ ] Tests are deterministic
|
||||
```
|
||||
|
||||
### Technique 2: The Question Approach
|
||||
|
||||
Instead of stating problems, ask questions to encourage thinking:
|
||||
|
||||
```markdown
|
||||
❌ "This will fail if the list is empty."
|
||||
✅ "What happens if `items` is an empty array?"
|
||||
|
||||
❌ "You need error handling here."
|
||||
✅ "How should this behave if the API call fails?"
|
||||
|
||||
❌ "This is inefficient."
|
||||
✅ "I see this loops through all users. Have we considered
|
||||
the performance impact with 100k users?"
|
||||
```
|
||||
|
||||
### Technique 3: Suggest, Don't Command
|
||||
|
||||
````markdown
|
||||
## Use Collaborative Language
|
||||
|
||||
❌ "You must change this to use async/await"
|
||||
✅ "Suggestion: async/await might make this more readable:
|
||||
```typescript
|
||||
async function fetchUser(id: string) {
|
||||
const user = await db.query('SELECT * FROM users WHERE id = ?', id);
|
||||
return user;
|
||||
}
|
||||
```
|
||||
What do you think?"
|
||||
|
||||
❌ "Extract this into a function"
|
||||
✅ "This logic appears in 3 places. Would it make sense to
|
||||
extract it into a shared utility function?"
|
||||
````
|
||||
|
||||
### Technique 4: Differentiate Severity
|
||||
|
||||
```markdown
|
||||
Use labels to indicate priority:
|
||||
|
||||
🔴 [blocking] - Must fix before merge
|
||||
🟡 [important] - Should fix, discuss if disagree
|
||||
🟢 [nit] - Nice to have, not blocking
|
||||
💡 [suggestion] - Alternative approach to consider
|
||||
📚 [learning] - Educational comment, no action needed
|
||||
🎉 [praise] - Good work, keep it up!
|
||||
|
||||
Example:
|
||||
"🔴 [blocking] This SQL query is vulnerable to injection.
|
||||
Please use parameterized queries."
|
||||
|
||||
"🟢 [nit] Consider renaming `data` to `userData` for clarity."
|
||||
|
||||
"🎉 [praise] Excellent test coverage! This will catch edge cases."
|
||||
```
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
name: deployment-pipeline-design
|
||||
description: Design multi-stage CI/CD pipelines with approval gates, security checks, and deployment orchestration. Use this skill when designing zero-downtime deployment pipelines, implementing canary rollout strategies, setting up multi-environment promotion workflows, or debugging failed deployment gates in CI/CD.
|
||||
---
|
||||
|
||||
# Deployment Pipeline Design
|
||||
|
||||
Architecture patterns for multi-stage CI/CD pipelines with approval gates, deployment strategies, and environment promotion workflows.
|
||||
|
||||
## Purpose
|
||||
|
||||
Design robust, secure deployment pipelines that balance speed with safety through proper stage organization, automated quality gates, and progressive delivery strategies. This skill covers both the structural design of pipeline architecture and the operational patterns for reliable production deployments.
|
||||
|
||||
## Input / Output
|
||||
|
||||
### What You Provide
|
||||
|
||||
- **Application type**: Language/runtime, containerized or bare-metal, monolith or microservices
|
||||
- **Deployment target**: Kubernetes, ECS, VMs, serverless, or platform-as-a-service
|
||||
- **Environment topology**: Number of environments (dev/staging/prod), region layout, air-gap requirements
|
||||
- **Rollout requirements**: Acceptable downtime, rollback SLA, traffic splitting needs, canary vs blue-green preference
|
||||
- **Gate constraints**: Approval teams, required test coverage thresholds, compliance scans (SAST, DAST, SCA)
|
||||
- **Monitoring stack**: Prometheus, Datadog, CloudWatch, or other metrics sources used for automated promotion decisions
|
||||
|
||||
### What This Skill Produces
|
||||
|
||||
- **Pipeline configuration**: Stage definitions, job dependencies, parallelism, and caching strategy
|
||||
- **Deployment strategy**: Chosen rollout pattern with annotated configuration (canary weights, blue-green switchover, rolling parameters)
|
||||
- **Health check setup**: Shallow vs deep readiness probes, post-deployment smoke test scripts
|
||||
- **Gate definitions**: Automated metric thresholds and manual approval workflows
|
||||
- **Rollback plan**: Automated rollback triggers and manual runbook steps
|
||||
|
||||
## When to Use
|
||||
|
||||
- Design CI/CD architecture for a new service or platform migration
|
||||
- Implement deployment gates between environments
|
||||
- Configure multi-environment pipelines with mandatory security scanning
|
||||
- Establish progressive delivery with canary or blue-green strategies
|
||||
- Debug pipelines where stages succeed but production behavior is wrong
|
||||
- Reduce mean time to recovery by automating rollback on metric degradation
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Health check passes in pipeline but service is unhealthy in production
|
||||
|
||||
The pipeline health check is hitting a shallow `/ping` endpoint that returns 200 even when the database is unreachable. Use a deep readiness check that verifies actual dependencies (see Health Checks section above).
|
||||
|
||||
### Canary deployment never promotes to 100%
|
||||
|
||||
Argo Rollouts requires a valid `AnalysisTemplate` to auto-promote. If the Prometheus query returns no data (e.g., metric name changed), the analysis stays inconclusive and promotion stalls. Add `inconclusiveLimit` so the rollout fails fast rather than hanging:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
metrics:
|
||||
- name: error-rate
|
||||
failureCondition: "result[0] > 0.05"
|
||||
inconclusiveLimit: 2 # fail after 2 inconclusive results, not hang indefinitely
|
||||
provider:
|
||||
prometheus:
|
||||
query: |
|
||||
sum(rate(http_requests_total{status=~"5.."}[2m]))
|
||||
/ sum(rate(http_requests_total[2m]))
|
||||
```
|
||||
|
||||
### Staging deploy succeeds but production job never starts
|
||||
|
||||
Check that production environment protection rules are configured — a missing reviewer assignment means the approval gate waits indefinitely with no notification. In GitHub Actions, ensure `Required reviewers` is set to an existing user or team in **Settings → Environments → production**.
|
||||
|
||||
### Docker layer cache busted on every run causing slow builds
|
||||
|
||||
If `COPY . .` appears before dependency installation, any source file change invalidates the dependency layer. Reorder to copy dependency manifests first:
|
||||
|
||||
```dockerfile
|
||||
# Good: dependencies cached separately from source code
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
```
|
||||
|
||||
### Rollback leaves database migrations applied to old code
|
||||
|
||||
A service rollback without a migration rollback causes schema/code mismatch errors. Always make migrations backward-compatible (additive only) for at least one release cycle, and keep undo scripts versioned alongside the migration:
|
||||
|
||||
```bash
|
||||
# migrations/V20240315__add_nullable_column.sql (forward)
|
||||
# migrations/V20240315__add_nullable_column.undo.sql (backward)
|
||||
```
|
||||
|
||||
Never run destructive migrations (DROP COLUMN, ALTER NOT NULL) until the old code version is fully retired from all environments.
|
||||
|
||||
## Advanced Topics
|
||||
|
||||
For platform-specific pipeline configurations, multi-region promotion workflows, and advanced Argo Rollouts patterns, see:
|
||||
|
||||
- [`references/advanced-strategies.md`](references/advanced-strategies.md) — Extended YAML examples, platform-specific configs (GitHub Actions, GitLab CI, Azure Pipelines), multi-region canary patterns, and database migration rollback strategies
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `github-actions-templates` - For GitHub Actions implementation patterns and reusable workflows
|
||||
- `gitlab-ci-patterns` - For GitLab CI/CD pipeline implementation
|
||||
- `secrets-management` - For secrets handling in CI/CD pipelines
|
||||
@@ -0,0 +1,629 @@
|
||||
# Advanced Deployment Strategies Reference
|
||||
|
||||
Extended configurations, platform-specific patterns, and advanced rollback strategies.
|
||||
Core patterns and decision tables live in [`../SKILL.md`](../SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Pipeline Configurations
|
||||
|
||||
### GitHub Actions — Full Production Pipeline with Reusable Workflows
|
||||
|
||||
```yaml
|
||||
# .github/workflows/production.yml
|
||||
name: Production Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_tests:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # for OIDC auth to cloud providers
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image_tag: ${{ steps.meta.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Docker metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
tags: |
|
||||
type=sha,prefix=,format=short
|
||||
|
||||
- name: Build and push (with layer cache)
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
security-scan:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run Trivy vulnerability scan
|
||||
uses: aquasecurity/trivy-action@0.28.0
|
||||
with:
|
||||
image-ref: ghcr.io/${{ github.repository }}:${{ needs.build.outputs.image_tag }}
|
||||
exit-code: 1
|
||||
severity: CRITICAL,HIGH
|
||||
|
||||
- name: SAST with Semgrep
|
||||
uses: semgrep/semgrep-action@v1
|
||||
with:
|
||||
config: auto
|
||||
|
||||
test:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_PASSWORD: test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run test suite
|
||||
run: make test-ci
|
||||
env:
|
||||
DATABASE_URL: postgres://postgres:test@localhost/test
|
||||
|
||||
deploy-staging:
|
||||
needs: [test, security-scan]
|
||||
environment:
|
||||
name: staging
|
||||
url: https://staging.example.com
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::123456789:role/deploy-staging
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Deploy to EKS staging
|
||||
run: |
|
||||
aws eks update-kubeconfig --name my-cluster-staging
|
||||
kubectl set image deployment/my-app \
|
||||
app=ghcr.io/${{ github.repository }}:${{ needs.build.outputs.image_tag }}
|
||||
kubectl rollout status deployment/my-app --timeout=5m
|
||||
|
||||
e2e-tests:
|
||||
needs: deploy-staging
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run Playwright E2E suite
|
||||
run: npx playwright test --reporter=github
|
||||
env:
|
||||
BASE_URL: https://staging.example.com
|
||||
|
||||
deploy-production:
|
||||
needs: e2e-tests
|
||||
environment:
|
||||
name: production
|
||||
url: https://app.example.com
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::123456789:role/deploy-production
|
||||
aws-region: us-east-1
|
||||
|
||||
- name: Deploy canary to production
|
||||
run: |
|
||||
aws eks update-kubeconfig --name my-cluster-prod
|
||||
kubectl argo rollouts set image my-app \
|
||||
app=ghcr.io/${{ github.repository }}:${{ needs.build.outputs.image_tag }}
|
||||
|
||||
- name: Monitor canary promotion
|
||||
run: |
|
||||
kubectl argo rollouts status my-app --timeout=30m
|
||||
|
||||
- name: Rollback on failure
|
||||
if: failure()
|
||||
run: kubectl argo rollouts abort my-app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### GitLab CI — Multi-Environment Pipeline with Dynamic Environments
|
||||
|
||||
```yaml
|
||||
# .gitlab-ci.yml
|
||||
stages:
|
||||
- build
|
||||
- test
|
||||
- staging
|
||||
- production
|
||||
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
- docker build --cache-from $CI_REGISTRY_IMAGE:latest -t $IMAGE .
|
||||
- docker push $IMAGE
|
||||
- docker tag $IMAGE $CI_REGISTRY_IMAGE:latest
|
||||
- docker push $CI_REGISTRY_IMAGE:latest
|
||||
|
||||
test:unit:
|
||||
stage: test
|
||||
image: $IMAGE
|
||||
script:
|
||||
- make test
|
||||
coverage: '/coverage: \d+\.\d+%/'
|
||||
artifacts:
|
||||
reports:
|
||||
coverage_report:
|
||||
coverage_format: cobertura
|
||||
path: coverage.xml
|
||||
|
||||
test:security:
|
||||
stage: test
|
||||
image:
|
||||
name: aquasec/trivy:latest
|
||||
entrypoint: [""]
|
||||
script:
|
||||
- trivy image --exit-code 1 --severity CRITICAL,HIGH $IMAGE
|
||||
|
||||
deploy:staging:
|
||||
stage: staging
|
||||
environment:
|
||||
name: staging
|
||||
url: https://staging.example.com
|
||||
on_stop: stop:staging
|
||||
script:
|
||||
- kubectl apply -f k8s/staging/
|
||||
- kubectl set image deployment/my-app app=$IMAGE -n staging
|
||||
- kubectl rollout status deployment/my-app -n staging
|
||||
only:
|
||||
- main
|
||||
|
||||
stop:staging:
|
||||
stage: staging
|
||||
environment:
|
||||
name: staging
|
||||
action: stop
|
||||
script:
|
||||
- kubectl delete namespace staging --ignore-not-found
|
||||
when: manual
|
||||
only:
|
||||
- main
|
||||
|
||||
deploy:production:
|
||||
stage: production
|
||||
environment:
|
||||
name: production
|
||||
url: https://app.example.com
|
||||
script:
|
||||
- kubectl set image deployment/my-app app=$IMAGE -n production
|
||||
- kubectl rollout status deployment/my-app -n production --timeout=10m
|
||||
when: manual
|
||||
only:
|
||||
- main
|
||||
rules:
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
when: manual
|
||||
allow_failure: false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Azure Pipelines — Multi-Stage with Approvals and Environments
|
||||
|
||||
```yaml
|
||||
# azure-pipelines.yml
|
||||
trigger:
|
||||
branches:
|
||||
include:
|
||||
- main
|
||||
|
||||
variables:
|
||||
imageRepository: 'myapp'
|
||||
containerRegistry: 'myregistry.azurecr.io'
|
||||
tag: '$(Build.BuildId)'
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
displayName: 'Build & Test'
|
||||
jobs:
|
||||
- job: BuildAndTest
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
steps:
|
||||
- task: Docker@2
|
||||
displayName: Build image
|
||||
inputs:
|
||||
command: build
|
||||
repository: $(imageRepository)
|
||||
dockerfile: Dockerfile
|
||||
tags: $(tag)
|
||||
|
||||
- task: Docker@2
|
||||
displayName: Push image
|
||||
inputs:
|
||||
command: push
|
||||
repository: $(imageRepository)
|
||||
tags: $(tag)
|
||||
|
||||
- script: make test
|
||||
displayName: Run tests
|
||||
|
||||
- stage: Staging
|
||||
displayName: 'Deploy to Staging'
|
||||
dependsOn: Build
|
||||
jobs:
|
||||
- deployment: DeployStaging
|
||||
environment: staging
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- task: KubernetesManifest@0
|
||||
inputs:
|
||||
action: deploy
|
||||
manifests: k8s/staging/*.yaml
|
||||
containers: $(containerRegistry)/$(imageRepository):$(tag)
|
||||
|
||||
- stage: Production
|
||||
displayName: 'Deploy to Production'
|
||||
dependsOn: Staging
|
||||
jobs:
|
||||
- deployment: DeployProduction
|
||||
environment:
|
||||
name: production
|
||||
resourceType: Kubernetes
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
strategy:
|
||||
canary:
|
||||
increments: [10, 25, 50]
|
||||
preDeploy:
|
||||
steps:
|
||||
- task: ManualValidation@0
|
||||
inputs:
|
||||
notifyUsers: 'release-managers@example.com'
|
||||
instructions: 'Verify staging metrics. Approve to start canary.'
|
||||
onTimeout: reject
|
||||
deploy:
|
||||
steps:
|
||||
- task: KubernetesManifest@0
|
||||
inputs:
|
||||
action: deploy
|
||||
manifests: k8s/production/*.yaml
|
||||
containers: $(containerRegistry)/$(imageRepository):$(tag)
|
||||
postRouteTraffic:
|
||||
steps:
|
||||
- script: ./scripts/verify-deployment.sh https://app.example.com
|
||||
on:
|
||||
failure:
|
||||
steps:
|
||||
- task: KubernetesManifest@0
|
||||
inputs:
|
||||
action: reject
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Multi-Region Canary Promotion
|
||||
|
||||
Deploy to a pilot region first, validate, then promote to remaining regions in parallel:
|
||||
|
||||
```yaml
|
||||
# deploy-multiregion.yml (GitHub Actions)
|
||||
jobs:
|
||||
deploy-pilot:
|
||||
environment: production-us-east-1
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy canary to pilot region
|
||||
run: |
|
||||
aws eks update-kubeconfig --name cluster-us-east-1 --region us-east-1
|
||||
kubectl argo rollouts set image my-app app=$IMAGE
|
||||
kubectl argo rollouts status my-app --timeout=20m
|
||||
|
||||
deploy-secondary:
|
||||
needs: deploy-pilot
|
||||
strategy:
|
||||
matrix:
|
||||
region: [us-west-2, eu-west-1, ap-southeast-1]
|
||||
environment: production-${{ matrix.region }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to ${{ matrix.region }}
|
||||
run: |
|
||||
aws eks update-kubeconfig --name cluster-${{ matrix.region }} \
|
||||
--region ${{ matrix.region }}
|
||||
kubectl argo rollouts set image my-app app=$IMAGE
|
||||
kubectl argo rollouts status my-app --timeout=20m
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Advanced Argo Rollouts Patterns
|
||||
|
||||
### Experiment-Based Canary (A/B Analysis)
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
replicas: 20
|
||||
strategy:
|
||||
canary:
|
||||
canaryService: my-app-canary
|
||||
stableService: my-app-stable
|
||||
trafficRouting:
|
||||
istio:
|
||||
virtualService:
|
||||
name: my-app-vsvc
|
||||
analysis:
|
||||
templates:
|
||||
- templateName: success-rate
|
||||
- templateName: latency-p99
|
||||
startingStep: 1
|
||||
args:
|
||||
- name: service-name
|
||||
value: my-app-canary
|
||||
steps:
|
||||
- setWeight: 5
|
||||
- experiment:
|
||||
templates:
|
||||
- name: baseline
|
||||
specRef: stable
|
||||
- name: canary
|
||||
specRef: canary
|
||||
analyses:
|
||||
- templateName: ab-test
|
||||
requiredForCompletion: true
|
||||
- setWeight: 20
|
||||
- pause: { duration: 10m }
|
||||
- setWeight: 50
|
||||
- pause: { duration: 15m }
|
||||
- setWeight: 100
|
||||
```
|
||||
|
||||
### Header-Based Canary (Internal Testing Before Traffic Split)
|
||||
|
||||
```yaml
|
||||
# Route users with X-Canary: true header to canary pods
|
||||
apiVersion: networking.istio.io/v1alpha3
|
||||
kind: VirtualService
|
||||
metadata:
|
||||
name: my-app-vsvc
|
||||
spec:
|
||||
http:
|
||||
- match:
|
||||
- headers:
|
||||
x-canary:
|
||||
exact: "true"
|
||||
route:
|
||||
- destination:
|
||||
host: my-app-canary
|
||||
- route:
|
||||
- destination:
|
||||
host: my-app-stable
|
||||
weight: 90
|
||||
- destination:
|
||||
host: my-app-canary
|
||||
weight: 10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Migration Rollback Strategies
|
||||
|
||||
### Expand/Contract Pattern (Safe Schema Changes)
|
||||
|
||||
Never break backward compatibility within a single deployment cycle. Use the expand/contract pattern across three releases:
|
||||
|
||||
```
|
||||
Release N: Add nullable column (expand)
|
||||
Release N+1: Backfill data, deploy new code that reads new column
|
||||
Release N+2: Drop old column (contract) — safe because no code references it
|
||||
```
|
||||
|
||||
```bash
|
||||
# Release N migration — backward compatible
|
||||
cat > migrations/V20240315__expand_add_email_v2.sql <<'EOF'
|
||||
ALTER TABLE users ADD COLUMN email_v2 VARCHAR(255);
|
||||
CREATE INDEX CONCURRENTLY idx_users_email_v2 ON users(email_v2);
|
||||
EOF
|
||||
|
||||
# Release N+2 migration — contract (only after old code retired)
|
||||
cat > migrations/V20240415__contract_drop_email.sql <<'EOF'
|
||||
ALTER TABLE users DROP COLUMN email;
|
||||
ALTER TABLE users RENAME COLUMN email_v2 TO email;
|
||||
EOF
|
||||
```
|
||||
|
||||
### Flyway with Undo Scripts
|
||||
|
||||
```bash
|
||||
# Forward migration
|
||||
flyway migrate
|
||||
|
||||
# Undo the most recent migration (requires Flyway Teams)
|
||||
flyway undo
|
||||
|
||||
# Undo a specific version
|
||||
flyway undo -target=20240315
|
||||
```
|
||||
|
||||
Undo script naming convention: `U20240315__expand_add_email_v2.sql` (prefix `U` instead of `V`).
|
||||
|
||||
### Zero-Downtime Index Creation (PostgreSQL)
|
||||
|
||||
```sql
|
||||
-- Blocking (avoid in production):
|
||||
CREATE INDEX idx_users_email ON users(email);
|
||||
|
||||
-- Non-blocking (safe for live systems):
|
||||
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
|
||||
```
|
||||
|
||||
Use `CREATE INDEX CONCURRENTLY` in all production migrations. Add it to a pre-deploy migration step that runs before the application update.
|
||||
|
||||
---
|
||||
|
||||
## Blue-Green with Database (Full Example)
|
||||
|
||||
```yaml
|
||||
# blue-green-deploy.yml
|
||||
steps:
|
||||
- name: Deploy green environment
|
||||
run: |
|
||||
kubectl apply -f k8s/green/
|
||||
|
||||
- name: Run database migrations (forward, backward-compatible)
|
||||
run: |
|
||||
kubectl exec -n production deploy/migration-job -- flyway migrate
|
||||
|
||||
- name: Smoke test green
|
||||
run: |
|
||||
kubectl port-forward -n production svc/my-app-green 8080:80 &
|
||||
sleep 3
|
||||
curl -sf http://localhost:8080/health/ready
|
||||
|
||||
- name: Switch traffic to green
|
||||
run: |
|
||||
kubectl patch service my-app \
|
||||
-p '{"spec":{"selector":{"slot":"green"}}}'
|
||||
|
||||
- name: Verify green is live
|
||||
run: ./scripts/verify-deployment.sh https://app.example.com
|
||||
|
||||
- name: Scale down blue
|
||||
run: |
|
||||
kubectl scale deployment my-app-blue --replicas=0
|
||||
|
||||
- name: Rollback to blue on failure
|
||||
if: failure()
|
||||
run: |
|
||||
kubectl patch service my-app \
|
||||
-p '{"spec":{"selector":{"slot":"blue"}}}'
|
||||
kubectl scale deployment my-app-blue --replicas=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment Freeze Automation
|
||||
|
||||
Automatically block production deployments during freeze windows:
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# scripts/check-freeze-window.py
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
FREEZE_WINDOWS = [
|
||||
# (month, day_start, day_end, description)
|
||||
(11, 25, 30, "US Thanksgiving"),
|
||||
(12, 20, 31, "Year-end freeze"),
|
||||
(1, 1, 2, "New Year"),
|
||||
]
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
month, day = now.month, now.day
|
||||
|
||||
for fm, d_start, d_end, label in FREEZE_WINDOWS:
|
||||
if fm == month and d_start <= day <= d_end:
|
||||
print(f"BLOCKED: Deployment freeze active — {label}")
|
||||
print("Override with FORCE_DEPLOY=true environment variable if critical.")
|
||||
if not os.environ.get("FORCE_DEPLOY"):
|
||||
sys.exit(1)
|
||||
|
||||
print("No active freeze window — deployment allowed.")
|
||||
```
|
||||
|
||||
Use in pipeline:
|
||||
|
||||
```yaml
|
||||
- name: Check deployment freeze window
|
||||
run: python scripts/check-freeze-window.py
|
||||
env:
|
||||
FORCE_DEPLOY: ${{ vars.FORCE_DEPLOY }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notification Templates
|
||||
|
||||
### Slack Deployment Notification
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# scripts/notify-slack.sh
|
||||
STATUS="${1:?pass 'success' or 'failure'}"
|
||||
WEBHOOK="${SLACK_WEBHOOK:?SLACK_WEBHOOK not set}"
|
||||
REPO="${GITHUB_REPOSITORY:-unknown}"
|
||||
SHA="${GITHUB_SHA:-unknown}"
|
||||
ACTOR="${GITHUB_ACTOR:-unknown}"
|
||||
|
||||
if [ "$STATUS" = "success" ]; then
|
||||
COLOR="good"
|
||||
EMOJI=":white_check_mark:"
|
||||
TEXT="Production deploy succeeded"
|
||||
else
|
||||
COLOR="danger"
|
||||
EMOJI=":red_circle:"
|
||||
TEXT="Production deploy FAILED — rollback triggered"
|
||||
fi
|
||||
|
||||
curl -sf -X POST "$WEBHOOK" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"attachments\": [{
|
||||
\"color\": \"$COLOR\",
|
||||
\"text\": \"$EMOJI $TEXT\",
|
||||
\"fields\": [
|
||||
{\"title\": \"Repo\", \"value\": \"$REPO\", \"short\": true},
|
||||
{\"title\": \"SHA\", \"value\": \"${SHA:0:7}\", \"short\": true},
|
||||
{\"title\": \"Deployed by\", \"value\": \"$ACTOR\", \"short\": true}
|
||||
]
|
||||
}]
|
||||
}"
|
||||
```
|
||||
@@ -0,0 +1,399 @@
|
||||
# deployment-pipeline-design — detailed patterns and worked examples
|
||||
|
||||
## Pipeline Stages
|
||||
|
||||
### Standard Pipeline Flow
|
||||
|
||||
```
|
||||
┌─────────┐ ┌──────┐ ┌─────────┐ ┌────────┐ ┌──────────┐
|
||||
│ Build │ → │ Test │ → │ Staging │ → │ Approve│ → │Production│
|
||||
└─────────┘ └──────┘ └─────────┘ └────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### Detailed Stage Breakdown
|
||||
|
||||
1. **Source** - Code checkout, dependency graph resolution
|
||||
2. **Build** - Compile, package, containerize, sign artifacts
|
||||
3. **Test** - Unit, integration, SAST/SCA security scans
|
||||
4. **Staging Deploy** - Deploy to staging environment with smoke tests
|
||||
5. **Integration Tests** - E2E, contract tests, performance baselines
|
||||
6. **Approval Gate** - Manual or automated metric-based gate
|
||||
7. **Production Deploy** - Canary, blue-green, or rolling strategy
|
||||
8. **Verification** - Deep health checks, synthetic monitoring
|
||||
9. **Rollback** - Automated rollback on failure signals
|
||||
|
||||
## Approval Gate Patterns
|
||||
|
||||
### Pattern 1: Manual Approval (GitHub Actions)
|
||||
|
||||
```yaml
|
||||
production-deploy:
|
||||
needs: staging-deploy
|
||||
environment:
|
||||
name: production
|
||||
url: https://app.example.com
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to production
|
||||
run: kubectl apply -f k8s/production/
|
||||
```
|
||||
|
||||
Environment protection rules in GitHub enforce required reviewers before this job starts. Configure reviewers at **Settings → Environments → production → Required reviewers**.
|
||||
|
||||
### Pattern 2: Time-Based Approval (GitLab CI)
|
||||
|
||||
```yaml
|
||||
deploy:production:
|
||||
stage: deploy
|
||||
script:
|
||||
- deploy.sh production
|
||||
environment:
|
||||
name: production
|
||||
when: delayed
|
||||
start_in: 30 minutes
|
||||
only:
|
||||
- main
|
||||
```
|
||||
|
||||
### Pattern 3: Multi-Approver (Azure Pipelines)
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- stage: Production
|
||||
dependsOn: Staging
|
||||
jobs:
|
||||
- deployment: Deploy
|
||||
environment:
|
||||
name: production
|
||||
resourceType: Kubernetes
|
||||
strategy:
|
||||
runOnce:
|
||||
preDeploy:
|
||||
steps:
|
||||
- task: ManualValidation@0
|
||||
inputs:
|
||||
notifyUsers: "team-leads@example.com"
|
||||
instructions: "Review staging metrics before approving"
|
||||
```
|
||||
|
||||
### Pattern 4: Automated Metric Gate
|
||||
|
||||
Use an AnalysisTemplate (Argo Rollouts) or a custom gate script to block promotion when error rates exceed a threshold:
|
||||
|
||||
```yaml
|
||||
# Argo Rollouts AnalysisTemplate — blocks canary promotion automatically
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: AnalysisTemplate
|
||||
metadata:
|
||||
name: success-rate
|
||||
spec:
|
||||
metrics:
|
||||
- name: success-rate
|
||||
interval: 60s
|
||||
successCondition: "result[0] >= 0.95"
|
||||
failureCondition: "result[0] < 0.90"
|
||||
inconclusiveLimit: 3
|
||||
provider:
|
||||
prometheus:
|
||||
address: http://prometheus:9090
|
||||
query: |
|
||||
sum(rate(http_requests_total{status!~"5..",job="my-app"}[2m]))
|
||||
/ sum(rate(http_requests_total{job="my-app"}[2m]))
|
||||
```
|
||||
|
||||
## Deployment Strategies
|
||||
|
||||
### Decision Table
|
||||
|
||||
| Strategy | Downtime | Rollback Speed | Cost Impact | Best For |
|
||||
|-------------|----------|----------------|-----------------|----------------------------------|
|
||||
| Rolling | None | ~minutes | None | Most stateless services |
|
||||
| Blue-Green | None | Instant | 2x infra (temp) | High-risk or database migrations |
|
||||
| Canary | None | Instant | Minimal | High-traffic, metric-driven |
|
||||
| Recreate | Yes | Fast | None | Dev/test, batch jobs |
|
||||
| Feature Flag | None | Instant | None | Gradual feature exposure |
|
||||
|
||||
### 1. Rolling Deployment
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
replicas: 10
|
||||
strategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxSurge: 2 # at most 12 pods during rollout
|
||||
maxUnavailable: 1 # at least 9 pods always serving
|
||||
```
|
||||
|
||||
Characteristics: gradual rollout, zero downtime, easy rollback, best for most applications.
|
||||
|
||||
### 2. Blue-Green Deployment
|
||||
|
||||
```bash
|
||||
# Switch traffic from blue to green
|
||||
kubectl apply -f k8s/green-deployment.yaml
|
||||
kubectl rollout status deployment/my-app-green
|
||||
|
||||
# Flip the service selector
|
||||
kubectl patch service my-app -p '{"spec":{"selector":{"version":"green"}}}'
|
||||
|
||||
# Rollback instantly if needed
|
||||
kubectl patch service my-app -p '{"spec":{"selector":{"version":"blue"}}}'
|
||||
```
|
||||
|
||||
Characteristics: instant switchover, easy rollback, doubles infrastructure cost temporarily, good for high-risk deployments with long warm-up times.
|
||||
|
||||
### 3. Canary Deployment (Argo Rollouts)
|
||||
|
||||
```yaml
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Rollout
|
||||
metadata:
|
||||
name: my-app
|
||||
spec:
|
||||
replicas: 10
|
||||
strategy:
|
||||
canary:
|
||||
analysis:
|
||||
templates:
|
||||
- templateName: success-rate
|
||||
startingStep: 2
|
||||
steps:
|
||||
- setWeight: 10
|
||||
- pause: { duration: 5m }
|
||||
- setWeight: 25
|
||||
- pause: { duration: 5m }
|
||||
- setWeight: 50
|
||||
- pause: { duration: 10m }
|
||||
- setWeight: 100
|
||||
```
|
||||
|
||||
Characteristics: gradual traffic shift, real-user metric validation, automated promotion or rollback, requires Argo Rollouts or a service mesh.
|
||||
|
||||
### 4. Feature Flags
|
||||
|
||||
```python
|
||||
from flagsmith import Flagsmith
|
||||
|
||||
flagsmith = Flagsmith(environment_key="API_KEY")
|
||||
|
||||
if flagsmith.has_feature("new_checkout_flow"):
|
||||
process_checkout_v2()
|
||||
else:
|
||||
process_checkout_v1()
|
||||
```
|
||||
|
||||
Characteristics: deploy without releasing, A/B testing, instant rollback per user segment, granular control independent of deployment.
|
||||
|
||||
## Pipeline Orchestration
|
||||
|
||||
### Multi-Stage Pipeline Example (GitHub Actions)
|
||||
|
||||
```yaml
|
||||
name: Production Pipeline
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image: ${{ steps.build.outputs.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build and push Docker image
|
||||
id: build
|
||||
run: |
|
||||
IMAGE=myapp:${{ github.sha }}
|
||||
docker build -t $IMAGE .
|
||||
docker push $IMAGE
|
||||
echo "image=$IMAGE" >> $GITHUB_OUTPUT
|
||||
|
||||
test:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Unit tests
|
||||
run: make test
|
||||
- name: Security scan
|
||||
run: trivy image ${{ needs.build.outputs.image }}
|
||||
|
||||
deploy-staging:
|
||||
needs: test
|
||||
environment:
|
||||
name: staging
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deploy to staging
|
||||
run: kubectl apply -f k8s/staging/
|
||||
|
||||
integration-test:
|
||||
needs: deploy-staging
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Run E2E tests
|
||||
run: npm run test:e2e
|
||||
|
||||
deploy-production:
|
||||
needs: integration-test
|
||||
environment:
|
||||
name: production # blocks here until required reviewers approve
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Canary deployment
|
||||
run: |
|
||||
kubectl apply -f k8s/production/
|
||||
kubectl argo rollouts promote my-app
|
||||
|
||||
verify:
|
||||
needs: deploy-production
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Deep health check
|
||||
run: |
|
||||
for i in {1..12}; do
|
||||
STATUS=$(curl -sf https://app.example.com/health/ready | jq -r '.status')
|
||||
[ "$STATUS" = "ok" ] && exit 0
|
||||
sleep 10
|
||||
done
|
||||
exit 1
|
||||
- name: Notify on success
|
||||
run: |
|
||||
curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
|
||||
-d '{"text":"Production deployment successful: ${{ github.sha }}"}'
|
||||
```
|
||||
|
||||
## Health Checks
|
||||
|
||||
### Shallow vs Deep Health Endpoints
|
||||
|
||||
A shallow `/ping` returns 200 even when downstream dependencies are broken. Use a deep readiness endpoint that verifies actual dependencies before promoting traffic.
|
||||
|
||||
```python
|
||||
# /health/ready — checks real dependencies, used by pipeline gate
|
||||
@app.get("/health/ready")
|
||||
async def readiness():
|
||||
checks = {
|
||||
"database": await check_db_connection(),
|
||||
"cache": await check_redis_connection(),
|
||||
"queue": await check_queue_connection(),
|
||||
}
|
||||
status = "ok" if all(checks.values()) else "degraded"
|
||||
code = 200 if status == "ok" else 503
|
||||
return JSONResponse({"status": status, "checks": checks}, status_code=code)
|
||||
```
|
||||
|
||||
### Post-Deployment Verification Script
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
# verify-deployment.sh — run after every production deploy
|
||||
set -euo pipefail
|
||||
|
||||
ENDPOINT="${1:?usage: verify-deployment.sh <base-url>}"
|
||||
MAX_ATTEMPTS=12
|
||||
SLEEP_SECONDS=10
|
||||
|
||||
for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
STATUS=$(curl -sf "$ENDPOINT/health/ready" | jq -r '.status' 2>/dev/null || echo "unreachable")
|
||||
if [ "$STATUS" = "ok" ]; then
|
||||
echo "Health check passed after $((i * SLEEP_SECONDS))s"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt $i/$MAX_ATTEMPTS: status=$STATUS — retrying in ${SLEEP_SECONDS}s"
|
||||
sleep "$SLEEP_SECONDS"
|
||||
done
|
||||
|
||||
echo "Health check failed after $((MAX_ATTEMPTS * SLEEP_SECONDS))s"
|
||||
exit 1
|
||||
```
|
||||
|
||||
## Rollback Strategies
|
||||
|
||||
### Automated Rollback in Pipeline
|
||||
|
||||
```yaml
|
||||
deploy-and-verify:
|
||||
steps:
|
||||
- name: Deploy new version
|
||||
run: kubectl apply -f k8s/
|
||||
|
||||
- name: Wait for rollout
|
||||
run: kubectl rollout status deployment/my-app --timeout=5m
|
||||
|
||||
- name: Post-deployment health check
|
||||
id: health
|
||||
run: ./scripts/verify-deployment.sh https://app.example.com
|
||||
|
||||
- name: Rollback on failure
|
||||
if: failure()
|
||||
run: |
|
||||
kubectl rollout undo deployment/my-app
|
||||
echo "Rolled back to previous revision"
|
||||
```
|
||||
|
||||
### Manual Rollback Commands
|
||||
|
||||
```bash
|
||||
# List revision history with change-cause annotations
|
||||
kubectl rollout history deployment/my-app
|
||||
|
||||
# Rollback to previous version
|
||||
kubectl rollout undo deployment/my-app
|
||||
|
||||
# Rollback to a specific revision
|
||||
kubectl rollout undo deployment/my-app --to-revision=3
|
||||
|
||||
# Verify rollback completed
|
||||
kubectl rollout status deployment/my-app
|
||||
```
|
||||
|
||||
For advanced rollback strategies including database migration rollbacks and Argo Rollouts abort flows, see [`references/advanced-strategies.md`](references/advanced-strategies.md).
|
||||
|
||||
## Monitoring and Metrics
|
||||
|
||||
### Key DORA Metrics to Track
|
||||
|
||||
| Metric | Target (Elite) | How to Measure |
|
||||
|--------------------------|----------------|------------------------------------------|
|
||||
| Deployment Frequency | Multiple/day | Pipeline run count per day |
|
||||
| Lead Time for Changes | < 1 hour | Commit timestamp → production deploy |
|
||||
| Change Failure Rate | < 5% | Failed deploys / total deploys |
|
||||
| Mean Time to Recovery | < 1 hour | Incident open → service restored |
|
||||
|
||||
### Post-Deployment Metric Verification
|
||||
|
||||
```yaml
|
||||
- name: Verify error rate post-deployment
|
||||
run: |
|
||||
sleep 60 # allow metrics to accumulate
|
||||
|
||||
ERROR_RATE=$(curl -sf "$PROMETHEUS_URL/api/v1/query" \
|
||||
--data-urlencode 'query=sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))' \
|
||||
| jq '.data.result[0].value[1]')
|
||||
|
||||
echo "Current error rate: $ERROR_RATE"
|
||||
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
|
||||
echo "Error rate $ERROR_RATE exceeds 1% threshold — triggering rollback"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Pipeline Best Practices
|
||||
|
||||
1. **Fail fast** — Run quick checks (lint, unit tests) before slow ones (E2E, security scans)
|
||||
2. **Parallel execution** — Run independent jobs concurrently to minimize total pipeline time
|
||||
3. **Caching** — Cache dependency layers and build artifacts between runs
|
||||
4. **Artifact promotion** — Build once, promote the same artifact through all environments
|
||||
5. **Environment parity** — Keep staging infrastructure as close to production as possible
|
||||
6. **Secrets management** — Use secret stores (Vault, AWS Secrets Manager, GitHub encrypted secrets) — never hardcode
|
||||
7. **Deployment windows** — Prefer low-traffic windows; enforce change freeze periods via gate policies
|
||||
8. **Idempotent deploys** — Ensure re-running a deploy produces the same result
|
||||
9. **Rollback automation** — Trigger rollback automatically on health check or metric threshold failure
|
||||
10. **Annotate deployments** — Send deployment markers to monitoring tools (Datadog, Grafana) for correlation
|
||||
@@ -0,0 +1,30 @@
|
||||
© 2025 Anthropic, PBC. All rights reserved.
|
||||
|
||||
LICENSE: Use of these materials (including all code, prompts, assets, files,
|
||||
and other components of this Skill) is governed by your agreement with
|
||||
Anthropic regarding use of Anthropic's services. If no separate agreement
|
||||
exists, use is governed by Anthropic's Consumer Terms of Service or
|
||||
Commercial Terms of Service, as applicable:
|
||||
https://www.anthropic.com/legal/consumer-terms
|
||||
https://www.anthropic.com/legal/commercial-terms
|
||||
Your applicable agreement is referred to as the "Agreement." "Services" are
|
||||
as defined in the Agreement.
|
||||
|
||||
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
|
||||
contrary, users may not:
|
||||
|
||||
- Extract these materials from the Services or retain copies of these
|
||||
materials outside the Services
|
||||
- Reproduce or copy these materials, except for temporary copies created
|
||||
automatically during authorized use of the Services
|
||||
- Create derivative works based on these materials
|
||||
- Distribute, sublicense, or transfer these materials to any third party
|
||||
- Make, offer to sell, sell, or import any inventions embodied in these
|
||||
materials
|
||||
- Reverse engineer, decompile, or disassemble these materials
|
||||
|
||||
The receipt, viewing, or possession of these materials does not convey or
|
||||
imply any license or right beyond those expressly granted above.
|
||||
|
||||
Anthropic retains all right, title, and interest in these materials,
|
||||
including all copyrights, patents, and other intellectual property rights.
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
name: docx
|
||||
description: "Use this skill whenever the user wants to create, read, edit, or manipulate Word documents (.docx files) or Word templates (.dotx files). Triggers include: any mention of 'Word doc', 'word document', '.docx', '.dotx', or requests to produce professional documents with formatting like tables of contents, headings, page numbers, or letterheads. Also use when extracting or reorganizing content from .docx or .dotx files, inserting or replacing images in documents, performing find-and-replace in Word files, working with tracked changes or comments, or converting content into a polished Word document. If the user asks for a 'report', 'memo', 'letter', 'template', or similar deliverable as a Word or .docx file, use this skill. Do NOT use for PDFs, spreadsheets, Google Docs, or general coding tasks unrelated to document generation."
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# DOCX creation, editing, and analysis
|
||||
|
||||
A `.docx` is a ZIP archive of XML files. Choose your approach by task:
|
||||
|
||||
| Task | Approach |
|
||||
|---|---|
|
||||
| **Create** a new document | Write a `docx` (npm) script — see gotchas below |
|
||||
| **Edit** an existing document | `unzip` → edit `word/document.xml` → `zip` (docx-js cannot open existing files) |
|
||||
| **Read** content | `pandoc -t markdown file.docx` |
|
||||
|
||||
> Script paths below are relative to this skill's directory.
|
||||
|
||||
## Creating with docx-js — gotchas
|
||||
|
||||
`docx` is preinstalled — do not run `npm install` first; write the script and `require('docx')` directly. Only if that require fails: `npm install docx`. The model knows the API; these are the footguns:
|
||||
|
||||
- **Page size defaults to A4.** For US Letter set `page: { size: { width: 12240, height: 15840 } }` (DXA; 1440 = 1″).
|
||||
- **Landscape:** pass portrait dimensions and `orientation: PageOrientation.LANDSCAPE` — docx-js swaps width/height internally.
|
||||
- **Tables need dual widths:** set `columnWidths` on the table AND `width` on every cell, both in `WidthType.DXA` (PERCENTAGE breaks in Google Docs). Column widths must sum to the table width.
|
||||
- **Table shading:** use `ShadingType.CLEAR`, never `SOLID` (renders black).
|
||||
- **Lists:** never insert `•` literally; use a `numbering` config with `LevelFormat.BULLET`.
|
||||
- **`ImageRun` requires `type:`** (`"png"`, `"jpg"`, …).
|
||||
- **`PageBreak` must be inside a `Paragraph`.**
|
||||
- **Never use `\n`** — use separate `Paragraph` elements.
|
||||
- **TOC:** headings must use built-in `HeadingLevel.*`; custom heading styles need `outlineLevel` set or they won't appear.
|
||||
- **Don't use a table as a horizontal rule** — use a paragraph bottom border instead.
|
||||
- **Dot-leader / right-aligned-on-same-line:** use `PositionalTab` (`alignment: PositionalTabAlignment.RIGHT`, `leader: PositionalTabLeader.DOT`) inside a `TextRun`, not literal `.` or space padding.
|
||||
|
||||
## Verify the output
|
||||
|
||||
After writing a `.docx`, render it and look at it:
|
||||
|
||||
```bash
|
||||
python scripts/office/soffice.py --headless --convert-to pdf output.docx
|
||||
pdftoppm -jpeg -r 100 output.pdf page
|
||||
ls page-*.jpg # then Read the images
|
||||
```
|
||||
|
||||
`pdftoppm` zero-pads page numbers to the width of the page count (`page-01.jpg`…`page-12.jpg`).
|
||||
|
||||
## Editing existing documents
|
||||
|
||||
Legacy `.doc` files must be converted first: `python scripts/office/soffice.py --headless --convert-to docx file.doc`.
|
||||
|
||||
```bash
|
||||
unzip -q doc.docx -d unpacked/
|
||||
find unpacked -type l -delete # strip symlink entries — docx from external parties is untrusted
|
||||
python scripts/merge_runs.py unpacked/ # coalesce fragmented runs so text is findable
|
||||
# edit unpacked/word/document.xml in place — do NOT reformat or pretty-print
|
||||
(cd unpacked && rm -f ../out.docx && zip -Xr ../out.docx .)
|
||||
python scripts/office/validate.py out.docx --original doc.docx # XSD checks; --auto-repair fixes common issues
|
||||
# redlining? add --author "<the name you redlined under>" to check every edit is tracked
|
||||
```
|
||||
|
||||
Word splits text across many `<w:r>` runs (revision ids, spell-check markers), so a phrase you can see in the document often doesn't exist as a contiguous string in the XML. `merge_runs.py` merges adjacent identically-formatted runs in `word/document.xml` without changing content or rendering; it also accepts a `.docx` directly (`python scripts/merge_runs.py doc.docx -o merged.docx`).
|
||||
|
||||
**Tracked changes:** when redlining, validate with `--author "<the name you redlined under>"` (needs `--original`) — it reports any text you changed without a `<w:ins>`/`<w:del>` around it, which is easy to do by accident and invisible in the accepted view. Wrap runs in `<w:ins>`/`<w:del>` with `w:id`, `w:author`, `w:date` attributes. Inside `<w:del>`, the text element is `<w:delText>`, not `<w:t>`. A deleted paragraph mark (`<w:pPr><w:rPr><w:del w:id=".." w:author=".." w:date=".."/></w:rPr></w:pPr>`) means "merge this paragraph into the next" — so deleting a paragraph outright is that plus a `<w:del>` around every run. The `<w:del/>` must come before the rPr's other children; their order is schema-enforced.
|
||||
|
||||
To produce a clean copy with all tracked changes accepted: `python scripts/accept_changes.py in.docx out.docx`.
|
||||
|
||||
Accepting a deleted paragraph mark should join that paragraph to the one below it, so a paragraph whose runs are *all* deleted vanishes. Word does this; `accept_changes.py` and `pandoc --track-changes=accept` don't always. Both fail the same way — they strip the deleted text but leave the emptied paragraph behind, which reads as a stray empty bullet when it was auto-numbered:
|
||||
|
||||
- `pandoc --track-changes=accept` never joins the paragraphs.
|
||||
- `accept_changes.py` (LibreOffice) joins them correctly, except when the deleted paragraph is followed by an empty spacer paragraph.
|
||||
|
||||
An empty bullet in either view is an artifact of that view, not a defect in the document. Check paragraph deletions in the XML.
|
||||
|
||||
## Comments
|
||||
|
||||
Comments require six cross-linked files. Use the helper — directory mode when you'll also be editing `document.xml` (saves an unzip/rezip cycle), `.docx`-direct mode otherwise:
|
||||
|
||||
```bash
|
||||
# Against an already-unpacked directory (preferred when also placing markers)
|
||||
python scripts/comment.py unpacked/ "Fees & expenses cap is too low"
|
||||
python scripts/comment.py unpacked/ "Agreed" --parent 0
|
||||
|
||||
# Against a .docx directly
|
||||
python scripts/comment.py contract.docx "This cap is too low" -o annotated.docx
|
||||
```
|
||||
|
||||
The script writes `comments.xml`, `commentsExtended.xml`, `commentsIds.xml`, `commentsExtensible.xml`, the relationships, and the content-type overrides. Comment IDs are auto-assigned. It then prints the `<w:commentRangeStart>`/`<w:commentRangeEnd>`/`<w:commentReference>` snippet to add to `word/document.xml` so the comment anchors to specific text — until you place those markers, the comment exists but is not visible.
|
||||
|
||||
## Dependencies
|
||||
|
||||
`docx` (npm, preinstalled — install only if `require('docx')` fails) · `pandoc` · LibreOffice (`soffice`) · `pdftoppm` (Poppler)
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
"""Accept all tracked changes in a DOCX file using LibreOffice.
|
||||
|
||||
Requires LibreOffice (soffice) to be installed.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from office.soffice import get_soffice_env
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LIBREOFFICE_PROFILE = "/tmp/libreoffice_docx_profile"
|
||||
MACRO_DIR = f"{LIBREOFFICE_PROFILE}/user/basic/Standard"
|
||||
|
||||
ACCEPT_CHANGES_MACRO = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE script:module PUBLIC "-//OpenOffice.org//DTD OfficeDocument 1.0//EN" "module.dtd">
|
||||
<script:module xmlns:script="http://openoffice.org/2000/script" script:name="Module1" script:language="StarBasic">
|
||||
Sub AcceptAllTrackedChanges()
|
||||
Dim document As Object
|
||||
Dim dispatcher As Object
|
||||
|
||||
document = ThisComponent.CurrentController.Frame
|
||||
dispatcher = createUnoService("com.sun.star.frame.DispatchHelper")
|
||||
|
||||
dispatcher.executeDispatch(document, ".uno:AcceptAllTrackedChanges", "", 0, Array())
|
||||
ThisComponent.store()
|
||||
ThisComponent.close(True)
|
||||
End Sub
|
||||
</script:module>"""
|
||||
|
||||
|
||||
def accept_changes(
|
||||
input_file: str,
|
||||
output_file: str,
|
||||
) -> tuple[None, str]:
|
||||
input_path = Path(input_file)
|
||||
output_path = Path(output_file)
|
||||
|
||||
if not input_path.exists():
|
||||
return None, f"Error: Input file not found: {input_file}"
|
||||
|
||||
if not input_path.suffix.lower() == ".docx":
|
||||
return None, f"Error: Input file is not a DOCX file: {input_file}"
|
||||
|
||||
try:
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(input_path, output_path)
|
||||
except Exception as e:
|
||||
return None, f"Error: Failed to copy input file to output location: {e}"
|
||||
|
||||
if not _setup_libreoffice_macro():
|
||||
return None, "Error: Failed to setup LibreOffice macro"
|
||||
|
||||
cmd = [
|
||||
"soffice",
|
||||
"--headless",
|
||||
f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
|
||||
"--norestore",
|
||||
"vnd.sun.star.script:Standard.Module1.AcceptAllTrackedChanges?language=Basic&location=application",
|
||||
str(output_path.absolute()),
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
check=False,
|
||||
env=get_soffice_env(),
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return (
|
||||
None,
|
||||
f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return None, f"Error: LibreOffice failed: {result.stderr}"
|
||||
|
||||
return (
|
||||
None,
|
||||
f"Successfully accepted all tracked changes: {input_file} -> {output_file}",
|
||||
)
|
||||
|
||||
|
||||
def _setup_libreoffice_macro() -> bool:
|
||||
macro_dir = Path(MACRO_DIR)
|
||||
macro_file = macro_dir / "Module1.xba"
|
||||
|
||||
if macro_file.exists() and "AcceptAllTrackedChanges" in macro_file.read_text():
|
||||
return True
|
||||
|
||||
if not macro_dir.exists():
|
||||
subprocess.run(
|
||||
[
|
||||
"soffice",
|
||||
"--headless",
|
||||
f"-env:UserInstallation=file://{LIBREOFFICE_PROFILE}",
|
||||
"--terminate_after_init",
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=10,
|
||||
check=False,
|
||||
env=get_soffice_env(),
|
||||
)
|
||||
macro_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
macro_file.write_text(ACCEPT_CHANGES_MACRO)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to setup LibreOffice macro: {e}")
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Accept all tracked changes in a DOCX file"
|
||||
)
|
||||
parser.add_argument("input_file", help="Input DOCX file with tracked changes")
|
||||
parser.add_argument(
|
||||
"output_file", help="Output DOCX file (clean, no tracked changes)"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
_, message = accept_changes(args.input_file, args.output_file)
|
||||
print(message)
|
||||
|
||||
if "Error" in message:
|
||||
raise SystemExit(1)
|
||||
Executable
+368
@@ -0,0 +1,368 @@
|
||||
"""Add comments to a DOCX document.
|
||||
|
||||
Accepts either an unpacked directory OR a .docx/.dotx file directly.
|
||||
|
||||
Usage:
|
||||
# Against an unpacked directory (writes satellite files in place)
|
||||
python comment.py unpacked/ "Comment text"
|
||||
python comment.py unpacked/ "Reply text" --parent 0
|
||||
|
||||
# Against a .docx directly (extracts, writes satellite files, rezips)
|
||||
python comment.py contract.docx "This cap is too low" -o annotated.docx
|
||||
python comment.py contract.docx "Comment" --id 5 # explicit ID
|
||||
|
||||
The comment ID is auto-assigned (max existing + 1) unless --id is given.
|
||||
Plain text is XML-escaped automatically; if you pass already-escaped text
|
||||
(e.g. &, ’) use --raw to skip escaping.
|
||||
|
||||
After running, add markers to word/document.xml so the comment is visible:
|
||||
<w:commentRangeStart w:id="N"/>
|
||||
... commented content ...
|
||||
<w:commentRangeEnd w:id="N"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="N"/></w:r>
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import random
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
from xml.parsers.expat import ExpatError
|
||||
from xml.sax.saxutils import escape as xml_escape
|
||||
|
||||
from office.helpers import opc_target, rezip as _rezip, safe_extract as _safe_extract
|
||||
|
||||
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||
NS = {
|
||||
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||
"w14": "http://schemas.microsoft.com/office/word/2010/wordml",
|
||||
"w15": "http://schemas.microsoft.com/office/word/2012/wordml",
|
||||
"w16cid": "http://schemas.microsoft.com/office/word/2016/wordml/cid",
|
||||
"w16cex": "http://schemas.microsoft.com/office/word/2018/wordml/cex",
|
||||
}
|
||||
|
||||
COMMENT_XML = """\
|
||||
<w:comment w:id="{id}" w:author="{author}" w:date="{date}" w:initials="{initials}">
|
||||
<w:p w14:paraId="{para_id}" w14:textId="77777777">
|
||||
<w:r>
|
||||
<w:rPr><w:rStyle w:val="CommentReference"/></w:rPr>
|
||||
<w:annotationRef/>
|
||||
</w:r>
|
||||
<w:r>
|
||||
<w:rPr>
|
||||
<w:color w:val="000000"/>
|
||||
<w:sz w:val="20"/>
|
||||
<w:szCs w:val="20"/>
|
||||
</w:rPr>
|
||||
<w:t xml:space="preserve">{text}</w:t>
|
||||
</w:r>
|
||||
</w:p>
|
||||
</w:comment>"""
|
||||
|
||||
COMMENT_MARKER_TEMPLATE = """
|
||||
Add to word/document.xml (markers must be direct children of w:p, never inside w:r):
|
||||
<w:commentRangeStart w:id="{cid}"/>
|
||||
<w:r>...</w:r>
|
||||
<w:commentRangeEnd w:id="{cid}"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="{cid}"/></w:r>"""
|
||||
|
||||
REPLY_MARKER_TEMPLATE = """
|
||||
Nest markers inside parent {pid}'s markers (direct children of w:p, never inside w:r):
|
||||
<w:commentRangeStart w:id="{pid}"/><w:commentRangeStart w:id="{cid}"/>
|
||||
<w:r>...</w:r>
|
||||
<w:commentRangeEnd w:id="{cid}"/><w:commentRangeEnd w:id="{pid}"/>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="{pid}"/></w:r>
|
||||
<w:r><w:rPr><w:rStyle w:val="CommentReference"/></w:rPr><w:commentReference w:id="{cid}"/></w:r>"""
|
||||
|
||||
SMART_QUOTE_ENTITIES = {
|
||||
"“": "“",
|
||||
"”": "”",
|
||||
"‘": "‘",
|
||||
"’": "’",
|
||||
}
|
||||
|
||||
|
||||
def _generate_hex_id() -> str:
|
||||
return f"{random.randint(0, 0x7FFFFFFE):08X}"
|
||||
|
||||
|
||||
def _encode_smart_quotes(text: str) -> str:
|
||||
for char, entity in SMART_QUOTE_ENTITIES.items():
|
||||
text = text.replace(char, entity)
|
||||
return text
|
||||
|
||||
|
||||
def _append_xml(xml_path: Path, root_tag: str, content: str) -> None:
|
||||
dom = defusedxml.minidom.parseString(xml_path.read_text(encoding="utf-8"))
|
||||
root = dom.getElementsByTagName(root_tag)[0]
|
||||
ns_attrs = " ".join(f'xmlns:{k}="{v}"' for k, v in NS.items())
|
||||
wrapper_dom = defusedxml.minidom.parseString(f"<root {ns_attrs}>{content}</root>")
|
||||
for child in wrapper_dom.documentElement.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE:
|
||||
root.appendChild(dom.importNode(child, True))
|
||||
output = _encode_smart_quotes(dom.toxml(encoding="UTF-8").decode("utf-8"))
|
||||
xml_path.write_text(output, encoding="utf-8")
|
||||
|
||||
|
||||
def _find_para_id(comments_path: Path, comment_id: int) -> str | None:
|
||||
dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8"))
|
||||
for c in dom.getElementsByTagName("w:comment"):
|
||||
if c.getAttribute("w:id") == str(comment_id):
|
||||
for p in c.getElementsByTagName("w:p"):
|
||||
if pid := p.getAttribute("w14:paraId"):
|
||||
return pid
|
||||
return None
|
||||
|
||||
|
||||
def _next_comment_id(comments_path: Path) -> int:
|
||||
if not comments_path.exists():
|
||||
return 0
|
||||
dom = defusedxml.minidom.parseString(comments_path.read_text(encoding="utf-8"))
|
||||
ids = []
|
||||
for c in dom.getElementsByTagName("w:comment"):
|
||||
try:
|
||||
ids.append(int(c.getAttribute("w:id")))
|
||||
except ValueError:
|
||||
pass
|
||||
return (max(ids) + 1) if ids else 0
|
||||
|
||||
|
||||
def _get_next_rid(rels_path: Path) -> int:
|
||||
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
||||
max_rid = 0
|
||||
for rel in dom.getElementsByTagName("Relationship"):
|
||||
rid = rel.getAttribute("Id")
|
||||
if rid and rid.startswith("rId"):
|
||||
try:
|
||||
max_rid = max(max_rid, int(rid[3:]))
|
||||
except ValueError:
|
||||
pass
|
||||
return max_rid + 1
|
||||
|
||||
|
||||
def _has_relationship(rels_path: Path, target: str) -> bool:
|
||||
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
||||
return any(
|
||||
rel.getAttribute("Target") == target
|
||||
for rel in dom.getElementsByTagName("Relationship")
|
||||
)
|
||||
|
||||
|
||||
def _has_content_type(ct_path: Path, part_name: str) -> bool:
|
||||
dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
|
||||
return any(
|
||||
o.getAttribute("PartName") == part_name
|
||||
for o in dom.getElementsByTagName("Override")
|
||||
)
|
||||
|
||||
|
||||
_COMMENT_RELS = [
|
||||
("http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments", "comments.xml"),
|
||||
("http://schemas.microsoft.com/office/2011/relationships/commentsExtended", "commentsExtended.xml"),
|
||||
("http://schemas.microsoft.com/office/2016/09/relationships/commentsIds", "commentsIds.xml"),
|
||||
("http://schemas.microsoft.com/office/2018/08/relationships/commentsExtensible", "commentsExtensible.xml"),
|
||||
]
|
||||
_COMMENT_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"),
|
||||
]
|
||||
|
||||
|
||||
def _ensure_comment_relationships(unpacked_dir: Path) -> None:
|
||||
rels_path = unpacked_dir / "word" / "_rels" / "document.xml.rels"
|
||||
if not rels_path.exists():
|
||||
return
|
||||
dom = defusedxml.minidom.parseString(rels_path.read_text(encoding="utf-8"))
|
||||
root = dom.documentElement
|
||||
comment_types = {rel_type for rel_type, _ in _COMMENT_RELS}
|
||||
existing = set()
|
||||
for rel in dom.getElementsByTagName("Relationship"):
|
||||
if rel.getAttribute("Type") not in comment_types:
|
||||
continue
|
||||
part = opc_target(
|
||||
rel.getAttribute("Target"),
|
||||
"word/document.xml",
|
||||
rel.getAttribute("TargetMode"),
|
||||
)
|
||||
if part is not None:
|
||||
existing.add(part)
|
||||
next_rid = _get_next_rid(rels_path)
|
||||
changed = False
|
||||
for rel_type, target in _COMMENT_RELS:
|
||||
if opc_target(target, "word/document.xml") in existing:
|
||||
continue
|
||||
rel = dom.createElement("Relationship")
|
||||
rel.setAttribute("Id", f"rId{next_rid}")
|
||||
rel.setAttribute("Type", rel_type)
|
||||
rel.setAttribute("Target", target)
|
||||
root.appendChild(rel)
|
||||
next_rid += 1
|
||||
changed = True
|
||||
if changed:
|
||||
rels_path.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
|
||||
|
||||
def _ensure_comment_content_types(unpacked_dir: Path) -> None:
|
||||
ct_path = unpacked_dir / "[Content_Types].xml"
|
||||
if not ct_path.exists():
|
||||
return
|
||||
dom = defusedxml.minidom.parseString(ct_path.read_text(encoding="utf-8"))
|
||||
root = dom.documentElement
|
||||
existing = {
|
||||
o.getAttribute("PartName")
|
||||
for o in dom.getElementsByTagName("Override")
|
||||
}
|
||||
changed = False
|
||||
for part_name, content_type in _COMMENT_OVERRIDES:
|
||||
if part_name in existing:
|
||||
continue
|
||||
override = dom.createElement("Override")
|
||||
override.setAttribute("PartName", part_name)
|
||||
override.setAttribute("ContentType", content_type)
|
||||
root.appendChild(override)
|
||||
changed = True
|
||||
if changed:
|
||||
ct_path.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
|
||||
|
||||
def add_comment(
|
||||
unpacked_dir: Path | str,
|
||||
text: str,
|
||||
comment_id: int | None = None,
|
||||
author: str = "Claude",
|
||||
initials: str = "C",
|
||||
parent_id: int | None = None,
|
||||
raw: bool = False,
|
||||
) -> tuple[int, str, str]:
|
||||
unpacked_dir = Path(unpacked_dir)
|
||||
if not raw:
|
||||
text = xml_escape(text)
|
||||
author = xml_escape(author, {'"': """})
|
||||
initials = xml_escape(initials, {'"': """})
|
||||
word = unpacked_dir / "word"
|
||||
if not word.exists():
|
||||
raise FileNotFoundError(f"{word} not found (not an unpacked .docx?)")
|
||||
|
||||
comments = word / "comments.xml"
|
||||
if comment_id is None:
|
||||
comment_id = _next_comment_id(comments)
|
||||
|
||||
parent_para = None
|
||||
if parent_id is not None:
|
||||
parent_para = _find_para_id(comments, parent_id) if comments.exists() else None
|
||||
if not parent_para:
|
||||
raise ValueError(f"parent comment {parent_id} not found")
|
||||
|
||||
para_id, durable_id = _generate_hex_id(), _generate_hex_id()
|
||||
ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
if not comments.exists():
|
||||
shutil.copy(TEMPLATE_DIR / "comments.xml", comments)
|
||||
_ensure_comment_relationships(unpacked_dir)
|
||||
_ensure_comment_content_types(unpacked_dir)
|
||||
_append_xml(
|
||||
comments,
|
||||
"w:comments",
|
||||
COMMENT_XML.format(
|
||||
id=comment_id, author=author, date=ts, initials=initials,
|
||||
para_id=para_id, text=text,
|
||||
),
|
||||
)
|
||||
|
||||
ext = word / "commentsExtended.xml"
|
||||
if not ext.exists():
|
||||
shutil.copy(TEMPLATE_DIR / "commentsExtended.xml", ext)
|
||||
if parent_para is not None:
|
||||
_append_xml(
|
||||
ext, "w15:commentsEx",
|
||||
f'<w15:commentEx w15:paraId="{para_id}" w15:paraIdParent="{parent_para}" w15:done="0"/>',
|
||||
)
|
||||
else:
|
||||
_append_xml(
|
||||
ext, "w15:commentsEx",
|
||||
f'<w15:commentEx w15:paraId="{para_id}" w15:done="0"/>',
|
||||
)
|
||||
|
||||
ids = word / "commentsIds.xml"
|
||||
if not ids.exists():
|
||||
shutil.copy(TEMPLATE_DIR / "commentsIds.xml", ids)
|
||||
_append_xml(
|
||||
ids, "w16cid:commentsIds",
|
||||
f'<w16cid:commentId w16cid:paraId="{para_id}" w16cid:durableId="{durable_id}"/>',
|
||||
)
|
||||
|
||||
extensible = word / "commentsExtensible.xml"
|
||||
if not extensible.exists():
|
||||
shutil.copy(TEMPLATE_DIR / "commentsExtensible.xml", extensible)
|
||||
_append_xml(
|
||||
extensible, "w16cex:commentsExtensible",
|
||||
f'<w16cex:commentExtensible w16cex:durableId="{durable_id}" w16cex:dateUtc="{ts}"/>',
|
||||
)
|
||||
|
||||
action = "reply" if parent_id is not None else "comment"
|
||||
return comment_id, para_id, f"Added {action} id={comment_id} (paraId={para_id})"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description="Add a comment to a DOCX (directory or .docx file).")
|
||||
p.add_argument("input", help="Unpacked DOCX directory OR a .docx/.dotx file")
|
||||
p.add_argument("text", help="Comment text (plain text; XML-escaped automatically)")
|
||||
p.add_argument("--raw", action="store_true",
|
||||
help="Treat text as pre-escaped XML (skip automatic escaping)")
|
||||
p.add_argument("--id", type=int, dest="comment_id",
|
||||
help="Comment ID (default: auto-assign as max existing + 1)")
|
||||
p.add_argument("--author", default="Claude", help="Author name")
|
||||
p.add_argument("--initials", default="C", help="Author initials")
|
||||
p.add_argument("--parent", type=int, help="Parent comment ID (makes this a reply)")
|
||||
p.add_argument("-o", "--output",
|
||||
help="Output .docx path (only used when input is a .docx; default: overwrite input)")
|
||||
args = p.parse_args()
|
||||
|
||||
src = Path(args.input)
|
||||
|
||||
try:
|
||||
if src.is_dir():
|
||||
if args.output:
|
||||
print("Warning: --output ignored for directory input", file=sys.stderr)
|
||||
cid, _, msg = add_comment(
|
||||
src, args.text, comment_id=args.comment_id,
|
||||
author=args.author, initials=args.initials,
|
||||
parent_id=args.parent, raw=args.raw,
|
||||
)
|
||||
print(msg)
|
||||
elif src.is_file() and src.suffix.lower() in (".docx", ".dotx"):
|
||||
out = Path(args.output) if args.output else src
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
with zipfile.ZipFile(src) as zf:
|
||||
_safe_extract(zf, tmp_path)
|
||||
cid, _, msg = add_comment(
|
||||
tmp_path, args.text, comment_id=args.comment_id,
|
||||
author=args.author, initials=args.initials,
|
||||
parent_id=args.parent, raw=args.raw,
|
||||
)
|
||||
_rezip(tmp_path, out)
|
||||
print(msg)
|
||||
print(f"Wrote {out} (comment defined; add markers to word/document.xml to make it visible)")
|
||||
else:
|
||||
print(f"Error: {src} is neither a directory nor a .docx/.dotx file", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except (FileNotFoundError, ValueError, zipfile.BadZipFile, ExpatError) as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.parent is not None:
|
||||
print(REPLY_MARKER_TEMPLATE.format(pid=args.parent, cid=cid))
|
||||
else:
|
||||
print(COMMENT_MARKER_TEMPLATE.format(cid=cid))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+310
@@ -0,0 +1,310 @@
|
||||
"""Merge adjacent identically-formatted runs in a DOCX.
|
||||
|
||||
Word fragments paragraph text across many <w:r> elements (revision ids,
|
||||
spell-check markers, editing history), which makes find-and-replace on
|
||||
word/document.xml unreliable — the string you're looking for is split
|
||||
across runs. This coalesces adjacent runs whose formatting (<w:rPr>) is
|
||||
identical, strips rsid attributes and proofErr markers, and consolidates the
|
||||
text elements — <w:t>, and <w:delText> for text inside a tracked deletion.
|
||||
|
||||
Rendering is unchanged. The text you search is what Word draws, which is not
|
||||
always the bytes in the file: an element without xml:space="preserve" has its
|
||||
edge whitespace trimmed before it reaches the page, so `<w:t>Hello </w:t>`
|
||||
followed by `<w:t>world</w:t>` reads "Helloworld" and merges to exactly that.
|
||||
|
||||
Runs in two different <w:ins>/<w:del> wrappers are never merged: that would
|
||||
rewrite tracked-change structure, collapsing separate revisions into one.
|
||||
|
||||
Only word/document.xml is processed (not headers, footers, or footnotes).
|
||||
|
||||
Usage:
|
||||
python merge_runs.py unpacked/ # after unzip, before editing
|
||||
python merge_runs.py document.docx # rewrite in place
|
||||
python merge_runs.py document.docx -o out.docx
|
||||
"""
|
||||
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
|
||||
from office.helpers import XML_SPACE, rendered_text, rezip, safe_extract
|
||||
|
||||
WORDML_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
|
||||
|
||||
def merge_runs(input_dir: str) -> tuple[int, str]:
|
||||
doc_xml = Path(input_dir) / "word" / "document.xml"
|
||||
|
||||
if not doc_xml.exists():
|
||||
return 0, f"Error: {doc_xml} not found"
|
||||
|
||||
try:
|
||||
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
|
||||
root = dom.documentElement
|
||||
run_names = _run_tag_names(root)
|
||||
|
||||
_remove_elements(root, "proofErr")
|
||||
|
||||
runs = _find_runs(root, run_names)
|
||||
_strip_rsid_attrs(runs)
|
||||
|
||||
merge_count = 0
|
||||
for container in {run.parentNode for run in runs}:
|
||||
merge_count += _merge_runs_in(container, run_names)
|
||||
|
||||
doc_xml.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
return merge_count, f"Merged {merge_count} runs"
|
||||
|
||||
except Exception as e:
|
||||
return 0, f"Error: {e}"
|
||||
|
||||
|
||||
|
||||
|
||||
def _is_element(node, tag: str) -> bool:
|
||||
name = node.localName or node.tagName
|
||||
return name == tag or name.endswith(f":{tag}")
|
||||
|
||||
|
||||
def _run_tag_names(root) -> set[str]:
|
||||
names = set()
|
||||
for attr in root.attributes.values():
|
||||
if attr.value == WORDML_NS:
|
||||
if attr.name == "xmlns":
|
||||
names.add("r")
|
||||
elif attr.name.startswith("xmlns:"):
|
||||
names.add(attr.name.split(":", 1)[1] + ":r")
|
||||
return names or {"w:r", "r"}
|
||||
|
||||
|
||||
def _find_elements(root, tag: str) -> list:
|
||||
results = []
|
||||
|
||||
def traverse(node):
|
||||
if node.nodeType == node.ELEMENT_NODE:
|
||||
if _is_element(node, tag):
|
||||
results.append(node)
|
||||
for child in node.childNodes:
|
||||
traverse(child)
|
||||
|
||||
traverse(root)
|
||||
return results
|
||||
|
||||
|
||||
def _find_runs(root, run_names: set[str]) -> list:
|
||||
return [e for e in _find_elements(root, "r") if _is_run(e, run_names)]
|
||||
|
||||
|
||||
def _get_child(parent, tag: str):
|
||||
return next(iter(_get_children(parent, tag)), None)
|
||||
|
||||
|
||||
def _get_children(parent, tag: str) -> list:
|
||||
return [
|
||||
child
|
||||
for child in parent.childNodes
|
||||
if child.nodeType == child.ELEMENT_NODE and _is_element(child, tag)
|
||||
]
|
||||
|
||||
|
||||
def _is_adjacent(elem1, elem2) -> bool:
|
||||
node = elem1.nextSibling
|
||||
while node:
|
||||
if node == elem2:
|
||||
return True
|
||||
if node.nodeType == node.ELEMENT_NODE:
|
||||
return False
|
||||
if node.nodeType == node.TEXT_NODE and node.data.strip(XML_SPACE):
|
||||
return False
|
||||
node = node.nextSibling
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
def _remove_elements(root, tag: str):
|
||||
for elem in _find_elements(root, tag):
|
||||
if elem.parentNode:
|
||||
elem.parentNode.removeChild(elem)
|
||||
|
||||
|
||||
def _strip_rsid_attrs(runs: list):
|
||||
for run in runs:
|
||||
for attr in list(run.attributes.values()):
|
||||
if "rsid" in attr.name.lower():
|
||||
run.removeAttribute(attr.name)
|
||||
|
||||
|
||||
|
||||
|
||||
def _merge_runs_in(container, run_names: set[str]) -> int:
|
||||
merge_count = 0
|
||||
run = _first_child_run(container, run_names)
|
||||
|
||||
while run:
|
||||
while True:
|
||||
next_elem = _next_element_sibling(run)
|
||||
if next_elem and _is_run(next_elem, run_names) and _can_merge(run, next_elem):
|
||||
_merge_run_content(run, next_elem)
|
||||
container.removeChild(next_elem)
|
||||
merge_count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
_consolidate_text(run)
|
||||
run = _next_sibling_run(run, run_names)
|
||||
|
||||
return merge_count
|
||||
|
||||
|
||||
def _first_child_run(container, run_names: set[str]):
|
||||
for child in container.childNodes:
|
||||
if child.nodeType == child.ELEMENT_NODE and _is_run(child, run_names):
|
||||
return child
|
||||
return None
|
||||
|
||||
|
||||
def _next_element_sibling(node):
|
||||
sibling = node.nextSibling
|
||||
while sibling:
|
||||
if sibling.nodeType == sibling.ELEMENT_NODE:
|
||||
return sibling
|
||||
sibling = sibling.nextSibling
|
||||
return None
|
||||
|
||||
|
||||
def _next_sibling_run(node, run_names: set[str]):
|
||||
sibling = node.nextSibling
|
||||
while sibling:
|
||||
if sibling.nodeType == sibling.ELEMENT_NODE:
|
||||
if _is_run(sibling, run_names):
|
||||
return sibling
|
||||
sibling = sibling.nextSibling
|
||||
return None
|
||||
|
||||
|
||||
def _is_run(node, run_names: set[str]) -> bool:
|
||||
return node.tagName in run_names
|
||||
|
||||
|
||||
def _can_merge(run1, run2) -> bool:
|
||||
rpr1 = _get_child(run1, "rPr")
|
||||
rpr2 = _get_child(run2, "rPr")
|
||||
|
||||
if (rpr1 is None) != (rpr2 is None):
|
||||
return False
|
||||
if rpr1 is None:
|
||||
return True
|
||||
return rpr1.toxml() == rpr2.toxml()
|
||||
|
||||
|
||||
def _merge_run_content(target, source):
|
||||
for child in list(source.childNodes):
|
||||
if child.nodeType == child.ELEMENT_NODE:
|
||||
name = child.localName or child.tagName
|
||||
if name != "rPr" and not name.endswith(":rPr"):
|
||||
target.appendChild(child)
|
||||
|
||||
|
||||
def _element_text(elem) -> str:
|
||||
return "".join(
|
||||
child.data
|
||||
for child in elem.childNodes
|
||||
if child.nodeType in (child.TEXT_NODE, child.CDATA_SECTION_NODE)
|
||||
)
|
||||
|
||||
|
||||
def _has_preserve(elem) -> bool:
|
||||
return elem.getAttribute("xml:space") == "preserve"
|
||||
|
||||
|
||||
def _rendered_text(elem) -> str:
|
||||
return rendered_text(_element_text(elem), _has_preserve(elem))
|
||||
|
||||
|
||||
def _consolidate_text(run):
|
||||
for tag in ("t", "delText"):
|
||||
_consolidate_text_elements(run, tag)
|
||||
|
||||
|
||||
def _consolidate_text_elements(run, tag: str):
|
||||
t_elements = _get_children(run, tag)
|
||||
|
||||
for i in range(len(t_elements) - 1, 0, -1):
|
||||
curr, prev = t_elements[i], t_elements[i - 1]
|
||||
|
||||
if _is_adjacent(prev, curr):
|
||||
merged = _rendered_text(prev) + _rendered_text(curr)
|
||||
had_preserve = _has_preserve(prev) or _has_preserve(curr)
|
||||
|
||||
new_text = run.ownerDocument.createTextNode(merged)
|
||||
for node in list(prev.childNodes):
|
||||
if node.nodeType in (node.TEXT_NODE, node.CDATA_SECTION_NODE):
|
||||
prev.removeChild(node)
|
||||
else:
|
||||
run.insertBefore(node, curr)
|
||||
prev.appendChild(new_text)
|
||||
for node in list(curr.childNodes):
|
||||
if node.nodeType not in (node.TEXT_NODE, node.CDATA_SECTION_NODE):
|
||||
run.insertBefore(node, curr)
|
||||
|
||||
if merged != merged.strip(XML_SPACE) or had_preserve:
|
||||
prev.setAttribute("xml:space", "preserve")
|
||||
elif prev.hasAttribute("xml:space"):
|
||||
prev.removeAttribute("xml:space")
|
||||
|
||||
run.removeChild(curr)
|
||||
|
||||
|
||||
|
||||
|
||||
def _merge_or_die(path: Path) -> str:
|
||||
_, msg = merge_runs(str(path))
|
||||
if msg.startswith("Error"):
|
||||
print(msg, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
return msg
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(
|
||||
description="Merge adjacent identically-formatted runs in a DOCX (directory or .docx file)."
|
||||
)
|
||||
p.add_argument("input", help="Unpacked DOCX directory OR a .docx/.dotx file")
|
||||
p.add_argument(
|
||||
"-o", "--output",
|
||||
help="Output .docx path (only valid when input is a .docx; default: overwrite input)",
|
||||
)
|
||||
args = p.parse_args()
|
||||
|
||||
src = Path(args.input)
|
||||
|
||||
try:
|
||||
if src.is_dir():
|
||||
if args.output:
|
||||
p.error("--output is only valid for .docx input; directory input is modified in place")
|
||||
print(_merge_or_die(src))
|
||||
elif src.is_file() and src.suffix.lower() in (".docx", ".dotx"):
|
||||
out = Path(args.output) if args.output else src
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
with zipfile.ZipFile(src) as zf:
|
||||
safe_extract(zf, tmp_path)
|
||||
msg = _merge_or_die(tmp_path)
|
||||
rezip(tmp_path, out)
|
||||
print(f"{msg}; wrote {out}")
|
||||
else:
|
||||
print(f"Error: {src} is neither a directory nor a .docx/.dotx file", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except (OSError, ValueError, zipfile.BadZipFile) as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
import posixpath
|
||||
import re
|
||||
import stat
|
||||
import tempfile
|
||||
import urllib.parse
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
OOXML_FAMILY = {
|
||||
".docx": "docx",
|
||||
".dotx": "docx",
|
||||
".pptx": "pptx",
|
||||
".potx": "pptx",
|
||||
".xlsx": "xlsx",
|
||||
".xltx": "xlsx",
|
||||
}
|
||||
|
||||
_SCHEME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:")
|
||||
|
||||
SLIDE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide"
|
||||
|
||||
|
||||
def opc_target(target: str, source_part: str, target_mode: str = "") -> str | None:
|
||||
if not target:
|
||||
return None
|
||||
if target_mode.lower() == "external":
|
||||
return None
|
||||
if _SCHEME_RE.match(target):
|
||||
return None
|
||||
|
||||
target = urllib.parse.unquote(target)
|
||||
|
||||
if "\\" in target:
|
||||
raise ValueError(f"relationship target is not a POSIX part name: {target!r}")
|
||||
|
||||
if target.startswith("/"):
|
||||
joined = target.lstrip("/")
|
||||
else:
|
||||
joined = posixpath.join(posixpath.dirname(source_part), target)
|
||||
|
||||
parts: list[str] = []
|
||||
for segment in posixpath.normpath(joined).split("/"):
|
||||
if segment in ("", "."):
|
||||
continue
|
||||
if segment == "..":
|
||||
if not parts:
|
||||
raise ValueError(f"relationship target escapes the package: {target!r}")
|
||||
parts.pop()
|
||||
else:
|
||||
parts.append(segment)
|
||||
|
||||
if not parts:
|
||||
raise ValueError(f"relationship target resolves to nothing: {target!r}")
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
def rels_source_part(rels_file: Path, unpacked_dir: Path) -> str:
|
||||
owner_dir = rels_file.parent.parent.relative_to(unpacked_dir)
|
||||
return posixpath.join(owner_dir.as_posix(), rels_file.name[: -len(".rels")]).lstrip("./")
|
||||
|
||||
|
||||
def part_text(data: bytes) -> str:
|
||||
return data.decode("utf-8", "surrogateescape")
|
||||
|
||||
|
||||
XML_SPACE = " \t\r\n"
|
||||
|
||||
|
||||
def rendered_text(text: str, preserve: bool) -> str:
|
||||
return text if preserve else text.strip(XML_SPACE)
|
||||
|
||||
|
||||
def safe_extract(zf: zipfile.ZipFile, dest: Path) -> None:
|
||||
dest = dest.resolve()
|
||||
for m in zf.infolist():
|
||||
if stat.S_ISLNK(m.external_attr >> 16):
|
||||
raise ValueError(f"symlink archive entry not allowed: {m.filename!r}")
|
||||
target = (dest / m.filename).resolve()
|
||||
if not target.is_relative_to(dest):
|
||||
raise ValueError(f"unsafe archive entry: {m.filename!r}")
|
||||
zf.extract(m, dest)
|
||||
|
||||
|
||||
def rezip(src_dir: Path, out_path: Path) -> None:
|
||||
files = sorted(p for p in src_dir.rglob("*") if p.is_file())
|
||||
ct = src_dir / "[Content_Types].xml"
|
||||
fd, tmp_name = tempfile.mkstemp(
|
||||
prefix=out_path.name + ".", suffix=".tmp", dir=out_path.parent
|
||||
)
|
||||
tmp_out = Path(tmp_name)
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as fh:
|
||||
with zipfile.ZipFile(fh, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
if ct.exists():
|
||||
zf.write(ct, ct.relative_to(src_dir), compress_type=zipfile.ZIP_STORED)
|
||||
for f in files:
|
||||
if f == ct:
|
||||
continue
|
||||
zf.write(f, f.relative_to(src_dir))
|
||||
if out_path.exists():
|
||||
mode = out_path.stat().st_mode & 0o777
|
||||
else:
|
||||
umask = os.umask(0)
|
||||
os.umask(umask)
|
||||
mode = 0o666 & ~umask
|
||||
os.chmod(tmp_out, mode)
|
||||
os.replace(tmp_out, out_path)
|
||||
finally:
|
||||
if tmp_out.exists():
|
||||
tmp_out.unlink()
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Find chart XML that PowerPoint refuses but the schema accepts.
|
||||
|
||||
Detection only: for either fault more than one repair is valid, and only the
|
||||
author knows which was meant.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Mapping
|
||||
|
||||
from . import part_text
|
||||
|
||||
|
||||
_CHART_PART_RE = re.compile(r"ppt/charts/chart\d+\.xml")
|
||||
|
||||
_GROUPING_RE = re.compile(r"""<c:grouping\b[^>]*?\bval=["'](\w+)["']""")
|
||||
_DLBL_POS_RE = re.compile(r"""<c:dLblPos\b[^>]*?\bval=["'](\w+)["']""")
|
||||
|
||||
def _strip_ext_lst(text: str) -> str:
|
||||
out, cursor = [], 0
|
||||
for lo, hi in _ext_lst_spans(text):
|
||||
out.append(text[cursor:lo])
|
||||
cursor = hi
|
||||
out.append(text[cursor:])
|
||||
return "".join(out)
|
||||
|
||||
_BAR_GROUP_RE = re.compile(r"<c:(bar3DChart|barChart)\b[^>]*(?<!/)>.*?</c:\1\s*>", re.DOTALL)
|
||||
|
||||
STACKED_GROUPINGS = frozenset({"stacked", "percentStacked"})
|
||||
ILLEGAL_ON_STACKED = frozenset({"outEnd"})
|
||||
LEGAL_ON_STACKED = ("ctr", "inEnd", "inBase")
|
||||
|
||||
|
||||
def _check_stacked_label_positions(part: str, xml: str) -> list[str]:
|
||||
problems: list[str] = []
|
||||
for match in _BAR_GROUP_RE.finditer(xml):
|
||||
block = _strip_ext_lst(match.group(0))
|
||||
group = match.group(1)
|
||||
|
||||
grouping = _GROUPING_RE.search(block)
|
||||
if grouping is None or grouping.group(1) not in STACKED_GROUPINGS:
|
||||
continue
|
||||
|
||||
bad = [p for p in _DLBL_POS_RE.findall(block) if p in ILLEGAL_ON_STACKED]
|
||||
for pos in sorted(set(bad)):
|
||||
problems.append(
|
||||
f'{part}: {bad.count(pos)} data label(s) use dLblPos="{pos}" on a '
|
||||
f"{grouping.group(1)} {group}; PowerPoint allows only "
|
||||
f"{', '.join(LEGAL_ON_STACKED)} there"
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
|
||||
_ANY_CHART_GROUP_RE = re.compile(r"<c:(\w+Chart)\b[^>]*(?<!/)>.*?</c:\1\s*>", re.DOTALL)
|
||||
|
||||
_AXID_RE = re.compile(
|
||||
r"""\s*<c:axId\b[^>]*?\bval=["'](-?\d+)["']\s*(?:/>|>\s*</c:axId\s*>)"""
|
||||
)
|
||||
|
||||
_AXIS_DECL_RE = re.compile(
|
||||
r"""<c:(catAx|valAx|serAx|dateAx)\b[^>]*(?<!/)>\s*<c:axId\b[^>]*?\bval=["'](-?\d+)["']"""
|
||||
)
|
||||
|
||||
AXID_LIMIT = {
|
||||
"barChart": 2, "lineChart": 2, "areaChart": 2, "scatterChart": 2,
|
||||
"bubbleChart": 2, "radarChart": 2, "stockChart": 2,
|
||||
"bar3DChart": 3, "line3DChart": 3, "area3DChart": 3,
|
||||
"surfaceChart": 3, "surface3DChart": 3,
|
||||
}
|
||||
|
||||
AXID_MINIMUM = {
|
||||
"barChart": 2, "lineChart": 2, "areaChart": 2, "scatterChart": 2,
|
||||
"bubbleChart": 2, "radarChart": 2, "stockChart": 2,
|
||||
"bar3DChart": 2, "area3DChart": 2, "surfaceChart": 2,
|
||||
"line3DChart": 3, "surface3DChart": 3,
|
||||
}
|
||||
|
||||
|
||||
def _declared_axes(xml: str) -> dict[str, list[str]]:
|
||||
axes: dict[str, list[str]] = {}
|
||||
for kind, axid in _AXIS_DECL_RE.findall(xml):
|
||||
axes.setdefault(kind, []).append(axid)
|
||||
return axes
|
||||
|
||||
|
||||
def _canonical_ids(axes: dict[str, list[str]], limit: int) -> list[str] | None:
|
||||
category = axes.get("catAx", []) + axes.get("dateAx", [])
|
||||
value = axes.get("valAx", [])
|
||||
series = axes.get("serAx", [])
|
||||
if len(category) != 1 or len(value) != 1 or len(series) > 1:
|
||||
return None
|
||||
ids = [category[0], value[0]]
|
||||
if limit >= 3 and series:
|
||||
ids.append(series[0])
|
||||
return ids
|
||||
|
||||
|
||||
def _undeclared_axes(kind: str, block: str, axes: dict[str, list[str]]) -> list[str] | None:
|
||||
if kind not in AXID_LIMIT:
|
||||
return None
|
||||
ids = _AXID_RE.findall(block)
|
||||
declared = {i for group in axes.values() for i in group}
|
||||
if len([i for i in ids if i in declared]) >= 2:
|
||||
return None
|
||||
return ids
|
||||
|
||||
|
||||
def _check_chart_axis_references(part: str, xml: str) -> list[str]:
|
||||
axes = _declared_axes(xml)
|
||||
problems: list[str] = []
|
||||
declared = {i for group in axes.values() for i in group}
|
||||
for match in _ANY_CHART_GROUP_RE.finditer(xml):
|
||||
kind, block = match.group(1), match.group(0)
|
||||
ids = _undeclared_axes(kind, block, axes)
|
||||
if ids is None:
|
||||
continue
|
||||
if not ids:
|
||||
problems.append(
|
||||
f"{part}: <c:{kind}> declares no <c:axId> this part can resolve; a chart "
|
||||
f"group needs {AXID_MINIMUM[kind]}, and PowerPoint discards one with fewer"
|
||||
)
|
||||
continue
|
||||
dead = [i for i in ids if i not in declared]
|
||||
canonical = _canonical_ids(axes, AXID_LIMIT[kind])
|
||||
if canonical is not None and len(canonical) >= AXID_MINIMUM[kind]:
|
||||
hint = f"Fix: point them at the axes this part declares ({', '.join(canonical)})"
|
||||
else:
|
||||
hint = ("Fix: the part declares several axes of a kind -- declare the "
|
||||
"secondary axes the series expects, or drop them")
|
||||
detail = (f"of which {', '.join(dead)} name no declared axis"
|
||||
if dead else f"only {len(ids)} of which this part declares")
|
||||
problems.append(
|
||||
f"{part}: <c:{kind}> references axId {', '.join(ids)}, {detail}, "
|
||||
f"leaving fewer than two live axes; PowerPoint discards the chart. {hint}"
|
||||
)
|
||||
return problems
|
||||
|
||||
|
||||
def _ext_lst_spans(text: str) -> list[tuple[int, int]]:
|
||||
spans: list[tuple[int, int]] = []
|
||||
depth = 0
|
||||
start = 0
|
||||
for match in re.finditer(r"<(/?)c:extLst\b[^>]*?(/?)>", text):
|
||||
closing, self_closing = match.group(1), match.group(2)
|
||||
if self_closing:
|
||||
continue
|
||||
if closing:
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
spans.append((start, match.end()))
|
||||
else:
|
||||
if depth == 0:
|
||||
start = match.start()
|
||||
depth += 1
|
||||
return spans
|
||||
|
||||
|
||||
CHART_CHECKS = (_check_stacked_label_positions, _check_chart_axis_references)
|
||||
|
||||
|
||||
def find_chart_problems(files: Mapping[str, bytes]) -> list[str]:
|
||||
problems: list[str] = []
|
||||
for part in sorted(n for n in files if _CHART_PART_RE.fullmatch(n)):
|
||||
xml = part_text(files[part])
|
||||
for check in CHART_CHECKS:
|
||||
problems.extend(check(part, xml))
|
||||
return problems
|
||||
@@ -0,0 +1,60 @@
|
||||
"""Pick the slide-XML schema errors PowerPoint refuses the file over.
|
||||
|
||||
A denylist over lxml's messages, so an unrecognised error class is a miss rather
|
||||
than a false alarm.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
SLIDE_PART_RE = re.compile(
|
||||
r"ppt/(slides|slideLayouts|slideMasters|notesSlides|notesMasters|handoutMasters)"
|
||||
r"/[^/]+\.xml"
|
||||
)
|
||||
|
||||
FATAL_SLIDE_ERRORS: tuple[tuple[re.Pattern[str], str], ...] = (
|
||||
(
|
||||
re.compile(r"\}tableStyleId': This element is not expected"),
|
||||
"two <a:tableStyleId> in one <a:tblPr> (the schema allows one)",
|
||||
),
|
||||
(
|
||||
re.compile(r"\}srgbClr', attribute 'val'"),
|
||||
"a colour that is not six hex digits",
|
||||
),
|
||||
(
|
||||
re.compile(r"\}txBody': Missing child element"),
|
||||
"a <p:txBody> with no children",
|
||||
),
|
||||
(
|
||||
re.compile(r"\}miter', attribute 'lim'"),
|
||||
'a line join with lim="NaN"',
|
||||
),
|
||||
(
|
||||
re.compile(r"\}uLnTx': This element is not expected"),
|
||||
"<a:uLnTx> in a position the schema forbids",
|
||||
),
|
||||
(
|
||||
re.compile(r"\}overrideClrMapping': This element is not expected"),
|
||||
"<p:overrideClrMapping> in a position the schema forbids",
|
||||
),
|
||||
(
|
||||
re.compile(r"\}nvGrpSpPr': Missing child element"),
|
||||
"a <p:nvGrpSpPr> with no children",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def is_schema_verdict(error: str) -> bool:
|
||||
return error.startswith("Element ")
|
||||
|
||||
|
||||
def fatal_slide_errors(errors: set[str]) -> list[str]:
|
||||
out = []
|
||||
for error in sorted(errors):
|
||||
for pattern, meaning in FATAL_SLIDE_ERRORS:
|
||||
if pattern.search(error):
|
||||
out.append(f"{meaning}: {error}")
|
||||
break
|
||||
return out
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Find masters sharing a theme part in the way PowerPoint refuses to open.
|
||||
|
||||
Reports only; the fix is to move <p:notesMasterIdLst> back to directly after
|
||||
<p:sldIdLst> in ppt/presentation.xml.
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import posixpath
|
||||
import re
|
||||
from typing import Mapping
|
||||
|
||||
from . import part_text
|
||||
|
||||
THEME_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
|
||||
|
||||
_MASTER_RE = re.compile(
|
||||
r"^ppt/(?P<group>slideMasters|notesMasters|handoutMasters)/"
|
||||
r"(?:slide|notes|handout)Master(?P<num>\d+)\.xml$"
|
||||
)
|
||||
_GROUP_ORDER = {"slideMasters": 0, "notesMasters": 1, "handoutMasters": 2}
|
||||
|
||||
_RELATIONSHIP_RE = re.compile(
|
||||
r"<Relationship\b[^>]*?(?:/>|>.*?</Relationship\s*>)", re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def _sort_key(name: str) -> tuple[int, int]:
|
||||
m = _MASTER_RE.match(name)
|
||||
assert m is not None
|
||||
return (_GROUP_ORDER[m.group("group")], int(m.group("num")))
|
||||
|
||||
|
||||
def _rels_path(part: str) -> str:
|
||||
directory, base = posixpath.split(part)
|
||||
return f"{directory}/_rels/{base}.rels"
|
||||
|
||||
|
||||
def _resolve(rels_path: str, target: str) -> str:
|
||||
if target.startswith("/"):
|
||||
return target.lstrip("/")
|
||||
part_dir = posixpath.dirname(posixpath.dirname(rels_path))
|
||||
return posixpath.normpath(posixpath.join(part_dir, target))
|
||||
|
||||
|
||||
def _theme_rel(files: Mapping[str, bytes], master: str):
|
||||
rels_path = _rels_path(master)
|
||||
rels = files.get(rels_path)
|
||||
if rels is None:
|
||||
return None
|
||||
for element in _RELATIONSHIP_RE.findall(part_text(rels)):
|
||||
if f'Type="{THEME_REL_TYPE}"' not in element:
|
||||
continue
|
||||
target = re.search(r'\bTarget="([^"]+)"', element)
|
||||
if target is None:
|
||||
continue
|
||||
return rels_path, element, _resolve(rels_path, target.group(1))
|
||||
return None
|
||||
|
||||
|
||||
def _masters(files: Mapping[str, bytes]) -> list[str]:
|
||||
return sorted((n for n in files if _MASTER_RE.match(n)), key=_sort_key)
|
||||
|
||||
|
||||
_PRESENTATION = "ppt/presentation.xml"
|
||||
_NOTES_MASTERS = "ppt/notesMasters/"
|
||||
_IGNORABLE_RE = re.compile(r"<!--.*?-->|<\?.*?\?>", re.DOTALL)
|
||||
_AFTER_SLDIDLST_RE = re.compile(
|
||||
r"<p:sldIdLst\b(?:[^>]*/>|[^>]*>.*?</p:sldIdLst\s*>)\s*(<[^>\s/]+)", re.DOTALL
|
||||
)
|
||||
|
||||
|
||||
def _notes_master_share_is_inert(files: Mapping[str, bytes]) -> bool:
|
||||
data = files.get(_PRESENTATION)
|
||||
if data is None:
|
||||
return False
|
||||
match = _AFTER_SLDIDLST_RE.search(_IGNORABLE_RE.sub("", part_text(data)))
|
||||
return match is not None and match.group(1) == "<p:notesMasterIdLst"
|
||||
|
||||
|
||||
def _shares(files: Mapping[str, bytes]):
|
||||
owner: dict[str, str] = {}
|
||||
for master in _masters(files):
|
||||
found = _theme_rel(files, master)
|
||||
if found is None:
|
||||
continue
|
||||
rels_path, element, theme = found
|
||||
if theme not in files:
|
||||
continue
|
||||
if theme in owner:
|
||||
yield master, rels_path, element, theme, owner[theme]
|
||||
else:
|
||||
owner[theme] = master
|
||||
|
||||
|
||||
def _is_inert(master: str, inert_notes: bool) -> bool:
|
||||
return inert_notes and master.startswith(_NOTES_MASTERS)
|
||||
|
||||
|
||||
def find_shared_master_themes(files: Mapping[str, bytes]) -> list[str]:
|
||||
return [
|
||||
f"{master} shares {theme} with {first}"
|
||||
for master, _, _, theme, first in _shares(files)
|
||||
]
|
||||
|
||||
|
||||
def live_shared_master_themes(files: Mapping[str, bytes]) -> list[str]:
|
||||
inert_notes = _notes_master_share_is_inert(files)
|
||||
return [
|
||||
f"{master} shares {theme} with {first}"
|
||||
for master, _, _, theme, first in _shares(files)
|
||||
if not _is_inert(master, inert_notes)
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,146 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/chartDrawing"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:complexType name="CT_ShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1" maxOccurs="1"
|
||||
/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Shape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvSpPr" type="CT_ShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="txBody" type="a:CT_TextBody" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="textlink" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fLocksText" type="xsd:boolean" use="optional" default="true"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_ConnectorNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvCxnSpPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Connector">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvCxnSpPr" type="CT_ConnectorNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_PictureNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvPicPr" type="a:CT_NonVisualPictureProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Picture">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvPicPr" type="CT_PictureNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="blipFill" type="a:CT_BlipFillProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional" default=""/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrameNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrame">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGraphicFramePr" type="CT_GraphicFrameNonVisual" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="macro" type="xsd:string" use="optional"/>
|
||||
<xsd:attribute name="fPublished" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShapeNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GroupShape">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvGrpSpPr" type="CT_GroupShapeNonVisual" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_ObjectChoices">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="sp" type="CT_Shape"/>
|
||||
<xsd:element name="grpSp" type="CT_GroupShape"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element name="cxnSp" type="CT_Connector"/>
|
||||
<xsd:element name="pic" type="CT_Picture"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:group>
|
||||
<xsd:simpleType name="ST_MarkerCoordinate">
|
||||
<xsd:restriction base="xsd:double">
|
||||
<xsd:minInclusive value="0.0"/>
|
||||
<xsd:maxInclusive value="1.0"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Marker">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="x" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="y" type="ST_MarkerCoordinate" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_RelSizeAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="to" type="CT_Marker"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_AbsSizeAnchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="from" type="CT_Marker"/>
|
||||
<xsd:element name="ext" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:group ref="EG_ObjectChoices"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_Anchor">
|
||||
<xsd:choice>
|
||||
<xsd:element name="relSizeAnchor" type="CT_RelSizeAnchor"/>
|
||||
<xsd:element name="absSizeAnchor" type="CT_AbsSizeAnchor"/>
|
||||
</xsd:choice>
|
||||
</xsd:group>
|
||||
<xsd:complexType name="CT_Drawing">
|
||||
<xsd:sequence>
|
||||
<xsd:group ref="EG_Anchor" minOccurs="0" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
</xsd:schema>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
elementFormDefault="qualified"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:element name="lockedCanvas" type="a:CT_GvmlGroupShape"/>
|
||||
</xsd:schema>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
+185
@@ -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>
|
||||
+287
@@ -0,0 +1,287 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
xmlns:dpct="http://schemas.openxmlformats.org/drawingml/2006/picture"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||
targetNamespace="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||
elementFormDefault="qualified">
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||
schemaLocation="dml-main.xsd"/>
|
||||
<xsd:import schemaLocation="wml.xsd"
|
||||
namespace="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/drawingml/2006/picture"
|
||||
schemaLocation="dml-picture.xsd"/>
|
||||
<xsd:import namespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
schemaLocation="shared-relationshipReference.xsd"/>
|
||||
<xsd:complexType name="CT_EffectExtent">
|
||||
<xsd:attribute name="l" type="a:ST_Coordinate" use="required"/>
|
||||
<xsd:attribute name="t" type="a:ST_Coordinate" use="required"/>
|
||||
<xsd:attribute name="r" type="a:ST_Coordinate" use="required"/>
|
||||
<xsd:attribute name="b" type="a:ST_Coordinate" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_WrapDistance">
|
||||
<xsd:restriction base="xsd:unsignedInt"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_Inline">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_WrapText">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="bothSides"/>
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="largest"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_WrapPath">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="start" type="a:CT_Point2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="lineTo" type="a:CT_Point2D" minOccurs="2" maxOccurs="unbounded"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="edited" type="xsd:boolean" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapNone"/>
|
||||
<xsd:complexType name="CT_WrapSquare">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapTight">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapThrough">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="wrapPolygon" type="CT_WrapPath" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="wrapText" type="ST_WrapText" use="required"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WrapTopBottom">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:group name="EG_WrapType">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="wrapNone" type="CT_WrapNone" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapSquare" type="CT_WrapSquare" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapTight" type="CT_WrapTight" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapThrough" type="CT_WrapThrough" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="wrapTopAndBottom" type="CT_WrapTopBottom" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
</xsd:group>
|
||||
<xsd:simpleType name="ST_PositionOffset">
|
||||
<xsd:restriction base="xsd:int"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_AlignH">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="inside"/>
|
||||
<xsd:enumeration value="outside"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_RelFromH">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="margin"/>
|
||||
<xsd:enumeration value="page"/>
|
||||
<xsd:enumeration value="column"/>
|
||||
<xsd:enumeration value="character"/>
|
||||
<xsd:enumeration value="leftMargin"/>
|
||||
<xsd:enumeration value="rightMargin"/>
|
||||
<xsd:enumeration value="insideMargin"/>
|
||||
<xsd:enumeration value="outsideMargin"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_PosH">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="align" type="ST_AlignH" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="relativeFrom" type="ST_RelFromH" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:simpleType name="ST_AlignV">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="top"/>
|
||||
<xsd:enumeration value="bottom"/>
|
||||
<xsd:enumeration value="center"/>
|
||||
<xsd:enumeration value="inside"/>
|
||||
<xsd:enumeration value="outside"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_RelFromV">
|
||||
<xsd:restriction base="xsd:token">
|
||||
<xsd:enumeration value="margin"/>
|
||||
<xsd:enumeration value="page"/>
|
||||
<xsd:enumeration value="paragraph"/>
|
||||
<xsd:enumeration value="line"/>
|
||||
<xsd:enumeration value="topMargin"/>
|
||||
<xsd:enumeration value="bottomMargin"/>
|
||||
<xsd:enumeration value="insideMargin"/>
|
||||
<xsd:enumeration value="outsideMargin"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:complexType name="CT_PosV">
|
||||
<xsd:sequence>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="align" type="ST_AlignV" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="posOffset" type="ST_PositionOffset" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="relativeFrom" type="ST_RelFromV" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_Anchor">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="simplePos" type="a:CT_Point2D"/>
|
||||
<xsd:element name="positionH" type="CT_PosH"/>
|
||||
<xsd:element name="positionV" type="CT_PosV"/>
|
||||
<xsd:element name="extent" type="a:CT_PositiveSize2D"/>
|
||||
<xsd:element name="effectExtent" type="CT_EffectExtent" minOccurs="0"/>
|
||||
<xsd:group ref="EG_WrapType"/>
|
||||
<xsd:element name="docPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGraphicFramePr" type="a:CT_NonVisualGraphicFrameProperties"
|
||||
minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="distT" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distB" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distL" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="distR" type="ST_WrapDistance" use="optional"/>
|
||||
<xsd:attribute name="simplePos" type="xsd:boolean"/>
|
||||
<xsd:attribute name="relativeHeight" type="xsd:unsignedInt" use="required"/>
|
||||
<xsd:attribute name="behindDoc" type="xsd:boolean" use="required"/>
|
||||
<xsd:attribute name="locked" type="xsd:boolean" use="required"/>
|
||||
<xsd:attribute name="layoutInCell" type="xsd:boolean" use="required"/>
|
||||
<xsd:attribute name="hidden" type="xsd:boolean" use="optional"/>
|
||||
<xsd:attribute name="allowOverlap" type="xsd:boolean" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_TxbxContent">
|
||||
<xsd:group ref="w:EG_BlockLevelElts" minOccurs="1" maxOccurs="unbounded"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_TextboxInfo">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="txbxContent" type="CT_TxbxContent" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="id" type="xsd:unsignedShort" use="optional" default="0"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_LinkedTextboxInformation">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="id" type="xsd:unsignedShort" use="required"/>
|
||||
<xsd:attribute name="seq" type="xsd:unsignedShort" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingShape">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="cNvSpPr" type="a:CT_NonVisualDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="cNvCnPr" type="a:CT_NonVisualConnectorProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="spPr" type="a:CT_ShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="style" type="a:CT_ShapeStyle" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="1">
|
||||
<xsd:element name="txbx" type="CT_TextboxInfo" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="linkedTxbx" type="CT_LinkedTextboxInformation" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="bodyPr" type="a:CT_TextBodyProperties" minOccurs="1" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="normalEastAsianFlow" type="xsd:boolean" use="optional" default="false"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_GraphicFrame">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="cNvFrPr" type="a:CT_NonVisualGraphicFrameProperties" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element ref="a:graphic" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingContentPartNonVisual">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="cNvContentPartPr" type="a:CT_NonVisualContentPartProperties" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingContentPart">
|
||||
<xsd:sequence>
|
||||
<xsd:element name="nvContentPartPr" type="CT_WordprocessingContentPartNonVisual" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="xfrm" type="a:CT_Transform2D" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="bwMode" type="a:ST_BlackWhiteMode" use="optional"/>
|
||||
<xsd:attribute ref="r:id" use="required"/>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingGroup">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="cNvPr" type="a:CT_NonVisualDrawingProps" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="cNvGrpSpPr" type="a:CT_NonVisualGroupDrawingShapeProps" minOccurs="1"
|
||||
maxOccurs="1"/>
|
||||
<xsd:element name="grpSpPr" type="a:CT_GroupShapeProperties" minOccurs="1" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element ref="wsp"/>
|
||||
<xsd:element name="grpSp" type="CT_WordprocessingGroup"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
<xsd:element ref="dpct:pic"/>
|
||||
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:complexType name="CT_WordprocessingCanvas">
|
||||
<xsd:sequence minOccurs="1" maxOccurs="1">
|
||||
<xsd:element name="bg" type="a:CT_BackgroundFormatting" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:element name="whole" type="a:CT_WholeE2oFormatting" minOccurs="0" maxOccurs="1"/>
|
||||
<xsd:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xsd:element ref="wsp"/>
|
||||
<xsd:element ref="dpct:pic"/>
|
||||
<xsd:element name="contentPart" type="CT_WordprocessingContentPart"/>
|
||||
<xsd:element ref="wgp"/>
|
||||
<xsd:element name="graphicFrame" type="CT_GraphicFrame"/>
|
||||
</xsd:choice>
|
||||
<xsd:element name="extLst" type="a:CT_OfficeArtExtensionList" minOccurs="0" maxOccurs="1"/>
|
||||
</xsd:sequence>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="wpc" type="CT_WordprocessingCanvas"/>
|
||||
<xsd:element name="wgp" type="CT_WordprocessingGroup"/>
|
||||
<xsd:element name="wsp" type="CT_WordprocessingShape"/>
|
||||
<xsd:element name="inline" type="CT_Inline"/>
|
||||
<xsd:element name="anchor" type="CT_Anchor"/>
|
||||
</xsd:schema>
|
||||
File diff suppressed because it is too large
Load Diff
+28
@@ -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>
|
||||
+144
@@ -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>
|
||||
+174
@@ -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>
|
||||
+25
@@ -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>
|
||||
+18
@@ -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>
|
||||
+59
@@ -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>
|
||||
+56
@@ -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>
|
||||
+195
@@ -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>
|
||||
@@ -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>
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
elementFormDefault="qualified"
|
||||
targetNamespace="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||
blockDefault="#all">
|
||||
<xsd:simpleType name="ST_RelationshipId">
|
||||
<xsd:restriction base="xsd:string"/>
|
||||
</xsd:simpleType>
|
||||
<xsd:attribute name="id" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="embed" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="link" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="dm" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="lo" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="qs" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="cs" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="blip" type="ST_RelationshipId" default=""/>
|
||||
<xsd:attribute name="pict" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="href" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="topLeft" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="topRight" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="bottomLeft" type="ST_RelationshipId"/>
|
||||
<xsd:attribute name="bottomRight" type="ST_RelationshipId"/>
|
||||
</xsd:schema>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
+12
@@ -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>
|
||||
+108
@@ -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>
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
||||
xmlns="urn:schemas-microsoft-com:office:word"
|
||||
targetNamespace="urn:schemas-microsoft-com:office:word" elementFormDefault="qualified"
|
||||
attributeFormDefault="unqualified">
|
||||
<xsd:element name="bordertop" type="CT_Border"/>
|
||||
<xsd:element name="borderleft" type="CT_Border"/>
|
||||
<xsd:element name="borderright" type="CT_Border"/>
|
||||
<xsd:element name="borderbottom" type="CT_Border"/>
|
||||
<xsd:complexType name="CT_Border">
|
||||
<xsd:attribute name="type" type="ST_BorderType" use="optional"/>
|
||||
<xsd:attribute name="width" type="xsd:positiveInteger" use="optional"/>
|
||||
<xsd:attribute name="shadow" type="ST_BorderShadow" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="wrap" type="CT_Wrap"/>
|
||||
<xsd:complexType name="CT_Wrap">
|
||||
<xsd:attribute name="type" type="ST_WrapType" use="optional"/>
|
||||
<xsd:attribute name="side" type="ST_WrapSide" use="optional"/>
|
||||
<xsd:attribute name="anchorx" type="ST_HorizontalAnchor" use="optional"/>
|
||||
<xsd:attribute name="anchory" type="ST_VerticalAnchor" use="optional"/>
|
||||
</xsd:complexType>
|
||||
<xsd:element name="anchorlock" type="CT_AnchorLock"/>
|
||||
<xsd:complexType name="CT_AnchorLock"/>
|
||||
<xsd:simpleType name="ST_BorderType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="none"/>
|
||||
<xsd:enumeration value="single"/>
|
||||
<xsd:enumeration value="thick"/>
|
||||
<xsd:enumeration value="double"/>
|
||||
<xsd:enumeration value="hairline"/>
|
||||
<xsd:enumeration value="dot"/>
|
||||
<xsd:enumeration value="dash"/>
|
||||
<xsd:enumeration value="dotDash"/>
|
||||
<xsd:enumeration value="dashDotDot"/>
|
||||
<xsd:enumeration value="triple"/>
|
||||
<xsd:enumeration value="thinThickSmall"/>
|
||||
<xsd:enumeration value="thickThinSmall"/>
|
||||
<xsd:enumeration value="thickBetweenThinSmall"/>
|
||||
<xsd:enumeration value="thinThick"/>
|
||||
<xsd:enumeration value="thickThin"/>
|
||||
<xsd:enumeration value="thickBetweenThin"/>
|
||||
<xsd:enumeration value="thinThickLarge"/>
|
||||
<xsd:enumeration value="thickThinLarge"/>
|
||||
<xsd:enumeration value="thickBetweenThinLarge"/>
|
||||
<xsd:enumeration value="wave"/>
|
||||
<xsd:enumeration value="doubleWave"/>
|
||||
<xsd:enumeration value="dashedSmall"/>
|
||||
<xsd:enumeration value="dashDotStroked"/>
|
||||
<xsd:enumeration value="threeDEmboss"/>
|
||||
<xsd:enumeration value="threeDEngrave"/>
|
||||
<xsd:enumeration value="HTMLOutset"/>
|
||||
<xsd:enumeration value="HTMLInset"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_BorderShadow">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="t"/>
|
||||
<xsd:enumeration value="true"/>
|
||||
<xsd:enumeration value="f"/>
|
||||
<xsd:enumeration value="false"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_WrapType">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="topAndBottom"/>
|
||||
<xsd:enumeration value="square"/>
|
||||
<xsd:enumeration value="none"/>
|
||||
<xsd:enumeration value="tight"/>
|
||||
<xsd:enumeration value="through"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_WrapSide">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="both"/>
|
||||
<xsd:enumeration value="left"/>
|
||||
<xsd:enumeration value="right"/>
|
||||
<xsd:enumeration value="largest"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_HorizontalAnchor">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="margin"/>
|
||||
<xsd:enumeration value="page"/>
|
||||
<xsd:enumeration value="text"/>
|
||||
<xsd:enumeration value="char"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
<xsd:simpleType name="ST_VerticalAnchor">
|
||||
<xsd:restriction base="xsd:string">
|
||||
<xsd:enumeration value="margin"/>
|
||||
<xsd:enumeration value="page"/>
|
||||
<xsd:enumeration value="text"/>
|
||||
<xsd:enumeration value="line"/>
|
||||
</xsd:restriction>
|
||||
</xsd:simpleType>
|
||||
</xsd:schema>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Helper for running LibreOffice (soffice) in environments where AF_UNIX
|
||||
sockets may be blocked (e.g., sandboxed VMs). Detects the restriction
|
||||
at runtime and applies an LD_PRELOAD shim if needed.
|
||||
|
||||
Usage:
|
||||
from office.soffice import run_soffice
|
||||
|
||||
result = run_soffice(["--headless", "--convert-to", "pdf", "input.docx"])
|
||||
|
||||
Call soffice through run_soffice, not through subprocess with get_soffice_env():
|
||||
the env dict carries the shim but names no user profile, and a non-root sandbox
|
||||
cannot bootstrap the default one -- soffice aborts with "User installation could
|
||||
not be completed" and converts nothing. get_soffice_env() stays public for the
|
||||
callers that build their own argv (they must pass -env:UserInstallation too).
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections.abc import Iterable
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_soffice_env() -> dict:
|
||||
env = os.environ.copy()
|
||||
env["SAL_USE_VCLPLUGIN"] = "svp"
|
||||
|
||||
if _needs_shim():
|
||||
shim = _ensure_shim()
|
||||
env["LD_PRELOAD"] = str(shim)
|
||||
|
||||
return env
|
||||
|
||||
|
||||
def run_soffice(args: Iterable[str], **kwargs) -> subprocess.CompletedProcess:
|
||||
args = list(args)
|
||||
with contextlib.ExitStack() as stack:
|
||||
if not any(str(a).startswith("-env:UserInstallation") for a in args):
|
||||
profile = stack.enter_context(
|
||||
tempfile.TemporaryDirectory(prefix="lo_profile_", ignore_cleanup_errors=True)
|
||||
)
|
||||
args = [f"-env:UserInstallation={Path(profile).as_uri()}"] + args
|
||||
return subprocess.run(["soffice"] + args, env=get_soffice_env(), **kwargs)
|
||||
|
||||
|
||||
|
||||
_SHIM_SO = Path(tempfile.gettempdir()) / "lo_socket_shim.so"
|
||||
|
||||
|
||||
def _needs_shim() -> bool:
|
||||
try:
|
||||
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
s.close()
|
||||
return False
|
||||
except OSError:
|
||||
return True
|
||||
|
||||
|
||||
def _ensure_shim() -> Path:
|
||||
if _SHIM_SO.exists():
|
||||
return _SHIM_SO
|
||||
|
||||
src = Path(tempfile.gettempdir()) / "lo_socket_shim.c"
|
||||
src.write_text(_SHIM_SOURCE)
|
||||
subprocess.run(
|
||||
["gcc", "-shared", "-fPIC", "-o", str(_SHIM_SO), str(src), "-ldl"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
src.unlink()
|
||||
return _SHIM_SO
|
||||
|
||||
|
||||
|
||||
_SHIM_SOURCE = r"""
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int (*real_socket)(int, int, int);
|
||||
static int (*real_socketpair)(int, int, int, int[2]);
|
||||
static int (*real_listen)(int, int);
|
||||
static int (*real_accept)(int, struct sockaddr *, socklen_t *);
|
||||
static int (*real_close)(int);
|
||||
static int (*real_read)(int, void *, size_t);
|
||||
|
||||
/* Per-FD bookkeeping (FDs >= 1024 are passed through unshimmed). */
|
||||
static int is_shimmed[1024];
|
||||
static int peer_of[1024];
|
||||
static int wake_r[1024]; /* accept() blocks reading this */
|
||||
static int wake_w[1024]; /* close() writes to this */
|
||||
static int listener_fd = -1; /* FD that received listen() */
|
||||
|
||||
__attribute__((constructor))
|
||||
static void init(void) {
|
||||
real_socket = dlsym(RTLD_NEXT, "socket");
|
||||
real_socketpair = dlsym(RTLD_NEXT, "socketpair");
|
||||
real_listen = dlsym(RTLD_NEXT, "listen");
|
||||
real_accept = dlsym(RTLD_NEXT, "accept");
|
||||
real_close = dlsym(RTLD_NEXT, "close");
|
||||
real_read = dlsym(RTLD_NEXT, "read");
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
peer_of[i] = -1;
|
||||
wake_r[i] = -1;
|
||||
wake_w[i] = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- socket ---------------------------------------------------------- */
|
||||
int socket(int domain, int type, int protocol) {
|
||||
if (domain == AF_UNIX) {
|
||||
int fd = real_socket(domain, type, protocol);
|
||||
if (fd >= 0) return fd;
|
||||
/* socket(AF_UNIX) blocked – fall back to socketpair(). */
|
||||
int sv[2];
|
||||
if (real_socketpair(domain, type, protocol, sv) == 0) {
|
||||
if (sv[0] >= 0 && sv[0] < 1024) {
|
||||
is_shimmed[sv[0]] = 1;
|
||||
peer_of[sv[0]] = sv[1];
|
||||
int wp[2];
|
||||
if (pipe(wp) == 0) {
|
||||
wake_r[sv[0]] = wp[0];
|
||||
wake_w[sv[0]] = wp[1];
|
||||
}
|
||||
}
|
||||
return sv[0];
|
||||
}
|
||||
errno = EPERM;
|
||||
return -1;
|
||||
}
|
||||
return real_socket(domain, type, protocol);
|
||||
}
|
||||
|
||||
/* ---- listen ---------------------------------------------------------- */
|
||||
int listen(int sockfd, int backlog) {
|
||||
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
|
||||
listener_fd = sockfd;
|
||||
return 0;
|
||||
}
|
||||
return real_listen(sockfd, backlog);
|
||||
}
|
||||
|
||||
/* ---- accept ---------------------------------------------------------- */
|
||||
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) {
|
||||
if (sockfd >= 0 && sockfd < 1024 && is_shimmed[sockfd]) {
|
||||
/* Block until close() writes to the wake pipe. */
|
||||
if (wake_r[sockfd] >= 0) {
|
||||
char buf;
|
||||
real_read(wake_r[sockfd], &buf, 1);
|
||||
}
|
||||
errno = ECONNABORTED;
|
||||
return -1;
|
||||
}
|
||||
return real_accept(sockfd, addr, addrlen);
|
||||
}
|
||||
|
||||
/* ---- close ----------------------------------------------------------- */
|
||||
int close(int fd) {
|
||||
if (fd >= 0 && fd < 1024 && is_shimmed[fd]) {
|
||||
int was_listener = (fd == listener_fd);
|
||||
is_shimmed[fd] = 0;
|
||||
|
||||
if (wake_w[fd] >= 0) { /* unblock accept() */
|
||||
char c = 0;
|
||||
write(wake_w[fd], &c, 1);
|
||||
real_close(wake_w[fd]);
|
||||
wake_w[fd] = -1;
|
||||
}
|
||||
if (wake_r[fd] >= 0) { real_close(wake_r[fd]); wake_r[fd] = -1; }
|
||||
if (peer_of[fd] >= 0) { real_close(peer_of[fd]); peer_of[fd] = -1; }
|
||||
|
||||
if (was_listener)
|
||||
_exit(0); /* conversion done – exit */
|
||||
}
|
||||
return real_close(fd);
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
result = run_soffice(sys.argv[1:])
|
||||
sys.exit(result.returncode)
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
Command line tool to validate Office document XML files against XSD schemas and tracked changes.
|
||||
|
||||
Usage:
|
||||
python validate.py <path> [--original <original_file>] [--auto-repair] [--author NAME]
|
||||
|
||||
The first argument can be either:
|
||||
- An unpacked directory containing the Office document XML files
|
||||
- A packed Office file (.docx/.pptx/.xlsx or .dotx/.potx/.xltx template) which will be unpacked to a temp directory
|
||||
|
||||
Auto-repair fixes:
|
||||
- paraId/durableId values that exceed OOXML limits
|
||||
- Missing xml:space="preserve" on w:t elements with whitespace
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.ElementTree as ET
|
||||
from defusedxml.common import DefusedXmlException
|
||||
|
||||
from helpers import OOXML_FAMILY, rezip, safe_extract
|
||||
from validators import DOCXSchemaValidator, PPTXSchemaValidator, RedliningValidator
|
||||
|
||||
WORD_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
|
||||
|
||||
def _fail(message: str):
|
||||
print(f"Error: {message}", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def _has_tracked_changes(unpacked_dir: Path) -> bool:
|
||||
document = unpacked_dir / "word" / "document.xml"
|
||||
if not document.is_file():
|
||||
return False
|
||||
try:
|
||||
root = ET.parse(document).getroot()
|
||||
except (ET.ParseError, DefusedXmlException):
|
||||
return False
|
||||
tracked = {f"{{{WORD_NS}}}ins", f"{{{WORD_NS}}}del"}
|
||||
return any(elem.tag in tracked for elem in root.iter())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Validate Office document XML files")
|
||||
parser.add_argument(
|
||||
"path",
|
||||
help="Path to unpacked directory or packed Office file (.docx/.pptx/.xlsx or .dotx/.potx/.xltx)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--original",
|
||||
required=False,
|
||||
default=None,
|
||||
help="Path to original file (.docx/.pptx/.xlsx or .dotx/.potx/.xltx). If omitted, all XSD errors are reported and redlining validation is skipped.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--verbose",
|
||||
action="store_true",
|
||||
help="Enable verbose output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--auto-repair",
|
||||
action="store_true",
|
||||
help="Automatically repair common issues (hex IDs, whitespace preservation). "
|
||||
"Modifies the input in place: repairs to a packed file are written back to it.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--author",
|
||||
default=None,
|
||||
help="The name you are redlining under. Passing it turns on the "
|
||||
"tracked-change check: any text differing from --original without a "
|
||||
"<w:ins>/<w:del> recording it is reported. Untracked edits carry no "
|
||||
"author, so the check covers them whoever made them — the name marks "
|
||||
"the run as redlining work and is not used to filter. Requires "
|
||||
"--original; docx only.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.author is not None and not args.original:
|
||||
_fail("--author requires --original")
|
||||
|
||||
path = Path(args.path)
|
||||
if not path.exists():
|
||||
_fail(f"{path} does not exist")
|
||||
|
||||
original_file = None
|
||||
if args.original:
|
||||
original_file = Path(args.original)
|
||||
if not original_file.is_file():
|
||||
_fail(f"{original_file} is not a file")
|
||||
if original_file.suffix.lower() not in OOXML_FAMILY:
|
||||
_fail(f"{original_file} must be one of: {', '.join(sorted(OOXML_FAMILY))}")
|
||||
|
||||
family = OOXML_FAMILY.get((original_file or path).suffix.lower())
|
||||
if family is None:
|
||||
_fail(
|
||||
f"Cannot determine file type from {path}. Use --original or provide one of: {', '.join(sorted(OOXML_FAMILY))}."
|
||||
)
|
||||
|
||||
if args.author is not None and family != "docx":
|
||||
_fail(f"--author only applies to docx files, not {family}")
|
||||
|
||||
packed_file = None
|
||||
temp_dir_ctx = None
|
||||
if path.is_file() and path.suffix.lower() in OOXML_FAMILY:
|
||||
packed_file = path
|
||||
temp_dir_ctx = tempfile.TemporaryDirectory()
|
||||
unpacked_dir = Path(temp_dir_ctx.name)
|
||||
try:
|
||||
with zipfile.ZipFile(path, "r") as zf:
|
||||
safe_extract(zf, unpacked_dir)
|
||||
except (zipfile.BadZipFile, ValueError, OSError) as e:
|
||||
_fail(f"cannot unpack {path}: {e}")
|
||||
else:
|
||||
if not path.is_dir():
|
||||
_fail(f"{path} is not a directory or Office file")
|
||||
unpacked_dir = path
|
||||
|
||||
match family:
|
||||
case "docx":
|
||||
validators = [
|
||||
DOCXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose),
|
||||
]
|
||||
if args.author is not None:
|
||||
validators.append(
|
||||
RedliningValidator(unpacked_dir, original_file, verbose=args.verbose)
|
||||
)
|
||||
elif original_file and _has_tracked_changes(unpacked_dir):
|
||||
print(
|
||||
"Note: this document has tracked changes; they were not "
|
||||
"checked against the original (pass --author to check)."
|
||||
)
|
||||
case "pptx":
|
||||
validators = [
|
||||
PPTXSchemaValidator(unpacked_dir, original_file, verbose=args.verbose),
|
||||
]
|
||||
case "xlsx":
|
||||
exts = ", ".join(k for k, v in sorted(OOXML_FAMILY.items()) if v == "xlsx")
|
||||
print(
|
||||
f"No XSD schema validation is performed for xlsx-family files ({exts}). "
|
||||
"For formula-error checking, use scripts/recalc.py instead."
|
||||
)
|
||||
sys.exit(0)
|
||||
case _:
|
||||
print(f"Error: Validation not supported for file type {family}")
|
||||
sys.exit(1)
|
||||
|
||||
if args.auto_repair:
|
||||
total_repairs = sum(v.repair() for v in validators)
|
||||
if total_repairs:
|
||||
print(f"Auto-repaired {total_repairs} issue(s)")
|
||||
if packed_file is not None:
|
||||
rezip(unpacked_dir, packed_file)
|
||||
print(f"Wrote repaired file to {packed_file}")
|
||||
|
||||
success = all([v.validate() for v in validators])
|
||||
|
||||
if temp_dir_ctx is not None:
|
||||
temp_dir_ctx.cleanup()
|
||||
|
||||
if success:
|
||||
print("All validations PASSED!")
|
||||
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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",
|
||||
]
|
||||
@@ -0,0 +1,875 @@
|
||||
"""
|
||||
Base validator with common validation logic for document files.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
from functools import lru_cache
|
||||
|
||||
import lxml.etree
|
||||
|
||||
from helpers import safe_extract
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def _load_schema(schema_path: str):
|
||||
with open(schema_path, "rb") as xsd_file:
|
||||
xsd_doc = lxml.etree.parse(
|
||||
xsd_file, parser=lxml.etree.XMLParser(), base_url=schema_path
|
||||
)
|
||||
return lxml.etree.XMLSchema(xsd_doc)
|
||||
|
||||
class BaseSchemaValidator:
|
||||
|
||||
IGNORED_VALIDATION_ERRORS = [
|
||||
"hyphenationZone",
|
||||
"purl.org/dc/terms",
|
||||
]
|
||||
|
||||
UNIQUE_ID_REQUIREMENTS = {
|
||||
"comment": ("id", "file"),
|
||||
"commentrangestart": ("id", "file"),
|
||||
"commentrangeend": ("id", "file"),
|
||||
"bookmarkstart": ("id", "file"),
|
||||
"bookmarkend": ("id", "file"),
|
||||
"sldid": ("id", "file"),
|
||||
"sldmasterid": ("id", "global"),
|
||||
"sldlayoutid": ("id", "global"),
|
||||
"cm": ("authorid", "file"),
|
||||
"sheet": ("sheetid", "file"),
|
||||
"definedname": ("id", "file"),
|
||||
"cxnsp": ("id", "file"),
|
||||
"sp": ("id", "file"),
|
||||
"pic": ("id", "file"),
|
||||
"grpsp": ("id", "file"),
|
||||
}
|
||||
|
||||
EXCLUDED_ID_CONTAINERS = {
|
||||
"sectionlst",
|
||||
}
|
||||
|
||||
ELEMENT_RELATIONSHIP_TYPES = {}
|
||||
|
||||
SCHEMA_MAPPINGS = {
|
||||
"word": "ISO-IEC29500-4_2016/wml.xsd",
|
||||
"ppt": "ISO-IEC29500-4_2016/pml.xsd",
|
||||
"xl": "ISO-IEC29500-4_2016/sml.xsd",
|
||||
"[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",
|
||||
"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": "ISO-IEC29500-4_2016/dml-chart.xsd",
|
||||
"theme": "ISO-IEC29500-4_2016/dml-main.xsd",
|
||||
"drawing": "ISO-IEC29500-4_2016/dml-main.xsd",
|
||||
}
|
||||
|
||||
MC_NAMESPACE = "http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace"
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
MAIN_CONTENT_FOLDERS = {"word", "ppt", "xl"}
|
||||
|
||||
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=None, verbose=False):
|
||||
self.unpacked_dir = Path(unpacked_dir).resolve()
|
||||
self.original_file = Path(original_file) if original_file else None
|
||||
self.verbose = verbose
|
||||
|
||||
self.schemas_dir = Path(__file__).parent.parent / "schemas"
|
||||
|
||||
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):
|
||||
raise NotImplementedError("Subclasses must implement the validate method")
|
||||
|
||||
def repair(self) -> int:
|
||||
return self.repair_whitespace_preservation()
|
||||
|
||||
def repair_whitespace_preservation(self) -> int:
|
||||
repairs = 0
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
try:
|
||||
content = xml_file.read_text(encoding="utf-8")
|
||||
dom = defusedxml.minidom.parseString(content)
|
||||
pending = []
|
||||
|
||||
for elem in dom.getElementsByTagName("*"):
|
||||
local_name = elem.tagName.rsplit(":", 1)[-1]
|
||||
if local_name in ("t", "delText", "instrText", "delInstrText"):
|
||||
text = "".join(
|
||||
child.data
|
||||
for child in elem.childNodes
|
||||
if child.nodeType in (child.TEXT_NODE, child.CDATA_SECTION_NODE)
|
||||
)
|
||||
ws = (" ", "\t", "\n", "\r")
|
||||
if text and (text.startswith(ws) or text.endswith(ws)):
|
||||
if elem.getAttribute("xml:space") != "preserve":
|
||||
elem.setAttribute("xml:space", "preserve")
|
||||
text_preview = repr(text[:30]) + "..." if len(text) > 30 else repr(text)
|
||||
pending.append(f" Repaired: {xml_file.name}: Added xml:space='preserve' to {elem.tagName}: {text_preview}")
|
||||
|
||||
if pending:
|
||||
xml_file.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
for message in pending:
|
||||
print(message)
|
||||
repairs += len(pending)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return repairs
|
||||
|
||||
def validate_xml(self):
|
||||
errors = []
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
try:
|
||||
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):
|
||||
errors = []
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
try:
|
||||
root = lxml.etree.parse(str(xml_file)).getroot()
|
||||
declared = set(root.nsmap.keys()) - {None}
|
||||
|
||||
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):
|
||||
errors = []
|
||||
global_ids = {}
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
try:
|
||||
root = lxml.etree.parse(str(xml_file)).getroot()
|
||||
file_ids = {}
|
||||
|
||||
mc_elements = root.xpath(
|
||||
".//mc:AlternateContent", namespaces={"mc": self.MC_NAMESPACE}
|
||||
)
|
||||
for elem in mc_elements:
|
||||
elem.getparent().remove(elem)
|
||||
|
||||
for elem in root.iter():
|
||||
if not hasattr(elem, "tag") or callable(elem.tag):
|
||||
continue
|
||||
tag = (
|
||||
elem.tag.split("}")[-1].lower()
|
||||
if "}" in elem.tag
|
||||
else elem.tag.lower()
|
||||
)
|
||||
|
||||
if tag in self.UNIQUE_ID_REQUIREMENTS:
|
||||
in_excluded_container = any(
|
||||
ancestor.tag.split("}")[-1].lower() in self.EXCLUDED_ID_CONTAINERS
|
||||
for ancestor in elem.iterancestors()
|
||||
)
|
||||
if in_excluded_container:
|
||||
continue
|
||||
|
||||
attr_name, scope = self.UNIQUE_ID_REQUIREMENTS[tag]
|
||||
|
||||
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":
|
||||
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":
|
||||
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):
|
||||
errors = []
|
||||
|
||||
rels_files = list(self.unpacked_dir.rglob("*.rels"))
|
||||
|
||||
if not rels_files:
|
||||
if self.verbose:
|
||||
print("PASSED - No .rels files found")
|
||||
return True
|
||||
|
||||
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")
|
||||
):
|
||||
all_files.append(file_path.resolve())
|
||||
|
||||
all_referenced_files = set()
|
||||
|
||||
if self.verbose:
|
||||
print(
|
||||
f"Found {len(rels_files)} .rels files and {len(all_files)} target files"
|
||||
)
|
||||
|
||||
for rels_file in rels_files:
|
||||
try:
|
||||
rels_root = lxml.etree.parse(str(rels_file)).getroot()
|
||||
|
||||
rels_dir = rels_file.parent
|
||||
|
||||
referenced_files = set()
|
||||
broken_refs = []
|
||||
|
||||
for rel in rels_root.findall(
|
||||
".//ns:Relationship",
|
||||
namespaces={"ns": self.PACKAGE_RELATIONSHIPS_NAMESPACE},
|
||||
):
|
||||
target = rel.get("Target")
|
||||
if rel.get("TargetMode") == "External":
|
||||
continue
|
||||
if target and not target.startswith(
|
||||
("http", "mailto:")
|
||||
):
|
||||
if target.startswith("/"):
|
||||
target_path = self.unpacked_dir / target.lstrip("/")
|
||||
elif rels_file.name == ".rels":
|
||||
target_path = self.unpacked_dir / target
|
||||
else:
|
||||
base_dir = rels_dir.parent
|
||||
target_path = base_dir / target
|
||||
|
||||
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))
|
||||
|
||||
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}")
|
||||
|
||||
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):
|
||||
import lxml.etree
|
||||
|
||||
errors = []
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
if xml_file.suffix == ".rels":
|
||||
continue
|
||||
|
||||
rels_dir = xml_file.parent / "_rels"
|
||||
rels_file = rels_dir / f"{xml_file.name}.rels"
|
||||
|
||||
if not rels_file.exists():
|
||||
continue
|
||||
|
||||
try:
|
||||
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:
|
||||
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)"
|
||||
)
|
||||
type_name = (
|
||||
rel_type.split("/")[-1] if "/" in rel_type else rel_type
|
||||
)
|
||||
rid_to_type[rid] = type_name
|
||||
|
||||
xml_root = lxml.etree.parse(str(xml_file)).getroot()
|
||||
|
||||
r_ns = self.OFFICE_RELATIONSHIPS_NAMESPACE
|
||||
rid_attrs_to_check = ["id", "embed", "link"]
|
||||
for elem in xml_root.iter():
|
||||
if not hasattr(elem, "tag") or callable(elem.tag):
|
||||
continue
|
||||
for attr_name in rid_attrs_to_check:
|
||||
rid_attr = elem.get(f"{{{r_ns}}}{attr_name}")
|
||||
if not rid_attr:
|
||||
continue
|
||||
xml_rel_path = xml_file.relative_to(self.unpacked_dir)
|
||||
elem_name = (
|
||||
elem.tag.split("}")[-1] if "}" in elem.tag else elem.tag
|
||||
)
|
||||
|
||||
if rid_attr not in rid_to_type:
|
||||
errors.append(
|
||||
f" {xml_rel_path}: Line {elem.sourceline}: "
|
||||
f"<{elem_name}> r:{attr_name} references non-existent relationship '{rid_attr}' "
|
||||
f"(valid IDs: {', '.join(sorted(rid_to_type.keys())[:5])}{'...' if len(rid_to_type) > 5 else ''})"
|
||||
)
|
||||
elif attr_name == "id" and self.ELEMENT_RELATIONSHIP_TYPES:
|
||||
expected_type = self._get_expected_relationship_type(
|
||||
elem_name
|
||||
)
|
||||
if expected_type:
|
||||
actual_type = rid_to_type[rid_attr]
|
||||
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):
|
||||
elem_lower = element_name.lower()
|
||||
|
||||
if elem_lower in self.ELEMENT_RELATIONSHIP_TYPES:
|
||||
return self.ELEMENT_RELATIONSHIP_TYPES[elem_lower]
|
||||
|
||||
if elem_lower.endswith("id") and len(elem_lower) > 2:
|
||||
prefix = elem_lower[:-2]
|
||||
if prefix.endswith("master"):
|
||||
return prefix.lower()
|
||||
elif prefix.endswith("layout"):
|
||||
return prefix.lower()
|
||||
else:
|
||||
if prefix == "sld":
|
||||
return "slide"
|
||||
return prefix.lower()
|
||||
|
||||
if elem_lower.endswith("reference") and len(elem_lower) > 9:
|
||||
prefix = elem_lower[:-9]
|
||||
return prefix.lower()
|
||||
|
||||
return None
|
||||
|
||||
def validate_content_types(self):
|
||||
errors = []
|
||||
|
||||
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:
|
||||
root = lxml.etree.parse(str(content_types_file)).getroot()
|
||||
declared_parts = set()
|
||||
declared_extensions = set()
|
||||
|
||||
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("/"))
|
||||
|
||||
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())
|
||||
|
||||
declarable_roots = {
|
||||
"sld",
|
||||
"sldLayout",
|
||||
"sldMaster",
|
||||
"presentation",
|
||||
"document",
|
||||
"workbook",
|
||||
"worksheet",
|
||||
"theme",
|
||||
}
|
||||
|
||||
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",
|
||||
}
|
||||
|
||||
all_files = list(self.unpacked_dir.rglob("*"))
|
||||
all_files = [f for f in all_files if f.is_file()]
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
path_str = str(xml_file.relative_to(self.unpacked_dir)).replace(
|
||||
"\\", "/"
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
for file_path in all_files:
|
||||
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:
|
||||
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):
|
||||
xml_file = Path(xml_file).resolve()
|
||||
unpacked_dir = self.unpacked_dir.resolve()
|
||||
|
||||
is_valid, current_errors = self._validate_single_file_xsd(
|
||||
xml_file, unpacked_dir
|
||||
)
|
||||
|
||||
if is_valid is None:
|
||||
return None, set()
|
||||
elif is_valid:
|
||||
return True, set()
|
||||
|
||||
original_errors = self._get_original_file_errors(xml_file)
|
||||
|
||||
assert current_errors is not None
|
||||
new_errors = current_errors - original_errors
|
||||
|
||||
new_errors = {
|
||||
e for e in new_errors
|
||||
if not any(pattern in e for pattern in self.IGNORED_VALIDATION_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:
|
||||
if verbose:
|
||||
print(
|
||||
f"PASSED - No new errors (original had {len(current_errors)} errors)"
|
||||
)
|
||||
return True, set()
|
||||
|
||||
def validate_against_xsd(self):
|
||||
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:
|
||||
original_error_count += 1
|
||||
valid_count += 1
|
||||
continue
|
||||
|
||||
new_errors.append(f" {relative_path}: {len(new_file_errors)} new error(s)")
|
||||
for error in list(new_file_errors)[:3]:
|
||||
new_errors.append(
|
||||
f" - {error[:250]}..." if len(error) > 250 else f" - {error}"
|
||||
)
|
||||
|
||||
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):
|
||||
if xml_file.name in self.SCHEMA_MAPPINGS:
|
||||
return self.schemas_dir / self.SCHEMA_MAPPINGS[xml_file.name]
|
||||
|
||||
if xml_file.suffix == ".rels":
|
||||
return self.schemas_dir / self.SCHEMA_MAPPINGS[".rels"]
|
||||
|
||||
if "charts/" in str(xml_file) and xml_file.name.startswith("chart"):
|
||||
return self.schemas_dir / self.SCHEMA_MAPPINGS["chart"]
|
||||
|
||||
if "theme/" in str(xml_file) and xml_file.name.startswith("theme"):
|
||||
return self.schemas_dir / self.SCHEMA_MAPPINGS["theme"]
|
||||
|
||||
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):
|
||||
xml_string = lxml.etree.tostring(xml_doc, encoding="unicode")
|
||||
xml_copy = lxml.etree.fromstring(xml_string)
|
||||
|
||||
for elem in xml_copy.iter():
|
||||
attrs_to_remove = []
|
||||
|
||||
for attr in elem.attrib:
|
||||
if "{" in attr:
|
||||
ns = attr.split("}")[0][1:]
|
||||
if ns not in self.OOXML_NAMESPACES:
|
||||
attrs_to_remove.append(attr)
|
||||
|
||||
for attr in attrs_to_remove:
|
||||
del elem.attrib[attr]
|
||||
|
||||
self._remove_ignorable_elements(xml_copy)
|
||||
|
||||
return lxml.etree.ElementTree(xml_copy)
|
||||
|
||||
def _remove_ignorable_elements(self, root):
|
||||
elements_to_remove = []
|
||||
|
||||
for elem in list(root):
|
||||
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
|
||||
|
||||
self._remove_ignorable_elements(elem)
|
||||
|
||||
for elem in elements_to_remove:
|
||||
root.remove(elem)
|
||||
|
||||
def _preprocess_for_mc_ignorable(self, xml_doc):
|
||||
root = xml_doc.getroot()
|
||||
|
||||
if f"{{{self.MC_NAMESPACE}}}Ignorable" in root.attrib:
|
||||
del root.attrib[f"{{{self.MC_NAMESPACE}}}Ignorable"]
|
||||
|
||||
return xml_doc
|
||||
|
||||
def _preprocess_for_schema(self, xml_doc, relative_path):
|
||||
return xml_doc
|
||||
|
||||
def _validate_single_file_xsd(self, xml_file, base_path, schema_path=None):
|
||||
schema_path = schema_path or self._get_schema_path(xml_file)
|
||||
if not schema_path:
|
||||
return None, None
|
||||
|
||||
try:
|
||||
schema = _load_schema(str(schema_path))
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
xml_doc = self._preprocess_for_schema(xml_doc, relative_path)
|
||||
|
||||
if schema.validate(xml_doc):
|
||||
return True, set()
|
||||
else:
|
||||
errors = set()
|
||||
for error in schema.error_log:
|
||||
errors.add(error.message)
|
||||
return False, errors
|
||||
|
||||
except Exception as e:
|
||||
return False, {str(e)}
|
||||
|
||||
def _get_original_file_errors(self, xml_file, schema_path=None):
|
||||
if self.original_file is None:
|
||||
return set()
|
||||
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
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)
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(self.original_file, "r") as zip_ref:
|
||||
safe_extract(zip_ref, temp_path)
|
||||
except (zipfile.BadZipFile, ValueError, OSError):
|
||||
return set()
|
||||
|
||||
original_xml_file = temp_path / relative_path
|
||||
|
||||
if not original_xml_file.exists():
|
||||
return set()
|
||||
|
||||
is_valid, errors = self._validate_single_file_xsd(
|
||||
original_xml_file, temp_path, schema_path=schema_path
|
||||
)
|
||||
return errors if errors else set()
|
||||
|
||||
def _remove_template_tags_from_text_nodes(self, xml_doc):
|
||||
warnings = []
|
||||
template_pattern = re.compile(r"\{\{[^}]*\}\}")
|
||||
|
||||
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
|
||||
|
||||
for elem in xml_copy.iter():
|
||||
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.")
|
||||
@@ -0,0 +1,466 @@
|
||||
"""
|
||||
Validator for Word document XML files against XSD schemas.
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
import lxml.etree
|
||||
|
||||
from helpers import safe_extract
|
||||
|
||||
from .base import BaseSchemaValidator
|
||||
|
||||
|
||||
class DOCXSchemaValidator(BaseSchemaValidator):
|
||||
|
||||
WORD_2006_NAMESPACE = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
W14_NAMESPACE = "http://schemas.microsoft.com/office/word/2010/wordml"
|
||||
W16CID_NAMESPACE = "http://schemas.microsoft.com/office/word/2016/wordml/cid"
|
||||
|
||||
ELEMENT_RELATIONSHIP_TYPES = {}
|
||||
|
||||
def validate(self):
|
||||
if not self.validate_xml():
|
||||
return False
|
||||
|
||||
all_valid = True
|
||||
if not self.validate_namespaces():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_unique_ids():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_file_references():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_content_types():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_against_xsd():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_whitespace_preservation():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_deletions():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_insertions():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_all_relationship_ids():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_id_constraints():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_comment_markers():
|
||||
all_valid = False
|
||||
|
||||
self.compare_paragraph_counts()
|
||||
|
||||
return all_valid
|
||||
|
||||
def validate_whitespace_preservation(self):
|
||||
errors = []
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
if xml_file.name != "document.xml":
|
||||
continue
|
||||
|
||||
try:
|
||||
root = lxml.etree.parse(str(xml_file)).getroot()
|
||||
|
||||
for elem in root.iter(f"{{{self.WORD_2006_NAMESPACE}}}t"):
|
||||
if elem.text:
|
||||
text = elem.text
|
||||
if re.search(r"^[ \t\n\r]", text) or re.search(
|
||||
r"[ \t\n\r]$", text
|
||||
):
|
||||
xml_space_attr = f"{{{self.XML_NAMESPACE}}}space"
|
||||
if (
|
||||
xml_space_attr not in elem.attrib
|
||||
or elem.attrib[xml_space_attr] != "preserve"
|
||||
):
|
||||
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):
|
||||
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}
|
||||
|
||||
for t_elem in root.xpath(".//w:del//w:t", namespaces=namespaces):
|
||||
if t_elem.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}"
|
||||
)
|
||||
|
||||
for instr_elem in root.xpath(
|
||||
".//w:del//w:instrText", namespaces=namespaces
|
||||
):
|
||||
text_preview = (
|
||||
repr(instr_elem.text or "")[:50] + "..."
|
||||
if len(repr(instr_elem.text or "")) > 50
|
||||
else repr(instr_elem.text or "")
|
||||
)
|
||||
errors.append(
|
||||
f" {xml_file.relative_to(self.unpacked_dir)}: "
|
||||
f"Line {instr_elem.sourceline}: <w:instrText> found within <w:del> (use <w:delInstrText>): {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 = 0
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
if xml_file.name != "document.xml":
|
||||
continue
|
||||
|
||||
try:
|
||||
root = lxml.etree.parse(str(xml_file)).getroot()
|
||||
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):
|
||||
original = self.original_file
|
||||
if original is None:
|
||||
return 0
|
||||
|
||||
count = 0
|
||||
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
with zipfile.ZipFile(original, "r") as zip_ref:
|
||||
safe_extract(zip_ref, Path(temp_dir))
|
||||
|
||||
doc_xml_path = temp_dir + "/word/document.xml"
|
||||
root = lxml.etree.parse(doc_xml_path).getroot()
|
||||
|
||||
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):
|
||||
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}
|
||||
|
||||
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):
|
||||
new_count = self.count_paragraphs_in_unpacked()
|
||||
if self.original_file is None:
|
||||
print(f"\nParagraphs: {new_count}")
|
||||
return
|
||||
|
||||
original_count = self.count_paragraphs_in_original()
|
||||
diff = new_count - original_count
|
||||
diff_str = f"+{diff}" if diff > 0 else str(diff)
|
||||
print(f"\nParagraphs: {original_count} → {new_count} ({diff_str})")
|
||||
|
||||
def _parse_id_value(self, val: str, base: int = 16) -> int:
|
||||
return int(val, base)
|
||||
|
||||
def validate_id_constraints(self):
|
||||
errors = []
|
||||
para_id_attr = f"{{{self.W14_NAMESPACE}}}paraId"
|
||||
durable_id_attr = f"{{{self.W16CID_NAMESPACE}}}durableId"
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
try:
|
||||
for elem in lxml.etree.parse(str(xml_file)).iter():
|
||||
if val := elem.get(para_id_attr):
|
||||
try:
|
||||
if self._parse_id_value(val, base=16) >= 0x80000000:
|
||||
errors.append(
|
||||
f" {xml_file.name}:{elem.sourceline}: paraId={val} >= 0x80000000"
|
||||
)
|
||||
except ValueError:
|
||||
errors.append(
|
||||
f" {xml_file.name}:{elem.sourceline}: "
|
||||
f"paraId={val} is not valid hex"
|
||||
)
|
||||
|
||||
if val := elem.get(durable_id_attr):
|
||||
if xml_file.name == "numbering.xml":
|
||||
try:
|
||||
if self._parse_id_value(val, base=10) >= 0x7FFFFFFF:
|
||||
errors.append(
|
||||
f" {xml_file.name}:{elem.sourceline}: "
|
||||
f"durableId={val} >= 0x7FFFFFFF"
|
||||
)
|
||||
except ValueError:
|
||||
errors.append(
|
||||
f" {xml_file.name}:{elem.sourceline}: "
|
||||
f"durableId={val} must be decimal in numbering.xml"
|
||||
)
|
||||
else:
|
||||
try:
|
||||
if self._parse_id_value(val, base=16) >= 0x7FFFFFFF:
|
||||
errors.append(
|
||||
f" {xml_file.name}:{elem.sourceline}: "
|
||||
f"durableId={val} >= 0x7FFFFFFF"
|
||||
)
|
||||
except ValueError:
|
||||
errors.append(
|
||||
f" {xml_file.name}:{elem.sourceline}: "
|
||||
f"durableId={val} is not valid hex"
|
||||
)
|
||||
except lxml.etree.XMLSyntaxError:
|
||||
continue
|
||||
|
||||
if errors:
|
||||
print(f"FAILED - {len(errors)} ID constraint violations:")
|
||||
for e in errors:
|
||||
print(e)
|
||||
elif self.verbose:
|
||||
print("PASSED - All paraId/durableId values within constraints")
|
||||
return not errors
|
||||
|
||||
def validate_comment_markers(self):
|
||||
errors = []
|
||||
|
||||
document_xml = None
|
||||
comments_xml = None
|
||||
for xml_file in self.xml_files:
|
||||
if xml_file.name == "document.xml" and "word" in str(xml_file):
|
||||
document_xml = xml_file
|
||||
elif xml_file.name == "comments.xml":
|
||||
comments_xml = xml_file
|
||||
|
||||
if not document_xml:
|
||||
if self.verbose:
|
||||
print("PASSED - No document.xml found (skipping comment validation)")
|
||||
return True
|
||||
|
||||
try:
|
||||
doc_root = lxml.etree.parse(str(document_xml)).getroot()
|
||||
namespaces = {"w": self.WORD_2006_NAMESPACE}
|
||||
|
||||
range_starts = {
|
||||
elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id")
|
||||
for elem in doc_root.xpath(
|
||||
".//w:commentRangeStart", namespaces=namespaces
|
||||
)
|
||||
}
|
||||
range_ends = {
|
||||
elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id")
|
||||
for elem in doc_root.xpath(
|
||||
".//w:commentRangeEnd", namespaces=namespaces
|
||||
)
|
||||
}
|
||||
references = {
|
||||
elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id")
|
||||
for elem in doc_root.xpath(
|
||||
".//w:commentReference", namespaces=namespaces
|
||||
)
|
||||
}
|
||||
|
||||
orphaned_ends = range_ends - range_starts
|
||||
for comment_id in sorted(
|
||||
orphaned_ends, key=lambda x: int(x) if x and x.isdigit() else 0
|
||||
):
|
||||
errors.append(
|
||||
f' document.xml: commentRangeEnd id="{comment_id}" has no matching commentRangeStart'
|
||||
)
|
||||
|
||||
orphaned_starts = range_starts - range_ends
|
||||
for comment_id in sorted(
|
||||
orphaned_starts, key=lambda x: int(x) if x and x.isdigit() else 0
|
||||
):
|
||||
errors.append(
|
||||
f' document.xml: commentRangeStart id="{comment_id}" has no matching commentRangeEnd'
|
||||
)
|
||||
|
||||
comment_ids = set()
|
||||
if comments_xml and comments_xml.exists():
|
||||
comments_root = lxml.etree.parse(str(comments_xml)).getroot()
|
||||
comment_ids = {
|
||||
elem.get(f"{{{self.WORD_2006_NAMESPACE}}}id")
|
||||
for elem in comments_root.xpath(
|
||||
".//w:comment", namespaces=namespaces
|
||||
)
|
||||
}
|
||||
|
||||
marker_ids = range_starts | range_ends | references
|
||||
invalid_refs = marker_ids - comment_ids
|
||||
for comment_id in sorted(
|
||||
invalid_refs, key=lambda x: int(x) if x and x.isdigit() else 0
|
||||
):
|
||||
if comment_id:
|
||||
errors.append(
|
||||
f' document.xml: marker id="{comment_id}" references non-existent comment'
|
||||
)
|
||||
|
||||
except (lxml.etree.XMLSyntaxError, Exception) as e:
|
||||
errors.append(f" Error parsing XML: {e}")
|
||||
|
||||
if errors:
|
||||
print(f"FAILED - {len(errors)} comment marker violations:")
|
||||
for error in errors:
|
||||
print(error)
|
||||
return False
|
||||
else:
|
||||
if self.verbose:
|
||||
print("PASSED - All comment markers properly paired")
|
||||
return True
|
||||
|
||||
def repair(self) -> int:
|
||||
repairs = super().repair()
|
||||
repairs += self.repair_durableId()
|
||||
return repairs
|
||||
|
||||
def repair_durableId(self) -> int:
|
||||
DURABLE_ID_ATTRS = ("w16cid:durableId", "w16cex:durableId")
|
||||
repairs = 0
|
||||
renames: dict = {}
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
try:
|
||||
content = xml_file.read_text(encoding="utf-8")
|
||||
dom = defusedxml.minidom.parseString(content)
|
||||
is_numbering = xml_file.name == "numbering.xml"
|
||||
base = 10 if is_numbering else 16
|
||||
pending = []
|
||||
seen_in_file = set()
|
||||
modified = False
|
||||
|
||||
for elem in dom.getElementsByTagName("*"):
|
||||
for attr_name in DURABLE_ID_ATTRS:
|
||||
if not elem.hasAttribute(attr_name):
|
||||
continue
|
||||
|
||||
durable_id = elem.getAttribute(attr_name)
|
||||
try:
|
||||
key = self._parse_id_value(durable_id, base=base)
|
||||
needs_repair = key >= 0x7FFFFFFF
|
||||
except ValueError:
|
||||
key = durable_id
|
||||
needs_repair = True
|
||||
|
||||
if needs_repair:
|
||||
if key in seen_in_file:
|
||||
value = random.randint(1, 0x7FFFFFFE)
|
||||
else:
|
||||
seen_in_file.add(key)
|
||||
if key not in renames:
|
||||
renames[key] = random.randint(1, 0x7FFFFFFE)
|
||||
value = renames[key]
|
||||
new_id = str(value) if is_numbering else f"{value:08X}"
|
||||
|
||||
elem.setAttribute(attr_name, new_id)
|
||||
pending.append(
|
||||
f" Repaired: {xml_file.name}: durableId {durable_id} → {new_id}"
|
||||
)
|
||||
modified = True
|
||||
|
||||
if modified:
|
||||
xml_file.write_bytes(dom.toxml(encoding="UTF-8"))
|
||||
for message in pending:
|
||||
print(message)
|
||||
repairs += len(pending)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return repairs
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise RuntimeError("This module should not be run directly.")
|
||||
@@ -0,0 +1,441 @@
|
||||
"""
|
||||
Validator for PowerPoint presentation XML files against XSD schemas.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from helpers import opc_target, rels_source_part, safe_extract
|
||||
|
||||
from .base import BaseSchemaValidator
|
||||
|
||||
|
||||
class PPTXSchemaValidator(BaseSchemaValidator):
|
||||
|
||||
PRESENTATIONML_NAMESPACE = (
|
||||
"http://schemas.openxmlformats.org/presentationml/2006/main"
|
||||
)
|
||||
|
||||
ELEMENT_RELATIONSHIP_TYPES = {
|
||||
"sldid": "slide",
|
||||
"sldmasterid": "slidemaster",
|
||||
"notesmasterid": "notesmaster",
|
||||
"sldlayoutid": "slidelayout",
|
||||
"themeid": "theme",
|
||||
"tablestyleid": "tablestyles",
|
||||
}
|
||||
|
||||
def validate(self):
|
||||
if not self.validate_xml():
|
||||
return False
|
||||
|
||||
all_valid = True
|
||||
if not self.validate_namespaces():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_unique_ids():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_uuid_ids():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_file_references():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_slide_layout_ids():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_content_types():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_against_xsd():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_notes_slide_references():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_all_relationship_ids():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_no_duplicate_slide_layouts():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_master_theme_uniqueness():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_charts():
|
||||
all_valid = False
|
||||
|
||||
if not self.validate_slides():
|
||||
all_valid = False
|
||||
|
||||
return all_valid
|
||||
|
||||
def _package_map(self) -> dict:
|
||||
wanted = []
|
||||
wanted += list(self.unpacked_dir.glob("[[]Content_Types[]].xml"))
|
||||
wanted += list(self.unpacked_dir.glob("ppt/presentation.xml"))
|
||||
wanted += list(self.unpacked_dir.glob("ppt/theme/*.xml"))
|
||||
wanted += list(self.unpacked_dir.glob("ppt/theme/_rels/*.rels"))
|
||||
wanted += list(self.unpacked_dir.glob("ppt/charts/chart*.xml"))
|
||||
for group in ("slideMasters", "notesMasters", "handoutMasters"):
|
||||
wanted += list(self.unpacked_dir.glob(f"ppt/{group}/*.xml"))
|
||||
wanted += list(self.unpacked_dir.glob(f"ppt/{group}/_rels/*.rels"))
|
||||
return {
|
||||
p.relative_to(self.unpacked_dir).as_posix(): p.read_bytes()
|
||||
for p in wanted
|
||||
if p.is_file()
|
||||
}
|
||||
|
||||
def validate_master_theme_uniqueness(self):
|
||||
from helpers.pptx_theme import _NOTES_MASTERS, live_shared_master_themes
|
||||
|
||||
shared = live_shared_master_themes(self._package_map())
|
||||
if shared:
|
||||
print(f"FAILED - Found {len(shared)} master(s) sharing a theme part:")
|
||||
for message in shared:
|
||||
print(f" {message}")
|
||||
if any(m.startswith(_NOTES_MASTERS) for m in shared):
|
||||
print(" Fix: in ppt/presentation.xml, move <p:notesMasterIdLst> back to "
|
||||
"directly after <p:sldIdLst>. PowerPoint reads that happily.")
|
||||
else:
|
||||
print(" Fix: give each master its own theme part.")
|
||||
return False
|
||||
|
||||
if self.verbose:
|
||||
print("PASSED - No master shares a theme part in a way PowerPoint refuses")
|
||||
return True
|
||||
|
||||
def validate_charts(self):
|
||||
from helpers.pptx_chart import find_chart_problems
|
||||
|
||||
problems = find_chart_problems(self._package_map())
|
||||
if problems:
|
||||
print(f"FAILED - Found {len(problems)} chart problem(s) PowerPoint rejects:")
|
||||
for message in problems:
|
||||
print(f" {message}")
|
||||
return False
|
||||
|
||||
if self.verbose:
|
||||
print("PASSED - Charts satisfy the constraints PowerPoint enforces")
|
||||
return True
|
||||
|
||||
def _original_slide_defects(self, schema) -> set[str]:
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
from helpers.pptx_slide import SLIDE_PART_RE, fatal_slide_errors
|
||||
|
||||
if self.original_file is None:
|
||||
return set()
|
||||
|
||||
found: set[str] = set()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
try:
|
||||
with zipfile.ZipFile(self.original_file, "r") as zf:
|
||||
safe_extract(zf, temp_path)
|
||||
except (zipfile.BadZipFile, ValueError, OSError):
|
||||
return set()
|
||||
|
||||
for part in sorted(temp_path.rglob("*.xml")):
|
||||
relative = part.relative_to(temp_path).as_posix()
|
||||
if not SLIDE_PART_RE.fullmatch(relative):
|
||||
continue
|
||||
ok, errors = self._validate_single_file_xsd(
|
||||
part.resolve(), temp_path.resolve(), schema_path=schema
|
||||
)
|
||||
if ok is None or ok or not errors:
|
||||
continue
|
||||
found |= set(fatal_slide_errors(set(errors)))
|
||||
return found
|
||||
|
||||
def validate_slides(self):
|
||||
from helpers.pptx_slide import (
|
||||
SLIDE_PART_RE,
|
||||
fatal_slide_errors,
|
||||
is_schema_verdict,
|
||||
)
|
||||
|
||||
schema = self.schemas_dir / self.SCHEMA_MAPPINGS["ppt"]
|
||||
inherited = self._original_slide_defects(schema)
|
||||
problems: list[str] = []
|
||||
broken: list[str] = []
|
||||
|
||||
for xml_file in self.xml_files:
|
||||
relative = xml_file.relative_to(self.unpacked_dir).as_posix()
|
||||
if not SLIDE_PART_RE.fullmatch(relative):
|
||||
continue
|
||||
ok, errors = self._validate_single_file_xsd(
|
||||
xml_file.resolve(), self.unpacked_dir.resolve(), schema_path=schema
|
||||
)
|
||||
if ok is None or not errors:
|
||||
continue
|
||||
|
||||
unreadable = [f"{relative}: {e}" for e in errors if not is_schema_verdict(e)]
|
||||
if unreadable:
|
||||
broken.extend(unreadable)
|
||||
continue
|
||||
if ok:
|
||||
continue
|
||||
|
||||
for message in fatal_slide_errors(set(errors)):
|
||||
if message in inherited:
|
||||
continue
|
||||
problems.append(f"{relative}: {message}")
|
||||
|
||||
if broken:
|
||||
print(f"FAILED - Could not check {len(broken)} slide part(s):")
|
||||
for message in sorted(broken):
|
||||
print(f" {message[:240]}")
|
||||
|
||||
if problems:
|
||||
print(f"FAILED - Found {len(problems)} slide problem(s) PowerPoint rejects:")
|
||||
for message in sorted(problems):
|
||||
print(f" {message[:240]}")
|
||||
|
||||
if broken or problems:
|
||||
return False
|
||||
|
||||
if self.verbose:
|
||||
print("PASSED - Slide XML has none of the defects PowerPoint refuses")
|
||||
return True
|
||||
|
||||
def _get_schema_path(self, xml_file):
|
||||
if xml_file.parent.name == "charts" and xml_file.name.startswith("chart"):
|
||||
return None
|
||||
return super()._get_schema_path(xml_file)
|
||||
|
||||
def _preprocess_for_schema(self, xml_doc, relative_path):
|
||||
if relative_path.as_posix() != "ppt/presentation.xml":
|
||||
return xml_doc
|
||||
|
||||
root = xml_doc.getroot()
|
||||
ns = f"{{{self.PRESENTATIONML_NAMESPACE}}}"
|
||||
notes = root.find(f"{ns}notesMasterIdLst")
|
||||
slides = root.find(f"{ns}sldIdLst")
|
||||
if notes is None or slides is None:
|
||||
return xml_doc
|
||||
|
||||
children = list(root)
|
||||
if children.index(notes) < children.index(slides):
|
||||
return xml_doc
|
||||
|
||||
root.remove(notes)
|
||||
root.insert(list(root).index(slides), notes)
|
||||
return xml_doc
|
||||
|
||||
def validate_uuid_ids(self):
|
||||
import lxml.etree
|
||||
|
||||
errors = []
|
||||
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()
|
||||
|
||||
for elem in root.iter():
|
||||
for attr, value in elem.attrib.items():
|
||||
attr_name = attr.split("}")[-1].lower()
|
||||
if attr_name == "id" or attr_name.endswith("id"):
|
||||
if self._looks_like_uuid(value):
|
||||
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):
|
||||
clean_value = value.strip("{}()").replace("-", "")
|
||||
return len(clean_value) == 32 and all(c.isalnum() for c in clean_value)
|
||||
|
||||
def validate_slide_layout_ids(self):
|
||||
import lxml.etree
|
||||
|
||||
errors = []
|
||||
|
||||
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:
|
||||
root = lxml.etree.parse(str(slide_master)).getroot()
|
||||
|
||||
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
|
||||
|
||||
rels_root = lxml.etree.parse(str(rels_file)).getroot()
|
||||
|
||||
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"))
|
||||
|
||||
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):
|
||||
import lxml.etree
|
||||
|
||||
errors = []
|
||||
slide_rels_files = list(self.unpacked_dir.glob("ppt/slides/_rels/*.xml.rels"))
|
||||
|
||||
for rels_file in slide_rels_files:
|
||||
try:
|
||||
root = lxml.etree.parse(str(rels_file)).getroot()
|
||||
|
||||
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):
|
||||
import lxml.etree
|
||||
|
||||
errors = []
|
||||
notes_slide_references = {}
|
||||
|
||||
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:
|
||||
root = lxml.etree.parse(str(rels_file)).getroot()
|
||||
|
||||
for rel in root.findall(
|
||||
f".//{{{self.PACKAGE_RELATIONSHIPS_NAMESPACE}}}Relationship"
|
||||
):
|
||||
rel_type = rel.get("Type", "")
|
||||
if "notesSlide" in rel_type:
|
||||
part = opc_target(
|
||||
rel.get("Target", ""),
|
||||
rels_source_part(rels_file, self.unpacked_dir),
|
||||
rel.get("TargetMode", ""),
|
||||
)
|
||||
if part:
|
||||
slide_name = rels_file.stem.replace(
|
||||
".xml", ""
|
||||
)
|
||||
|
||||
notes_slide_references.setdefault(part, []).append(
|
||||
(slide_name, rels_file)
|
||||
)
|
||||
|
||||
except (lxml.etree.XMLSyntaxError, Exception) as e:
|
||||
errors.append(
|
||||
f" {rels_file.relative_to(self.unpacked_dir)}: Error: {e}"
|
||||
)
|
||||
|
||||
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.")
|
||||
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Validator for tracked changes in Word documents.
|
||||
|
||||
Detects untracked edits in word/document.xml: text that differs from the
|
||||
original without a <w:ins>/<w:del> wrapper recording it. The tracked changes
|
||||
that are new relative to the original are undone, and the result is compared
|
||||
against the original; whatever text still differs was edited without being
|
||||
tracked.
|
||||
|
||||
Only the document body is compared. Headers, footers, footnotes and endnotes
|
||||
are separate parts and are not checked.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.ElementTree as ET
|
||||
from defusedxml.common import DefusedXmlException
|
||||
|
||||
from helpers import rendered_text, safe_extract
|
||||
|
||||
|
||||
class RedliningValidator:
|
||||
|
||||
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 repair(self) -> int:
|
||||
return 0
|
||||
|
||||
def validate(self):
|
||||
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
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
|
||||
safe_extract(zip_ref, 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
|
||||
|
||||
try:
|
||||
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, DefusedXmlException) as e:
|
||||
print(f"FAILED - Error parsing XML files: {e}")
|
||||
return False
|
||||
|
||||
new_changes = self._new_tracked_changes(original_root, modified_root)
|
||||
self._remove_tracked_changes(modified_root, new_changes)
|
||||
|
||||
modified_text = self._extract_text_content(modified_root)
|
||||
original_text = self._extract_text_content(original_root)
|
||||
|
||||
if modified_text != original_text:
|
||||
error_message = self._generate_detailed_diff(
|
||||
original_text, modified_text
|
||||
)
|
||||
print(error_message)
|
||||
return False
|
||||
|
||||
if self.verbose:
|
||||
print(
|
||||
f"PASSED - All {len(new_changes)} change(s) against the original "
|
||||
"are properly tracked"
|
||||
)
|
||||
return True
|
||||
|
||||
def _tracked_change_elements(self, root):
|
||||
ins_tag = f"{{{self.namespaces['w']}}}ins"
|
||||
del_tag = f"{{{self.namespaces['w']}}}del"
|
||||
return [elem for elem in root.iter() if elem.tag in (ins_tag, del_tag)]
|
||||
|
||||
def _rendered_text(self, elem):
|
||||
preserve = elem.get("{http://www.w3.org/XML/1998/namespace}space") == "preserve"
|
||||
return rendered_text(elem.text or "", preserve)
|
||||
|
||||
def _text_elements(self, elem):
|
||||
w = self.namespaces["w"]
|
||||
return [
|
||||
node
|
||||
for node in elem.iter()
|
||||
if node.tag in (f"{{{w}}}t", f"{{{w}}}delText")
|
||||
]
|
||||
|
||||
def _tracked_change_key(self, elem):
|
||||
w = self.namespaces["w"]
|
||||
text = "".join(self._rendered_text(node) for node in self._text_elements(elem))
|
||||
return (elem.tag, elem.get(f"{{{w}}}author"), elem.get(f"{{{w}}}date"), text)
|
||||
|
||||
def _new_tracked_changes(self, original_root, modified_root):
|
||||
original = self._tracked_change_elements(original_root)
|
||||
modified = self._tracked_change_elements(modified_root)
|
||||
|
||||
pool = {}
|
||||
for elem in original:
|
||||
pool.setdefault(self._tracked_change_key(elem), []).append(elem)
|
||||
|
||||
matched, leftover = set(), []
|
||||
for elem in modified:
|
||||
bucket = pool.get(self._tracked_change_key(elem))
|
||||
if bucket:
|
||||
matched.add(bucket.pop())
|
||||
else:
|
||||
leftover.append(elem)
|
||||
|
||||
def group(elem):
|
||||
return self._tracked_change_key(elem)[:3]
|
||||
|
||||
def text_of(elems):
|
||||
return "".join(self._tracked_change_key(e)[3] for e in elems)
|
||||
|
||||
unmatched_original = {}
|
||||
for elem in original:
|
||||
if elem not in matched:
|
||||
unmatched_original.setdefault(group(elem), []).append(elem)
|
||||
|
||||
by_group = {}
|
||||
for elem in leftover:
|
||||
by_group.setdefault(group(elem), []).append(elem)
|
||||
|
||||
new = set()
|
||||
for key, elems in by_group.items():
|
||||
rebuilt = text_of(elems)
|
||||
if rebuilt and rebuilt == text_of(unmatched_original.get(key, [])):
|
||||
continue
|
||||
new.update(elems)
|
||||
return new
|
||||
|
||||
def _generate_detailed_diff(self, original_text, modified_text):
|
||||
error_parts = [
|
||||
"FAILED - Document text doesn't match after removing the 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",
|
||||
" 4. Rewrote another author's <w:ins>/<w:del> and changed its text on",
|
||||
" the way. A tracked change from the original is recognised by its",
|
||||
" author, date and text; anything that doesn't reproduce one exactly",
|
||||
" reads as new, and the text it carried is reported missing.",
|
||||
"",
|
||||
"For pre-redlined documents, use correct patterns:",
|
||||
" - To reject another's INSERTION: Nest <w:del> inside their <w:ins>",
|
||||
" - To reject PART of one: nest <w:del> around only the runs you reject.",
|
||||
" Their <w:ins> may be split around it, so long as the pieces keep",
|
||||
" their author and date and still spell out the same text.",
|
||||
" - To restore another's DELETION: Add new <w:ins> AFTER their <w:del>",
|
||||
"",
|
||||
]
|
||||
|
||||
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):
|
||||
try:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
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")
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"diff",
|
||||
"--word-diff=plain",
|
||||
"--word-diff-regex=.",
|
||||
"-U0",
|
||||
"--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)
|
||||
|
||||
if content_lines:
|
||||
return "\n".join(content_lines)
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"diff",
|
||||
"--word-diff=plain",
|
||||
"-U0",
|
||||
"--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):
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
def _remove_tracked_changes(self, root, targets):
|
||||
ins_tag = f"{{{self.namespaces['w']}}}ins"
|
||||
del_tag = f"{{{self.namespaces['w']}}}del"
|
||||
|
||||
for parent in root.iter():
|
||||
to_remove = []
|
||||
for child in parent:
|
||||
if child.tag == ins_tag and child in targets:
|
||||
to_remove.append(child)
|
||||
for elem in to_remove:
|
||||
parent.remove(elem)
|
||||
|
||||
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 in targets:
|
||||
to_process.append((child, list(parent).index(child)))
|
||||
|
||||
for del_elem, del_index in reversed(to_process):
|
||||
for elem in del_elem.iter():
|
||||
if elem.tag == deltext_tag:
|
||||
elem.tag = t_tag
|
||||
|
||||
for child in reversed(list(del_elem)):
|
||||
parent.insert(del_index, child)
|
||||
parent.remove(del_elem)
|
||||
|
||||
def _extract_text_content(self, root):
|
||||
p_tag = f"{{{self.namespaces['w']}}}p"
|
||||
t_tag = f"{{{self.namespaces['w']}}}t"
|
||||
|
||||
paragraphs = []
|
||||
for p_elem in root.findall(f".//{p_tag}"):
|
||||
text_parts = []
|
||||
for t_elem in p_elem.findall(f".//{t_tag}"):
|
||||
text_parts.append(self._rendered_text(t_elem))
|
||||
paragraph_text = "".join(text_parts)
|
||||
if paragraph_text:
|
||||
paragraphs.append(paragraph_text)
|
||||
|
||||
return "\n".join(paragraphs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise RuntimeError("This module should not be run directly.")
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" ?>
|
||||
<w:comments xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14">
|
||||
</w:comments>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" ?>
|
||||
<w15:commentsEx xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14">
|
||||
</w15:commentsEx>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" ?>
|
||||
<w16cex:commentsExtensible xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" xmlns:cr="http://schemas.microsoft.com/office/comments/2020/reactions" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl cr w16du wp14">
|
||||
</w16cex:commentsExtensible>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" ?>
|
||||
<w16cid:commentsIds xmlns:wpc="http://schemas.microsoft.com/office/word/2010/wordprocessingCanvas" xmlns:cx="http://schemas.microsoft.com/office/drawing/2014/chartex" xmlns:cx1="http://schemas.microsoft.com/office/drawing/2015/9/8/chartex" xmlns:cx2="http://schemas.microsoft.com/office/drawing/2015/10/21/chartex" xmlns:cx3="http://schemas.microsoft.com/office/drawing/2016/5/9/chartex" xmlns:cx4="http://schemas.microsoft.com/office/drawing/2016/5/10/chartex" xmlns:cx5="http://schemas.microsoft.com/office/drawing/2016/5/11/chartex" xmlns:cx6="http://schemas.microsoft.com/office/drawing/2016/5/12/chartex" xmlns:cx7="http://schemas.microsoft.com/office/drawing/2016/5/13/chartex" xmlns:cx8="http://schemas.microsoft.com/office/drawing/2016/5/14/chartex" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:aink="http://schemas.microsoft.com/office/drawing/2016/ink" xmlns:am3d="http://schemas.microsoft.com/office/drawing/2017/model3d" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:oel="http://schemas.microsoft.com/office/2019/extlst" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:m="http://schemas.openxmlformats.org/officeDocument/2006/math" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:wp14="http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:w10="urn:schemas-microsoft-com:office:word" xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:w14="http://schemas.microsoft.com/office/word/2010/wordml" xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml" xmlns:w16cex="http://schemas.microsoft.com/office/word/2018/wordml/cex" xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid" xmlns:w16="http://schemas.microsoft.com/office/word/2018/wordml" xmlns:w16du="http://schemas.microsoft.com/office/word/2023/wordml/word16du" xmlns:w16sdtdh="http://schemas.microsoft.com/office/word/2020/wordml/sdtdatahash" xmlns:w16sdtfl="http://schemas.microsoft.com/office/word/2024/wordml/sdtformatlock" xmlns:w16se="http://schemas.microsoft.com/office/word/2015/wordml/symex" xmlns:wpg="http://schemas.microsoft.com/office/word/2010/wordprocessingGroup" xmlns:wpi="http://schemas.microsoft.com/office/word/2010/wordprocessingInk" xmlns:wne="http://schemas.microsoft.com/office/word/2006/wordml" xmlns:wps="http://schemas.microsoft.com/office/word/2010/wordprocessingShape" mc:Ignorable="w14 w15 w16se w16cid w16 w16cex w16sdtdh w16sdtfl w16du wp14">
|
||||
</w16cid:commentsIds>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" ?>
|
||||
<w15:people xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml">
|
||||
</w15:people>
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
name: error-handling-patterns
|
||||
description: Master error handling patterns across languages including exceptions, Result types, error propagation, and graceful degradation to build resilient applications. Use when implementing error handling, designing APIs, or improving application reliability.
|
||||
---
|
||||
|
||||
# Error Handling Patterns
|
||||
|
||||
Build resilient applications with robust error handling strategies that gracefully handle failures and provide excellent debugging experiences.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Implementing error handling in new features
|
||||
- Designing error-resilient APIs
|
||||
- Debugging production issues
|
||||
- Improving application reliability
|
||||
- Creating better error messages for users and developers
|
||||
- Implementing retry and circuit breaker patterns
|
||||
- Handling async/concurrent errors
|
||||
- Building fault-tolerant distributed systems
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Error Handling Philosophies
|
||||
|
||||
**Exceptions vs Result Types:**
|
||||
|
||||
- **Exceptions**: Traditional try-catch, disrupts control flow
|
||||
- **Result Types**: Explicit success/failure, functional approach
|
||||
- **Error Codes**: C-style, requires discipline
|
||||
- **Option/Maybe Types**: For nullable values
|
||||
|
||||
**When to Use Each:**
|
||||
|
||||
- Exceptions: Unexpected errors, exceptional conditions
|
||||
- Result Types: Expected errors, validation failures
|
||||
- Panics/Crashes: Unrecoverable errors, programming bugs
|
||||
|
||||
### 2. Error Categories
|
||||
|
||||
**Recoverable Errors:**
|
||||
|
||||
- Network timeouts
|
||||
- Missing files
|
||||
- Invalid user input
|
||||
- API rate limits
|
||||
|
||||
**Unrecoverable Errors:**
|
||||
|
||||
- Out of memory
|
||||
- Stack overflow
|
||||
- Programming bugs (null pointer, etc.)
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Fail Fast**: Validate input early, fail quickly
|
||||
2. **Preserve Context**: Include stack traces, metadata, timestamps
|
||||
3. **Meaningful Messages**: Explain what happened and how to fix it
|
||||
4. **Log Appropriately**: Error = log, expected failure = don't spam logs
|
||||
5. **Handle at Right Level**: Catch where you can meaningfully handle
|
||||
6. **Clean Up Resources**: Use try-finally, context managers, defer
|
||||
7. **Don't Swallow Errors**: Log or re-throw, don't silently ignore
|
||||
8. **Type-Safe Errors**: Use typed errors when possible
|
||||
|
||||
```python
|
||||
# Good error handling example
|
||||
def process_order(order_id: str) -> Order:
|
||||
"""Process order with comprehensive error handling."""
|
||||
try:
|
||||
# Validate input
|
||||
if not order_id:
|
||||
raise ValidationError("Order ID is required")
|
||||
|
||||
# Fetch order
|
||||
order = db.get_order(order_id)
|
||||
if not order:
|
||||
raise NotFoundError("Order", order_id)
|
||||
|
||||
# Process payment
|
||||
try:
|
||||
payment_result = payment_service.charge(order.total)
|
||||
except PaymentServiceError as e:
|
||||
# Log and wrap external service error
|
||||
logger.error(f"Payment failed for order {order_id}: {e}")
|
||||
raise ExternalServiceError(
|
||||
f"Payment processing failed",
|
||||
service="payment_service",
|
||||
details={"order_id": order_id, "amount": order.total}
|
||||
) from e
|
||||
|
||||
# Update order
|
||||
order.status = "completed"
|
||||
order.payment_id = payment_result.id
|
||||
db.save(order)
|
||||
|
||||
return order
|
||||
|
||||
except ApplicationError:
|
||||
# Re-raise known application errors
|
||||
raise
|
||||
except Exception as e:
|
||||
# Log unexpected errors
|
||||
logger.exception(f"Unexpected error processing order {order_id}")
|
||||
raise ApplicationError(
|
||||
"Order processing failed",
|
||||
code="INTERNAL_ERROR"
|
||||
) from e
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Catching Too Broadly**: `except Exception` hides bugs
|
||||
- **Empty Catch Blocks**: Silently swallowing errors
|
||||
- **Logging and Re-throwing**: Creates duplicate log entries
|
||||
- **Not Cleaning Up**: Forgetting to close files, connections
|
||||
- **Poor Error Messages**: "Error occurred" is not helpful
|
||||
- **Returning Error Codes**: Use exceptions or Result types
|
||||
- **Ignoring Async Errors**: Unhandled promise rejections
|
||||
@@ -0,0 +1,516 @@
|
||||
# error-handling-patterns — detailed patterns and worked examples
|
||||
|
||||
## Language-Specific Patterns
|
||||
|
||||
### Python Error Handling
|
||||
|
||||
**Custom Exception Hierarchy:**
|
||||
|
||||
```python
|
||||
class ApplicationError(Exception):
|
||||
"""Base exception for all application errors."""
|
||||
def __init__(self, message: str, code: str = None, details: dict = None):
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
self.details = details or {}
|
||||
self.timestamp = datetime.utcnow()
|
||||
|
||||
class ValidationError(ApplicationError):
|
||||
"""Raised when validation fails."""
|
||||
pass
|
||||
|
||||
class NotFoundError(ApplicationError):
|
||||
"""Raised when resource not found."""
|
||||
pass
|
||||
|
||||
class ExternalServiceError(ApplicationError):
|
||||
"""Raised when external service fails."""
|
||||
def __init__(self, message: str, service: str, **kwargs):
|
||||
super().__init__(message, **kwargs)
|
||||
self.service = service
|
||||
|
||||
# Usage
|
||||
def get_user(user_id: str) -> User:
|
||||
user = db.query(User).filter_by(id=user_id).first()
|
||||
if not user:
|
||||
raise NotFoundError(
|
||||
f"User not found",
|
||||
code="USER_NOT_FOUND",
|
||||
details={"user_id": user_id}
|
||||
)
|
||||
return user
|
||||
```
|
||||
|
||||
**Context Managers for Cleanup:**
|
||||
|
||||
```python
|
||||
from contextlib import contextmanager
|
||||
|
||||
@contextmanager
|
||||
def database_transaction(session):
|
||||
"""Ensure transaction is committed or rolled back."""
|
||||
try:
|
||||
yield session
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
# Usage
|
||||
with database_transaction(db.session) as session:
|
||||
user = User(name="Alice")
|
||||
session.add(user)
|
||||
# Automatic commit or rollback
|
||||
```
|
||||
|
||||
**Retry with Exponential Backoff:**
|
||||
|
||||
```python
|
||||
import time
|
||||
from functools import wraps
|
||||
from typing import TypeVar, Callable
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def retry(
|
||||
max_attempts: int = 3,
|
||||
backoff_factor: float = 2.0,
|
||||
exceptions: tuple = (Exception,)
|
||||
):
|
||||
"""Retry decorator with exponential backoff."""
|
||||
def decorator(func: Callable[..., T]) -> Callable[..., T]:
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs) -> T:
|
||||
last_exception = None
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except exceptions as e:
|
||||
last_exception = e
|
||||
if attempt < max_attempts - 1:
|
||||
sleep_time = backoff_factor ** attempt
|
||||
time.sleep(sleep_time)
|
||||
continue
|
||||
raise
|
||||
raise last_exception
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
# Usage
|
||||
@retry(max_attempts=3, exceptions=(NetworkError,))
|
||||
def fetch_data(url: str) -> dict:
|
||||
response = requests.get(url, timeout=5)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
```
|
||||
|
||||
### TypeScript/JavaScript Error Handling
|
||||
|
||||
**Custom Error Classes:**
|
||||
|
||||
```typescript
|
||||
// Custom error classes
|
||||
class ApplicationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public code: string,
|
||||
public statusCode: number = 500,
|
||||
public details?: Record<string, any>,
|
||||
) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends ApplicationError {
|
||||
constructor(message: string, details?: Record<string, any>) {
|
||||
super(message, "VALIDATION_ERROR", 400, details);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends ApplicationError {
|
||||
constructor(resource: string, id: string) {
|
||||
super(`${resource} not found`, "NOT_FOUND", 404, { resource, id });
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
function getUser(id: string): User {
|
||||
const user = users.find((u) => u.id === id);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User", id);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
**Result Type Pattern:**
|
||||
|
||||
```typescript
|
||||
// Result type for explicit error handling
|
||||
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
|
||||
|
||||
// Helper functions
|
||||
function Ok<T>(value: T): Result<T, never> {
|
||||
return { ok: true, value };
|
||||
}
|
||||
|
||||
function Err<E>(error: E): Result<never, E> {
|
||||
return { ok: false, error };
|
||||
}
|
||||
|
||||
// Usage
|
||||
function parseJSON<T>(json: string): Result<T, SyntaxError> {
|
||||
try {
|
||||
const value = JSON.parse(json) as T;
|
||||
return Ok(value);
|
||||
} catch (error) {
|
||||
return Err(error as SyntaxError);
|
||||
}
|
||||
}
|
||||
|
||||
// Consuming Result
|
||||
const result = parseJSON<User>(userJson);
|
||||
if (result.ok) {
|
||||
console.log(result.value.name);
|
||||
} else {
|
||||
console.error("Parse failed:", result.error.message);
|
||||
}
|
||||
|
||||
// Chaining Results
|
||||
function chain<T, U, E>(
|
||||
result: Result<T, E>,
|
||||
fn: (value: T) => Result<U, E>,
|
||||
): Result<U, E> {
|
||||
return result.ok ? fn(result.value) : result;
|
||||
}
|
||||
```
|
||||
|
||||
**Async Error Handling:**
|
||||
|
||||
```typescript
|
||||
// Async/await with proper error handling
|
||||
async function fetchUserOrders(userId: string): Promise<Order[]> {
|
||||
try {
|
||||
const user = await getUser(userId);
|
||||
const orders = await getOrders(user.id);
|
||||
return orders;
|
||||
} catch (error) {
|
||||
if (error instanceof NotFoundError) {
|
||||
return []; // Return empty array for not found
|
||||
}
|
||||
if (error instanceof NetworkError) {
|
||||
// Retry logic
|
||||
return retryFetchOrders(userId);
|
||||
}
|
||||
// Re-throw unexpected errors
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Promise error handling
|
||||
function fetchData(url: string): Promise<Data> {
|
||||
return fetch(url)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new NetworkError(`HTTP ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Fetch failed:", error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Rust Error Handling
|
||||
|
||||
**Result and Option Types:**
|
||||
|
||||
```rust
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read};
|
||||
|
||||
// Result type for operations that can fail
|
||||
fn read_file(path: &str) -> Result<String, io::Error> {
|
||||
let mut file = File::open(path)?; // ? operator propagates errors
|
||||
let mut contents = String::new();
|
||||
file.read_to_string(&mut contents)?;
|
||||
Ok(contents)
|
||||
}
|
||||
|
||||
// Custom error types
|
||||
#[derive(Debug)]
|
||||
enum AppError {
|
||||
Io(io::Error),
|
||||
Parse(std::num::ParseIntError),
|
||||
NotFound(String),
|
||||
Validation(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for AppError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
AppError::Io(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Using custom error type
|
||||
fn read_number_from_file(path: &str) -> Result<i32, AppError> {
|
||||
let contents = read_file(path)?; // Auto-converts io::Error
|
||||
let number = contents.trim().parse()
|
||||
.map_err(AppError::Parse)?; // Explicitly convert ParseIntError
|
||||
Ok(number)
|
||||
}
|
||||
|
||||
// Option for nullable values
|
||||
fn find_user(id: &str) -> Option<User> {
|
||||
users.iter().find(|u| u.id == id).cloned()
|
||||
}
|
||||
|
||||
// Combining Option and Result
|
||||
fn get_user_age(id: &str) -> Result<u32, AppError> {
|
||||
find_user(id)
|
||||
.ok_or_else(|| AppError::NotFound(id.to_string()))
|
||||
.map(|user| user.age)
|
||||
}
|
||||
```
|
||||
|
||||
### Go Error Handling
|
||||
|
||||
**Explicit Error Returns:**
|
||||
|
||||
```go
|
||||
// Basic error handling
|
||||
func getUser(id string) (*User, error) {
|
||||
user, err := db.QueryUser(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query user: %w", err)
|
||||
}
|
||||
if user == nil {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Custom error types
|
||||
type ValidationError struct {
|
||||
Field string
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ValidationError) Error() string {
|
||||
return fmt.Sprintf("validation failed for %s: %s", e.Field, e.Message)
|
||||
}
|
||||
|
||||
// Sentinel errors for comparison
|
||||
var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
)
|
||||
|
||||
// Error checking
|
||||
user, err := getUser("123")
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
// Handle not found
|
||||
} else {
|
||||
// Handle other errors
|
||||
}
|
||||
}
|
||||
|
||||
// Error wrapping and unwrapping
|
||||
func processUser(id string) error {
|
||||
user, err := getUser(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("process user failed: %w", err)
|
||||
}
|
||||
// Process user
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unwrap errors
|
||||
err := processUser("123")
|
||||
if err != nil {
|
||||
var valErr *ValidationError
|
||||
if errors.As(err, &valErr) {
|
||||
fmt.Printf("Validation error: %s\n", valErr.Field)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Universal Patterns
|
||||
|
||||
### Pattern 1: Circuit Breaker
|
||||
|
||||
Prevent cascading failures in distributed systems.
|
||||
|
||||
```python
|
||||
from enum import Enum
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
class CircuitState(Enum):
|
||||
CLOSED = "closed" # Normal operation
|
||||
OPEN = "open" # Failing, reject requests
|
||||
HALF_OPEN = "half_open" # Testing if recovered
|
||||
|
||||
class CircuitBreaker:
|
||||
def __init__(
|
||||
self,
|
||||
failure_threshold: int = 5,
|
||||
timeout: timedelta = timedelta(seconds=60),
|
||||
success_threshold: int = 2
|
||||
):
|
||||
self.failure_threshold = failure_threshold
|
||||
self.timeout = timeout
|
||||
self.success_threshold = success_threshold
|
||||
self.failure_count = 0
|
||||
self.success_count = 0
|
||||
self.state = CircuitState.CLOSED
|
||||
self.last_failure_time = None
|
||||
|
||||
def call(self, func: Callable[[], T]) -> T:
|
||||
if self.state == CircuitState.OPEN:
|
||||
if datetime.now() - self.last_failure_time > self.timeout:
|
||||
self.state = CircuitState.HALF_OPEN
|
||||
self.success_count = 0
|
||||
else:
|
||||
raise Exception("Circuit breaker is OPEN")
|
||||
|
||||
try:
|
||||
result = func()
|
||||
self.on_success()
|
||||
return result
|
||||
except Exception as e:
|
||||
self.on_failure()
|
||||
raise
|
||||
|
||||
def on_success(self):
|
||||
self.failure_count = 0
|
||||
if self.state == CircuitState.HALF_OPEN:
|
||||
self.success_count += 1
|
||||
if self.success_count >= self.success_threshold:
|
||||
self.state = CircuitState.CLOSED
|
||||
self.success_count = 0
|
||||
|
||||
def on_failure(self):
|
||||
self.failure_count += 1
|
||||
self.last_failure_time = datetime.now()
|
||||
if self.failure_count >= self.failure_threshold:
|
||||
self.state = CircuitState.OPEN
|
||||
|
||||
# Usage
|
||||
circuit_breaker = CircuitBreaker()
|
||||
|
||||
def fetch_data():
|
||||
return circuit_breaker.call(lambda: external_api.get_data())
|
||||
```
|
||||
|
||||
### Pattern 2: Error Aggregation
|
||||
|
||||
Collect multiple errors instead of failing on first error.
|
||||
|
||||
```typescript
|
||||
class ErrorCollector {
|
||||
private errors: Error[] = [];
|
||||
|
||||
add(error: Error): void {
|
||||
this.errors.push(error);
|
||||
}
|
||||
|
||||
hasErrors(): boolean {
|
||||
return this.errors.length > 0;
|
||||
}
|
||||
|
||||
getErrors(): Error[] {
|
||||
return [...this.errors];
|
||||
}
|
||||
|
||||
throw(): never {
|
||||
if (this.errors.length === 1) {
|
||||
throw this.errors[0];
|
||||
}
|
||||
throw new AggregateError(
|
||||
this.errors,
|
||||
`${this.errors.length} errors occurred`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Usage: Validate multiple fields
|
||||
function validateUser(data: any): User {
|
||||
const errors = new ErrorCollector();
|
||||
|
||||
if (!data.email) {
|
||||
errors.add(new ValidationError("Email is required"));
|
||||
} else if (!isValidEmail(data.email)) {
|
||||
errors.add(new ValidationError("Email is invalid"));
|
||||
}
|
||||
|
||||
if (!data.name || data.name.length < 2) {
|
||||
errors.add(new ValidationError("Name must be at least 2 characters"));
|
||||
}
|
||||
|
||||
if (!data.age || data.age < 18) {
|
||||
errors.add(new ValidationError("Age must be 18 or older"));
|
||||
}
|
||||
|
||||
if (errors.hasErrors()) {
|
||||
errors.throw();
|
||||
}
|
||||
|
||||
return data as User;
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Graceful Degradation
|
||||
|
||||
Provide fallback functionality when errors occur.
|
||||
|
||||
```python
|
||||
from typing import Optional, Callable, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def with_fallback(
|
||||
primary: Callable[[], T],
|
||||
fallback: Callable[[], T],
|
||||
log_error: bool = True
|
||||
) -> T:
|
||||
"""Try primary function, fall back to fallback on error."""
|
||||
try:
|
||||
return primary()
|
||||
except Exception as e:
|
||||
if log_error:
|
||||
logger.error(f"Primary function failed: {e}")
|
||||
return fallback()
|
||||
|
||||
# Usage
|
||||
def get_user_profile(user_id: str) -> UserProfile:
|
||||
return with_fallback(
|
||||
primary=lambda: fetch_from_cache(user_id),
|
||||
fallback=lambda: fetch_from_database(user_id)
|
||||
)
|
||||
|
||||
# Multiple fallbacks
|
||||
def get_exchange_rate(currency: str) -> float:
|
||||
return (
|
||||
try_function(lambda: api_provider_1.get_rate(currency))
|
||||
or try_function(lambda: api_provider_2.get_rate(currency))
|
||||
or try_function(lambda: cache.get_rate(currency))
|
||||
or DEFAULT_RATE
|
||||
)
|
||||
|
||||
def try_function(func: Callable[[], Optional[T]]) -> Optional[T]:
|
||||
try:
|
||||
return func()
|
||||
except Exception:
|
||||
return None
|
||||
```
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
name: find-skills
|
||||
description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
|
||||
---
|
||||
|
||||
# Find Skills
|
||||
|
||||
This skill helps you discover and install skills from the open agent skills ecosystem.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when the user:
|
||||
|
||||
- Asks "how do I do X" where X might be a common task with an existing skill
|
||||
- Says "find a skill for X" or "is there a skill for X"
|
||||
- Asks "can you do X" where X is a specialized capability
|
||||
- Expresses interest in extending agent capabilities
|
||||
- Wants to search for tools, templates, or workflows
|
||||
- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
|
||||
|
||||
## What is the Skills CLI?
|
||||
|
||||
The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
|
||||
|
||||
**Key commands:**
|
||||
|
||||
- `npx skills find [query] [--owner <owner>]` - Search for skills interactively or by keyword, optionally scoped to a GitHub owner
|
||||
- `npx skills add <package>` - Install a skill from GitHub or other sources
|
||||
- `npx skills update` - Update all installed skills
|
||||
|
||||
**Browse skills at:** https://skills.sh/
|
||||
|
||||
## How to Help Users Find Skills
|
||||
|
||||
### Step 1: Understand What They Need
|
||||
|
||||
When a user asks for help with something, identify:
|
||||
|
||||
1. The domain (e.g., React, testing, design, deployment)
|
||||
2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
|
||||
3. Whether this is a common enough task that a skill likely exists
|
||||
|
||||
### Step 2: Check the Leaderboard First
|
||||
|
||||
Before running a CLI search, check the [skills.sh leaderboard](https://skills.sh/) to see if a well-known skill already exists for the domain. The leaderboard ranks skills by total installs, surfacing the most popular and battle-tested options.
|
||||
|
||||
For example, top skills for web development include:
|
||||
- `vercel-labs/agent-skills` — React, Next.js, web design (100K+ installs each)
|
||||
- `anthropics/skills` — Frontend design, document processing (100K+ installs)
|
||||
|
||||
### Step 3: Search for Skills
|
||||
|
||||
If the leaderboard doesn't cover the user's need, run the find command:
|
||||
|
||||
```bash
|
||||
npx skills find [query] [--owner <owner>]
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
- User asks "how do I make my React app faster?" → `npx skills find react performance`
|
||||
- User asks "can you help me with PR reviews?" → `npx skills find pr review`
|
||||
- User asks "I need to create a changelog" → `npx skills find changelog`
|
||||
|
||||
### Step 4: Verify Quality Before Recommending
|
||||
|
||||
**Do not recommend a skill based solely on search results.** Always verify:
|
||||
|
||||
1. **Install count** — Prefer skills with 1K+ installs. Be cautious with anything under 100.
|
||||
2. **Source reputation** — Official sources (`vercel-labs`, `anthropics`, `microsoft`) are more trustworthy than unknown authors.
|
||||
3. **GitHub stars** — Check the source repository. A skill from a repo with <100 stars should be treated with skepticism.
|
||||
|
||||
### Step 5: Present Options to the User
|
||||
|
||||
When you find relevant skills, present them to the user with:
|
||||
|
||||
1. The skill name and what it does
|
||||
2. The install count and source
|
||||
3. The install command they can run
|
||||
4. A link to learn more at skills.sh
|
||||
|
||||
Example response:
|
||||
|
||||
```
|
||||
I found a skill that might help! The "react-best-practices" skill provides
|
||||
React and Next.js performance optimization guidelines from Vercel Engineering.
|
||||
(185K installs)
|
||||
|
||||
To install it:
|
||||
npx skills add vercel-labs/agent-skills@react-best-practices
|
||||
|
||||
Learn more: https://skills.sh/vercel-labs/agent-skills/react-best-practices
|
||||
```
|
||||
|
||||
### Step 6: Offer to Install
|
||||
|
||||
If the user wants to proceed, you can install the skill for them:
|
||||
|
||||
```bash
|
||||
npx skills add <owner/repo@skill> -g -y
|
||||
```
|
||||
|
||||
The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
|
||||
|
||||
## Common Skill Categories
|
||||
|
||||
When searching, consider these common categories:
|
||||
|
||||
| Category | Example Queries |
|
||||
| --------------- | ---------------------------------------- |
|
||||
| Web Development | react, nextjs, typescript, css, tailwind |
|
||||
| Testing | testing, jest, playwright, e2e |
|
||||
| DevOps | deploy, docker, kubernetes, ci-cd |
|
||||
| Documentation | docs, readme, changelog, api-docs |
|
||||
| Code Quality | review, lint, refactor, best-practices |
|
||||
| Design | ui, ux, design-system, accessibility |
|
||||
| Productivity | workflow, automation, git |
|
||||
|
||||
## Tips for Effective Searches
|
||||
|
||||
1. **Use specific keywords**: "react testing" is better than just "testing"
|
||||
2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
|
||||
3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
|
||||
|
||||
## When No Skills Are Found
|
||||
|
||||
If no relevant skills exist:
|
||||
|
||||
1. Acknowledge that no existing skill was found
|
||||
2. Offer to help with the task directly using your general capabilities
|
||||
3. Suggest the user could create their own skill with `npx skills init`
|
||||
|
||||
Example:
|
||||
|
||||
```
|
||||
I searched for skills related to "xyz" but didn't find any matches.
|
||||
I can still help you with this task directly! Would you like me to proceed?
|
||||
|
||||
If this is something you do often, you could create your own skill:
|
||||
npx skills init my-xyz-skill
|
||||
```
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
name: git-advanced-workflows
|
||||
description: Master advanced Git workflows including rebasing, cherry-picking, bisect, worktrees, and reflog to maintain clean history and recover from any situation. Use when managing complex Git histories, collaborating on feature branches, or troubleshooting repository issues.
|
||||
---
|
||||
|
||||
# Git Advanced Workflows
|
||||
|
||||
Master advanced Git techniques to maintain clean history, collaborate effectively, and recover from any situation with confidence.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Cleaning up commit history before merging
|
||||
- Applying specific commits across branches
|
||||
- Finding commits that introduced bugs
|
||||
- Working on multiple features simultaneously
|
||||
- Recovering from Git mistakes or lost commits
|
||||
- Managing complex branch workflows
|
||||
- Preparing clean PRs for review
|
||||
- Synchronizing diverged branches
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Interactive Rebase
|
||||
|
||||
Interactive rebase is the Swiss Army knife of Git history editing.
|
||||
|
||||
**Common Operations:**
|
||||
|
||||
- `pick`: Keep commit as-is
|
||||
- `reword`: Change commit message
|
||||
- `edit`: Amend commit content
|
||||
- `squash`: Combine with previous commit
|
||||
- `fixup`: Like squash but discard message
|
||||
- `drop`: Remove commit entirely
|
||||
|
||||
**Basic Usage:**
|
||||
|
||||
```bash
|
||||
# Rebase last 5 commits
|
||||
git rebase -i HEAD~5
|
||||
|
||||
# Rebase all commits on current branch
|
||||
git rebase -i $(git merge-base HEAD main)
|
||||
|
||||
# Rebase onto specific commit
|
||||
git rebase -i abc123
|
||||
```
|
||||
|
||||
### 2. Cherry-Picking
|
||||
|
||||
Apply specific commits from one branch to another without merging entire branches.
|
||||
|
||||
```bash
|
||||
# Cherry-pick single commit
|
||||
git cherry-pick abc123
|
||||
|
||||
# Cherry-pick range of commits (exclusive start)
|
||||
git cherry-pick abc123..def456
|
||||
|
||||
# Cherry-pick without committing (stage changes only)
|
||||
git cherry-pick -n abc123
|
||||
|
||||
# Cherry-pick and edit commit message
|
||||
git cherry-pick -e abc123
|
||||
```
|
||||
|
||||
### 3. Git Bisect
|
||||
|
||||
Binary search through commit history to find the commit that introduced a bug.
|
||||
|
||||
```bash
|
||||
# Start bisect
|
||||
git bisect start
|
||||
|
||||
# Mark current commit as bad
|
||||
git bisect bad
|
||||
|
||||
# Mark known good commit
|
||||
git bisect good v1.0.0
|
||||
|
||||
# Git will checkout middle commit - test it
|
||||
# Then mark as good or bad
|
||||
git bisect good # or: git bisect bad
|
||||
|
||||
# Continue until bug found
|
||||
# When done
|
||||
git bisect reset
|
||||
```
|
||||
|
||||
**Automated Bisect:**
|
||||
|
||||
```bash
|
||||
# Use script to test automatically
|
||||
git bisect start HEAD v1.0.0
|
||||
git bisect run ./test.sh
|
||||
|
||||
# test.sh should exit 0 for good, 1-127 (except 125) for bad
|
||||
```
|
||||
|
||||
### 4. Worktrees
|
||||
|
||||
Work on multiple branches simultaneously without stashing or switching.
|
||||
|
||||
```bash
|
||||
# List existing worktrees
|
||||
git worktree list
|
||||
|
||||
# Add new worktree for feature branch
|
||||
git worktree add ../project-feature feature/new-feature
|
||||
|
||||
# Add worktree and create new branch
|
||||
git worktree add -b bugfix/urgent ../project-hotfix main
|
||||
|
||||
# Remove worktree
|
||||
git worktree remove ../project-feature
|
||||
|
||||
# Prune stale worktrees
|
||||
git worktree prune
|
||||
```
|
||||
|
||||
### 5. Reflog
|
||||
|
||||
Your safety net - tracks all ref movements, even deleted commits.
|
||||
|
||||
```bash
|
||||
# View reflog
|
||||
git reflog
|
||||
|
||||
# View reflog for specific branch
|
||||
git reflog show feature/branch
|
||||
|
||||
# Restore deleted commit
|
||||
git reflog
|
||||
# Find commit hash
|
||||
git checkout abc123
|
||||
git branch recovered-branch
|
||||
|
||||
# Restore deleted branch
|
||||
git reflog
|
||||
git branch deleted-branch abc123
|
||||
```
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Use --force-with-lease**: Safer than --force, prevents overwriting others' work
|
||||
2. **Rebase Only Local Commits**: Don't rebase commits that have been pushed and shared
|
||||
3. **Descriptive Commit Messages**: Future you will thank present you
|
||||
4. **Atomic Commits**: Each commit should be a single logical change
|
||||
5. **Test Before Force Push**: Ensure history rewrite didn't break anything
|
||||
6. **Keep Reflog Aware**: Reflog is your safety net, but entries for commits not reachable from the current tip expire after 30 days by default (`gc.reflogExpireUnreachable`), and reachable entries after 90 days (`gc.reflogExpire`)
|
||||
7. **Branch Before Risky Operations**: Create backup branch before complex rebases
|
||||
|
||||
```bash
|
||||
# Safe force push
|
||||
git push --force-with-lease origin feature/branch
|
||||
|
||||
# Create backup before risky operation
|
||||
git branch backup-branch
|
||||
git rebase -i main
|
||||
# If something goes wrong
|
||||
git reset --hard backup-branch
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Rebasing Public Branches**: Causes history conflicts for collaborators
|
||||
- **Force Pushing Without Lease**: Can overwrite teammate's work
|
||||
- **Losing Work in Rebase**: Resolve conflicts carefully, test after rebase
|
||||
- **Forgetting Worktree Cleanup**: Orphaned worktrees consume disk space
|
||||
- **Not Backing Up Before Experiment**: Always create safety branch
|
||||
- **Bisect on Dirty Working Directory**: Commit or stash before bisecting
|
||||
|
||||
## Recovery Commands
|
||||
|
||||
```bash
|
||||
# Abort operations in progress
|
||||
git rebase --abort
|
||||
git merge --abort
|
||||
git cherry-pick --abort
|
||||
git bisect reset
|
||||
|
||||
# Restore file to version from specific commit
|
||||
git restore --source=abc123 path/to/file
|
||||
|
||||
# Undo last commit but keep changes
|
||||
git reset --soft HEAD^
|
||||
|
||||
# Undo last commit and discard changes
|
||||
# WARNING: --hard also deletes uncommitted changes (staged and unstaged),
|
||||
# and the reflog cannot recover them. Check `git status` and stash first.
|
||||
git reset --hard HEAD^
|
||||
|
||||
# Recover a deleted branch soon. By default, git gc can prune HEAD's reflog
|
||||
# entries for unmerged commits once they're 30 days old, counted from when
|
||||
# each entry was recorded, not from the deletion
|
||||
# Deleting a branch also deletes its own reflog; search HEAD's reflog
|
||||
git reflog
|
||||
git branch recovered-branch abc123
|
||||
```
|
||||
@@ -0,0 +1,203 @@
|
||||
# git-advanced-workflows — detailed patterns and worked examples
|
||||
|
||||
## Practical Workflows
|
||||
|
||||
### Workflow 1: Clean Up Feature Branch Before PR
|
||||
|
||||
```bash
|
||||
# Start with feature branch
|
||||
git checkout feature/user-auth
|
||||
|
||||
# Interactive rebase to clean history
|
||||
git rebase -i main
|
||||
|
||||
# Example rebase operations:
|
||||
# - Squash "fix typo" commits
|
||||
# - Reword commit messages for clarity
|
||||
# - Reorder commits logically
|
||||
# - Drop unnecessary commits
|
||||
|
||||
# Force push cleaned branch (safe if no one else is using it)
|
||||
git push --force-with-lease origin feature/user-auth
|
||||
```
|
||||
|
||||
### Workflow 2: Apply Hotfix to Multiple Releases
|
||||
|
||||
```bash
|
||||
# Create fix on main
|
||||
git checkout main
|
||||
git commit -m "fix: critical security patch"
|
||||
|
||||
# Apply to release branches
|
||||
git checkout release/2.0
|
||||
git cherry-pick abc123
|
||||
|
||||
git checkout release/1.9
|
||||
git cherry-pick abc123
|
||||
|
||||
# Handle conflicts if they arise
|
||||
git cherry-pick --continue
|
||||
# or
|
||||
git cherry-pick --abort
|
||||
```
|
||||
|
||||
### Workflow 3: Find Bug Introduction
|
||||
|
||||
```bash
|
||||
# Start bisect
|
||||
git bisect start
|
||||
git bisect bad HEAD
|
||||
git bisect good v2.1.0
|
||||
|
||||
# Git checks out middle commit - run tests
|
||||
npm test
|
||||
|
||||
# If tests fail
|
||||
git bisect bad
|
||||
|
||||
# If tests pass
|
||||
git bisect good
|
||||
|
||||
# Git will automatically checkout next commit to test
|
||||
# Repeat until bug found
|
||||
|
||||
# Automated version
|
||||
git bisect start HEAD v2.1.0
|
||||
git bisect run npm test
|
||||
```
|
||||
|
||||
### Workflow 4: Multi-Branch Development
|
||||
|
||||
```bash
|
||||
# Main project directory
|
||||
cd ~/projects/myapp
|
||||
|
||||
# Create worktree for urgent bugfix
|
||||
git worktree add ../myapp-hotfix hotfix/critical-bug
|
||||
|
||||
# Work on hotfix in separate directory
|
||||
cd ../myapp-hotfix
|
||||
# Make changes, commit
|
||||
git commit -m "fix: resolve critical bug"
|
||||
git push origin hotfix/critical-bug
|
||||
|
||||
# Return to main work without interruption
|
||||
cd ~/projects/myapp
|
||||
git fetch origin
|
||||
git cherry-pick hotfix/critical-bug
|
||||
|
||||
# Clean up when done
|
||||
git worktree remove ../myapp-hotfix
|
||||
```
|
||||
|
||||
### Workflow 5: Recover from Mistakes
|
||||
|
||||
```bash
|
||||
# Accidentally reset to wrong commit
|
||||
git reset --hard HEAD~5 # Oh no!
|
||||
|
||||
# Use reflog to find lost commits
|
||||
git reflog
|
||||
# Output shows:
|
||||
# abc123 HEAD@{0}: reset: moving to HEAD~5
|
||||
# def456 HEAD@{1}: commit: my important changes
|
||||
|
||||
# Recover lost commits
|
||||
git reset --hard def456
|
||||
|
||||
# Or create branch from lost commit
|
||||
git branch recovery def456
|
||||
```
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Rebase vs Merge Strategy
|
||||
|
||||
**When to Rebase:**
|
||||
|
||||
- Cleaning up local commits before pushing
|
||||
- Keeping feature branch up-to-date with main
|
||||
- Creating linear history for easier review
|
||||
|
||||
**When to Merge:**
|
||||
|
||||
- Integrating completed features into main
|
||||
- Preserving exact history of collaboration
|
||||
- Public branches used by others
|
||||
|
||||
```bash
|
||||
# Update feature branch with main changes (rebase)
|
||||
git checkout feature/my-feature
|
||||
git fetch origin
|
||||
git rebase origin/main
|
||||
|
||||
# Handle conflicts
|
||||
git status
|
||||
# Fix conflicts in files
|
||||
git add .
|
||||
git rebase --continue
|
||||
|
||||
# Or merge instead
|
||||
git merge origin/main
|
||||
```
|
||||
|
||||
### Autosquash Workflow
|
||||
|
||||
Automatically squash fixup commits during rebase.
|
||||
|
||||
```bash
|
||||
# Make initial commit
|
||||
git commit -m "feat: add user authentication"
|
||||
|
||||
# Later, fix something in that commit
|
||||
# Stage changes
|
||||
git commit --fixup HEAD # or specify commit hash
|
||||
|
||||
# Make more changes
|
||||
git commit --fixup abc123
|
||||
|
||||
# Rebase with autosquash
|
||||
git rebase -i --autosquash main
|
||||
|
||||
# Git automatically marks fixup commits
|
||||
```
|
||||
|
||||
### Split Commit
|
||||
|
||||
Break one commit into multiple logical commits.
|
||||
|
||||
```bash
|
||||
# Start interactive rebase
|
||||
git rebase -i HEAD~3
|
||||
|
||||
# Mark commit to split with 'edit'
|
||||
# Git will stop at that commit
|
||||
|
||||
# Reset commit but keep changes
|
||||
git reset HEAD^
|
||||
|
||||
# Stage and commit in logical chunks
|
||||
git add file1.py
|
||||
git commit -m "feat: add validation"
|
||||
|
||||
git add file2.py
|
||||
git commit -m "feat: add error handling"
|
||||
|
||||
# Continue rebase
|
||||
git rebase --continue
|
||||
```
|
||||
|
||||
### Partial Cherry-Pick
|
||||
|
||||
Cherry-pick only specific files from a commit.
|
||||
|
||||
```bash
|
||||
# Show files in commit
|
||||
git show --name-only abc123
|
||||
|
||||
# Checkout specific files from commit
|
||||
git checkout abc123 -- path/to/file1.py path/to/file2.py
|
||||
|
||||
# Stage and commit
|
||||
git commit -m "cherry-pick: apply specific changes from abc123"
|
||||
```
|
||||
@@ -0,0 +1,322 @@
|
||||
---
|
||||
name: github-actions-templates
|
||||
description: Create production-ready GitHub Actions workflows for automated testing, building, and deploying applications. Use when setting up CI/CD with GitHub Actions, automating development workflows, or creating reusable workflow templates.
|
||||
---
|
||||
|
||||
# GitHub Actions Templates
|
||||
|
||||
Production-ready GitHub Actions workflow patterns for testing, building, and deploying applications.
|
||||
|
||||
## Purpose
|
||||
|
||||
Create efficient, secure GitHub Actions workflows for continuous integration and deployment across various tech stacks.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Automate testing and deployment
|
||||
- Build Docker images and push to registries
|
||||
- Deploy to Kubernetes clusters
|
||||
- Run security scans
|
||||
- Implement matrix builds for multiple environments
|
||||
|
||||
## Common Workflow Patterns
|
||||
|
||||
### Pattern 1: Test Workflow
|
||||
|
||||
```yaml
|
||||
name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, develop]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: "npm"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run linter
|
||||
run: npm run lint
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage/lcov.info
|
||||
```
|
||||
|
||||
### Pattern 2: Build and Push Docker Image
|
||||
|
||||
```yaml
|
||||
name: Build and Push
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ["v*"]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Log in to Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
```
|
||||
|
||||
### Pattern 3: Deploy to Kubernetes
|
||||
|
||||
```yaml
|
||||
name: Deploy to Kubernetes
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Configure AWS credentials
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Update kubeconfig
|
||||
run: |
|
||||
aws eks update-kubeconfig --name production-cluster --region us-west-2
|
||||
|
||||
- name: Deploy to Kubernetes
|
||||
run: |
|
||||
kubectl apply -f k8s/
|
||||
kubectl rollout status deployment/my-app -n production
|
||||
kubectl get services -n production
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
kubectl get pods -n production
|
||||
kubectl describe deployment my-app -n production
|
||||
```
|
||||
|
||||
### Pattern 4: Matrix Build
|
||||
|
||||
```yaml
|
||||
name: Matrix Build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run tests
|
||||
run: pytest
|
||||
```
|
||||
|
||||
## Workflow Best Practices
|
||||
|
||||
1. **Use specific action versions** (@v4, not @latest)
|
||||
2. **Cache dependencies** to speed up builds
|
||||
3. **Use secrets** for sensitive data
|
||||
4. **Implement status checks** on PRs
|
||||
5. **Use matrix builds** for multi-version testing
|
||||
6. **Set appropriate permissions**
|
||||
7. **Use reusable workflows** for common patterns
|
||||
8. **Implement approval gates** for production
|
||||
9. **Add notification steps** for failures
|
||||
10. **Use self-hosted runners** for sensitive workloads
|
||||
|
||||
## Reusable Workflows
|
||||
|
||||
```yaml
|
||||
# .github/workflows/reusable-test.yml
|
||||
name: Reusable Test Workflow
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
node-version:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
NPM_TOKEN:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
- run: npm ci
|
||||
- run: npm test
|
||||
```
|
||||
|
||||
**Use reusable workflow:**
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
call-test:
|
||||
uses: ./.github/workflows/reusable-test.yml
|
||||
with:
|
||||
node-version: "20.x"
|
||||
secrets:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
```
|
||||
|
||||
## Security Scanning
|
||||
|
||||
```yaml
|
||||
name: Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Trivy vulnerability scanner
|
||||
uses: aquasecurity/trivy-action@0.28.0
|
||||
with:
|
||||
scan-type: "fs"
|
||||
scan-ref: "."
|
||||
format: "sarif"
|
||||
output: "trivy-results.sarif"
|
||||
|
||||
- name: Upload Trivy results to GitHub Security
|
||||
uses: github/codeql-action/upload-sarif@v3
|
||||
with:
|
||||
sarif_file: "trivy-results.sarif"
|
||||
|
||||
- name: Run Snyk Security Scan
|
||||
uses: snyk/actions/node@0.4.0
|
||||
env:
|
||||
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
|
||||
```
|
||||
|
||||
## Deployment with Approvals
|
||||
|
||||
```yaml
|
||||
name: Deploy to Production
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["v*"]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: production
|
||||
url: https://app.example.com
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy application
|
||||
run: |
|
||||
echo "Deploying to production..."
|
||||
# Deployment commands here
|
||||
|
||||
- name: Notify Slack
|
||||
if: success()
|
||||
uses: slackapi/slack-github-action@v1
|
||||
with:
|
||||
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
|
||||
payload: |
|
||||
{
|
||||
"text": "Deployment to production completed successfully!"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## Related Skills
|
||||
|
||||
- `gitlab-ci-patterns` - For GitLab CI workflows
|
||||
- `deployment-pipeline-design` - For pipeline architecture
|
||||
- `secrets-management` - For secrets handling
|
||||
@@ -0,0 +1,537 @@
|
||||
---
|
||||
name: javascript-testing-patterns
|
||||
description: Implement comprehensive testing strategies using Jest, Vitest, and Testing Library for unit tests, integration tests, and end-to-end testing with mocking, fixtures, and test-driven development. Use when writing JavaScript/TypeScript tests, setting up test infrastructure, or implementing TDD/BDD workflows.
|
||||
---
|
||||
|
||||
# JavaScript Testing Patterns
|
||||
|
||||
Comprehensive guide for implementing robust testing strategies in JavaScript/TypeScript applications using modern testing frameworks and best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Setting up test infrastructure for new projects
|
||||
- Writing unit tests for functions and classes
|
||||
- Creating integration tests for APIs and services
|
||||
- Implementing end-to-end tests for user flows
|
||||
- Mocking external dependencies and APIs
|
||||
- Testing React, Vue, or other frontend components
|
||||
- Implementing test-driven development (TDD)
|
||||
- Setting up continuous testing in CI/CD pipelines
|
||||
|
||||
## Testing Frameworks
|
||||
|
||||
### Jest - Full-Featured Testing Framework
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// jest.config.ts
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
|
||||
collectCoverageFrom: [
|
||||
"src/**/*.ts",
|
||||
"!src/**/*.d.ts",
|
||||
"!src/**/*.interface.ts",
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 80,
|
||||
lines: 80,
|
||||
statements: 80,
|
||||
},
|
||||
},
|
||||
setupFilesAfterEnv: ["<rootDir>/src/test/setup.ts"],
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Vitest - Fast, Vite-Native Testing
|
||||
|
||||
**Setup:**
|
||||
|
||||
```typescript
|
||||
// vitest.config.ts
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
globals: true,
|
||||
environment: "node",
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["text", "json", "html"],
|
||||
exclude: ["**/*.d.ts", "**/*.config.ts", "**/dist/**"],
|
||||
},
|
||||
setupFiles: ["./src/test/setup.ts"],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Unit Testing Patterns
|
||||
|
||||
### Pattern 1: Testing Pure Functions
|
||||
|
||||
```typescript
|
||||
// utils/calculator.ts
|
||||
export function add(a: number, b: number): number {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
export function divide(a: number, b: number): number {
|
||||
if (b === 0) {
|
||||
throw new Error("Division by zero");
|
||||
}
|
||||
return a / b;
|
||||
}
|
||||
|
||||
// utils/calculator.test.ts
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { add, divide } from "./calculator";
|
||||
|
||||
describe("Calculator", () => {
|
||||
describe("add", () => {
|
||||
it("should add two positive numbers", () => {
|
||||
expect(add(2, 3)).toBe(5);
|
||||
});
|
||||
|
||||
it("should add negative numbers", () => {
|
||||
expect(add(-2, -3)).toBe(-5);
|
||||
});
|
||||
|
||||
it("should handle zero", () => {
|
||||
expect(add(0, 5)).toBe(5);
|
||||
expect(add(5, 0)).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("divide", () => {
|
||||
it("should divide two numbers", () => {
|
||||
expect(divide(10, 2)).toBe(5);
|
||||
});
|
||||
|
||||
it("should handle decimal results", () => {
|
||||
expect(divide(5, 2)).toBe(2.5);
|
||||
});
|
||||
|
||||
it("should throw error when dividing by zero", () => {
|
||||
expect(() => divide(10, 0)).toThrow("Division by zero");
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Testing Classes
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
export class UserService {
|
||||
private users: Map<string, User> = new Map();
|
||||
|
||||
create(user: User): User {
|
||||
if (this.users.has(user.id)) {
|
||||
throw new Error("User already exists");
|
||||
}
|
||||
this.users.set(user.id, user);
|
||||
return user;
|
||||
}
|
||||
|
||||
findById(id: string): User | undefined {
|
||||
return this.users.get(id);
|
||||
}
|
||||
|
||||
update(id: string, updates: Partial<User>): User {
|
||||
const user = this.users.get(id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
const updated = { ...user, ...updates };
|
||||
this.users.set(id, updated);
|
||||
return updated;
|
||||
}
|
||||
|
||||
delete(id: string): boolean {
|
||||
return this.users.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { UserService } from "./user.service";
|
||||
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new UserService();
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a new user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
const created = service.create(user);
|
||||
|
||||
expect(created).toEqual(user);
|
||||
expect(service.findById("1")).toEqual(user);
|
||||
});
|
||||
|
||||
it("should throw error if user already exists", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
expect(() => service.create(user)).toThrow("User already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
it("should update existing user", () => {
|
||||
const user = { id: "1", name: "John", email: "john@example.com" };
|
||||
service.create(user);
|
||||
|
||||
const updated = service.update("1", { name: "Jane" });
|
||||
|
||||
expect(updated.name).toBe("Jane");
|
||||
expect(updated.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", () => {
|
||||
expect(() => service.update("999", { name: "Jane" })).toThrow(
|
||||
"User not found",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: Testing Async Functions
|
||||
|
||||
```typescript
|
||||
// services/api.service.ts
|
||||
export class ApiService {
|
||||
async fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`https://api.example.com/users/${id}`);
|
||||
if (!response.ok) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async createUser(user: CreateUserDTO): Promise<User> {
|
||||
const response = await fetch("https://api.example.com/users", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(user),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// services/api.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { ApiService } from "./api.service";
|
||||
|
||||
// Mock fetch globally
|
||||
global.fetch = vi.fn();
|
||||
|
||||
describe("ApiService", () => {
|
||||
let service: ApiService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new ApiService();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("fetchUser", () => {
|
||||
it("should fetch user successfully", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => mockUser,
|
||||
});
|
||||
|
||||
const user = await service.fetchUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(fetch).toHaveBeenCalledWith("https://api.example.com/users/1");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", async () => {
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: false,
|
||||
});
|
||||
|
||||
await expect(service.fetchUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const newUser = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...newUser };
|
||||
|
||||
(fetch as any).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => createdUser,
|
||||
});
|
||||
|
||||
const user = await service.createUser(newUser);
|
||||
|
||||
expect(user).toEqual(createdUser);
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"https://api.example.com/users",
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify(newUser),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Mocking Patterns
|
||||
|
||||
### Pattern 1: Mocking Modules
|
||||
|
||||
```typescript
|
||||
// services/email.service.ts
|
||||
import nodemailer from "nodemailer";
|
||||
|
||||
export class EmailService {
|
||||
private transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASS,
|
||||
},
|
||||
});
|
||||
|
||||
async sendEmail(to: string, subject: string, html: string) {
|
||||
await this.transporter.sendMail({
|
||||
from: process.env.EMAIL_FROM,
|
||||
to,
|
||||
subject,
|
||||
html,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// services/email.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EmailService } from "./email.service";
|
||||
|
||||
vi.mock("nodemailer", () => ({
|
||||
default: {
|
||||
createTransport: vi.fn(() => ({
|
||||
sendMail: vi.fn().mockResolvedValue({ messageId: "123" }),
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("EmailService", () => {
|
||||
let service: EmailService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new EmailService();
|
||||
});
|
||||
|
||||
it("should send email successfully", async () => {
|
||||
await service.sendEmail(
|
||||
"test@example.com",
|
||||
"Test Subject",
|
||||
"<p>Test Body</p>",
|
||||
);
|
||||
|
||||
expect(service["transporter"].sendMail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "test@example.com",
|
||||
subject: "Test Subject",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Dependency Injection for Testing
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
export interface IUserRepository {
|
||||
findById(id: string): Promise<User | null>;
|
||||
create(user: User): Promise<User>;
|
||||
}
|
||||
|
||||
export class UserService {
|
||||
constructor(private userRepository: IUserRepository) {}
|
||||
|
||||
async getUser(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async createUser(userData: CreateUserDTO): Promise<User> {
|
||||
// Business logic here
|
||||
const user = { id: generateId(), ...userData };
|
||||
return this.userRepository.create(user);
|
||||
}
|
||||
}
|
||||
|
||||
// services/user.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { UserService, IUserRepository } from "./user.service";
|
||||
|
||||
describe("UserService", () => {
|
||||
let service: UserService;
|
||||
let mockRepository: IUserRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = {
|
||||
findById: vi.fn(),
|
||||
create: vi.fn(),
|
||||
};
|
||||
service = new UserService(mockRepository);
|
||||
});
|
||||
|
||||
describe("getUser", () => {
|
||||
it("should return user if found", async () => {
|
||||
const mockUser = { id: "1", name: "John", email: "john@example.com" };
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(mockUser);
|
||||
|
||||
const user = await service.getUser("1");
|
||||
|
||||
expect(user).toEqual(mockUser);
|
||||
expect(mockRepository.findById).toHaveBeenCalledWith("1");
|
||||
});
|
||||
|
||||
it("should throw error if user not found", async () => {
|
||||
vi.mocked(mockRepository.findById).mockResolvedValue(null);
|
||||
|
||||
await expect(service.getUser("999")).rejects.toThrow("User not found");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", async () => {
|
||||
const userData = { name: "John", email: "john@example.com" };
|
||||
const createdUser = { id: "1", ...userData };
|
||||
|
||||
vi.mocked(mockRepository.create).mockResolvedValue(createdUser);
|
||||
|
||||
const user = await service.createUser(userData);
|
||||
|
||||
expect(user).toEqual(createdUser);
|
||||
expect(mockRepository.create).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 3: Spying on Functions
|
||||
|
||||
```typescript
|
||||
// utils/logger.ts
|
||||
export const logger = {
|
||||
info: (message: string) => console.log(`INFO: ${message}`),
|
||||
error: (message: string) => console.error(`ERROR: ${message}`),
|
||||
};
|
||||
|
||||
// services/order.service.ts
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
export class OrderService {
|
||||
async processOrder(orderId: string): Promise<void> {
|
||||
logger.info(`Processing order ${orderId}`);
|
||||
// Process order logic
|
||||
logger.info(`Order ${orderId} processed successfully`);
|
||||
}
|
||||
}
|
||||
|
||||
// services/order.service.test.ts
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { OrderService } from "./order.service";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
describe("OrderService", () => {
|
||||
let service: OrderService;
|
||||
let loggerSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new OrderService();
|
||||
loggerSpy = vi.spyOn(logger, "info");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
loggerSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should log order processing", async () => {
|
||||
await service.processOrder("123");
|
||||
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Processing order 123");
|
||||
expect(loggerSpy).toHaveBeenCalledWith("Order 123 processed successfully");
|
||||
expect(loggerSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
Integration tests verify real database operations and HTTP endpoints using `supertest` and a test database instance. Always truncate tables in `beforeEach` and tear down in `afterAll`.
|
||||
|
||||
For full API integration test examples (supertest + PostgreSQL) and database repository integration tests, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Frontend Testing with Testing Library
|
||||
|
||||
Test React components by rendering them and querying by role, placeholder, or test ID. Test hooks with `renderHook` + `act`. Prefer semantic queries (`getByRole`, `getByPlaceholderText`) over `data-testid`.
|
||||
|
||||
For complete React component test examples (UserForm, hooks with `renderHook`/`act`), see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Test Fixtures and Factories
|
||||
|
||||
Use `@faker-js/faker` to generate realistic test data factories. Factories accept optional `overrides` so tests can set only the fields they care about:
|
||||
|
||||
```typescript
|
||||
// tests/fixtures/user.fixture.ts
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export function createUserFixture(overrides?: Partial<User>): User {
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
createdAt: faker.date.past(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
For snapshot testing, coverage configuration, test organization patterns, promise testing, and timer mocking, see [references/advanced-testing-patterns.md](references/advanced-testing-patterns.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Follow AAA Pattern**: Arrange, Act, Assert
|
||||
2. **One assertion per test**: Or logically related assertions
|
||||
3. **Descriptive test names**: Should describe what is being tested
|
||||
4. **Use beforeEach/afterEach**: For setup and teardown
|
||||
5. **Mock external dependencies**: Keep tests isolated
|
||||
6. **Test edge cases**: Not just happy paths
|
||||
7. **Avoid implementation details**: Test behavior, not implementation
|
||||
8. **Use test factories**: For consistent test data
|
||||
9. **Keep tests fast**: Mock slow operations
|
||||
10. **Write tests first (TDD)**: When possible
|
||||
11. **Maintain test coverage**: Aim for 80%+ coverage
|
||||
12. **Use TypeScript**: For type-safe tests
|
||||
13. **Test error handling**: Not just success cases
|
||||
14. **Use data-testid sparingly**: Prefer semantic queries
|
||||
15. **Clean up after tests**: Prevent test pollution
|
||||
@@ -0,0 +1,513 @@
|
||||
# Advanced JavaScript Testing Patterns
|
||||
|
||||
Advanced patterns for integration testing, frontend component testing, fixtures, snapshot testing, coverage, and common test utilities.
|
||||
|
||||
## Integration Testing
|
||||
|
||||
### Pattern 1: API Integration Tests
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.api.test.ts
|
||||
import request from "supertest";
|
||||
import { app } from "../../src/app";
|
||||
import { pool } from "../../src/config/database";
|
||||
|
||||
describe("User API Integration Tests", () => {
|
||||
beforeAll(async () => {
|
||||
// Setup test database
|
||||
await pool.query("CREATE TABLE IF NOT EXISTS users (...)");
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// Clear data before each test
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
describe("POST /api/users", () => {
|
||||
it("should create a new user", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
name: userData.name,
|
||||
email: userData.email,
|
||||
});
|
||||
expect(response.body).toHaveProperty("id");
|
||||
expect(response.body).not.toHaveProperty("password");
|
||||
});
|
||||
|
||||
it("should return 400 if email is invalid", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "invalid-email",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty("error");
|
||||
});
|
||||
|
||||
it("should return 409 if email already exists", async () => {
|
||||
const userData = {
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
};
|
||||
|
||||
await request(app).post("/api/users").send(userData);
|
||||
|
||||
const response = await request(app)
|
||||
.post("/api/users")
|
||||
.send(userData)
|
||||
.expect(409);
|
||||
|
||||
expect(response.body.error).toContain("already exists");
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/users/:id", () => {
|
||||
it("should get user by id", async () => {
|
||||
const createResponse = await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const userId = createResponse.body.id;
|
||||
|
||||
const response = await request(app)
|
||||
.get(`/api/users/${userId}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
id: userId,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return 404 if user not found", async () => {
|
||||
await request(app).get("/api/users/999").expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authentication", () => {
|
||||
it("should require authentication for protected routes", async () => {
|
||||
await request(app).get("/api/users/me").expect(401);
|
||||
});
|
||||
|
||||
it("should allow access with valid token", async () => {
|
||||
// Create user and login
|
||||
await request(app).post("/api/users").send({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const loginResponse = await request(app).post("/api/auth/login").send({
|
||||
email: "john@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
|
||||
const token = loginResponse.body.token;
|
||||
|
||||
const response = await request(app)
|
||||
.get("/api/users/me")
|
||||
.set("Authorization", `Bearer ${token}`)
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.email).toBe("john@example.com");
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Database Integration Tests
|
||||
|
||||
```typescript
|
||||
// tests/integration/user.repository.test.ts
|
||||
import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "../../src/repositories/user.repository";
|
||||
|
||||
describe("UserRepository Integration Tests", () => {
|
||||
let pool: Pool;
|
||||
let repository: UserRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
pool = new Pool({
|
||||
host: "localhost",
|
||||
port: 5432,
|
||||
database: "test_db",
|
||||
user: "test_user",
|
||||
password: "test_password",
|
||||
});
|
||||
|
||||
repository = new UserRepository(pool);
|
||||
|
||||
// Create tables
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await pool.query("DROP TABLE IF EXISTS users");
|
||||
await pool.end();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await pool.query("TRUNCATE TABLE users CASCADE");
|
||||
});
|
||||
|
||||
it("should create a user", async () => {
|
||||
const user = await repository.create({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
expect(user).toHaveProperty("id");
|
||||
expect(user.name).toBe("John Doe");
|
||||
expect(user.email).toBe("john@example.com");
|
||||
});
|
||||
|
||||
it("should find user by email", async () => {
|
||||
await repository.create({
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
password: "hashed_password",
|
||||
});
|
||||
|
||||
const user = await repository.findByEmail("john@example.com");
|
||||
|
||||
expect(user).toBeTruthy();
|
||||
expect(user?.name).toBe("John Doe");
|
||||
});
|
||||
|
||||
it("should return null if user not found", async () => {
|
||||
const user = await repository.findByEmail("nonexistent@example.com");
|
||||
expect(user).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend Testing with Testing Library
|
||||
|
||||
### Pattern 1: React Component Testing
|
||||
|
||||
```typescript
|
||||
// components/UserForm.tsx
|
||||
import { useState } from 'react';
|
||||
|
||||
interface Props {
|
||||
onSubmit: (user: { name: string; email: string }) => void;
|
||||
}
|
||||
|
||||
export function UserForm({ onSubmit }: Props) {
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSubmit({ name, email });
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
data-testid="name-input"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
// components/UserForm.test.tsx
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { UserForm } from './UserForm';
|
||||
|
||||
describe('UserForm', () => {
|
||||
it('should render form inputs', () => {
|
||||
render(<UserForm onSubmit={vi.fn()} />);
|
||||
|
||||
expect(screen.getByPlaceholderText('Name')).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText('Email')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should update input values', () => {
|
||||
render(<UserForm onSubmit={vi.fn()} />);
|
||||
|
||||
const nameInput = screen.getByTestId('name-input') as HTMLInputElement;
|
||||
const emailInput = screen.getByTestId('email-input') as HTMLInputElement;
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'John Doe' } });
|
||||
fireEvent.change(emailInput, { target: { value: 'john@example.com' } });
|
||||
|
||||
expect(nameInput.value).toBe('John Doe');
|
||||
expect(emailInput.value).toBe('john@example.com');
|
||||
});
|
||||
|
||||
it('should call onSubmit with form data', () => {
|
||||
const onSubmit = vi.fn();
|
||||
render(<UserForm onSubmit={onSubmit} />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('name-input'), {
|
||||
target: { value: 'John Doe' },
|
||||
});
|
||||
fireEvent.change(screen.getByTestId('email-input'), {
|
||||
target: { value: 'john@example.com' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Submit' }));
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern 2: Testing Hooks
|
||||
|
||||
```typescript
|
||||
// hooks/useCounter.ts
|
||||
import { useState, useCallback } from "react";
|
||||
|
||||
export function useCounter(initialValue = 0) {
|
||||
const [count, setCount] = useState(initialValue);
|
||||
|
||||
const increment = useCallback(() => setCount((c) => c + 1), []);
|
||||
const decrement = useCallback(() => setCount((c) => c - 1), []);
|
||||
const reset = useCallback(() => setCount(initialValue), [initialValue]);
|
||||
|
||||
return { count, increment, decrement, reset };
|
||||
}
|
||||
|
||||
// hooks/useCounter.test.ts
|
||||
import { renderHook, act } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { useCounter } from "./useCounter";
|
||||
|
||||
describe("useCounter", () => {
|
||||
it("should initialize with default value", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
expect(result.current.count).toBe(0);
|
||||
});
|
||||
|
||||
it("should initialize with custom value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
|
||||
it("should increment count", () => {
|
||||
const { result } = renderHook(() => useCounter());
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(1);
|
||||
});
|
||||
|
||||
it("should decrement count", () => {
|
||||
const { result } = renderHook(() => useCounter(5));
|
||||
|
||||
act(() => {
|
||||
result.current.decrement();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(4);
|
||||
});
|
||||
|
||||
it("should reset to initial value", () => {
|
||||
const { result } = renderHook(() => useCounter(10));
|
||||
|
||||
act(() => {
|
||||
result.current.increment();
|
||||
result.current.increment();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(12);
|
||||
|
||||
act(() => {
|
||||
result.current.reset();
|
||||
});
|
||||
|
||||
expect(result.current.count).toBe(10);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Test Fixtures and Factories
|
||||
|
||||
```typescript
|
||||
// tests/fixtures/user.fixture.ts
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export function createUserFixture(overrides?: Partial<User>): User {
|
||||
return {
|
||||
id: faker.string.uuid(),
|
||||
name: faker.person.fullName(),
|
||||
email: faker.internet.email(),
|
||||
createdAt: faker.date.past(),
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function createUsersFixture(count: number): User[] {
|
||||
return Array.from({ length: count }, () => createUserFixture());
|
||||
}
|
||||
|
||||
// Usage in tests
|
||||
import {
|
||||
createUserFixture,
|
||||
createUsersFixture,
|
||||
} from "../fixtures/user.fixture";
|
||||
|
||||
describe("UserService", () => {
|
||||
it("should process user", () => {
|
||||
const user = createUserFixture({ name: "John Doe" });
|
||||
// Use user in test
|
||||
});
|
||||
|
||||
it("should handle multiple users", () => {
|
||||
const users = createUsersFixture(10);
|
||||
// Use users in test
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Snapshot Testing
|
||||
|
||||
```typescript
|
||||
// components/UserCard.test.tsx
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { UserCard } from './UserCard';
|
||||
|
||||
describe('UserCard', () => {
|
||||
it('should match snapshot', () => {
|
||||
const user = {
|
||||
id: '1',
|
||||
name: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
avatar: 'https://example.com/avatar.jpg',
|
||||
};
|
||||
|
||||
const { container } = render(<UserCard user={user} />);
|
||||
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should match snapshot with loading state', () => {
|
||||
const { container } = render(<UserCard loading />);
|
||||
expect(container.firstChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## Coverage Reports
|
||||
|
||||
```typescript
|
||||
// package.json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "vitest",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"test:ui": "vitest --ui"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Test Organization
|
||||
|
||||
```typescript
|
||||
describe("UserService", () => {
|
||||
describe("createUser", () => {
|
||||
it("should create user successfully", () => {});
|
||||
it("should throw error if email exists", () => {});
|
||||
it("should hash password", () => {});
|
||||
});
|
||||
|
||||
describe("updateUser", () => {
|
||||
it("should update user", () => {});
|
||||
it("should throw error if not found", () => {});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Promises
|
||||
|
||||
```typescript
|
||||
// Using async/await
|
||||
it("should fetch user", async () => {
|
||||
const user = await service.fetchUser("1");
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
// Testing rejections
|
||||
it("should throw error", async () => {
|
||||
await expect(service.fetchUser("invalid")).rejects.toThrow("Not found");
|
||||
});
|
||||
```
|
||||
|
||||
### Testing Timers
|
||||
|
||||
```typescript
|
||||
import { vi } from "vitest";
|
||||
|
||||
it("should call function after delay", () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const callback = vi.fn();
|
||||
setTimeout(callback, 1000);
|
||||
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(callback).toHaveBeenCalled();
|
||||
|
||||
vi.useRealTimers();
|
||||
});
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: modern-javascript-patterns
|
||||
description: Master ES6+ features including async/await, destructuring, spread operators, arrow functions, promises, modules, iterators, generators, and functional programming patterns for writing clean, efficient JavaScript code. Use when refactoring legacy code, implementing modern patterns, or optimizing JavaScript applications.
|
||||
---
|
||||
|
||||
# Modern JavaScript Patterns
|
||||
|
||||
Comprehensive guide for mastering modern JavaScript (ES6+) features, functional programming patterns, and best practices for writing clean, maintainable, and performant code.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Refactoring legacy JavaScript to modern syntax
|
||||
- Implementing functional programming patterns
|
||||
- Optimizing JavaScript performance
|
||||
- Writing maintainable and readable code
|
||||
- Working with asynchronous operations
|
||||
- Building modern web applications
|
||||
- Migrating from callbacks to Promises/async-await
|
||||
- Implementing data transformation pipelines
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use const by default**: Only use let when reassignment is needed
|
||||
2. **Prefer arrow functions**: Especially for callbacks
|
||||
3. **Use template literals**: Instead of string concatenation
|
||||
4. **Destructure objects and arrays**: For cleaner code
|
||||
5. **Use async/await**: Instead of Promise chains
|
||||
6. **Avoid mutating data**: Use spread operator and array methods
|
||||
7. **Use optional chaining**: Prevent "Cannot read property of undefined"
|
||||
8. **Use nullish coalescing**: For default values
|
||||
9. **Prefer array methods**: Over traditional loops
|
||||
10. **Use modules**: For better code organization
|
||||
11. **Write pure functions**: Easier to test and reason about
|
||||
12. **Use meaningful variable names**: Self-documenting code
|
||||
13. **Keep functions small**: Single responsibility principle
|
||||
14. **Handle errors properly**: Use try/catch with async/await
|
||||
15. **Use strict mode**: `'use strict'` for better error catching
|
||||
|
||||
For common pitfalls (this binding, promise anti-patterns, memory leaks), see [references/advanced-patterns.md](references/advanced-patterns.md).
|
||||
@@ -0,0 +1,487 @@
|
||||
# Advanced Modern JavaScript Patterns
|
||||
|
||||
Advanced patterns covering functional programming, modern class features, ES6 modules, iterators, generators, modern operators, and performance optimization.
|
||||
|
||||
## Functional Programming Patterns
|
||||
|
||||
### 1. Array Methods
|
||||
|
||||
**Map, Filter, Reduce:**
|
||||
|
||||
```javascript
|
||||
const users = [
|
||||
{ id: 1, name: "John", age: 30, active: true },
|
||||
{ id: 2, name: "Jane", age: 25, active: false },
|
||||
{ id: 3, name: "Bob", age: 35, active: true },
|
||||
];
|
||||
|
||||
// Map - Transform array
|
||||
const names = users.map((user) => user.name);
|
||||
const upperNames = users.map((user) => user.name.toUpperCase());
|
||||
|
||||
// Filter - Select elements
|
||||
const activeUsers = users.filter((user) => user.active);
|
||||
const adults = users.filter((user) => user.age >= 18);
|
||||
|
||||
// Reduce - Aggregate data
|
||||
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
|
||||
const avgAge = totalAge / users.length;
|
||||
|
||||
// Group by property
|
||||
const byActive = users.reduce((groups, user) => {
|
||||
const key = user.active ? "active" : "inactive";
|
||||
return {
|
||||
...groups,
|
||||
[key]: [...(groups[key] || []), user],
|
||||
};
|
||||
}, {});
|
||||
|
||||
// Chaining methods
|
||||
const result = users
|
||||
.filter((user) => user.active)
|
||||
.map((user) => user.name)
|
||||
.sort()
|
||||
.join(", ");
|
||||
```
|
||||
|
||||
**Advanced Array Methods:**
|
||||
|
||||
```javascript
|
||||
// Find - First matching element
|
||||
const user = users.find((u) => u.id === 2);
|
||||
|
||||
// FindIndex - Index of first match
|
||||
const index = users.findIndex((u) => u.name === "Jane");
|
||||
|
||||
// Some - At least one matches
|
||||
const hasActive = users.some((u) => u.active);
|
||||
|
||||
// Every - All match
|
||||
const allAdults = users.every((u) => u.age >= 18);
|
||||
|
||||
// FlatMap - Map and flatten
|
||||
const userTags = [
|
||||
{ name: "John", tags: ["admin", "user"] },
|
||||
{ name: "Jane", tags: ["user"] },
|
||||
];
|
||||
const allTags = userTags.flatMap((u) => u.tags);
|
||||
|
||||
// From - Create array from iterable
|
||||
const str = "hello";
|
||||
const chars = Array.from(str);
|
||||
const numbers = Array.from({ length: 5 }, (_, i) => i + 1);
|
||||
|
||||
// Of - Create array from arguments
|
||||
const arr = Array.of(1, 2, 3);
|
||||
```
|
||||
|
||||
### 2. Higher-Order Functions
|
||||
|
||||
**Functions as Arguments:**
|
||||
|
||||
```javascript
|
||||
// Custom forEach
|
||||
function forEach(array, callback) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
callback(array[i], i, array);
|
||||
}
|
||||
}
|
||||
|
||||
// Custom map
|
||||
function map(array, transform) {
|
||||
const result = [];
|
||||
for (const item of array) {
|
||||
result.push(transform(item));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Custom filter
|
||||
function filter(array, predicate) {
|
||||
const result = [];
|
||||
for (const item of array) {
|
||||
if (predicate(item)) {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
**Functions Returning Functions:**
|
||||
|
||||
```javascript
|
||||
// Currying
|
||||
const multiply = (a) => (b) => a * b;
|
||||
const double = multiply(2);
|
||||
const triple = multiply(3);
|
||||
|
||||
console.log(double(5)); // 10
|
||||
console.log(triple(5)); // 15
|
||||
|
||||
// Partial application
|
||||
function partial(fn, ...args) {
|
||||
return (...moreArgs) => fn(...args, ...moreArgs);
|
||||
}
|
||||
|
||||
const add = (a, b, c) => a + b + c;
|
||||
const add5 = partial(add, 5);
|
||||
console.log(add5(3, 2)); // 10
|
||||
|
||||
// Memoization
|
||||
function memoize(fn) {
|
||||
const cache = new Map();
|
||||
return (...args) => {
|
||||
const key = JSON.stringify(args);
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
const result = fn(...args);
|
||||
cache.set(key, result);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
const fibonacci = memoize((n) => {
|
||||
if (n <= 1) return n;
|
||||
return fibonacci(n - 1) + fibonacci(n - 2);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Composition and Piping
|
||||
|
||||
```javascript
|
||||
// Function composition
|
||||
const compose =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduceRight((acc, fn) => fn(acc), x);
|
||||
|
||||
const pipe =
|
||||
(...fns) =>
|
||||
(x) =>
|
||||
fns.reduce((acc, fn) => fn(acc), x);
|
||||
|
||||
// Example usage
|
||||
const addOne = (x) => x + 1;
|
||||
const double = (x) => x * 2;
|
||||
const square = (x) => x * x;
|
||||
|
||||
const composed = compose(square, double, addOne);
|
||||
console.log(composed(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
const piped = pipe(addOne, double, square);
|
||||
console.log(piped(3)); // ((3 + 1) * 2)^2 = 64
|
||||
|
||||
// Practical example
|
||||
const processUser = pipe(
|
||||
(user) => ({ ...user, name: user.name.trim() }),
|
||||
(user) => ({ ...user, email: user.email.toLowerCase() }),
|
||||
(user) => ({ ...user, age: parseInt(user.age) }),
|
||||
);
|
||||
|
||||
const user = processUser({
|
||||
name: " John ",
|
||||
email: "JOHN@EXAMPLE.COM",
|
||||
age: "30",
|
||||
});
|
||||
```
|
||||
|
||||
### 4. Pure Functions and Immutability
|
||||
|
||||
```javascript
|
||||
// Impure function (modifies input)
|
||||
function addItemImpure(cart, item) {
|
||||
cart.items.push(item);
|
||||
cart.total += item.price;
|
||||
return cart;
|
||||
}
|
||||
|
||||
// Pure function (no side effects)
|
||||
function addItemPure(cart, item) {
|
||||
return {
|
||||
...cart,
|
||||
items: [...cart.items, item],
|
||||
total: cart.total + item.price,
|
||||
};
|
||||
}
|
||||
|
||||
// Immutable array operations
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
|
||||
// Add to array
|
||||
const withSix = [...numbers, 6];
|
||||
|
||||
// Remove from array
|
||||
const withoutThree = numbers.filter((n) => n !== 3);
|
||||
|
||||
// Update array element
|
||||
const doubled = numbers.map((n) => (n === 3 ? n * 2 : n));
|
||||
|
||||
// Immutable object operations
|
||||
const user = { name: "John", age: 30 };
|
||||
|
||||
// Update property
|
||||
const olderUser = { ...user, age: 31 };
|
||||
|
||||
// Add property
|
||||
const withEmail = { ...user, email: "john@example.com" };
|
||||
|
||||
// Remove property
|
||||
const { age, ...withoutAge } = user;
|
||||
|
||||
// Deep cloning (simple approach)
|
||||
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
// Better deep cloning
|
||||
const structuredClone = (obj) => globalThis.structuredClone(obj);
|
||||
```
|
||||
|
||||
## Modern Class Features
|
||||
|
||||
```javascript
|
||||
// Class syntax
|
||||
class User {
|
||||
// Private fields
|
||||
#password;
|
||||
|
||||
// Public fields
|
||||
id;
|
||||
name;
|
||||
|
||||
// Static field
|
||||
static count = 0;
|
||||
|
||||
constructor(id, name, password) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.#password = password;
|
||||
User.count++;
|
||||
}
|
||||
|
||||
// Public method
|
||||
greet() {
|
||||
return `Hello, ${this.name}`;
|
||||
}
|
||||
|
||||
// Private method
|
||||
#hashPassword(password) {
|
||||
return `hashed_${password}`;
|
||||
}
|
||||
|
||||
// Getter
|
||||
get displayName() {
|
||||
return this.name.toUpperCase();
|
||||
}
|
||||
|
||||
// Setter
|
||||
set password(newPassword) {
|
||||
this.#password = this.#hashPassword(newPassword);
|
||||
}
|
||||
|
||||
// Static method
|
||||
static create(id, name, password) {
|
||||
return new User(id, name, password);
|
||||
}
|
||||
}
|
||||
|
||||
// Inheritance
|
||||
class Admin extends User {
|
||||
constructor(id, name, password, role) {
|
||||
super(id, name, password);
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
greet() {
|
||||
return `${super.greet()}, I'm an admin`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Modules (ES6)
|
||||
|
||||
```javascript
|
||||
// Exporting
|
||||
// math.js
|
||||
export const PI = 3.14159;
|
||||
export function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
export class Calculator {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Default export
|
||||
export default function multiply(a, b) {
|
||||
return a * b;
|
||||
}
|
||||
|
||||
// Importing
|
||||
// app.js
|
||||
import multiply, { PI, add, Calculator } from "./math.js";
|
||||
|
||||
// Rename imports
|
||||
import { add as sum } from "./math.js";
|
||||
|
||||
// Import all
|
||||
import * as Math from "./math.js";
|
||||
|
||||
// Dynamic imports
|
||||
const module = await import("./math.js");
|
||||
const { add } = await import("./math.js");
|
||||
|
||||
// Conditional loading
|
||||
if (condition) {
|
||||
const module = await import("./feature.js");
|
||||
module.init();
|
||||
}
|
||||
```
|
||||
|
||||
## Iterators and Generators
|
||||
|
||||
```javascript
|
||||
// Custom iterator
|
||||
const range = {
|
||||
from: 1,
|
||||
to: 5,
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return {
|
||||
current: this.from,
|
||||
last: this.to,
|
||||
|
||||
next() {
|
||||
if (this.current <= this.last) {
|
||||
return { done: false, value: this.current++ };
|
||||
} else {
|
||||
return { done: true };
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
for (const num of range) {
|
||||
console.log(num); // 1, 2, 3, 4, 5
|
||||
}
|
||||
|
||||
// Generator function
|
||||
function* rangeGenerator(from, to) {
|
||||
for (let i = from; i <= to; i++) {
|
||||
yield i;
|
||||
}
|
||||
}
|
||||
|
||||
for (const num of rangeGenerator(1, 5)) {
|
||||
console.log(num);
|
||||
}
|
||||
|
||||
// Infinite generator
|
||||
function* fibonacci() {
|
||||
let [prev, curr] = [0, 1];
|
||||
while (true) {
|
||||
yield curr;
|
||||
[prev, curr] = [curr, prev + curr];
|
||||
}
|
||||
}
|
||||
|
||||
// Async generator
|
||||
async function* fetchPages(url) {
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const response = await fetch(`${url}?page=${page}`);
|
||||
const data = await response.json();
|
||||
if (data.length === 0) break;
|
||||
yield data;
|
||||
page++;
|
||||
}
|
||||
}
|
||||
|
||||
for await (const page of fetchPages("/api/users")) {
|
||||
console.log(page);
|
||||
}
|
||||
```
|
||||
|
||||
## Modern Operators
|
||||
|
||||
```javascript
|
||||
// Optional chaining
|
||||
const user = { name: "John", address: { city: "NYC" } };
|
||||
const city = user?.address?.city;
|
||||
const zipCode = user?.address?.zipCode; // undefined
|
||||
|
||||
// Function call
|
||||
const result = obj.method?.();
|
||||
|
||||
// Array access
|
||||
const first = arr?.[0];
|
||||
|
||||
// Nullish coalescing
|
||||
const value = null ?? "default"; // 'default'
|
||||
const value = undefined ?? "default"; // 'default'
|
||||
const value = 0 ?? "default"; // 0 (not 'default')
|
||||
const value = "" ?? "default"; // '' (not 'default')
|
||||
|
||||
// Logical assignment
|
||||
let a = null;
|
||||
a ??= "default"; // a = 'default'
|
||||
|
||||
let b = 5;
|
||||
b ??= 10; // b = 5 (unchanged)
|
||||
|
||||
let obj = { count: 0 };
|
||||
obj.count ||= 1; // obj.count = 1
|
||||
obj.count &&= 2; // obj.count = 2
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
```javascript
|
||||
// Debounce
|
||||
function debounce(fn, delay) {
|
||||
let timeoutId;
|
||||
return (...args) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => fn(...args), delay);
|
||||
};
|
||||
}
|
||||
|
||||
const searchDebounced = debounce(search, 300);
|
||||
|
||||
// Throttle
|
||||
function throttle(fn, limit) {
|
||||
let inThrottle;
|
||||
return (...args) => {
|
||||
if (!inThrottle) {
|
||||
fn(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => (inThrottle = false), limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const scrollThrottled = throttle(handleScroll, 100);
|
||||
|
||||
// Lazy evaluation
|
||||
function* lazyMap(iterable, transform) {
|
||||
for (const item of iterable) {
|
||||
yield transform(item);
|
||||
}
|
||||
}
|
||||
|
||||
// Use only what you need
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
const doubled = lazyMap(numbers, (x) => x * 2);
|
||||
const first = doubled.next().value; // Only computes first value
|
||||
```
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
1. **this binding confusion**: Use arrow functions or bind()
|
||||
2. **Async/await without error handling**: Always use try/catch
|
||||
3. **Promise creation unnecessary**: Don't wrap already async functions
|
||||
4. **Mutation of objects**: Use spread operator or Object.assign()
|
||||
5. **Forgetting await**: Async functions return promises
|
||||
6. **Blocking event loop**: Avoid synchronous operations
|
||||
7. **Memory leaks**: Clean up event listeners and timers
|
||||
8. **Not handling promise rejections**: Use catch() or try/catch
|
||||
@@ -0,0 +1,457 @@
|
||||
# modern-javascript-patterns — detailed patterns and worked examples
|
||||
|
||||
## ES6+ Core Features
|
||||
|
||||
### 1. Arrow Functions
|
||||
|
||||
**Syntax and Use Cases:**
|
||||
|
||||
```javascript
|
||||
// Traditional function
|
||||
function add(a, b) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
// Arrow function
|
||||
const add = (a, b) => a + b;
|
||||
|
||||
// Single parameter (parentheses optional)
|
||||
const double = (x) => x * 2;
|
||||
|
||||
// No parameters
|
||||
const getRandom = () => Math.random();
|
||||
|
||||
// Multiple statements (need curly braces)
|
||||
const processUser = (user) => {
|
||||
const normalized = user.name.toLowerCase();
|
||||
return { ...user, name: normalized };
|
||||
};
|
||||
|
||||
// Returning objects (wrap in parentheses)
|
||||
const createUser = (name, age) => ({ name, age });
|
||||
```
|
||||
|
||||
**Lexical 'this' Binding:**
|
||||
|
||||
```javascript
|
||||
class Counter {
|
||||
constructor() {
|
||||
this.count = 0;
|
||||
}
|
||||
|
||||
// Arrow function preserves 'this' context
|
||||
increment = () => {
|
||||
this.count++;
|
||||
};
|
||||
|
||||
// Traditional function loses 'this' in callbacks
|
||||
incrementTraditional() {
|
||||
setTimeout(function () {
|
||||
this.count++; // 'this' is undefined
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Arrow function maintains 'this'
|
||||
incrementArrow() {
|
||||
setTimeout(() => {
|
||||
this.count++; // 'this' refers to Counter instance
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Destructuring
|
||||
|
||||
**Object Destructuring:**
|
||||
|
||||
```javascript
|
||||
const user = {
|
||||
id: 1,
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
address: {
|
||||
city: "New York",
|
||||
country: "USA",
|
||||
},
|
||||
};
|
||||
|
||||
// Basic destructuring
|
||||
const { name, email } = user;
|
||||
|
||||
// Rename variables
|
||||
const { name: userName, email: userEmail } = user;
|
||||
|
||||
// Default values
|
||||
const { age = 25 } = user;
|
||||
|
||||
// Nested destructuring
|
||||
const {
|
||||
address: { city, country },
|
||||
} = user;
|
||||
|
||||
// Rest operator
|
||||
const { id, ...userWithoutId } = user;
|
||||
|
||||
// Function parameters
|
||||
function greet({ name, age = 18 }) {
|
||||
console.log(`Hello ${name}, you are ${age}`);
|
||||
}
|
||||
greet(user);
|
||||
```
|
||||
|
||||
**Array Destructuring:**
|
||||
|
||||
```javascript
|
||||
const numbers = [1, 2, 3, 4, 5];
|
||||
|
||||
// Basic destructuring
|
||||
const [first, second] = numbers;
|
||||
|
||||
// Skip elements
|
||||
const [, , third] = numbers;
|
||||
|
||||
// Rest operator
|
||||
const [head, ...tail] = numbers;
|
||||
|
||||
// Swapping variables
|
||||
let a = 1,
|
||||
b = 2;
|
||||
[a, b] = [b, a];
|
||||
|
||||
// Function return values
|
||||
function getCoordinates() {
|
||||
return [10, 20];
|
||||
}
|
||||
const [x, y] = getCoordinates();
|
||||
|
||||
// Default values
|
||||
const [one, two, three = 0] = [1, 2];
|
||||
```
|
||||
|
||||
### 3. Spread and Rest Operators
|
||||
|
||||
**Spread Operator:**
|
||||
|
||||
```javascript
|
||||
// Array spreading
|
||||
const arr1 = [1, 2, 3];
|
||||
const arr2 = [4, 5, 6];
|
||||
const combined = [...arr1, ...arr2];
|
||||
|
||||
// Object spreading
|
||||
const defaults = { theme: "dark", lang: "en" };
|
||||
const userPrefs = { theme: "light" };
|
||||
const settings = { ...defaults, ...userPrefs };
|
||||
|
||||
// Function arguments
|
||||
const numbers = [1, 2, 3];
|
||||
Math.max(...numbers);
|
||||
|
||||
// Copying arrays/objects (shallow copy)
|
||||
const copy = [...arr1];
|
||||
const objCopy = { ...user };
|
||||
|
||||
// Adding items immutably
|
||||
const newArr = [...arr1, 4, 5];
|
||||
const newObj = { ...user, age: 30 };
|
||||
```
|
||||
|
||||
**Rest Parameters:**
|
||||
|
||||
```javascript
|
||||
// Collect function arguments
|
||||
function sum(...numbers) {
|
||||
return numbers.reduce((total, num) => total + num, 0);
|
||||
}
|
||||
sum(1, 2, 3, 4, 5);
|
||||
|
||||
// With regular parameters
|
||||
function greet(greeting, ...names) {
|
||||
return `${greeting} ${names.join(", ")}`;
|
||||
}
|
||||
greet("Hello", "John", "Jane", "Bob");
|
||||
|
||||
// Object rest
|
||||
const { id, ...userData } = user;
|
||||
|
||||
// Array rest
|
||||
const [first, ...rest] = [1, 2, 3, 4, 5];
|
||||
```
|
||||
|
||||
### 4. Template Literals
|
||||
|
||||
```javascript
|
||||
// Basic usage
|
||||
const name = "John";
|
||||
const greeting = `Hello, ${name}!`;
|
||||
|
||||
// Multi-line strings
|
||||
const html = `
|
||||
<div>
|
||||
<h1>${title}</h1>
|
||||
<p>${content}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Expression evaluation
|
||||
const price = 19.99;
|
||||
const total = `Total: $${(price * 1.2).toFixed(2)}`;
|
||||
|
||||
// Tagged template literals
|
||||
function highlight(strings, ...values) {
|
||||
return strings.reduce((result, str, i) => {
|
||||
const value = values[i] || "";
|
||||
return result + str + `<mark>${value}</mark>`;
|
||||
}, "");
|
||||
}
|
||||
|
||||
const name = "John";
|
||||
const age = 30;
|
||||
const html = highlight`Name: ${name}, Age: ${age}`;
|
||||
// Output: "Name: <mark>John</mark>, Age: <mark>30</mark>"
|
||||
```
|
||||
|
||||
### 5. Enhanced Object Literals
|
||||
|
||||
```javascript
|
||||
const name = "John";
|
||||
const age = 30;
|
||||
|
||||
// Shorthand property names
|
||||
const user = { name, age };
|
||||
|
||||
// Shorthand method names
|
||||
const calculator = {
|
||||
add(a, b) {
|
||||
return a + b;
|
||||
},
|
||||
subtract(a, b) {
|
||||
return a - b;
|
||||
},
|
||||
};
|
||||
|
||||
// Computed property names
|
||||
const field = "email";
|
||||
const user = {
|
||||
name: "John",
|
||||
[field]: "john@example.com",
|
||||
[`get${field.charAt(0).toUpperCase()}${field.slice(1)}`]() {
|
||||
return this[field];
|
||||
},
|
||||
};
|
||||
|
||||
// Dynamic property creation
|
||||
const createUser = (name, ...props) => {
|
||||
return props.reduce(
|
||||
(user, [key, value]) => ({
|
||||
...user,
|
||||
[key]: value,
|
||||
}),
|
||||
{ name },
|
||||
);
|
||||
};
|
||||
|
||||
const user = createUser("John", ["age", 30], ["email", "john@example.com"]);
|
||||
```
|
||||
|
||||
## Asynchronous Patterns
|
||||
|
||||
### 1. Promises
|
||||
|
||||
**Creating and Using Promises:**
|
||||
|
||||
```javascript
|
||||
// Creating a promise
|
||||
const fetchUser = (id) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (id > 0) {
|
||||
resolve({ id, name: "John" });
|
||||
} else {
|
||||
reject(new Error("Invalid ID"));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
// Using promises
|
||||
fetchUser(1)
|
||||
.then((user) => console.log(user))
|
||||
.catch((error) => console.error(error))
|
||||
.finally(() => console.log("Done"));
|
||||
|
||||
// Chaining promises
|
||||
fetchUser(1)
|
||||
.then((user) => fetchUserPosts(user.id))
|
||||
.then((posts) => processPosts(posts))
|
||||
.then((result) => console.log(result))
|
||||
.catch((error) => console.error(error));
|
||||
```
|
||||
|
||||
**Promise Combinators:**
|
||||
|
||||
```javascript
|
||||
// Promise.all - Wait for all promises
|
||||
const promises = [fetchUser(1), fetchUser(2), fetchUser(3)];
|
||||
|
||||
Promise.all(promises)
|
||||
.then((users) => console.log(users))
|
||||
.catch((error) => console.error("At least one failed:", error));
|
||||
|
||||
// Promise.allSettled - Wait for all, regardless of outcome
|
||||
Promise.allSettled(promises).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === "fulfilled") {
|
||||
console.log("Success:", result.value);
|
||||
} else {
|
||||
console.log("Error:", result.reason);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Promise.race - First to complete
|
||||
Promise.race(promises)
|
||||
.then((winner) => console.log("First:", winner))
|
||||
.catch((error) => console.error(error));
|
||||
|
||||
// Promise.any - First to succeed
|
||||
Promise.any(promises)
|
||||
.then((first) => console.log("First success:", first))
|
||||
.catch((error) => console.error("All failed:", error));
|
||||
```
|
||||
|
||||
### 2. Async/Await
|
||||
|
||||
**Basic Usage:**
|
||||
|
||||
```javascript
|
||||
// Async function always returns a Promise
|
||||
async function fetchUser(id) {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const user = await response.json();
|
||||
return user;
|
||||
}
|
||||
|
||||
// Error handling with try/catch
|
||||
async function getUserData(id) {
|
||||
try {
|
||||
const user = await fetchUser(id);
|
||||
const posts = await fetchUserPosts(user.id);
|
||||
return { user, posts };
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Sequential vs Parallel execution
|
||||
async function sequential() {
|
||||
const user1 = await fetchUser(1); // Wait
|
||||
const user2 = await fetchUser(2); // Then wait
|
||||
return [user1, user2];
|
||||
}
|
||||
|
||||
async function parallel() {
|
||||
const [user1, user2] = await Promise.all([fetchUser(1), fetchUser(2)]);
|
||||
return [user1, user2];
|
||||
}
|
||||
```
|
||||
|
||||
**Advanced Patterns:**
|
||||
|
||||
```javascript
|
||||
// Async IIFE
|
||||
(async () => {
|
||||
const result = await someAsyncOperation();
|
||||
console.log(result);
|
||||
})();
|
||||
|
||||
// Async iteration
|
||||
async function processUsers(userIds) {
|
||||
for (const id of userIds) {
|
||||
const user = await fetchUser(id);
|
||||
await processUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
// Top-level await (ES2022)
|
||||
const config = await fetch("/config.json").then((r) => r.json());
|
||||
|
||||
// Retry logic
|
||||
async function fetchWithRetry(url, retries = 3) {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
return await fetch(url);
|
||||
} catch (error) {
|
||||
if (i === retries - 1) throw error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * (i + 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout wrapper
|
||||
async function withTimeout(promise, ms) {
|
||||
const timeout = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error("Timeout")), ms),
|
||||
);
|
||||
return Promise.race([promise, timeout]);
|
||||
}
|
||||
```
|
||||
|
||||
## Functional Programming Patterns
|
||||
|
||||
Functional programming in JavaScript centers on pure functions, immutability, and composable transformations.
|
||||
|
||||
Key topics covered in [references/advanced-patterns.md](./advanced-patterns.md):
|
||||
- **Array methods** — `map`, `filter`, `reduce`, `find`, `findIndex`, `some`, `every`, `flatMap`, `Array.from`
|
||||
- **Higher-order functions** — custom `forEach`/`map`/`filter`, currying, partial application, memoization
|
||||
- **Composition and piping** — `compose`/`pipe` utilities with practical data transformation examples
|
||||
- **Pure functions and immutability** — immutable array/object operations, deep cloning with `structuredClone`
|
||||
|
||||
## Modern Class Features
|
||||
|
||||
ES2022 classes support private fields (`#field`), static fields, getters/setters, and private methods. See [references/advanced-patterns.md](./advanced-patterns.md) for a full example with inheritance.
|
||||
|
||||
## Modules (ES6)
|
||||
|
||||
```javascript
|
||||
// Named exports
|
||||
export const PI = 3.14159;
|
||||
export function add(a, b) { return a + b; }
|
||||
|
||||
// Default export
|
||||
export default function multiply(a, b) { return a * b; }
|
||||
|
||||
// Import
|
||||
import multiply, { PI, add } from "./math.js";
|
||||
|
||||
// Dynamic import (code splitting)
|
||||
const { add } = await import("./math.js");
|
||||
```
|
||||
|
||||
For re-exports, namespace imports, and conditional dynamic loading see [references/advanced-patterns.md](./advanced-patterns.md).
|
||||
|
||||
## Iterators and Generators
|
||||
|
||||
Generators (`function*`) and async generators (`async function*`) enable lazy sequences and async pagination. See [references/advanced-patterns.md](./advanced-patterns.md) for custom iterator, range generator, fibonacci, and `for await...of` examples.
|
||||
|
||||
## Modern Operators
|
||||
|
||||
```javascript
|
||||
// Optional chaining — safe property access
|
||||
const city = user?.address?.city;
|
||||
const result = obj.method?.();
|
||||
|
||||
// Nullish coalescing — default only for null/undefined (not 0 or "")
|
||||
const value = null ?? "default"; // 'default'
|
||||
const zero = 0 ?? "default"; // 0
|
||||
|
||||
// Logical assignment
|
||||
a ??= "default"; // assign if null/undefined
|
||||
obj.count ||= 1; // assign if falsy
|
||||
obj.count &&= 2; // assign if truthy
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
See [references/advanced-patterns.md](./advanced-patterns.md) for debounce, throttle, and lazy evaluation with generators.
|
||||
@@ -0,0 +1,114 @@
|
||||
---
|
||||
name: nextjs-app-router-patterns
|
||||
description: Master Next.js 14+ App Router with Server Components, streaming, parallel routes, and advanced data fetching. Use when building Next.js applications, implementing SSR/SSG, or optimizing React Server Components.
|
||||
---
|
||||
|
||||
# Next.js App Router Patterns
|
||||
|
||||
Comprehensive patterns for Next.js 14+ App Router architecture, Server Components, and modern full-stack React development.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building new Next.js applications with App Router
|
||||
- Migrating from Pages Router to App Router
|
||||
- Implementing Server Components and streaming
|
||||
- Setting up parallel and intercepting routes
|
||||
- Optimizing data fetching and caching
|
||||
- Building full-stack features with Server Actions
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### 1. Rendering Modes
|
||||
|
||||
| Mode | Where | When to Use |
|
||||
| --------------------- | ------------ | ----------------------------------------- |
|
||||
| **Server Components** | Server only | Data fetching, heavy computation, secrets |
|
||||
| **Client Components** | Browser | Interactivity, hooks, browser APIs |
|
||||
| **Static** | Build time | Content that rarely changes |
|
||||
| **Dynamic** | Request time | Personalized or real-time data |
|
||||
| **Streaming** | Progressive | Large pages, slow data sources |
|
||||
|
||||
### 2. File Conventions
|
||||
|
||||
```
|
||||
app/
|
||||
├── layout.tsx # Shared UI wrapper
|
||||
├── page.tsx # Route UI
|
||||
├── loading.tsx # Loading UI (Suspense)
|
||||
├── error.tsx # Error boundary
|
||||
├── not-found.tsx # 404 UI
|
||||
├── route.ts # API endpoint
|
||||
├── template.tsx # Re-mounted layout
|
||||
├── default.tsx # Parallel route fallback
|
||||
└── opengraph-image.tsx # OG image generation
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
// app/layout.tsx
|
||||
import { Inter } from 'next/font/google'
|
||||
import { Providers } from './providers'
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
|
||||
export const metadata = {
|
||||
title: { default: 'My App', template: '%s | My App' },
|
||||
description: 'Built with Next.js App Router',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body className={inter.className}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
|
||||
// app/page.tsx - Server Component by default
|
||||
async function getProducts() {
|
||||
const res = await fetch('https://api.example.com/products', {
|
||||
next: { revalidate: 3600 }, // ISR: revalidate every hour
|
||||
})
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export default async function HomePage() {
|
||||
const products = await getProducts()
|
||||
|
||||
return (
|
||||
<main>
|
||||
<h1>Products</h1>
|
||||
<ProductGrid products={products} />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Do's
|
||||
|
||||
- **Start with Server Components** - Add 'use client' only when needed
|
||||
- **Colocate data fetching** - Fetch data where it's used
|
||||
- **Use Suspense boundaries** - Enable streaming for slow data
|
||||
- **Leverage parallel routes** - Independent loading states
|
||||
- **Use Server Actions** - For mutations with progressive enhancement
|
||||
|
||||
### Don'ts
|
||||
|
||||
- **Don't pass serializable data** - Server → Client boundary limitations
|
||||
- **Don't use hooks in Server Components** - No useState, useEffect
|
||||
- **Don't fetch in Client Components** - Use Server Components or React Query
|
||||
- **Don't over-nest layouts** - Each layout adds to the component tree
|
||||
- **Don't ignore loading states** - Always provide loading.tsx or Suspense
|
||||
@@ -0,0 +1,428 @@
|
||||
# nextjs-app-router-patterns — detailed patterns and worked examples
|
||||
|
||||
## Patterns
|
||||
|
||||
### Pattern 1: Server Components with Data Fetching
|
||||
|
||||
```typescript
|
||||
// app/products/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
import { ProductList, ProductListSkeleton } from '@/components/products'
|
||||
import { FilterSidebar } from '@/components/filters'
|
||||
|
||||
interface SearchParams {
|
||||
category?: string
|
||||
sort?: 'price' | 'name' | 'date'
|
||||
page?: string
|
||||
}
|
||||
|
||||
export default async function ProductsPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<SearchParams>
|
||||
}) {
|
||||
const params = await searchParams
|
||||
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<FilterSidebar />
|
||||
<Suspense
|
||||
key={JSON.stringify(params)}
|
||||
fallback={<ProductListSkeleton />}
|
||||
>
|
||||
<ProductList
|
||||
category={params.category}
|
||||
sort={params.sort}
|
||||
page={Number(params.page) || 1}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// components/products/ProductList.tsx - Server Component
|
||||
async function getProducts(filters: ProductFilters) {
|
||||
const res = await fetch(
|
||||
`${process.env.API_URL}/products?${new URLSearchParams(filters)}`,
|
||||
{ next: { tags: ['products'] } }
|
||||
)
|
||||
if (!res.ok) throw new Error('Failed to fetch products')
|
||||
return res.json()
|
||||
}
|
||||
|
||||
export async function ProductList({ category, sort, page }: ProductFilters) {
|
||||
const { products, totalPages } = await getProducts({ category, sort, page })
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{products.map((product) => (
|
||||
<ProductCard key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
<Pagination currentPage={page} totalPages={totalPages} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Client Components with 'use client'
|
||||
|
||||
```typescript
|
||||
// components/products/AddToCartButton.tsx
|
||||
'use client'
|
||||
|
||||
import { useState, useTransition } from 'react'
|
||||
import { addToCart } from '@/app/actions/cart'
|
||||
|
||||
export function AddToCartButton({ productId }: { productId: string }) {
|
||||
const [isPending, startTransition] = useTransition()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const handleClick = () => {
|
||||
setError(null)
|
||||
startTransition(async () => {
|
||||
const result = await addToCart(productId)
|
||||
if (result.error) {
|
||||
setError(result.error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
disabled={isPending}
|
||||
className="btn-primary"
|
||||
>
|
||||
{isPending ? 'Adding...' : 'Add to Cart'}
|
||||
</button>
|
||||
{error && <p className="text-red-500 text-sm">{error}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Server Actions
|
||||
|
||||
```typescript
|
||||
// app/actions/cart.ts
|
||||
"use server";
|
||||
|
||||
import { revalidateTag } from "next/cache";
|
||||
import { cookies } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export async function addToCart(productId: string) {
|
||||
const cookieStore = await cookies();
|
||||
const sessionId = cookieStore.get("session")?.value;
|
||||
|
||||
if (!sessionId) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
try {
|
||||
await db.cart.upsert({
|
||||
where: { sessionId_productId: { sessionId, productId } },
|
||||
update: { quantity: { increment: 1 } },
|
||||
create: { sessionId, productId, quantity: 1 },
|
||||
});
|
||||
|
||||
revalidateTag("cart");
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { error: "Failed to add item to cart" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkout(formData: FormData) {
|
||||
const address = formData.get("address") as string;
|
||||
const payment = formData.get("payment") as string;
|
||||
|
||||
// Validate
|
||||
if (!address || !payment) {
|
||||
return { error: "Missing required fields" };
|
||||
}
|
||||
|
||||
// Process order
|
||||
const order = await processOrder({ address, payment });
|
||||
|
||||
// Redirect to confirmation
|
||||
redirect(`/orders/${order.id}/confirmation`);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Parallel Routes
|
||||
|
||||
```typescript
|
||||
// app/dashboard/layout.tsx
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
analytics,
|
||||
team,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
analytics: React.ReactNode
|
||||
team: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className="dashboard-grid">
|
||||
<main>{children}</main>
|
||||
<aside className="analytics-panel">{analytics}</aside>
|
||||
<aside className="team-panel">{team}</aside>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// app/dashboard/@analytics/page.tsx
|
||||
export default async function AnalyticsSlot() {
|
||||
const stats = await getAnalytics()
|
||||
return <AnalyticsChart data={stats} />
|
||||
}
|
||||
|
||||
// app/dashboard/@analytics/loading.tsx
|
||||
export default function AnalyticsLoading() {
|
||||
return <ChartSkeleton />
|
||||
}
|
||||
|
||||
// app/dashboard/@team/page.tsx
|
||||
export default async function TeamSlot() {
|
||||
const members = await getTeamMembers()
|
||||
return <TeamList members={members} />
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 5: Intercepting Routes (Modal Pattern)
|
||||
|
||||
```typescript
|
||||
// File structure for photo modal
|
||||
// app/
|
||||
// ├── @modal/
|
||||
// │ ├── (.)photos/[id]/page.tsx # Intercept
|
||||
// │ └── default.tsx
|
||||
// ├── photos/
|
||||
// │ └── [id]/page.tsx # Full page
|
||||
// └── layout.tsx
|
||||
|
||||
// app/@modal/(.)photos/[id]/page.tsx
|
||||
import { Modal } from '@/components/Modal'
|
||||
import { PhotoDetail } from '@/components/PhotoDetail'
|
||||
|
||||
export default async function PhotoModal({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const photo = await getPhoto(id)
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<PhotoDetail photo={photo} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
// app/photos/[id]/page.tsx - Full page version
|
||||
export default async function PhotoPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const photo = await getPhoto(id)
|
||||
|
||||
return (
|
||||
<div className="photo-page">
|
||||
<PhotoDetail photo={photo} />
|
||||
<RelatedPhotos photoId={id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// app/layout.tsx
|
||||
export default function RootLayout({
|
||||
children,
|
||||
modal,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
modal: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<html>
|
||||
<body>
|
||||
{children}
|
||||
{modal}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 6: Streaming with Suspense
|
||||
|
||||
```typescript
|
||||
// app/product/[id]/page.tsx
|
||||
import { Suspense } from 'react'
|
||||
|
||||
export default async function ProductPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
|
||||
// This data loads first (blocking)
|
||||
const product = await getProduct(id)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Immediate render */}
|
||||
<ProductHeader product={product} />
|
||||
|
||||
{/* Stream in reviews */}
|
||||
<Suspense fallback={<ReviewsSkeleton />}>
|
||||
<Reviews productId={id} />
|
||||
</Suspense>
|
||||
|
||||
{/* Stream in recommendations */}
|
||||
<Suspense fallback={<RecommendationsSkeleton />}>
|
||||
<Recommendations productId={id} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// These components fetch their own data
|
||||
async function Reviews({ productId }: { productId: string }) {
|
||||
const reviews = await getReviews(productId) // Slow API
|
||||
return <ReviewList reviews={reviews} />
|
||||
}
|
||||
|
||||
async function Recommendations({ productId }: { productId: string }) {
|
||||
const products = await getRecommendations(productId) // ML-based, slow
|
||||
return <ProductCarousel products={products} />
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 7: Route Handlers (API Routes)
|
||||
|
||||
```typescript
|
||||
// app/api/products/route.ts
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const category = searchParams.get("category");
|
||||
|
||||
const products = await db.product.findMany({
|
||||
where: category ? { category } : undefined,
|
||||
take: 20,
|
||||
});
|
||||
|
||||
return NextResponse.json(products);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const body = await request.json();
|
||||
|
||||
const product = await db.product.create({
|
||||
data: body,
|
||||
});
|
||||
|
||||
return NextResponse.json(product, { status: 201 });
|
||||
}
|
||||
|
||||
// app/api/products/[id]/route.ts
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
const { id } = await params;
|
||||
const product = await db.product.findUnique({ where: { id } });
|
||||
|
||||
if (!product) {
|
||||
return NextResponse.json({ error: "Product not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(product);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 8: Metadata and SEO
|
||||
|
||||
```typescript
|
||||
// app/products/[slug]/page.tsx
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
type Props = {
|
||||
params: Promise<{ slug: string }>
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: Props): Promise<Metadata> {
|
||||
const { slug } = await params
|
||||
const product = await getProduct(slug)
|
||||
|
||||
if (!product) return {}
|
||||
|
||||
return {
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
openGraph: {
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
images: [{ url: product.image, width: 1200, height: 630 }],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: product.name,
|
||||
description: product.description,
|
||||
images: [product.image],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const products = await db.product.findMany({ select: { slug: true } })
|
||||
return products.map((p) => ({ slug: p.slug }))
|
||||
}
|
||||
|
||||
export default async function ProductPage({ params }: Props) {
|
||||
const { slug } = await params
|
||||
const product = await getProduct(slug)
|
||||
|
||||
if (!product) notFound()
|
||||
|
||||
return <ProductDetail product={product} />
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
### Data Cache
|
||||
|
||||
```typescript
|
||||
// No cache (always fresh)
|
||||
fetch(url, { cache: "no-store" });
|
||||
|
||||
// Cache forever (static)
|
||||
fetch(url, { cache: "force-cache" });
|
||||
|
||||
// ISR - revalidate after 60 seconds
|
||||
fetch(url, { next: { revalidate: 60 } });
|
||||
|
||||
// Tag-based invalidation
|
||||
fetch(url, { next: { tags: ["products"] } });
|
||||
|
||||
// Invalidate via Server Action
|
||||
("use server");
|
||||
import { revalidateTag, revalidatePath } from "next/cache";
|
||||
|
||||
export async function updateProduct(id: string, data: ProductData) {
|
||||
await db.product.update({ where: { id }, data });
|
||||
revalidateTag("products");
|
||||
revalidatePath("/products");
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,45 @@
|
||||
---
|
||||
name: nodejs-backend-patterns
|
||||
description: Build production-ready Node.js backend services with Express/Fastify, implementing middleware patterns, error handling, authentication, database integration, and API design best practices. Use when creating Node.js servers, REST APIs, GraphQL backends, or microservices architectures.
|
||||
---
|
||||
|
||||
# Node.js Backend Patterns
|
||||
|
||||
Comprehensive guidance for building scalable, maintainable, and production-ready Node.js backend applications with modern frameworks, architectural patterns, and best practices.
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
- Building REST APIs or GraphQL servers
|
||||
- Creating microservices with Node.js
|
||||
- Implementing authentication and authorization
|
||||
- Designing scalable backend architectures
|
||||
- Setting up middleware and error handling
|
||||
- Integrating databases (SQL and NoSQL)
|
||||
- Building real-time applications with WebSockets
|
||||
- Implementing background job processing
|
||||
|
||||
## Detailed patterns and worked examples
|
||||
|
||||
Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use TypeScript**: Type safety prevents runtime errors
|
||||
2. **Implement proper error handling**: Use custom error classes
|
||||
3. **Validate input**: Use libraries like Zod or Joi
|
||||
4. **Use environment variables**: Never hardcode secrets
|
||||
5. **Implement logging**: Use structured logging (Pino, Winston)
|
||||
6. **Add rate limiting**: Prevent abuse
|
||||
7. **Use HTTPS**: Always in production
|
||||
8. **Implement CORS properly**: Don't use `*` in production
|
||||
9. **Use dependency injection**: Easier testing and maintenance
|
||||
10. **Write tests**: Unit, integration, and E2E tests
|
||||
11. **Handle graceful shutdown**: Clean up resources
|
||||
12. **Use connection pooling**: For databases
|
||||
13. **Implement health checks**: For monitoring
|
||||
14. **Use compression**: Reduce response size
|
||||
15. **Monitor performance**: Use APM tools
|
||||
|
||||
## Testing Patterns
|
||||
|
||||
See `javascript-testing-patterns` skill for comprehensive testing guidance.
|
||||
@@ -0,0 +1,430 @@
|
||||
# Node.js Advanced Patterns
|
||||
|
||||
Advanced patterns for dependency injection, database integration, authentication, caching, and API response formatting.
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
### DI Container
|
||||
|
||||
```typescript
|
||||
// di-container.ts
|
||||
import { Pool } from "pg";
|
||||
import { UserRepository } from "./repositories/user.repository";
|
||||
import { UserService } from "./services/user.service";
|
||||
import { UserController } from "./controllers/user.controller";
|
||||
import { AuthService } from "./services/auth.service";
|
||||
|
||||
class Container {
|
||||
private instances = new Map<string, any>();
|
||||
|
||||
register<T>(key: string, factory: () => T): void {
|
||||
this.instances.set(key, factory);
|
||||
}
|
||||
|
||||
resolve<T>(key: string): T {
|
||||
const factory = this.instances.get(key);
|
||||
if (!factory) {
|
||||
throw new Error(`No factory registered for ${key}`);
|
||||
}
|
||||
return factory();
|
||||
}
|
||||
|
||||
singleton<T>(key: string, factory: () => T): void {
|
||||
let instance: T;
|
||||
this.instances.set(key, () => {
|
||||
if (!instance) {
|
||||
instance = factory();
|
||||
}
|
||||
return instance;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const container = new Container();
|
||||
|
||||
// Register dependencies
|
||||
container.singleton(
|
||||
"db",
|
||||
() =>
|
||||
new Pool({
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
}),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"userRepository",
|
||||
() => new UserRepository(container.resolve("db")),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"userService",
|
||||
() => new UserService(container.resolve("userRepository")),
|
||||
);
|
||||
|
||||
container.register(
|
||||
"userController",
|
||||
() => new UserController(container.resolve("userService")),
|
||||
);
|
||||
|
||||
container.singleton(
|
||||
"authService",
|
||||
() => new AuthService(container.resolve("userRepository")),
|
||||
);
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
### PostgreSQL with Connection Pool
|
||||
|
||||
```typescript
|
||||
// config/database.ts
|
||||
import { Pool, PoolConfig } from "pg";
|
||||
|
||||
const poolConfig: PoolConfig = {
|
||||
host: process.env.DB_HOST,
|
||||
port: parseInt(process.env.DB_PORT || "5432"),
|
||||
database: process.env.DB_NAME,
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
};
|
||||
|
||||
export const pool = new Pool(poolConfig);
|
||||
|
||||
// Test connection
|
||||
pool.on("connect", () => {
|
||||
console.log("Database connected");
|
||||
});
|
||||
|
||||
pool.on("error", (err) => {
|
||||
console.error("Unexpected database error", err);
|
||||
process.exit(-1);
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
export const closeDatabase = async () => {
|
||||
await pool.end();
|
||||
console.log("Database connection closed");
|
||||
};
|
||||
```
|
||||
|
||||
### MongoDB with Mongoose
|
||||
|
||||
```typescript
|
||||
// config/mongoose.ts
|
||||
import mongoose from "mongoose";
|
||||
|
||||
const connectDB = async () => {
|
||||
try {
|
||||
await mongoose.connect(process.env.MONGODB_URI!, {
|
||||
maxPoolSize: 10,
|
||||
serverSelectionTimeoutMS: 5000,
|
||||
socketTimeoutMS: 45000,
|
||||
});
|
||||
|
||||
console.log("MongoDB connected");
|
||||
} catch (error) {
|
||||
console.error("MongoDB connection error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
mongoose.connection.on("disconnected", () => {
|
||||
console.log("MongoDB disconnected");
|
||||
});
|
||||
|
||||
mongoose.connection.on("error", (err) => {
|
||||
console.error("MongoDB error:", err);
|
||||
});
|
||||
|
||||
export { connectDB };
|
||||
|
||||
// Model example
|
||||
import { Schema, model, Document } from "mongoose";
|
||||
|
||||
interface IUser extends Document {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
{
|
||||
name: { type: String, required: true },
|
||||
email: { type: String, required: true, unique: true },
|
||||
password: { type: String, required: true },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Indexes
|
||||
userSchema.index({ email: 1 });
|
||||
|
||||
export const User = model<IUser>("User", userSchema);
|
||||
```
|
||||
|
||||
### Transaction Pattern
|
||||
|
||||
```typescript
|
||||
// services/order.service.ts
|
||||
import { Pool } from "pg";
|
||||
|
||||
export class OrderService {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async createOrder(userId: string, items: any[]) {
|
||||
const client = await this.db.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
// Create order
|
||||
const orderResult = await client.query(
|
||||
"INSERT INTO orders (user_id, total) VALUES ($1, $2) RETURNING id",
|
||||
[userId, calculateTotal(items)],
|
||||
);
|
||||
const orderId = orderResult.rows[0].id;
|
||||
|
||||
// Create order items
|
||||
for (const item of items) {
|
||||
await client.query(
|
||||
"INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ($1, $2, $3, $4)",
|
||||
[orderId, item.productId, item.quantity, item.price],
|
||||
);
|
||||
|
||||
// Update inventory
|
||||
await client.query(
|
||||
"UPDATE products SET stock = stock - $1 WHERE id = $2",
|
||||
[item.quantity, item.productId],
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
return orderId;
|
||||
} catch (error) {
|
||||
await client.query("ROLLBACK");
|
||||
throw error;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
### JWT Authentication
|
||||
|
||||
```typescript
|
||||
// services/auth.service.ts
|
||||
import jwt from "jsonwebtoken";
|
||||
import bcrypt from "bcrypt";
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
export class AuthService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
const user = await this.userRepository.findByEmail(email);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const isValid = await bcrypt.compare(password, user.password);
|
||||
|
||||
if (!isValid) {
|
||||
throw new UnauthorizedError("Invalid credentials");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
const refreshToken = this.generateRefreshToken({
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
return {
|
||||
token,
|
||||
refreshToken,
|
||||
user: {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async refreshToken(refreshToken: string) {
|
||||
try {
|
||||
const payload = jwt.verify(
|
||||
refreshToken,
|
||||
process.env.REFRESH_TOKEN_SECRET!,
|
||||
) as { userId: string };
|
||||
|
||||
const user = await this.userRepository.findById(payload.userId);
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedError("User not found");
|
||||
}
|
||||
|
||||
const token = this.generateToken({
|
||||
userId: user.id,
|
||||
email: user.email,
|
||||
});
|
||||
|
||||
return { token };
|
||||
} catch (error) {
|
||||
throw new UnauthorizedError("Invalid refresh token");
|
||||
}
|
||||
}
|
||||
|
||||
private generateToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.JWT_SECRET!, {
|
||||
expiresIn: "15m",
|
||||
});
|
||||
}
|
||||
|
||||
private generateRefreshToken(payload: any): string {
|
||||
return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET!, {
|
||||
expiresIn: "7d",
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
```typescript
|
||||
// utils/cache.ts
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
retryStrategy: (times) => {
|
||||
const delay = Math.min(times * 50, 2000);
|
||||
return delay;
|
||||
},
|
||||
});
|
||||
|
||||
export class CacheService {
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const data = await redis.get(key);
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
async set(key: string, value: any, ttl?: number): Promise<void> {
|
||||
const serialized = JSON.stringify(value);
|
||||
if (ttl) {
|
||||
await redis.setex(key, ttl, serialized);
|
||||
} else {
|
||||
await redis.set(key, serialized);
|
||||
}
|
||||
}
|
||||
|
||||
async delete(key: string): Promise<void> {
|
||||
await redis.del(key);
|
||||
}
|
||||
|
||||
async invalidatePattern(pattern: string): Promise<void> {
|
||||
const keys = await redis.keys(pattern);
|
||||
if (keys.length > 0) {
|
||||
await redis.del(...keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache decorator
|
||||
export function Cacheable(ttl: number = 300) {
|
||||
return function (
|
||||
target: any,
|
||||
propertyKey: string,
|
||||
descriptor: PropertyDescriptor,
|
||||
) {
|
||||
const originalMethod = descriptor.value;
|
||||
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const cache = new CacheService();
|
||||
const cacheKey = `${propertyKey}:${JSON.stringify(args)}`;
|
||||
|
||||
const cached = await cache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const result = await originalMethod.apply(this, args);
|
||||
await cache.set(cacheKey, result, ttl);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return descriptor;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Response Format
|
||||
|
||||
```typescript
|
||||
// utils/response.ts
|
||||
import { Response } from "express";
|
||||
|
||||
export class ApiResponse {
|
||||
static success<T>(
|
||||
res: Response,
|
||||
data: T,
|
||||
message?: string,
|
||||
statusCode = 200,
|
||||
) {
|
||||
return res.status(statusCode).json({
|
||||
status: "success",
|
||||
message,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
static error(res: Response, message: string, statusCode = 500, errors?: any) {
|
||||
return res.status(statusCode).json({
|
||||
status: "error",
|
||||
message,
|
||||
...(errors && { errors }),
|
||||
});
|
||||
}
|
||||
|
||||
static paginated<T>(
|
||||
res: Response,
|
||||
data: T[],
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number,
|
||||
) {
|
||||
return res.json({
|
||||
status: "success",
|
||||
data,
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
pages: Math.ceil(total / limit),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,599 @@
|
||||
# nodejs-backend-patterns — detailed patterns and worked examples
|
||||
|
||||
## Core Frameworks
|
||||
|
||||
### Express.js - Minimalist Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import compression from "compression";
|
||||
|
||||
const app = express();
|
||||
|
||||
// Security middleware
|
||||
app.use(helmet());
|
||||
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(",") }));
|
||||
app.use(compression());
|
||||
|
||||
// Body parsing
|
||||
app.use(express.json({ limit: "10mb" }));
|
||||
app.use(express.urlencoded({ extended: true, limit: "10mb" }));
|
||||
|
||||
// Request logging
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
console.log(`${req.method} ${req.path}`);
|
||||
next();
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
});
|
||||
```
|
||||
|
||||
### Fastify - High Performance Framework
|
||||
|
||||
**Basic Setup:**
|
||||
|
||||
```typescript
|
||||
import Fastify from "fastify";
|
||||
import helmet from "@fastify/helmet";
|
||||
import cors from "@fastify/cors";
|
||||
import compress from "@fastify/compress";
|
||||
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Plugins
|
||||
await fastify.register(helmet);
|
||||
await fastify.register(cors, { origin: true });
|
||||
await fastify.register(compress);
|
||||
|
||||
// Type-safe routes with schema validation
|
||||
fastify.post<{
|
||||
Body: { name: string; email: string };
|
||||
Reply: { id: string; name: string };
|
||||
}>(
|
||||
"/users",
|
||||
{
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["name", "email"],
|
||||
properties: {
|
||||
name: { type: "string", minLength: 1 },
|
||||
email: { type: "string", format: "email" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { name, email } = request.body;
|
||||
return { id: "123", name };
|
||||
},
|
||||
);
|
||||
|
||||
await fastify.listen({ port: 3000, host: "0.0.0.0" });
|
||||
```
|
||||
|
||||
## Architectural Patterns
|
||||
|
||||
### Pattern 1: Layered Architecture
|
||||
|
||||
**Structure:**
|
||||
|
||||
```
|
||||
src/
|
||||
├── controllers/ # Handle HTTP requests/responses
|
||||
├── services/ # Business logic
|
||||
├── repositories/ # Data access layer
|
||||
├── models/ # Data models
|
||||
├── middleware/ # Express/Fastify middleware
|
||||
├── routes/ # Route definitions
|
||||
├── utils/ # Helper functions
|
||||
├── config/ # Configuration
|
||||
└── types/ # TypeScript types
|
||||
```
|
||||
|
||||
**Controller Layer:**
|
||||
|
||||
```typescript
|
||||
// controllers/user.controller.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { UserService } from "../services/user.service";
|
||||
import { CreateUserDTO, UpdateUserDTO } from "../types/user.types";
|
||||
|
||||
export class UserController {
|
||||
constructor(private userService: UserService) {}
|
||||
|
||||
async createUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const userData: CreateUserDTO = req.body;
|
||||
const user = await this.userService.createUser(userData);
|
||||
res.status(201).json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async getUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const user = await this.userService.getUserById(id);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates: UpdateUserDTO = req.body;
|
||||
const user = await this.userService.updateUser(id, updates);
|
||||
res.json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteUser(req: Request, res: Response, next: NextFunction) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
await this.userService.deleteUser(id);
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Service Layer:**
|
||||
|
||||
```typescript
|
||||
// services/user.service.ts
|
||||
import { UserRepository } from "../repositories/user.repository";
|
||||
import { CreateUserDTO, UpdateUserDTO, User } from "../types/user.types";
|
||||
import { NotFoundError, ValidationError } from "../utils/errors";
|
||||
import bcrypt from "bcrypt";
|
||||
|
||||
export class UserService {
|
||||
constructor(private userRepository: UserRepository) {}
|
||||
|
||||
async createUser(userData: CreateUserDTO): Promise<User> {
|
||||
// Validation
|
||||
const existingUser = await this.userRepository.findByEmail(userData.email);
|
||||
if (existingUser) {
|
||||
throw new ValidationError("Email already exists");
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(userData.password, 10);
|
||||
|
||||
// Create user
|
||||
const user = await this.userRepository.create({
|
||||
...userData,
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
// Remove password from response
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async getUserById(id: string): Promise<User> {
|
||||
const user = await this.userRepository.findById(id);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async updateUser(id: string, updates: UpdateUserDTO): Promise<User> {
|
||||
const user = await this.userRepository.update(id, updates);
|
||||
if (!user) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
const { password, ...userWithoutPassword } = user;
|
||||
return userWithoutPassword as User;
|
||||
}
|
||||
|
||||
async deleteUser(id: string): Promise<void> {
|
||||
const deleted = await this.userRepository.delete(id);
|
||||
if (!deleted) {
|
||||
throw new NotFoundError("User not found");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Repository Layer:**
|
||||
|
||||
```typescript
|
||||
// repositories/user.repository.ts
|
||||
import { Pool } from "pg";
|
||||
import { CreateUserDTO, UpdateUserDTO, UserEntity } from "../types/user.types";
|
||||
|
||||
export class UserRepository {
|
||||
constructor(private db: Pool) {}
|
||||
|
||||
async create(
|
||||
userData: CreateUserDTO & { password: string },
|
||||
): Promise<UserEntity> {
|
||||
const query = `
|
||||
INSERT INTO users (name, email, password)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, name, email, password, created_at, updated_at
|
||||
`;
|
||||
const { rows } = await this.db.query(query, [
|
||||
userData.name,
|
||||
userData.email,
|
||||
userData.password,
|
||||
]);
|
||||
return rows[0];
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<UserEntity | null> {
|
||||
const query = "SELECT * FROM users WHERE id = $1";
|
||||
const { rows } = await this.db.query(query, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async findByEmail(email: string): Promise<UserEntity | null> {
|
||||
const query = "SELECT * FROM users WHERE email = $1";
|
||||
const { rows } = await this.db.query(query, [email]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async update(id: string, updates: UpdateUserDTO): Promise<UserEntity | null> {
|
||||
const fields = Object.keys(updates);
|
||||
const values = Object.values(updates);
|
||||
|
||||
const setClause = fields
|
||||
.map((field, idx) => `${field} = $${idx + 2}`)
|
||||
.join(", ");
|
||||
|
||||
const query = `
|
||||
UPDATE users
|
||||
SET ${setClause}, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
`;
|
||||
|
||||
const { rows } = await this.db.query(query, [id, ...values]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<boolean> {
|
||||
const query = "DELETE FROM users WHERE id = $1";
|
||||
const { rowCount } = await this.db.query(query, [id]);
|
||||
return rowCount > 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Dependency Injection
|
||||
|
||||
Use a DI container to wire up repositories, services, and controllers. For a full container implementation, see [references/advanced-patterns.md](./advanced-patterns.md).
|
||||
|
||||
## Middleware Patterns
|
||||
|
||||
### Authentication Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/auth.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { UnauthorizedError } from "../utils/errors";
|
||||
|
||||
interface JWTPayload {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
user?: JWTPayload;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const authenticate = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace("Bearer ", "");
|
||||
|
||||
if (!token) {
|
||||
throw new UnauthorizedError("No token provided");
|
||||
}
|
||||
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
|
||||
|
||||
req.user = payload;
|
||||
next();
|
||||
} catch (error) {
|
||||
next(new UnauthorizedError("Invalid token"));
|
||||
}
|
||||
};
|
||||
|
||||
export const authorize = (...roles: string[]) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
if (!req.user) {
|
||||
return next(new UnauthorizedError("Not authenticated"));
|
||||
}
|
||||
|
||||
// Check if user has required role
|
||||
const hasRole = roles.some((role) => req.user?.roles?.includes(role));
|
||||
|
||||
if (!hasRole) {
|
||||
return next(new UnauthorizedError("Insufficient permissions"));
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Validation Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/validation.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AnyZodObject, ZodError } from "zod";
|
||||
import { ValidationError } from "../utils/errors";
|
||||
|
||||
export const validate = (schema: AnyZodObject) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
await schema.parseAsync({
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
params: req.params,
|
||||
});
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const errors = error.errors.map((err) => ({
|
||||
field: err.path.join("."),
|
||||
message: err.message,
|
||||
}));
|
||||
next(new ValidationError("Validation failed", errors));
|
||||
} else {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Usage with Zod
|
||||
import { z } from "zod";
|
||||
|
||||
const createUserSchema = z.object({
|
||||
body: z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
}),
|
||||
});
|
||||
|
||||
router.post("/users", validate(createUserSchema), userController.createUser);
|
||||
```
|
||||
|
||||
### Rate Limiting Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/rate-limit.middleware.ts
|
||||
import rateLimit from "express-rate-limit";
|
||||
import RedisStore from "rate-limit-redis";
|
||||
import Redis from "ioredis";
|
||||
|
||||
const redis = new Redis({
|
||||
host: process.env.REDIS_HOST,
|
||||
port: parseInt(process.env.REDIS_PORT || "6379"),
|
||||
});
|
||||
|
||||
export const apiLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: "rl:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000, // 15 minutes
|
||||
max: 100, // Limit each IP to 100 requests per windowMs
|
||||
message: "Too many requests from this IP, please try again later",
|
||||
standardHeaders: true,
|
||||
legacyHeaders: false,
|
||||
});
|
||||
|
||||
export const authLimiter = rateLimit({
|
||||
store: new RedisStore({
|
||||
client: redis,
|
||||
prefix: "rl:auth:",
|
||||
}),
|
||||
windowMs: 15 * 60 * 1000,
|
||||
max: 5, // Stricter limit for auth endpoints
|
||||
skipSuccessfulRequests: true,
|
||||
});
|
||||
```
|
||||
|
||||
### Request Logging Middleware
|
||||
|
||||
```typescript
|
||||
// middleware/logger.middleware.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import pino from "pino";
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || "info",
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
});
|
||||
|
||||
export const requestLogger = (
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
const start = Date.now();
|
||||
|
||||
// Log response when finished
|
||||
res.on("finish", () => {
|
||||
const duration = Date.now() - start;
|
||||
logger.info({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
status: res.statusCode,
|
||||
duration: `${duration}ms`,
|
||||
userAgent: req.headers["user-agent"],
|
||||
ip: req.ip,
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
export { logger };
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Custom Error Classes
|
||||
|
||||
```typescript
|
||||
// utils/errors.ts
|
||||
export class AppError extends Error {
|
||||
constructor(
|
||||
public message: string,
|
||||
public statusCode: number = 500,
|
||||
public isOperational: boolean = true,
|
||||
) {
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, AppError.prototype);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
export class ValidationError extends AppError {
|
||||
constructor(
|
||||
message: string,
|
||||
public errors?: any[],
|
||||
) {
|
||||
super(message, 400);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotFoundError extends AppError {
|
||||
constructor(message: string = "Resource not found") {
|
||||
super(message, 404);
|
||||
}
|
||||
}
|
||||
|
||||
export class UnauthorizedError extends AppError {
|
||||
constructor(message: string = "Unauthorized") {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends AppError {
|
||||
constructor(message: string = "Forbidden") {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
export class ConflictError extends AppError {
|
||||
constructor(message: string) {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Global Error Handler
|
||||
|
||||
```typescript
|
||||
// middleware/error-handler.ts
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { AppError } from "../utils/errors";
|
||||
import { logger } from "./logger.middleware";
|
||||
|
||||
export const errorHandler = (
|
||||
err: Error,
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
) => {
|
||||
if (err instanceof AppError) {
|
||||
return res.status(err.statusCode).json({
|
||||
status: "error",
|
||||
message: err.message,
|
||||
...(err instanceof ValidationError && { errors: err.errors }),
|
||||
});
|
||||
}
|
||||
|
||||
// Log unexpected errors
|
||||
logger.error({
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
});
|
||||
|
||||
// Don't leak error details in production
|
||||
const message =
|
||||
process.env.NODE_ENV === "production"
|
||||
? "Internal server error"
|
||||
: err.message;
|
||||
|
||||
res.status(500).json({
|
||||
status: "error",
|
||||
message,
|
||||
});
|
||||
};
|
||||
|
||||
// Async error wrapper
|
||||
export const asyncHandler = (
|
||||
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>,
|
||||
) => {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Database Patterns
|
||||
|
||||
Node.js supports both SQL and NoSQL databases. Use connection pooling for all production databases.
|
||||
|
||||
Key patterns covered in [references/advanced-patterns.md](./advanced-patterns.md):
|
||||
- **PostgreSQL with connection pool** — `pg` Pool configuration and graceful shutdown
|
||||
- **MongoDB with Mongoose** — connection management and schema definition
|
||||
- **Transaction pattern** — `BEGIN`/`COMMIT`/`ROLLBACK` with `pg` client
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
JWT-based auth with access tokens (short-lived, 15m) and refresh tokens (7d). Full `AuthService` implementation with `bcrypt` password comparison in [references/advanced-patterns.md](./advanced-patterns.md).
|
||||
|
||||
## Caching Strategies
|
||||
|
||||
Redis-backed `CacheService` with get/set/delete/invalidatePattern, plus a `@Cacheable` decorator for method-level caching. See [references/advanced-patterns.md](./advanced-patterns.md).
|
||||
|
||||
## API Response Format
|
||||
|
||||
Standardized `ApiResponse` helper with `success`, `error`, and `paginated` static methods. See [references/advanced-patterns.md](./advanced-patterns.md).
|
||||
@@ -0,0 +1,30 @@
|
||||
© 2025 Anthropic, PBC. All rights reserved.
|
||||
|
||||
LICENSE: Use of these materials (including all code, prompts, assets, files,
|
||||
and other components of this Skill) is governed by your agreement with
|
||||
Anthropic regarding use of Anthropic's services. If no separate agreement
|
||||
exists, use is governed by Anthropic's Consumer Terms of Service or
|
||||
Commercial Terms of Service, as applicable:
|
||||
https://www.anthropic.com/legal/consumer-terms
|
||||
https://www.anthropic.com/legal/commercial-terms
|
||||
Your applicable agreement is referred to as the "Agreement." "Services" are
|
||||
as defined in the Agreement.
|
||||
|
||||
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
|
||||
contrary, users may not:
|
||||
|
||||
- Extract these materials from the Services or retain copies of these
|
||||
materials outside the Services
|
||||
- Reproduce or copy these materials, except for temporary copies created
|
||||
automatically during authorized use of the Services
|
||||
- Create derivative works based on these materials
|
||||
- Distribute, sublicense, or transfer these materials to any third party
|
||||
- Make, offer to sell, sell, or import any inventions embodied in these
|
||||
materials
|
||||
- Reverse engineer, decompile, or disassemble these materials
|
||||
|
||||
The receipt, viewing, or possession of these materials does not convey or
|
||||
imply any license or right beyond those expressly granted above.
|
||||
|
||||
Anthropic retains all right, title, and interest in these materials,
|
||||
including all copyrights, patents, and other intellectual property rights.
|
||||
@@ -0,0 +1,314 @@
|
||||
---
|
||||
name: pdf
|
||||
description: Use this skill whenever the user wants to do anything with PDF files. This includes reading or extracting text/tables from PDFs, combining or merging multiple PDFs into one, splitting PDFs apart, rotating pages, adding watermarks, creating new PDFs, filling PDF forms, encrypting/decrypting PDFs, extracting images, and OCR on scanned PDFs to make them searchable. If the user mentions a .pdf file or asks to produce one, use this skill.
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# PDF Processing Guide
|
||||
|
||||
## Overview
|
||||
|
||||
This guide covers essential PDF processing operations using Python libraries and command-line tools. For advanced features, JavaScript libraries, and detailed examples, see REFERENCE.md. If you need to fill out a PDF form, read FORMS.md and follow its instructions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```python
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
# Read a PDF
|
||||
reader = PdfReader("document.pdf")
|
||||
print(f"Pages: {len(reader.pages)}")
|
||||
|
||||
# Extract text
|
||||
text = ""
|
||||
for page in reader.pages:
|
||||
text += page.extract_text()
|
||||
```
|
||||
|
||||
## Python Libraries
|
||||
|
||||
### pypdf - Basic Operations
|
||||
|
||||
#### Merge PDFs
|
||||
```python
|
||||
from pypdf import PdfWriter, PdfReader
|
||||
|
||||
writer = PdfWriter()
|
||||
for pdf_file in ["doc1.pdf", "doc2.pdf", "doc3.pdf"]:
|
||||
reader = PdfReader(pdf_file)
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
with open("merged.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
#### Split PDF
|
||||
```python
|
||||
reader = PdfReader("input.pdf")
|
||||
for i, page in enumerate(reader.pages):
|
||||
writer = PdfWriter()
|
||||
writer.add_page(page)
|
||||
with open(f"page_{i+1}.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
#### Extract Metadata
|
||||
```python
|
||||
reader = PdfReader("document.pdf")
|
||||
meta = reader.metadata
|
||||
print(f"Title: {meta.title}")
|
||||
print(f"Author: {meta.author}")
|
||||
print(f"Subject: {meta.subject}")
|
||||
print(f"Creator: {meta.creator}")
|
||||
```
|
||||
|
||||
#### Rotate Pages
|
||||
```python
|
||||
reader = PdfReader("input.pdf")
|
||||
writer = PdfWriter()
|
||||
|
||||
page = reader.pages[0]
|
||||
page.rotate(90) # Rotate 90 degrees clockwise
|
||||
writer.add_page(page)
|
||||
|
||||
with open("rotated.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
### pdfplumber - Text and Table Extraction
|
||||
|
||||
#### Extract Text with Layout
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
for page in pdf.pages:
|
||||
text = page.extract_text()
|
||||
print(text)
|
||||
```
|
||||
|
||||
#### Extract Tables
|
||||
```python
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
for i, page in enumerate(pdf.pages):
|
||||
tables = page.extract_tables()
|
||||
for j, table in enumerate(tables):
|
||||
print(f"Table {j+1} on page {i+1}:")
|
||||
for row in table:
|
||||
print(row)
|
||||
```
|
||||
|
||||
#### Advanced Table Extraction
|
||||
```python
|
||||
import pandas as pd
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
all_tables = []
|
||||
for page in pdf.pages:
|
||||
tables = page.extract_tables()
|
||||
for table in tables:
|
||||
if table: # Check if table is not empty
|
||||
df = pd.DataFrame(table[1:], columns=table[0])
|
||||
all_tables.append(df)
|
||||
|
||||
# Combine all tables
|
||||
if all_tables:
|
||||
combined_df = pd.concat(all_tables, ignore_index=True)
|
||||
combined_df.to_excel("extracted_tables.xlsx", index=False)
|
||||
```
|
||||
|
||||
### reportlab - Create PDFs
|
||||
|
||||
#### Basic PDF Creation
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.pdfgen import canvas
|
||||
|
||||
c = canvas.Canvas("hello.pdf", pagesize=letter)
|
||||
width, height = letter
|
||||
|
||||
# Add text
|
||||
c.drawString(100, height - 100, "Hello World!")
|
||||
c.drawString(100, height - 120, "This is a PDF created with reportlab")
|
||||
|
||||
# Add a line
|
||||
c.line(100, height - 140, 400, height - 140)
|
||||
|
||||
# Save
|
||||
c.save()
|
||||
```
|
||||
|
||||
#### Create PDF with Multiple Pages
|
||||
```python
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
|
||||
doc = SimpleDocTemplate("report.pdf", pagesize=letter)
|
||||
styles = getSampleStyleSheet()
|
||||
story = []
|
||||
|
||||
# Add content
|
||||
title = Paragraph("Report Title", styles['Title'])
|
||||
story.append(title)
|
||||
story.append(Spacer(1, 12))
|
||||
|
||||
body = Paragraph("This is the body of the report. " * 20, styles['Normal'])
|
||||
story.append(body)
|
||||
story.append(PageBreak())
|
||||
|
||||
# Page 2
|
||||
story.append(Paragraph("Page 2", styles['Heading1']))
|
||||
story.append(Paragraph("Content for page 2", styles['Normal']))
|
||||
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
```
|
||||
|
||||
#### Subscripts and Superscripts
|
||||
|
||||
**IMPORTANT**: Never use Unicode subscript/superscript characters (₀₁₂₃₄₅₆₇₈₉, ⁰¹²³⁴⁵⁶⁷⁸⁹) in ReportLab PDFs. The built-in fonts do not include these glyphs, causing them to render as solid black boxes.
|
||||
|
||||
Instead, use ReportLab's XML markup tags in Paragraph objects:
|
||||
```python
|
||||
from reportlab.platypus import Paragraph
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
|
||||
styles = getSampleStyleSheet()
|
||||
|
||||
# Subscripts: use <sub> tag
|
||||
chemical = Paragraph("H<sub>2</sub>O", styles['Normal'])
|
||||
|
||||
# Superscripts: use <super> tag
|
||||
squared = Paragraph("x<super>2</super> + y<super>2</super>", styles['Normal'])
|
||||
```
|
||||
|
||||
For canvas-drawn text (not Paragraph objects), manually adjust font the size and position rather than using Unicode subscripts/superscripts.
|
||||
|
||||
## Command-Line Tools
|
||||
|
||||
### pdftotext (poppler-utils)
|
||||
```bash
|
||||
# Extract text
|
||||
pdftotext input.pdf output.txt
|
||||
|
||||
# Extract text preserving layout
|
||||
pdftotext -layout input.pdf output.txt
|
||||
|
||||
# Extract specific pages
|
||||
pdftotext -f 1 -l 5 input.pdf output.txt # Pages 1-5
|
||||
```
|
||||
|
||||
### qpdf
|
||||
```bash
|
||||
# Merge PDFs
|
||||
qpdf --empty --pages file1.pdf file2.pdf -- merged.pdf
|
||||
|
||||
# Split pages
|
||||
qpdf input.pdf --pages . 1-5 -- pages1-5.pdf
|
||||
qpdf input.pdf --pages . 6-10 -- pages6-10.pdf
|
||||
|
||||
# Rotate pages
|
||||
qpdf input.pdf output.pdf --rotate=+90:1 # Rotate page 1 by 90 degrees
|
||||
|
||||
# Remove password
|
||||
qpdf --password=mypassword --decrypt encrypted.pdf decrypted.pdf
|
||||
```
|
||||
|
||||
### pdftk (if available)
|
||||
```bash
|
||||
# Merge
|
||||
pdftk file1.pdf file2.pdf cat output merged.pdf
|
||||
|
||||
# Split
|
||||
pdftk input.pdf burst
|
||||
|
||||
# Rotate
|
||||
pdftk input.pdf rotate 1east output rotated.pdf
|
||||
```
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Extract Text from Scanned PDFs
|
||||
```python
|
||||
# Requires: pip install pytesseract pdf2image
|
||||
import pytesseract
|
||||
from pdf2image import convert_from_path
|
||||
|
||||
# Convert PDF to images
|
||||
images = convert_from_path('scanned.pdf')
|
||||
|
||||
# OCR each page
|
||||
text = ""
|
||||
for i, image in enumerate(images):
|
||||
text += f"Page {i+1}:\n"
|
||||
text += pytesseract.image_to_string(image)
|
||||
text += "\n\n"
|
||||
|
||||
print(text)
|
||||
```
|
||||
|
||||
### Add Watermark
|
||||
```python
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
# Create watermark (or load existing)
|
||||
watermark = PdfReader("watermark.pdf").pages[0]
|
||||
|
||||
# Apply to all pages
|
||||
reader = PdfReader("document.pdf")
|
||||
writer = PdfWriter()
|
||||
|
||||
for page in reader.pages:
|
||||
page.merge_page(watermark)
|
||||
writer.add_page(page)
|
||||
|
||||
with open("watermarked.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
### Extract Images
|
||||
```bash
|
||||
# Using pdfimages (poppler-utils)
|
||||
pdfimages -j input.pdf output_prefix
|
||||
|
||||
# This extracts all images as output_prefix-000.jpg, output_prefix-001.jpg, etc.
|
||||
```
|
||||
|
||||
### Password Protection
|
||||
```python
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
reader = PdfReader("input.pdf")
|
||||
writer = PdfWriter()
|
||||
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
|
||||
# Add password
|
||||
writer.encrypt("userpassword", "ownerpassword")
|
||||
|
||||
with open("encrypted.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Best Tool | Command/Code |
|
||||
|------|-----------|--------------|
|
||||
| Merge PDFs | pypdf | `writer.add_page(page)` |
|
||||
| Split PDFs | pypdf | One page per file |
|
||||
| Extract text | pdfplumber | `page.extract_text()` |
|
||||
| Extract tables | pdfplumber | `page.extract_tables()` |
|
||||
| Create PDFs | reportlab | Canvas or Platypus |
|
||||
| Command line merge | qpdf | `qpdf --empty --pages ...` |
|
||||
| OCR scanned PDFs | pytesseract | Convert to image first |
|
||||
| Fill PDF forms | pdf-lib or pypdf (see FORMS.md) | See FORMS.md |
|
||||
|
||||
## Next Steps
|
||||
|
||||
- For advanced pypdfium2 usage, see REFERENCE.md
|
||||
- For JavaScript libraries (pdf-lib), see REFERENCE.md
|
||||
- If you need to fill out a PDF form, follow the instructions in FORMS.md
|
||||
- For troubleshooting guides, see REFERENCE.md
|
||||
@@ -0,0 +1,294 @@
|
||||
**CRITICAL: You MUST complete these steps in order. Do not skip ahead to writing code.**
|
||||
|
||||
If you need to fill out a PDF form, first check to see if the PDF has fillable form fields. Run this script from this file's directory:
|
||||
`python scripts/check_fillable_fields <file.pdf>`, and depending on the result go to either the "Fillable fields" or "Non-fillable fields" and follow those instructions.
|
||||
|
||||
# Fillable fields
|
||||
If the PDF has fillable form fields:
|
||||
- Run this script from this file's directory: `python scripts/extract_form_field_info.py <input.pdf> <field_info.json>`. It will create a JSON file with a list of fields in this format:
|
||||
```
|
||||
[
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"rect": ([left, bottom, right, top] bounding box in PDF coordinates, y=0 is the bottom of the page),
|
||||
"type": ("text", "checkbox", "radio_group", or "choice"),
|
||||
},
|
||||
// Checkboxes have "checked_value" and "unchecked_value" properties:
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"type": "checkbox",
|
||||
"checked_value": (Set the field to this value to check the checkbox),
|
||||
"unchecked_value": (Set the field to this value to uncheck the checkbox),
|
||||
},
|
||||
// Radio groups have a "radio_options" list with the possible choices.
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"type": "radio_group",
|
||||
"radio_options": [
|
||||
{
|
||||
"value": (set the field to this value to select this radio option),
|
||||
"rect": (bounding box for the radio button for this option)
|
||||
},
|
||||
// Other radio options
|
||||
]
|
||||
},
|
||||
// Multiple choice fields have a "choice_options" list with the possible choices:
|
||||
{
|
||||
"field_id": (unique ID for the field),
|
||||
"page": (page number, 1-based),
|
||||
"type": "choice",
|
||||
"choice_options": [
|
||||
{
|
||||
"value": (set the field to this value to select this option),
|
||||
"text": (display text of the option)
|
||||
},
|
||||
// Other choice options
|
||||
],
|
||||
}
|
||||
]
|
||||
```
|
||||
- Convert the PDF to PNGs (one image for each page) with this script (run from this file's directory):
|
||||
`python scripts/convert_pdf_to_images.py <file.pdf> <output_directory>`
|
||||
Then analyze the images to determine the purpose of each form field (make sure to convert the bounding box PDF coordinates to image coordinates).
|
||||
- Create a `field_values.json` file in this format with the values to be entered for each field:
|
||||
```
|
||||
[
|
||||
{
|
||||
"field_id": "last_name", // Must match the field_id from `extract_form_field_info.py`
|
||||
"description": "The user's last name",
|
||||
"page": 1, // Must match the "page" value in field_info.json
|
||||
"value": "Simpson"
|
||||
},
|
||||
{
|
||||
"field_id": "Checkbox12",
|
||||
"description": "Checkbox to be checked if the user is 18 or over",
|
||||
"page": 1,
|
||||
"value": "/On" // If this is a checkbox, use its "checked_value" value to check it. If it's a radio button group, use one of the "value" values in "radio_options".
|
||||
},
|
||||
// more fields
|
||||
]
|
||||
```
|
||||
- Run the `fill_fillable_fields.py` script from this file's directory to create a filled-in PDF:
|
||||
`python scripts/fill_fillable_fields.py <input pdf> <field_values.json> <output pdf>`
|
||||
This script will verify that the field IDs and values you provide are valid; if it prints error messages, correct the appropriate fields and try again.
|
||||
|
||||
# Non-fillable fields
|
||||
If the PDF doesn't have fillable form fields, you'll add text annotations. First try to extract coordinates from the PDF structure (more accurate), then fall back to visual estimation if needed.
|
||||
|
||||
## Step 1: Try Structure Extraction First
|
||||
|
||||
Run this script to extract text labels, lines, and checkboxes with their exact PDF coordinates:
|
||||
`python scripts/extract_form_structure.py <input.pdf> form_structure.json`
|
||||
|
||||
This creates a JSON file containing:
|
||||
- **labels**: Every text element with exact coordinates (x0, top, x1, bottom in PDF points)
|
||||
- **lines**: Horizontal lines that define row boundaries
|
||||
- **checkboxes**: Small square rectangles that are checkboxes (with center coordinates)
|
||||
- **row_boundaries**: Row top/bottom positions calculated from horizontal lines
|
||||
|
||||
**Check the results**: If `form_structure.json` has meaningful labels (text elements that correspond to form fields), use **Approach A: Structure-Based Coordinates**. If the PDF is scanned/image-based and has few or no labels, use **Approach B: Visual Estimation**.
|
||||
|
||||
---
|
||||
|
||||
## Approach A: Structure-Based Coordinates (Preferred)
|
||||
|
||||
Use this when `extract_form_structure.py` found text labels in the PDF.
|
||||
|
||||
### A.1: Analyze the Structure
|
||||
|
||||
Read form_structure.json and identify:
|
||||
|
||||
1. **Label groups**: Adjacent text elements that form a single label (e.g., "Last" + "Name")
|
||||
2. **Row structure**: Labels with similar `top` values are in the same row
|
||||
3. **Field columns**: Entry areas start after label ends (x0 = label.x1 + gap)
|
||||
4. **Checkboxes**: Use the checkbox coordinates directly from the structure
|
||||
|
||||
**Coordinate system**: PDF coordinates where y=0 is at TOP of page, y increases downward.
|
||||
|
||||
### A.2: Check for Missing Elements
|
||||
|
||||
The structure extraction may not detect all form elements. Common cases:
|
||||
- **Circular checkboxes**: Only square rectangles are detected as checkboxes
|
||||
- **Complex graphics**: Decorative elements or non-standard form controls
|
||||
- **Faded or light-colored elements**: May not be extracted
|
||||
|
||||
If you see form fields in the PDF images that aren't in form_structure.json, you'll need to use **visual analysis** for those specific fields (see "Hybrid Approach" below).
|
||||
|
||||
### A.3: Create fields.json with PDF Coordinates
|
||||
|
||||
For each field, calculate entry coordinates from the extracted structure:
|
||||
|
||||
**Text fields:**
|
||||
- entry x0 = label x1 + 5 (small gap after label)
|
||||
- entry x1 = next label's x0, or row boundary
|
||||
- entry top = same as label top
|
||||
- entry bottom = row boundary line below, or label bottom + row_height
|
||||
|
||||
**Checkboxes:**
|
||||
- Use the checkbox rectangle coordinates directly from form_structure.json
|
||||
- entry_bounding_box = [checkbox.x0, checkbox.top, checkbox.x1, checkbox.bottom]
|
||||
|
||||
Create fields.json using `pdf_width` and `pdf_height` (signals PDF coordinates):
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{"page_number": 1, "pdf_width": 612, "pdf_height": 792}
|
||||
],
|
||||
"form_fields": [
|
||||
{
|
||||
"page_number": 1,
|
||||
"description": "Last name entry field",
|
||||
"field_label": "Last Name",
|
||||
"label_bounding_box": [43, 63, 87, 73],
|
||||
"entry_bounding_box": [92, 63, 260, 79],
|
||||
"entry_text": {"text": "Smith", "font_size": 10}
|
||||
},
|
||||
{
|
||||
"page_number": 1,
|
||||
"description": "US Citizen Yes checkbox",
|
||||
"field_label": "Yes",
|
||||
"label_bounding_box": [260, 200, 280, 210],
|
||||
"entry_bounding_box": [285, 197, 292, 205],
|
||||
"entry_text": {"text": "X"}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Use `pdf_width`/`pdf_height` and coordinates directly from form_structure.json.
|
||||
|
||||
### A.4: Validate Bounding Boxes
|
||||
|
||||
Before filling, check your bounding boxes for errors:
|
||||
`python scripts/check_bounding_boxes.py fields.json`
|
||||
|
||||
This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling.
|
||||
|
||||
---
|
||||
|
||||
## Approach B: Visual Estimation (Fallback)
|
||||
|
||||
Use this when the PDF is scanned/image-based and structure extraction found no usable text labels (e.g., all text shows as "(cid:X)" patterns).
|
||||
|
||||
### B.1: Convert PDF to Images
|
||||
|
||||
`python scripts/convert_pdf_to_images.py <input.pdf> <images_dir/>`
|
||||
|
||||
### B.2: Initial Field Identification
|
||||
|
||||
Examine each page image to identify form sections and get **rough estimates** of field locations:
|
||||
- Form field labels and their approximate positions
|
||||
- Entry areas (lines, boxes, or blank spaces for text input)
|
||||
- Checkboxes and their approximate locations
|
||||
|
||||
For each field, note approximate pixel coordinates (they don't need to be precise yet).
|
||||
|
||||
### B.3: Zoom Refinement (CRITICAL for accuracy)
|
||||
|
||||
For each field, crop a region around the estimated position to refine coordinates precisely.
|
||||
|
||||
**Create a zoomed crop using ImageMagick:**
|
||||
```bash
|
||||
magick <page_image> -crop <width>x<height>+<x>+<y> +repage <crop_output.png>
|
||||
```
|
||||
|
||||
Where:
|
||||
- `<x>, <y>` = top-left corner of crop region (use your rough estimate minus padding)
|
||||
- `<width>, <height>` = size of crop region (field area plus ~50px padding on each side)
|
||||
|
||||
**Example:** To refine a "Name" field estimated around (100, 150):
|
||||
```bash
|
||||
magick images_dir/page_1.png -crop 300x80+50+120 +repage crops/name_field.png
|
||||
```
|
||||
|
||||
(Note: if the `magick` command isn't available, try `convert` with the same arguments).
|
||||
|
||||
**Examine the cropped image** to determine precise coordinates:
|
||||
1. Identify the exact pixel where the entry area begins (after the label)
|
||||
2. Identify where the entry area ends (before next field or edge)
|
||||
3. Identify the top and bottom of the entry line/box
|
||||
|
||||
**Convert crop coordinates back to full image coordinates:**
|
||||
- full_x = crop_x + crop_offset_x
|
||||
- full_y = crop_y + crop_offset_y
|
||||
|
||||
Example: If the crop started at (50, 120) and the entry box starts at (52, 18) within the crop:
|
||||
- entry_x0 = 52 + 50 = 102
|
||||
- entry_top = 18 + 120 = 138
|
||||
|
||||
**Repeat for each field**, grouping nearby fields into single crops when possible.
|
||||
|
||||
### B.4: Create fields.json with Refined Coordinates
|
||||
|
||||
Create fields.json using `image_width` and `image_height` (signals image coordinates):
|
||||
```json
|
||||
{
|
||||
"pages": [
|
||||
{"page_number": 1, "image_width": 1700, "image_height": 2200}
|
||||
],
|
||||
"form_fields": [
|
||||
{
|
||||
"page_number": 1,
|
||||
"description": "Last name entry field",
|
||||
"field_label": "Last Name",
|
||||
"label_bounding_box": [120, 175, 242, 198],
|
||||
"entry_bounding_box": [255, 175, 720, 218],
|
||||
"entry_text": {"text": "Smith", "font_size": 10}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Important**: Use `image_width`/`image_height` and the refined pixel coordinates from the zoom analysis.
|
||||
|
||||
### B.5: Validate Bounding Boxes
|
||||
|
||||
Before filling, check your bounding boxes for errors:
|
||||
`python scripts/check_bounding_boxes.py fields.json`
|
||||
|
||||
This checks for intersecting bounding boxes and entry boxes that are too small for the font size. Fix any reported errors before filling.
|
||||
|
||||
---
|
||||
|
||||
## Hybrid Approach: Structure + Visual
|
||||
|
||||
Use this when structure extraction works for most fields but misses some elements (e.g., circular checkboxes, unusual form controls).
|
||||
|
||||
1. **Use Approach A** for fields that were detected in form_structure.json
|
||||
2. **Convert PDF to images** for visual analysis of missing fields
|
||||
3. **Use zoom refinement** (from Approach B) for the missing fields
|
||||
4. **Combine coordinates**: For fields from structure extraction, use `pdf_width`/`pdf_height`. For visually-estimated fields, you must convert image coordinates to PDF coordinates:
|
||||
- pdf_x = image_x * (pdf_width / image_width)
|
||||
- pdf_y = image_y * (pdf_height / image_height)
|
||||
5. **Use a single coordinate system** in fields.json - convert all to PDF coordinates with `pdf_width`/`pdf_height`
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Validate Before Filling
|
||||
|
||||
**Always validate bounding boxes before filling:**
|
||||
`python scripts/check_bounding_boxes.py fields.json`
|
||||
|
||||
This checks for:
|
||||
- Intersecting bounding boxes (which would cause overlapping text)
|
||||
- Entry boxes that are too small for the specified font size
|
||||
|
||||
Fix any reported errors in fields.json before proceeding.
|
||||
|
||||
## Step 3: Fill the Form
|
||||
|
||||
The fill script auto-detects the coordinate system and handles conversion:
|
||||
`python scripts/fill_pdf_form_with_annotations.py <input.pdf> fields.json <output.pdf>`
|
||||
|
||||
## Step 4: Verify Output
|
||||
|
||||
Convert the filled PDF to images and verify text placement:
|
||||
`python scripts/convert_pdf_to_images.py <output.pdf> <verify_images/>`
|
||||
|
||||
If text is mispositioned:
|
||||
- **Approach A**: Check that you're using PDF coordinates from form_structure.json with `pdf_width`/`pdf_height`
|
||||
- **Approach B**: Check that image dimensions match and coordinates are accurate pixels
|
||||
- **Hybrid**: Ensure coordinate conversions are correct for visually-estimated fields
|
||||
@@ -0,0 +1,612 @@
|
||||
# PDF Processing Advanced Reference
|
||||
|
||||
This document contains advanced PDF processing features, detailed examples, and additional libraries not covered in the main skill instructions.
|
||||
|
||||
## pypdfium2 Library (Apache/BSD License)
|
||||
|
||||
### Overview
|
||||
pypdfium2 is a Python binding for PDFium (Chromium's PDF library). It's excellent for fast PDF rendering, image generation, and serves as a PyMuPDF replacement.
|
||||
|
||||
### Render PDF to Images
|
||||
```python
|
||||
import pypdfium2 as pdfium
|
||||
from PIL import Image
|
||||
|
||||
# Load PDF
|
||||
pdf = pdfium.PdfDocument("document.pdf")
|
||||
|
||||
# Render page to image
|
||||
page = pdf[0] # First page
|
||||
bitmap = page.render(
|
||||
scale=2.0, # Higher resolution
|
||||
rotation=0 # No rotation
|
||||
)
|
||||
|
||||
# Convert to PIL Image
|
||||
img = bitmap.to_pil()
|
||||
img.save("page_1.png", "PNG")
|
||||
|
||||
# Process multiple pages
|
||||
for i, page in enumerate(pdf):
|
||||
bitmap = page.render(scale=1.5)
|
||||
img = bitmap.to_pil()
|
||||
img.save(f"page_{i+1}.jpg", "JPEG", quality=90)
|
||||
```
|
||||
|
||||
### Extract Text with pypdfium2
|
||||
```python
|
||||
import pypdfium2 as pdfium
|
||||
|
||||
pdf = pdfium.PdfDocument("document.pdf")
|
||||
for i, page in enumerate(pdf):
|
||||
text = page.get_text()
|
||||
print(f"Page {i+1} text length: {len(text)} chars")
|
||||
```
|
||||
|
||||
## JavaScript Libraries
|
||||
|
||||
### pdf-lib (MIT License)
|
||||
|
||||
pdf-lib is a powerful JavaScript library for creating and modifying PDF documents in any JavaScript environment.
|
||||
|
||||
#### Load and Manipulate Existing PDF
|
||||
```javascript
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import fs from 'fs';
|
||||
|
||||
async function manipulatePDF() {
|
||||
// Load existing PDF
|
||||
const existingPdfBytes = fs.readFileSync('input.pdf');
|
||||
const pdfDoc = await PDFDocument.load(existingPdfBytes);
|
||||
|
||||
// Get page count
|
||||
const pageCount = pdfDoc.getPageCount();
|
||||
console.log(`Document has ${pageCount} pages`);
|
||||
|
||||
// Add new page
|
||||
const newPage = pdfDoc.addPage([600, 400]);
|
||||
newPage.drawText('Added by pdf-lib', {
|
||||
x: 100,
|
||||
y: 300,
|
||||
size: 16
|
||||
});
|
||||
|
||||
// Save modified PDF
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
fs.writeFileSync('modified.pdf', pdfBytes);
|
||||
}
|
||||
```
|
||||
|
||||
#### Create Complex PDFs from Scratch
|
||||
```javascript
|
||||
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
import fs from 'fs';
|
||||
|
||||
async function createPDF() {
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
|
||||
// Add fonts
|
||||
const helveticaFont = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const helveticaBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||
|
||||
// Add page
|
||||
const page = pdfDoc.addPage([595, 842]); // A4 size
|
||||
const { width, height } = page.getSize();
|
||||
|
||||
// Add text with styling
|
||||
page.drawText('Invoice #12345', {
|
||||
x: 50,
|
||||
y: height - 50,
|
||||
size: 18,
|
||||
font: helveticaBold,
|
||||
color: rgb(0.2, 0.2, 0.8)
|
||||
});
|
||||
|
||||
// Add rectangle (header background)
|
||||
page.drawRectangle({
|
||||
x: 40,
|
||||
y: height - 100,
|
||||
width: width - 80,
|
||||
height: 30,
|
||||
color: rgb(0.9, 0.9, 0.9)
|
||||
});
|
||||
|
||||
// Add table-like content
|
||||
const items = [
|
||||
['Item', 'Qty', 'Price', 'Total'],
|
||||
['Widget', '2', '$50', '$100'],
|
||||
['Gadget', '1', '$75', '$75']
|
||||
];
|
||||
|
||||
let yPos = height - 150;
|
||||
items.forEach(row => {
|
||||
let xPos = 50;
|
||||
row.forEach(cell => {
|
||||
page.drawText(cell, {
|
||||
x: xPos,
|
||||
y: yPos,
|
||||
size: 12,
|
||||
font: helveticaFont
|
||||
});
|
||||
xPos += 120;
|
||||
});
|
||||
yPos -= 25;
|
||||
});
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
fs.writeFileSync('created.pdf', pdfBytes);
|
||||
}
|
||||
```
|
||||
|
||||
#### Advanced Merge and Split Operations
|
||||
```javascript
|
||||
import { PDFDocument } from 'pdf-lib';
|
||||
import fs from 'fs';
|
||||
|
||||
async function mergePDFs() {
|
||||
// Create new document
|
||||
const mergedPdf = await PDFDocument.create();
|
||||
|
||||
// Load source PDFs
|
||||
const pdf1Bytes = fs.readFileSync('doc1.pdf');
|
||||
const pdf2Bytes = fs.readFileSync('doc2.pdf');
|
||||
|
||||
const pdf1 = await PDFDocument.load(pdf1Bytes);
|
||||
const pdf2 = await PDFDocument.load(pdf2Bytes);
|
||||
|
||||
// Copy pages from first PDF
|
||||
const pdf1Pages = await mergedPdf.copyPages(pdf1, pdf1.getPageIndices());
|
||||
pdf1Pages.forEach(page => mergedPdf.addPage(page));
|
||||
|
||||
// Copy specific pages from second PDF (pages 0, 2, 4)
|
||||
const pdf2Pages = await mergedPdf.copyPages(pdf2, [0, 2, 4]);
|
||||
pdf2Pages.forEach(page => mergedPdf.addPage(page));
|
||||
|
||||
const mergedPdfBytes = await mergedPdf.save();
|
||||
fs.writeFileSync('merged.pdf', mergedPdfBytes);
|
||||
}
|
||||
```
|
||||
|
||||
### pdfjs-dist (Apache License)
|
||||
|
||||
PDF.js is Mozilla's JavaScript library for rendering PDFs in the browser.
|
||||
|
||||
#### Basic PDF Loading and Rendering
|
||||
```javascript
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
// Configure worker (important for performance)
|
||||
pdfjsLib.GlobalWorkerOptions.workerSrc = './pdf.worker.js';
|
||||
|
||||
async function renderPDF() {
|
||||
// Load PDF
|
||||
const loadingTask = pdfjsLib.getDocument('document.pdf');
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
console.log(`Loaded PDF with ${pdf.numPages} pages`);
|
||||
|
||||
// Get first page
|
||||
const page = await pdf.getPage(1);
|
||||
const viewport = page.getViewport({ scale: 1.5 });
|
||||
|
||||
// Render to canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
document.body.appendChild(canvas);
|
||||
}
|
||||
```
|
||||
|
||||
#### Extract Text with Coordinates
|
||||
```javascript
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
async function extractText() {
|
||||
const loadingTask = pdfjsLib.getDocument('document.pdf');
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
let fullText = '';
|
||||
|
||||
// Extract text from all pages
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const textContent = await page.getTextContent();
|
||||
|
||||
const pageText = textContent.items
|
||||
.map(item => item.str)
|
||||
.join(' ');
|
||||
|
||||
fullText += `\n--- Page ${i} ---\n${pageText}`;
|
||||
|
||||
// Get text with coordinates for advanced processing
|
||||
const textWithCoords = textContent.items.map(item => ({
|
||||
text: item.str,
|
||||
x: item.transform[4],
|
||||
y: item.transform[5],
|
||||
width: item.width,
|
||||
height: item.height
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(fullText);
|
||||
return fullText;
|
||||
}
|
||||
```
|
||||
|
||||
#### Extract Annotations and Forms
|
||||
```javascript
|
||||
import * as pdfjsLib from 'pdfjs-dist';
|
||||
|
||||
async function extractAnnotations() {
|
||||
const loadingTask = pdfjsLib.getDocument('annotated.pdf');
|
||||
const pdf = await loadingTask.promise;
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const annotations = await page.getAnnotations();
|
||||
|
||||
annotations.forEach(annotation => {
|
||||
console.log(`Annotation type: ${annotation.subtype}`);
|
||||
console.log(`Content: ${annotation.contents}`);
|
||||
console.log(`Coordinates: ${JSON.stringify(annotation.rect)}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Command-Line Operations
|
||||
|
||||
### poppler-utils Advanced Features
|
||||
|
||||
#### Extract Text with Bounding Box Coordinates
|
||||
```bash
|
||||
# Extract text with bounding box coordinates (essential for structured data)
|
||||
pdftotext -bbox-layout document.pdf output.xml
|
||||
|
||||
# The XML output contains precise coordinates for each text element
|
||||
```
|
||||
|
||||
#### Advanced Image Conversion
|
||||
```bash
|
||||
# Convert to PNG images with specific resolution
|
||||
pdftoppm -png -r 300 document.pdf output_prefix
|
||||
|
||||
# Convert specific page range with high resolution
|
||||
pdftoppm -png -r 600 -f 1 -l 3 document.pdf high_res_pages
|
||||
|
||||
# Convert to JPEG with quality setting
|
||||
pdftoppm -jpeg -jpegopt quality=85 -r 200 document.pdf jpeg_output
|
||||
```
|
||||
|
||||
#### Extract Embedded Images
|
||||
```bash
|
||||
# Extract all embedded images with metadata
|
||||
pdfimages -j -p document.pdf page_images
|
||||
|
||||
# List image info without extracting
|
||||
pdfimages -list document.pdf
|
||||
|
||||
# Extract images in their original format
|
||||
pdfimages -all document.pdf images/img
|
||||
```
|
||||
|
||||
### qpdf Advanced Features
|
||||
|
||||
#### Complex Page Manipulation
|
||||
```bash
|
||||
# Split PDF into groups of pages
|
||||
qpdf --split-pages=3 input.pdf output_group_%02d.pdf
|
||||
|
||||
# Extract specific pages with complex ranges
|
||||
qpdf input.pdf --pages input.pdf 1,3-5,8,10-end -- extracted.pdf
|
||||
|
||||
# Merge specific pages from multiple PDFs
|
||||
qpdf --empty --pages doc1.pdf 1-3 doc2.pdf 5-7 doc3.pdf 2,4 -- combined.pdf
|
||||
```
|
||||
|
||||
#### PDF Optimization and Repair
|
||||
```bash
|
||||
# Optimize PDF for web (linearize for streaming)
|
||||
qpdf --linearize input.pdf optimized.pdf
|
||||
|
||||
# Remove unused objects and compress
|
||||
qpdf --optimize-level=all input.pdf compressed.pdf
|
||||
|
||||
# Attempt to repair corrupted PDF structure
|
||||
qpdf --check input.pdf
|
||||
qpdf --fix-qdf damaged.pdf repaired.pdf
|
||||
|
||||
# Show detailed PDF structure for debugging
|
||||
qpdf --show-all-pages input.pdf > structure.txt
|
||||
```
|
||||
|
||||
#### Advanced Encryption
|
||||
```bash
|
||||
# Add password protection with specific permissions
|
||||
qpdf --encrypt user_pass owner_pass 256 --print=none --modify=none -- input.pdf encrypted.pdf
|
||||
|
||||
# Check encryption status
|
||||
qpdf --show-encryption encrypted.pdf
|
||||
|
||||
# Remove password protection (requires password)
|
||||
qpdf --password=secret123 --decrypt encrypted.pdf decrypted.pdf
|
||||
```
|
||||
|
||||
## Advanced Python Techniques
|
||||
|
||||
### pdfplumber Advanced Features
|
||||
|
||||
#### Extract Text with Precise Coordinates
|
||||
```python
|
||||
import pdfplumber
|
||||
|
||||
with pdfplumber.open("document.pdf") as pdf:
|
||||
page = pdf.pages[0]
|
||||
|
||||
# Extract all text with coordinates
|
||||
chars = page.chars
|
||||
for char in chars[:10]: # First 10 characters
|
||||
print(f"Char: '{char['text']}' at x:{char['x0']:.1f} y:{char['y0']:.1f}")
|
||||
|
||||
# Extract text by bounding box (left, top, right, bottom)
|
||||
bbox_text = page.within_bbox((100, 100, 400, 200)).extract_text()
|
||||
```
|
||||
|
||||
#### Advanced Table Extraction with Custom Settings
|
||||
```python
|
||||
import pdfplumber
|
||||
import pandas as pd
|
||||
|
||||
with pdfplumber.open("complex_table.pdf") as pdf:
|
||||
page = pdf.pages[0]
|
||||
|
||||
# Extract tables with custom settings for complex layouts
|
||||
table_settings = {
|
||||
"vertical_strategy": "lines",
|
||||
"horizontal_strategy": "lines",
|
||||
"snap_tolerance": 3,
|
||||
"intersection_tolerance": 15
|
||||
}
|
||||
tables = page.extract_tables(table_settings)
|
||||
|
||||
# Visual debugging for table extraction
|
||||
img = page.to_image(resolution=150)
|
||||
img.save("debug_layout.png")
|
||||
```
|
||||
|
||||
### reportlab Advanced Features
|
||||
|
||||
#### Create Professional Reports with Tables
|
||||
```python
|
||||
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
|
||||
from reportlab.lib.styles import getSampleStyleSheet
|
||||
from reportlab.lib import colors
|
||||
|
||||
# Sample data
|
||||
data = [
|
||||
['Product', 'Q1', 'Q2', 'Q3', 'Q4'],
|
||||
['Widgets', '120', '135', '142', '158'],
|
||||
['Gadgets', '85', '92', '98', '105']
|
||||
]
|
||||
|
||||
# Create PDF with table
|
||||
doc = SimpleDocTemplate("report.pdf")
|
||||
elements = []
|
||||
|
||||
# Add title
|
||||
styles = getSampleStyleSheet()
|
||||
title = Paragraph("Quarterly Sales Report", styles['Title'])
|
||||
elements.append(title)
|
||||
|
||||
# Add table with advanced styling
|
||||
table = Table(data)
|
||||
table.setStyle(TableStyle([
|
||||
('BACKGROUND', (0, 0), (-1, 0), colors.grey),
|
||||
('TEXTCOLOR', (0, 0), (-1, 0), colors.whitesmoke),
|
||||
('ALIGN', (0, 0), (-1, -1), 'CENTER'),
|
||||
('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
|
||||
('FONTSIZE', (0, 0), (-1, 0), 14),
|
||||
('BOTTOMPADDING', (0, 0), (-1, 0), 12),
|
||||
('BACKGROUND', (0, 1), (-1, -1), colors.beige),
|
||||
('GRID', (0, 0), (-1, -1), 1, colors.black)
|
||||
]))
|
||||
elements.append(table)
|
||||
|
||||
doc.build(elements)
|
||||
```
|
||||
|
||||
## Complex Workflows
|
||||
|
||||
### Extract Figures/Images from PDF
|
||||
|
||||
#### Method 1: Using pdfimages (fastest)
|
||||
```bash
|
||||
# Extract all images with original quality
|
||||
pdfimages -all document.pdf images/img
|
||||
```
|
||||
|
||||
#### Method 2: Using pypdfium2 + Image Processing
|
||||
```python
|
||||
import pypdfium2 as pdfium
|
||||
from PIL import Image
|
||||
import numpy as np
|
||||
|
||||
def extract_figures(pdf_path, output_dir):
|
||||
pdf = pdfium.PdfDocument(pdf_path)
|
||||
|
||||
for page_num, page in enumerate(pdf):
|
||||
# Render high-resolution page
|
||||
bitmap = page.render(scale=3.0)
|
||||
img = bitmap.to_pil()
|
||||
|
||||
# Convert to numpy for processing
|
||||
img_array = np.array(img)
|
||||
|
||||
# Simple figure detection (non-white regions)
|
||||
mask = np.any(img_array != [255, 255, 255], axis=2)
|
||||
|
||||
# Find contours and extract bounding boxes
|
||||
# (This is simplified - real implementation would need more sophisticated detection)
|
||||
|
||||
# Save detected figures
|
||||
# ... implementation depends on specific needs
|
||||
```
|
||||
|
||||
### Batch PDF Processing with Error Handling
|
||||
```python
|
||||
import os
|
||||
import glob
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def batch_process_pdfs(input_dir, operation='merge'):
|
||||
pdf_files = glob.glob(os.path.join(input_dir, "*.pdf"))
|
||||
|
||||
if operation == 'merge':
|
||||
writer = PdfWriter()
|
||||
for pdf_file in pdf_files:
|
||||
try:
|
||||
reader = PdfReader(pdf_file)
|
||||
for page in reader.pages:
|
||||
writer.add_page(page)
|
||||
logger.info(f"Processed: {pdf_file}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to process {pdf_file}: {e}")
|
||||
continue
|
||||
|
||||
with open("batch_merged.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
|
||||
elif operation == 'extract_text':
|
||||
for pdf_file in pdf_files:
|
||||
try:
|
||||
reader = PdfReader(pdf_file)
|
||||
text = ""
|
||||
for page in reader.pages:
|
||||
text += page.extract_text()
|
||||
|
||||
output_file = pdf_file.replace('.pdf', '.txt')
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(text)
|
||||
logger.info(f"Extracted text from: {pdf_file}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to extract text from {pdf_file}: {e}")
|
||||
continue
|
||||
```
|
||||
|
||||
### Advanced PDF Cropping
|
||||
```python
|
||||
from pypdf import PdfWriter, PdfReader
|
||||
|
||||
reader = PdfReader("input.pdf")
|
||||
writer = PdfWriter()
|
||||
|
||||
# Crop page (left, bottom, right, top in points)
|
||||
page = reader.pages[0]
|
||||
page.mediabox.left = 50
|
||||
page.mediabox.bottom = 50
|
||||
page.mediabox.right = 550
|
||||
page.mediabox.top = 750
|
||||
|
||||
writer.add_page(page)
|
||||
with open("cropped.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
## Performance Optimization Tips
|
||||
|
||||
### 1. For Large PDFs
|
||||
- Use streaming approaches instead of loading entire PDF in memory
|
||||
- Use `qpdf --split-pages` for splitting large files
|
||||
- Process pages individually with pypdfium2
|
||||
|
||||
### 2. For Text Extraction
|
||||
- `pdftotext -bbox-layout` is fastest for plain text extraction
|
||||
- Use pdfplumber for structured data and tables
|
||||
- Avoid `pypdf.extract_text()` for very large documents
|
||||
|
||||
### 3. For Image Extraction
|
||||
- `pdfimages` is much faster than rendering pages
|
||||
- Use low resolution for previews, high resolution for final output
|
||||
|
||||
### 4. For Form Filling
|
||||
- pdf-lib maintains form structure better than most alternatives
|
||||
- Pre-validate form fields before processing
|
||||
|
||||
### 5. Memory Management
|
||||
```python
|
||||
# Process PDFs in chunks
|
||||
def process_large_pdf(pdf_path, chunk_size=10):
|
||||
reader = PdfReader(pdf_path)
|
||||
total_pages = len(reader.pages)
|
||||
|
||||
for start_idx in range(0, total_pages, chunk_size):
|
||||
end_idx = min(start_idx + chunk_size, total_pages)
|
||||
writer = PdfWriter()
|
||||
|
||||
for i in range(start_idx, end_idx):
|
||||
writer.add_page(reader.pages[i])
|
||||
|
||||
# Process chunk
|
||||
with open(f"chunk_{start_idx//chunk_size}.pdf", "wb") as output:
|
||||
writer.write(output)
|
||||
```
|
||||
|
||||
## Troubleshooting Common Issues
|
||||
|
||||
### Encrypted PDFs
|
||||
```python
|
||||
# Handle password-protected PDFs
|
||||
from pypdf import PdfReader
|
||||
|
||||
try:
|
||||
reader = PdfReader("encrypted.pdf")
|
||||
if reader.is_encrypted:
|
||||
reader.decrypt("password")
|
||||
except Exception as e:
|
||||
print(f"Failed to decrypt: {e}")
|
||||
```
|
||||
|
||||
### Corrupted PDFs
|
||||
```bash
|
||||
# Use qpdf to repair
|
||||
qpdf --check corrupted.pdf
|
||||
qpdf --replace-input corrupted.pdf
|
||||
```
|
||||
|
||||
### Text Extraction Issues
|
||||
```python
|
||||
# Fallback to OCR for scanned PDFs
|
||||
import pytesseract
|
||||
from pdf2image import convert_from_path
|
||||
|
||||
def extract_text_with_ocr(pdf_path):
|
||||
images = convert_from_path(pdf_path)
|
||||
text = ""
|
||||
for i, image in enumerate(images):
|
||||
text += pytesseract.image_to_string(image)
|
||||
return text
|
||||
```
|
||||
|
||||
## License Information
|
||||
|
||||
- **pypdf**: BSD License
|
||||
- **pdfplumber**: MIT License
|
||||
- **pypdfium2**: Apache/BSD License
|
||||
- **reportlab**: BSD License
|
||||
- **poppler-utils**: GPL-2 License
|
||||
- **qpdf**: Apache License
|
||||
- **pdf-lib**: MIT License
|
||||
- **pdfjs-dist**: Apache License
|
||||
@@ -0,0 +1,65 @@
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
import sys
|
||||
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class RectAndField:
|
||||
rect: list[float]
|
||||
rect_type: str
|
||||
field: dict
|
||||
|
||||
|
||||
def get_bounding_box_messages(fields_json_stream) -> list[str]:
|
||||
messages = []
|
||||
fields = json.load(fields_json_stream)
|
||||
messages.append(f"Read {len(fields['form_fields'])} fields")
|
||||
|
||||
def rects_intersect(r1, r2):
|
||||
disjoint_horizontal = r1[0] >= r2[2] or r1[2] <= r2[0]
|
||||
disjoint_vertical = r1[1] >= r2[3] or r1[3] <= r2[1]
|
||||
return not (disjoint_horizontal or disjoint_vertical)
|
||||
|
||||
rects_and_fields = []
|
||||
for f in fields["form_fields"]:
|
||||
rects_and_fields.append(RectAndField(f["label_bounding_box"], "label", f))
|
||||
rects_and_fields.append(RectAndField(f["entry_bounding_box"], "entry", f))
|
||||
|
||||
has_error = False
|
||||
for i, ri in enumerate(rects_and_fields):
|
||||
for j in range(i + 1, len(rects_and_fields)):
|
||||
rj = rects_and_fields[j]
|
||||
if ri.field["page_number"] == rj.field["page_number"] and rects_intersect(ri.rect, rj.rect):
|
||||
has_error = True
|
||||
if ri.field is rj.field:
|
||||
messages.append(f"FAILURE: intersection between label and entry bounding boxes for `{ri.field['description']}` ({ri.rect}, {rj.rect})")
|
||||
else:
|
||||
messages.append(f"FAILURE: intersection between {ri.rect_type} bounding box for `{ri.field['description']}` ({ri.rect}) and {rj.rect_type} bounding box for `{rj.field['description']}` ({rj.rect})")
|
||||
if len(messages) >= 20:
|
||||
messages.append("Aborting further checks; fix bounding boxes and try again")
|
||||
return messages
|
||||
if ri.rect_type == "entry":
|
||||
if "entry_text" in ri.field:
|
||||
font_size = ri.field["entry_text"].get("font_size", 14)
|
||||
entry_height = ri.rect[3] - ri.rect[1]
|
||||
if entry_height < font_size:
|
||||
has_error = True
|
||||
messages.append(f"FAILURE: entry bounding box height ({entry_height}) for `{ri.field['description']}` is too short for the text content (font size: {font_size}). Increase the box height or decrease the font size.")
|
||||
if len(messages) >= 20:
|
||||
messages.append("Aborting further checks; fix bounding boxes and try again")
|
||||
return messages
|
||||
|
||||
if not has_error:
|
||||
messages.append("SUCCESS: All bounding boxes are valid")
|
||||
return messages
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: check_bounding_boxes.py [fields.json]")
|
||||
sys.exit(1)
|
||||
with open(sys.argv[1]) as f:
|
||||
messages = get_bounding_box_messages(f)
|
||||
for msg in messages:
|
||||
print(msg)
|
||||
@@ -0,0 +1,11 @@
|
||||
import sys
|
||||
from pypdf import PdfReader
|
||||
|
||||
|
||||
|
||||
|
||||
reader = PdfReader(sys.argv[1])
|
||||
if (reader.get_fields()):
|
||||
print("This PDF has fillable form fields")
|
||||
else:
|
||||
print("This PDF does not have fillable form fields; you will need to visually determine where to enter data")
|
||||
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pdf2image import convert_from_path
|
||||
|
||||
|
||||
|
||||
|
||||
def convert(pdf_path, output_dir, max_dim=1000):
|
||||
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")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: convert_pdf_to_images.py [input pdf] [output directory]")
|
||||
sys.exit(1)
|
||||
pdf_path = sys.argv[1]
|
||||
output_directory = sys.argv[2]
|
||||
convert(pdf_path, output_directory)
|
||||
@@ -0,0 +1,37 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
|
||||
|
||||
|
||||
def create_validation_image(page_number, fields_json_path, input_path, output_path):
|
||||
with open(fields_json_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
img = Image.open(input_path)
|
||||
draw = ImageDraw.Draw(img)
|
||||
num_boxes = 0
|
||||
|
||||
for field in data["form_fields"]:
|
||||
if field["page_number"] == page_number:
|
||||
entry_box = field['entry_bounding_box']
|
||||
label_box = field['label_bounding_box']
|
||||
draw.rectangle(entry_box, outline='red', width=2)
|
||||
draw.rectangle(label_box, outline='blue', width=2)
|
||||
num_boxes += 2
|
||||
|
||||
img.save(output_path)
|
||||
print(f"Created validation image at {output_path} with {num_boxes} bounding boxes")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 5:
|
||||
print("Usage: create_validation_image.py [page number] [fields.json file] [input image path] [output image path]")
|
||||
sys.exit(1)
|
||||
page_number = int(sys.argv[1])
|
||||
fields_json_path = sys.argv[2]
|
||||
input_image_path = sys.argv[3]
|
||||
output_image_path = sys.argv[4]
|
||||
create_validation_image(page_number, fields_json_path, input_image_path, output_image_path)
|
||||
@@ -0,0 +1,122 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from pypdf import PdfReader
|
||||
|
||||
|
||||
|
||||
|
||||
def get_full_annotation_field_id(annotation):
|
||||
components = []
|
||||
while annotation:
|
||||
field_name = annotation.get('/T')
|
||||
if field_name:
|
||||
components.append(field_name)
|
||||
annotation = annotation.get('/Parent')
|
||||
return ".".join(reversed(components)) if components else None
|
||||
|
||||
|
||||
def make_field_dict(field, field_id):
|
||||
field_dict = {"field_id": field_id}
|
||||
ft = field.get('/FT')
|
||||
if ft == "/Tx":
|
||||
field_dict["type"] = "text"
|
||||
elif ft == "/Btn":
|
||||
field_dict["type"] = "checkbox"
|
||||
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: PdfReader):
|
||||
fields = reader.get_fields()
|
||||
|
||||
field_info_by_id = {}
|
||||
possible_radio_names = 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 = {}
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
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")
|
||||
|
||||
def sort_key(f):
|
||||
if "radio_options" in f:
|
||||
rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0]
|
||||
else:
|
||||
rect = f.get("rect") or [0, 0, 0, 0]
|
||||
adjusted_position = [-rect[1], rect[0]]
|
||||
return [f.get("page"), adjusted_position]
|
||||
|
||||
sorted_fields = fields_with_location + list(radio_fields_by_id.values())
|
||||
sorted_fields.sort(key=sort_key)
|
||||
|
||||
return sorted_fields
|
||||
|
||||
|
||||
def write_field_info(pdf_path: str, json_output_path: str):
|
||||
reader = PdfReader(pdf_path)
|
||||
field_info = get_field_info(reader)
|
||||
with open(json_output_path, "w") as f:
|
||||
json.dump(field_info, f, indent=2)
|
||||
print(f"Wrote {len(field_info)} fields to {json_output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: extract_form_field_info.py [input pdf] [output json]")
|
||||
sys.exit(1)
|
||||
write_field_info(sys.argv[1], sys.argv[2])
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Extract form structure from a non-fillable PDF.
|
||||
|
||||
This script analyzes the PDF to find:
|
||||
- Text labels with their exact coordinates
|
||||
- Horizontal lines (row boundaries)
|
||||
- Checkboxes (small rectangles)
|
||||
|
||||
Output: A JSON file with the form structure that can be used to generate
|
||||
accurate field coordinates for filling.
|
||||
|
||||
Usage: python extract_form_structure.py <input.pdf> <output.json>
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import pdfplumber
|
||||
|
||||
|
||||
def extract_form_structure(pdf_path):
|
||||
structure = {
|
||||
"pages": [],
|
||||
"labels": [],
|
||||
"lines": [],
|
||||
"checkboxes": [],
|
||||
"row_boundaries": []
|
||||
}
|
||||
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
for page_num, page in enumerate(pdf.pages, 1):
|
||||
structure["pages"].append({
|
||||
"page_number": page_num,
|
||||
"width": float(page.width),
|
||||
"height": float(page.height)
|
||||
})
|
||||
|
||||
words = page.extract_words()
|
||||
for word in words:
|
||||
structure["labels"].append({
|
||||
"page": page_num,
|
||||
"text": word["text"],
|
||||
"x0": round(float(word["x0"]), 1),
|
||||
"top": round(float(word["top"]), 1),
|
||||
"x1": round(float(word["x1"]), 1),
|
||||
"bottom": round(float(word["bottom"]), 1)
|
||||
})
|
||||
|
||||
for line in page.lines:
|
||||
if abs(float(line["x1"]) - float(line["x0"])) > page.width * 0.5:
|
||||
structure["lines"].append({
|
||||
"page": page_num,
|
||||
"y": round(float(line["top"]), 1),
|
||||
"x0": round(float(line["x0"]), 1),
|
||||
"x1": round(float(line["x1"]), 1)
|
||||
})
|
||||
|
||||
for rect in page.rects:
|
||||
width = float(rect["x1"]) - float(rect["x0"])
|
||||
height = float(rect["bottom"]) - float(rect["top"])
|
||||
if 5 <= width <= 15 and 5 <= height <= 15 and abs(width - height) < 2:
|
||||
structure["checkboxes"].append({
|
||||
"page": page_num,
|
||||
"x0": round(float(rect["x0"]), 1),
|
||||
"top": round(float(rect["top"]), 1),
|
||||
"x1": round(float(rect["x1"]), 1),
|
||||
"bottom": round(float(rect["bottom"]), 1),
|
||||
"center_x": round((float(rect["x0"]) + float(rect["x1"])) / 2, 1),
|
||||
"center_y": round((float(rect["top"]) + float(rect["bottom"])) / 2, 1)
|
||||
})
|
||||
|
||||
lines_by_page = {}
|
||||
for line in structure["lines"]:
|
||||
page = line["page"]
|
||||
if page not in lines_by_page:
|
||||
lines_by_page[page] = []
|
||||
lines_by_page[page].append(line["y"])
|
||||
|
||||
for page, y_coords in lines_by_page.items():
|
||||
y_coords = sorted(set(y_coords))
|
||||
for i in range(len(y_coords) - 1):
|
||||
structure["row_boundaries"].append({
|
||||
"page": page,
|
||||
"row_top": y_coords[i],
|
||||
"row_bottom": y_coords[i + 1],
|
||||
"row_height": round(y_coords[i + 1] - y_coords[i], 1)
|
||||
})
|
||||
|
||||
return structure
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) != 3:
|
||||
print("Usage: extract_form_structure.py <input.pdf> <output.json>")
|
||||
sys.exit(1)
|
||||
|
||||
pdf_path = sys.argv[1]
|
||||
output_path = sys.argv[2]
|
||||
|
||||
print(f"Extracting structure from {pdf_path}...")
|
||||
structure = extract_form_structure(pdf_path)
|
||||
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(structure, f, indent=2)
|
||||
|
||||
print(f"Found:")
|
||||
print(f" - {len(structure['pages'])} pages")
|
||||
print(f" - {len(structure['labels'])} text labels")
|
||||
print(f" - {len(structure['lines'])} horizontal lines")
|
||||
print(f" - {len(structure['checkboxes'])} checkboxes")
|
||||
print(f" - {len(structure['row_boundaries'])} row boundaries")
|
||||
print(f"Saved to {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,98 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
|
||||
from extract_form_field_info import get_field_info
|
||||
|
||||
|
||||
|
||||
|
||||
def fill_pdf_fields(input_pdf_path: str, fields_json_path: str, output_pdf_path: str):
|
||||
with open(fields_json_path) as f:
|
||||
fields = json.load(f)
|
||||
fields_by_page = {}
|
||||
for field in fields:
|
||||
if "value" in field:
|
||||
field_id = field["field_id"]
|
||||
page = field["page"]
|
||||
if page not in fields_by_page:
|
||||
fields_by_page[page] = {}
|
||||
fields_by_page[page][field_id] = field["value"]
|
||||
|
||||
reader = PdfReader(input_pdf_path)
|
||||
|
||||
has_error = False
|
||||
field_info = get_field_info(reader)
|
||||
fields_by_ids = {f["field_id"]: f for f in field_info}
|
||||
for field in fields:
|
||||
existing_field = fields_by_ids.get(field["field_id"])
|
||||
if not existing_field:
|
||||
has_error = True
|
||||
print(f"ERROR: `{field['field_id']}` is not a valid field ID")
|
||||
elif field["page"] != existing_field["page"]:
|
||||
has_error = True
|
||||
print(f"ERROR: Incorrect page number for `{field['field_id']}` (got {field['page']}, expected {existing_field['page']})")
|
||||
else:
|
||||
if "value" in field:
|
||||
err = validation_error_for_field_value(existing_field, field["value"])
|
||||
if err:
|
||||
print(err)
|
||||
has_error = True
|
||||
if has_error:
|
||||
sys.exit(1)
|
||||
|
||||
writer = PdfWriter(clone_from=reader)
|
||||
for page, field_values in fields_by_page.items():
|
||||
writer.update_page_form_field_values(writer.pages[page - 1], field_values, auto_regenerate=False)
|
||||
|
||||
writer.set_need_appearances_writer(True)
|
||||
|
||||
with open(output_pdf_path, "wb") as f:
|
||||
writer.write(f)
|
||||
|
||||
|
||||
def validation_error_for_field_value(field_info, field_value):
|
||||
field_type = field_info["type"]
|
||||
field_id = field_info["field_id"]
|
||||
if field_type == "checkbox":
|
||||
checked_val = field_info["checked_value"]
|
||||
unchecked_val = field_info["unchecked_value"]
|
||||
if field_value != checked_val and field_value != unchecked_val:
|
||||
return f'ERROR: Invalid value "{field_value}" for checkbox field "{field_id}". The checked value is "{checked_val}" and the unchecked value is "{unchecked_val}"'
|
||||
elif field_type == "radio_group":
|
||||
option_values = [opt["value"] for opt in field_info["radio_options"]]
|
||||
if field_value not in option_values:
|
||||
return f'ERROR: Invalid value "{field_value}" for radio group field "{field_id}". Valid values are: {option_values}'
|
||||
elif field_type == "choice":
|
||||
choice_values = [opt["value"] for opt in field_info["choice_options"]]
|
||||
if field_value not in choice_values:
|
||||
return f'ERROR: Invalid value "{field_value}" for choice field "{field_id}". Valid values are: {choice_values}'
|
||||
return None
|
||||
|
||||
|
||||
def monkeypatch_pydpf_method():
|
||||
from pypdf.generic import DictionaryObject
|
||||
from pypdf.constants import FieldDictionaryAttributes
|
||||
|
||||
original_get_inherited = DictionaryObject.get_inherited
|
||||
|
||||
def patched_get_inherited(self, key: str, default = None):
|
||||
result = original_get_inherited(self, key, default)
|
||||
if key == FieldDictionaryAttributes.Opt:
|
||||
if isinstance(result, list) and all(isinstance(v, list) and len(v) == 2 for v in result):
|
||||
result = [r[0] for r in result]
|
||||
return result
|
||||
|
||||
DictionaryObject.get_inherited = patched_get_inherited
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: fill_fillable_fields.py [input pdf] [field_values.json] [output pdf]")
|
||||
sys.exit(1)
|
||||
monkeypatch_pydpf_method()
|
||||
input_pdf = sys.argv[1]
|
||||
fields_json = sys.argv[2]
|
||||
output_pdf = sys.argv[3]
|
||||
fill_pdf_fields(input_pdf, fields_json, output_pdf)
|
||||
@@ -0,0 +1,107 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
from pypdf import PdfReader, PdfWriter
|
||||
from pypdf.annotations import FreeText
|
||||
|
||||
|
||||
|
||||
|
||||
def transform_from_image_coords(bbox, image_width, image_height, pdf_width, pdf_height):
|
||||
x_scale = pdf_width / image_width
|
||||
y_scale = pdf_height / image_height
|
||||
|
||||
left = bbox[0] * x_scale
|
||||
right = bbox[2] * x_scale
|
||||
|
||||
top = pdf_height - (bbox[1] * y_scale)
|
||||
bottom = pdf_height - (bbox[3] * y_scale)
|
||||
|
||||
return left, bottom, right, top
|
||||
|
||||
|
||||
def transform_from_pdf_coords(bbox, pdf_height):
|
||||
left = bbox[0]
|
||||
right = bbox[2]
|
||||
|
||||
pypdf_top = pdf_height - bbox[1]
|
||||
pypdf_bottom = pdf_height - bbox[3]
|
||||
|
||||
return left, pypdf_bottom, right, pypdf_top
|
||||
|
||||
|
||||
def fill_pdf_form(input_pdf_path, fields_json_path, output_pdf_path):
|
||||
|
||||
with open(fields_json_path, "r") as f:
|
||||
fields_data = json.load(f)
|
||||
|
||||
reader = PdfReader(input_pdf_path)
|
||||
writer = PdfWriter()
|
||||
|
||||
writer.append(reader)
|
||||
|
||||
pdf_dimensions = {}
|
||||
for i, page in enumerate(reader.pages):
|
||||
mediabox = page.mediabox
|
||||
pdf_dimensions[i + 1] = [mediabox.width, mediabox.height]
|
||||
|
||||
annotations = []
|
||||
for field in fields_data["form_fields"]:
|
||||
page_num = field["page_number"]
|
||||
|
||||
page_info = next(p for p in fields_data["pages"] if p["page_number"] == page_num)
|
||||
pdf_width, pdf_height = pdf_dimensions[page_num]
|
||||
|
||||
if "pdf_width" in page_info:
|
||||
transformed_entry_box = transform_from_pdf_coords(
|
||||
field["entry_bounding_box"],
|
||||
float(pdf_height)
|
||||
)
|
||||
else:
|
||||
image_width = page_info["image_width"]
|
||||
image_height = page_info["image_height"]
|
||||
transformed_entry_box = transform_from_image_coords(
|
||||
field["entry_bounding_box"],
|
||||
image_width, image_height,
|
||||
float(pdf_width), float(pdf_height)
|
||||
)
|
||||
|
||||
if "entry_text" not in field or "text" not in field["entry_text"]:
|
||||
continue
|
||||
entry_text = field["entry_text"]
|
||||
text = entry_text["text"]
|
||||
if not text:
|
||||
continue
|
||||
|
||||
font_name = entry_text.get("font", "Arial")
|
||||
font_size = str(entry_text.get("font_size", 14)) + "pt"
|
||||
font_color = entry_text.get("font_color", "000000")
|
||||
|
||||
annotation = FreeText(
|
||||
text=text,
|
||||
rect=transformed_entry_box,
|
||||
font=font_name,
|
||||
font_size=font_size,
|
||||
font_color=font_color,
|
||||
border_color=None,
|
||||
background_color=None,
|
||||
)
|
||||
annotations.append(annotation)
|
||||
writer.add_annotation(page_number=page_num - 1, annotation=annotation)
|
||||
|
||||
with open(output_pdf_path, "wb") as output:
|
||||
writer.write(output)
|
||||
|
||||
print(f"Successfully filled PDF form and saved to {output_pdf_path}")
|
||||
print(f"Added {len(annotations)} text annotations")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 4:
|
||||
print("Usage: fill_pdf_form_with_annotations.py [input pdf] [fields.json] [output pdf]")
|
||||
sys.exit(1)
|
||||
input_pdf = sys.argv[1]
|
||||
fields_json = sys.argv[2]
|
||||
output_pdf = sys.argv[3]
|
||||
|
||||
fill_pdf_form(input_pdf, fields_json, output_pdf)
|
||||
@@ -0,0 +1,30 @@
|
||||
© 2025 Anthropic, PBC. All rights reserved.
|
||||
|
||||
LICENSE: Use of these materials (including all code, prompts, assets, files,
|
||||
and other components of this Skill) is governed by your agreement with
|
||||
Anthropic regarding use of Anthropic's services. If no separate agreement
|
||||
exists, use is governed by Anthropic's Consumer Terms of Service or
|
||||
Commercial Terms of Service, as applicable:
|
||||
https://www.anthropic.com/legal/consumer-terms
|
||||
https://www.anthropic.com/legal/commercial-terms
|
||||
Your applicable agreement is referred to as the "Agreement." "Services" are
|
||||
as defined in the Agreement.
|
||||
|
||||
ADDITIONAL RESTRICTIONS: Notwithstanding anything in the Agreement to the
|
||||
contrary, users may not:
|
||||
|
||||
- Extract these materials from the Services or retain copies of these
|
||||
materials outside the Services
|
||||
- Reproduce or copy these materials, except for temporary copies created
|
||||
automatically during authorized use of the Services
|
||||
- Create derivative works based on these materials
|
||||
- Distribute, sublicense, or transfer these materials to any third party
|
||||
- Make, offer to sell, sell, or import any inventions embodied in these
|
||||
materials
|
||||
- Reverse engineer, decompile, or disassemble these materials
|
||||
|
||||
The receipt, viewing, or possession of these materials does not convey or
|
||||
imply any license or right beyond those expressly granted above.
|
||||
|
||||
Anthropic retains all right, title, and interest in these materials,
|
||||
including all copyrights, patents, and other intellectual property rights.
|
||||
@@ -0,0 +1,238 @@
|
||||
---
|
||||
name: pptx
|
||||
description: "Use this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an email or summary); editing, modifying, or updating existing presentations; combining or splitting slide files; working with templates (.potx), layouts, speaker notes, or comments. Trigger whenever the user mentions \"deck,\" \"slides,\" \"presentation,\" or references a .pptx or .potx filename, regardless of what they plan to do with the content afterward. If a .pptx or .potx file needs to be opened, created, or touched, use this skill."
|
||||
license: Proprietary. LICENSE.txt has complete terms
|
||||
---
|
||||
|
||||
# PPTX creation, editing, and analysis
|
||||
|
||||
A `.pptx` is a ZIP archive of XML files. Choose your approach by task:
|
||||
|
||||
| Task | Approach |
|
||||
|---|---|
|
||||
| **Create** a new deck | Write a `pptxgenjs` script — see gotchas below |
|
||||
| **Edit** an existing deck, or build from a template | unzip → edit `ppt/slides/slideN.xml` → zip |
|
||||
| **Read** content | `markitdown deck.pptx` (one block per slide under `<!-- Slide number: N -->` markers); visual grid: `python scripts/thumbnail.py deck.pptx` |
|
||||
|
||||
## Scripts
|
||||
|
||||
Paths are relative to this skill's directory. Everything else is plain Python, `node`, or shell.
|
||||
|
||||
| Script | What it does |
|
||||
|---|---|
|
||||
| `scripts/thumbnail.py deck.pptx [prefix]` | Labeled grid of every slide, for picking template layouts. `.pptx` only. Pass `prefix` — it defaults to `thumbnails`, which overwrites the grids of any other deck done in the same directory |
|
||||
| `scripts/add_slide.py unpacked/ slide2.xml [--after slideN.xml]` | Duplicate a slide (or a `slideLayoutN.xml`) with all the package bookkeeping. Also takes a `.pptx` directly with `-o out.pptx` |
|
||||
| `scripts/clean.py unpacked/` | Delete slides, media, and rels no longer referenced. Run **after** `<p:sldIdLst>` is final |
|
||||
| `scripts/office/validate.py deck.pptx [--original src.pptx]` | Schema, relationship, content-type, chart and slide checks; each failure names its fix. Pass `--original` for any template-derived deck — it baselines the schema checks against the template, so the template's own XSD errors don't read as yours |
|
||||
| `scripts/office/soffice.py --headless --convert-to pdf deck.pptx` | LibreOffice wrapper — bare `soffice` hangs in this sandbox |
|
||||
|
||||
## Creating with pptxgenjs — gotchas
|
||||
|
||||
`pptxgenjs` is preinstalled — do not run `npm install` first; write the script and `require('pptxgenjs')` directly. Only if that require fails: `npm install pptxgenjs`. The model knows the API; these are the footguns:
|
||||
|
||||
- **Set `pres.layout` before adding slides.** The default canvas is `LAYOUT_16x9` = **10" × 5.625"**, not 13.3" wide. Coordinates past the edge are written, not clamped — the shape just isn't on the slide. (`LAYOUT_WIDE` is 13.3" × 7.5".)
|
||||
- **Hex colors: never `#`, never 8 digits.** `color: "FF0000"`. Both `"#FF0000"` and alpha baked into the hex (`"00000020"`) **corrupt the file**. For translucency: `transparency: 0-100` on fills and images, `opacity: 0.0-1.0` on shadows — each is silently ignored on the other.
|
||||
- **pptxgenjs mutates option objects in place** (converts values to EMU on first use). Never share one `shadow`/options object across two `add*` calls — build a fresh object each time.
|
||||
- **Shadow `offset` must be ≥ 0** — a negative offset corrupts the file. To cast a shadow upward, use `angle: 270` with a positive offset.
|
||||
- **`letterSpacing` is silently ignored** — the real option is `charSpacing`.
|
||||
- **Lists:** `bullet: true` on each item, never a literal `•` (renders double bullets). Set `breakLine: true` on every array item except the last. Space bulleted paragraphs with `paraSpaceAfter`, not `lineSpacing` (huge gaps).
|
||||
- **One `new pptxgen()` per output file** — never reuse an instance.
|
||||
- **`rectRadius` only works on `ROUNDED_RECTANGLE`**, not `RECTANGLE`.
|
||||
- **Gradient fills aren't supported** — use a gradient image as the background instead.
|
||||
- **Text boxes have built-in internal padding** — set `margin: 0` whenever text must align with a shape, line, or icon at the same x.
|
||||
- **Speaker notes go in `slide.addNotes("...")`** (plain text, once per slide), never in a text box on the slide.
|
||||
- **Keep charts native.** Use `addChart()` for everything PowerPoint can chart (pass an array of `{type, data, options}` for combos). For PowerPoint-native features the library doesn't expose (trendlines, error bars), compute the extra series yourself or post-process the generated OOXML — do not fall back to a rendered image. Only chart types PowerPoint has no native form for (Sankey, network, chord) go in as images.
|
||||
- **Default charts render bare** — no title, no data labels, dated palette. Set `showTitle` + `title`, `showValue: true` + `dataLabelPosition`, `chartColors: [...]` from your palette, and quiet the frame (`catAxisLabelColor`/`valAxisLabelColor`, `valGridLine: { color, size }`, `catGridLine: { style: "none" }`, `showLegend: false` for a single series).
|
||||
- **On a stacked bar or column chart, `dataLabelPosition` must be `ctr`, `inEnd`, or `inBase`.** `outEnd` **corrupts the file**.
|
||||
- **A combo series using `secondaryValAxis`/`secondaryCatAxis` needs both `valAxes` and `catAxes` on the chart options, two entries each.** Without them pptxgenjs writes axis *ids* it never declares, and PowerPoint **discards that chart** and reports the file as corrupt. Supplying only `valAxes` is not enough.
|
||||
- **After `writeFile()`, run `python scripts/office/validate.py deck.pptx`.** It reports the two chart faults above and the slide-XML defects PowerPoint refuses, and names the fix for each. Fix them in your generator, not by hand-editing the packed XML.
|
||||
- **Never reorder the children of `<p:presentation>`.** pptxgenjs writes `<p:notesMasterIdLst>` right after `<p:sldIdLst>` and points both masters at one theme part. PowerPoint reads that happily — move the element and the same deck becomes unopenable.
|
||||
- **Icons:** render `react-icons` to SVG (`ReactDOMServer.renderToStaticMarkup`), rasterize with `sharp` at ≥256px, and insert via `addImage({ data: "image/png;base64," + buf.toString("base64") })` — the `image/png;base64,` prefix is required (`react-icons`, `react`, `react-dom`, and `sharp` are preinstalled — `npm install react-icons react react-dom sharp` only if a require fails).
|
||||
|
||||
## Editing existing decks and templates
|
||||
|
||||
Pick layouts first: `python scripts/thumbnail.py template.pptx template-thumbs` writes a labeled grid of every slide and prints the file(s) it created — `template-thumbs.jpg`, split into `template-thumbs-N.jpg` past 12 slides. **Always pass that second argument, named after the deck.** It defaults to `thumbnails`, so two decks thumbnailed in one directory silently overwrite each other's grids — the first deck's are simply gone (template analysis only — visual QA needs the full-resolution renders from [Converting to Images](#converting-to-images); it only accepts `.pptx`, so copy a `.potx` to a `.pptx` name first). Use it with `markitdown` to map each content section onto a template slide, and vary the layouts — don't put every section on the same title-and-bullets slide.
|
||||
|
||||
```bash
|
||||
python3 -c "import sys,zipfile; zipfile.ZipFile(sys.argv[1]).extractall('unpacked')" deck.pptx
|
||||
python scripts/add_slide.py unpacked/ slide2.xml --after slide2.xml # duplicate a slide (or slideLayoutN.xml); prints the new slide's path
|
||||
# reorder / delete slides = edit <p:sldIdLst> in ppt/presentation.xml
|
||||
python scripts/clean.py unpacked/ # after deletions: removes orphaned slides, media, rels
|
||||
# edit slide content in ppt/slides/slideN.xml
|
||||
(cd unpacked && rm -f ../out.pptx && zip -Xr ../out.pptx .) # zip from INSIDE the dir; rm first or deleted parts survive
|
||||
python scripts/office/validate.py out.pptx --original deck.pptx
|
||||
```
|
||||
|
||||
- **Do all structural work — add, delete, reorder — before editing any slide's content.** `add_slide.py` copies a slide file verbatim, so duplicating after you edit clones the edited content; and `clean.py` deletes any slide missing from `<p:sldIdLst>`, including one you just wrote.
|
||||
- **Never copy a slide file by hand** — `add_slide.py` does every registration a new slide needs and reports what it made (`Created ppt/slides/slide17.xml from slide2.xml`). It also works directly on a file: `add_slide.py deck.pptx slide2.xml -o out.pptx` — **pass `-o`, or it rewrites the input deck in place.** A duplicated slide still *references* its source's chart/SmartArt/embedded-object parts rather than cloning them, so editing one slide's chart changes the other's.
|
||||
- **If you use `python-pptx`**, three things it won't do: duplicate a slide (its only entry point is `add_slide(layout)`), preserve formatting through `text_frame.text = "..."` (that collapses the paragraph to a single unstyled run — assign `run.text` instead), or read the SVG/EMF most template art uses (`add_picture` raises `UnidentifiedImageError`).
|
||||
- Legacy `.ppt` must be converted first: `python scripts/office/soffice.py --headless --convert-to pptx file.ppt`. `.potx` templates unpack and pack identically — keep the `.potx` extension on the output.
|
||||
- To reuse a template icon or image, duplicate a slide or layout that already contains it.
|
||||
|
||||
When filling in a template:
|
||||
|
||||
- If you script an XML transform, parse with `defusedxml.minidom` — round-tripping OOXML through `xml.etree.ElementTree` rewrites namespace prefixes and corrupts the deck.
|
||||
- **Template slots ≠ source items.** If the template shows 4 team members and you have 3, delete the 4th member's entire group (image + text boxes), not just its text — then check for orphaned visuals in QA.
|
||||
- One `<a:p>` per list item — never concatenate items into a single paragraph. Copy the sibling `<a:pPr>` to preserve spacing, and put `b="1"` on the `<a:rPr>` of titles, section headers, and inline labels (`Status:`, `Owner:`).
|
||||
- Let bullets inherit from the layout; only add `<a:buChar>`, `<a:buAutoNum>` (numbered), or `<a:buNone>` to override — never a literal `•` in the text.
|
||||
- Text with leading or trailing spaces needs `xml:space="preserve"` on its `<a:t>`.
|
||||
|
||||
## Design Ideas
|
||||
|
||||
**Don't create boring slides.** Plain bullets on a white background won't impress anyone. Consider ideas from this list for each slide.
|
||||
|
||||
### Before Starting
|
||||
|
||||
- **Pick a bold, content-informed color palette**: The palette should feel designed for THIS topic. If swapping your colors into a completely different presentation would still "work," you haven't made specific enough choices.
|
||||
- **Dominance over equality**: One color should dominate (60-70% visual weight), with 1-2 supporting tones and one sharp accent. Never give all colors equal weight.
|
||||
- **Dark/light contrast**: Dark backgrounds for title + conclusion slides, light for content ("sandwich" structure). Or commit to dark throughout for a premium feel.
|
||||
- **Commit to a visual motif**: Pick ONE distinctive element and repeat it — rounded image frames, icons in colored circles. Carry it across every slide. **Do not use a color bar or accent stripe as your motif** (see Avoid list).
|
||||
|
||||
### Color Palettes
|
||||
|
||||
Choose colors that match your topic — don't default to generic blue. Use these palettes as inspiration:
|
||||
|
||||
| Theme | Primary | Secondary | Accent |
|
||||
|-------|---------|-----------|--------|
|
||||
| **Midnight Executive** | `1E2761` (navy) | `CADCFC` (ice blue) | `FFFFFF` (white) |
|
||||
| **Forest & Moss** | `2C5F2D` (forest) | `97BC62` (moss) | `F5F5F5` (cream) |
|
||||
| **Coral Energy** | `F96167` (coral) | `F9E795` (gold) | `2F3C7E` (navy) |
|
||||
| **Warm Terracotta** | `B85042` (terracotta) | `E7E8D1` (sand) | `A7BEAE` (sage) |
|
||||
| **Ocean Gradient** | `065A82` (deep blue) | `1C7293` (teal) | `21295C` (midnight) |
|
||||
| **Charcoal Minimal** | `36454F` (charcoal) | `F2F2F2` (off-white) | `212121` (black) |
|
||||
| **Teal Trust** | `028090` (teal) | `00A896` (seafoam) | `02C39A` (mint) |
|
||||
| **Berry & Cream** | `6D2E46` (berry) | `A26769` (dusty rose) | `ECE2D0` (cream) |
|
||||
| **Sage Calm** | `84B59F` (sage) | `69A297` (eucalyptus) | `50808E` (slate) |
|
||||
| **Cherry Bold** | `990011` (cherry) | `FCF6F5` (off-white) | `2F3C7E` (navy) |
|
||||
|
||||
### For Each Slide
|
||||
|
||||
**Every slide needs a visual element** — image, chart, icon, or shape. Text-only slides are forgettable.
|
||||
|
||||
**Layout options:**
|
||||
- Two-column (text left, illustration on right)
|
||||
- Icon + text rows (icon in colored circle, bold header, description below)
|
||||
- 2x2 or 2x3 grid (image on one side, grid of content blocks on other)
|
||||
- Half-bleed image (full left or right side) with content overlay
|
||||
|
||||
**Data display:**
|
||||
- Large stat callouts (big numbers 60-72pt with small labels below)
|
||||
- Comparison columns (before/after, pros/cons, side-by-side options)
|
||||
- Timeline or process flow (numbered steps, arrows)
|
||||
|
||||
**Visual polish:**
|
||||
- Icons in small colored circles next to section headers
|
||||
- Italic accent text for key stats or taglines
|
||||
|
||||
### Typography
|
||||
|
||||
**Font names you write into the .pptx are rendered by the user's PowerPoint, not by this environment.** Your visual QA renders via LibreOffice, which substitutes fonts it doesn't have — and for some fonts the substitute has different widths, so your QA preview can show text overflow (or fit) that the real deck won't have. To keep your QA trustworthy:
|
||||
|
||||
- **Safe fonts** (render true-to-width in QA *and* ship with Office): **Arial, Calibri, Cambria, Times New Roman, Courier New, Bookman Old Style, Century Schoolbook**. Use these for body text and anything where fit matters.
|
||||
- **Headers with personality at zero QA risk**: pair a safe-list serif header (Cambria, Bookman Old Style, Century Schoolbook) with a safe-list sans body (Calibri or Arial). You get visual contrast without giving up reliable overflow checks.
|
||||
- **If the user asks for a font outside the safe list** (e.g. Georgia or Trebuchet MS): use it where the user asked, but size those containers with extra slack (~10%) and don't trust QA text-fit on those elements — the preview of that font is approximate. If the user hasn't specified, prefer safe-list fonts for body text.
|
||||
- **QA-unreliable fonts** (substitute has different widths — overflow checks can be wrong): Georgia, Trebuchet MS, Impact, Arial Black, Garamond, Consolas, Palatino Linotype. Calibri Light substitution varies by environment; treat as QA-unreliable. Fine for titles/accents with slack; don't trust QA text-fit on these.
|
||||
- **Never default to Aptos** — Office's post-2023 default has no metric-compatible substitute here *and* is missing from older Office installs, so it's unreliable on both ends.
|
||||
|
||||
| Element | Size |
|
||||
|---------|------|
|
||||
| Slide title | 36-44pt bold |
|
||||
| Section header | 20-24pt bold |
|
||||
| Body text | 14-16pt |
|
||||
| Captions | 10-12pt muted |
|
||||
|
||||
### Spacing
|
||||
|
||||
- 0.5" minimum margins
|
||||
- 0.3-0.5" between content blocks
|
||||
- Leave breathing room—don't fill every inch
|
||||
|
||||
### Avoid (Common Mistakes)
|
||||
|
||||
- **Don't repeat the same layout** — vary columns, cards, and callouts across slides
|
||||
- **Don't center body text** — left-align paragraphs and lists; center only titles
|
||||
- **Don't skimp on size contrast** — titles need 36pt+ to stand out from 14-16pt body
|
||||
- **Don't default to blue** — pick colors that reflect the specific topic
|
||||
- **Don't mix spacing randomly** — choose 0.3" or 0.5" gaps and use consistently
|
||||
- **Don't style one slide and leave the rest plain** — commit fully or keep it simple throughout
|
||||
- **Don't create text-only slides** — add images, icons, charts, or visual elements; avoid plain title + bullets
|
||||
- **Don't forget text box padding** — when aligning lines or shapes with text edges, set `margin: 0` on the text box or offset the shape to account for padding
|
||||
- **Don't use low-contrast elements** — icons AND text need strong contrast against the background; avoid light text on light backgrounds or dark text on dark backgrounds
|
||||
- **NEVER use accent lines under titles** — these are a hallmark of AI-generated slides; use whitespace or background color instead
|
||||
- **NEVER add decorative color bars or accent stripes** — this includes: header/footer bars spanning the slide width, vertical sidebar stripes down one edge of the slide, thin accent stripes along one edge of a card or content block, and "single-side borders" on rectangles. These read as AI-generated filler. If you want to set a card apart, use a subtle background tint, a drop shadow, or an icon — not an edge stripe.
|
||||
- **Don't default to cream/beige backgrounds** — when no background is specified, use white (`FFFFFF`) or the user's brand palette; avoid warm-neutral defaults like `F5F5DC`, `FAF0E6`, `FAEBD7`, `FFF8E1`
|
||||
- **Don't ship text that overflows its shape** — if text doesn't fit, reduce font size, split across slides, or enlarge the container; never leave content cut off or spilling past bounds
|
||||
|
||||
## QA (Required)
|
||||
|
||||
Your first render usually has a few real issues — overlaps, overflow, misalignment. Find and fix those, re-render only the slides you changed, and stop.
|
||||
|
||||
### Content QA
|
||||
|
||||
```bash
|
||||
markitdown output.pptx
|
||||
```
|
||||
|
||||
Check for missing content, typos, wrong order.
|
||||
|
||||
**When using templates, check for leftover placeholder text:**
|
||||
|
||||
```bash
|
||||
markitdown output.pptx | grep -iE "\bx{3,}\b|lorem|ipsum|\bTODO|\[insert|this.*(page|slide).*layout"
|
||||
```
|
||||
|
||||
If grep returns results, fix them before declaring success.
|
||||
|
||||
### File QA (required)
|
||||
|
||||
```bash
|
||||
python scripts/office/validate.py output.pptx # built from scratch
|
||||
python scripts/office/validate.py output.pptx --original src.pptx # built from a template
|
||||
```
|
||||
|
||||
**If the deck came from a template, always pass `--original`.** A template may itself
|
||||
contain parts the XSD rejects, so a bare run can report failures you never caused — and
|
||||
a genuine regression can hide among them. `--original` baselines
|
||||
the schema and slide checks against the template, suppressing errors it already had.
|
||||
The structural checks — relationships, content types, charts — ignore `--original` and
|
||||
report template-inherited problems either way, so read those on their own merits.
|
||||
|
||||
pptxgenjs emits chart XML PowerPoint refuses to open, and every other tool
|
||||
accepts: python-pptx opens those decks, LibreOffice renders them, the XSD
|
||||
passes them. Every failure names its fix. Fix it in the generator and rebuild.
|
||||
|
||||
### Visual QA
|
||||
|
||||
Convert the slides to images (see [Converting to Images](#converting-to-images)) and inspect every one. After staring at the generating code you tend to see what you expect rather than what rendered, so look at the images fresh (a subagent works well for this if you have one). User-visible defects to look for:
|
||||
|
||||
- **Text overflow or text cut off at a box or slide boundary — check this first.** It is the most common defect and always user-visible. (For a font the previewer renders unreliably per Typography, the preview is approximate: trust the ~10% slack you left, not its apparent fit.)
|
||||
- Overlapping elements (text through shapes, lines through words, stacked elements)
|
||||
- Source citations or footers colliding with content above
|
||||
- Elements too close (< 0.3" gaps) or cards/sections nearly touching
|
||||
- Uneven gaps (large empty area in one place, cramped in another)
|
||||
- Insufficient margin from slide edges (< 0.5")
|
||||
- Columns or similar elements not aligned consistently
|
||||
- Low-contrast text (e.g., light gray text on cream-colored background)
|
||||
- Template decoration mispositioned after text replacement — e.g., a title underline positioned for one line, but the replaced title wrapped to two
|
||||
- Low-contrast icons (e.g., dark icons on dark backgrounds without a contrasting circle)
|
||||
- Text boxes too narrow causing excessive wrapping
|
||||
- Leftover placeholder content
|
||||
|
||||
## Converting to Images
|
||||
|
||||
Convert presentations to individual slide images for visual inspection:
|
||||
|
||||
```bash
|
||||
python scripts/office/soffice.py --headless --convert-to pdf output.pptx
|
||||
rm -f slide-*.jpg
|
||||
pdftoppm -jpeg -r 150 output.pdf slide
|
||||
ls -1 "$PWD"/slide-*.jpg
|
||||
```
|
||||
|
||||
**Pass the absolute paths printed above directly to the view tool.** The `rm` clears stale images from prior runs. `pdftoppm` zero-pads based on page count: `slide-1.jpg` for decks under 10 pages, `slide-01.jpg` for 10-99, `slide-001.jpg` for 100+.
|
||||
|
||||
**After fixes, rerun all four commands above** — the PDF must be regenerated from the edited `.pptx` before `pdftoppm` can reflect your changes.
|
||||
|
||||
## Dependencies
|
||||
|
||||
`pptxgenjs` (npm, preinstalled — install only if `require('pptxgenjs')` fails) · `markitdown[pptx]`, `Pillow`, `defusedxml`, `lxml` (pip — text dump, thumbnail, clean, validate) · LibreOffice (`soffice`, auto-configured for sandboxed environments via `scripts/office/soffice.py`) · `pdftoppm` (Poppler)
|
||||
Executable
+367
@@ -0,0 +1,367 @@
|
||||
"""Add a slide to a PPTX: duplicate an existing slide or instantiate a layout.
|
||||
|
||||
Does all of the package bookkeeping, so the deck stays valid:
|
||||
- writes the new ppt/slides/slideN.xml (and its .rels, minus any
|
||||
notesSlide reference, so the source's speaker notes aren't shared)
|
||||
- registers it in [Content_Types].xml
|
||||
- adds a slide relationship with a fresh rId to presentation.xml.rels
|
||||
- inserts <p:sldId id="..." r:id="..."/> with a fresh id into
|
||||
<p:sldIdLst> — at the end, or after --after SLIDE
|
||||
|
||||
Works on an unpacked directory (during an editing session) or directly on a
|
||||
.pptx/.potx file (extracted to a temp dir, then rezipped atomically; the
|
||||
temp dir is discarded, so unpack the output if you still need to edit the
|
||||
new slide's content).
|
||||
|
||||
Usage:
|
||||
python add_slide.py unpacked/ slide2.xml # duplicate slide2
|
||||
python add_slide.py unpacked/ slideLayout3.xml # new slide from a layout
|
||||
python add_slide.py unpacked/ slide2.xml --after slide2.xml
|
||||
python add_slide.py deck.pptx slide2.xml # rewrite deck.pptx in place
|
||||
python add_slide.py deck.pptx slide2.xml -o out.pptx
|
||||
|
||||
A duplicated slide still holds the source's content: edit ppt/slides/slideN.xml
|
||||
(printed on success) to change it. To list layouts: ls <dir>/ppt/slideLayouts/
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from typing import NoReturn
|
||||
import tempfile
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
from office.helpers import rezip, safe_extract
|
||||
|
||||
MINIMAL_SLIDE_XML = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<p:sld xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:p="http://schemas.openxmlformats.org/presentationml/2006/main">
|
||||
<p:cSld>
|
||||
<p:spTree>
|
||||
<p:nvGrpSpPr>
|
||||
<p:cNvPr id="1" name=""/>
|
||||
<p:cNvGrpSpPr/>
|
||||
<p:nvPr/>
|
||||
</p:nvGrpSpPr>
|
||||
<p:grpSpPr>
|
||||
<a:xfrm>
|
||||
<a:off x="0" y="0"/>
|
||||
<a:ext cx="0" cy="0"/>
|
||||
<a:chOff x="0" y="0"/>
|
||||
<a:chExt cx="0" cy="0"/>
|
||||
</a:xfrm>
|
||||
</p:grpSpPr>
|
||||
</p:spTree>
|
||||
</p:cSld>
|
||||
<p:clrMapOvr>
|
||||
<a:masterClrMapping/>
|
||||
</p:clrMapOvr>
|
||||
</p:sld>'''
|
||||
|
||||
SHARED_PART_TYPES = ("chart", "diagramData", "oleObject", "package")
|
||||
|
||||
NOTES_SLIDE_TYPE_RE = re.compile(r"""Type=["'][^"']*/relationships/notesSlide["']""")
|
||||
RELATIONSHIP_RE = re.compile(r"<Relationship\b[^>]*?(?:/>|>.*?</Relationship\s*>)", re.DOTALL)
|
||||
|
||||
SLIDE_ID_MIN = 256
|
||||
SLIDE_ID_MAX = 2147483647
|
||||
|
||||
|
||||
def _die(msg: str) -> NoReturn:
|
||||
print(f"Error: {msg}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_next_slide_number(slides_dir: Path) -> int:
|
||||
existing = [int(m.group(1)) for f in slides_dir.glob("slide*.xml")
|
||||
if (m := re.match(r"slide(\d+)\.xml", f.name))]
|
||||
return max(existing) + 1 if existing else 1
|
||||
|
||||
|
||||
def parse_source(source: str) -> tuple[str, str | None]:
|
||||
if source.startswith("slideLayout") and source.endswith(".xml"):
|
||||
return ("layout", source)
|
||||
|
||||
return ("slide", None)
|
||||
|
||||
|
||||
def create_slide_from_layout(unpacked_dir: Path, layout_file: str, after: str | None = None) -> str:
|
||||
slides_dir = unpacked_dir / "ppt" / "slides"
|
||||
rels_dir = slides_dir / "_rels"
|
||||
layout_path = unpacked_dir / "ppt" / "slideLayouts" / layout_file
|
||||
|
||||
if not layout_path.exists():
|
||||
_die(f"{layout_path} not found")
|
||||
|
||||
next_num = get_next_slide_number(slides_dir)
|
||||
dest = f"slide{next_num}.xml"
|
||||
after_rid = _precheck_registration(unpacked_dir, after, dest)
|
||||
slides_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
(slides_dir / dest).write_text(MINIMAL_SLIDE_XML, encoding="utf-8")
|
||||
|
||||
rels_dir.mkdir(exist_ok=True)
|
||||
rels_xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" Target="../slideLayouts/{layout_file}"/>
|
||||
</Relationships>'''
|
||||
(rels_dir / f"{dest}.rels").write_text(rels_xml, encoding="utf-8")
|
||||
|
||||
_register_slide(unpacked_dir, dest, layout_file, after_rid)
|
||||
return dest
|
||||
|
||||
|
||||
def duplicate_slide(unpacked_dir: Path, source: str, after: str | None = None) -> str:
|
||||
slides_dir = unpacked_dir / "ppt" / "slides"
|
||||
rels_dir = slides_dir / "_rels"
|
||||
source_slide = slides_dir / source
|
||||
|
||||
if not source_slide.exists():
|
||||
_die(f"{source_slide} not found")
|
||||
|
||||
next_num = get_next_slide_number(slides_dir)
|
||||
dest = f"slide{next_num}.xml"
|
||||
after_rid = _precheck_registration(unpacked_dir, after, dest)
|
||||
|
||||
shutil.copy2(source_slide, slides_dir / dest)
|
||||
|
||||
source_rels = rels_dir / f"{source}.rels"
|
||||
shared_parts: list[str] = []
|
||||
if source_rels.exists():
|
||||
dest_rels = rels_dir / f"{dest}.rels"
|
||||
shutil.copy2(source_rels, dest_rels)
|
||||
rels_content = dest_rels.read_text(encoding="utf-8")
|
||||
rels_content = RELATIONSHIP_RE.sub(
|
||||
lambda m: "" if NOTES_SLIDE_TYPE_RE.search(m.group(0)) else m.group(0),
|
||||
rels_content,
|
||||
)
|
||||
dest_rels.write_text(rels_content, encoding="utf-8")
|
||||
shared_parts = sorted({
|
||||
t for t in re.findall(r'Type="[^"]*/relationships/(\w+)"', rels_content)
|
||||
if t in SHARED_PART_TYPES
|
||||
})
|
||||
|
||||
_register_slide(unpacked_dir, dest, source, after_rid)
|
||||
if shared_parts:
|
||||
print(
|
||||
f"Note: {dest} shares its {', '.join(shared_parts)} part(s) with {source} "
|
||||
f"(they are referenced, not copied) — editing those parts changes both slides"
|
||||
)
|
||||
return dest
|
||||
|
||||
|
||||
def _precheck_registration(unpacked_dir: Path, after: str | None, dest: str) -> str | None:
|
||||
pres_path = unpacked_dir / "ppt" / "presentation.xml"
|
||||
if not pres_path.exists():
|
||||
_die(f"{pres_path} not found — is this an unpacked PPTX?")
|
||||
xml = pres_path.read_text(encoding="utf-8")
|
||||
|
||||
has_slot = (
|
||||
"</p:sldIdLst>" in xml
|
||||
or re.search(r"<p:sldIdLst\s*/>", xml)
|
||||
or "</p:sldMasterIdLst>" in xml
|
||||
)
|
||||
if not has_slot:
|
||||
_die("presentation.xml has no <p:sldIdLst> (or <p:sldMasterIdLst> to anchor a new one)")
|
||||
|
||||
stale = []
|
||||
content_types = unpacked_dir / "[Content_Types].xml"
|
||||
if content_types.exists() and f'PartName="/ppt/slides/{dest}"' in content_types.read_text(encoding="utf-8"):
|
||||
stale.append("[Content_Types].xml")
|
||||
pres_rels = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
||||
if pres_rels.exists() and _find_slide_relationship(
|
||||
pres_rels.read_text(encoding="utf-8"), dest
|
||||
):
|
||||
stale.append("presentation.xml.rels")
|
||||
if stale:
|
||||
_die(
|
||||
f"{dest} is still registered in {' and '.join(stale)} but absent from ppt/slides/ — "
|
||||
f"run clean.py first"
|
||||
)
|
||||
|
||||
if not after:
|
||||
return None
|
||||
after_rid = _rid_for_slide(unpacked_dir, after)
|
||||
if not re.search(rf'<p:sldId\b[^>]*r:id="{re.escape(after_rid)}"[^>]*>', xml):
|
||||
_die(f"{after} ({after_rid}) is not listed in <p:sldIdLst>")
|
||||
return after_rid
|
||||
|
||||
|
||||
def _register_slide(unpacked_dir: Path, dest: str, source_desc: str, after_rid: str | None) -> None:
|
||||
_add_to_content_types(unpacked_dir, dest)
|
||||
rid = _add_to_presentation_rels(unpacked_dir, dest)
|
||||
slide_id = _get_next_slide_id(unpacked_dir)
|
||||
pos, total = _insert_into_sld_id_lst(unpacked_dir, slide_id, rid, after_rid)
|
||||
|
||||
print(f"Created ppt/slides/{dest} from {source_desc}")
|
||||
print(
|
||||
f'Inserted <p:sldId id="{slide_id}" r:id="{rid}"/> into <p:sldIdLst> '
|
||||
f"at position {pos} of {total}"
|
||||
)
|
||||
|
||||
|
||||
def _add_to_content_types(unpacked_dir: Path, dest: str) -> None:
|
||||
content_types_path = unpacked_dir / "[Content_Types].xml"
|
||||
content_types = content_types_path.read_text(encoding="utf-8")
|
||||
|
||||
new_override = f'<Override PartName="/ppt/slides/{dest}" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml"/>'
|
||||
|
||||
if f'PartName="/ppt/slides/{dest}"' not in content_types:
|
||||
content_types = content_types.replace("</Types>", f" {new_override}\n</Types>")
|
||||
content_types_path.write_text(content_types, encoding="utf-8")
|
||||
|
||||
|
||||
def _add_to_presentation_rels(unpacked_dir: Path, dest: str) -> str:
|
||||
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
||||
pres_rels = pres_rels_path.read_text(encoding="utf-8")
|
||||
|
||||
existing = _find_slide_relationship(pres_rels, dest)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
pres_xml = (unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8")
|
||||
used = {int(n) for n in re.findall(r'\bId="rId(\d+)"', pres_rels)}
|
||||
used |= {int(n) for n in re.findall(r'\br:id="rId(\d+)"', pres_xml)}
|
||||
rid = f"rId{max(used) + 1 if used else 1}"
|
||||
|
||||
new_rel = f'<Relationship Id="{rid}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" Target="slides/{dest}"/>'
|
||||
pres_rels = pres_rels.replace("</Relationships>", f" {new_rel}\n</Relationships>")
|
||||
pres_rels_path.write_text(pres_rels, encoding="utf-8")
|
||||
|
||||
return rid
|
||||
|
||||
|
||||
def _find_slide_relationship(pres_rels: str, slide_name: str) -> str | None:
|
||||
for m in re.finditer(r"<Relationship\b[^>]*>", pres_rels):
|
||||
element = m.group(0)
|
||||
if re.search(rf'Target="(?:/ppt/)?slides/{re.escape(slide_name)}"', element):
|
||||
id_match = re.search(r'\bId="([^"]+)"', element)
|
||||
if id_match:
|
||||
return id_match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def _get_next_slide_id(unpacked_dir: Path) -> int:
|
||||
pres_content = (unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8")
|
||||
used = {int(m) for m in re.findall(r'<p:sldId[^>]*\bid="(\d+)"', pres_content)}
|
||||
|
||||
candidate = max((i for i in used if i >= SLIDE_ID_MIN), default=SLIDE_ID_MIN - 1) + 1
|
||||
if candidate <= SLIDE_ID_MAX and candidate not in used:
|
||||
return candidate
|
||||
for i in range(SLIDE_ID_MIN, SLIDE_ID_MAX + 1):
|
||||
if i not in used:
|
||||
return i
|
||||
_die("no slide id available in [256, 2147483647] — the deck is full")
|
||||
|
||||
|
||||
def _insert_into_sld_id_lst(
|
||||
unpacked_dir: Path, slide_id: int, rid: str, after_rid: str | None = None
|
||||
) -> tuple[int, int]:
|
||||
pres_path = unpacked_dir / "ppt" / "presentation.xml"
|
||||
xml = pres_path.read_text(encoding="utf-8")
|
||||
entry = f'<p:sldId id="{slide_id}" r:id="{rid}"/>'
|
||||
|
||||
if f'r:id="{rid}"' in xml:
|
||||
_die(f"presentation.xml already references {rid}; refusing to add a duplicate")
|
||||
|
||||
if after_rid:
|
||||
open_tag = re.search(rf'<p:sldId\b[^>]*r:id="{re.escape(after_rid)}"[^>]*>', xml)
|
||||
if not open_tag:
|
||||
_die(f"{after_rid} is not listed in <p:sldIdLst>")
|
||||
end = open_tag.end()
|
||||
if not open_tag.group(0).endswith("/>"):
|
||||
close = xml.find("</p:sldId>", end)
|
||||
if close == -1:
|
||||
_die(f"unclosed <p:sldId> for {after_rid} in presentation.xml")
|
||||
end = close + len("</p:sldId>")
|
||||
xml = xml[:end] + entry + xml[end:]
|
||||
elif "</p:sldIdLst>" in xml:
|
||||
xml = xml.replace("</p:sldIdLst>", f"{entry}</p:sldIdLst>", 1)
|
||||
elif re.search(r"<p:sldIdLst\s*/>", xml):
|
||||
xml = re.sub(r"<p:sldIdLst\s*/>", f"<p:sldIdLst>{entry}</p:sldIdLst>", xml, count=1)
|
||||
elif "</p:sldMasterIdLst>" in xml:
|
||||
xml = xml.replace(
|
||||
"</p:sldMasterIdLst>", f"</p:sldMasterIdLst><p:sldIdLst>{entry}</p:sldIdLst>", 1
|
||||
)
|
||||
else:
|
||||
_die("presentation.xml has no <p:sldIdLst> (or <p:sldMasterIdLst> to anchor a new one)")
|
||||
|
||||
pres_path.write_text(xml, encoding="utf-8")
|
||||
|
||||
lst = re.search(r"<p:sldIdLst>(.*)</p:sldIdLst>", xml, re.DOTALL)
|
||||
entries = re.findall(r"<p:sldId\b[^>]*>", lst.group(1)) if lst else []
|
||||
position = next(
|
||||
(i for i, e in enumerate(entries, 1) if f'r:id="{rid}"' in e), len(entries)
|
||||
)
|
||||
return position, len(entries)
|
||||
|
||||
|
||||
def _rid_for_slide(unpacked_dir: Path, slide_name: str) -> str:
|
||||
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
||||
rid = _find_slide_relationship(pres_rels_path.read_text(encoding="utf-8"), slide_name)
|
||||
if not rid:
|
||||
_die(f"{slide_name} has no relationship in presentation.xml.rels")
|
||||
return rid
|
||||
|
||||
|
||||
def add_slide(unpacked_dir: Path, source: str, after: str | None = None) -> str:
|
||||
source_type, layout_file = parse_source(source)
|
||||
if source_type == "layout" and layout_file is not None:
|
||||
return create_slide_from_layout(unpacked_dir, layout_file, after)
|
||||
return duplicate_slide(unpacked_dir, source, after)
|
||||
|
||||
|
||||
def add_slide_to_package(
|
||||
package: Path, source: str, after: str | None = None, output: Path | None = None
|
||||
) -> str:
|
||||
out = output or package
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_path = Path(tmp)
|
||||
with zipfile.ZipFile(package) as zf:
|
||||
safe_extract(zf, tmp_path)
|
||||
dest = add_slide(tmp_path, source, after)
|
||||
rezip(tmp_path, out)
|
||||
print(f"Wrote {out} — the new slide is ppt/slides/{dest} inside it (unpack to edit its content)")
|
||||
return dest
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Add a slide to a PPTX: duplicate a slide or instantiate a layout. "
|
||||
"Registers content types, relationships, and <p:sldIdLst>."
|
||||
)
|
||||
parser.add_argument("target", help="Unpacked PPTX directory OR a .pptx/.potx file")
|
||||
parser.add_argument(
|
||||
"source",
|
||||
help="slideN.xml to duplicate, or slideLayoutN.xml to create from a layout "
|
||||
"(list layouts with: ls <dir>/ppt/slideLayouts/)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--after",
|
||||
metavar="SLIDE",
|
||||
help="insert after this slide, e.g. slide2.xml (default: append at the end)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
help="output file (only with a .pptx/.potx target; default: rewrite the input in place)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
target = Path(args.target)
|
||||
if target.is_dir():
|
||||
if args.output:
|
||||
parser.error("--output is only valid for .pptx/.potx input; a directory is modified in place")
|
||||
add_slide(target, args.source, args.after)
|
||||
elif target.is_file() and target.suffix.lower() in (".pptx", ".potx"):
|
||||
try:
|
||||
add_slide_to_package(target, args.source, args.after, Path(args.output) if args.output else None)
|
||||
except (OSError, ValueError, zipfile.BadZipFile) as e:
|
||||
_die(str(e))
|
||||
else:
|
||||
_die(f"{target} is neither a directory nor a .pptx/.potx file")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+309
@@ -0,0 +1,309 @@
|
||||
"""Remove unreferenced files from an unpacked PPTX directory.
|
||||
|
||||
Usage: python clean.py <unpacked_dir>
|
||||
|
||||
Example:
|
||||
python clean.py unpacked/
|
||||
|
||||
This script removes:
|
||||
- Orphaned slides (not in sldIdLst) and their relationships
|
||||
- [trash] directory (unreferenced files)
|
||||
- Orphaned .rels files for deleted resources
|
||||
- Unreferenced media, embeddings, charts, diagrams, drawings, ink files
|
||||
- Unreferenced theme files
|
||||
- Unreferenced notes slides
|
||||
- Content-Type overrides for deleted files
|
||||
"""
|
||||
|
||||
import posixpath
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import defusedxml.minidom
|
||||
|
||||
from office.helpers import SLIDE_REL_TYPE, opc_target, rels_source_part
|
||||
|
||||
|
||||
def _slide_rids(pres_rels_path: Path, unpacked_dir: Path) -> dict[str, str]:
|
||||
source_part = rels_source_part(pres_rels_path, unpacked_dir)
|
||||
rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
|
||||
|
||||
rids: dict[str, str] = {}
|
||||
for rel in rels_dom.getElementsByTagName("Relationship"):
|
||||
if rel.getAttribute("Type") != SLIDE_REL_TYPE:
|
||||
continue
|
||||
part = opc_target(
|
||||
rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode")
|
||||
)
|
||||
if part is not None:
|
||||
rids[rel.getAttribute("Id")] = part
|
||||
return rids
|
||||
|
||||
|
||||
def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]:
|
||||
pres_path = unpacked_dir / "ppt" / "presentation.xml"
|
||||
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
||||
|
||||
if not pres_path.exists() or not pres_rels_path.exists():
|
||||
return set()
|
||||
|
||||
rid_to_slide = _slide_rids(pres_rels_path, unpacked_dir)
|
||||
|
||||
pres_content = pres_path.read_text(encoding="utf-8")
|
||||
referenced_rids = set(re.findall(r'<p:sldId[^>]*r:id="([^"]+)"', pres_content))
|
||||
|
||||
return {
|
||||
posixpath.basename(rid_to_slide[rid])
|
||||
for rid in referenced_rids
|
||||
if rid in rid_to_slide
|
||||
}
|
||||
|
||||
|
||||
class RefusedToClean(Exception):
|
||||
"""The package does not look the way a readable package should."""
|
||||
|
||||
|
||||
def remove_orphaned_slides(unpacked_dir: Path) -> list[str]:
|
||||
slides_dir = unpacked_dir / "ppt" / "slides"
|
||||
slides_rels_dir = slides_dir / "_rels"
|
||||
pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
|
||||
|
||||
if not slides_dir.exists():
|
||||
return []
|
||||
|
||||
referenced_slides = get_slides_in_sldidlst(unpacked_dir)
|
||||
on_disk = sorted(slides_dir.glob("slide*.xml"))
|
||||
|
||||
if on_disk and not any(s.name in referenced_slides for s in on_disk):
|
||||
listed = re.findall(
|
||||
r'<p:sldId[^>]*r:id="([^"]+)"',
|
||||
(unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8")
|
||||
if (unpacked_dir / "ppt" / "presentation.xml").exists()
|
||||
else "",
|
||||
)
|
||||
if listed:
|
||||
raise RefusedToClean(
|
||||
f"<p:sldIdLst> lists {len(listed)} slide(s) and none of the "
|
||||
f"{len(on_disk)} slide(s) on disk match any of them. Refusing to "
|
||||
f"delete them all — this is a parse failure, not an empty deck."
|
||||
)
|
||||
|
||||
removed = []
|
||||
|
||||
for slide_file in on_disk:
|
||||
if slide_file.name not in referenced_slides:
|
||||
rel_path = slide_file.relative_to(unpacked_dir)
|
||||
slide_file.unlink()
|
||||
removed.append(str(rel_path))
|
||||
|
||||
rels_file = slides_rels_dir / f"{slide_file.name}.rels"
|
||||
if rels_file.exists():
|
||||
rels_file.unlink()
|
||||
removed.append(str(rels_file.relative_to(unpacked_dir)))
|
||||
|
||||
if removed and pres_rels_path.exists():
|
||||
rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
|
||||
source_part = rels_source_part(pres_rels_path, unpacked_dir)
|
||||
changed = False
|
||||
|
||||
for rel in list(rels_dom.getElementsByTagName("Relationship")):
|
||||
if rel.getAttribute("Type") != SLIDE_REL_TYPE:
|
||||
continue
|
||||
part = opc_target(
|
||||
rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode")
|
||||
)
|
||||
if part is None:
|
||||
continue
|
||||
if posixpath.basename(part) not in referenced_slides:
|
||||
if rel.parentNode:
|
||||
rel.parentNode.removeChild(rel)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
with open(pres_rels_path, "wb") as f:
|
||||
f.write(rels_dom.toxml(encoding="utf-8"))
|
||||
|
||||
return removed
|
||||
|
||||
|
||||
def remove_trash_directory(unpacked_dir: Path) -> list[str]:
|
||||
trash_dir = unpacked_dir / "[trash]"
|
||||
removed = []
|
||||
|
||||
if trash_dir.exists() and trash_dir.is_dir():
|
||||
for file_path in trash_dir.iterdir():
|
||||
if file_path.is_file():
|
||||
rel_path = file_path.relative_to(unpacked_dir)
|
||||
removed.append(str(rel_path))
|
||||
file_path.unlink()
|
||||
trash_dir.rmdir()
|
||||
|
||||
return removed
|
||||
|
||||
|
||||
def _referenced_by(rels_files, unpacked_dir: Path) -> set:
|
||||
referenced = set()
|
||||
|
||||
for rels_file in rels_files:
|
||||
source_part = rels_source_part(rels_file, unpacked_dir)
|
||||
dom = defusedxml.minidom.parse(str(rels_file))
|
||||
for rel in dom.getElementsByTagName("Relationship"):
|
||||
part = opc_target(
|
||||
rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode")
|
||||
)
|
||||
if part is not None:
|
||||
referenced.add(Path(part))
|
||||
|
||||
return referenced
|
||||
|
||||
|
||||
def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]:
|
||||
resource_dirs = ["charts", "diagrams", "drawings"]
|
||||
removed = []
|
||||
|
||||
for dir_name in resource_dirs:
|
||||
rels_dir = unpacked_dir / "ppt" / dir_name / "_rels"
|
||||
if not rels_dir.exists():
|
||||
continue
|
||||
|
||||
for rels_file in rels_dir.glob("*.rels"):
|
||||
resource_file = rels_dir.parent / rels_file.name.replace(".rels", "")
|
||||
if not resource_file.exists():
|
||||
rels_file.unlink()
|
||||
removed.append(str(rels_file.relative_to(unpacked_dir)))
|
||||
|
||||
return removed
|
||||
|
||||
|
||||
def get_referenced_files(unpacked_dir: Path) -> set:
|
||||
return _referenced_by(sorted(unpacked_dir.rglob("*.rels")), unpacked_dir)
|
||||
|
||||
|
||||
def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]:
|
||||
resource_dirs = ["media", "embeddings", "charts", "diagrams", "tags", "drawings", "ink"]
|
||||
removed = []
|
||||
|
||||
for dir_name in resource_dirs:
|
||||
dir_path = unpacked_dir / "ppt" / dir_name
|
||||
if not dir_path.exists():
|
||||
continue
|
||||
|
||||
for file_path in dir_path.glob("*"):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
rel_path = file_path.relative_to(unpacked_dir)
|
||||
if rel_path not in referenced:
|
||||
file_path.unlink()
|
||||
removed.append(str(rel_path))
|
||||
|
||||
theme_dir = unpacked_dir / "ppt" / "theme"
|
||||
if theme_dir.exists():
|
||||
for file_path in theme_dir.glob("theme*.xml"):
|
||||
rel_path = file_path.relative_to(unpacked_dir)
|
||||
if rel_path not in referenced:
|
||||
file_path.unlink()
|
||||
removed.append(str(rel_path))
|
||||
theme_rels = theme_dir / "_rels" / f"{file_path.name}.rels"
|
||||
if theme_rels.exists():
|
||||
theme_rels.unlink()
|
||||
removed.append(str(theme_rels.relative_to(unpacked_dir)))
|
||||
|
||||
notes_dir = unpacked_dir / "ppt" / "notesSlides"
|
||||
if notes_dir.exists():
|
||||
for file_path in notes_dir.glob("*.xml"):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
rel_path = file_path.relative_to(unpacked_dir)
|
||||
if rel_path not in referenced:
|
||||
file_path.unlink()
|
||||
removed.append(str(rel_path))
|
||||
|
||||
notes_rels_dir = notes_dir / "_rels"
|
||||
if notes_rels_dir.exists():
|
||||
for file_path in notes_rels_dir.glob("*.rels"):
|
||||
notes_file = notes_dir / file_path.name.replace(".rels", "")
|
||||
if not notes_file.exists():
|
||||
file_path.unlink()
|
||||
removed.append(str(file_path.relative_to(unpacked_dir)))
|
||||
|
||||
return removed
|
||||
|
||||
|
||||
def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None:
|
||||
ct_path = unpacked_dir / "[Content_Types].xml"
|
||||
if not ct_path.exists():
|
||||
return
|
||||
|
||||
dom = defusedxml.minidom.parse(str(ct_path))
|
||||
changed = False
|
||||
|
||||
for override in list(dom.getElementsByTagName("Override")):
|
||||
part_name = override.getAttribute("PartName").lstrip("/")
|
||||
if part_name in removed_files:
|
||||
if override.parentNode:
|
||||
override.parentNode.removeChild(override)
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
with open(ct_path, "wb") as f:
|
||||
f.write(dom.toxml(encoding="utf-8"))
|
||||
|
||||
|
||||
def clean_unused_files(unpacked_dir: Path) -> list[str]:
|
||||
all_removed = []
|
||||
|
||||
if list(unpacked_dir.rglob("*.rels")) and not get_referenced_files(unpacked_dir):
|
||||
raise RefusedToClean(
|
||||
"no relationship in this package names a part we can resolve. "
|
||||
"Refusing to treat every file as unreferenced."
|
||||
)
|
||||
|
||||
slides_removed = remove_orphaned_slides(unpacked_dir)
|
||||
all_removed.extend(slides_removed)
|
||||
|
||||
trash_removed = remove_trash_directory(unpacked_dir)
|
||||
all_removed.extend(trash_removed)
|
||||
|
||||
while True:
|
||||
removed_rels = remove_orphaned_rels_files(unpacked_dir)
|
||||
referenced = get_referenced_files(unpacked_dir)
|
||||
removed_files = remove_orphaned_files(unpacked_dir, referenced)
|
||||
|
||||
total_removed = removed_rels + removed_files
|
||||
if not total_removed:
|
||||
break
|
||||
|
||||
all_removed.extend(total_removed)
|
||||
|
||||
if all_removed:
|
||||
update_content_types(unpacked_dir, all_removed)
|
||||
|
||||
return all_removed
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: python clean.py <unpacked_dir>", file=sys.stderr)
|
||||
print("Example: python clean.py unpacked/", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
unpacked_dir = Path(sys.argv[1])
|
||||
|
||||
if not unpacked_dir.exists():
|
||||
print(f"Error: {unpacked_dir} not found", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
removed = clean_unused_files(unpacked_dir)
|
||||
except (RefusedToClean, ValueError) as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
print("Nothing was deleted.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if removed:
|
||||
print(f"Removed {len(removed)} unreferenced files:")
|
||||
for f in removed:
|
||||
print(f" {f}")
|
||||
else:
|
||||
print("No unreferenced files found")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user