document-cli 1.6.0 → 1.7.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/cli.js CHANGED
@@ -1194,7 +1194,7 @@ function registerSetMetadataCommand(program) {
1194
1194
  }
1195
1195
  //#endregion
1196
1196
  //#region package.json
1197
- var version = "1.6.0";
1197
+ var version = "1.7.0";
1198
1198
  //#endregion
1199
1199
  //#region src/program.ts
1200
1200
  function createProgram() {
@@ -1221,7 +1221,7 @@ function createProgram() {
1221
1221
  //#region src/cli.ts
1222
1222
  async function launchTui(startPath, signal) {
1223
1223
  try {
1224
- const { runTui } = await import("./tui-CqYlr4nN.js");
1224
+ const { runTui } = await import("./tui-BzdX4QQg.js");
1225
1225
  await runTui({
1226
1226
  startPath,
1227
1227
  signal
package/dist/index.cjs CHANGED
@@ -1661,7 +1661,7 @@ function registerSetMetadataCommand(program) {
1661
1661
  }
1662
1662
  //#endregion
1663
1663
  //#region package.json
1664
- var version = "1.6.0";
1664
+ var version = "1.7.0";
1665
1665
  //#endregion
1666
1666
  //#region src/program.ts
1667
1667
  function createProgram() {
package/dist/index.js CHANGED
@@ -1660,7 +1660,7 @@ function registerSetMetadataCommand(program) {
1660
1660
  }
1661
1661
  //#endregion
1662
1662
  //#region package.json
1663
- var version = "1.6.0";
1663
+ var version = "1.7.0";
1664
1664
  //#endregion
1665
1665
  //#region src/program.ts
1666
1666
  function createProgram() {
@@ -230,6 +230,7 @@ function selectionKeyFor(screen) {
230
230
  case "slideDetail":
231
231
  case "notesEditor": return `${screen.kind}:${screen.slideIndex}`;
232
232
  case "shapeEditor": return `shapeEditor:${screen.slideIndex}:${screen.shapeIndex}`;
233
+ case "slideTableDetail": return `slideTableDetail:${screen.slideIndex}:${screen.tableIndex}`;
233
234
  case "spreadsheetGrid":
234
235
  case "printSettingsEditor": return `${screen.kind}:${screen.sheetIndex}`;
235
236
  case "cellDetail": return `cellDetail:${screen.sheetIndex}:${screen.row}:${screen.col}`;
@@ -435,6 +436,38 @@ function mutate(state, doc, apply) {
435
436
  undoStack: pushSnapshot(state.undoStack, snapshot)
436
437
  };
437
438
  }
439
+ function mutateGuarded(state, doc, apply) {
440
+ try {
441
+ return mutate(state, doc, apply);
442
+ } catch (error) {
443
+ return withStatus(state, "warning", error instanceof Error ? error.message : String(error));
444
+ }
445
+ }
446
+ function mergePptxTableCells(table, startRow, startColumn, rowSpan, colSpan) {
447
+ if (!Number.isInteger(rowSpan) || rowSpan < 1 || !Number.isInteger(colSpan) || colSpan < 1) throw new Error(`mergeSlideTableCells: rowSpan and colSpan must be positive integers, got rowSpan=${rowSpan}, colSpan=${colSpan}`);
448
+ const rows = table.rows();
449
+ if (startRow + rowSpan > rows.length) throw new Error(`mergeSlideTableCells: rowSpan ${rowSpan} starting at row ${startRow} exceeds this table's own ${rows.length} rows`);
450
+ const anchorRow = rows[startRow];
451
+ if (anchorRow === void 0) throw new Error(`mergeSlideTableCells: row ${startRow} does not exist in this table`);
452
+ const columnCount = anchorRow.cells().length;
453
+ if (startColumn + colSpan > columnCount) throw new Error(`mergeSlideTableCells: colSpan ${colSpan} starting at column ${startColumn} exceeds this table's own ${columnCount} columns`);
454
+ for (let rowOffset = 0; rowOffset < rowSpan; rowOffset++) {
455
+ const row = rows[startRow + rowOffset];
456
+ if (row === void 0) throw new Error(`mergeSlideTableCells: row ${startRow + rowOffset} does not exist in this table`);
457
+ const cells = row.cells();
458
+ for (let columnOffset = 0; columnOffset < colSpan; columnOffset++) {
459
+ const cell = cells[startColumn + columnOffset];
460
+ if (cell === void 0) throw new Error(`mergeSlideTableCells: column ${startColumn + columnOffset} does not exist in row ${startRow + rowOffset}`);
461
+ if (rowOffset === 0 && columnOffset === 0) {
462
+ cell.colSpan = colSpan;
463
+ cell.rowSpan = rowSpan;
464
+ continue;
465
+ }
466
+ if (columnOffset > 0) cell.horizontalMerge = true;
467
+ if (rowOffset > 0) cell.verticalMerge = true;
468
+ }
469
+ }
470
+ }
438
471
  function mutateMarkdown(state, doc, source) {
439
472
  const snapshot = encodeMarkdownText(doc.source);
440
473
  return {
@@ -685,11 +718,21 @@ function appReducer(state, action) {
685
718
  case "APPEND_TABLE": {
686
719
  const doc = wordprocessingDocument(state);
687
720
  if (doc === void 0) return wrongDocument(state, "a docx or odt document");
688
- return mutate(state, doc, () => {
689
- doc.editor.body.appendTable({
721
+ return mutateGuarded(state, doc, () => {
722
+ const table = doc.editor.body.appendTable({
690
723
  rows: action.rows,
691
724
  columns: action.columns
692
725
  });
726
+ if (action.merge !== void 0) table.mergeCells(action.merge.startRow, action.merge.startColumn, action.merge.rowSpan, action.merge.colSpan);
727
+ });
728
+ }
729
+ case "MERGE_TABLE_CELLS": {
730
+ const doc = wordprocessingDocument(state);
731
+ if (doc === void 0) return wrongDocument(state, "a docx or odt document");
732
+ const table = tableAt(doc, action.tableIndex);
733
+ if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex}`);
734
+ return mutateGuarded(state, doc, () => {
735
+ table.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
693
736
  });
694
737
  }
695
738
  case "SET_TABLE_CELL_TEXT": {
@@ -754,6 +797,22 @@ function appReducer(state, action) {
754
797
  });
755
798
  });
756
799
  }
800
+ case "MERGE_SLIDE_TABLE_CELLS": {
801
+ const doc = presentationDocument(state);
802
+ if (doc === void 0) return wrongDocument(state, "a pptx or odp document");
803
+ if (doc.format === "odp") {
804
+ const entry = doc.editor.slides()[action.slideIndex]?.tables()[action.tableIndex];
805
+ if (entry === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex} on slide ${action.slideIndex}`);
806
+ return mutateGuarded(state, doc, () => {
807
+ entry.table.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
808
+ });
809
+ }
810
+ const table = doc.editor.slides()[action.slideIndex]?.tables()[action.tableIndex];
811
+ if (table === void 0) return withStatus(state, "warning", `There is no table at index ${action.tableIndex} on slide ${action.slideIndex}`);
812
+ return mutateGuarded(state, doc, () => {
813
+ mergePptxTableCells(table, action.startRow, action.startColumn, action.rowSpan, action.colSpan);
814
+ });
815
+ }
757
816
  case "ADD_TEXTBOX": {
758
817
  const doc = shapeHostDocument(state);
759
818
  if (doc === void 0) return wrongDocument(state, "a pptx, odp or odg document");
@@ -826,6 +885,15 @@ function appReducer(state, action) {
826
885
  case "SET_CELL_VALUE": return withSheet(state, action.sheetIndex, (sheet) => {
827
886
  sheet.cell(action.row, action.column).value = action.value;
828
887
  });
888
+ case "MERGE_CELLS": {
889
+ const doc = spreadsheetDocument(state);
890
+ if (doc === void 0) return wrongDocument(state, "an ods document");
891
+ const sheet = sheetAt(doc, action.sheetIndex);
892
+ if (sheet === void 0) return withStatus(state, "warning", `There is no sheet at index ${action.sheetIndex}`);
893
+ return mutateGuarded(state, doc, () => {
894
+ sheet.mergeCells(action.startRow, action.startColumn, action.rowSpan, action.colSpan);
895
+ });
896
+ }
829
897
  case "SET_SHEET_PRINT_SETTINGS": return withSheet(state, action.sheetIndex, (sheet) => {
830
898
  sheet.printSettings = action.printSettings;
831
899
  });
@@ -1606,6 +1674,14 @@ function truncatePreview(text, maxLength) {
1606
1674
  if (singleLine.length === 0) return "(empty)";
1607
1675
  return singleLine.length > maxLength ? `${singleLine.slice(0, Math.max(0, maxLength - 1))}…` : singleLine;
1608
1676
  }
1677
+ function parsePositiveIntField(raw, fallback) {
1678
+ const parsed = Number.parseInt(raw, 10);
1679
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
1680
+ }
1681
+ function parseNonNegativeIntField(raw, fallback) {
1682
+ const parsed = Number.parseInt(raw, 10);
1683
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
1684
+ }
1609
1685
  //#endregion
1610
1686
  //#region src/tui/screens/shared/paragraph-family.tsx
1611
1687
  function paragraphFamilyDocument(openDocument) {
@@ -1669,6 +1745,8 @@ function tableSummary(table) {
1669
1745
  function listSummary(list, index) {
1670
1746
  return `List ${index} (${list.itemCount} item${list.itemCount === 1 ? "" : "s"})`;
1671
1747
  }
1748
+ const DEFAULT_TABLE_ROWS$1 = 2;
1749
+ const DEFAULT_TABLE_COLUMNS$1 = 2;
1672
1750
  function ParagraphFamilyBodyList(props) {
1673
1751
  const { adapter } = props;
1674
1752
  const state = useAppState();
@@ -1711,9 +1789,88 @@ function ParagraphFamilyBodyList(props) {
1711
1789
  if (row.kind !== "header") acc.push(rowIndex);
1712
1790
  return acc;
1713
1791
  }, []);
1792
+ const [tableWizard, setTableWizard] = useState("closed");
1793
+ const [wizardDraft, setWizardDraft] = useState("");
1794
+ const [wizardRows, setWizardRows] = useState(DEFAULT_TABLE_ROWS$1);
1795
+ const [wizardColumns, setWizardColumns] = useState(DEFAULT_TABLE_COLUMNS$1);
1796
+ const [wizardStartRow, setWizardStartRow] = useState(0);
1797
+ const [wizardStartColumn, setWizardStartColumn] = useState(0);
1798
+ const [wizardRowSpan, setWizardRowSpan] = useState(1);
1799
+ const wizardOpen = tableWizard !== "closed";
1800
+ const closeWizard = () => {
1801
+ setTableWizard("closed");
1802
+ };
1803
+ const commitAppendTable = (merge) => {
1804
+ const newIndex = tables.length;
1805
+ dispatch({
1806
+ type: "APPEND_TABLE",
1807
+ rows: wizardRows,
1808
+ columns: wizardColumns,
1809
+ merge
1810
+ });
1811
+ closeWizard();
1812
+ dispatch({
1813
+ type: "PUSH_SCREEN",
1814
+ screen: {
1815
+ kind: "tableView",
1816
+ blockIndex: newIndex
1817
+ }
1818
+ });
1819
+ };
1820
+ useInput((input) => {
1821
+ if (input === "T") {
1822
+ setWizardDraft(String(DEFAULT_TABLE_ROWS$1));
1823
+ setTableWizard("rows");
1824
+ }
1825
+ }, { isActive: !anyOverlayOpen(state) && !wizardOpen });
1826
+ useInput((input, key) => {
1827
+ if (key.escape) {
1828
+ closeWizard();
1829
+ return;
1830
+ }
1831
+ if (input === "y" || input === "Y") {
1832
+ setWizardDraft("0");
1833
+ setTableWizard("mergeStartRow");
1834
+ return;
1835
+ }
1836
+ if (input === "n" || input === "N" || key.return) commitAppendTable(void 0);
1837
+ }, { isActive: !anyOverlayOpen(state) && tableWizard === "mergePrompt" });
1838
+ const submitWizardRows = (raw) => {
1839
+ setWizardRows(parsePositiveIntField(raw, DEFAULT_TABLE_ROWS$1));
1840
+ setWizardDraft(String(DEFAULT_TABLE_COLUMNS$1));
1841
+ setTableWizard("columns");
1842
+ };
1843
+ const submitWizardColumns = (raw) => {
1844
+ setWizardColumns(parsePositiveIntField(raw, DEFAULT_TABLE_COLUMNS$1));
1845
+ setTableWizard("mergePrompt");
1846
+ };
1847
+ const submitWizardMergeStartRow = (raw) => {
1848
+ setWizardStartRow(Math.min(parseNonNegativeIntField(raw, 0), Math.max(0, wizardRows - 1)));
1849
+ setWizardDraft("0");
1850
+ setTableWizard("mergeStartColumn");
1851
+ };
1852
+ const submitWizardMergeStartColumn = (raw) => {
1853
+ setWizardStartColumn(Math.min(parseNonNegativeIntField(raw, 0), Math.max(0, wizardColumns - 1)));
1854
+ setWizardDraft("1");
1855
+ setTableWizard("mergeRowSpan");
1856
+ };
1857
+ const submitWizardMergeRowSpan = (raw) => {
1858
+ setWizardRowSpan(Math.min(parsePositiveIntField(raw, 1), Math.max(1, wizardRows - wizardStartRow)));
1859
+ setWizardDraft("1");
1860
+ setTableWizard("mergeColSpan");
1861
+ };
1862
+ const submitWizardMergeColSpan = (raw) => {
1863
+ const colSpan = Math.min(parsePositiveIntField(raw, 1), Math.max(1, wizardColumns - wizardStartColumn));
1864
+ commitAppendTable({
1865
+ startRow: wizardStartRow,
1866
+ startColumn: wizardStartColumn,
1867
+ rowSpan: wizardRowSpan,
1868
+ colSpan
1869
+ });
1870
+ };
1714
1871
  const { selectedIndex } = usePersistedSelection(selectionKeyFor({ kind: "bodyList" }), {
1715
1872
  itemCount: selectableRowIndices.length,
1716
- isActive: !anyOverlayOpen(state),
1873
+ isActive: !anyOverlayOpen(state) && !wizardOpen,
1717
1874
  onBack: () => {
1718
1875
  dispatch({ type: "POP_SCREEN" });
1719
1876
  },
@@ -1800,9 +1957,89 @@ function ParagraphFamilyBodyList(props) {
1800
1957
  });
1801
1958
  }
1802
1959
  }),
1960
+ tableWizard === "rows" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1961
+ color: "cyan",
1962
+ children: "Rows: "
1963
+ }), /* @__PURE__ */ jsx(TextField, {
1964
+ value: wizardDraft,
1965
+ isFocused: true,
1966
+ onChange: setWizardDraft,
1967
+ onSubmit: submitWizardRows,
1968
+ onCancel: closeWizard
1969
+ })] }) : void 0,
1970
+ tableWizard === "columns" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsx(Text, {
1971
+ color: "cyan",
1972
+ children: "Columns: "
1973
+ }), /* @__PURE__ */ jsx(TextField, {
1974
+ value: wizardDraft,
1975
+ isFocused: true,
1976
+ onChange: setWizardDraft,
1977
+ onSubmit: submitWizardColumns,
1978
+ onCancel: closeWizard
1979
+ })] }) : void 0,
1980
+ tableWizard === "mergePrompt" ? /* @__PURE__ */ jsx(Text, {
1981
+ color: "cyan",
1982
+ children: "Merge cells now? y/N"
1983
+ }) : void 0,
1984
+ tableWizard === "mergeStartRow" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
1985
+ color: "cyan",
1986
+ children: [
1987
+ "Merge start row (0-",
1988
+ Math.max(0, wizardRows - 1),
1989
+ "): "
1990
+ ]
1991
+ }), /* @__PURE__ */ jsx(TextField, {
1992
+ value: wizardDraft,
1993
+ isFocused: true,
1994
+ onChange: setWizardDraft,
1995
+ onSubmit: submitWizardMergeStartRow,
1996
+ onCancel: closeWizard
1997
+ })] }) : void 0,
1998
+ tableWizard === "mergeStartColumn" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
1999
+ color: "cyan",
2000
+ children: [
2001
+ "Merge start column (0-",
2002
+ Math.max(0, wizardColumns - 1),
2003
+ "): "
2004
+ ]
2005
+ }), /* @__PURE__ */ jsx(TextField, {
2006
+ value: wizardDraft,
2007
+ isFocused: true,
2008
+ onChange: setWizardDraft,
2009
+ onSubmit: submitWizardMergeStartColumn,
2010
+ onCancel: closeWizard
2011
+ })] }) : void 0,
2012
+ tableWizard === "mergeRowSpan" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2013
+ color: "cyan",
2014
+ children: [
2015
+ "Merge row span (1-",
2016
+ Math.max(1, wizardRows - wizardStartRow),
2017
+ "): "
2018
+ ]
2019
+ }), /* @__PURE__ */ jsx(TextField, {
2020
+ value: wizardDraft,
2021
+ isFocused: true,
2022
+ onChange: setWizardDraft,
2023
+ onSubmit: submitWizardMergeRowSpan,
2024
+ onCancel: closeWizard
2025
+ })] }) : void 0,
2026
+ tableWizard === "mergeColSpan" ? /* @__PURE__ */ jsxs(Box, { children: [/* @__PURE__ */ jsxs(Text, {
2027
+ color: "cyan",
2028
+ children: [
2029
+ "Merge column span (1-",
2030
+ Math.max(1, wizardColumns - wizardStartColumn),
2031
+ "): "
2032
+ ]
2033
+ }), /* @__PURE__ */ jsx(TextField, {
2034
+ value: wizardDraft,
2035
+ isFocused: true,
2036
+ onChange: setWizardDraft,
2037
+ onSubmit: submitWizardMergeColSpan,
2038
+ onCancel: closeWizard
2039
+ })] }) : void 0,
1803
2040
  /* @__PURE__ */ jsx(Text, {
1804
2041
  dimColor: true,
1805
- children: "Enter to open, a to append a paragraph, Esc back"
2042
+ children: "Enter to open, a to append a paragraph, T to append a table, Esc back"
1806
2043
  })
