@mcpher/gas-fakes 1.0.19 → 1.0.21

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 (72) hide show
  1. package/README.md +63 -30
  2. package/gasmess/bruce/pbx.js +53 -2
  3. package/gprompts/gas-inventory.js +92 -0
  4. package/gprompts/gas-inventory.json +176 -0
  5. package/gprompts/inventory-list.json +1 -0
  6. package/gprompts/model.json +34 -0
  7. package/gprompts/package-lock.json +1103 -0
  8. package/gprompts/package.json +18 -0
  9. package/gprompts/temp_fetch.mjs +9 -0
  10. package/gprompts/update-progress.js +142 -0
  11. package/package.json +6 -2
  12. package/setup.sh +147 -0
  13. package/src/index.js +9 -2
  14. package/src/services/advdocs/app.js +4 -23
  15. package/src/services/advdrive/app.js +6 -28
  16. package/src/services/advforms/app.js +6 -25
  17. package/src/services/advgmail/app.js +11 -0
  18. package/src/services/advgmail/fakeadvgmail.js +39 -0
  19. package/src/services/advgmail/fakeadvgmaillabels.js +119 -0
  20. package/src/services/advgmail/fakeadvgmailusers.js +23 -0
  21. package/src/services/advgmail/gmailapis.js +15 -0
  22. package/src/services/advsheets/app.js +6 -26
  23. package/src/services/advslides/app.js +6 -28
  24. package/src/services/common/lazyloader.js +22 -0
  25. package/src/services/documentapp/app.js +8 -42
  26. package/src/services/documentapp/appenderhelpers.js +21 -23
  27. package/src/services/documentapp/elementhelpers.js +0 -1
  28. package/src/services/documentapp/elementoptions.js +22 -32
  29. package/src/services/documentapp/fakeelement.js +20 -3
  30. package/src/services/documentapp/fakelistitem.js +177 -28
  31. package/src/services/documentapp/fakeparagraph.js +194 -7
  32. package/src/services/documentapp/faketable.js +16 -0
  33. package/src/services/documentapp/faketablerow.js +15 -0
  34. package/src/services/documentapp/nrhelpers.js +1 -0
  35. package/src/services/documentapp/shadowdocument.js +10 -0
  36. package/src/services/driveapp/app.js +6 -28
  37. package/src/services/enums/gmailenums.js +8 -0
  38. package/src/services/formapp/app.js +5 -40
  39. package/src/services/gmailapp/app.js +11 -0
  40. package/src/services/gmailapp/fakegmailapp.js +35 -0
  41. package/src/services/gmailapp/fakegmaillabel.js +44 -0
  42. package/src/services/logger/app.js +8 -0
  43. package/src/services/logger/fakelogger.js +162 -0
  44. package/src/services/scriptapp/app.js +7 -1
  45. package/src/services/scriptapp/behavior.js +1 -1
  46. package/src/services/session/app.js +10 -0
  47. package/src/services/slidesapp/app.js +5 -40
  48. package/src/services/spreadsheetapp/app.js +6 -50
  49. package/src/services/spreadsheetapp/fakeprotection.js +6 -7
  50. package/src/services/spreadsheetapp/fakesheet.js +3 -4
  51. package/src/services/stores/app.js +0 -1
  52. package/src/services/urlfetchapp/app.js +0 -1
  53. package/src/services/utilities/app.js +6 -20
  54. package/src/support/gmailcacher.js +7 -0
  55. package/src/support/helpers.js +2 -2
  56. package/src/support/proxies.js +20 -1
  57. package/src/support/sxgmail.js +55 -0
  58. package/src/support/syncit.js +5 -2
  59. package/src/support/utils.js +46 -15
  60. package/src/support/workersync/sxfunctions.js +5 -10
  61. package/togas.bash +18 -5
  62. package/ghissues/image-size-inconsistency-issue.sh +0 -46
  63. package/ghissues/issue-formapp-create-title-inconsistency.sh +0 -51
  64. package/ghissues/issue-positioned-image.sh +0 -25
  65. package/ghissues/post-issue.sh +0 -53
  66. package/ghissues/protection-editors-issue.sh +0 -33
  67. package/ghissues/review-sandbox-listing-issue.sh +0 -45
  68. package/ghissues/sandbox-issue.sh +0 -31
  69. package/ghissues/setup-under-construction.sh +0 -107
  70. package/src/services/base/app.js +0 -33
  71. /package/{regenerate-progress-reports.sh → gprompts/regenerate-progress-reports.sh} +0 -0
  72. /package/src/services/{base → session}/fakesession.js +0 -0
