@mcpher/gas-fakes 2.5.2 → 2.5.3

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.
Files changed (50) hide show
  1. package/README.md +8 -1
  2. package/package.json +1 -1
  3. package/pngs/srv.jpg +0 -0
  4. package/src/cli/app.js +1 -0
  5. package/src/cli/togas.js +23 -14
  6. package/src/index.js +1 -0
  7. package/src/services/advslides/fakeadvslides.js +11 -5
  8. package/src/services/base/app.js +9 -0
  9. package/src/services/base/fakebase.js +28 -0
  10. package/src/services/common/fakeadvresource.js +3 -2
  11. package/src/services/enums/baseenums.js +47 -0
  12. package/src/services/enums/slidesenums.js +3 -1
  13. package/src/services/html/serverworker.js +1 -1
  14. package/src/services/slidesapp/app.js +5 -0
  15. package/src/services/slidesapp/fakeautofit.js +1 -1
  16. package/src/services/slidesapp/fakeborder.js +106 -0
  17. package/src/services/slidesapp/fakecolorscheme.js +1 -1
  18. package/src/services/slidesapp/fakefill.js +216 -0
  19. package/src/services/slidesapp/fakegroup.js +35 -0
  20. package/src/services/slidesapp/fakeimage.js +118 -0
  21. package/src/services/slidesapp/fakelayout.js +351 -0
  22. package/src/services/slidesapp/fakeline.js +2 -2
  23. package/src/services/slidesapp/fakelinefill.js +15 -16
  24. package/src/services/slidesapp/fakelink.js +20 -3
  25. package/src/services/slidesapp/fakelist.js +36 -0
  26. package/src/services/slidesapp/fakeliststyle.js +105 -0
  27. package/src/services/slidesapp/fakemaster.js +358 -0
  28. package/src/services/slidesapp/fakenotesmaster.js +125 -0
  29. package/src/services/slidesapp/fakenotespage.js +102 -2
  30. package/src/services/slidesapp/fakepagebackground.js +109 -1
  31. package/src/services/slidesapp/fakepageelement.js +157 -18
  32. package/src/services/slidesapp/fakepageelementrange.js +28 -0
  33. package/src/services/slidesapp/fakepagerange.js +28 -0
  34. package/src/services/slidesapp/fakeparagraphstyle.js +139 -0
  35. package/src/services/slidesapp/fakepicturefill.js +32 -0
  36. package/src/services/slidesapp/fakepresentation.js +126 -2
  37. package/src/services/slidesapp/fakeshape.js +9 -0
  38. package/src/services/slidesapp/fakeslide.js +216 -24
  39. package/src/services/slidesapp/fakesolidfill.js +45 -0
  40. package/src/services/slidesapp/fakespeakerspotlight.js +18 -0
  41. package/src/services/slidesapp/faketable.js +55 -9
  42. package/src/services/slidesapp/faketablecell.js +141 -12
  43. package/src/services/slidesapp/faketablecellrange.js +28 -0
  44. package/src/services/slidesapp/faketablecolumn.js +72 -0
  45. package/src/services/slidesapp/faketablerow.js +31 -0
  46. package/src/services/slidesapp/faketextrange.js +179 -135
  47. package/src/services/slidesapp/faketextstyle.js +158 -0
  48. package/src/services/slidesapp/fakevideo.js +35 -0
  49. package/src/services/slidesapp/fakewordart.js +22 -0
  50. package/src/services/slidesapp/pageelementfactory.js +24 -1
