@mcpher/gas-fakes 2.3.10 → 2.3.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.
Files changed (50) hide show
  1. package/README.md +14 -14
  2. package/gas-fakes.js +1 -0
  3. package/gf_agent/scripts/builder.js +26 -1
  4. package/package.json +1 -1
  5. package/src/cli/lib-manager.js +14 -4
  6. package/src/cli/setup.js +17 -4
  7. package/src/services/chartsapp/fakechartsapp.js +6 -1
  8. package/src/services/documentapp/elementhelpers.js +27 -0
  9. package/src/services/documentapp/fakeelement.js +8 -0
  10. package/src/services/documentapp/fakeparagraph.js +14 -0
  11. package/src/services/documentapp/faketext.js +174 -4
  12. package/src/services/driveapp/driveiterators.js +53 -8
  13. package/src/services/driveapp/fakedriveapp.js +49 -15
  14. package/src/services/driveapp/fakedrivefile.js +105 -0
  15. package/src/services/driveapp/fakedrivefolder.js +8 -0
  16. package/src/services/driveapp/fakedrivemeta.js +68 -16
  17. package/src/services/driveapp/fakefolderapp.js +19 -6
  18. package/src/services/enums/chartsenums.js +53 -20
  19. package/src/services/enums/driveenums.js +1 -0
  20. package/src/services/slidesapp/fakeautofit.js +23 -15
  21. package/src/services/slidesapp/fakecolorscheme.js +160 -0
  22. package/src/services/slidesapp/fakelayout.js +11 -1
  23. package/src/services/slidesapp/fakemaster.js +10 -0
  24. package/src/services/slidesapp/fakepresentation.js +27 -0
  25. package/src/services/slidesapp/fakeslide.js +9 -0
  26. package/src/services/slidesapp/faketextrange.js +6 -11
  27. package/src/services/spreadsheetapp/chartenummapping.js +15 -0
  28. package/src/services/spreadsheetapp/fakebooleancondition.js +119 -0
  29. package/src/services/spreadsheetapp/fakecellimage.js +42 -0
  30. package/src/services/spreadsheetapp/fakecellimagebuilder.js +59 -0
  31. package/src/services/spreadsheetapp/fakeconditionalformatrule.js +55 -0
  32. package/src/services/spreadsheetapp/fakeconditionalformatrulebuilder.js +330 -0
  33. package/src/services/spreadsheetapp/fakedevelopermetadata.js +32 -4
  34. package/src/services/spreadsheetapp/fakedevelopermetadatalocation.js +27 -5
  35. package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +155 -21
  36. package/src/services/spreadsheetapp/fakegradientcondition.js +71 -0
  37. package/src/services/spreadsheetapp/fakesheet.js +63 -0
  38. package/src/services/spreadsheetapp/fakesheetrange.js +12 -1
  39. package/src/services/spreadsheetapp/fakespreadsheet.js +30 -11
  40. package/src/services/spreadsheetapp/fakespreadsheetapp.js +21 -3
  41. package/src/services/urlfetchapp/app.js +33 -1
  42. package/src/support/fileiterators.js +3 -1
  43. package/src/support/filesharers.js +7 -3
  44. package/src/support/peeker.js +8 -2
  45. package/src/support/sheetutils.js +1 -0
  46. package/src/support/sxdrive.js +26 -15
  47. package/src/support/syncit.js +4 -6
  48. package/src/support/workersync/synchronizer.js +24 -4
  49. package/src/support/workersync/worker.js +13 -2
  50. package/summarize_advanced.js +0 -69
package/README.md CHANGED
@@ -35,7 +35,7 @@ Now you can run apps script code directly from your console - for example
35
35
  gas-fakes -s "const files=DriveApp.getRootFolder().searchFiles('title contains \"Untitled\"');while (files.hasNext()) {console.log(files.next().getName())};"