@@ -2,13 +2,13 @@
2
2
  * @file Provides a fake implementation of the ListItem class.
3
3
  */
4
4
  import { Proxies } from '../../support/proxies.js';
5
- import { FakeContainerElement } from './fakecontainerelement.js';
5
+ import { FakeParagraph } from './fakeparagraph.js';
6
6
  import { registerElement } from './elementRegistry.js';
7
- import { getText, updateParagraphStyle, getAttributes as getAttributesHelper } from './elementhelpers.js';
8
- import { appendText } from './appenderhelpers.js';
7
+ import { getText, updateParagraphStyle, getAttributes as getAttributesHelper, findItem } from './elementhelpers.js';
8
+ import { appendText, insertImage } from './appenderhelpers.js';
9
9
  import { notYetImplemented, signatureArgs } from '../../support/helpers.js';
10
10
  import { Utils } from '../../support/utils.js';
11
- const { is } = Utils;
11
+ const { is, lobify } = Utils;
12
12
 
13
13
  /**
14
14
  * Creates a new proxied FakeListItem instance.
@@ -22,11 +22,11 @@ export const newFakeListItem = (...args) => {
22
22
  /**
23
23
  * A fake implementation of the ListItem class for DocumentApp.
24
24
  * @class FakeListItem
25
- * @extends {FakeContainerElement}
25
+ * @extends {FakeParagraph}
26
26
  * @implements {GoogleAppsScript.Document.ListItem}
27
27
  * @see https://developers.google.com/apps-script/reference/document/list-item
28
28
  */