@@ -0,0 +1,28 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+
3
+ /**
4
+ * create a new FakeTableCellRange instance
5
+ * @param {...any} args
6
+ * @returns {FakeTableCellRange}
7
+ */
8
+ export const newFakeTableCellRange = (...args) => {
9
+ return Proxies.guard(new FakeTableCellRange(...args));
10
+ };
11
+
12
+ export class FakeTableCellRange {
13
+ constructor(cells) {
14
+ this.__cells = cells;
15
+ }
16
+
17
+ /**
18
+ * Returns the list of TableCell instances.
19
+ * @returns {FakeTableCell[]} The cells.
20
+ */
21
+ getTableCells() {
22
+ return this.__cells;
23
+ }
24
+
25
+ toString() {
26
+ return 'TableCellRange';
27
+ }
28
+ }
@@ -0,0 +1,72 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { newFakeTableCell } from './faketablecell.js';
3
+
4
+ /**
5
+ * create a new FakeTableColumn instance
6
+ * @param {...any} args
7
+ * @returns {FakeTableColumn}
8
+ */
9
+ export const newFakeTableColumn = (...args) => {
10
+ return Proxies.guard(new FakeTableColumn(...args));
11
+ };
12
+
13
+ export class FakeTableColumn {
14
+ constructor(table, colIndex) {
15
+ this.__table = table;
16
+ this.__colIndex = colIndex;
17
+ }
18
+
19
+ /**
20
+ * Gets a cell by its index.
21
+ * @param {number} index The row index.
22
+ * @returns {FakeTableCell} The cell.
23
+ */
24
+ getCell(index) {
25
+ return newFakeTableCell(this.__table, index, this.__colIndex);
26
+ }
27
+
28
+ /**
29
+ * Gets the 0-based index of the column.
30
+ * @returns {number} The index.
31
+ */
32
+ getIndex() {
33
+ return this.__colIndex;
34
+ }
35
+
36
+ /**
37
+ * Gets the number of cells in this column.
38
+ * @returns {number} The number of cells.
39
+ */
40
+ getNumCells() {
41
+ return this.__table.getNumRows();
42
+ }
43
+
44
+ /**
45
+ * Gets the table containing the current column.
46
+ * @returns {FakeTable} The parent table.
47
+ */
48
+ getParentTable() {
49
+ return this.__table;
50
+ }
51
+
52
+ /**
53
+ * Gets the width of the column in points.
54
+ * @returns {number} The width.
55
+ */
56
+ getWidth() {
57
+ const tableResource = this.__table.__resource.table;
58
+ const colProperties = tableResource?.tableColumns?.[this.__colIndex];
59
+ return this.__table.__normalize(colProperties?.columnWidth);
60
+ }
61
+
62
+ /**
63
+ * Removes the table column.
64
+ */
65
+ remove() {
66
+ throw new Error('TableColumn.remove() not yet implemented');
67
+ }
68
+
69
+ toString() {
70
+ return 'TableColumn';
71
+ }
72
+ }
@@ -38,6 +38,22 @@ export class FakeTableRow {
38
38
  return cells[index];
39
39
  }
40
40
 
41
+ /**
42
+ * Returns the 0-based index of the row.
43
+ * @returns {number} The index.
44
+ */
45
+ getIndex() {
46
+ return this.__rowIndex;
47
+ }
48
+
49
+ /**
50
+ * Returns the minimum height of the row in points.
51
+ * @returns {number} The minimum height.
52
+ */
53
+ getMinimumHeight() {
54
+ return this.__table.__normalize(this.__resource?.rowHeight);
55
+ }
56
+
41
57
  /**
42
58
  * Gets the number of cells in the row.
43
59
  * @returns {number} The number of cells.
@@ -46,6 +62,21 @@ export class FakeTableRow {
46
62
  return (this.__resource?.tableCells || []).length;
47
63
  }
48
64
 
65
+ /**
66
+ * Returns the table containing the current row.
67
+ * @returns {FakeTable} The parent table.
68
+ */
69
+ getParentTable() {
70
+ return this.__table;
71
+ }
72
+
73
+ /**
74
+ * Removes the table row.
75
+ */
76
+ remove() {
77
+ throw new Error('TableRow.remove() not yet implemented');
78
+ }
79
+
49
80
  toString() {
50
81
  return 'TableRow';
51
82
  }
@@ -1,6 +1,9 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
2
  import { newFakeParagraph } from './fakeparagraph.js';
3
3
  import { newFakeAutoText } from './fakeautotext.js';
