@mcpher/gas-fakes 2.0.12 → 2.0.13
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/README.md +3 -1
- package/package.json +3 -2
- package/src/services/slidesapp/fakepageelement.js +14 -1
- package/src/services/slidesapp/fakeslide.js +116 -0
- package/src/services/slidesapp/faketable.js +63 -0
- package/src/services/slidesapp/faketablecell.js +51 -0
- package/src/services/slidesapp/faketablerow.js +49 -0
- package/src/services/slidesapp/faketextrange.js +2 -0
- package/src/services/slidesapp/pageelementfactory.js +4 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# <img src="./logo.png" alt="gas-fakes logo" width="50" align="top">
|
|
1
|
+
# <img src="./logo.png" alt="gas-fakes logo" width="50" align="top"> Run Native Apps Script code anywhere with gas-fakes
|
|
2
2
|
|
|
3
3
|
I use clasp/antigravity to develop Google Apps Script (GAS) applications, but when using GAS native services, there's way too much back and forwards to the GAS IDE going while testing. I set myself the ambition of implementing a fake version of the GAS runtime environment on Node so I could at least do some testing and debugging of Apps Scripts locally on Node.
|
|
4
4
|
|
|
@@ -170,6 +170,8 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
|
|
|
170
170
|
- [gas fakes cli](gas-fakes-cli.md)
|
|
171
171
|
- [running gas-fakes on google cloud run](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
172
172
|
- [running gas-fakes on google kubernetes engine](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
173
|
+
- [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
|
|
174
|
+
- [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
|
|
173
175
|
- [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
|
|
174
176
|
- [Inside the volatile world of a Google Document](https://ramblings.mcpher.com/inside-the-volatile-world-of-a-google-document/)
|
|
175
177
|
- [Apps Script Services on Node – using apps script libraries](https://ramblings.mcpher.com/apps-script-services-on-node-using-apps-script-libraries/)
|
package/package.json
CHANGED
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"zod": "^4.3.6"
|
|
26
26
|
},
|
|
27
27
|
"overrides": {
|
|
28
|
-
"minimatch": "^10.2.1"
|
|
28
|
+
"minimatch": "^10.2.1",
|
|
29
|
+
"glob": "^13.0.0"
|
|
29
30
|
},
|
|
30
31
|
"type": "module",
|
|
31
32
|
"scripts": {
|
|
@@ -36,7 +37,7 @@
|
|
|
36
37
|
},
|
|
37
38
|
"name": "@mcpher/gas-fakes",
|
|
38
39
|
"author": "bruce mcpherson",
|
|
39
|
-
"version": "2.0.
|
|
40
|
+
"version": "2.0.13",
|
|
40
41
|
"license": "MIT",
|
|
41
42
|
"main": "main.js",
|
|
42
43
|
"description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
|
|
@@ -64,6 +64,18 @@ export class FakePageElement {
|
|
|
64
64
|
throw new Error('PageElement is not a line.');
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Returns the page element as a table.
|
|
69
|
+
* @returns {FakeTable} The table.
|
|
70
|
+
*/
|
|
71
|
+
asTable() {
|
|
72
|
+
if (this.__resource.table) {
|
|
73
|
+
const { newFakeTable } = PageElementRegistry;
|
|
74
|
+
return newFakeTable(this.__resource, this.__page);
|
|
75
|
+
}
|
|
76
|
+
throw new Error('PageElement is not a table.');
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
/**
|
|
68
80
|
* Gets the type of the page element.
|
|
69
81
|
* @returns {SlidesApp.PageElementType} The type.
|
|
@@ -391,5 +403,6 @@ export class FakePageElement {
|
|
|
391
403
|
|
|
392
404
|
export const PageElementRegistry = {
|
|
393
405
|
newFakeShape: null,
|
|
394
|
-
newFakeLine: null
|
|
406
|
+
newFakeLine: null,
|
|
407
|
+
newFakeTable: null
|
|
395
408
|
};
|
|
@@ -167,6 +167,23 @@ export class FakeSlide {
|
|
|
167
167
|
return newElement.asShape();
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
/**
|
|
171
|
+
* Inserts a text box.
|
|
172
|
+
* @param {string} text The text to insert.
|
|
173
|
+
* @param {number} left The left position.
|
|
174
|
+
* @param {number} top The top position.
|
|
175
|
+
* @param {number} width The width.
|
|
176
|
+
* @param {number} height The height.
|
|
177
|
+
* @returns {FakeShape} The new text box.
|
|
178
|
+
*/
|
|
179
|
+
insertTextBox(text, left, top, width, height) {
|
|
180
|
+
const shape = this.insertShape('TEXT_BOX', left, top, width, height);
|
|
181
|
+
if (text) {
|
|
182
|
+
shape.getText().setText(text);
|
|
183
|
+
}
|
|
184
|
+
return shape;
|
|
185
|
+
}
|
|
186
|
+
|
|
170
187
|
/**
|
|
171
188
|
* Inserts a line.
|
|
172
189
|
* @param {SlidesApp.LineCategory} lineCategory The line category.
|
|
@@ -212,6 +229,105 @@ export class FakeSlide {
|
|
|
212
229
|
return newElement.asLine();
|
|
213
230
|
}
|
|
214
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Inserts a table.
|
|
234
|
+
* @param {number|FakeTable} rowsOrTable The number of rows or a table to copy.
|
|
235
|
+
* @param {number} [columns] The number of columns (if rowsOrTable is a number).
|
|
236
|
+
* @param {number} [left]
|
|
237
|
+
* @param {number} [top]
|
|
238
|
+
* @param {number} [width]
|
|
239
|
+
* @param {number} [height]
|
|
240
|
+
* @returns {FakeTable} The new table.
|
|
241
|
+
*/
|
|
242
|
+
insertTable(rowsOrTable, columns, left = 0, top = 0, width = 300, height = 300) {
|
|
243
|
+
const presentationId = this.__presentation.getId();
|
|
244
|
+
const objectId = `table_${Math.random().toString(36).substring(2, 11)}`;
|
|
245
|
+
let request = null;
|
|
246
|
+
|
|
247
|
+
if (typeof rowsOrTable === 'number') {
|
|
248
|
+
request = {
|
|
249
|
+
createTable: {
|
|
250
|
+
objectId,
|
|
251
|
+
rows: rowsOrTable,
|
|
252
|
+
columns: columns,
|
|
253
|
+
elementProperties: {
|
|
254
|
+
pageObjectId: this.getObjectId(),
|
|
255
|
+
size: {
|
|
256
|
+
width: { magnitude: width, unit: 'PT' },
|
|
257
|
+
height: { magnitude: height, unit: 'PT' }
|
|
258
|
+
},
|
|
259
|
+
transform: {
|
|
260
|
+
scaleX: 1,
|
|
261
|
+
scaleY: 1,
|
|
262
|
+
translateX: left,
|
|
263
|
+
translateY: top,
|
|
264
|
+
unit: 'PT'
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
} else {
|
|
270
|
+
// Copy table logic - use duplicateObject if it's the same presentation?
|
|
271
|
+
// Or manually create new table with same rows/cols.
|
|
272
|
+
// Slide.insertTable(Table) usually means copy.
|
|
273
|
+
const table = rowsOrTable;
|
|
274
|
+
request = {
|
|
275
|
+
createTable: {
|
|
276
|
+
objectId,
|
|
277
|
+
rows: table.getNumRows(),
|
|
278
|
+
columns: table.getNumColumns(),
|
|
279
|
+
elementProperties: {
|
|
280
|
+
pageObjectId: this.getObjectId(),
|
|
281
|
+
size: {
|
|
282
|
+
width: { magnitude: table.getWidth(), unit: 'PT' },
|
|
283
|
+
height: { magnitude: table.getHeight(), unit: 'PT' }
|
|
284
|
+
},
|
|
285
|
+
transform: {
|
|
286
|
+
scaleX: 1,
|
|
287
|
+
scaleY: 1,
|
|
288
|
+
translateX: table.getLeft(),
|
|
289
|
+
translateY: table.getTop(),
|
|
290
|
+
unit: 'PT'
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
try {
|
|
298
|
+
Slides.Presentations.batchUpdate([request], presentationId);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (!err?.message?.includes('already exists')) throw err;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const elements = this.getPageElements();
|
|
304
|
+
const newElement = elements.find(e => e.getObjectId() === objectId);
|
|
305
|
+
if (!newElement) throw new Error('New table not found');
|
|
306
|
+
|
|
307
|
+
const newTable = newElement.asTable();
|
|
308
|
+
|
|
309
|
+
// If copying, we should probably copy cell contents too.
|
|
310
|
+
if (typeof rowsOrTable !== 'number') {
|
|
311
|
+
const sourceTable = rowsOrTable;
|
|
312
|
+
const targetTable = newTable;
|
|
313
|
+
const rows = sourceTable.getRows();
|
|
314
|
+
const targetRows = targetTable.getRows();
|
|
315
|
+
|
|
316
|
+
for (let r = 0; r < rows.length; r++) {
|
|
317
|
+
const cells = rows[r].getCells();
|
|
318
|
+
const targetCells = targetRows[r].getCells();
|
|
319
|
+
for (let c = 0; c < cells.length; c++) {
|
|
320
|
+
const text = cells[c].getText().asString();
|
|
321
|
+
if (text) {
|
|
322
|
+
targetCells[c].getText().setText(text);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return newTable;
|
|
329
|
+
}
|
|
330
|
+
|
|
215
331
|
duplicate() {
|
|
216
332
|
const presentationId = this.__presentation.getId();
|
|
217
333
|
const objectId = `slide_${Math.random().toString(36).substring(2, 11)}`;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { FakePageElement, PageElementRegistry } from './fakepageelement.js';
|
|
3
|
+
import { newFakeTableRow } from './faketablerow.js';
|
|
4
|
+
|
|
5
|
+
export const newFakeTable = (...args) => {
|
|
6
|
+
return Proxies.guard(new FakeTable(...args));
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
PageElementRegistry.newFakeTable = newFakeTable;
|
|
10
|
+
|
|
11
|
+
export class FakeTable extends FakePageElement {
|
|
12
|
+
constructor(resource, page) {
|
|
13
|
+
super(resource, page);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get __presentation() {
|
|
17
|
+
return this.__page.__presentation || this.__page.__slide?.__presentation;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Gets the rows in the table.
|
|
22
|
+
* @returns {FakeTableRow[]} The rows.
|
|
23
|
+
*/
|
|
24
|
+
getRows() {
|
|
25
|
+
return (this.__resource.table?.tableRows || []).map((row, index) =>
|
|
26
|
+
newFakeTableRow(row, this, index)
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Gets a row by its index.
|
|
32
|
+
* @param {number} index The row index.
|
|
33
|
+
* @returns {FakeTableRow} The row.
|
|
34
|
+
*/
|
|
35
|
+
getRow(index) {
|
|
36
|
+
const rows = this.getRows();
|
|
37
|
+
if (index < 0 || index >= rows.length) {
|
|
38
|
+
throw new Error(`Row index ${index} out of bounds`);
|
|
39
|
+
}
|
|
40
|
+
return rows[index];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets the number of rows in the table.
|
|
45
|
+
* @returns {number} The number of rows.
|
|
46
|
+
*/
|
|
47
|
+
getNumRows() {
|
|
48
|
+
return (this.__resource.table?.tableRows || []).length;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Gets the number of columns in the table.
|
|
53
|
+
* @returns {number} The number of columns.
|
|
54
|
+
*/
|
|
55
|
+
getNumColumns() {
|
|
56
|
+
const rows = this.__resource.table?.tableRows || [];
|
|
57
|
+
return rows.length > 0 ? (rows[0].tableCells || []).length : 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
toString() {
|
|
61
|
+
return 'Table';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeTextRange } from './faketextrange.js';
|
|
3
|
+
|
|
4
|
+
export const newFakeTableCell = (...args) => {
|
|
5
|
+
return Proxies.guard(new FakeTableCell(...args));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class FakeTableCell {
|
|
9
|
+
constructor(resource, table, rowIndex, colIndex) {
|
|
10
|
+
this.__resource = resource;
|
|
11
|
+
this.__table = table;
|
|
12
|
+
this.__rowIndex = rowIndex;
|
|
13
|
+
this.__colIndex = colIndex;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Gets the text in the cell.
|
|
18
|
+
* @returns {FakeTextRange} The text range.
|
|
19
|
+
*/
|
|
20
|
+
getText() {
|
|
21
|
+
// FakeTableCell in Slides API doesn't have a direct shape resource to pass to TextRange?
|
|
22
|
+
// Actually, TableCell has text property in Slides API.
|
|
23
|
+
// Wait, let's check Slides API TableCell resource.
|
|
24
|
+
// It has `text` field of type `TextContent`.
|
|
25
|
+
// FakeTextRange expects a `shape` with `__resource.shape.text`.
|
|
26
|
+
// We might need to adapt FakeTextRange or mock a shape-like object.
|
|
27
|
+
|
|
28
|
+
// Let's create a proxy for shape so FakeTextRange can work.
|
|
29
|
+
const mockShape = {
|
|
30
|
+
getObjectId: () => this.__table.getObjectId(),
|
|
31
|
+
__resource: {
|
|
32
|
+
shape: {
|
|
33
|
+
text: this.__resource.text || { textElements: [] }
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
__cellLocation: {
|
|
37
|
+
rowIndex: this.__rowIndex,
|
|
38
|
+
columnIndex: this.__colIndex
|
|
39
|
+
},
|
|
40
|
+
__presentation: this.__table.__presentation
|
|
41
|
+
};
|
|
42
|
+
if (!this.__resource.text) {
|
|
43
|
+
this.__resource.text = mockShape.__resource.shape.text;
|
|
44
|
+
}
|
|
45
|
+
return newFakeTextRange(mockShape);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
toString() {
|
|
49
|
+
return 'TableCell';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeTableCell } from './faketablecell.js';
|
|
3
|
+
|
|
4
|
+
export const newFakeTableRow = (...args) => {
|
|
5
|
+
return Proxies.guard(new FakeTableRow(...args));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class FakeTableRow {
|
|
9
|
+
constructor(resource, table, rowIndex) {
|
|
10
|
+
this.__resource = resource;
|
|
11
|
+
this.__table = table;
|
|
12
|
+
this.__rowIndex = rowIndex;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Gets the cells in the row.
|
|
17
|
+
* @returns {FakeTableCell[]} The cells.
|
|
18
|
+
*/
|
|
19
|
+
getCells() {
|
|
20
|
+
return (this.__resource.tableCells || []).map((cell, colIndex) =>
|
|
21
|
+
newFakeTableCell(cell, this.__table, this.__rowIndex, colIndex)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gets a cell by its index.
|
|
27
|
+
* @param {number} index The cell index.
|
|
28
|
+
* @returns {FakeTableCell} The cell.
|
|
29
|
+
*/
|
|
30
|
+
getCell(index) {
|
|
31
|
+
const cells = this.getCells();
|
|
32
|
+
if (index < 0 || index >= cells.length) {
|
|
33
|
+
throw new Error(`Cell index ${index} out of bounds`);
|
|
34
|
+
}
|
|
35
|
+
return cells[index];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the number of cells in the row.
|
|
40
|
+
* @returns {number} The number of cells.
|
|
41
|
+
*/
|
|
42
|
+
getNumCells() {
|
|
43
|
+
return (this.__resource.tableCells || []).length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
toString() {
|
|
47
|
+
return 'TableRow';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -83,6 +83,7 @@ export class FakeTextRange {
|
|
|
83
83
|
requests.push({
|
|
84
84
|
deleteText: {
|
|
85
85
|
objectId: objectId,
|
|
86
|
+
cellLocation: this.__shape.__cellLocation,
|
|
86
87
|
textRange: {
|
|
87
88
|
type: 'FROM_START_INDEX',
|
|
88
89
|
startIndex: 0
|
|
@@ -104,6 +105,7 @@ export class FakeTextRange {
|
|
|
104
105
|
requests.push({
|
|
105
106
|
insertText: {
|
|
106
107
|
objectId: objectId,
|
|
108
|
+
cellLocation: this.__shape.__cellLocation,
|
|
107
109
|
insertionIndex: 0,
|
|
108
110
|
text: newText
|
|
109
111
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { newFakeShape } from './fakeshape.js';
|
|
2
2
|
import { newFakeLine } from './fakeline.js';
|
|
3
|
+
import { newFakeTable } from './faketable.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Converts a base PageElement to a more specific subclass (Shape, Line, etc.)
|
|
@@ -14,6 +15,9 @@ export const asSpecificPageElement = (pageElement) => {
|
|
|
14
15
|
if (resource.line) {
|
|
15
16
|
return newFakeLine(resource, pageElement.__page);
|
|
16
17
|
}
|
|
18
|
+
if (resource.table) {
|
|
19
|
+
return newFakeTable(resource, pageElement.__page);
|
|
20
|
+
}
|
|
17
21
|
// Add other types as they are implemented
|
|
18
22
|
return pageElement;
|
|
19
23
|
};
|