36
36
  ```
37
37
 
38
- For details see [gas fakes cli](gas-fakes-cli.md)
38
+ For details see [gas fakes cli](notes/gas-fakes-cli.md)
39
39
 
40
40
  ### Configuration
41
41
 
@@ -168,7 +168,7 @@ There are a couple of syntactical differences between Node and Apps Script. Not
168
168
  // this required on Node but not on Apps Script
169
169
  if (ScriptApp.isFake) testFakes()
170
170
  ````
171
- For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](pull-test-push.md)
171
+ For inspiration on pushing modified files to the IDE, see the togas.sh bash script I use for the test suite. There's also a complete push pull workflow available - see - [push test pull](notes/pull-test-push.md)
172
172
 
173
173
 
174
174
  ## Help
@@ -188,11 +188,11 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
188
188
  - [readme](README.md)
189
189
  - [Natural Language Automation with Gemini Skills & MCP Server](gemini-skills-mcp.md) - new skills-based agent approach.
190
190
  - [gf_agent documentation](../gf_agent/README.md) - instructions for the Gemini CLI automation agent and MCP server.
191
- - [gas fakes cli](gas-fakes-cli.md)
191
+ - [gas fakes cli](notes/gas-fakes-cli.md)
192
192
  - [github actions using adc](https://github.com/brucemcpherson/gas-fakes-actions-adc)
193
193
  - [github actions using dwd and wif](https://github.com/brucemcpherson/gas-fakes-actions-dwd)
194
- - [ksuite as a back end](ksuite_poc.md)
195
- - [msgraph as a back end](msgraph.md)
194
+ - [ksuite as a back end](notes/ksuite_poc.md)
195
+ - [msgraph as a back end](notes/msgraph.md)
196
196
  - [resurrecting scriptDb repo](https://github.com/brucemcpherson/scriptdb-redux)
197
197
  - [Resurrecting ScriptDb – nosql database for Apps Script](https://ramblings.mcpher.com/resurrecting-scriptdb-nosql-database-for-apps-script/)
198
198
  - [gas-fakes in serverless containers](https://docs.google.com/presentation/d/1JlXF9T--DD4ERHopyP3WyAMhjRCxxHblgCP5ynxaJ3k/edit?usp=sharing)
@@ -203,7 +203,7 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
203
203
  - [running gas-fakes on Amazon AWS lambda](https://github.com/brucemcpherson/gas-fakes-containers)
204
204
  - [running gas-fakes on Azure ACA](https://github.com/brucemcpherson/gas-fakes-containers)
205
205
  - [running gas-fakes on Github actions](https://github.com/brucemcpherson/gas-fakes-containers)
206
- - [jdbc notes](jdbc-notes.md)
206
+ - [jdbc notes](notes/jdbc-notes.md)
207
207
  - [Yes – you can run native apps script code on Azure ACA as well!](https://ramblings.mcpher.com/yes-you-can-run-native-apps-script-code-on-azure-aca-as-well/)
208
208
  - [Yes – you can run native apps script code on AWS Lambda!](https://ramblings.mcpher.com/apps-script-on-aws-lambda/)
209
209
  - [initial idea and thoughts](https://ramblings.mcpher.com/a-proof-of-concept-implementation-of-apps-script-environment-on-node/)
@@ -213,16 +213,16 @@ As I mentioned earlier, to take this further, I'm going to need a lot of help to
213
213
  - [Turning async into synch on Node using workers](https://ramblings.mcpher.com/turning-async-into-synch-on-node-using-workers/)
214
214
  - [All about Apps Script Enums and how to fake them](https://ramblings.mcpher.com/all-about-apps-script-enums-and-how-to-fake-them/)
215
215
  - [colaborators](collaborators.md) - additional information for collaborators
216
- - [oddities](oddities.md) - a collection of oddities uncovered during this project
217
- - [named colors](named-colors.md)
218
- - [sandbox](sandbox.md)
219
- - [senstive scopes](workspace_scopes.md)
220
- - [using apps script libraries with gas-fakes](libraries.md)
216
+ - [oddities](notes/oddities.md) - a collection of oddities uncovered during this project
217
+ - [named colors](notes/named-colors.md)
218
+ - [sandbox](notes/sandbox.md)
219
+ - [senstive scopes](notes/workspace_scopes.md)
220
+ - [using apps script libraries with gas-fakes](notes/libraries.md)
221
221
  - [how libhandler works](libhandler.md)
222
222
  - [article:using apps script libraries with gas-fakes](https://ramblings.mcpher.com/how-to-use-apps-script-libraries-directly-from-node/)
223
- - [named range identity](named-range-identity.md)
224
- - [Workspace scopes with local authentication](workspace_scopes.md)
225
- - [push test pull](pull-test-push.md)
223
+ - [named range identity](notes/named-range-identity.md)
224
+ - [Workspace scopes with local authentication](notes/workspace_scopes.md)
225
+ - [push test pull](notes/pull-test-push.md)
226
226
  - [sharing cache and properties between gas-fakes and live apps script](https://ramblings.mcpher.com/sharing-cache-and-properties-between-gas-fakes-and-live-apps-script/)
227
227
  - [gas-fakes-cli now has built in mcp server and gemini extension](https://ramblings.mcpher.com/gas-fakes-cli-now-has-built-in-mcp-server-and-gemini-extension/)
228
228
  - [gas-fakes CLI: Run apps script code directly from your terminal](https://ramblings.mcpher.com/gas-fakes-cli-run-apps-script-code-directly-from-your-terminal/)
package/gas-fakes.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ process.env.GF_CLI_INTERACTIVE = "true";
3
4
  import { main } from "./src/cli/app.js";
4
5
 
5
6
  main().catch((error) => {
@@ -56,7 +56,32 @@ async function build() {
56
56
  }
57
57
 
58
58
  await fs.writeFile(INDEX_FILE, masterIndex);
59
- console.log('Build complete! Skills and Index generated.');
59
+
60
+ // Aggregate knowledge files into SKILL.md
61
+ const TEMPLATE_FILE = './gf_agent/scripts/SKILL.template.md';
62
+ const KNOWLEDGE_DIR = './gf_agent/knowledge';
63
+ const SKILL_OUTPUT = './gf_agent/SKILL.md';
64
+
65
+ let skillMarkdown = await fs.readFile(TEMPLATE_FILE, 'utf-8');
66
+
67
+ try {
68
+ const knowledgeFiles = await fs.readdir(KNOWLEDGE_DIR);
69
+ // Sort files to ensure deterministic aggregation (e.g., 01-drive.md, 02-syntax.md)
70
+ knowledgeFiles.sort();
71
+
72
+ for (const kFile of knowledgeFiles) {
73
+ if (kFile.endsWith('.md')) {
74
+ const kContent = await fs.readFile(path.join(KNOWLEDGE_DIR, kFile), 'utf-8');
75
+ skillMarkdown += `\n${kContent}\n`;
76
+ }
77
+ }
78
+ } catch (err) {
79
+ console.log("No knowledge directory found or error reading it:", err.message);
80
+ }
81
+
82
+ await fs.writeFile(SKILL_OUTPUT, skillMarkdown);
83
+
84
+ console.log('Build complete! Skills, Index, and monolithic SKILL.md generated.');
60
85
  }
61
86
 
62
87
  build().catch(console.error);
package/package.json CHANGED
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "name": "@mcpher/gas-fakes",
42
42
  "author": "bruce mcpherson",
43
- "version": "2.3.10",
43
+ "version": "2.3.13",
44
44
  "license": "MIT",
45
45
  "main": "main.js",
46
46
  "description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
@@ -1,6 +1,6 @@
1
1
  import fs from "fs";
2
2
  import { parse } from "acorn";
3
- import { Auth } from "../support/auth.js"; // Relative to gas-fakes-cli directory
3
+ import { Auth, _identities } from "../support/auth.js"; // Relative to gas-fakes-cli directory
4
4
  import { checkForGcloudCli, spawnCommand } from "./utils.js";
5
5
 
6
6
  async function getAccessToken(pattern) {
@@ -8,12 +8,22 @@ async function getAccessToken(pattern) {
8
8
  // Authorization pattern 1
9
9
  // We use cloud-platform as it provides sufficient access for Drive API fetching
10
10
  // while avoiding the 'well-known client ID' block that targets Workspace scopes like drive.readonly.
11
- const auth = await Auth.setAuth(
11
+ const client = await Auth.setAuth(
12
12
  ["https://www.googleapis.com/auth/cloud-platform"],
13
13
  true
14
14
  );
15
- auth.cachedCredential = null;
16
- return await auth.getAccessToken();
15
+ if (client.cachedCredential !== undefined) {
16
+ client.cachedCredential = null;
17
+ }
18
+ const tokenResponse = await client.getAccessToken();
19
+ const token = tokenResponse.token || tokenResponse;
20
+
21
+ // CRITICAL: We MUST clear the global identities map so that this temporary
22
+ // library-fetching auth does not trick the downstream executor.js and syncit.js
23
+ // into thinking the full environment (and the background worker) has been initialized.
24
+ _identities.clear();
25
+
26
+ return token;
17
27
  } else {
18
28
  // Authorization pattern 2
19
29
  await checkForGcloudCli();
package/src/cli/setup.js CHANGED
@@ -622,9 +622,22 @@ export async function initializeConfiguration(options = {}) {
622
622
 
623
623
  if (skillResponse.installSkills) {
624
624
  console.log("Installing Gemini CLI skills and MCP server...");
625
+ let manualSkillCmd;
625
626
  try {
626
- // 1. Install the agent skill
627
- const skillCmd = "gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
627
+ // 1. Install or link the agent skill
628
+ let skillCmd;
629
+ const localSkillPath = path.resolve(process.cwd(), "gf_agent", "SKILL.md");
630
+ const isLocalClone = fs.existsSync(localSkillPath);
631
+
632
+ if (isLocalClone) {
633
+ console.log("Detected local gas-fakes repository. Linking local skill for development...");
634
+ skillCmd = "gemini skills link ./gf_agent";
635
+ manualSkillCmd = "1. gemini skills link ./gf_agent";
636
+ } else {
637
+ skillCmd = "gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
638
+ manualSkillCmd = "1. gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent";
639
+ }
640
+
628
641
  console.log(`Executing: ${skillCmd}`);
629
642
  execSync(skillCmd, { stdio: "inherit" });
630
643
 
@@ -635,11 +648,11 @@ export async function initializeConfiguration(options = {}) {
635
648
 
636
649
  console.log("\x1b[1;32mInstallation complete!\x1b[0m");
637
650
  console.log("\nYou can now use natural language to automate tasks:");
638
- console.log(" \x1b[1;33m\"Create a spreadsheet of my recent Drive files\"\x1b[0m");
651
+ console.log(" \x1b[1;33m\“Create a sheet called ‘Todays drive files' and add any files on Drive modified today to it\"\x1b[0m");
639
652
  } catch (err) {
640
653
  console.error(`\x1b[1;31mError during Gemini installation: ${err.message}\x1b[0m`);
641
654
  console.log("You may need to install them manually:");
642
- console.log("1. gemini skills install https://github.com/brucemcpherson/gas-fakes.git --path gf_agent");
655
+ console.log(manualSkillCmd);
643
656
  console.log("2. gemini mcp add --scope project gas-fakes-mcp gas-fakes mcp");
644
657
  }
645
658
  } else {
@@ -19,7 +19,12 @@ export const newFakeChartsApp = (...args) => {
19
19
  export class FakeChartsApp {
20
20
  constructor() {
21
21
  const enumProps = [
22
- "ChartType", // ChartType An enumeration of the possible chart types.
22
+ "ChartType", // ChartType An enumeration of the possible chart types.
23
+ "ChartHiddenDimensionStrategy",
24
+ "ChartMergeStrategy",
25
+ "CurveStyle",
26
+ "PointStyle",
27
+ "Position"
23
28
  ];
24
29
 
25
30
  // import all known enums as props of chartsapp
@@ -286,3 +286,30 @@ export const updateParagraphStyle = (element, paragraphStyle, fields) => {
286
286
  shadow.refresh();
287
287
  return element;
288
288
  };
289
+
290
+ /**
291
+ * Updates the text style for a given element or range within it.
292
+ * @param {import('./fakeelement.js').FakeElement} element The element whose text style to update.
293
+ * @param {GoogleAppsScript.Document.TextStyle} textStyle The style object to apply.
294
+ * @param {string} fields The comma-separated string of field names to update.
295
+ * @param {object} [range] Optional specific range. Defaults to the element's range.
296
+ * @returns {import('./fakeelement.js').FakeElement} The element, for chaining.
297
+ */
298
+ export const updateTextStyle = (element, textStyle, fields, range = null) => {
299
+ const shadow = element.shadowDocument;
300
+ const item = element.__elementMapItem;
301
+ const updateRange = range || {
302
+ startIndex: item.startIndex,
303
+ endIndex: item.endIndex,
304
+ segmentId: shadow.__segmentId,
305
+ tabId: shadow.__tabId,
306
+ };
307
+
308
+ const requests = [{
309
+ updateTextStyle: { range: updateRange, textStyle, fields },
310
+ }];
311
+
312
+ Docs.Documents.batchUpdate({ requests }, shadow.getId());
313
+ shadow.refresh();
314
+ return element;
315
+ };
@@ -96,6 +96,14 @@ export class FakeElement {
96
96
  });
97
97
  }
98
98
 
99
+ /**
100
+ * Gets the shadow document manager associated with this element's structure.
101
+ * @type {import('./shadowdocument.js').ShadowDocument | null}
102
+ */
103
+ get shadowDocument() {
104
+ return this.__shadowDocument;
105
+ }
106
+
99
107
  get __structure() {
100
108
  if (this.__isDetached) return null;
101
109
  return this.__shadowDocument.structure;
@@ -4,6 +4,7 @@ import { Utils } from '../../support/utils.js';
4
4
  import { imageOptions } from './elementoptions.js';
5
5
  import { FakeContainerElement } from './fakecontainerelement.js';
6
6
  import { registerElement } from './elementRegistry.js';
7
+ import { newFakeText } from './faketext.js';
7
8
  import { appendText, addPositionedImage, appendImage, insertImage } from './appenderhelpers.js';
8
9
  import { getText as getTextHelper, getAttributes as getAttributesHelper, updateParagraphStyle } from './elementhelpers.js';
9
10
 
@@ -32,6 +33,19 @@ export class FakeParagraph extends FakeContainerElement {
32
33
  return getTextHelper(this);
33
34
  }
34
35
 
36
+ /**
37
+ * Returns the contents of the paragraph as a Text element.
38
+ * @returns {GoogleAppsScript.Document.Text} The text element.
39
+ */
40
+ editAsText() {
41
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.editAsText');
42
+ if (nargs !== 0) matchThrow();
43
+ // In gas-fakes, we can return a FakeText that points to the same underlying item.
44
+ // The getText and updateTextStyle methods will work correctly because they
45
+ // handle both PARAGRAPH and TEXT items or ranges.
46
+ return newFakeText(this.__shadowDocument, this.__name);
47
+ }
48
+
35
49
  appendText(text) {
36
50
  const { nargs, matchThrow } = signatureArgs(arguments, 'Paragraph.appendText');
37
51
  if (nargs !== 1) matchThrow();
@@ -1,9 +1,15 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
+ import { signatureArgs } from '../../support/helpers.js';
3
+ import { Utils } from '../../support/utils.js';
2
4
  import { FakeElement } from './fakeelement.js';
3
5
  import { registerElement } from './elementRegistry.js';
6
+ import { getAttributes as getAttributesHelper, updateTextStyle, getText as getTextHelper } from './elementhelpers.js';
7
+
8
+ const { is } = Utils;
4
9
 
5
10
  /**
6
11
  * A fake implementation of the Text class for DocumentApp.
12
+ * @implements {GoogleAppsScript.Document.Text}
7
13
  * @see https://developers.google.com/apps-script/reference/document/text
8
14
  */
9
15
  class FakeText extends FakeElement {
@@ -16,9 +22,173 @@ class FakeText extends FakeElement {
16
22
  * @returns {string} The text contents.
17
23
  */
18
24
  getText() {
19
- const item = this.__elementMapItem;
20
- // A Text element corresponds to a textRun in the API.
21
- return item.textRun?.content || '';
25
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.getText');
26
+ if (nargs !== 0) matchThrow();
27
+ return getTextHelper(this);
28
+ }
29
+
30
+ getAttributes() {
31
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.getAttributes');
32
+ if (nargs !== 0) matchThrow();
33
+ return getAttributesHelper(this);
34
+ }
35
+
36
+ setAttributes(attributes) {
37
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setAttributes');
38
+ if (nargs !== 1 || !is.object(attributes)) matchThrow();
39
+
40
+ const textStyle = {};
41
+ const textFields = [];
42
+ const Attribute = DocumentApp.Attribute;
43
+
44
+ const colorToRgb = (hex) => {
45
+ if (!hex || !hex.startsWith('#') || hex.length !== 7) return null;
46
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
47
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
48
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
49
+ return { red: r, green: g, blue: b };
50
+ };
51
+
52
+ for (const key in attributes) {
53
+ const value = attributes[key];
54
+ // Note: GAS allows null to clear some attributes, but let's handle the basics first.
55
+ switch (String(key)) {
56
+ case String(Attribute.BACKGROUND_COLOR): textStyle.backgroundColor = { color: { rgbColor: colorToRgb(value) } }; textFields.push('backgroundColor'); break;
57
+ case String(Attribute.BOLD): textStyle.bold = value; textFields.push('bold'); break;
58
+ case String(Attribute.FONT_FAMILY): textStyle.weightedFontFamily = { fontFamily: value }; textFields.push('weightedFontFamily'); break;
59
+ case String(Attribute.FONT_SIZE): textStyle.fontSize = { magnitude: value, unit: 'PT' }; textFields.push('fontSize'); break;
60
+ case String(Attribute.FOREGROUND_COLOR): textStyle.foregroundColor = { color: { rgbColor: colorToRgb(value) } }; textFields.push('foregroundColor'); break;
61
+ case String(Attribute.ITALIC): textStyle.italic = value; textFields.push('italic'); break;
62
+ case String(Attribute.LINK_URL): textStyle.link = { url: value }; textFields.push('link'); break;
63
+ case String(Attribute.STRIKETHROUGH): textStyle.strikethrough = value; textFields.push('strikethrough'); break;
64
+ case String(Attribute.UNDERLINE): textStyle.underline = value; textFields.push('underline'); break;
65
+ }
66
+ }
67
+
68
+ if (textFields.length === 0) return this;
69
+ return updateTextStyle(this, textStyle, textFields.join(','));
70
+ }
71
+
72
+ setBold(bold) {
73
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setBold');
74
+ if (nargs !== 1 || !is.boolean(bold)) matchThrow();
75
+ return updateTextStyle(this, { bold }, 'bold');
76
+ }
77
+
78
+ isBold() {
79
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.isBold');
80
+ if (nargs !== 0) matchThrow();
81
+ return !!this.getAttributes()[DocumentApp.Attribute.BOLD];
82
+ }
83
+
84
+ setItalic(italic) {
85
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setItalic');
86
+ if (nargs !== 1 || !is.boolean(italic)) matchThrow();
87
+ return updateTextStyle(this, { italic }, 'italic');
88
+ }
89
+
90
+ isItalic() {
91
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.isItalic');
92
+ if (nargs !== 0) matchThrow();
93
+ return !!this.getAttributes()[DocumentApp.Attribute.ITALIC];
94
+ }
95
+
96
+ setUnderline(underline) {
97
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setUnderline');
98
+ if (nargs !== 1 || !is.boolean(underline)) matchThrow();
99
+ return updateTextStyle(this, { underline }, 'underline');
100
+ }
101
+
102
+ isUnderline() {
103
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.isUnderline');
104
+ if (nargs !== 0) matchThrow();
105
+ return !!this.getAttributes()[DocumentApp.Attribute.UNDERLINE];
106
+ }
107
+
108
+ setFontFamily(fontFamily) {
109
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setFontFamily');
110
+ if (nargs !== 1 || !is.string(fontFamily)) matchThrow();
111
+ return updateTextStyle(this, { weightedFontFamily: { fontFamily } }, 'weightedFontFamily');
112
+ }
113
+
114
+ getFontFamily() {
115
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.getFontFamily');
116
+ if (nargs !== 0) matchThrow();
117
+ return this.getAttributes()[DocumentApp.Attribute.FONT_FAMILY];
118
+ }
119
+
120
+ setFontSize(fontSize) {
121
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setFontSize');
122
+ if (nargs !== 1 || !is.number(fontSize)) matchThrow();
123
+ return updateTextStyle(this, { fontSize: { magnitude: fontSize, unit: 'PT' } }, 'fontSize');
124
+ }
125
+
126
+ getFontSize() {
127
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.getFontSize');
128
+ if (nargs !== 0) matchThrow();
129
+ return this.getAttributes()[DocumentApp.Attribute.FONT_SIZE];
130
+ }
131
+
132
+ setForegroundColor(color) {
133
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setForegroundColor');
134
+ if (nargs !== 1 || !is.string(color)) matchThrow();
135
+ const colorToRgb = (hex) => {
136
+ if (!hex || !hex.startsWith('#') || hex.length !== 7) return null;
137
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
138
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
139
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
140
+ return { red: r, green: g, blue: b };
141
+ };
142
+ return updateTextStyle(this, { foregroundColor: { color: { rgbColor: colorToRgb(color) } } }, 'foregroundColor');
143
+ }
144
+
145
+ getForegroundColor() {
146
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.getForegroundColor');
147
+ if (nargs !== 0) matchThrow();
148
+ return this.getAttributes()[DocumentApp.Attribute.FOREGROUND_COLOR];
149
+ }
150
+
151
+ setBackgroundColor(color) {
152
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setBackgroundColor');
153
+ if (nargs !== 1 || !is.string(color)) matchThrow();
154
+ const colorToRgb = (hex) => {
155
+ if (!hex || !hex.startsWith('#') || hex.length !== 7) return null;
156
+ const r = parseInt(hex.slice(1, 3), 16) / 255;
157
+ const g = parseInt(hex.slice(3, 5), 16) / 255;
158
+ const b = parseInt(hex.slice(5, 7), 16) / 255;
159
+ return { red: r, green: g, blue: b };
160
+ };
161
+ return updateTextStyle(this, { backgroundColor: { color: { rgbColor: colorToRgb(color) } } }, 'backgroundColor');
162
+ }
163
+
164
+ getBackgroundColor() {
165
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.getBackgroundColor');
166
+ if (nargs !== 0) matchThrow();
167
+ return this.getAttributes()[DocumentApp.Attribute.BACKGROUND_COLOR];
168
+ }
169
+
170
+ setLinkUrl(url) {
171
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setLinkUrl');
172
+ if (nargs !== 1 || (!is.string(url) && !is.null(url))) matchThrow();
173
+ return updateTextStyle(this, { link: { url: url } }, 'link');
174
+ }
175
+
176
+ getLinkUrl() {
177
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.getLinkUrl');
178
+ if (nargs !== 0) matchThrow();
179
+ return this.getAttributes()[DocumentApp.Attribute.LINK_URL];
180
+ }
181
+
182
+ setStrikethrough(strikethrough) {
183
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.setStrikethrough');
184
+ if (nargs !== 1 || !is.boolean(strikethrough)) matchThrow();
185
+ return updateTextStyle(this, { strikethrough }, 'strikethrough');
186
+ }
187
+
188
+ isStrikethrough() {
189
+ const { nargs, matchThrow } = signatureArgs(arguments, 'Text.isStrikethrough');
190
+ if (nargs !== 0) matchThrow();
191
+ return !!this.getAttributes()[DocumentApp.Attribute.STRIKETHROUGH];
22
192
  }
23
193
 
24
194
  toString() {
@@ -28,4 +198,4 @@ class FakeText extends FakeElement {
28
198
 
29
199
  export const newFakeText = (...args) => Proxies.guard(new FakeText(...args));
30
200
 
31
- registerElement('TEXT', newFakeText);
201
+ registerElement('TEXT', newFakeText);
@@ -14,14 +14,33 @@ export const getFilesIterator = ({
14
14
  qob,
15
15
  parentId = null,
16
16
  folderTypes,
17
- fileTypes
17
+ fileTypes,
18
+ token
18
19
  }) => {
19
20
 
20
21
 
21
22
  // parentId can be null to search everywhere
22
23
  if (!is.null(parentId)) assert.nonEmptyString(parentId)
23
- assert.boolean(folderTypes)
24
- assert.boolean(fileTypes)
24
+ assert.boolean(folderTypes || !!token)
25
+ assert.boolean(fileTypes || !!token)
26
+
27
+ let state = {
28
+ qob,
29
+ parentId,
30
+ folderTypes,
31
+ fileTypes,
32
+ pageToken: null,
33
+ tank: []
34
+ }
35
+
36
+ if (token) {
37
+ const saved = JSON.parse(Buffer.from(token, 'base64').toString())
38
+ state = { ...state, ...saved }
39
+ qob = state.qob
40
+ parentId = state.parentId
41
+ folderTypes = state.folderTypes
42
+ fileTypes = state.fileTypes
43
+ }
25
44
 
26
45
  // DriveApp doesnt give option to specify these so this will be fixed
27
46
  const fields = `files(${minFields}),nextPageToken`
@@ -49,9 +68,9 @@ export const getFilesIterator = ({
49
68
  */
50
69
  function* filesink() {
51
70
  // the result tank
52
- let tank = []
71
+ let tank = [...state.tank]
53
72
  // the next page token
54
- let pageToken = null
73
+ let pageToken = state.pageToken
55
74
 
56
75
  do {
57
76
  // if nothing in the tank, fill it up
@@ -63,10 +82,14 @@ export const getFilesIterator = ({
63
82
  // format the results into the folder or file object
64
83
  assert.array(data.files)
65
84
  assert.function(DriveApp.__settleClass)
66
- tank = data.files.map(DriveApp.__settleClass)
85
+
86
+ // we store raw data in state for continuation
87
+ state.tank = data.files
88
+ tank = [...state.tank]
67
89
 
68
90
  // the presence of a nextPageToken is the signal that there's more to come
69
91
  pageToken = data.nextPageToken
92
+ state.pageToken = pageToken
70
93
 
71
94
  // if we still have nothing in the tank but there's a page token, keep going
72
95
  if (!tank.length && !pageToken) break;
@@ -74,7 +97,12 @@ export const getFilesIterator = ({
74
97
 
75
98
  // if we've got anything in the tank send back the oldest one
76
99
  if (tank.length) {
77
- yield tank.splice(0, 1)[0]
100
+ const raw = tank.splice(0, 1)[0]
101
+ state.tank.splice(0, 1)
102
+ yield {
103
+ __fakeResolved: DriveApp.__settleClass(raw),
104
+ __fakeRaw: raw
105
+ }
78
106
  }
79
107
 
80
108
  // if there's still anything left in the tank,
@@ -85,9 +113,26 @@ export const getFilesIterator = ({
85
113
  // create the iterator
86
114
  const fileit = filesink()
87
115
 
116
+ const continuationHandler = (peeked) => {
117
+ // if we've peeked, we need to make sure the peeked item is included in the tank
118
+ // because GAS continuation tokens should represent the state AFTER the last next() call.
119
+ // Peeker already called generator.next(), so the item to be returned by the NEXT next() call
120
+ // is in peeked.value.
121
+ const tank = [...state.tank]
122
+ if (peeked && !peeked.done) {
123
+ // Put the pre-fetched item back at the start of the tank for the token
124
+ tank.unshift(peeked.value.__fakeRaw || peeked.value)
125
+ }
126
+ const tokenState = {
127
+ ...state,
128
+ tank
129
+ }
130
+ return Buffer.from(JSON.stringify(tokenState)).toString('base64')
131
+ }
132
+
88
133
  // a regular iterator doesnt support the same methods
89
134
  // as Apps Script so we'll fake that too
90
- return newPeeker(fileit)
135
+ return newPeeker(fileit, continuationHandler)
91
136
 
92
137
  }
93
138