4
+ import { newFakeTextStyle } from './faketextstyle.js';
5
+ import { newFakeParagraphStyle } from './fakeparagraphstyle.js';
6
+ import { newFakeListStyle } from './fakeliststyle.js';
4
7
  import { AutofitType } from '../enums/slidesenums.js';
5
8
 
6
9
  export const newFakeTextRange = (...args) => {
@@ -10,8 +13,6 @@ export const newFakeTextRange = (...args) => {
10
13
  export class FakeTextRange {
11
14
  constructor(shape, startIndex = null, endIndex = null) {
12
15
  this.__shape = shape;
13
- // APIs use 0-based index? Apps Script TextRange uses indices?
14
- // If null, represents the entire text.
15
16
  this.__startIndex = startIndex;
16
17
  this.__endIndex = endIndex;
17
18
  }
@@ -21,40 +22,143 @@ export class FakeTextRange {
21
22
  if (shapeResource && shapeResource.shape && shapeResource.shape.text) {
22
23
  return shapeResource.shape.text;
23
24
  }
25
+ if (shapeResource && shapeResource.table) {
26
+ return shapeResource.table.tableRows?.[this.__shape.__cellLocation?.rowIndex]?.tableCells?.[this.__shape.__cellLocation?.columnIndex]?.text || {};
27
+ }
24
28
  return {};
25
29
  }
26
30
 
27
31
  getStartIndex() {
28
- // If indices are null, it starts at 0?
29
32
  return this.__startIndex !== null ? this.__startIndex : 0;
30
33
  }
31
34
 
32
35
  getEndIndex() {
33
- // If indices are null, it ends at full length?
34
- // Caution: If we haven't computed length...
35
- // But usually FakeTextRange is created with explicit indices by getParagraphs.
36
- // If created without indices (full range), we need to calculate length.
37
36
  if (this.__endIndex !== null) return this.__endIndex;
38
- return this.asString().length;
37
+ return this.getLength();
38
+ }
39
+
40
+ appendParagraph(text) {
41
+ this.appendText(text + '\n');
42
+ const paragraphs = this.getParagraphs();
43
+ return paragraphs[paragraphs.length - 1];
44
+ }
45
+
46
+ appendRange(range) {
47
+ this.appendText(range.asString());
48
+ return this;
49
+ }
50
+
51
+ appendText(text) {
52
+ // In Google Slides API, if the text ends in a newline, the insertion index
53
+ // should be length - 1 to be inside the text.
54
+ // But actually, for appending, length is correct if we want to add after.
55
+ // The issue is likely that asString() is NOT matching API length.
56
+ return this.insertText(this.asString().length, text);
57
+ }
58
+
59
+ getLength() {
60
+ const textElements = this.__resource?.textElements || [];
61
+ if (textElements.length === 0) return 0;
62
+
63
+ const lastElement = textElements[textElements.length - 1];
64
+ let totalLength = lastElement.endIndex || 0;
65
+
66
+ if (this.__startIndex !== null && this.__endIndex !== null) {
67
+ return Math.max(0, this.__endIndex - this.__startIndex);
68
+ }
69
+ if (this.__startIndex !== null) {
70
+ return Math.max(0, totalLength - this.__startIndex);
71
+ }
72
+ return totalLength;
73
+ }
74
+
75
+ getLinks() {
76
+ return this.getTextStyle().hasLink() ? [this] : [];
77
+ }
78
+
79
+ getListParagraphs() {
80
+ return this.getParagraphs().filter(p => p.getRange().getListStyle().isInList());
81
+ }
82
+
83
+ getListStyle() {
84
+ return newFakeListStyle(this);
85
+ }
86
+
87
+ getParagraphStyle() {
88
+ return newFakeParagraphStyle(this);
89
+ }
90
+
91
+ getRange(start, end) {
92
+ return newFakeTextRange(this.__shape, this.getStartIndex() + start, this.getStartIndex() + end);
93
+ }
94
+
95
+ getRuns() {
96
+ return [this];
97
+ }
98
+
99
+ getTextStyle() {
100
+ return newFakeTextStyle(this);
101
+ }
102
+
103
+ insertParagraph(offset, text) {
104
+ this.insertText(offset, text + '\n');
105
+ return this.getParagraphs().find(p => p.getRange().getStartIndex() === this.getStartIndex() + offset);
106
+ }
107
+
108
+ insertRange(offset, range) {
109
+ return this.insertText(offset, range.asString());
110
+ }
111
+
112
+ replaceAllText(findText, replaceText, matchCase) {
113
+ const presentationId = this.__shape.__presentation.getId();
114
+ const requests = [{
115
+ replaceAllText: {
116
+ replaceText: replaceText,
117
+ containsText: {
118
+ text: findText,
119
+ matchCase: matchCase
120
+ },
121
+ pageObjectIds: [this.__shape.getParentPage().getObjectId()]
122
+ }
123
+ }];
124
+
125
+ const response = Slides.Presentations.batchUpdate({ requests }, presentationId);
126
+ return response.replies[0].replaceAllText.occurrencesChanged || 0;
127
+ }
128
+
129
+ select() {
130
+ return this;
39
131
  }
40
132
 
41
- /**
42
- * Gets the rendered string of the text range.
43
- * @returns {string} The text.
44
- */
45
133
  asString() {
46
134
  const textElements = this.__resource?.textElements || [];
47
- let fullText = textElements.map(te => {
48
- if (te.textRun) return te.textRun.content;
49
- if (te.autoText) return te.autoText.content || '[AutoText]'; // Use placeholder
50
- return '';
51
- }).join('');
135
+ if (textElements.length === 0) return '';
136
+
137
+ // We need the full length of the underlying text resource to use API indices.
138
+ const lastElement = textElements[textElements.length - 1];
139
+ const fullLength = lastElement.endIndex || 0;
140
+
141
+ // Reconstruct the full string based on API indices to ensure parity.
142
+ const chars = new Array(fullLength).fill(null);
143
+ for (const te of textElements) {
144
+ const start = te.startIndex || 0;
145
+ const end = te.endIndex;
146
+
147
+ if (te.textRun) {
148
+ const content = te.textRun.content;
149
+ for (let i = 0; i < content.length; i++) {
150
+ if (start + i < fullLength) chars[start + i] = content[i];
151
+ }
152
+ } else if (te.autoText) {
153
+ if (start < fullLength) chars[start] = te.autoText.content || ' ';
154
+ } else if (te.paragraphMarker) {
155
+ if (end > 0 && end <= fullLength) chars[end - 1] = '\n';
156
+ }
157
+ }
52
158
 
53
- // If indices are provided, slice the text.
54
- // We assume indices are 0-based relative to the start of the SHAPE text (if referencing shape).
159
+ let fullText = chars.map(c => c === null ? '\n' : c).join('');
55
160
 
56
161
  if (this.__startIndex !== null && this.__endIndex !== null) {
57
- // Ensure indices are within bounds? Or just slice.
58
162
  fullText = fullText.slice(this.__startIndex, this.__endIndex);
59
163
  } else if (this.__startIndex !== null) {
60
164
  fullText = fullText.slice(this.__startIndex);
@@ -67,78 +171,68 @@ export class FakeTextRange {
67
171
  return this.asString();
68
172
  }
69
173
 
70
- /**
71
- * Sets the text content.
72
- * @param {string} newText The new text.
73
- * @returns {FakeTextRange} This range.
74
- */
75
174
  setText(newText) {
76
- const objectId = this.__shape.getObjectId();
77
- const presentationId = this.__shape.__presentation.getId();
78
-
79
- const requests = [];
80
-
81
- const currentText = this.asString();
82
-
83
- if (currentText.length > 0) {
84
- requests.push({
85
- deleteText: {
86
- objectId: objectId,
87
- cellLocation: this.__shape.__cellLocation,
88
- textRange: {
89
- type: 'FROM_START_INDEX',
90
- startIndex: 0
91
- }
92
- }
93
- });
94
- }
95
-
175
+ this.clear();
96
176
  if (newText) {
97
- // Apps Script shapes always end with a newline.
98
- // If we insert text that ends with a newline, we get double newline (one explicit, one implicit).
99
- // To match expected behavior where setText("A\n") results in "A" (plus implicit \n),
100
- // or at least doesn't create "A\n\n", we should strip the trailing newline.
101
-
102
177
  if (newText.endsWith('\n')) {
103
178
  newText = newText.slice(0, -1);
104
179
  }
105
-
106
- requests.push({
107
- insertText: {
108
- objectId: objectId,
109
- cellLocation: this.__shape.__cellLocation,
110
- insertionIndex: 0,
111
- text: newText
112
- }
113
- });
180
+ this.appendText(newText);
114
181
  }
182
+ return this;
183
+ }
115
184
 
116
- if (requests.length > 0) {
117
- Slides.Presentations.batchUpdate(requests, presentationId);
185
+ insertText(offset, text) {
186
+ const objectId = this.__shape.getObjectId();
187
+ const presentationId = this.__shape.__presentation.getId();
188
+
189
+ const totalLen = this.getLength();
190
+ let insertionIndex = this.getStartIndex() + offset;
191
+
192
+ // In Slides API, insertionIndex cannot be after the final paragraph marker.
193
+ // If the shape has length 12 (0-11 text, 11-12 marker), max insertionIndex is 11.
194
+ if (insertionIndex >= totalLen && totalLen > 0) {
195
+ insertionIndex = totalLen - 1;
118
196
  }
119
197
 
198
+ Slides.Presentations.batchUpdate({ requests: [{
199
+ insertText: {
200
+ objectId: objectId,
201
+ cellLocation: this.__shape.__cellLocation,
202
+ insertionIndex: insertionIndex,
203
+ text: text
204
+ }
205
+ }] }, presentationId);
120
206
  return this;
121
207
  }
122
208
 
123
209
  clear() {
124
- return this.setText('');
125
- }
126
-
127
- clear() {
128
- return this.setText('');
210
+ if (this.getLength() === 0) return this;
211
+ const objectId = this.__shape.getObjectId();
212
+ const presentationId = this.__shape.__presentation.getId();
213
+
214
+ const type = (this.__startIndex === null && this.__endIndex === null) ? 'ALL' : 'FIXED_RANGE';
215
+ const deleteRange = { type };
216
+ if (type === 'FIXED_RANGE') {
217
+ deleteRange.startIndex = this.getStartIndex();
218
+ deleteRange.endIndex = this.getEndIndex();
219
+ }
220
+
221
+ Slides.Presentations.batchUpdate({ requests: [{
222
+ deleteText: {
223
+ objectId: objectId,
224
+ cellLocation: this.__shape.__cellLocation,
225
+ textRange: deleteRange
226
+ }
227
+ }] }, presentationId);
228
+ return this;
129
229
  }
130
230
 
131
- /**
132
- * Gets the paragraphs in the text range.
133
- * @returns {FakeParagraph[]} The paragraphs.
134
- */
135
231
  getParagraphs() {
136
232
  const fullText = this.asString();
137
233
  if (fullText.length === 0) return [];
138
234
 
139
235
  const paragraphs = [];
140
- let startIndex = 0;
141
-
142
236
  let currentIndex = 0;
143
237
  let paragraphIndex = 0;
144
238
 
@@ -149,15 +243,10 @@ export class FakeTextRange {
149
243
  if (nextNewline === -1) {
150
244
  endIndex = fullText.length;
151
245
  } else {
152
- endIndex = nextNewline + 1; // Include the newline
153
- }
154
-
155
- if (startIndex + currentIndex >= startIndex + endIndex) {
156
- // Empty segment
157
- if (endIndex === fullText.length) break; // Trailing empty
246
+ endIndex = nextNewline + 1;
158
247
  }
159
248
 
160
- const range = newFakeTextRange(this.__shape, startIndex + currentIndex, startIndex + endIndex);
249
+ const range = newFakeTextRange(this.__shape, this.getStartIndex() + currentIndex, this.getStartIndex() + endIndex);
161
250
  paragraphs.push(newFakeParagraph(range, paragraphIndex++));
162
251
 
163
252
  currentIndex = endIndex;
@@ -166,76 +255,31 @@ export class FakeTextRange {
166
255
  return paragraphs;
167
256
  }
168
257
 
169
- /**
170
- * Gets the auto texts in the text range.
171
- * @returns {FakeAutoText[]} The auto texts.
172
- */
173
258
  getAutoTexts() {
174
259
  const textElements = this.__resource?.textElements || [];
175
260
  const autoTexts = [];
176
261
  let charIndex = 0;
177
262
  let autoTextIndex = 0;
178
263
 
179
- // We need to iterate text elements to find autoText and calculate their ranges.
180
- // But FakeTextRange might be a sub-range (startIndex, endIndex).
181
- // We should only return AutoTexts falling within this range.
182
-
183
264
  const rangeStart = this.getStartIndex();
184
265
  const rangeEnd = this.getEndIndex();
185
266
 
186
267
  for (const element of textElements) {
187
268
  const elementLength = (element.textRun ? element.textRun.content.length : 0) +
188
- (element.autoText ? 1 : 0) + // AutoText usually has content length 1?
189
- (element.paragraphMarker ? 0 : 0); // Markers don't consume content length exactly?
190
- // Actually, paragraph markers ARE elements.
191
- // Wait, textElements is a flat list.
192
- // TextRun has content string.
193
- // AutoText has no content string property directly? It renders as text.
194
- // In API, AutoText is a separate kind of element.
195
- // And it usually corresponds to a specific character length (e.g. 1 char for page number).
196
-
197
- // Let's assume AutoText has length 1 for indexing purposes if it doesn't have explicit content length.
198
- // But wait, getStartIndex/EndIndex logic in asString() uses textElements.map...
199
- // asString() uses: te.textRun ? te.textRun.content : ''.
200
- // So AutoText currently contributes 0 length to `asString()`!
201
- // If AutoText is not returned by `asString()`, then it doesn't exist in the string view?
202
- // If so, our `currentIndex` logic in `getParagraphs` works on `asString()`.
203
-
204
- // If user inserts Page Number, it appears as text.
205
- // So AutoText MUST contribute to content.
206
- // We probably need to mock the content of AutoText if it's missing.
207
- // Or assume `te.autoText` implies some content.
208
-
209
- // For now, if we encounter an autoText element:
269
+ (element.autoText ? 1 : 0);
270
+
210
271
  if (element.autoText) {
211
- // It's an auto text.
212
- // If it falls within [rangeStart, rangeEnd).
213
- // Since asString() implementation currently IGNORES autoText (returns ''), their index is effective 0?
214
- // We need to FIX `asString` to include AutoText content if we want consistent indexing?
215
- // OR `AutoText` objects are separate.
216
-
217
- // If `asString` returns "Page 1", then `1` is the auto text?
218
- // We'll proceed with creating the object.
219
-
220
- // Check bounds
221
- // If asString() skips it, its index is not advancing charIndex?
222
- // This suggests we need to look at how we populate text elements in tests.
223
- // If we don't allow creating AutoText in tests yet, `getAutoTexts()` will just return empty list.
224
- // That fulfills the requirement of "Implementing the method".
225
- // We will implement logic assuming `element.autoText` exists.
226
-
227
- const autoText = newFakeAutoText(
228
- newFakeTextRange(this.__shape, charIndex, charIndex + 1), // Assuming length 1
229
- element.autoText.type,
230
- autoTextIndex++
231
- );
232
- autoTexts.push(autoText);
272
+ if (charIndex >= rangeStart && charIndex < rangeEnd) {
273
+ const autoText = newFakeAutoText(
274
+ newFakeTextRange(this.__shape, charIndex, charIndex + 1),
275
+ element.autoText.type,
276
+ autoTextIndex++
277
+ );
278
+ autoTexts.push(autoText);
279
+ }
233
280
  }
234
281
 
235
- if (element.textRun) {
236
- charIndex += element.textRun.content.length;
237
- }
238
- // Paragraph markers?
282
+ charIndex += elementLength;
239
283
  }
240
284
  return autoTexts;
241
285
  }