29
- export class FakeListItem extends FakeContainerElement {
29
+ export class FakeListItem extends FakeParagraph {
30
30
  /**
31
31
  * @param {import('./shadowdocument.js').ShadowDocument} shadowDocument The shadow document manager.
32
32
  * @param {string|object} nameOrItem The name of the element or the element's API resource.
@@ -36,25 +36,6 @@ export class FakeListItem extends FakeContainerElement {
36
36
  super(shadowDocument, nameOrItem);
37
37
  }
38
38
 
39
- /**
40
- * Gets the text content of the list item, flattening all child text elements.
41
- * @returns {string} The text content.
42
- * @see https://developers.google.com/apps-script/reference/document/list-item#getText()
43
- */
44
- getText() {
45
- return getText(this);
46
- }
47
-
48
- /**
49
- * Appends text to the list item.
50
- * @param {string|GoogleAppsScript.Document.Text} textOrTextElement The text to append.
51
- * @returns {GoogleAppsScript.Document.ListItem} The list item, for chaining.
52
- */
53
- appendText(textOrTextElement) {
54
- appendText(this, textOrTextElement);
55
- return this;
56
- }
57
-
58
39
  /**
59
40
  * Gets a copy of the element's attributes.
60
41
  * @returns {object} The attributes.
@@ -77,12 +58,12 @@ export class FakeListItem extends FakeContainerElement {
77
58
  const nestingLevel = this.getNestingLevel();
78
59
  // The shadowDocument is only available on attached elements.
79
60
  if (this.__isDetached) return null;
80
- const docResource = this.shadowDocument.resource;
61
+ const docResource = this.shadowDocument.resource;
81
62
  // The lists can be at the top level or inside the first tab.
82
- const {lists} = this.shadowDocument.__unpackDocumentTab(docResource);
63
+ const { lists } = this.shadowDocument.__unpackDocumentTab(docResource);
83
64
  // Find the list by ID.
84
65
  const list = lists?.[listId];
85
- if (!list) return null;
66
+ if (!list) return null;
86
67
 
87
68
  const levelProps = list.listProperties?.nestingLevels?.[nestingLevel];
88
69
  // For a default list, the API might not return explicit level properties.
@@ -198,6 +179,107 @@ export class FakeListItem extends FakeContainerElement {
198
179
  return this;
199
180
  }
200
181
 
182
+ getAlignment() {
183
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.getAlignment');
184
+ if (nargs !== 0) matchThrow();
185
+ return this.getAttributes()[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT];
186
+ }
187
+
188
+ setAlignment(alignment) {
189
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.setAlignment');
190
+ if (nargs !== 1) matchThrow();
191
+
192
+ const alignmentMap = {
193
+ [DocumentApp.HorizontalAlignment.LEFT]: 'START',
194
+ [DocumentApp.HorizontalAlignment.CENTER]: 'CENTER',
195
+ [DocumentApp.HorizontalAlignment.RIGHT]: 'END',
196
+ [DocumentApp.HorizontalAlignment.JUSTIFIED]: 'JUSTIFY',
197
+ };
198
+
199
+ const apiAlignment = alignmentMap[alignment];
200
+ if (!apiAlignment) {
201
+ throw new Error(`Invalid argument: ${alignment}`);
202
+ }
203
+
204
+ return updateParagraphStyle(this, { alignment: apiAlignment }, 'alignment');
205
+ }
206
+
207
+ getIndentEnd() {
208
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.getIndentEnd');
209
+ if (nargs !== 0) matchThrow();
210
+ return this.getAttributes()[DocumentApp.Attribute.INDENT_END];
211
+ }
212
+
213
+ setIndentEnd(indentEnd) {
214
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.setIndentEnd');
215
+ if (nargs !== 1 || !is.number(indentEnd)) matchThrow();
216
+ return updateParagraphStyle(this, { indentEnd: { magnitude: indentEnd, unit: 'PT' } }, 'indentEnd');
217
+ }
218
+
219
+ getIndentFirstLine() {
220
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.getIndentFirstLine');
221
+ if (nargs !== 0) matchThrow();
222
+ return this.getAttributes()[DocumentApp.Attribute.INDENT_FIRST_LINE];
223
+ }
224
+
225
+ getIndentStart() {
226
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.getIndentStart');
227
+ if (nargs !== 0) matchThrow();
228
+ return this.getAttributes()[DocumentApp.Attribute.INDENT_START];
229
+ }
230
+
231
+ getLineSpacing() {
232
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.getLineSpacing');
233
+ if (nargs !== 0) matchThrow();
234
+ return this.getAttributes()[DocumentApp.Attribute.LINE_SPACING];
235
+ }
236
+
237
+ setLineSpacing(multiplier) {
238
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.setLineSpacing');
239
+ if (nargs !== 1 || !is.number(multiplier)) matchThrow();
240
+ return updateParagraphStyle(this, { lineSpacing: multiplier * 100 }, 'lineSpacing');
241
+ }
242
+
243
+ getSpacingAfter() {
244
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.getSpacingAfter');
245
+ if (nargs !== 0) matchThrow();
246
+ return this.getAttributes()[DocumentApp.Attribute.SPACING_AFTER];
247
+ }
248
+
249
+ setSpacingAfter(spacingAfter) {
250
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.setSpacingAfter');
251
+ if (nargs !== 1 || !is.number(spacingAfter)) matchThrow();
252
+ return updateParagraphStyle(this, { spaceBelow: { magnitude: spacingAfter, unit: 'PT' } }, 'spaceBelow');
253
+ }
254
+
255
+ getSpacingBefore() {
256
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.getSpacingBefore');
257
+ if (nargs !== 0) matchThrow();
258
+ return this.getAttributes()[DocumentApp.Attribute.SPACING_BEFORE];
259
+ }
260
+
261
+ setSpacingBefore(spacingBefore) {
262
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.setSpacingBefore');
263
+ if (nargs !== 1 || !is.number(spacingBefore)) matchThrow();
264
+ return updateParagraphStyle(this, { spaceAbove: { magnitude: spacingBefore, unit: 'PT' } }, 'spaceAbove');
265
+ }
266
+
267
+ isLeftToRight() {
268
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.isLeftToRight');
269
+ if (nargs !== 0) matchThrow();
270
+ // The getAttributes helper will return true/false/null.
271
+ const ltr = this.getAttributes()[DocumentApp.Attribute.LEFT_TO_RIGHT];
272
+ // Default to true if null, which matches live behavior for new paragraphs.
273
+ return ltr === null ? true : ltr;
274
+ }
275
+
276
+ setLeftToRight(leftToRight) {
277
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.setLeftToRight');
278
+ if (nargs !== 1 || !is.boolean(leftToRight)) matchThrow();
279
+ const direction = leftToRight ? 'LEFT_TO_RIGHT' : 'RIGHT_TO_LEFT';
280
+ return updateParagraphStyle(this, { direction }, 'direction');
281
+ }
282
+
201
283
 
202
284
  /**
203
285
  * Sets the glyph type of the list item.
@@ -236,6 +318,73 @@ export class FakeListItem extends FakeContainerElement {
236
318
  return updateParagraphStyle(this, { indentStart: { magnitude: indentStart, unit: 'PT' } }, 'indentStart');
237
319
  }
238
320
 
321
+ clear() {
322
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.clear');
323
+ if (nargs !== 0) matchThrow();
324
+
325
+ const item = this.__elementMapItem;
326
+ const requests = [];
327
+
328
+ // Find the named range associated with this element to protect it.
329
+ const nr = this.shadowDocument.getNamedRange(item.__name);
330
+ if (!nr) {
331
+ throw new Error(`Internal error: Could not find named range for element ${item.__name}`);
332
+ }
333
+
334
+ // Delete the content, but not the final newline that defines the paragraph.
335
+ if (item.endIndex - 1 > item.startIndex) {
336
+ requests.push({
337
+ deleteContentRange: {
338
+ range: { startIndex: item.startIndex, endIndex: item.endIndex - 1, segmentId: this.__segmentId }
339
+ }
340
+ });
341
+
342
+ // Protect the named range by deleting the old one and recreating it with the same name but the new, smaller range.
343
+ requests.push({ deleteNamedRange: { namedRangeId: nr.namedRangeId } });
344
+ requests.push({
345
+ createNamedRange: { name: nr.name, range: { startIndex: item.startIndex, endIndex: item.startIndex + 1, segmentId: this.__segmentId } },
346
+ });
347
+
348
+ Docs.Documents.batchUpdate({ requests }, this.shadowDocument.getId());
349
+ this.shadowDocument.refresh();
350
+ }
351
+ return this;
352
+ }
353
+
354
+ setText(text) {
355
+ const { nargs, matchThrow } = signatureArgs(arguments, 'ListItem.setText');
356
+ if (nargs !== 1 || !is.string(text)) matchThrow();
357
+
358
+ const item = this.__elementMapItem;
359
+ const requests = [];
360
+
361
+ // Find the named range associated with this element to protect it.
362
+ const nr = this.shadowDocument.getNamedRange(item.__name);
363
+ if (!nr) {
364
+ throw new Error(`Internal error: Could not find named range for element ${item.__name}`);
365
+ }
366
+
367
+ if (item.endIndex - 1 > item.startIndex) {
368
+ requests.push({ deleteContentRange: { range: { startIndex: item.startIndex, endIndex: item.endIndex - 1, segmentId: this.__segmentId } } });
369
+ }
370
+ if (text) {
371
+ requests.push({ insertText: { location: { index: item.startIndex, segmentId: this.__segmentId }, text } });
372
+ }
373
+
374
+ // As you correctly pointed out, we must protect the named range.
375
+ // Delete the old one and recreate it with the same name but the new, updated range.
376
+ requests.push({ deleteNamedRange: { namedRangeId: nr.namedRangeId } });
377
+ requests.push({
378
+ createNamedRange: {
379
+ name: nr.name,
380
+ range: { startIndex: item.startIndex, endIndex: item.startIndex + (text ? text.length : 0) + 1, segmentId: this.__segmentId },
381
+ },
382
+ });
383
+
384
+ Docs.Documents.batchUpdate({ requests }, this.shadowDocument.getId());
385
+ this.shadowDocument.refresh();
386
+ }
387
+
239
388
  /**
240
389
  * Sets the nesting level of the list item.
241
390
  * @param {number} nestingLevel The new nesting level, starting from 0.
@@ -1,17 +1,18 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
2
  import { signatureArgs } from '../../support/helpers.js';
3
3
  import { Utils } from '../../support/utils.js';
4
+ import { imageOptions } from './elementoptions.js';
4
5
  import { FakeContainerElement } from './fakecontainerelement.js';
5
6
  import { registerElement } from './elementRegistry.js';
6
7
  import { appendText, addPositionedImage, appendImage, insertImage } from './appenderhelpers.js';
7
- import { getText as getTextHelper, getAttributes as getAttributesHelper } from './elementhelpers.js';
8
+ import { getText as getTextHelper, getAttributes as getAttributesHelper, updateParagraphStyle } from './elementhelpers.js';
8
9
 
9
10
  const { is } = Utils;
10
11
 
11
12
  /**
12
13
  * @implements {GoogleAppsScript.Document.Paragraph}
13
14
  */
14
- class FakeParagraph extends FakeContainerElement {
15
+ export class FakeParagraph extends FakeContainerElement {
15
16
  constructor(shadowDocument, name) {
16
17
  const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph');
17
18
  // An attached element has a shadowDocument and a string name (its ID).
@@ -52,15 +53,32 @@ class FakeParagraph extends FakeContainerElement {
52
53
  appendInlineImage(image) {
53
54
  const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.appendInlineImage');
54
55
  if (nargs !== 1) matchThrow();
55
- // This uses the generic appendImage helper, which is designed to handle insertions into paragraphs.
56
- return appendImage(this, image);
56
+
57
+ // The generic appendImage helper creates a new paragraph. We need to insert into the existing one.
58
+ // We can use the insertImage helper, which inserts at a specific child index.
59
+ return this.insertInlineImage(this.getNumChildren(), image);
57
60
  }
58
61
 
59
62
  insertInlineImage(childIndex, image) {
60
63
  const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.insertInlineImage');
61
64
  if (nargs !== 2) matchThrow();
62
- // This uses the generic insertImage helper.
63
- return insertImage(this, childIndex, image);
65
+
66
+ // The generic insertImage helper is for top-level containers. We need a specific implementation.
67
+ const children = [];
68
+ for (let i = 0; i < this.getNumChildren(); i++) {
69
+ children.push(this.getChild(i));
70
+ }
71
+
72
+ if (childIndex < 0 || childIndex > children.length) {
73
+ throw new Error(`Child index (${childIndex}) must be less than or equal to the number of child elements (${children.length}).`);
74
+ }
75
+
76
+ // Determine the character index for insertion.
77
+ const insertIndex = (childIndex === children.length)
78
+ ? this.__elementMapItem.endIndex - 1 // Append before the final newline
79
+ : children[childIndex].__elementMapItem.startIndex; // Insert before the specified child
80
+
81
+ return this.__insertImageAtIndex(image, insertIndex);
64
82
  }
65
83
 
66
84
  setHeading(heading) {
@@ -131,12 +149,181 @@ class FakeParagraph extends FakeContainerElement {
131
149
  return apiToEnumMap[namedStyleType] || DocumentApp.ParagraphHeading.NORMAL;
132
150
  }
133
151
 
152
+ getAlignment() {
153
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.getAlignment');
154
+ if (nargs !== 0) matchThrow();
155
+ return this.getAttributes()[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT];
156
+ }
157
+
158
+ setAlignment(alignment) {
159
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setAlignment');
160
+ if (nargs !== 1) matchThrow();
161
+
162
+ const alignmentMap = {
163
+ [DocumentApp.HorizontalAlignment.LEFT]: 'START',
164
+ [DocumentApp.HorizontalAlignment.CENTER]: 'CENTER',
165
+ [DocumentApp.HorizontalAlignment.RIGHT]: 'END',
166
+ [DocumentApp.HorizontalAlignment.JUSTIFIED]: 'JUSTIFY',
167
+ };
168
+
169
+ const apiAlignment = alignmentMap[alignment];
170
+ if (!apiAlignment) {
171
+ throw new Error(`Invalid argument: ${alignment}`);
172
+ }
173
+
174
+ return updateParagraphStyle(this, { alignment: apiAlignment }, 'alignment');
175
+ }
176
+
177
+ getIndentEnd() {
178
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.getIndentEnd');
179
+ if (nargs !== 0) matchThrow();
180
+ return this.getAttributes()[DocumentApp.Attribute.INDENT_END];
181
+ }
182
+
183
+ setIndentEnd(indentEnd) {
184
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setIndentEnd');
185
+ if (nargs !== 1 || !is.number(indentEnd)) matchThrow();
186
+ return updateParagraphStyle(this, { indentEnd: { magnitude: indentEnd, unit: 'PT' } }, 'indentEnd');
187
+ }
188
+
189
+ getIndentFirstLine() {
190
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.getIndentFirstLine');
191
+ if (nargs !== 0) matchThrow();
192
+ return this.getAttributes()[DocumentApp.Attribute.INDENT_FIRST_LINE];
193
+ }
194
+
195
+ setIndentFirstLine(indentFirstLine) {
196
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setIndentFirstLine');
197
+ if (nargs !== 1 || !is.number(indentFirstLine)) matchThrow();
198
+ return updateParagraphStyle(this, { indentFirstLine: { magnitude: indentFirstLine, unit: 'PT' } }, 'indentFirstLine');
199
+ }
200
+
201
+ getIndentStart() {
202
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.getIndentStart');
203
+ if (nargs !== 0) matchThrow();
204
+ return this.getAttributes()[DocumentApp.Attribute.INDENT_START];
205
+ }
206
+
207
+ setIndentStart(indentStart) {
208
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setIndentStart');
209
+ if (nargs !== 1 || !is.number(indentStart)) matchThrow();
210
+ return updateParagraphStyle(this, { indentStart: { magnitude: indentStart, unit: 'PT' } }, 'indentStart');
211
+ }
212
+
213
+ getLineSpacing() {
214
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.getLineSpacing');
215
+ if (nargs !== 0) matchThrow();
216
+ return this.getAttributes()[DocumentApp.Attribute.LINE_SPACING];
217
+ }
218
+
219
+ setLineSpacing(multiplier) {
220
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setLineSpacing');
221
+ if (nargs !== 1 || !is.number(multiplier)) matchThrow();
222
+ return updateParagraphStyle(this, { lineSpacing: multiplier * 100 }, 'lineSpacing');
223
+ }
224
+
225
+ getSpacingAfter() {
226
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.getSpacingAfter');
227
+ if (nargs !== 0) matchThrow();
228
+ return this.getAttributes()[DocumentApp.Attribute.SPACING_AFTER];
229
+ }
230
+
231
+ setSpacingAfter(spacingAfter) {
232
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setSpacingAfter');
233
+ if (nargs !== 1 || !is.number(spacingAfter)) matchThrow();
234
+ return updateParagraphStyle(this, { spaceBelow: { magnitude: spacingAfter, unit: 'PT' } }, 'spaceBelow');
235
+ }
236
+
237
+ getSpacingBefore() {
238
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.getSpacingBefore');
239
+ if (nargs !== 0) matchThrow();
240
+ return this.getAttributes()[DocumentApp.Attribute.SPACING_BEFORE];
241
+ }
242
+
243
+ setSpacingBefore(spacingBefore) {
244
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setSpacingBefore');
245
+ if (nargs !== 1 || !is.number(spacingBefore)) matchThrow();
246
+ return updateParagraphStyle(this, { spaceAbove: { magnitude: spacingBefore, unit: 'PT' } }, 'spaceAbove');
247
+ }
248
+
249
+ isLeftToRight() {
250
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.isLeftToRight');
251
+ if (nargs !== 0) matchThrow();
252
+ // The getAttributes helper will return true/false/null.
253
+ const ltr = this.getAttributes()[DocumentApp.Attribute.LEFT_TO_RIGHT];
254
+ // Default to true if null, which matches live behavior for new paragraphs.
255
+ return ltr === null ? true : ltr;
256
+ }
257
+
258
+ setLeftToRight(leftToRight) {
259
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.setLeftToRight');
260
+ if (nargs !== 1 || !is.boolean(leftToRight)) matchThrow();
261
+ const direction = leftToRight ? 'LEFT_TO_RIGHT' : 'RIGHT_TO_LEFT';
262
+ return updateParagraphStyle(this, { direction }, 'direction');
263
+ }
264
+
134
265
  toString() {
135
266
  return 'Paragraph';
136
267
  }
268
+
269
+ /**
270
+ * Helper to insert an image at a specific character index.
271
+ * @param {GoogleAppsScript.Base.BlobSource} image The image to insert.
272
+ * @param {number} index The character index.
273
+ * @returns {GoogleAppsScript.Document.InlineImage} The new image.
274
+ * @private
275
+ */
276
+ __insertImageAtIndex(image, index) {
277
+ // As you correctly pointed out, we must protect the named range of the parent paragraph.
278
+ const item = this.__elementMapItem;
279
+ const nr = this.shadowDocument.getNamedRange(item.__name);
280
+ if (!nr) {
281
+ throw new Error(`Internal error: Could not find named range for element ${item.__name}`);
282
+ }
283
+ // This is a simplified and corrected version of the logic in imageOptions.getMainRequest.
284
+ // It directly creates the insertInlineImage request without the complex logic
285
+ // for adding new paragraphs, which was causing the empty request array.
286
+ const { uri, size, cleanup } = imageOptions.__getImageUriAndSize(image);
287
+
288
+ const requests = [{
289
+ insertInlineImage: {
290
+ uri,
291
+ location: { index, segmentId: this.__segmentId },
292
+ objectSize: size,
293
+ },
294
+ }];
295
+
296
+ // Add the protection requests to the batch update.
297
+ // This tells the API to preserve the identity of the paragraph being modified.
298
+ requests.push({ deleteNamedRange: { namedRangeId: nr.namedRangeId } });
299
+ requests.push({
300
+ createNamedRange: {
301
+ name: nr.name,
302
+ // The new range will be expanded by 1 character for the inserted image.
303
+ range: { startIndex: item.startIndex, endIndex: item.endIndex + 1, segmentId: this.__segmentId },
304
+ },
305
+ });
306
+ try {
307
+ Docs.Documents.batchUpdate({ requests }, this.shadowDocument.getId());
308
+ this.shadowDocument.refresh();
309
+ // After refresh, find the newly created image element to return it.
310
+ // This is a simplification; a more robust solution would find the image
311
+ // based on its new objectId from the batchUpdate response.
312
+ const children = [];
313
+ for (let i = 0; i < this.getNumChildren(); i++) {
314
+ children.push(this.getChild(i));
315
+ }
316
+ // The source 'image' can be a Blob or a detached InlineImage.
317
+ // We need to get the name from the underlying blob in either case.
318
+ const sourceImageName = image.getName ? image.getName() : image.getBlob().getName();
319
+ return children.find(c => c.getType() === DocumentApp.ElementType.INLINE_IMAGE && c.getBlob().getName() === sourceImageName);
320
+ } finally {
321
+ if (cleanup) cleanup();
322
+ }
323
+ }
137
324
  }
138
325
 
139
326
  export const newFakeParagraph = (...args) => Proxies.guard(new FakeParagraph(...args));
140
327
 
141
328
  registerElement('PARAGRAPH', newFakeParagraph);
142
- registerElement('LIST_ITEM', newFakeParagraph); // ListItems are also paragraphs
329
+ registerElement('LIST_ITEM', newFakeParagraph); // ListItems are also paragraphs
@@ -5,6 +5,7 @@
5
5
  import { Proxies } from '../../support/proxies.js';
6
6
  import { FakeContainerElement } from './fakecontainerelement.js';
7
7
  import { registerElement } from './elementRegistry.js';
8
+ import { newFakeElement } from './fakeelement.js';
8
9
  import { signatureArgs } from '../../support/helpers.js';
9
10
  import { Utils } from '../../support/utils.js';
10
11
  const { is } = Utils;
@@ -45,6 +46,21 @@ export class FakeTable extends FakeContainerElement {
45
46
  return this.getChild(rowIndex);
46
47
  }
47
48
 
49
+ /**
50
+ * Gets the text content of the table.
51
+ * @returns {string} The text content.
52
+ * @see https://developers.google.com/apps-script/reference/document/table#getText()
53
+ */
54
+ getText() {
55
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Table.getText');
56
+ if (nargs !== 0) matchThrow();
57
+ // Live behavior: A Table's text is the concatenation of its non-empty rows' text, separated by newlines.
58
+ return this.__children
59
+ .map(childTwig => newFakeElement(this.shadowDocument, childTwig.name).__cast().getText())
60
+ .filter(text => text.length > 0) // Ignore empty rows
61
+ .join('\n');
62
+ }
63
+
48
64
  getValues() {
49
65
  return Array.from({ length: this.getNumRows() }, (_, r) => {
50
66
  const row = this.getRow(r);
@@ -2,6 +2,8 @@
2
2
  * @file Provides a fake implementation of the TableRow class.
3
3
  */
4
4
  import { registerElement } from './elementRegistry.js';
5
+ import { newFakeElement } from './fakeelement.js';
6
+ import { signatureArgs } from '../../support/helpers.js';
5
7
  import { Proxies } from '../../support/proxies.js';
6
8
  import { FakeContainerElement } from './fakecontainerelement.js';
7
9
 
@@ -35,6 +37,19 @@ export class FakeTableRow extends FakeContainerElement {
35
37
  getNumCells() {
36
38
  return this.getNumChildren();
37
39
  }
40
+
41
+ /**
42
+ * Gets the text content of the table row.
43
+ * @returns {string} The text content.
44
+ */
45
+ getText() {
46
+ const { nargs, matchThrow } = signatureArgs(arguments, 'TableRow.getText');
47
+ if (nargs !== 0) matchThrow();
48
+ // Live Apps Script joins cell text with a newline character.
49
+ return this.__children
50
+ .map(childTwig => newFakeElement(this.shadowDocument, childTwig.name).__cast().getText())
51
+ .join('\n');
52
+ }
38
53
  }
39
54
 
40
55
  /**
@@ -55,6 +55,7 @@ const addNrRequest = (type, element, addRequests, segmentId) => {
55
55
  };
56
56
 
57
57
  // either finds a matching named range, or adds it to the creation queue
58
+
58
59
  export const findOrCreateNamedRangeName = (element, type, currentNr, addRequests, segmentId) => {
59
60
  const { endIndex, startIndex } = element;
60
61
  const prefix = makeNrPrefix(type, segmentId);
@@ -320,6 +320,16 @@ class ShadowDocument {
320
320
  return null;
321
321
  }
322
322
 
323
+ get namedRanges () {
324
+ return this.__unpackDocumentTab(this.resource).documentTab.namedRanges || {}
325
+ }
326
+
327
+ getNamedRange (name) {
328
+ const nrs = this.namedRanges
329
+ if (!nrs) return null
330
+ return nrs[name]?.namedRanges?.[0] || null
331
+ }
332
+
323
333
  /**
324
334
  * Gets all footnotes in the document.
325
335
  * @returns {FakeFootnote[]} An array of footnotes.
@@ -1,32 +1,10 @@
1
- // fake Apps Script DriveApp
2
1
  /**
3
- * the idea here is to create a global entry for the singleton
4
- * before we actually have everything we need to create it.
5
- * We do this by using a proxy, intercepting calls to the
6
- * initial sigleton and diverting them to a completed one
2
+ * the idea here is to create an empty global entry for the singleton
3
+ * but only load it when it is actually used.
7
4
  */
8
- import { newFakeDriveApp } from './fakedrive.js'
9
- import { Proxies } from '../../support/proxies.js'
10
5
 
11
- // This will eventually hold a proxy for DriveApp
12
- let _app = null
6
+ import { lazyLoaderApp } from '../common/lazyloader.js'
7
+ import { newFakeDriveApp as maker } from './fakedrive.js'
13
8
 
14
- /**
15
- * adds to global space to mimic Apps Script behavior
16
- */
17
- const name = "DriveApp"
18
- if (typeof globalThis[name] === typeof undefined) {
19
-
20
- const getApp = () => {
21
- // if it hasne been intialized yet then do that
22
- if (!_app) {
23
- console.log('...activating proxy for', name)
24
- _app = newFakeDriveApp()
25
- }
26
- // this is the actual driveApp we'll return from the proxy
27
- return _app
28
- }
29
-
30
- Proxies.registerProxy(name, getApp)
31
-
32
- }
9
+ let _app = null;
10
+ _app = lazyLoaderApp(_app, 'DriveApp', maker)
@@ -0,0 +1,8 @@
1
+ export const LabelType = newFakeGasenum([
2
+ "INBOX",
3
+ "SPAM",
4
+ "TRASH",
5
+ "STARRED",
6
+ "CHAT",
7
+ "PRIORITY_INBOX"
8
+ ])
@@ -1,44 +1,9 @@
1
1
  /**
2
- * the idea here is to create a global entry for the singleton
3
- * before we actually have everything we need to create it.
4
- * We do this by using a proxy, intercepting calls to the
5
- * initial singleton and diverting them to a completed one
2
+ * the idea here is to create an empty global entry for the singleton
3
+ * but only load it when it is actually used.
6
4
  */
7
- import { newFakeFormApp } from './fakeformapp.js';
8
- import { Proxies } from '../../support/proxies.js';
5
+ import { newFakeFormApp as maker} from './fakeformapp.js';
6
+ import { lazyLoaderApp } from '../common/lazyloader.js'
9
7
 
10
8
  let _app = null;
11
-
12
- const name = 'FormApp';
13
- const serviceName = 'FormApp';
14
-
15
- if (typeof globalThis[name] === typeof undefined) {
16
- const getApp = () => {
17
- if (!_app) {
18
- const realApp = newFakeFormApp();
19
-
20
- _app = new Proxy(realApp, {
21
- get(target, prop, receiver) {
22
- if (prop === 'toString') {
23
- return () => name;
24
- }
25
-
26
- const serviceBehavior = ScriptApp.__behavior.sandboxService[serviceName];
27
-
28
- if (!serviceBehavior.enabled) {
29
- throw new Error(`${name} service is disabled by sandbox settings.`);
30
- }
31
-
32
- const allowedMethods = serviceBehavior.methods;
33
- if (allowedMethods && typeof target[prop] === 'function' && !allowedMethods.includes(prop)) {
34
- throw new Error(`Method ${name}.${prop} is not allowed by sandbox settings.`);
35
- }
36
-
37
- return Reflect.get(target, prop, receiver);
38
- },
39
- });
40
- }
41
- return _app;
42
- };
43
- Proxies.registerProxy(name, getApp);
44
- }
9
+ _app = lazyLoaderApp(_app, 'FormApp', maker)