1807
2044
  ]
1808
2045
  });
@@ -2079,7 +2316,7 @@ function RunEditorScreen() {
2079
2316
  }
2080
2317
  //#endregion
2081
2318
  //#region src/tui/screens/editors/docx/table-view.tsx
2082
- const CELL_WIDTH$1 = 16;
2319
+ const CELL_WIDTH$2 = 16;
2083
2320
  function TableViewScreen() {
2084
2321
  const state = useAppState();
2085
2322
  const dispatch = useAppDispatch();
@@ -2087,6 +2324,7 @@ function TableViewScreen() {
2087
2324
  row: 0,
2088
2325
  column: 0
2089
2326
  });
2327
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
2090
2328
  const screen = currentScreen(state);
2091
2329
  const doc = paragraphFamilyDocument(state.openDocument);
2092
2330
  const table = screen.kind === "tableView" && doc !== void 0 ? liveTableAt(doc, screen.blockIndex) : void 0;
@@ -2095,6 +2333,22 @@ function TableViewScreen() {
2095
2333
  const columnCount = rows[0]?.cells().length ?? 0;
2096
2334
  const clampedRow = rowCount === 0 ? 0 : Math.min(cursor.row, rowCount - 1);
2097
2335
  const clampedColumn = columnCount === 0 ? 0 : Math.min(cursor.column, columnCount - 1);
2336
+ const commitMerge = (anchor) => {
2337
+ if (screen.kind !== "tableView") return;
2338
+ const startRow = Math.min(anchor.row, clampedRow);
2339
+ const startColumn = Math.min(anchor.column, clampedColumn);
2340
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
2341
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
2342
+ dispatch({
2343
+ type: "MERGE_TABLE_CELLS",
2344
+ tableIndex: screen.blockIndex,
2345
+ startRow,
2346
+ startColumn,
2347
+ rowSpan,
2348
+ colSpan
2349
+ });
2350
+ setMergeAnchor(void 0);
2351
+ };
2098
2352
  useInput((input, key) => {
2099
2353
  if (table === void 0 || screen.kind !== "tableView") return;
2100
2354
  if (key.upArrow || input === "k") {
@@ -2126,18 +2380,36 @@ function TableViewScreen() {
2126
2380
  return;
2127
2381
  }
2128
2382
  if (key.escape) {
2383
+ if (mergeAnchor !== void 0) {
2384
+ setMergeAnchor(void 0);
2385
+ return;
2386
+ }
2129
2387
  dispatch({ type: "POP_SCREEN" });
2130
2388
  return;
2131
2389
  }
2132
- if (key.return && rowCount > 0 && columnCount > 0) dispatch({
2133
- type: "PUSH_SCREEN",
2134
- screen: {
2135
- kind: "tableCellDetail",
2136
- blockIndex: screen.blockIndex,
2390
+ if (input === "m" && rowCount > 0 && columnCount > 0) {
2391
+ if (mergeAnchor === void 0) setMergeAnchor({
2137
2392
  row: clampedRow,
2138
- col: clampedColumn
2393
+ column: clampedColumn
2394
+ });
2395
+ else commitMerge(mergeAnchor);
2396
+ return;
2397
+ }
2398
+ if (key.return && rowCount > 0 && columnCount > 0) {
2399
+ if (mergeAnchor !== void 0) {
2400
+ commitMerge(mergeAnchor);
2401
+ return;
2139
2402
  }
2140
- });
2403
+ dispatch({
2404
+ type: "PUSH_SCREEN",
2405
+ screen: {
2406
+ kind: "tableCellDetail",
2407
+ blockIndex: screen.blockIndex,
2408
+ row: clampedRow,
2409
+ col: clampedColumn
2410
+ }
2411
+ });
2412
+ }
2141
2413
  }, { isActive: !anyOverlayOpen(state) });
2142
2414
  if (screen.kind !== "tableView") return /* @__PURE__ */ jsx(Text, {
2143
2415
  color: "red",
@@ -2175,10 +2447,11 @@ function TableViewScreen() {
2175
2447
  children: "This table has no rows."
2176
2448
  }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsx(Box, { children: row.cells().map((cell, columnIndex) => {
2177
2449
  const isSelected = rowIndex === clampedRow && columnIndex === clampedColumn;
2450
+ const isAnchor = rowIndex === mergeAnchor?.row && columnIndex === mergeAnchor?.column;
2178
2451
  return /* @__PURE__ */ jsx(Box, {
2179
- width: CELL_WIDTH$1,
2452
+ width: CELL_WIDTH$2,
2180
2453
  borderStyle: "single",
2181
- borderColor: isSelected ? "cyan" : "gray",
2454
+ borderColor: isSelected ? "cyan" : isAnchor ? "yellow" : "gray",
2182
2455
  children: /* @__PURE__ */ jsx(Text, {
2183
2456
  color: isSelected ? "cyan" : void 0,
2184
2457
  inverse: isSelected,
@@ -2188,7 +2461,7 @@ function TableViewScreen() {
2188
2461
  }) }, rowIndex)),
2189
2462
  /* @__PURE__ */ jsx(Text, {
2190
2463
  dimColor: true,
2191
- children: "Arrows/hjkl move, Enter to edit a cell, Esc back"
2464
+ children: mergeAnchor === void 0 ? "Arrows/hjkl move, Enter to edit a cell, m to anchor a merge, Esc back" : "Arrows/hjkl to the opposite corner, m/Enter to merge, Esc to cancel"
2192
2465
  })
2193
2466
  ]
2194
2467
  });
@@ -4367,6 +4640,30 @@ function ShapeEditorScreen(props) {
4367
4640
  });
4368
4641
  }
4369
4642
  //#endregion
4643
+ //#region src/tui/screens/shared/slide-table.ts
4644
+ function resolveSlideTable(doc, slideIndex, tableIndex) {
4645
+ const content = doc.format === "odp" ? readOdpContent(doc.editor.toPackage()) : readPptxContent(doc.editor.toPackage());
4646
+ if (content.kind !== "presentation") throw new Error("readPptxContent/readOdpContent always resolve a presentation package to the presentation ContentDocument variant.");
4647
+ const slide = content.slides[slideIndex];
4648
+ if (slide === void 0) return;
4649
+ return slide.shapes.flatMap((shape) => shape.blocks.filter((block) => block.kind === "table"))[tableIndex];
4650
+ }
4651
+ function slideTableCellText(cell) {
4652
+ return cell.blocks.filter((block) => block.kind === "paragraph").map((paragraph) => paragraph.runs.map((run) => run.text).join("")).join("\n");
4653
+ }
4654
+ function summarizeGridTable(table, index) {
4655
+ const rows = table.rows();
4656
+ return {
4657
+ index,
4658
+ rowCount: rows.length,
4659
+ columnCount: rows[0]?.cells().length ?? 0
4660
+ };
4661
+ }
4662
+ function summarizeSlideTables(doc, slideIndex) {
4663
+ if (doc.format === "odp") return (doc.editor.slides()[slideIndex]?.tables() ?? []).map((entry, index) => summarizeGridTable(entry.table, index));
4664
+ return (doc.editor.slides()[slideIndex]?.tables() ?? []).map((table, index) => summarizeGridTable(table, index));
4665
+ }
4666
+ //#endregion
4370
4667
  //#region src/tui/screens/editors/pptx/slide-detail.tsx
4371
4668
  const IMAGE_EXTENSION_TO_FORMAT = {
4372
4669
  png: "png",
@@ -4375,10 +4672,6 @@ const IMAGE_EXTENSION_TO_FORMAT = {
4375
4672
  };
4376
4673
  const DEFAULT_TABLE_ROWS = 2;
4377
4674
  const DEFAULT_TABLE_COLUMNS = 2;
4378
- function parsePositiveIntField(raw, fallback) {
4379
- const parsed = Number.parseInt(raw, 10);
4380
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
4381
- }
4382
4675
  function imageFormatFromPath(path) {
4383
4676
  const dotIndex = path.lastIndexOf(".");
4384
4677
  if (dotIndex < 0) return;
@@ -4399,34 +4692,63 @@ function SlideDetailScreen(props) {
4399
4692
  const doc = assertPresentationDocument(state.openDocument);
4400
4693
  const { slideIndex } = props.screen;
4401
4694
  const slide = doc.editor.slides()[slideIndex];
4402
- const rows = (slide === void 0 ? [] : slide.shapes()).map((shape, index) => ({
4695
+ const shapes = slide === void 0 ? [] : slide.shapes();
4696
+ const tableSummaries = slide === void 0 ? [] : summarizeSlideTables(doc, slideIndex);
4697
+ const rows = [...shapes.map((shape, index) => ({
4698
+ kind: "shape",
4403
4699
  index,
4404
4700
  text: shape.text,
4405
4701
  frame: shape.frame
4406
- }));
4702
+ })), ...tableSummaries.length > 0 ? [{
4703
+ kind: "tablesHeader",
4704
+ count: tableSummaries.length
4705
+ }, ...tableSummaries.map((summary) => ({
4706
+ kind: "table",
4707
+ index: summary.index,
4708
+ rowCount: summary.rowCount,
4709
+ columnCount: summary.columnCount
4710
+ }))] : []];
4711
+ const selectableRowIndices = rows.reduce((acc, row, index) => {
4712
+ if (row.kind !== "tablesHeader") acc.push(index);
4713
+ return acc;
4714
+ }, []);
4407
4715
  const [addMode, setAddMode] = useState("closed");
4408
4716
  const [draft, setDraft] = useState("");
4409
4717
  const [imageError, setImageError] = useState(void 0);
4410
4718
  const [tableRows, setTableRows] = useState(DEFAULT_TABLE_ROWS);
4411
4719
  const formIsOpen = addMode !== "closed";
4412
4720
  const { selectedIndex } = useNavigationInput({
4413
- itemCount: rows.length,
4721
+ itemCount: selectableRowIndices.length,
4414
4722
  isActive: !overlayOpen && !formIsOpen,
4415
4723
  onBack: () => {
4416
4724
  dispatch({ type: "POP_SCREEN" });
4417
4725
  },
4418
4726
  onSelect: (index) => {
4419
- dispatch({
4420
- type: "SET_SELECTION",
4421
- key: selectionKeyFor(props.screen),
4422
- index
4423
- });
4424
- dispatch({
4727
+ const rowIndex = selectableRowIndices[index];
4728
+ const row = rowIndex === void 0 ? void 0 : rows[rowIndex];
4729
+ if (row === void 0) return;
4730
+ if (row.kind === "shape") {
4731
+ dispatch({
4732
+ type: "SET_SELECTION",
4733
+ key: selectionKeyFor(props.screen),
4734
+ index
4735
+ });
4736
+ dispatch({
4737
+ type: "PUSH_SCREEN",
4738
+ screen: {
4739
+ kind: "shapeEditor",
4740
+ slideIndex,
4741
+ shapeIndex: row.index
4742
+ }
4743
+ });
4744
+ return;
4745
+ }
4746
+ if (row.kind === "table") dispatch({
4425
4747
  type: "PUSH_SCREEN",
4426
4748
  screen: {
4427
- kind: "shapeEditor",
4749
+ kind: "slideTableDetail",
4428
4750
  slideIndex,
4429
- shapeIndex: index
4751
+ tableIndex: row.index
4430
4752
  }
4431
4753
  });
4432
4754
  },
@@ -4434,6 +4756,7 @@ function SlideDetailScreen(props) {
4434
4756
  setAddMode("chooseKind");
4435
4757
  }
4436
4758
  });
4759
+ const listSelectedIndex = selectableRowIndices[selectedIndex] ?? -1;
4437
4760
  useInput((input, key) => {
4438
4761
  if (key.escape) {
4439
4762
  setAddMode("closed");
@@ -4516,9 +4839,9 @@ function SlideDetailScreen(props) {
4516
4839
  "Slide ",
4517
4840
  slideIndex + 1,
4518
4841
  " -- ",
4519
- rows.length,
4842
+ shapes.length,
4520
4843
  " shape",
4521
- rows.length === 1 ? "" : "s"
4844
+ shapes.length === 1 ? "" : "s"
4522
4845
  ]
4523
4846
  }),
4524
4847
  slide === void 0 ? /* @__PURE__ */ jsx(Text, {
@@ -4526,20 +4849,45 @@ function SlideDetailScreen(props) {
4526
4849
  children: "This slide no longer exists -- press Esc to go back"
4527
4850
  }) : /* @__PURE__ */ jsx(ListView, {
4528
4851
  items: rows,
4529
- selectedIndex,
4852
+ selectedIndex: listSelectedIndex,
4530
4853
  emptyMessage: "No shapes yet -- press 'a' to add one",
4531
- renderItem: (row, isSelected) => /* @__PURE__ */ jsxs(Text, {
4532
- color: isSelected ? "cyan" : void 0,
4533
- inverse: isSelected,
4534
- children: [
4535
- row.index + 1,
4536
- ". ",
4537
- describeSlideFamilyShape({
4538
- text: row.text,
4539
- frame: row.frame
4540
- })
4541
- ]
4542
- })
4854
+ renderItem: (row, isSelected) => {
4855
+ if (row.kind === "tablesHeader") return /* @__PURE__ */ jsxs(Text, {
4856
+ bold: true,
4857
+ dimColor: true,
4858
+ children: [
4859
+ "Tables (",
4860
+ row.count,
4861
+ ")"
4862
+ ]
4863
+ });
4864
+ if (row.kind === "table") return /* @__PURE__ */ jsxs(Text, {
4865
+ color: isSelected ? "cyan" : void 0,
4866
+ inverse: isSelected,
4867
+ children: [
4868
+ " ",
4869
+ "Table ",
4870
+ row.index + 1,
4871
+ " (",
4872
+ row.rowCount,
4873
+ "x",
4874
+ row.columnCount,
4875
+ ")"
4876
+ ]
4877
+ });
4878
+ return /* @__PURE__ */ jsxs(Text, {
4879
+ color: isSelected ? "cyan" : void 0,
4880
+ inverse: isSelected,
4881
+ children: [
4882
+ row.index + 1,
4883
+ ". ",
4884
+ describeSlideFamilyShape({
4885
+ text: row.text,
4886
+ frame: row.frame
4887
+ })
4888
+ ]
4889
+ });
4890
+ }
4543
4891
  }),
4544
4892
  addMode === "chooseKind" ? /* @__PURE__ */ jsx(Text, {
4545
4893
  color: "cyan",
@@ -4607,6 +4955,146 @@ function SlideDetailScreen(props) {
4607
4955
  });
4608
4956
  }
4609
4957
  //#endregion
4958
+ //#region src/tui/screens/editors/pptx/slide-table-detail.tsx
4959
+ const CELL_WIDTH$1 = 16;
4960
+ function SlideTableDetailScreen(props) {
4961
+ const state = useAppState();
4962
+ const dispatch = useAppDispatch();
4963
+ const doc = assertPresentationDocument(state.openDocument);
4964
+ const { slideIndex, tableIndex } = props.screen;
4965
+ const [cursor, setCursor] = useState({
4966
+ row: 0,
4967
+ column: 0
4968
+ });
4969
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
4970
+ const table = resolveSlideTable(doc, slideIndex, tableIndex);
4971
+ const rows = table?.rows ?? [];
4972
+ const rowCount = rows.length;
4973
+ const columnCount = rows[0]?.cells.length ?? 0;
4974
+ const clampedRow = rowCount === 0 ? 0 : Math.min(cursor.row, rowCount - 1);
4975
+ const clampedColumn = columnCount === 0 ? 0 : Math.min(cursor.column, columnCount - 1);
4976
+ const commitMerge = (anchor) => {
4977
+ const startRow = Math.min(anchor.row, clampedRow);
4978
+ const startColumn = Math.min(anchor.column, clampedColumn);
4979
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
4980
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
4981
+ dispatch({
4982
+ type: "MERGE_SLIDE_TABLE_CELLS",
4983
+ slideIndex,
4984
+ tableIndex,
4985
+ startRow,
4986
+ startColumn,
4987
+ rowSpan,
4988
+ colSpan
4989
+ });
4990
+ setMergeAnchor(void 0);
4991
+ };
4992
+ useInput((input, key) => {
4993
+ if (table === void 0) return;
4994
+ if (key.upArrow || input === "k") {
4995
+ setCursor({
4996
+ row: Math.max(0, clampedRow - 1),
4997
+ column: clampedColumn
4998
+ });
4999
+ return;
5000
+ }
5001
+ if (key.downArrow || input === "j") {
5002
+ setCursor({
5003
+ row: rowCount === 0 ? 0 : Math.min(rowCount - 1, clampedRow + 1),
5004
+ column: clampedColumn
5005
+ });
5006
+ return;
5007
+ }
5008
+ if (key.leftArrow || input === "h") {
5009
+ setCursor({
5010
+ row: clampedRow,
5011
+ column: Math.max(0, clampedColumn - 1)
5012
+ });
5013
+ return;
5014
+ }
5015
+ if (key.rightArrow || input === "l") {
5016
+ setCursor({
5017
+ row: clampedRow,
5018
+ column: columnCount === 0 ? 0 : Math.min(columnCount - 1, clampedColumn + 1)
5019
+ });
5020
+ return;
5021
+ }
5022
+ if (key.escape) {
5023
+ if (mergeAnchor !== void 0) {
5024
+ setMergeAnchor(void 0);
5025
+ return;
5026
+ }
5027
+ dispatch({ type: "POP_SCREEN" });
5028
+ return;
5029
+ }
5030
+ if (input === "m" || key.return) {
5031
+ if (mergeAnchor === void 0) {
5032
+ setMergeAnchor({
5033
+ row: clampedRow,
5034
+ column: clampedColumn
5035
+ });
5036
+ return;
5037
+ }
5038
+ commitMerge(mergeAnchor);
5039
+ }
5040
+ }, { isActive: !anyOverlayOpen(state) });
5041
+ if (table === void 0) return /* @__PURE__ */ jsxs(Box, {
5042
+ flexDirection: "column",
5043
+ children: [/* @__PURE__ */ jsxs(Text, {
5044
+ bold: true,
5045
+ children: [
5046
+ "Slide ",
5047
+ slideIndex + 1,
5048
+ ", table ",
5049
+ tableIndex + 1
5050
+ ]
5051
+ }), /* @__PURE__ */ jsx(Text, {
5052
+ color: "yellow",
5053
+ children: "This table no longer exists -- press Esc to go back"
5054
+ })]
5055
+ });
5056
+ return /* @__PURE__ */ jsxs(Box, {
5057
+ flexDirection: "column",
5058
+ children: [
5059
+ /* @__PURE__ */ jsxs(Text, {
5060
+ bold: true,
5061
+ children: [
5062
+ "Slide ",
5063
+ slideIndex + 1,
5064
+ ", table ",
5065
+ tableIndex + 1,
5066
+ " (",
5067
+ rowCount,
5068
+ "x",
5069
+ columnCount,
5070
+ ")"
5071
+ ]
5072
+ }),
5073
+ rows.length === 0 ? /* @__PURE__ */ jsx(Text, {
5074
+ dimColor: true,
5075
+ children: "This table has no rows."
5076
+ }) : rows.map((row, rowIndex) => /* @__PURE__ */ jsx(Box, { children: row.cells.map((cell, columnIndex) => {
5077
+ const isCursor = rowIndex === clampedRow && columnIndex === clampedColumn;
5078
+ const isAnchor = rowIndex === mergeAnchor?.row && columnIndex === mergeAnchor?.column;
5079
+ return /* @__PURE__ */ jsx(Box, {
5080
+ width: CELL_WIDTH$1,
5081
+ borderStyle: "single",
5082
+ borderColor: isCursor ? "cyan" : isAnchor ? "yellow" : "gray",
5083
+ children: /* @__PURE__ */ jsx(Text, {
5084
+ color: isCursor ? "cyan" : void 0,
5085
+ inverse: isCursor,
5086
+ children: truncatePreview(slideTableCellText(cell), 14)
5087
+ })
5088
+ }, columnIndex);
5089
+ }) }, rowIndex)),
5090
+ /* @__PURE__ */ jsx(Text, {
5091
+ dimColor: true,
5092
+ children: mergeAnchor === void 0 ? "Arrows/hjkl move, m to anchor a merge, Esc back" : "Arrows/hjkl to the opposite corner, m/Enter to merge, Esc to cancel"
5093
+ })
5094
+ ]
5095
+ });
5096
+ }
5097
+ //#endregion
4610
5098
  //#region src/tui/screens/editors/pptx/index.tsx
4611
5099
  function PptxSlideListScreen() {
4612
5100
  const doc = useAppState().openDocument;
@@ -5225,7 +5713,8 @@ const RESERVED_LETTERS = /* @__PURE__ */ new Set([
5225
5713
  "k",
5226
5714
  "l",
5227
5715
  "p",
5228
- "t"
5716
+ "t",
5717
+ "m"
5229
5718
  ]);
5230
5719
  function windowStart(cursor, total, viewport) {
5231
5720
  const maxStart = Math.max(0, total - viewport);
@@ -5248,6 +5737,7 @@ function OdsSpreadsheetGridScreen() {
5248
5737
  const [cursorColumn, setCursorColumn] = useState(0);
5249
5738
  const [viewMode, setViewMode] = useState("grid");
5250
5739
  const [editSession, setEditSession] = useState(void 0);
5740
+ const [mergeAnchor, setMergeAnchor] = useState(void 0);
5251
5741
  const { columns: terminalColumns, rows: terminalRows } = useWindowSize();
5252
5742
  const sheet = resolveSheet(doc.editor, sheetIndex);
5253
5743
  const { rowCount, columnCount } = sheetExtent(sheet);
@@ -5266,6 +5756,21 @@ function OdsSpreadsheetGridScreen() {
5266
5756
  seedKind
5267
5757
  });
5268
5758
  }
5759
+ function commitMerge(anchor) {
5760
+ const startRow = Math.min(anchor.row, clampedRow);
5761
+ const startColumn = Math.min(anchor.column, clampedColumn);
5762
+ const rowSpan = Math.abs(clampedRow - anchor.row) + 1;
5763
+ const colSpan = Math.abs(clampedColumn - anchor.column) + 1;
5764
+ dispatch({
5765
+ type: "MERGE_CELLS",
5766
+ sheetIndex,
5767
+ startRow,
5768
+ startColumn,
5769
+ rowSpan,
5770
+ colSpan
5771
+ });
5772
+ setMergeAnchor(void 0);
5773
+ }
5269
5774
  useInput((input) => {
5270
5775
  if (input === "t") {
5271
5776
  setViewMode((mode) => mode === "grid" ? "compact" : "grid");
@@ -5313,10 +5818,26 @@ function OdsSpreadsheetGridScreen() {
5313
5818
  return;
5314
5819
  }
5315
5820
  if (key.escape) {
5821
+ if (mergeAnchor !== void 0) {
5822
+ setMergeAnchor(void 0);
5823
+ return;
5824
+ }
5316
5825
  dispatch({ type: "POP_SCREEN" });
5317
5826
  return;
5318
5827
  }
5828
+ if (input === "m") {
5829
+ if (mergeAnchor === void 0) setMergeAnchor({
5830
+ row: clampedRow,
5831
+ column: clampedColumn
5832
+ });
5833
+ else commitMerge(mergeAnchor);
5834
+ return;
5835
+ }
5319
5836
  if (key.return) {
5837
+ if (mergeAnchor !== void 0) {
5838
+ commitMerge(mergeAnchor);
5839
+ return;
5840
+ }
5320
5841
  const cell = cells.get(cellKey(clampedRow, clampedColumn));
5321
5842
  const seedKind = cell === void 0 || cell.value.kind === "empty" ? "string" : cell.value.kind;
5322
5843
  beginEdit(cell === void 0 ? "" : rawEditableText(cell.value), seedKind);
@@ -5399,10 +5920,11 @@ function OdsSpreadsheetGridScreen() {
5399
5920
  children: [`${row + 1}`.padStart(4), " "]
5400
5921
  }), visibleColumns.map((column) => {
5401
5922
  const isCursor = row === clampedRow && column === clampedColumn;
5923
+ const isAnchor = row === mergeAnchor?.row && column === mergeAnchor?.column;
5402
5924
  const cell = cells.get(cellKey(row, column));
5403
5925
  return /* @__PURE__ */ jsx(Text, {
5404
5926
  inverse: isCursor,
5405
- color: isCursor ? "cyan" : void 0,
5927
+ color: isCursor ? "cyan" : isAnchor ? "yellow" : void 0,
5406
5928
  children: padCell(cell === void 0 ? "" : cell.displayText, CELL_WIDTH)
5407
5929
  }, column);
5408
5930
  })] }, row))]
@@ -5440,13 +5962,9 @@ function OdsSpreadsheetGridScreen() {
5440
5962
  setEditSession(void 0);
5441
5963
  }
5442
5964
  }),
5443
- /* @__PURE__ */ jsxs(Text, {
5965
+ /* @__PURE__ */ jsx(Text, {
5444
5966
  dimColor: true,
5445
- children: [
5446
- "hjkl/arrows move, Enter/type to edit, p print settings, t ",
5447
- viewMode === "grid" ? "compact list" : "grid",
5448
- " view, Esc back"
5449
- ]
5967
+ children: mergeAnchor === void 0 ? `hjkl/arrows move, Enter/type to edit, m to anchor a merge, p print settings, t ${viewMode === "grid" ? "compact list" : "grid"} view, Esc back` : "hjkl/arrows to the opposite corner, m/Enter to merge, Esc to cancel"
5450
5968
  })
5451
5969
  ]
5452
5970
  });
@@ -6498,6 +7016,7 @@ function ScreenBody({ screen }) {
6498
7016
  case "slideList": return format === "odp" ? /* @__PURE__ */ jsx(OdpSlideListScreen, {}) : /* @__PURE__ */ jsx(PptxSlideListScreen, {});
6499
7017
  case "slideDetail": return /* @__PURE__ */ jsx(SlideDetailScreen, { screen });
6500
7018
  case "shapeEditor": return /* @__PURE__ */ jsx(ShapeEditorScreen, { screen });
7019
+ case "slideTableDetail": return /* @__PURE__ */ jsx(SlideTableDetailScreen, { screen });
6501
7020
  case "notesEditor": return /* @__PURE__ */ jsx(NotesEditorScreen, { screen });
6502
7021
  case "sheetList": return /* @__PURE__ */ jsx(OdsSheetListScreen, {});
6503
7022
  case "spreadsheetGrid": return /* @__PURE__ */ jsx(OdsSpreadsheetGridScreen, {});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "document-cli",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "CLI and interactive Ink TUI for documents.js: every docx/pptx/odt/odp/ods/odg/odf/pdf/odm/odb/xlsx/markdown conversion, bridge, and editor as a scriptable command or a terminal app.",
5
5
  "type": "module",
6
6
  "repository": {