@trebco/treb 29.6.2 → 29.7.5

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.
@@ -0,0 +1,85 @@
1
+ /*
2
+ * This file is part of TREB.
3
+ *
4
+ * TREB is free software: you can redistribute it and/or modify it under the
5
+ * terms of the GNU General Public License as published by the Free Software
6
+ * Foundation, either version 3 of the License, or (at your option) any
7
+ * later version.
8
+ *
9
+ * TREB is distributed in the hope that it will be useful, but WITHOUT ANY
10
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12
+ * details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License along
15
+ * with TREB. If not, see <https://www.gnu.org/licenses/>.
16
+ *
17
+ * Copyright 2022-2024 trebco, llc.
18
+ * info@treb.app
19
+ *
20
+ */
21
+
22
+ import type { CellValue, IArea, CellStyle } from 'treb-base-types';
23
+
24
+
25
+ /**
26
+ * this is a structure for copy/paste data. clipboard data may include
27
+ * relative formauls and resolved styles, so it's suitable for pasting into
28
+ * other areas of the spreadsheet.
29
+ *
30
+ * @privateRemarks
31
+ * work in progress. atm we're not using the system clipboard, although it
32
+ * might be useful to merge this with grid copy/paste routines in the future.
33
+ *
34
+ * if it hits the clipboard this should use mime type `application/x-treb-data`
35
+ *
36
+ */
37
+ export interface ClipboardDataElement {
38
+
39
+ /** calculated cell value */
40
+ calculated: CellValue,
41
+
42
+ /** the actual cell value or formula */
43
+ value: CellValue,
44
+
45
+ /** cell style. this may include row/column styles from the copy source */
46
+ style?: CellStyle,
47
+
48
+ /** area. if this cell is part of an array, this is the array range */
49
+ area?: IArea,
50
+
51
+ /* TODO: merge, like area */
52
+
53
+ /* TODO: table */
54
+
55
+ }
56
+
57
+ /** clipboard data is a 2d array */
58
+ export type ClipboardData = ClipboardDataElement[][];
59
+
60
+ /**
61
+ * optional paste options. we can paste formulas or values, and we
62
+ * can use the source style, target style, or just use the source
63
+ * number formats.
64
+ */
65
+ export interface PasteOptions {
66
+
67
+ /**
68
+ * when clipboard data includes formulas, optionally paste calculated
69
+ * values instead of the original formulas. defaults to false.
70
+ */
71
+ values?: boolean;
72
+
73
+ /**
74
+ * when pasting data from the clipboard, we can copy formatting/style
75
+ * from the original data, or we can retain the target range formatting
76
+ * and just paste data. a third option allows pasting source number
77
+ * formats but dropping other style information.
78
+ *
79
+ * defaults to "source", meaning paste source styles.
80
+ */
81
+
82
+ formatting?: 'source'|'target'|'number-formats'
83
+
84
+ }
85
+
@@ -116,6 +116,7 @@ import type { ClipboardCellData } from './clipboard_data';
116
116
 
117
117
  import type { ExternalEditorConfig } from './external_editor_config';
118
118
  import { ExternalEditor } from '../editors/external_editor';
119
+ import type { ClipboardData, PasteOptions } from './clipboard_data2';
119
120
 
