@mcpher/gas-fakes 2.5.4 → 2.5.6
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/.claspignore +1 -1
- package/README.md +27 -1
- package/exgcp.sh +63 -0
- package/package.json +1 -1
- package/src/services/advslides/fakeadvslides.js +20 -1
- package/src/services/documentapp/appenderhelpers.js +8 -3
- package/src/services/documentapp/elementhelpers.js +148 -7
- package/src/services/documentapp/elementoptions.js +40 -0
- package/src/services/documentapp/elements.js +7 -0
- package/src/services/documentapp/fakebookmark.js +1 -3
- package/src/services/documentapp/fakecontainerelement.js +188 -0
- package/src/services/documentapp/fakedate.js +92 -0
- package/src/services/documentapp/fakedocument.js +51 -0
- package/src/services/documentapp/fakedocumenttab.js +9 -25
- package/src/services/documentapp/fakeelement.js +453 -90
- package/src/services/documentapp/fakeequation.js +28 -0
- package/src/services/documentapp/fakeequationfunction.js +37 -0
- package/src/services/documentapp/fakeequationfunctionargumentseparator.js +28 -0
- package/src/services/documentapp/fakeequationsymbol.js +37 -0
- package/src/services/documentapp/fakefootersection.js +64 -1
- package/src/services/documentapp/fakeheadersection.js +64 -0
- package/src/services/documentapp/fakeperson.js +67 -0
- package/src/services/documentapp/fakerangeelement.js +27 -1
- package/src/services/documentapp/fakerichlink.js +79 -0
- package/src/services/documentapp/fakesectionelement.js +51 -12
- package/src/services/documentapp/faketablecell.js +98 -0
- package/src/services/documentapp/nrhelpers.js +2 -1
- package/src/services/documentapp/shadowdocument.js +19 -2
- package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +2 -7
- package/src/support/sxslides.js +5 -4
package/.claspignore
CHANGED
package/README.md
CHANGED
|
@@ -157,7 +157,33 @@ For pushing modified files back to the Apps Script IDE, use the built-in `gas-fa
|
|
|
157
157
|
|
|
158
158
|
As I mentioned earlier, to take this further, I'm going to need a lot of help to extend the methods and services supported - so if you feel this would be useful to you, and would like to collaborate, please ping me on bruce@mcpher.com and we'll talk.
|
|
159
159
|
|
|
160
|
-
|
|
160
|
+
# Gas-Fakes Progress Summary
|
|
161
|
+
|
|
162
|
+
| Service | Classes | Methods | Completed | In Progress | Not Started |
|
|
163
|
+
|---|---|---|---|---|---|
|
|
164
|
+
| [Base](./progress/base.md) | 17 | 127 | 93 | 2 | 32 |
|
|
165
|
+
| [Cache](./progress/cache.md) | 2 | 11 | 7 | 4 | 0 |
|
|
166
|
+
| [Calendar](./progress/calendar.md) | 13 | 273 | 273 | 0 | 0 |
|
|
167
|
+
| [Charts](./progress/charts.md) | 29 | 238 | 37 | 0 | 201 |
|
|
168
|
+
| [Content](./progress/content.md) | 3 | 16 | 16 | 0 | 0 |
|
|
169
|
+
| [Document](./progress/document.md) | 47 | 1032 | 880 | 12 | 140 |
|
|
170
|
+
| [Drive](./progress/drive.md) | 8 | 164 | 124 | 8 | 32 |
|
|
171
|
+
| [Forms](./progress/forms.md) | 41 | 504 | 266 | 0 | 238 |
|
|
172
|
+
| [Gmail](./progress/gmail.md) | 6 | 168 | 167 | 0 | 1 |
|
|
173
|
+
| [HTML](./progress/html.md) | 6 | 39 | 34 | 0 | 5 |
|
|
174
|
+
| [JDBC](./progress/jdbc.md) | 20 | 753 | 311 | 0 | 442 |
|
|
175
|
+
| [Lock](./progress/lock.md) | 2 | 7 | 7 | 0 | 0 |
|
|
176
|
+
| [Mail](./progress/mail.md) | 1 | 5 | 0 | 0 | 5 |
|
|
177
|
+
| [Properties](./progress/properties.md) | 4 | 11 | 6 | 5 | 0 |
|
|
178
|
+
| [Script](./progress/script.md) | 16 | 84 | 22 | 0 | 62 |
|
|
179
|
+
| [Slides](./progress/slides.md) | 76 | 1288 | 1005 | 0 | 283 |
|
|
180
|
+
| [Spreadsheet](./progress/spreadsheet.md) | 108 | 1771 | 1301 | 19 | 451 |
|
|
181
|
+
| [URL Fetch](./progress/urlfetch.md) | 2 | 13 | 12 | 0 | 1 |
|
|
182
|
+
| [Utilities](./progress/utilities.md) | 5 | 59 | 59 | 0 | 0 |
|
|
183
|
+
| [XML](./progress/xml.md) | 14 | 149 | 142 | 0 | 7 |
|
|
184
|
+
| **Total** | **420** | **6712** | **4762** | **50** | **1900** |
|
|
185
|
+
|
|
186
|
+
## <img src="./pngs/logo.png" alt="gas-fakes logo" width="50" align="top"> Further Reading
|
|
161
187
|
|
|
162
188
|
|
|
163
189
|
|
package/exgcp.sh
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# This script reads the GCP_PROJECT_ID from a .env file
|
|
4
|
+
# and exports it as GOOGLE_CLOUD_PROJECT for the current shell session.
|
|
5
|
+
#
|
|
6
|
+
# Usage: source . ./exgcp.sh
|
|
7
|
+
|
|
8
|
+
# Define the path to your .env file relative to the script's location
|
|
9
|
+
ENV_FILE="./.env"
|
|
10
|
+
|
|
11
|
+
# Check if the .env file exists
|
|
12
|
+
|
|
13
|
+
if [ ! -f "$ENV_FILE" ]; then
|
|
14
|
+
echo "Error: .env file not found at path: $ENV_FILE"
|
|
15
|
+
# Use 'return' instead of 'exit' so it doesn't close the user's terminal when sourced
|
|
16
|
+
return 1
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Read the GCP_PROJECT_ID, remove quotes, and handle potential carriage returns
|
|
20
|
+
GOOGLE_CLOUD_PROJECT_VALUE=$(grep -E '^GOOGLE_CLOUD_PROJECT=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
21
|
+
GEMINI_API_KEY_VALUE=$(grep -E '^GEMINI_API_KEY=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
22
|
+
ANTIGRAVITY_API_KEY_VALUE=$(grep -E '^ANTIGRAVITY_API_KEY=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
23
|
+
GEMINI_MODEL_VALUE=$(grep -E '^GEMINI_MODEL=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
24
|
+
OMDB_API_KEY_VALUE=$(grep -E '^OMDB_API_KEY=' "$ENV_FILE" | cut -d '=' -f2 | tr -d '"\r')
|
|
25
|
+
# Check if a value was extracted
|
|
26
|
+
if [ -z "$GOOGLE_CLOUD_PROJECT_VALUE" ]; then
|
|
27
|
+
echo "Error: GOOGLE_CLOUD_PROJECT not found or is empty in $ENV_FILE."
|
|
28
|
+
return 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
if [ -z "GEMINI_API_KEY_VALUE" ]; then
|
|
32
|
+
echo "GEMINI_API_KEY not found or is empty in $ENV_FILE."
|
|
33
|
+
else
|
|
34
|
+
echo "exported: GEMINI_API_KEY"
|
|
35
|
+
export GEMINI_API_KEY="$GEMINI_API_KEY_VALUE"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if [ -z "ANTIGRAVITY_API_KEY_VALUE" ]; then
|
|
39
|
+
echo "ANTIGRAVITY_API_KEY not found or is empty in $ENV_FILE."
|
|
40
|
+
else
|
|
41
|
+
echo "exported: ANTIGRAVITY_API_KEY"
|
|
42
|
+
export ANTIGRAVITY_API_KEY="$ANTIGRAVITY_API_KEY_VALUE"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [ -z "OMDB_API_KEY_VALUE" ]; then
|
|
46
|
+
echo "OMDB_API_KEY not found or is empty in $ENV_FILE."
|
|
47
|
+
else
|
|
48
|
+
echo "exported: OMDB_API_KEY"
|
|
49
|
+
export OMDB_API_KEY="$OMDB_API_KEY_VALUE"
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if [ -z "GEMINI_MODEL_VALUE" ]; then
|
|
54
|
+
echo "GEMINI_MODEL not found or is empty in $ENV_FILE."
|
|
55
|
+
else
|
|
56
|
+
echo "exported: GEMINI_MODEL=$GEMINI_MODEL_VALUE"
|
|
57
|
+
export GEMINI_MODEL="$GEMINI_MODEL_VALUE"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Export the variable for the current session
|
|
61
|
+
export GOOGLE_CLOUD_PROJECT="$GOOGLE_CLOUD_PROJECT_VALUE"
|
|
62
|
+
|
|
63
|
+
echo "exported: GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT"
|
package/package.json
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"name": "@mcpher/gas-fakes",
|
|
41
41
|
"author": "bruce mcpherson",
|
|
42
|
-
"version": "2.5.
|
|
42
|
+
"version": "2.5.6",
|
|
43
43
|
"license": "MIT",
|
|
44
44
|
"main": "main.js",
|
|
45
45
|
"description": "An implementation of the Google Workspace Apps Script runtime: Run native App Script Code on Node and Cloud Run",
|
|
@@ -4,13 +4,32 @@ import { gError } from '../../support/helpers.js';
|
|
|
4
4
|
import { slidesCacher } from '../../support/slidescacher.js';
|
|
5
5
|
import { Proxies } from '../../support/proxies.js';
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* @class FakeAdvSlidesPresentationsPages
|
|
9
|
+
*/
|
|
10
|
+
class FakeAdvSlidesPresentationsPages extends FakeAdvResource {
|
|
11
|
+
constructor(mainService) {
|
|
12
|
+
super(mainService, 'presentations', Syncit.fxSlides);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
//https://developers.google.com/workspace/slides/api/reference/rest/v1/presentations.pages/getThumbnail
|
|
17
|
+
getThumbnail(presentationId, pageObjectId, options) {
|
|
18
|
+
ScriptApp.__behavior.isAccessible(presentationId, 'Slides', 'read');
|
|
19
|
+
const { response, data } = this._call('getThumbnail', { presentationId, pageObjectId, ...options }, {}, 'pages');
|
|
20
|
+
gError(response, 'slides.presentations.pages', 'getThumbnail');
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
/**
|
|
8
26
|
* @class FakeAdvSlidesPresentations
|
|
9
27
|
*/
|
|
10
28
|
class FakeAdvSlidesPresentations extends FakeAdvResource {
|
|
11
29
|
constructor(mainService) {
|
|
12
30
|
super(mainService, 'presentations', Syncit.fxSlides);
|
|
13
|
-
this.slides = mainService
|
|
31
|
+
this.slides = mainService;
|
|
32
|
+
this.Pages = Proxies.guard(new FakeAdvSlidesPresentationsPages(mainService));
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
// Override 'get' to use the caching-enabled function fxSlidesGet.
|
|
@@ -4,7 +4,7 @@ const { is, isBlob , stringCircular, lobify} = Utils
|
|
|
4
4
|
import { getElementFactory } from './elementRegistry.js'
|
|
5
5
|
import { signatureArgs, notYetImplemented } from '../../support/helpers.js';
|
|
6
6
|
import { findItem } from './elementhelpers.js';
|
|
7
|
-
import { paragraphOptions, pageBreakOptions, tableOptions, textOptions, listItemOptions, imageOptions, positionedImageOptions } from './elementoptions.js';
|
|
7
|
+
import { horizontalRuleOptions, paragraphOptions, pageBreakOptions, tableOptions, textOptions, listItemOptions, imageOptions, positionedImageOptions } from './elementoptions.js';
|
|
8
8
|
import { deleteContentRange, createParagraphBullets, reverseUpdateContent, deleteParagraphBullets } from "./elementblasters.js";
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -430,10 +430,15 @@ export const createFootnote = (parent, text) => {
|
|
|
430
430
|
};
|
|
431
431
|
|
|
432
432
|
|
|
433
|
-
// THE API has no way of inserting a horizontal rule
|
|
434
|
-
// parking this for now - it'll need to be resurrected if this issue ever gets resolved
|
|
433
|
+
// THE API has no way of inserting a horizontal rule natively.
|
|
435
434
|
// https://issuetracker.google.com/issues/437825936
|
|
435
|
+
export const appendHorizontalRule = (self, horizontalRule) => {
|
|
436
|
+
return notYetImplemented('appendHorizontalRule');
|
|
437
|
+
};
|
|
436
438
|
|
|
439
|
+
export const insertHorizontalRule = (self, childIndex, horizontalRule) => {
|
|
440
|
+
return notYetImplemented('insertHorizontalRule');
|
|
441
|
+
};
|
|
437
442
|
|
|
438
443
|
export const appendText = (self, textOrTextElement) => {
|
|
439
444
|
return elementInserter(self, textOrTextElement, null, textOptions);
|
|
@@ -32,6 +32,9 @@ export const getElementProp = (se) => {
|
|
|
32
32
|
if (se.horizontalRule) return { prop: null, type: 'HORIZONTAL_RULE' };
|
|
33
33
|
if (se.footnoteReference) return { prop: null, type: 'FOOTNOTE_REFERENCE' };
|
|
34
34
|
if (se.inlineObjectElement) return { prop: null, type: 'INLINE_IMAGE' };
|
|
35
|
+
if (se.person) return { prop: null, type: 'PERSON' };
|
|
36
|
+
if (se.richLink) return { prop: null, type: 'RICH_LINK' };
|
|
37
|
+
if (se.date) return { prop: null, type: 'DATE' };
|
|
35
38
|
if (se.positionedObjectElement) return { prop: null, type: 'POSITIONED_IMAGE' };
|
|
36
39
|
|
|
37
40
|
if (se.body) {
|
|
@@ -53,18 +56,24 @@ const getTextRecursive = (twig, structure) => {
|
|
|
53
56
|
const item = structure.elementMap.get(twig.name);
|
|
54
57
|
if (!item) return '';
|
|
55
58
|
|
|
56
|
-
// Base case: Text element.
|
|
57
|
-
if (item.
|
|
58
|
-
return item.
|
|
59
|
+
// Base case: Text element.
|
|
60
|
+
if (item.textRun) {
|
|
61
|
+
return item.textRun.content || '';
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
// Recursive case: Container element.
|
|
62
|
-
|
|
65
|
+
// Note: For Paragraph, item.paragraph.elements will be processed as children via __twig.children
|
|
66
|
+
if (item.__twig && item.__twig.children && item.__twig.children.length > 0) {
|
|
63
67
|
return item.__twig.children
|
|
64
68
|
.map(childTwig => getTextRecursive(childTwig, structure))
|
|
65
69
|
.join('');
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
// Fallback for elements that might not have children in twig yet (e.g. Paragraph during initial map)
|
|
73
|
+
if (item.paragraph) {
|
|
74
|
+
return item.paragraph.elements.map(e => e.textRun?.content || '').join('');
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
return '';
|
|
69
78
|
};
|
|
70
79
|
|
|
@@ -74,11 +83,16 @@ const getTextRecursive = (twig, structure) => {
|
|
|
74
83
|
* @returns {string} The text content.
|
|
75
84
|
*/
|
|
76
85
|
export const getText = (element) => {
|
|
77
|
-
if (!element
|
|
78
|
-
|
|
86
|
+
if (!element) return '';
|
|
87
|
+
const item = element.__elementMapItem;
|
|
88
|
+
if (!item) return '';
|
|
89
|
+
|
|
90
|
+
if (element.__isDetached) {
|
|
79
91
|
let text = '';
|
|
80
92
|
|
|
81
|
-
if (item.
|
|
93
|
+
if (item.textRun) {
|
|
94
|
+
text = item.textRun.content || '';
|
|
95
|
+
} else if (item.paragraph) { // It's a Paragraph
|
|
82
96
|
text = item.paragraph.elements?.map(e => e.textRun?.content || '').join('') || '';
|
|
83
97
|
} else if (item.content) { // It's a TableCell
|
|
84
98
|
text = item.content.map(structuralElement =>
|
|
@@ -197,6 +211,133 @@ export const getAttributes = (element) => {
|
|
|
197
211
|
return attributes;
|
|
198
212
|
};
|
|
199
213
|
|
|
214
|
+
/**
|
|
215
|
+
* Converts a DocumentApp.Attribute map to Docs API style objects.
|
|
216
|
+
* @param {object} attributes The attributes to convert.
|
|
217
|
+
* @returns {{paragraphStyle: object, textStyle: object, paraFields: string, textFields: string}}
|
|
218
|
+
*/
|
|
219
|
+
export const attributesToStyle = (attributes) => {
|
|
220
|
+
const paragraphStyle = {};
|
|
221
|
+
const textStyle = {};
|
|
222
|
+
const paraFields = [];
|
|
223
|
+
const textFields = [];
|
|
224
|
+
|
|
225
|
+
const Attribute = DocumentApp.Attribute;
|
|
226
|
+
|
|
227
|
+
const hexToRgb = (hex) => {
|
|
228
|
+
if (!hex || !is.string(hex) || !hex.startsWith('#')) return null;
|
|
229
|
+
const r = parseInt(hex.slice(1, 3), 16) / 255;
|
|
230
|
+
const g = parseInt(hex.slice(3, 5), 16) / 255;
|
|
231
|
+
const b = parseInt(hex.slice(5, 7), 16) / 255;
|
|
232
|
+
return { color: { rgbColor: { red: r, green: g, blue: b } } };
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const alignmentMap = {
|
|
236
|
+
[DocumentApp.HorizontalAlignment.LEFT]: 'START',
|
|
237
|
+
[DocumentApp.HorizontalAlignment.CENTER]: 'CENTER',
|
|
238
|
+
[DocumentApp.HorizontalAlignment.RIGHT]: 'END',
|
|
239
|
+
[DocumentApp.HorizontalAlignment.JUSTIFIED]: 'JUSTIFY',
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
243
|
+
// Note: keys are likely strings like "BOLD", "HORIZONTAL_ALIGNMENT"
|
|
244
|
+
switch (key) {
|
|
245
|
+
case 'HORIZONTAL_ALIGNMENT':
|
|
246
|
+
case Attribute.HORIZONTAL_ALIGNMENT.toString():
|
|
247
|
+
paragraphStyle.alignment = alignmentMap[value] || value;
|
|
248
|
+
paraFields.push('alignment');
|
|
249
|
+
break;
|
|
250
|
+
case 'LEFT_TO_RIGHT':
|
|
251
|
+
case Attribute.LEFT_TO_RIGHT.toString():
|
|
252
|
+
paragraphStyle.direction = value ? 'LEFT_TO_RIGHT' : 'RIGHT_TO_LEFT';
|
|
253
|
+
paraFields.push('direction');
|
|
254
|
+
break;
|
|
255
|
+
case 'LINE_SPACING':
|
|
256
|
+
case Attribute.LINE_SPACING.toString():
|
|
257
|
+
paragraphStyle.lineSpacing = Math.round(value * 100);
|
|
258
|
+
paraFields.push('lineSpacing');
|
|
259
|
+
break;
|
|
260
|
+
case 'INDENT_START':
|
|
261
|
+
case Attribute.INDENT_START.toString():
|
|
262
|
+
paragraphStyle.indentStart = { magnitude: value, unit: 'PT' };
|
|
263
|
+
paraFields.push('indentStart');
|
|
264
|
+
break;
|
|
265
|
+
case 'INDENT_END':
|
|
266
|
+
case Attribute.INDENT_END.toString():
|
|
267
|
+
paragraphStyle.indentEnd = { magnitude: value, unit: 'PT' };
|
|
268
|
+
paraFields.push('indentEnd');
|
|
269
|
+
break;
|
|
270
|
+
case 'INDENT_FIRST_LINE':
|
|
271
|
+
case Attribute.INDENT_FIRST_LINE.toString():
|
|
272
|
+
paragraphStyle.indentFirstLine = { magnitude: value, unit: 'PT' };
|
|
273
|
+
paraFields.push('indentFirstLine');
|
|
274
|
+
break;
|
|
275
|
+
case 'SPACING_BEFORE':
|
|
276
|
+
case Attribute.SPACING_BEFORE.toString():
|
|
277
|
+
paragraphStyle.spaceAbove = { magnitude: value, unit: 'PT' };
|
|
278
|
+
paraFields.push('spaceAbove');
|
|
279
|
+
break;
|
|
280
|
+
case 'SPACING_AFTER':
|
|
281
|
+
case Attribute.SPACING_AFTER.toString():
|
|
282
|
+
paragraphStyle.spaceBelow = { magnitude: value, unit: 'PT' };
|
|
283
|
+
paraFields.push('spaceBelow');
|
|
284
|
+
break;
|
|
285
|
+
case 'BACKGROUND_COLOR':
|
|
286
|
+
case Attribute.BACKGROUND_COLOR.toString():
|
|
287
|
+
textStyle.backgroundColor = hexToRgb(value);
|
|
288
|
+
textFields.push('backgroundColor');
|
|
289
|
+
break;
|
|
290
|
+
case 'BOLD':
|
|
291
|
+
case Attribute.BOLD.toString():
|
|
292
|
+
textStyle.bold = value;
|
|
293
|
+
textFields.push('bold');
|
|
294
|
+
break;
|
|
295
|
+
case 'FONT_FAMILY':
|
|
296
|
+
case Attribute.FONT_FAMILY.toString():
|
|
297
|
+
textStyle.weightedFontFamily = { fontFamily: value };
|
|
298
|
+
textFields.push('weightedFontFamily');
|
|
299
|
+
break;
|
|
300
|
+
case 'FONT_SIZE':
|
|
301
|
+
case Attribute.FONT_SIZE.toString():
|
|
302
|
+
textStyle.fontSize = { magnitude: value, unit: 'PT' };
|
|
303
|
+
textFields.push('fontSize');
|
|
304
|
+
break;
|
|
305
|
+
case 'FOREGROUND_COLOR':
|
|
306
|
+
case Attribute.FOREGROUND_COLOR.toString():
|
|
307
|
+
textStyle.foregroundColor = hexToRgb(value);
|
|
308
|
+
textFields.push('foregroundColor');
|
|
309
|
+
break;
|
|
310
|
+
case 'ITALIC':
|
|
311
|
+
case Attribute.ITALIC.toString():
|
|
312
|
+
textStyle.italic = value;
|
|
313
|
+
textFields.push('italic');
|
|
314
|
+
break;
|
|
315
|
+
case 'STRIKETHROUGH':
|
|
316
|
+
case Attribute.STRIKETHROUGH.toString():
|
|
317
|
+
textStyle.strikethrough = value;
|
|
318
|
+
textFields.push('strikethrough');
|
|
319
|
+
break;
|
|
320
|
+
case 'UNDERLINE':
|
|
321
|
+
case Attribute.UNDERLINE.toString():
|
|
322
|
+
textStyle.underline = value;
|
|
323
|
+
textFields.push('underline');
|
|
324
|
+
break;
|
|
325
|
+
case 'LINK_URL':
|
|
326
|
+
case Attribute.LINK_URL.toString():
|
|
327
|
+
textStyle.link = { url: value };
|
|
328
|
+
textFields.push('link');
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
paragraphStyle,
|
|
335
|
+
textStyle,
|
|
336
|
+
paraFields: paraFields.join(','),
|
|
337
|
+
textFields: textFields.join(',')
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
|
|
200
341
|
export const findItem = (elementMap, type, startIndex, segmentId) => {
|
|
201
342
|
const item = Array.from(elementMap.values()).find(f => {
|
|
202
343
|
// segmentId from API is empty string for body, but we might pass null. Normalize.
|
|
@@ -58,6 +58,34 @@ const handleTextless = (loc, isAppend, self, type, extras = {}) => {
|
|
|
58
58
|
|
|
59
59
|
break;
|
|
60
60
|
|
|
61
|
+
case 'HORIZONTAL_RULE':
|
|
62
|
+
reqs.push({
|
|
63
|
+
insertText: {
|
|
64
|
+
location,
|
|
65
|
+
text: '\n'
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
reqs.push({
|
|
69
|
+
updateParagraphStyle: {
|
|
70
|
+
range: {
|
|
71
|
+
startIndex: loc.index,
|
|
72
|
+
endIndex: loc.index + 1,
|
|
73
|
+
segmentId: loc.segmentId,
|
|
74
|
+
tabId: loc.tabId
|
|
75
|
+
},
|
|
76
|
+
paragraphStyle: {
|
|
77
|
+
borderBottom: {
|
|
78
|
+
color: { color: { rgbColor: { blue: 0, green: 0, red: 0 } } },
|
|
79
|
+
width: { magnitude: 1, unit: 'PT' },
|
|
80
|
+
padding: { magnitude: 0, unit: 'PT' },
|
|
81
|
+
dashStyle: 'SOLID'
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
fields: 'borderBottom'
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
|
|
61
89
|
default:
|
|
62
90
|
throw new Error(`unknown type ${type} in handleTextless `)
|
|
63
91
|
}
|
|
@@ -263,6 +291,18 @@ export const pageBreakOptions = {
|
|
|
263
291
|
|
|
264
292
|
};
|
|
265
293
|
|
|
294
|
+
export const horizontalRuleOptions = {
|
|
295
|
+
elementType: ElementType.HORIZONTAL_RULE,
|
|
296
|
+
insertMethodSignature: 'DocumentApp.Body.horizontalRule',
|
|
297
|
+
packCanBeNull: true,
|
|
298
|
+
canAcceptText: false,
|
|
299
|
+
getMainRequest: ({ location: loc, isAppend, self, leading }) => {
|
|
300
|
+
return handleTextless(loc, isAppend, self, 'HORIZONTAL_RULE')
|
|
301
|
+
},
|
|
302
|
+
getStyleRequests: null,
|
|
303
|
+
findType: ElementType.PARAGRAPH.toString()
|
|
304
|
+
};
|
|
305
|
+
|
|
266
306
|
export const tableOptions = {
|
|
267
307
|
elementType: ElementType.TABLE,
|
|
268
308
|
packCanBeNull: true,
|
|
@@ -17,5 +17,12 @@ import './fakefootnote.js';
|
|
|
17
17
|
import './fakeinlineimage.js';
|
|
18
18
|
import './fakefootnotesection.js';
|
|
19
19
|
import './fakepositionedimage.js';
|
|
20
|
+
import './fakedate.js';
|
|
21
|
+
import './fakeperson.js';
|
|
22
|
+
import './fakerichlink.js';
|
|
23
|
+
import './fakeequation.js';
|
|
24
|
+
import './fakeequationfunction.js';
|
|
25
|
+
import './fakeequationfunctionargumentseparator.js';
|
|
26
|
+
import './fakeequationsymbol.js';
|
|
20
27
|
// As you create more element types (e.g., Table, ListItem), import them here.
|
|
21
28
|
// import './faketable.js';
|
|
@@ -41,8 +41,6 @@ export class FakeBookmark extends FakeContainerElement {
|
|
|
41
41
|
*/
|
|
42
42
|
getId() {
|
|
43
43
|
const item = this.__elementMapItem;
|
|
44
|
-
// The name is the full named range name, e.g., "kix.abcdef123".
|
|
45
|
-
// The ID is the part after "kix.".
|
|
46
44
|
return item.name.startsWith(BOOKMARK_PREFIX) ? item.name.substring(BOOKMARK_PREFIX.length) : item.name;
|
|
47
45
|
}
|
|
48
46
|
|
|
@@ -67,7 +65,7 @@ export class FakeBookmark extends FakeContainerElement {
|
|
|
67
65
|
}
|
|
68
66
|
|
|
69
67
|
const factory = getElementFactory(containingElementItem.__type);
|
|
70
|
-
const element = factory(shadow
|
|
68
|
+
const element = factory(shadow, containingElementItem.__name);
|
|
71
69
|
const offset = startIndex - containingElementItem.startIndex;
|
|
72
70
|
|
|
73
71
|
return newFakePosition(element, offset);
|
|
@@ -10,6 +10,7 @@ const { is } = Utils;
|
|
|
10
10
|
import { getElementProp } from './elementhelpers.js';
|
|
11
11
|
import { FakeElement } from './fakeelement.js';
|
|
12
12
|
import { createFootnote } from './appenderhelpers.js';
|
|
13
|
+
import { newFakeRangeElement } from './fakerangeelement.js';
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Creates a new proxied FakeContainerElement instance.
|
|
@@ -183,6 +184,133 @@ export class FakeContainerElement extends FakeElement {
|
|
|
183
184
|
return elements;
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Clears the contents of the element.
|
|
189
|
+
* @returns {GoogleAppsScript.Document.ContainerElement} The current element.
|
|
190
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#clear()
|
|
191
|
+
*/
|
|
192
|
+
clear() {
|
|
193
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.clear');
|
|
194
|
+
if (nargs !== 0) matchThrow();
|
|
195
|
+
|
|
196
|
+
// Snapshot the children names to avoid issues with index shifting during removal
|
|
197
|
+
const childrenNames = this.__twig.children.map(c => c.name);
|
|
198
|
+
childrenNames.forEach(name => {
|
|
199
|
+
newFakeElement(this.shadowDocument, name).removeFromParent();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
return this;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Obtains a Text version of the current element, for editing.
|
|
207
|
+
* @returns {GoogleAppsScript.Document.Text} a text version of the current element
|
|
208
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#editAsText()
|
|
209
|
+
*/
|
|
210
|
+
editAsText() {
|
|
211
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.editAsText');
|
|
212
|
+
if (nargs !== 0) matchThrow();
|
|
213
|
+
// For now, we'll mark it as not implemented but mark as draft.
|
|
214
|
+
throw new Error('editAsText() is not yet implemented in gas-fakes');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Searches the contents of the element for a descendant of the specified type.
|
|
219
|
+
* @param {GoogleAppsScript.Document.ElementType} elementType The type of element to search for.
|
|
220
|
+
* @param {GoogleAppsScript.Document.RangeElement} [from] The element to start searching from.
|
|
221
|
+
* @returns {GoogleAppsScript.Document.RangeElement | null} A search result indicating the position of the search element.
|
|
222
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#findElement(ElementType,RangeElement)
|
|
223
|
+
*/
|
|
224
|
+
findElement(elementType, from = null) {
|
|
225
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.findElement');
|
|
226
|
+
if (nargs < 1 || nargs > 2) matchThrow();
|
|
227
|
+
|
|
228
|
+
const searchFromElement = from ? from.getElement() : null;
|
|
229
|
+
let foundStart = !searchFromElement;
|
|
230
|
+
|
|
231
|
+
const findRecursive = (container) => {
|
|
232
|
+
const numChildren = container.getNumChildren();
|
|
233
|
+
for (let i = 0; i < numChildren; i++) {
|
|
234
|
+
const child = container.getChild(i);
|
|
235
|
+
|
|
236
|
+
if (!foundStart) {
|
|
237
|
+
if (child.__name === searchFromElement.__name) {
|
|
238
|
+
foundStart = true;
|
|
239
|
+
} else if (child.getNumChildren) {
|
|
240
|
+
const result = findRecursive(child);
|
|
241
|
+
if (result) return result;
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (child.getType().toString() === elementType.toString()) {
|
|
247
|
+
return newFakeRangeElement({ element: child });
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (child.getNumChildren) {
|
|
251
|
+
const result = findRecursive(child);
|
|
252
|
+
if (result) return result;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return findRecursive(this);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Searches the contents of the element for the specified text pattern using regular expressions.
|
|
263
|
+
* @param {string} searchPattern The text pattern to search for.
|
|
264
|
+
* @param {GoogleAppsScript.Document.RangeElement} [from] The element to start searching from.
|
|
265
|
+
* @returns {GoogleAppsScript.Document.RangeElement | null} a search result indicating the position of the search text, or null if there is no match
|
|
266
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#findText(String,RangeElement)
|
|
267
|
+
*/
|
|
268
|
+
findText(searchPattern, from = null) {
|
|
269
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.findText');
|
|
270
|
+
if (nargs < 1 || nargs > 2) matchThrow();
|
|
271
|
+
|
|
272
|
+
const searchFromElement = from ? from.getElement() : null;
|
|
273
|
+
let foundStart = !searchFromElement;
|
|
274
|
+
const regex = new RegExp(searchPattern);
|
|
275
|
+
|
|
276
|
+
const findRecursive = (container) => {
|
|
277
|
+
const numChildren = container.getNumChildren();
|
|
278
|
+
for (let i = 0; i < numChildren; i++) {
|
|
279
|
+
const child = container.getChild(i);
|
|
280
|
+
|
|
281
|
+
if (!foundStart) {
|
|
282
|
+
if (child.__name === searchFromElement.__name) {
|
|
283
|
+
foundStart = true;
|
|
284
|
+
} else if (child.getNumChildren) {
|
|
285
|
+
const result = findRecursive(child);
|
|
286
|
+
if (result) return result;
|
|
287
|
+
}
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (child.getType().toString() === 'TEXT') {
|
|
292
|
+
const text = child.getText();
|
|
293
|
+
const match = regex.exec(text);
|
|
294
|
+
if (match) {
|
|
295
|
+
return newFakeRangeElement({
|
|
296
|
+
element: child,
|
|
297
|
+
startOffset: match.index,
|
|
298
|
+
endOffsetInclusive: match.index + match[0].length - 1
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (child.getNumChildren) {
|
|
304
|
+
const result = findRecursive(child);
|
|
305
|
+
if (result) return result;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
return findRecursive(this);
|
|
312
|
+
}
|
|
313
|
+
|
|
186
314
|
/**
|
|
187
315
|
* Retrieves the child element at the specified index.
|
|
188
316
|
* @param {number} childIndex The zero-based index of the child element to retrieve.
|
|
@@ -250,6 +378,66 @@ export class FakeContainerElement extends FakeElement {
|
|
|
250
378
|
return newFakeElement(this.shadowDocument, childTwig.name).__cast();
|
|
251
379
|
}
|
|
252
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Retrieves the contents of the element as a text string.
|
|
383
|
+
* @returns {string} the contents of the element as text string
|
|
384
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#getText()
|
|
385
|
+
*/
|
|
386
|
+
getText() {
|
|
387
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.getText');
|
|
388
|
+
if (nargs !== 0) matchThrow();
|
|
389
|
+
|
|
390
|
+
return getText(this);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Gets the text alignment.
|
|
395
|
+
* @returns {GoogleAppsScript.Document.TextAlignment | null} the type of text alignment
|
|
396
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#getTextAlignment()
|
|
397
|
+
*/
|
|
398
|
+
getTextAlignment() {
|
|
399
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.getTextAlignment');
|
|
400
|
+
if (nargs !== 0) matchThrow();
|
|
401
|
+
|
|
402
|
+
const attrs = this.getAttributes();
|
|
403
|
+
// ContainerElement doesn't have a single text alignment, it's usually paragraph-level.
|
|
404
|
+
// However, if we are a Paragraph, this should work.
|
|
405
|
+
return attrs[DocumentApp.Attribute.TEXT_ALIGNMENT] || null;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Sets the text alignment.
|
|
410
|
+
* @param {GoogleAppsScript.Document.TextAlignment} textAlignment The text alignment to set.
|
|
411
|
+
* @returns {GoogleAppsScript.Document.ContainerElement} the current element
|
|
412
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#setTextAlignment(TextAlignment)
|
|
413
|
+
*/
|
|
414
|
+
setTextAlignment(textAlignment) {
|
|
415
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.setTextAlignment');
|
|
416
|
+
if (nargs !== 1) matchThrow();
|
|
417
|
+
|
|
418
|
+
this.setAttributes({
|
|
419
|
+
[DocumentApp.Attribute.TEXT_ALIGNMENT]: textAlignment
|
|
420
|
+
});
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Replaces all occurrences of a search pattern with a replacement string.
|
|
426
|
+
* @param {string} searchPattern The text pattern to search for.
|
|
427
|
+
* @param {string} replacement The replacement string.
|
|
428
|
+
* @returns {GoogleAppsScript.Document.ContainerElement} the current element
|
|
429
|
+
* @see https://developers.google.com/apps-script/reference/document/container-element#replaceText(String,String)
|
|
430
|
+
*/
|
|
431
|
+
replaceText(searchPattern, replacement) {
|
|
432
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'ContainerElement.replaceText');
|
|
433
|
+
if (nargs !== 2 || !is.string(searchPattern) || !is.string(replacement)) matchThrow();
|
|
434
|
+
|
|
435
|
+
// replaceText in GAS is global if called on Body, or scoped if called on other containers.
|
|
436
|
+
// Implementing scoped replaceText via API is complex (requires finding and replacing chunks).
|
|
437
|
+
// For now, let's mark it.
|
|
438
|
+
throw new Error('replaceText() is not yet implemented in gas-fakes');
|
|
439
|
+
}
|
|
440
|
+
|
|
253
441
|
/**
|
|
254
442
|
* Gets the index of a given child element.
|
|
255
443
|
* @param {GoogleAppsScript.Document.Element} child The child element to find.
|