@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.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.5.9",
3
- "commit": "afa819caa1ebbbf3880064fe1ff0a3477acf29ba",
4
- "builtAt": "2026-02-27T08:16:50.523Z"
2
+ "version": "0.6.0",
3
+ "commit": "3e86276898dcac0ab6e58fb3fd5732fe712d069a",
4
+ "builtAt": "2026-02-27T09:13:13.678Z"
5
5
  }
@@ -1 +1 @@
1
- 5c0b19b1e764a81d2372636e26464b4ed27af3661042686be3fd49e82f6ea289
1
+ c7c3dae0b04843ef55701aa1424c692bd8fd4a90bdea1187fb53373a6da54271
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heylemon/lemonade",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "AI gateway CLI for Lemon - local AI assistant with integrations",
5
5
  "publishConfig": {
6
6
  "access": "restricted"
@@ -205,49 +205,78 @@ new Paragraph({
205
205
  })
206
206
  ```
207
207
 
208
- ### Tables: Dual Width Rules (CRITICAL)
208
+ ### Tables: Width Rules (CRITICAL)
209
209
 
210
- Tables MUST specify width in two places. Always use `WidthType.DXA`, never PERCENTAGE:
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 width = 100% of content area
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: 2000, type: WidthType.DXA }, // Individual cell width in DXA
223
- children: [ new Paragraph("Header 1") ]
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: 2500, type: WidthType.DXA },
227
- children: [ new Paragraph("Header 2") ]
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: 2000, type: WidthType.DXA },
231
- children: [ new Paragraph("Header 3") ]
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 rows follow
250
+ // Data row — same COL_WIDTHS
236
251
  new TableRow({
237
252
  children: [
238
253
  new TableCell({
239
- width: { size: 2000, type: WidthType.DXA },
254
+ width: { size: COL_WIDTHS[0], type: WidthType.DXA },
240
255
  shading: { fill: "E8F4F8", type: ShadingType.CLEAR },
241
- children: [ new Paragraph("Data 1") ]
256
+ children: [new Paragraph({ children: [new TextRun({ text: "Enterprise", size: 22 })] })]
242
257
  }),
243
- // ... more cells
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
- Cell widths must sum to total table width. 6.5 inches of content (8.5 - 2 margins) = ~9360 DXA.
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 both table width AND individual cell widths in DXA
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
- - Mix WidthType.PERCENTAGE and WidthType.DXA on different table cells
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 or inconsistent width types. Ensure every cell has `width: { size: X, type: WidthType.DXA }`.
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 first row and patches tblGrid to match.
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 first-row cell widths."""
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
- # Get first row's cell widths
30
- first_row = tbl.find('w:tr', NSMAP)
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
- cells = first_row.findall('w:tc', NSMAP)
35
- cell_widths = []
36
- for tc in cells:
37
- tcPr = tc.find('w:tcPr', NSMAP)
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
- if all_100 and len(cell_widths) == len(grid_cols):
60
- for gc, width in zip(grid_cols, cell_widths):
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
- # Also ensure tblLayout is set to fixed for Pages
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 fix_docx_tables.py <input.docx> [output.docx]")
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]