@heylemon/lemonade 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/dist/gateway/skills-http.js +74 -19
- package/package.json +1 -1
- package/skills/docx/SKILL.md +25 -30
- package/skills/docx/scripts/accept_changes.py +0 -17
- package/skills/docx/scripts/comment.py +10 -39
- package/skills/docx/scripts/office/helpers/merge_runs.py +1 -33
- package/skills/docx/scripts/office/helpers/simplify_redlines.py +0 -43
- package/skills/docx/scripts/office/pack.py +0 -30
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
- package/skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
- package/skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
- package/skills/docx/scripts/office/soffice.py +0 -55
- package/skills/docx/scripts/office/unpack.py +5 -27
- package/skills/docx/scripts/office/validate.py +19 -14
- package/skills/docx/scripts/office/validators/base.py +48 -224
- package/skills/docx/scripts/office/validators/docx.py +44 -117
- package/skills/docx/scripts/office/validators/pptx.py +2 -42
- package/skills/docx/scripts/office/validators/redlining.py +3 -40
- package/skills/pdf/SKILL.md +22 -15
- package/skills/pdf/{FORMS.md → forms.md} +0 -14
- package/skills/pdf/scripts/check_bounding_boxes.py +0 -5
- package/skills/pdf/scripts/check_fillable_fields.py +0 -1
- package/skills/pdf/scripts/convert_pdf_to_images.py +0 -2
- package/skills/pdf/scripts/create_validation_image.py +0 -4
- package/skills/pdf/scripts/extract_form_field_info.py +1 -31
- package/skills/pdf/scripts/extract_form_structure.py +0 -9
- package/skills/pdf/scripts/fill_fillable_fields.py +0 -23
- package/skills/pdf/scripts/fill_pdf_form_with_annotations.py +3 -38
- package/skills/pptx/SKILL.md +2 -29
- package/skills/pptx/editing.md +2 -2
- package/skills/pptx/pptxgenjs.md +53 -8
- package/skills/pptx/scripts/add_slide.py +0 -30
- package/skills/pptx/scripts/clean.py +0 -23
- package/skills/pptx/scripts/office/helpers/merge_runs.py +1 -33
- package/skills/pptx/scripts/office/helpers/simplify_redlines.py +0 -43
- package/skills/pptx/scripts/office/pack.py +0 -30
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
- package/skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
- package/skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
- package/skills/pptx/scripts/office/soffice.py +0 -55
- package/skills/pptx/scripts/office/unpack.py +5 -27
- package/skills/pptx/scripts/office/validate.py +19 -14
- package/skills/pptx/scripts/office/validators/base.py +48 -224
- package/skills/pptx/scripts/office/validators/docx.py +44 -117
- package/skills/pptx/scripts/office/validators/pptx.py +2 -42
- package/skills/pptx/scripts/office/validators/redlining.py +3 -40
- package/skills/pptx/scripts/thumbnail.py +0 -31
- package/skills/xlsx/SKILL.md +3 -26
- package/skills/xlsx/scripts/office/helpers/merge_runs.py +1 -33
- package/skills/xlsx/scripts/office/helpers/simplify_redlines.py +0 -43
- package/skills/xlsx/scripts/office/pack.py +0 -30
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -1499
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -1085
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -3081
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -287
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -1676
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -174
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -582
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -4439
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -570
- package/skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -116
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -42
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -50
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -49
- package/skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -33
- package/skills/xlsx/scripts/office/soffice.py +0 -55
- package/skills/xlsx/scripts/office/unpack.py +5 -27
- package/skills/xlsx/scripts/office/validate.py +19 -14
- package/skills/xlsx/scripts/office/validators/base.py +48 -224
- package/skills/xlsx/scripts/office/validators/docx.py +44 -117
- package/skills/xlsx/scripts/office/validators/pptx.py +2 -42
- package/skills/xlsx/scripts/office/validators/redlining.py +3 -40
- package/skills/xlsx/scripts/recalc.py +2 -26
- package/skills/docx/scripts/__init__.py +0 -1
- package/skills/docx/scripts/office/helpers/__init__.py +0 -0
- package/skills/docx/scripts/office/validators/__init__.py +0 -15
- package/skills/pptx/scripts/__init__.py +0 -0
- package/skills/pptx/scripts/office/helpers/__init__.py +0 -0
- package/skills/pptx/scripts/office/validators/__init__.py +0 -15
- package/skills/xlsx/scripts/office/helpers/__init__.py +0 -0
- package/skills/xlsx/scripts/office/validators/__init__.py +0 -15
- /package/skills/pdf/{REFERENCE.md → reference.md} +0 -0
|
@@ -9,7 +9,6 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class RedliningValidator:
|
|
12
|
-
"""Validator for tracked changes in Word documents."""
|
|
13
12
|
|
|
14
13
|
def __init__(self, unpacked_dir, original_docx, verbose=False, author="Claude"):
|
|
15
14
|
self.unpacked_dir = Path(unpacked_dir)
|
|
@@ -21,29 +20,23 @@ class RedliningValidator:
|
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
def repair(self) -> int:
|
|
24
|
-
"""No auto-repairs for redlining validation. Returns 0."""
|
|
25
23
|
return 0
|
|
26
24
|
|
|
27
25
|
def validate(self):
|
|
28
|
-
"""Main validation method that returns True if valid, False otherwise."""
|
|
29
|
-
# Verify unpacked directory exists and has correct structure
|
|
30
26
|
modified_file = self.unpacked_dir / "word" / "document.xml"
|
|
31
27
|
if not modified_file.exists():
|
|
32
28
|
print(f"FAILED - Modified document.xml not found at {modified_file}")
|
|
33
29
|
return False
|
|
34
30
|
|
|
35
|
-
# First, check if there are any tracked changes by the author to validate
|
|
36
31
|
try:
|
|
37
32
|
import xml.etree.ElementTree as ET
|
|
38
33
|
|
|
39
34
|
tree = ET.parse(modified_file)
|
|
40
35
|
root = tree.getroot()
|
|
41
36
|
|
|
42
|
-
# Check for w:del or w:ins tags by the specified author
|
|
43
37
|
del_elements = root.findall(".//w:del", self.namespaces)
|
|
44
38
|
ins_elements = root.findall(".//w:ins", self.namespaces)
|
|
45
39
|
|
|
46
|
-
# Filter to only include changes by the specified author
|
|
47
40
|
author_del_elements = [
|
|
48
41
|
elem
|
|
49
42
|
for elem in del_elements
|
|
@@ -55,21 +48,17 @@ class RedliningValidator:
|
|
|
55
48
|
if elem.get(f"{{{self.namespaces['w']}}}author") == self.author
|
|
56
49
|
]
|
|
57
50
|
|
|
58
|
-
# Redlining validation is only needed if tracked changes by the author have been used.
|
|
59
51
|
if not author_del_elements and not author_ins_elements:
|
|
60
52
|
if self.verbose:
|
|
61
53
|
print(f"PASSED - No tracked changes by {self.author} found.")
|
|
62
54
|
return True
|
|
63
55
|
|
|
64
56
|
except Exception:
|
|
65
|
-
# If we can't parse the XML, continue with full validation
|
|
66
57
|
pass
|
|
67
58
|
|
|
68
|
-
# Create temporary directory for unpacking original docx
|
|
69
59
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
70
60
|
temp_path = Path(temp_dir)
|
|
71
61
|
|
|
72
|
-
# Unpack original docx
|
|
73
62
|
try:
|
|
74
63
|
with zipfile.ZipFile(self.original_docx, "r") as zip_ref:
|
|
75
64
|
zip_ref.extractall(temp_path)
|
|
@@ -84,7 +73,6 @@ class RedliningValidator:
|
|
|
84
73
|
)
|
|
85
74
|
return False
|
|
86
75
|
|
|
87
|
-
# Parse both XML files using xml.etree.ElementTree for redlining validation
|
|
88
76
|
try:
|
|
89
77
|
import xml.etree.ElementTree as ET
|
|
90
78
|
|
|
@@ -96,16 +84,13 @@ class RedliningValidator:
|
|
|
96
84
|
print(f"FAILED - Error parsing XML files: {e}")
|
|
97
85
|
return False
|
|
98
86
|
|
|
99
|
-
# Remove the author's tracked changes from both documents
|
|
100
87
|
self._remove_author_tracked_changes(original_root)
|
|
101
88
|
self._remove_author_tracked_changes(modified_root)
|
|
102
89
|
|
|
103
|
-
# Extract and compare text content
|
|
104
90
|
modified_text = self._extract_text_content(modified_root)
|
|
105
91
|
original_text = self._extract_text_content(original_root)
|
|
106
92
|
|
|
107
93
|
if modified_text != original_text:
|
|
108
|
-
# Show detailed character-level differences for each paragraph
|
|
109
94
|
error_message = self._generate_detailed_diff(
|
|
110
95
|
original_text, modified_text
|
|
111
96
|
)
|
|
@@ -117,7 +102,6 @@ class RedliningValidator:
|
|
|
117
102
|
return True
|
|
118
103
|
|
|
119
104
|
def _generate_detailed_diff(self, original_text, modified_text):
|
|
120
|
-
"""Generate detailed word-level differences using git word diff."""
|
|
121
105
|
error_parts = [
|
|
122
106
|
f"FAILED - Document text doesn't match after removing {self.author}'s tracked changes",
|
|
123
107
|
"",
|
|
@@ -132,7 +116,6 @@ class RedliningValidator:
|
|
|
132
116
|
"",
|
|
133
117
|
]
|
|
134
118
|
|
|
135
|
-
# Show git word diff
|
|
136
119
|
git_diff = self._get_git_word_diff(original_text, modified_text)
|
|
137
120
|
if git_diff:
|
|
138
121
|
error_parts.extend(["Differences:", "============", git_diff])
|
|
@@ -142,26 +125,23 @@ class RedliningValidator:
|
|
|
142
125
|
return "\n".join(error_parts)
|
|
143
126
|
|
|
144
127
|
def _get_git_word_diff(self, original_text, modified_text):
|
|
145
|
-
"""Generate word diff using git with character-level precision."""
|
|
146
128
|
try:
|
|
147
129
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
148
130
|
temp_path = Path(temp_dir)
|
|
149
131
|
|
|
150
|
-
# Create two files
|
|
151
132
|
original_file = temp_path / "original.txt"
|
|
152
133
|
modified_file = temp_path / "modified.txt"
|
|
153
134
|
|
|
154
135
|
original_file.write_text(original_text, encoding="utf-8")
|
|
155
136
|
modified_file.write_text(modified_text, encoding="utf-8")
|
|
156
137
|
|
|
157
|
-
# Try character-level diff first for precise differences
|
|
158
138
|
result = subprocess.run(
|
|
159
139
|
[
|
|
160
140
|
"git",
|
|
161
141
|
"diff",
|
|
162
142
|
"--word-diff=plain",
|
|
163
|
-
"--word-diff-regex=.",
|
|
164
|
-
"-U0",
|
|
143
|
+
"--word-diff-regex=.",
|
|
144
|
+
"-U0",
|
|
165
145
|
"--no-index",
|
|
166
146
|
str(original_file),
|
|
167
147
|
str(modified_file),
|
|
@@ -171,9 +151,7 @@ class RedliningValidator:
|
|
|
171
151
|
)
|
|
172
152
|
|
|
173
153
|
if result.stdout.strip():
|
|
174
|
-
# Clean up the output - remove git diff header lines
|
|
175
154
|
lines = result.stdout.split("\n")
|
|
176
|
-
# Skip the header lines (diff --git, index, +++, ---, @@)
|
|
177
155
|
content_lines = []
|
|
178
156
|
in_content = False
|
|
179
157
|
for line in lines:
|
|
@@ -186,13 +164,12 @@ class RedliningValidator:
|
|
|
186
164
|
if content_lines:
|
|
187
165
|
return "\n".join(content_lines)
|
|
188
166
|
|
|
189
|
-
# Fallback to word-level diff if character-level is too verbose
|
|
190
167
|
result = subprocess.run(
|
|
191
168
|
[
|
|
192
169
|
"git",
|
|
193
170
|
"diff",
|
|
194
171
|
"--word-diff=plain",
|
|
195
|
-
"-U0",
|
|
172
|
+
"-U0",
|
|
196
173
|
"--no-index",
|
|
197
174
|
str(original_file),
|
|
198
175
|
str(modified_file),
|
|
@@ -214,18 +191,15 @@ class RedliningValidator:
|
|
|
214
191
|
return "\n".join(content_lines)
|
|
215
192
|
|
|
216
193
|
except (subprocess.CalledProcessError, FileNotFoundError, Exception):
|
|
217
|
-
# Git not available or other error, return None to use fallback
|
|
218
194
|
pass
|
|
219
195
|
|
|
220
196
|
return None
|
|
221
197
|
|
|
222
198
|
def _remove_author_tracked_changes(self, root):
|
|
223
|
-
"""Remove tracked changes authored by the specified author from the XML root."""
|
|
224
199
|
ins_tag = f"{{{self.namespaces['w']}}}ins"
|
|
225
200
|
del_tag = f"{{{self.namespaces['w']}}}del"
|
|
226
201
|
author_attr = f"{{{self.namespaces['w']}}}author"
|
|
227
202
|
|
|
228
|
-
# Remove w:ins elements
|
|
229
203
|
for parent in root.iter():
|
|
230
204
|
to_remove = []
|
|
231
205
|
for child in parent:
|
|
@@ -234,7 +208,6 @@ class RedliningValidator:
|
|
|
234
208
|
for elem in to_remove:
|
|
235
209
|
parent.remove(elem)
|
|
236
210
|
|
|
237
|
-
# Unwrap content in w:del elements where author matches
|
|
238
211
|
deltext_tag = f"{{{self.namespaces['w']}}}delText"
|
|
239
212
|
t_tag = f"{{{self.namespaces['w']}}}t"
|
|
240
213
|
|
|
@@ -244,36 +217,26 @@ class RedliningValidator:
|
|
|
244
217
|
if child.tag == del_tag and child.get(author_attr) == self.author:
|
|
245
218
|
to_process.append((child, list(parent).index(child)))
|
|
246
219
|
|
|
247
|
-
# Process in reverse order to maintain indices
|
|
248
220
|
for del_elem, del_index in reversed(to_process):
|
|
249
|
-
# Convert w:delText to w:t before moving
|
|
250
221
|
for elem in del_elem.iter():
|
|
251
222
|
if elem.tag == deltext_tag:
|
|
252
223
|
elem.tag = t_tag
|
|
253
224
|
|
|
254
|
-
# Move all children of w:del to its parent before removing w:del
|
|
255
225
|
for child in reversed(list(del_elem)):
|
|
256
226
|
parent.insert(del_index, child)
|
|
257
227
|
parent.remove(del_elem)
|
|
258
228
|
|
|
259
229
|
def _extract_text_content(self, root):
|
|
260
|
-
"""Extract text content from Word XML, preserving paragraph structure.
|
|
261
|
-
|
|
262
|
-
Empty paragraphs are skipped to avoid false positives when tracked
|
|
263
|
-
insertions add only structural elements without text content.
|
|
264
|
-
"""
|
|
265
230
|
p_tag = f"{{{self.namespaces['w']}}}p"
|
|
266
231
|
t_tag = f"{{{self.namespaces['w']}}}t"
|
|
267
232
|
|
|
268
233
|
paragraphs = []
|
|
269
234
|
for p_elem in root.findall(f".//{p_tag}"):
|
|
270
|
-
# Get all text elements within this paragraph
|
|
271
235
|
text_parts = []
|
|
272
236
|
for t_elem in p_elem.findall(f".//{t_tag}"):
|
|
273
237
|
if t_elem.text:
|
|
274
238
|
text_parts.append(t_elem.text)
|
|
275
239
|
paragraph_text = "".join(text_parts)
|
|
276
|
-
# Skip empty paragraphs - they don't affect content validation
|
|
277
240
|
if paragraph_text:
|
|
278
241
|
paragraphs.append(paragraph_text)
|
|
279
242
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
1
|
"""Create thumbnail grids from PowerPoint presentation slides.
|
|
3
2
|
|
|
4
3
|
Creates a grid layout of slide thumbnails for quick visual analysis.
|
|
@@ -27,7 +26,6 @@ import defusedxml.minidom
|
|
|
27
26
|
from office.soffice import get_soffice_env
|
|
28
27
|
from PIL import Image, ImageDraw, ImageFont
|
|
29
28
|
|
|
30
|
-
# Constants
|
|
31
29
|
THUMBNAIL_WIDTH = 300
|
|
32
30
|
CONVERSION_DPI = 100
|
|
33
31
|
MAX_COLS = 6
|
|
@@ -71,7 +69,6 @@ def main():
|
|
|
71
69
|
output_path = Path(f"{args.output_prefix}.jpg")
|
|
72
70
|
|
|
73
71
|
try:
|
|
74
|
-
# Get slide info (filenames and hidden status) in presentation order
|
|
75
72
|
slide_info = get_slide_info(input_path)
|
|
76
73
|
|
|
77
74
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
@@ -82,7 +79,6 @@ def main():
|
|
|
82
79
|
print("Error: No slides found", file=sys.stderr)
|
|
83
80
|
sys.exit(1)
|
|
84
81
|
|
|
85
|
-
# Build slide list with images (visible) or placeholders (hidden)
|
|
86
82
|
slides = build_slide_list(slide_info, visible_images, temp_path)
|
|
87
83
|
|
|
88
84
|
grid_files = create_grids(slides, cols, THUMBNAIL_WIDTH, output_path)
|
|
@@ -97,12 +93,7 @@ def main():
|
|
|
97
93
|
|
|
98
94
|
|
|
99
95
|
def get_slide_info(pptx_path: Path) -> list[dict]:
|
|
100
|
-
"""Get slide filenames and hidden status in presentation order.
|
|
101
|
-
|
|
102
|
-
Returns list of dicts with 'name' and 'hidden' keys.
|
|
103
|
-
"""
|
|
104
96
|
with zipfile.ZipFile(pptx_path, "r") as zf:
|
|
105
|
-
# Read presentation.xml.rels to get rId -> slide filename mapping
|
|
106
97
|
rels_content = zf.read("ppt/_rels/presentation.xml.rels").decode("utf-8")
|
|
107
98
|
rels_dom = defusedxml.minidom.parseString(rels_content)
|
|
108
99
|
|
|
@@ -114,7 +105,6 @@ def get_slide_info(pptx_path: Path) -> list[dict]:
|
|
|
114
105
|
if "slide" in rel_type and target.startswith("slides/"):
|
|
115
106
|
rid_to_slide[rid] = target.replace("slides/", "")
|
|
116
107
|
|
|
117
|
-
# Read presentation.xml to get slide order and hidden status
|
|
118
108
|
pres_content = zf.read("ppt/presentation.xml").decode("utf-8")
|
|
119
109
|
pres_dom = defusedxml.minidom.parseString(pres_content)
|
|
120
110
|
|
|
@@ -122,7 +112,6 @@ def get_slide_info(pptx_path: Path) -> list[dict]:
|
|
|
122
112
|
for sld_id in pres_dom.getElementsByTagName("p:sldId"):
|
|
123
113
|
rid = sld_id.getAttribute("r:id")
|
|
124
114
|
if rid in rid_to_slide:
|
|
125
|
-
# Check if slide is hidden (show="0")
|
|
126
115
|
hidden = sld_id.getAttribute("show") == "0"
|
|
127
116
|
slides.append({"name": rid_to_slide[rid], "hidden": hidden})
|
|
128
117
|
|
|
@@ -134,11 +123,6 @@ def build_slide_list(
|
|
|
134
123
|
visible_images: list[Path],
|
|
135
124
|
temp_dir: Path,
|
|
136
125
|
) -> list[tuple[Path, str]]:
|
|
137
|
-
"""Build list of (image_path, slide_name) tuples.
|
|
138
|
-
|
|
139
|
-
Hidden slides get placeholder images.
|
|
140
|
-
"""
|
|
141
|
-
# Get placeholder size from first visible image
|
|
142
126
|
if visible_images:
|
|
143
127
|
with Image.open(visible_images[0]) as img:
|
|
144
128
|
placeholder_size = img.size
|
|
@@ -150,13 +134,11 @@ def build_slide_list(
|
|
|
150
134
|
|
|
151
135
|
for info in slide_info:
|
|
152
136
|
if info["hidden"]:
|
|
153
|
-
# Create placeholder for hidden slide
|
|
154
137
|
placeholder_path = temp_dir / f"hidden-{info['name']}.jpg"
|
|
155
138
|
placeholder_img = create_hidden_placeholder(placeholder_size)
|
|
156
139
|
placeholder_img.save(placeholder_path, "JPEG")
|
|
157
140
|
slides.append((placeholder_path, f"{info['name']} (hidden)"))
|
|
158
141
|
else:
|
|
159
|
-
# Use visible image
|
|
160
142
|
if visible_idx < len(visible_images):
|
|
161
143
|
slides.append((visible_images[visible_idx], info["name"]))
|
|
162
144
|
visible_idx += 1
|
|
@@ -165,7 +147,6 @@ def build_slide_list(
|
|
|
165
147
|
|
|
166
148
|
|
|
167
149
|
def create_hidden_placeholder(size: tuple[int, int]) -> Image.Image:
|
|
168
|
-
"""Create placeholder image for hidden slides (gray with X pattern)."""
|
|
169
150
|
img = Image.new("RGB", size, color="#F0F0F0")
|
|
170
151
|
draw = ImageDraw.Draw(img)
|
|
171
152
|
line_width = max(5, min(size) // 100)
|
|
@@ -175,10 +156,8 @@ def create_hidden_placeholder(size: tuple[int, int]) -> Image.Image:
|
|
|
175
156
|
|
|
176
157
|
|
|
177
158
|
def convert_to_images(pptx_path: Path, temp_dir: Path) -> list[Path]:
|
|
178
|
-
"""Convert PowerPoint to images via PDF."""
|
|
179
159
|
pdf_path = temp_dir / f"{pptx_path.stem}.pdf"
|
|
180
160
|
|
|
181
|
-
# Convert to PDF
|
|
182
161
|
result = subprocess.run(
|
|
183
162
|
[
|
|
184
163
|
"soffice",
|
|
@@ -196,7 +175,6 @@ def convert_to_images(pptx_path: Path, temp_dir: Path) -> list[Path]:
|
|
|
196
175
|
if result.returncode != 0 or not pdf_path.exists():
|
|
197
176
|
raise RuntimeError("PDF conversion failed")
|
|
198
177
|
|
|
199
|
-
# Convert PDF to images
|
|
200
178
|
result = subprocess.run(
|
|
201
179
|
[
|
|
202
180
|
"pdftoppm",
|
|
@@ -221,7 +199,6 @@ def create_grids(
|
|
|
221
199
|
width: int,
|
|
222
200
|
output_path: Path,
|
|
223
201
|
) -> list[str]:
|
|
224
|
-
"""Create thumbnail grids, max cols×(cols+1) images per grid."""
|
|
225
202
|
max_per_grid = cols * (cols + 1)
|
|
226
203
|
grid_files = []
|
|
227
204
|
|
|
@@ -250,21 +227,17 @@ def create_grid(
|
|
|
250
227
|
cols: int,
|
|
251
228
|
width: int,
|
|
252
229
|
) -> Image.Image:
|
|
253
|
-
"""Create a single thumbnail grid."""
|
|
254
230
|
font_size = int(width * FONT_SIZE_RATIO)
|
|
255
231
|
label_padding = int(font_size * LABEL_PADDING_RATIO)
|
|
256
232
|
|
|
257
|
-
# Get dimensions from first image
|
|
258
233
|
with Image.open(slides[0][0]) as img:
|
|
259
234
|
aspect = img.height / img.width
|
|
260
235
|
height = int(width * aspect)
|
|
261
236
|
|
|
262
|
-
# Calculate grid size
|
|
263
237
|
rows = (len(slides) + cols - 1) // cols
|
|
264
238
|
grid_w = cols * width + (cols + 1) * GRID_PADDING
|
|
265
239
|
grid_h = rows * (height + font_size + label_padding * 2) + (rows + 1) * GRID_PADDING
|
|
266
240
|
|
|
267
|
-
# Create grid
|
|
268
241
|
grid = Image.new("RGB", (grid_w, grid_h), "white")
|
|
269
242
|
draw = ImageDraw.Draw(grid)
|
|
270
243
|
|
|
@@ -273,7 +246,6 @@ def create_grid(
|
|
|
273
246
|
except Exception:
|
|
274
247
|
font = ImageFont.load_default()
|
|
275
248
|
|
|
276
|
-
# Place thumbnails
|
|
277
249
|
for i, (img_path, slide_name) in enumerate(slides):
|
|
278
250
|
row, col = i // cols, i % cols
|
|
279
251
|
x = col * width + (col + 1) * GRID_PADDING
|
|
@@ -281,7 +253,6 @@ def create_grid(
|
|
|
281
253
|
row * (height + font_size + label_padding * 2) + (row + 1) * GRID_PADDING
|
|
282
254
|
)
|
|
283
255
|
|
|
284
|
-
# Add slide filename label
|
|
285
256
|
label = slide_name
|
|
286
257
|
bbox = draw.textbbox((0, 0), label, font=font)
|
|
287
258
|
text_w = bbox[2] - bbox[0]
|
|
@@ -292,7 +263,6 @@ def create_grid(
|
|
|
292
263
|
font=font,
|
|
293
264
|
)
|
|
294
265
|
|
|
295
|
-
# Add thumbnail
|
|
296
266
|
y_thumbnail = y_base + label_padding + font_size + label_padding
|
|
297
267
|
|
|
298
268
|
with Image.open(img_path) as img:
|
|
@@ -302,7 +272,6 @@ def create_grid(
|
|
|
302
272
|
ty = y_thumbnail + (height - h) // 2
|
|
303
273
|
grid.paste(img, (tx, ty))
|
|
304
274
|
|
|
305
|
-
# Add border
|
|
306
275
|
if BORDER_WIDTH > 0:
|
|
307
276
|
draw.rectangle(
|
|
308
277
|
[
|
package/skills/xlsx/SKILL.md
CHANGED
|
@@ -4,29 +4,6 @@ description: "Use this skill any time a spreadsheet file is the primary input or
|
|
|
4
4
|
license: Proprietary. LICENSE.txt has complete terms
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Excel / Spreadsheet Skill
|
|
8
|
-
|
|
9
|
-
## IMPORTANT: Save to Desktop
|
|
10
|
-
|
|
11
|
-
**Always save created `.xlsx` and `.csv` files to `~/Desktop/`** (e.g. `~/Desktop/spreadsheet.xlsx`). Never save to the agent workspace or hidden directories — the user needs easy access to the file.
|
|
12
|
-
|
|
13
|
-
## CRITICAL: Integration Priority
|
|
14
|
-
|
|
15
|
-
### 1. `lemon-sheets` CLI (For Google Sheets)
|
|
16
|
-
If user wants a Google Sheet (shareable, collaborative), use `lemon-sheets`:
|
|
17
|
-
- `lemon-sheets create "Title"` - Create a new sheet
|
|
18
|
-
- `lemon-sheets read <id>` - Read a spreadsheet
|
|
19
|
-
- `lemon-sheets update <id>` - Update cell values
|
|
20
|
-
- `lemon-sheets append <id>` - Add rows
|
|
21
|
-
|
|
22
|
-
### 2. Local XLSX (For Files)
|
|
23
|
-
If user wants a local `.xlsx` file, or `lemon-sheets` is not connected, use the local creation methods below.
|
|
24
|
-
|
|
25
|
-
### 3. Browser (LAST RESORT)
|
|
26
|
-
Only if `lemon-sheets` CLI fails AND user explicitly requests Google Sheets in browser.
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
7
|
# Requirements for Outputs
|
|
31
8
|
|
|
32
9
|
## All Excel files
|
|
@@ -114,7 +91,7 @@ df.info() # Column info
|
|
|
114
91
|
df.describe() # Statistics
|
|
115
92
|
|
|
116
93
|
# Write Excel
|
|
117
|
-
df.to_excel(
|
|
94
|
+
df.to_excel('output.xlsx', index=False)
|
|
118
95
|
```
|
|
119
96
|
|
|
120
97
|
## Excel File Workflows
|
|
@@ -197,7 +174,7 @@ sheet['A1'].alignment = Alignment(horizontal='center')
|
|
|
197
174
|
# Column width
|
|
198
175
|
sheet.column_dimensions['A'].width = 20
|
|
199
176
|
|
|
200
|
-
wb.save(
|
|
177
|
+
wb.save('output.xlsx')
|
|
201
178
|
```
|
|
202
179
|
|
|
203
180
|
### Editing existing Excel files
|
|
@@ -224,7 +201,7 @@ sheet.delete_cols(3) # Delete column 3
|
|
|
224
201
|
new_sheet = wb.create_sheet('NewSheet')
|
|
225
202
|
new_sheet['A1'] = 'Data'
|
|
226
203
|
|
|
227
|
-
wb.save(
|
|
204
|
+
wb.save('modified.xlsx')
|
|
228
205
|
```
|
|
229
206
|
|
|
230
207
|
## Recalculating formulas
|
|
@@ -14,14 +14,6 @@ import defusedxml.minidom
|
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def merge_runs(input_dir: str) -> tuple[int, str]:
|
|
17
|
-
"""Merge adjacent runs in document.xml.
|
|
18
|
-
|
|
19
|
-
Args:
|
|
20
|
-
input_dir: Path to unpacked DOCX directory
|
|
21
|
-
|
|
22
|
-
Returns:
|
|
23
|
-
(merge_count, message)
|
|
24
|
-
"""
|
|
25
17
|
doc_xml = Path(input_dir) / "word" / "document.xml"
|
|
26
18
|
|
|
27
19
|
if not doc_xml.exists():
|
|
@@ -31,14 +23,11 @@ def merge_runs(input_dir: str) -> tuple[int, str]:
|
|
|
31
23
|
dom = defusedxml.minidom.parseString(doc_xml.read_text(encoding="utf-8"))
|
|
32
24
|
root = dom.documentElement
|
|
33
25
|
|
|
34
|
-
# Clean up elements that block merging
|
|
35
26
|
_remove_elements(root, "proofErr")
|
|
36
27
|
_strip_run_rsid_attrs(root)
|
|
37
28
|
|
|
38
|
-
# Find all containers that have runs
|
|
39
29
|
containers = {run.parentNode for run in _find_elements(root, "r")}
|
|
40
30
|
|
|
41
|
-
# Merge runs in each container
|
|
42
31
|
merge_count = 0
|
|
43
32
|
for container in containers:
|
|
44
33
|
merge_count += _merge_runs_in(container)
|
|
@@ -50,11 +39,9 @@ def merge_runs(input_dir: str) -> tuple[int, str]:
|
|
|
50
39
|
return 0, f"Error: {e}"
|
|
51
40
|
|
|
52
41
|
|
|
53
|
-
# --- Element helpers ---
|
|
54
42
|
|
|
55
43
|
|
|
56
44
|
def _find_elements(root, tag: str) -> list:
|
|
57
|
-
"""Find all elements matching tag name (with or without namespace)."""
|
|
58
45
|
results = []
|
|
59
46
|
|
|
60
47
|
def traverse(node):
|
|
@@ -70,7 +57,6 @@ def _find_elements(root, tag: str) -> list:
|
|
|
70
57
|
|
|
71
58
|
|
|
72
59
|
def _get_child(parent, tag: str):
|
|
73
|
-
"""Get first child element matching tag name."""
|
|
74
60
|
for child in parent.childNodes:
|
|
75
61
|
if child.nodeType == child.ELEMENT_NODE:
|
|
76
62
|
name = child.localName or child.tagName
|
|
@@ -80,7 +66,6 @@ def _get_child(parent, tag: str):
|
|
|
80
66
|
|
|
81
67
|
|
|
82
68
|
def _get_children(parent, tag: str) -> list:
|
|
83
|
-
"""Get all direct child elements matching tag name."""
|
|
84
69
|
results = []
|
|
85
70
|
for child in parent.childNodes:
|
|
86
71
|
if child.nodeType == child.ELEMENT_NODE:
|
|
@@ -91,7 +76,6 @@ def _get_children(parent, tag: str) -> list:
|
|
|
91
76
|
|
|
92
77
|
|
|
93
78
|
def _is_adjacent(elem1, elem2) -> bool:
|
|
94
|
-
"""Check if two elements are adjacent (only whitespace between them)."""
|
|
95
79
|
node = elem1.nextSibling
|
|
96
80
|
while node:
|
|
97
81
|
if node == elem2:
|
|
@@ -104,34 +88,28 @@ def _is_adjacent(elem1, elem2) -> bool:
|
|
|
104
88
|
return False
|
|
105
89
|
|
|
106
90
|
|
|
107
|
-
# --- Cleanup functions ---
|
|
108
91
|
|
|
109
92
|
|
|
110
93
|
def _remove_elements(root, tag: str):
|
|
111
|
-
"""Remove all elements matching tag name."""
|
|
112
94
|
for elem in _find_elements(root, tag):
|
|
113
95
|
if elem.parentNode:
|
|
114
96
|
elem.parentNode.removeChild(elem)
|
|
115
97
|
|
|
116
98
|
|
|
117
99
|
def _strip_run_rsid_attrs(root):
|
|
118
|
-
"""Remove rsid attributes from all run elements."""
|
|
119
100
|
for run in _find_elements(root, "r"):
|
|
120
101
|
for attr in list(run.attributes.values()):
|
|
121
102
|
if "rsid" in attr.name.lower():
|
|
122
103
|
run.removeAttribute(attr.name)
|
|
123
104
|
|
|
124
105
|
|
|
125
|
-
# --- Merge functions ---
|
|
126
106
|
|
|
127
107
|
|
|
128
108
|
def _merge_runs_in(container) -> int:
|
|
129
|
-
"""Merge adjacent runs with identical formatting in a container element."""
|
|
130
109
|
merge_count = 0
|
|
131
110
|
run = _first_child_run(container)
|
|
132
111
|
|
|
133
112
|
while run:
|
|
134
|
-
# Absorb adjacent runs with same formatting
|
|
135
113
|
while True:
|
|
136
114
|
next_elem = _next_element_sibling(run)
|
|
137
115
|
if next_elem and _is_run(next_elem) and _can_merge(run, next_elem):
|
|
@@ -148,7 +126,6 @@ def _merge_runs_in(container) -> int:
|
|
|
148
126
|
|
|
149
127
|
|
|
150
128
|
def _first_child_run(container):
|
|
151
|
-
"""Get the first run child of a container."""
|
|
152
129
|
for child in container.childNodes:
|
|
153
130
|
if child.nodeType == child.ELEMENT_NODE and _is_run(child):
|
|
154
131
|
return child
|
|
@@ -156,7 +133,6 @@ def _first_child_run(container):
|
|
|
156
133
|
|
|
157
134
|
|
|
158
135
|
def _next_element_sibling(node):
|
|
159
|
-
"""Get the next element sibling, skipping text/whitespace nodes."""
|
|
160
136
|
sibling = node.nextSibling
|
|
161
137
|
while sibling:
|
|
162
138
|
if sibling.nodeType == sibling.ELEMENT_NODE:
|
|
@@ -166,25 +142,21 @@ def _next_element_sibling(node):
|
|
|
166
142
|
|
|
167
143
|
|
|
168
144
|
def _next_sibling_run(node):
|
|
169
|
-
"""Get the next sibling that is a run element."""
|
|
170
145
|
sibling = node.nextSibling
|
|
171
146
|
while sibling:
|
|
172
147
|
if sibling.nodeType == sibling.ELEMENT_NODE:
|
|
173
148
|
if _is_run(sibling):
|
|
174
149
|
return sibling
|
|
175
|
-
# Skip non-run elements (bookmarks, etc.) but keep looking
|
|
176
150
|
sibling = sibling.nextSibling
|
|
177
151
|
return None
|
|
178
152
|
|
|
179
153
|
|
|
180
154
|
def _is_run(node) -> bool:
|
|
181
|
-
"""Check if node is a run element."""
|
|
182
155
|
name = node.localName or node.tagName
|
|
183
156
|
return name == "r" or name.endswith(":r")
|
|
184
157
|
|
|
185
158
|
|
|
186
159
|
def _can_merge(run1, run2) -> bool:
|
|
187
|
-
"""Check if two runs have identical formatting."""
|
|
188
160
|
rpr1 = _get_child(run1, "rPr")
|
|
189
161
|
rpr2 = _get_child(run2, "rPr")
|
|
190
162
|
|
|
@@ -192,11 +164,10 @@ def _can_merge(run1, run2) -> bool:
|
|
|
192
164
|
return False
|
|
193
165
|
if rpr1 is None:
|
|
194
166
|
return True
|
|
195
|
-
return rpr1.toxml() == rpr2.toxml()
|
|
167
|
+
return rpr1.toxml() == rpr2.toxml()
|
|
196
168
|
|
|
197
169
|
|
|
198
170
|
def _merge_run_content(target, source):
|
|
199
|
-
"""Move content from source run to target run (excluding rPr)."""
|
|
200
171
|
for child in list(source.childNodes):
|
|
201
172
|
if child.nodeType == child.ELEMENT_NODE:
|
|
202
173
|
name = child.localName or child.tagName
|
|
@@ -205,10 +176,8 @@ def _merge_run_content(target, source):
|
|
|
205
176
|
|
|
206
177
|
|
|
207
178
|
def _consolidate_text(run):
|
|
208
|
-
"""Merge adjacent <w:t> elements within a run."""
|
|
209
179
|
t_elements = _get_children(run, "t")
|
|
210
180
|
|
|
211
|
-
# Work backwards to safely remove elements
|
|
212
181
|
for i in range(len(t_elements) - 1, 0, -1):
|
|
213
182
|
curr, prev = t_elements[i], t_elements[i - 1]
|
|
214
183
|
|
|
@@ -222,7 +191,6 @@ def _consolidate_text(run):
|
|
|
222
191
|
else:
|
|
223
192
|
prev.appendChild(run.ownerDocument.createTextNode(merged))
|
|
224
193
|
|
|
225
|
-
# Preserve whitespace if needed
|
|
226
194
|
if merged.startswith(" ") or merged.endswith(" "):
|
|
227
195
|
prev.setAttribute("xml:space", "preserve")
|
|
228
196
|
elif prev.hasAttribute("xml:space"):
|