@heylemon/lemonade 0.5.9 → 0.6.0
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
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
c7c3dae0b04843ef55701aa1424c692bd8fd4a90bdea1187fb53373a6da54271
|
package/package.json
CHANGED
package/skills/docx/SKILL.md
CHANGED
|
@@ -205,49 +205,78 @@ new Paragraph({
|
|
|
205
205
|
})
|
|
206
206
|
```
|
|
207
207
|
|
|
208
|
-
### Tables:
|
|
208
|
+
### Tables: Width Rules (CRITICAL)
|
|
209
209
|
|
|
210
|
-
Tables
|
|
210
|
+
Tables need width at two levels:
|
|
211
|
+
1. **Table level:** `WidthType.PERCENTAGE` (100%) for the outer container
|
|
212
|
+
2. **Cell level:** `WidthType.DXA` for every individual cell — never PERCENTAGE on cells
|
|
213
|
+
|
|
214
|
+
Cell widths MUST sum to the content area width. With 1-inch margins on US Letter:
|
|
215
|
+
- Content width = 8.5" - 2" = 6.5" = **9360 DXA**
|
|
216
|
+
- Divide 9360 among your columns (e.g., 3 cols: 3000 + 3000 + 3360 = 9360)
|
|
217
|
+
|
|
218
|
+
**Every row must use the same cell widths for each column.** Do not vary widths between rows — this causes misaligned columns in Pages and Google Docs. Define your column widths once and reuse them:
|
|
211
219
|
|
|
212
220
|
```javascript
|
|
221
|
+
// Define column widths ONCE — reuse for every row
|
|
222
|
+
const COL_WIDTHS = [3000, 3000, 3360]; // Must sum to 9360
|
|
223
|
+
|
|
213
224
|
new Table({
|
|
214
225
|
width: {
|
|
215
226
|
size: 100,
|
|
216
|
-
type: WidthType.PERCENTAGE // Table
|
|
227
|
+
type: WidthType.PERCENTAGE // Table stretches to content area
|
|
217
228
|
},
|
|
218
229
|
rows: [
|
|
230
|
+
// Header row
|
|
219
231
|
new TableRow({
|
|
220
232
|
children: [
|
|
221
233
|
new TableCell({
|
|
222
|
-
width: { size:
|
|
223
|
-
|
|
234
|
+
width: { size: COL_WIDTHS[0], type: WidthType.DXA },
|
|
235
|
+
shading: { fill: "1B3A5C", type: ShadingType.CLEAR },
|
|
236
|
+
children: [new Paragraph({ children: [new TextRun({ text: "Category", bold: true, color: "FFFFFF", size: 22 })] })]
|
|
224
237
|
}),
|
|
225
238
|
new TableCell({
|
|
226
|
-
width: { size:
|
|
227
|
-
|
|
239
|
+
width: { size: COL_WIDTHS[1], type: WidthType.DXA },
|
|
240
|
+
shading: { fill: "1B3A5C", type: ShadingType.CLEAR },
|
|
241
|
+
children: [new Paragraph({ children: [new TextRun({ text: "Q3 Revenue", bold: true, color: "FFFFFF", size: 22 })] })]
|
|
228
242
|
}),
|
|
229
243
|
new TableCell({
|
|
230
|
-
width: { size:
|
|
231
|
-
|
|
244
|
+
width: { size: COL_WIDTHS[2], type: WidthType.DXA },
|
|
245
|
+
shading: { fill: "1B3A5C", type: ShadingType.CLEAR },
|
|
246
|
+
children: [new Paragraph({ children: [new TextRun({ text: "Growth YoY", bold: true, color: "FFFFFF", size: 22 })] })]
|
|
232
247
|
})
|
|
233
248
|
]
|
|
234
249
|
}),
|
|
235
|
-
// Data
|
|
250
|
+
// Data row — same COL_WIDTHS
|
|
236
251
|
new TableRow({
|
|
237
252
|
children: [
|
|
238
253
|
new TableCell({
|
|
239
|
-
width: { size:
|
|
254
|
+
width: { size: COL_WIDTHS[0], type: WidthType.DXA },
|
|
240
255
|
shading: { fill: "E8F4F8", type: ShadingType.CLEAR },
|
|
241
|
-
children: [
|
|
256
|
+
children: [new Paragraph({ children: [new TextRun({ text: "Enterprise", size: 22 })] })]
|
|
242
257
|
}),
|
|
243
|
-
|
|
258
|
+
new TableCell({
|
|
259
|
+
width: { size: COL_WIDTHS[1], type: WidthType.DXA },
|
|
260
|
+
shading: { fill: "E8F4F8", type: ShadingType.CLEAR },
|
|
261
|
+
children: [new Paragraph({ children: [new TextRun({ text: "$2.1M", size: 22 })] })]
|
|
262
|
+
}),
|
|
263
|
+
new TableCell({
|
|
264
|
+
width: { size: COL_WIDTHS[2], type: WidthType.DXA },
|
|
265
|
+
shading: { fill: "E8F4F8", type: ShadingType.CLEAR },
|
|
266
|
+
children: [new Paragraph({ children: [new TextRun({ text: "34%", size: 22 })] })]
|
|
267
|
+
})
|
|
244
268
|
]
|
|
245
269
|
})
|
|
246
270
|
]
|
|
247
271
|
});
|
|
248
272
|
```
|
|
249
273
|
|
|
250
|
-
|
|
274
|
+
**Common table width breakdowns (all sum to 9360 DXA):**
|
|
275
|
+
- 2 columns: 4680 + 4680
|
|
276
|
+
- 3 columns: 3120 + 3120 + 3120
|
|
277
|
+
- 4 columns: 2340 + 2340 + 2340 + 2340
|
|
278
|
+
- 5 columns: 1872 + 1872 + 1872 + 1872 + 1872
|
|
279
|
+
- Uneven: 2400 + 4560 + 2400 (wider middle column)
|
|
251
280
|
|
|
252
281
|
### Cell Shading (Background Colors)
|
|
253
282
|
|
|
@@ -362,7 +391,8 @@ Or use a more sophisticated approach with field codes (requires XML manipulation
|
|
|
362
391
|
- Create separate Paragraph objects for each paragraph (never use \n)
|
|
363
392
|
- Use List APIs for bullets/numbers (never hardcode • or -)
|
|
364
393
|
- Always include `type` parameter on ImageRun
|
|
365
|
-
- Set
|
|
394
|
+
- Set table width to 100% PERCENTAGE, and all cell widths in DXA summing to 9360
|
|
395
|
+
- Use the same cell widths for each column across ALL rows
|
|
366
396
|
- Use `ShadingType.CLEAR` for cell backgrounds
|
|
367
397
|
- Use `HeadingLevel.HEADING_1`, etc., with `outlineLevel` for TOC
|
|
368
398
|
- Use `children` with TextRun for headings, NOT the `run` property (Pages ignores `run`)
|
|
@@ -372,7 +402,8 @@ Or use a more sophisticated approach with field codes (requires XML manipulation
|
|
|
372
402
|
|
|
373
403
|
**DON'T:**
|
|
374
404
|
- Use newlines (\n) — create separate Paragraphs instead
|
|
375
|
-
-
|
|
405
|
+
- Use WidthType.PERCENTAGE on individual cells (only on the table container)
|
|
406
|
+
- Use different cell widths for the same column in different rows
|
|
376
407
|
- Omit `type` on ImageRun (causes runtime errors)
|
|
377
408
|
- Use unicode bullet characters manually
|
|
378
409
|
- Put PageBreak outside a Paragraph
|
|
@@ -757,7 +788,7 @@ These features render consistently across all apps:
|
|
|
757
788
|
|
|
758
789
|
**Document won't open in Word:** XML malformed. Check for unclosed tags, invalid characters. Run `validate.py` to check structure.
|
|
759
790
|
|
|
760
|
-
**Tables look misaligned:** Missing cell widths
|
|
791
|
+
**Tables look misaligned:** Three common causes: (1) Missing cell widths — ensure every cell has `width: { size: X, type: WidthType.DXA }`. (2) Inconsistent widths across rows — every row must use the same width for each column. (3) Cell widths don't sum to 9360 DXA — recalculate so they add up exactly. Also ensure `fix_tables.py` was run after generation.
|
|
761
792
|
|
|
762
793
|
**Page breaks in wrong place:** PageBreak must be inside Paragraph. `new Paragraph({ children: [new PageBreak()] })`.
|
|
763
794
|
|
|
@@ -3,20 +3,120 @@ Post-process a docx-js generated .docx file to fix tblGrid values for Pages comp
|
|
|
3
3
|
|
|
4
4
|
docx-js generates <w:gridCol w:w="100"/> for all columns regardless of actual cell widths.
|
|
5
5
|
Pages relies on tblGrid for layout, so tables render as 1-char-wide columns.
|
|
6
|
-
This script reads the tcW values from each table's
|
|
6
|
+
This script reads the tcW values from each table's rows (handling gridSpan for merged cells)
|
|
7
|
+
and patches tblGrid to match.
|
|
7
8
|
"""
|
|
8
9
|
import sys
|
|
9
10
|
import zipfile
|
|
10
11
|
import os
|
|
11
|
-
import re
|
|
12
12
|
import shutil
|
|
13
13
|
from lxml import etree
|
|
14
14
|
|
|
15
15
|
WORD_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
|
|
16
16
|
NSMAP = {'w': WORD_NS}
|
|
17
17
|
|
|
18
|
+
|
|
19
|
+
def get_cell_span(tc):
|
|
20
|
+
"""Get the gridSpan value for a cell (defaults to 1 if not set)."""
|
|
21
|
+
tcPr = tc.find('w:tcPr', NSMAP)
|
|
22
|
+
if tcPr is not None:
|
|
23
|
+
gridSpan = tcPr.find('w:gridSpan', NSMAP)
|
|
24
|
+
if gridSpan is not None:
|
|
25
|
+
val = gridSpan.get(f'{{{WORD_NS}}}val')
|
|
26
|
+
if val:
|
|
27
|
+
try:
|
|
28
|
+
return int(val)
|
|
29
|
+
except ValueError:
|
|
30
|
+
pass
|
|
31
|
+
return 1
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_cell_width(tc, default=2340):
|
|
35
|
+
"""Get the DXA width for a cell."""
|
|
36
|
+
tcPr = tc.find('w:tcPr', NSMAP)
|
|
37
|
+
if tcPr is not None:
|
|
38
|
+
tcW = tcPr.find('w:tcW', NSMAP)
|
|
39
|
+
if tcW is not None:
|
|
40
|
+
w_val = tcW.get(f'{{{WORD_NS}}}w')
|
|
41
|
+
w_type = tcW.get(f'{{{WORD_NS}}}type', 'dxa')
|
|
42
|
+
if w_type == 'dxa' and w_val:
|
|
43
|
+
try:
|
|
44
|
+
return int(w_val)
|
|
45
|
+
except ValueError:
|
|
46
|
+
pass
|
|
47
|
+
return default
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def resolve_column_widths(tbl, num_grid_cols):
|
|
51
|
+
"""
|
|
52
|
+
Scan all rows to determine individual column widths.
|
|
53
|
+
Handles gridSpan (merged cells) by finding a row where each column
|
|
54
|
+
appears unmerged, or by splitting merged widths evenly.
|
|
55
|
+
"""
|
|
56
|
+
col_widths = [None] * num_grid_cols
|
|
57
|
+
|
|
58
|
+
rows = tbl.findall('w:tr', NSMAP)
|
|
59
|
+
for tr in rows:
|
|
60
|
+
cells = tr.findall('w:tc', NSMAP)
|
|
61
|
+
col_idx = 0
|
|
62
|
+
for tc in cells:
|
|
63
|
+
span = get_cell_span(tc)
|
|
64
|
+
width = get_cell_width(tc)
|
|
65
|
+
|
|
66
|
+
if span == 1 and col_idx < num_grid_cols:
|
|
67
|
+
if col_widths[col_idx] is None:
|
|
68
|
+
col_widths[col_idx] = width
|
|
69
|
+
elif span > 1:
|
|
70
|
+
# Merged cell -- split width evenly if we haven't resolved these columns yet
|
|
71
|
+
per_col = width // span
|
|
72
|
+
remainder = width - (per_col * span)
|
|
73
|
+
for i in range(span):
|
|
74
|
+
target = col_idx + i
|
|
75
|
+
if target < num_grid_cols and col_widths[target] is None:
|
|
76
|
+
col_widths[target] = per_col + (1 if i < remainder else 0)
|
|
77
|
+
|
|
78
|
+
col_idx += span
|
|
79
|
+
|
|
80
|
+
# Stop early if all columns are resolved
|
|
81
|
+
if all(w is not None for w in col_widths):
|
|
82
|
+
break
|
|
83
|
+
|
|
84
|
+
# Fill any remaining None values with a reasonable default
|
|
85
|
+
for i in range(num_grid_cols):
|
|
86
|
+
if col_widths[i] is None:
|
|
87
|
+
col_widths[i] = 2340
|
|
88
|
+
|
|
89
|
+
return col_widths
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def grid_needs_fix(grid_cols):
|
|
93
|
+
"""Check if the tblGrid looks like a docx-js default that needs patching."""
|
|
94
|
+
if not grid_cols:
|
|
95
|
+
return False
|
|
96
|
+
values = []
|
|
97
|
+
for gc in grid_cols:
|
|
98
|
+
val = gc.get(f'{{{WORD_NS}}}w')
|
|
99
|
+
if val:
|
|
100
|
+
try:
|
|
101
|
+
values.append(int(val))
|
|
102
|
+
except ValueError:
|
|
103
|
+
return True
|
|
104
|
+
else:
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
# Fix if all values are identical small numbers (docx-js default is 100)
|
|
108
|
+
if len(set(values)) == 1 and values[0] <= 200:
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
# Fix if total grid width is unreasonably small (< 1000 DXA for a multi-column table)
|
|
112
|
+
if len(values) > 1 and sum(values) < 1000:
|
|
113
|
+
return True
|
|
114
|
+
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
18
118
|
def fix_tables(xml_content):
|
|
19
|
-
"""Fix tblGrid values to match
|
|
119
|
+
"""Fix tblGrid values to match actual cell widths."""
|
|
20
120
|
root = etree.fromstring(xml_content)
|
|
21
121
|
tables = root.findall('.//w:tbl', NSMAP)
|
|
22
122
|
fixes = 0
|
|
@@ -26,42 +126,21 @@ def fix_tables(xml_content):
|
|
|
26
126
|
if grid is None:
|
|
27
127
|
continue
|
|
28
128
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if first_row is None:
|
|
129
|
+
grid_cols = grid.findall('w:gridCol', NSMAP)
|
|
130
|
+
if not grid_cols:
|
|
32
131
|
continue
|
|
33
132
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if tcPr is not None:
|
|
39
|
-
tcW = tcPr.find('w:tcW', NSMAP)
|
|
40
|
-
if tcW is not None:
|
|
41
|
-
w_val = tcW.get(f'{{{WORD_NS}}}w')
|
|
42
|
-
w_type = tcW.get(f'{{{WORD_NS}}}type', 'dxa')
|
|
43
|
-
if w_type == 'dxa' and w_val:
|
|
44
|
-
try:
|
|
45
|
-
cell_widths.append(int(w_val))
|
|
46
|
-
except ValueError:
|
|
47
|
-
cell_widths.append(2340) # default fallback
|
|
48
|
-
else:
|
|
49
|
-
cell_widths.append(2340)
|
|
50
|
-
else:
|
|
51
|
-
cell_widths.append(2340)
|
|
52
|
-
else:
|
|
53
|
-
cell_widths.append(2340)
|
|
54
|
-
|
|
55
|
-
# Check if grid needs fixing (all gridCol are 100 = docx-js default)
|
|
56
|
-
grid_cols = grid.findall('w:gridCol', NSMAP)
|
|
57
|
-
all_100 = all(gc.get(f'{{{WORD_NS}}}w') == '100' for gc in grid_cols)
|
|
133
|
+
num_grid_cols = len(grid_cols)
|
|
134
|
+
|
|
135
|
+
if grid_needs_fix(grid_cols):
|
|
136
|
+
col_widths = resolve_column_widths(tbl, num_grid_cols)
|
|
58
137
|
|
|
59
|
-
|
|
60
|
-
for gc, width in zip(grid_cols,
|
|
138
|
+
# Update gridCol values
|
|
139
|
+
for gc, width in zip(grid_cols, col_widths):
|
|
61
140
|
gc.set(f'{{{WORD_NS}}}w', str(width))
|
|
62
141
|
fixes += 1
|
|
63
142
|
|
|
64
|
-
#
|
|
143
|
+
# Ensure tblLayout is set to fixed for consistent rendering
|
|
65
144
|
tblPr = tbl.find('w:tblPr', NSMAP)
|
|
66
145
|
if tblPr is not None:
|
|
67
146
|
layout = tblPr.find('w:tblLayout', NSMAP)
|
|
@@ -71,6 +150,7 @@ def fix_tables(xml_content):
|
|
|
71
150
|
|
|
72
151
|
return etree.tostring(root, xml_declaration=True, encoding='UTF-8', standalone=True), fixes
|
|
73
152
|
|
|
153
|
+
|
|
74
154
|
def fix_docx(input_path, output_path=None):
|
|
75
155
|
"""Fix a .docx file's table grids for Pages compatibility."""
|
|
76
156
|
if output_path is None:
|
|
@@ -80,11 +160,9 @@ def fix_docx(input_path, output_path=None):
|
|
|
80
160
|
tmp_dir = tempfile.mkdtemp(prefix='fix_tables_')
|
|
81
161
|
|
|
82
162
|
try:
|
|
83
|
-
# Extract
|
|
84
163
|
with zipfile.ZipFile(input_path, 'r') as z:
|
|
85
164
|
z.extractall(tmp_dir)
|
|
86
165
|
|
|
87
|
-
# Fix document.xml
|
|
88
166
|
doc_path = os.path.join(tmp_dir, 'word', 'document.xml')
|
|
89
167
|
with open(doc_path, 'rb') as f:
|
|
90
168
|
xml_content = f.read()
|
|
@@ -94,7 +172,6 @@ def fix_docx(input_path, output_path=None):
|
|
|
94
172
|
with open(doc_path, 'wb') as f:
|
|
95
173
|
f.write(fixed_xml)
|
|
96
174
|
|
|
97
|
-
# Repack
|
|
98
175
|
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zout:
|
|
99
176
|
for root_dir, dirs, files in os.walk(tmp_dir):
|
|
100
177
|
for file in files:
|
|
@@ -108,9 +185,10 @@ def fix_docx(input_path, output_path=None):
|
|
|
108
185
|
finally:
|
|
109
186
|
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
110
187
|
|
|
188
|
+
|
|
111
189
|
if __name__ == '__main__':
|
|
112
190
|
if len(sys.argv) < 2:
|
|
113
|
-
print("Usage: python
|
|
191
|
+
print("Usage: python fix_tables.py <input.docx> [output.docx]")
|
|
114
192
|
sys.exit(1)
|
|
115
193
|
|
|
116
194
|
input_file = sys.argv[1]
|