120
121
  interface DoubleClickData {
121
122
  timeout?: number;
@@ -416,6 +417,227 @@ export class Grid extends GridBase {
416
417
 
417
418
  }
418
419
 
420
+
421
+ // --- Copy/paste API methods ------------------------------------------------
422
+ //
423
+ // moving here with a view towards (eventually) merging with the UI/browser
424
+ // copy/paste routines (in grid)
425
+ //
426
+
427
+
428
+ /**
429
+ * internal composite for cut/copy. mostly identical except we
430
+ * read data as A1 for cut, so it will retain references. also
431
+ * cut clears the data.
432
+ *
433
+ * FIXME: merge with grid cut/copy/paste routines. we already
434
+ * handle recycling and relative addressing, the only thing missing
435
+ * is alternate formats.
436
+ */
437
+ public CopyArea(resolved: Area, semantics: 'cut'|'copy' = 'copy'): ClipboardData {
438
+
439
+ // resolve range so we can use it later -> Area
440
+ const sheet = (resolved.start.sheet_id ? this.model.sheets.Find(resolved.start.sheet_id) : this.active_sheet) || this.active_sheet;
441
+
442
+ // get style data, !apply theme but do apply r/c styles
443
+ const style_data = sheet.GetCellStyle(resolved, false);
444
+
445
+ // flag we want R1C1 (copy)
446
+ const r1c1 = (semantics !== 'cut');
447
+
448
+ // NOTE: we're losing arrays here. need to fix. also think
449
+ // about merges? we'll reimplement what grid does (only in part)
450
+
451
+ const data: ClipboardData = [];
452
+
453
+ for (const { cell, row, column } of sheet.cells.IterateRC(resolved)) {
454
+
455
+ // raw value
456
+ let value = cell.value;
457
+
458
+ // seems like we're using a loop function unecessarily
459
+ if (r1c1 && value && cell.type === ValueType.formula) {
460
+ value = this.FormatR1C1(value, { row, column })[0][0];
461
+ }
462
+
463
+ const r = row - resolved.start.row;
464
+ const c = column - resolved.start.column;
465
+
466
+ if (!data[r]) {
467
+ data[r] = [];
468
+ }
469
+
470
+ let array_head: IArea|undefined;
471
+ if (cell.area) {
472
+
473
+ // scrubbing to just area (and unlinking)
474
+ array_head = {
475
+ start: {
476
+ row: cell.area.start.row - resolved.start.row,
477
+ column: cell.area.start.column - resolved.start.column,
478
+ },
479
+ end: {
480
+ row: cell.area.end.row - resolved.start.row,
481
+ column: cell.area.end.column - resolved.start.column,
482
+ },
483
+ };
484
+
485
+ }
486
+
487
+ data[r][c] = {
488
+ value,
489
+ calculated: cell.calculated,
490
+ style: style_data[r][c],
491
+ area: array_head,
492
+ };
493
+
494
+ }
495
+
496
+ // EmbeddedSpreadsheet.clipboard = structuredClone(data);
497
+
498
+ if (semantics === 'cut') {
499
+ this.SetRange(resolved, undefined, { recycle: true }); // clear
500
+ }
501
+
502
+ return data;
503
+
504
+ }
505
+
506
+ /**
507
+ * paste clipboard data into a target range. this method does not use
508
+ * the system clipboard; pass in clipboard data returned from the Cut or
509
+ * Copy method.
510
+ *
511
+ * @param target - the target to paste data into. this can be larger
512
+ * than the clipboard data, in which case values will be recycled in
513
+ * blocks. if the target is smaller than the source data, we will expand
514
+ * it to fit the data.
515
+ *
516
+ * @param data - clipboard data to paste.
517
+ *
518
+ * @privateRemarks LLM API
519
+ *
520
+ * @privateRemarks this was async when we were thinking of using the
521
+ * system clipboard, but that's pretty broken so we're not going to
522
+ * bother atm.
523
+ */
524
+ public PasteArea(resolved: Area, data: ClipboardData, options: PasteOptions = {}): void {
525
+
526
+ if (!data) {
527
+ throw new Error('no clipboad data');
528
+ }
529
+
530
+ // paste has some special semantics. if the target smaller than the
531
+ // source data, we write the full data irrespective of size (similar
532
+ // to "spill"). otherwise, we recycle in blocks.
533
+
534
+ // the setrange method will recycle, but we also need to recycle styles.
535
+
536
+ // start with data length
537
+
538
+ const rows = data.length;
539
+ const columns = data[0]?.length || 0;
540
+
541
+ // target -> block size
542
+
543
+ resolved.Resize(
544
+ Math.max(1, Math.floor(resolved.rows / rows)) * rows,
545
+ Math.max(1, Math.floor(resolved.columns / columns)) * columns );
546
+
547
+ const sheet = (resolved.start.sheet_id ? this.model.sheets.Find(resolved.start.sheet_id) : this.active_sheet) || this.active_sheet;
548
+
549
+ const values: CellValue[][] = [];
550
+
551
+ // optionally collect calculated values, instead of raw values
552
+
553
+ if (options.values) {
554
+ for (const [index, row] of data.entries()) {
555
+ values[index] = [];
556
+ for (const cell of row) {
557
+ values[index].push(typeof cell.calculated === 'undefined' ? cell.value : cell.calculated);
558
+ }
559
+ }
560
+ }
561
+
562
+ // this is to resolve the reference in the callback,
563
+ // but we should copy -- there's a possibility that
564
+ // this points to the static member, which could get
565
+ // overwritten. FIXME
566
+
567
+ const local = data;
568
+
569
+ // batch to limit events, sync up undo
570
+
571
+ const events = this.Batch(() => {
572
+
573
+ // this needs to change to support arrays (and potentially merges...)
574
+
575
+ // actually we could leave as is for just calculated values
576
+
577
+ if (options.values) {
578
+ this.SetRange(resolved, values, {
579
+ r1c1: true, recycle: true,
580
+ });
581
+ }
582
+ else {
583
+
584
+ // so this is for formulas only now
585
+
586
+ // start by clearing... (but leave styles as-is for now)
587
+
588
+ // probably a better way to do this
589
+ this.SetRange(resolved, undefined, { recycle: true });
590
+
591
+ for (const address of resolved) {
592
+ const r = (address.row - resolved.start.row) % rows;
593
+ const c = (address.column - resolved.start.column) % columns;
594
+
595
+ const cell_data = local[r][c];
596
+
597
+ if (cell_data.area) {
598
+ // only the head
599
+ if (cell_data.area.start.row === r && cell_data.area.start.column === c) {
600
+ const array_target = new Area(cell_data.area.start, cell_data.area.end);
601
+ array_target.Shift(resolved.start.row, resolved.start.column);
602
+ this.SetRange(array_target, cell_data.value, { r1c1: true, array: true });
603
+ }
604
+ }
605
+ else if (cell_data.value) {
606
+ this.SetRange(new Area(address), cell_data.value, { r1c1: true });
607
+ }
608
+
609
+ }
610
+
611
+ }
612
+
613
+ if (options.formatting === 'number-formats') {
614
+
615
+ // number format only, and apply delta
616
+
617
+ for (const address of resolved) {
618
+ const r = (address.row - resolved.start.row) % rows;
619
+ const c = (address.column - resolved.start.column) % columns;
620
+ const number_format = (local[r][c].style || {}).number_format;
621
+ sheet.UpdateCellStyle(address, { number_format }, true);
622
+ }
623
+ }
624
+ else if (options.formatting !== 'target') {
625
+
626
+ // use source formatting (default)
627
+ for (const address of resolved) {
628
+ const r = (address.row - resolved.start.row) % rows;
629
+ const c = (address.column - resolved.start.column) % columns;
630
+ sheet.UpdateCellStyle(address, local[r][c].style || {}, false);
631
+ }
632
+
633
+ }
634
+
635
+ }, true);
636
+
637
+ this.grid_events.Publish(events);
638
+
639
+ }
640
+
419
641
  // --- public methods --------------------------------------------------------
420
642
 
421
643
  /**
@@ -1883,23 +2105,19 @@ export class Grid extends GridBase {
1883
2105
  let convert = false;
1884
2106
 
1885
2107
  if (options.argument_separator === ',' && this.parser.argument_separator !== ArgumentSeparatorType.Comma) {
1886
- // this.parser.argument_separator = ArgumentSeparatorType.Comma;
1887
- // this.parser.decimal_mark = DecimalMarkType.Period;
1888
2108
  this.parser.SetLocaleSettings(DecimalMarkType.Period);
1889
-
1890
2109
  convert = true;
1891
2110
  }
1892
2111
 
1893
2112
  if (options.argument_separator === ';' && this.parser.argument_separator !== ArgumentSeparatorType.Semicolon) {
1894
- // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1895
- // this.parser.decimal_mark = DecimalMarkType.Comma;
1896
2113
  this.parser.SetLocaleSettings(DecimalMarkType.Comma);
1897
-
1898
2114
  convert = true;
1899
2115
  }
1900
2116
 
1901
2117
  if (convert) {
1902
2118
 
2119
+ this.parser.flags.r1c1 = r1c1;
2120
+
1903
2121
  const Convert = (value: CellValue): CellValue => {
1904
2122
  if (typeof value === 'string' && value[0] === '=') {
1905
2123
  const result = this.parser.Parse(value);
@@ -1908,8 +2126,10 @@ export class Grid extends GridBase {
1908
2126
  missing: '',
1909
2127
  convert_decimal: current.decimal_mark,
1910
2128
  convert_argument_separator: current.argument_separator,
2129
+ pass_through_addresses: true,
1911
2130
  });
1912
2131
  }
2132
+ // console.info("CVT", this.parser.flags, result.expression, value);
1913
2133
  }
1914
2134
  return value;
1915
2135
  };
@@ -411,6 +411,9 @@ export interface RenderOptions {
411
411
  /** base for offsetting relative R1C1 addresses */
412
412
  r1c1_base?: UnitAddress;
413
413
 
414
+ /** if we're just translating, don't have to render addresses */
415
+ pass_through_addresses?: boolean;
416
+
414
417
  missing: string;
415
418
  convert_decimal: DecimalMarkType;
416
419
  convert_argument_separator: ArgumentSeparatorType;
@@ -34,6 +34,7 @@ import type {
34
34
  ParserFlags,
35
35
  UnitStructuredReference,
36
36
  RenderOptions,
37
+ BaseExpressionUnit,
37
38
  } from './parser-types';
38
39
  import {
39
40
  ArgumentSeparatorType,
@@ -334,6 +335,77 @@ export class Parser {
334
335
  }
335
336
  }
336
337
 
338
+ /**
339
+ * recursive tree walk that allows substitution. this should be
340
+ * a drop-in replacement for the original Walk function but I'm
341
+ * keeping it separate temporarily just in case it breaks something.
342
+ *
343
+ * @param func - in this version function can return `true` (continue
344
+ * walking subtree), `false` (don't walk subtree), or an ExpressionUnit.
345
+ * in the last case, we'll replace the original unit with the substitution.
346
+ * obviously in that case we don't recurse.
347
+ */
348
+ public Walk2(unit: ExpressionUnit, func: (unit: ExpressionUnit) => boolean|ExpressionUnit|undefined): ExpressionUnit {
349
+
350
+ const result = func(unit);
351
+ if (typeof result === 'object') {
352
+ return result;
353
+ }
354
+
355
+ switch (unit.type) {
356
+ case 'address':
357
+ case 'missing':
358
+ case 'literal':
359
+ case 'complex':
360
+ case 'identifier':
361
+ case 'operator':
362
+ case 'structured-reference':
363
+ break;
364
+
365
+ case 'dimensioned':
366
+ if (result) {
367
+ unit.expression = this.Walk2(unit.expression, func) as BaseExpressionUnit; // could be an issue
368
+ unit.unit = this.Walk2(unit.unit, func) as UnitIdentifier; // could be an issue
369
+ }
370
+ break;
371
+
372
+ case 'range':
373
+ if (func(unit)) {
374
+ unit.start = this.Walk2(unit.start, func) as UnitAddress; // could be an issue
375
+ unit.end = this.Walk2(unit.end, func) as UnitAddress; // could be an issue
376
+ }
377
+ break;
378
+
379
+ case 'binary':
380
+ if (func(unit)) {
381
+ unit.left = this.Walk2(unit.left, func);
382
+ unit.right = this.Walk2(unit.right, func);
383
+ }
384
+ break;
385
+
386
+ case 'unary':
387
+ if (func(unit)) {
388
+ unit.operand = this.Walk2(unit.operand, func);
389
+ }
390
+ break;
391
+
392
+ case 'group':
393
+ if (func(unit)) {
394
+ unit.elements = unit.elements.map(source => this.Walk2(source, func));
395
+ }
396
+ break;
397
+
398
+ case 'call':
399
+ if (func(unit)) {
400
+ unit.args = unit.args.map(source => this.Walk2(source, func));
401
+ }
402
+ break;
403
+ }
404
+
405
+ return unit;
406
+
407
+ }
408
+
337
409
  /**
338
410
  * recursive tree walk.
339
411
  *
@@ -382,13 +454,19 @@ export class Parser {
382
454
 
383
455
  case 'group':
384
456
  if (func(unit)) {
385
- unit.elements.forEach((element) => this.Walk(element, func));
457
+ // unit.elements.forEach((element) => this.Walk(element, func));
458
+ for (const element of unit.elements) {
459
+ this.Walk(element, func);
460
+ }
386
461
  }
387
462
  return;
388
463
 
389
464
  case 'call':
390
465
  if (func(unit)) {
391
- unit.args.forEach((arg) => this.Walk(arg, func));
466
+ for (const arg of unit.args) {
467
+ this.Walk(arg, func);
468
+ }
469
+ // unit.args.forEach((arg) => this.Walk(arg, func));
392
470
  }
393
471
  }
394
472
  }
@@ -489,9 +567,15 @@ export class Parser {
489
567
 
490
568
  switch (unit.type) {
491
569
  case 'address':
570
+ if (options.pass_through_addresses) {
571
+ return unit.label;
572
+ }
492
573
  return options.r1c1 ? this.R1C1Label(unit, options.r1c1_base) : this.AddressLabel(unit, offset);
493
574
 
494
575
  case 'range':
576
+ if (options.pass_through_addresses) {
577
+ return unit.label;
578
+ }
495
579
  return options.r1c1 ?
496
580
  this.R1C1Label(unit.start, options.r1c1_base) + ':' + this.R1C1Label(unit.end, options.r1c1_base) :
497
581
  this.AddressLabel(unit.start, offset) + ':' + this.AddressLabel(unit.end, offset);