@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.
- package/README.md +14 -14
- package/gas-fakes.js +1 -0
- package/gf_agent/scripts/builder.js +26 -1
- package/package.json +1 -1
- package/src/cli/lib-manager.js +14 -4
- package/src/cli/setup.js +17 -4
- package/src/services/chartsapp/fakechartsapp.js +6 -1
- package/src/services/documentapp/elementhelpers.js +27 -0
- package/src/services/documentapp/fakeelement.js +8 -0
- package/src/services/documentapp/fakeparagraph.js +14 -0
- package/src/services/documentapp/faketext.js +174 -4
- package/src/services/driveapp/driveiterators.js +53 -8
- package/src/services/driveapp/fakedriveapp.js +49 -15
- package/src/services/driveapp/fakedrivefile.js +105 -0
- package/src/services/driveapp/fakedrivefolder.js +8 -0
- package/src/services/driveapp/fakedrivemeta.js +68 -16
- package/src/services/driveapp/fakefolderapp.js +19 -6
- package/src/services/enums/chartsenums.js +53 -20
- package/src/services/enums/driveenums.js +1 -0
- package/src/services/slidesapp/fakeautofit.js +23 -15
- package/src/services/slidesapp/fakecolorscheme.js +160 -0
- package/src/services/slidesapp/fakelayout.js +11 -1
- package/src/services/slidesapp/fakemaster.js +10 -0
- package/src/services/slidesapp/fakepresentation.js +27 -0
- package/src/services/slidesapp/fakeslide.js +9 -0
- package/src/services/slidesapp/faketextrange.js +6 -11
- package/src/services/spreadsheetapp/chartenummapping.js +15 -0
- package/src/services/spreadsheetapp/fakebooleancondition.js +119 -0
- package/src/services/spreadsheetapp/fakecellimage.js +42 -0
- package/src/services/spreadsheetapp/fakecellimagebuilder.js +59 -0
- package/src/services/spreadsheetapp/fakeconditionalformatrule.js +55 -0
- package/src/services/spreadsheetapp/fakeconditionalformatrulebuilder.js +330 -0
- package/src/services/spreadsheetapp/fakedevelopermetadata.js +32 -4
- package/src/services/spreadsheetapp/fakedevelopermetadatalocation.js +27 -5
- package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +155 -21
- package/src/services/spreadsheetapp/fakegradientcondition.js +71 -0
- package/src/services/spreadsheetapp/fakesheet.js +63 -0
- package/src/services/spreadsheetapp/fakesheetrange.js +12 -1
- package/src/services/spreadsheetapp/fakespreadsheet.js +30 -11
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +21 -3
- package/src/services/urlfetchapp/app.js +33 -1
- package/src/support/fileiterators.js +3 -1
- package/src/support/filesharers.js +7 -3
- package/src/support/peeker.js +8 -2
- package/src/support/sheetutils.js +1 -0
- package/src/support/sxdrive.js +26 -15
- package/src/support/syncit.js +4 -6
- package/src/support/workersync/synchronizer.js +24 -4
- package/src/support/workersync/worker.js +13 -2
- 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
|
@@ -56,7 +56,32 @@ async function build() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
await fs.writeFile(INDEX_FILE, masterIndex);
|
|
59
|
-
|
|
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.
|
|
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",
|
package/src/cli/lib-manager.js
CHANGED
|
@@ -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
|
|
11
|
+
const client = await Auth.setAuth(
|
|
12
12
|
["https://www.googleapis.com/auth/cloud-platform"],
|
|
13
13
|
true
|
|
14
14
|
);
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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", //
|
|
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
|
|
20
|
-
|
|
21
|
-
return
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|