pdfdancer-client-typescript 1.0.14 → 1.0.16

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.
@@ -61,6 +61,213 @@ const DEBUG =
61
61
  (process.env.PDFDANCER_CLIENT_DEBUG ?? '') === '1' ||
62
62
  (process.env.PDFDANCER_CLIENT_DEBUG ?? '').toLowerCase() === 'yes';
63
63
 
64
+ /**
65
+ * Configuration for retry mechanism on REST API calls.
66
+ */
67
+ export interface RetryConfig {
68
+ /**
69
+ * Maximum number of retry attempts (default: 3)
70
+ */
71
+ maxRetries?: number;
72
+
73
+ /**
74
+ * Initial delay in milliseconds before first retry (default: 1000)
75
+ * Subsequent delays use exponential backoff
76
+ */
77
+ initialDelay?: number;
78
+
79
+ /**
80
+ * Maximum delay in milliseconds between retries (default: 10000)
81
+ */
82
+ maxDelay?: number;
83
+
84
+ /**
85
+ * HTTP status codes that should trigger a retry (default: [429, 500, 502, 503, 504])
86
+ */
87
+ retryableStatusCodes?: number[];
88
+
89
+ /**
90
+ * Whether to retry on network errors (connection failures, timeouts) (default: true)
91
+ */
92
+ retryOnNetworkError?: boolean;
93
+
94
+ /**
95
+ * Exponential backoff multiplier (default: 2)
96
+ */
97
+ backoffMultiplier?: number;
98
+
99
+ /**
100
+ * Whether to add random jitter to retry delays to prevent thundering herd (default: true)
101
+ */
102
+ useJitter?: boolean;
103
+
104
+ /**
105
+ * Whether to respect Retry-After headers from server responses (default: true)
106
+ * When enabled, the client will use the server-specified delay instead of exponential backoff
107
+ * for responses that include a Retry-After header (typically 429 or 503 responses)
108
+ */
109
+ respectRetryAfter?: boolean;
110
+ }
111
+
112
+ /**
113
+ * Default retry configuration
114
+ */
115
+ const DEFAULT_RETRY_CONFIG: Required<RetryConfig> = {
116
+ maxRetries: 3,
117
+ initialDelay: 1000,
118
+ maxDelay: 10000,
119
+ retryableStatusCodes: [429, 500, 502, 503, 504],
120
+ retryOnNetworkError: true,
121
+ backoffMultiplier: 2,
122
+ useJitter: true,
123
+ respectRetryAfter: true
124
+ };
125
+
126
+ /**
127
+ * Static helper function for retry logic with exponential backoff.
128
+ * Used by static methods that don't have access to instance retry config.
129
+ */
130
+ async function fetchWithRetry(
131
+ url: string,
132
+ // eslint-disable-next-line no-undef
133
+ options: RequestInit,
134
+ retryConfig: Required<RetryConfig>,
135
+ context: string = 'request'
136
+ ): Promise<Response> {
137
+ let lastError: Error | null = null;
138
+ let lastResponse: Response | null = null;
139
+
140
+ for (let attempt = 0; attempt <= retryConfig.maxRetries; attempt++) {
141
+ try {
142
+ const response = await fetch(url, options);
143
+
144
+ // Check if we should retry based on status code
145
+ if (!response.ok && retryConfig.retryableStatusCodes.includes(response.status)) {
146
+ lastResponse = response;
147
+
148
+ // If this is not the last attempt, wait and retry
149
+ if (attempt < retryConfig.maxRetries) {
150
+ let delay: number;
151
+ let delaySource = 'exponential backoff';
152
+
153
+ // Check for Retry-After header if configured
154
+ if (retryConfig.respectRetryAfter) {
155
+ const retryAfterDelay = parseRetryAfter(response);
156
+ if (retryAfterDelay !== null) {
157
+ // Use Retry-After header value, but cap at maxDelay
158
+ delay = Math.min(retryAfterDelay, retryConfig.maxDelay);
159
+ delaySource = 'Retry-After header';
160
+ } else {
161
+ // Fall back to exponential backoff
162
+ delay = calculateRetryDelay(attempt, retryConfig);
163
+ }
164
+ } else {
165
+ // Use exponential backoff
166
+ delay = calculateRetryDelay(attempt, retryConfig);
167
+ }
168
+
169
+ if (DEBUG) {
170
+ console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (status: ${response.status}, source: ${delaySource})`);
171
+ }
172
+ await sleep(delay);
173
+ continue;
174
+ }
175
+ }
176
+
177
+ // Request succeeded or non-retryable error
178
+ return response;
179
+
180
+ } catch (error) {
181
+ lastError = error as Error;
182
+
183
+ // Check if this is a network error and we should retry
184
+ if (retryConfig.retryOnNetworkError && attempt < retryConfig.maxRetries) {
185
+ const delay = calculateRetryDelay(attempt, retryConfig);
186
+ if (DEBUG) {
187
+ const errorMessage = error instanceof Error ? error.message : String(error);
188
+ console.log(`${Date.now() / 1000}|Retry attempt ${attempt + 1}/${retryConfig.maxRetries} for ${context} after ${delay}ms (error: ${errorMessage})`);
189
+ }
190
+ await sleep(delay);
191
+ continue;
192
+ }
193
+
194
+ // Non-retryable error or last attempt
195
+ throw error;
196
+ }
197
+ }
198
+
199
+ // If we exhausted all retries due to retryable status codes, return the last response
200
+ if (lastResponse) {
201
+ return lastResponse;
202
+ }
203
+
204
+ // If we exhausted all retries due to network errors, throw the last error
205
+ if (lastError) {
206
+ throw lastError;
207
+ }
208
+
209
+ // This should never happen, but just in case
210
+ throw new Error('Unexpected retry exhaustion');
211
+ }
212
+
213
+ /**
214
+ * Parses the Retry-After header from a response.
215
+ * Supports both delay-seconds (integer) and HTTP-date formats.
216
+ * Returns the delay in milliseconds, or null if the header is invalid or missing.
217
+ */
218
+ function parseRetryAfter(response: Response): number | null {
219
+ const retryAfter = response.headers.get('Retry-After');
220
+ if (!retryAfter) {
221
+ return null;
222
+ }
223
+
224
+ // Try parsing as delay-seconds (integer)
225
+ const delaySeconds = parseInt(retryAfter, 10);
226
+ if (!isNaN(delaySeconds) && delaySeconds >= 0) {
227
+ return delaySeconds * 1000; // Convert to milliseconds
228
+ }
229
+
230
+ // Try parsing as HTTP-date
231
+ try {
232
+ const retryDate = new Date(retryAfter);
233
+ if (!isNaN(retryDate.getTime())) {
234
+ const now = Date.now();
235
+ const delay = retryDate.getTime() - now;
236
+ // Only return positive delays
237
+ return delay > 0 ? delay : 0;
238
+ }
239
+ } catch {
240
+ // Invalid date format
241
+ }
242
+
243
+ return null;
244
+ }
245
+
246
+ /**
247
+ * Calculates the delay for the next retry attempt using exponential backoff.
248
+ */
249
+ function calculateRetryDelay(attemptNumber: number, retryConfig: Required<RetryConfig>): number {
250
+ // Calculate base delay: initialDelay * (backoffMultiplier ^ attemptNumber)
251
+ let delay = retryConfig.initialDelay * Math.pow(retryConfig.backoffMultiplier, attemptNumber);
252
+
253
+ // Cap at maxDelay
254
+ delay = Math.min(delay, retryConfig.maxDelay);
255
+
256
+ // Add jitter if enabled (randomize between 50% and 100% of calculated delay)
257
+ if (retryConfig.useJitter) {
258
+ delay = delay * (0.5 + Math.random() * 0.5);
259
+ }
260
+
261
+ return Math.floor(delay);
262
+ }
263
+
264
+ /**
265
+ * Sleep for the specified number of milliseconds.
266
+ */
267
+ function sleep(ms: number): Promise<void> {
268
+ return new Promise(resolve => setTimeout(resolve, ms));
269
+ }
270
+
64
271
  /**
65
272
  * Generate a timestamp string in the format expected by the API.
66
273
  * Format: YYYY-MM-DDTHH:MM:SS.ffffffZ (with microseconds)
@@ -326,6 +533,93 @@ class PageClient {
326
533
  async getSnapshot(types?: ObjectType[]): Promise<PageSnapshot> {
327
534
  return this._client.getPageSnapshot(this._pageIndex, types);
328
535
  }
536
+
537
+ // Singular convenience methods - return the first element or null
538
+
539
+ async selectPath() {
540
+ const paths = await this.selectPaths();
541
+ return paths.length > 0 ? paths[0] : null;
542
+ }
543
+
544
+ async selectPathAt(x: number, y: number, tolerance: number = 0) {
545
+ const paths = await this.selectPathsAt(x, y, tolerance);
546
+ return paths.length > 0 ? paths[0] : null;
547
+ }
548
+
549
+ async selectImage() {
550
+ const images = await this.selectImages();
551
+ return images.length > 0 ? images[0] : null;
552
+ }
553
+
554
+ async selectImageAt(x: number, y: number, tolerance: number = 0) {
555
+ const images = await this.selectImagesAt(x, y, tolerance);
556
+ return images.length > 0 ? images[0] : null;
557
+ }
558
+
559
+ async selectForm() {
560
+ const forms = await this.selectForms();
561
+ return forms.length > 0 ? forms[0] : null;
562
+ }
563
+
564
+ async selectFormAt(x: number, y: number, tolerance: number = 0) {
565
+ const forms = await this.selectFormsAt(x, y, tolerance);
566
+ return forms.length > 0 ? forms[0] : null;
567
+ }
568
+
569
+ async selectFormField() {
570
+ const fields = await this.selectFormFields();
571
+ return fields.length > 0 ? fields[0] : null;
572
+ }
573
+
574
+ async selectFormFieldAt(x: number, y: number, tolerance: number = 0) {
575
+ const fields = await this.selectFormFieldsAt(x, y, tolerance);
576
+ return fields.length > 0 ? fields[0] : null;
577
+ }
578
+
579
+ async selectFormFieldByName(fieldName: string) {
580
+ const fields = await this.selectFormFieldsByName(fieldName);
581
+ return fields.length > 0 ? fields[0] : null;
582
+ }
583
+
584
+ async selectParagraph() {
585
+ const paragraphs = await this.selectParagraphs();
586
+ return paragraphs.length > 0 ? paragraphs[0] : null;
587
+ }
588
+
589
+ async selectParagraphStartingWith(text: string) {
590
+ const paragraphs = await this.selectParagraphsStartingWith(text);
591
+ return paragraphs.length > 0 ? paragraphs[0] : null;
592
+ }
593
+
594
+ async selectParagraphMatching(pattern: string) {
595
+ const paragraphs = await this.selectParagraphsMatching(pattern);
596
+ return paragraphs.length > 0 ? paragraphs[0] : null;
597
+ }
598
+
599
+ async selectParagraphAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
600
+ const paragraphs = await this.selectParagraphsAt(x, y, tolerance);
601
+ return paragraphs.length > 0 ? paragraphs[0] : null;
602
+ }
603
+
604
+ async selectTextLine() {
605
+ const lines = await this.selectTextLines();
606
+ return lines.length > 0 ? lines[0] : null;
607
+ }
608
+
609
+ async selectTextLineStartingWith(text: string) {
610
+ const lines = await this.selectTextLinesStartingWith(text);
611
+ return lines.length > 0 ? lines[0] : null;
612
+ }
613
+
614
+ async selectTextLineMatching(pattern: string) {
615
+ const lines = await this.selectTextLinesMatching(pattern);
616
+ return lines.length > 0 ? lines[0] : null;
617
+ }
618
+
619
+ async selectTextLineAt(x: number, y: number, tolerance: number = DEFAULT_TOLERANCE) {
620
+ const lines = await this.selectTextLinesAt(x, y, tolerance);
621
+ return lines.length > 0 ? lines[0] : null;
622
+ }
329
623
  }
330
624
 
331
625
  // noinspection ExceptionCaughtLocallyJS,JSUnusedLocalSymbols
@@ -344,6 +638,7 @@ export class PDFDancer {
344
638
  private _sessionId!: string;
345
639
  private _userId?: string;
346
640
  private _fingerprintCache?: string;
641
+ private _retryConfig: Required<RetryConfig>;
347
642
 
348
643
  // Snapshot caches for optimizing find operations
349
644
  private _documentSnapshotCache: DocumentSnapshot | null = null;
@@ -357,9 +652,10 @@ export class PDFDancer {
357
652
  */
358
653
  private constructor(
359
654
  token: string,
360
- pdfData: Uint8Array | File | ArrayBuffer,
655
+ pdfData: Uint8Array | File | ArrayBuffer | string,
361
656
  baseUrl: string | null = null,
362
- readTimeout: number = 60000
657
+ readTimeout: number = 60000,
658
+ retryConfig?: RetryConfig
363
659
  ) {
364
660
 
365
661
  if (!token || !token.trim()) {
@@ -384,6 +680,12 @@ export class PDFDancer {
384
680
  this._baseUrl = resolvedBaseUrl.replace(/\/$/, ''); // Remove trailing slash
385
681
  this._readTimeout = readTimeout;
386
682
 
683
+ // Merge retry config with defaults
684
+ this._retryConfig = {
685
+ ...DEFAULT_RETRY_CONFIG,
686
+ ...retryConfig
687
+ };
688
+
387
689
  // Process PDF data with validation
388
690
  this._pdfBytes = this._processPdfData(pdfData);
389
691
 
@@ -402,7 +704,13 @@ export class PDFDancer {
402
704
  return this;
403
705
  }
404
706
 
405
- static async open(pdfData: Uint8Array, token?: string, baseUrl?: string, timeout?: number): Promise<PDFDancer> {
707
+ static async open(
708
+ pdfData: Uint8Array,
709
+ token?: string,
710
+ baseUrl?: string,
711
+ timeout?: number,
712
+ retryConfig?: RetryConfig
713
+ ): Promise<PDFDancer> {
406
714
  const resolvedBaseUrl =
407
715
  baseUrl ??
408
716
  process.env.PDFDANCER_BASE_URL ??
@@ -414,7 +722,7 @@ export class PDFDancer {
414
722
  resolvedToken = await PDFDancer._obtainAnonymousToken(resolvedBaseUrl, resolvedTimeout);
415
723
  }
416
724
 
417
- const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout);
725
+ const client = new PDFDancer(resolvedToken, pdfData, resolvedBaseUrl, resolvedTimeout, retryConfig);
418
726
  return await client.init();
419
727
  }
420
728
 
@@ -428,6 +736,7 @@ export class PDFDancer {
428
736
  * @param token Authentication token (optional, can use PDFDANCER_TOKEN env var)
429
737
  * @param baseUrl Base URL for the PDFDancer API (optional)
430
738
  * @param timeout Request timeout in milliseconds (default: 60000)
739
+ * @param retryConfig Retry configuration (optional, uses defaults if not specified)
431
740
  */
432
741
  static async new(
433
742
  options?: {
@@ -437,7 +746,8 @@ export class PDFDancer {
437
746
  },
438
747
  token?: string,
439
748
  baseUrl?: string,
440
- timeout?: number
749
+ timeout?: number,
750
+ retryConfig?: RetryConfig
441
751
  ): Promise<PDFDancer> {
442
752
  const resolvedBaseUrl =
443
753
  baseUrl ??
@@ -471,18 +781,23 @@ export class PDFDancer {
471
781
  // Generate fingerprint for this request
472
782
  const fingerprint = await generateFingerprint();
473
783
 
474
- // Make request to create endpoint
475
- const response = await fetch(url, {
476
- method: 'POST',
477
- headers: {
478
- 'Authorization': `Bearer ${resolvedToken}`,
479
- 'Content-Type': 'application/json',
480
- 'X-Generated-At': generateTimestamp(),
481
- 'X-Fingerprint': fingerprint
784
+ // Make request to create endpoint with retry logic
785
+ const response = await fetchWithRetry(
786
+ url,
787
+ {
788
+ method: 'POST',
789
+ headers: {
790
+ 'Authorization': `Bearer ${resolvedToken}`,
791
+ 'Content-Type': 'application/json',
792
+ 'X-Generated-At': generateTimestamp(),
793
+ 'X-Fingerprint': fingerprint
794
+ },
795
+ body: JSON.stringify(createRequest.toDict()),
796
+ signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
482
797
  },
483
- body: JSON.stringify(createRequest.toDict()),
484
- signal: resolvedTimeout > 0 ? AbortSignal.timeout(resolvedTimeout) : undefined
485
- });
798
+ DEFAULT_RETRY_CONFIG,
799
+ 'POST /session/new'
800
+ );
486
801
 
487
802
  logGeneratedAtHeader(response, 'POST', '/session/new');
488
803
 
@@ -503,6 +818,11 @@ export class PDFDancer {
503
818
  client._readTimeout = resolvedTimeout;
504
819
  client._pdfBytes = new Uint8Array();
505
820
  client._sessionId = sessionId;
821
+ // Initialize retry config
822
+ client._retryConfig = {
823
+ ...DEFAULT_RETRY_CONFIG,
824
+ ...retryConfig
825
+ };
506
826
  // Initialize caches
507
827
  client._documentSnapshotCache = null;
508
828
  client._pageSnapshotCache = new Map();
@@ -523,15 +843,20 @@ export class PDFDancer {
523
843
 
524
844
  try {
525
845
  const fingerprint = await generateFingerprint();
526
- const response = await fetch(url, {
527
- method: 'POST',
528
- headers: {
529
- 'Content-Type': 'application/json',
530
- 'X-Fingerprint': fingerprint,
531
- 'X-Generated-At': generateTimestamp()
846
+ const response = await fetchWithRetry(
847
+ url,
848
+ {
849
+ method: 'POST',
850
+ headers: {
851
+ 'Content-Type': 'application/json',
852
+ 'X-Fingerprint': fingerprint,
853
+ 'X-Generated-At': generateTimestamp()
854
+ },
855
+ signal: timeout > 0 ? AbortSignal.timeout(timeout) : undefined
532
856
  },
533
- signal: timeout > 0 ? AbortSignal.timeout(timeout) : undefined
534
- });
857
+ DEFAULT_RETRY_CONFIG,
858
+ 'POST /keys/anon'
859
+ );
535
860
 
536
861
  if (!response.ok) {
537
862
  const errorText = await response.text().catch(() => '');
@@ -561,7 +886,7 @@ export class PDFDancer {
561
886
  /**
562
887
  * Process PDF data from various input types with strict validation.
563
888
  */
564
- private _processPdfData(pdfData: Uint8Array | File | ArrayBuffer): Uint8Array {
889
+ private _processPdfData(pdfData: Uint8Array | File | ArrayBuffer | string): Uint8Array {
565
890
  if (!pdfData) {
566
891
  throw new ValidationException("PDF data cannot be null");
567
892
  }
@@ -581,6 +906,16 @@ export class PDFDancer {
581
906
  } else if (pdfData instanceof File) {
582
907
  // Note: File reading will be handled asynchronously in the session creation
583
908
  return new Uint8Array(); // Placeholder, will be replaced in _createSession
909
+ } else if (typeof pdfData === 'string') {
910
+ // Handle string as filepath
911
+ if (!fs.existsSync(pdfData)) {
912
+ throw new ValidationException(`PDF file not found: ${pdfData}`);
913
+ }
914
+ const fileData = new Uint8Array(fs.readFileSync(pdfData));
915
+ if (fileData.length === 0) {
916
+ throw new ValidationException("PDF file is empty");
917
+ }
918
+ return fileData;
584
919
  } else {
585
920
  throw new ValidationException(`Unsupported PDF data type: ${typeof pdfData}`);
586
921
  }
@@ -662,16 +997,20 @@ export class PDFDancer {
662
997
 
663
998
  const fingerprint = await this._getFingerprint();
664
999
 
665
- const response = await fetch(this._buildUrl('/session/create'), {
666
- method: 'POST',
667
- headers: {
668
- 'Authorization': `Bearer ${this._token}`,
669
- 'X-Generated-At': generateTimestamp(),
670
- 'X-Fingerprint': fingerprint
1000
+ const response = await this._fetchWithRetry(
1001
+ this._buildUrl('/session/create'),
1002
+ {
1003
+ method: 'POST',
1004
+ headers: {
1005
+ 'Authorization': `Bearer ${this._token}`,
1006
+ 'X-Generated-At': generateTimestamp(),
1007
+ 'X-Fingerprint': fingerprint
1008
+ },
1009
+ body: formData,
1010
+ signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
671
1011
  },
672
- body: formData,
673
- signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
674
- });
1012
+ 'POST /session/create'
1013
+ );
675
1014
 
676
1015
  logGeneratedAtHeader(response, 'POST', '/session/create');
677
1016
 
@@ -716,6 +1055,19 @@ export class PDFDancer {
716
1055
  return this._fingerprintCache;
717
1056
  }
718
1057
 
1058
+ /**
1059
+ * Executes a fetch request with retry logic based on the configured retry policy.
1060
+ * Implements exponential backoff with optional jitter.
1061
+ */
1062
+ private async _fetchWithRetry(
1063
+ url: string,
1064
+ // eslint-disable-next-line no-undef
1065
+ options: RequestInit,
1066
+ context: string = 'request'
1067
+ ): Promise<Response> {
1068
+ return fetchWithRetry(url, options, this._retryConfig, context);
1069
+ }
1070
+
719
1071
  /**
720
1072
  * Make HTTP request with session headers and error handling.
721
1073
  */
@@ -743,12 +1095,16 @@ export class PDFDancer {
743
1095
  };
744
1096
 
745
1097
  try {
746
- const response = await fetch(url.toString(), {
747
- method,
748
- headers,
749
- body: data ? JSON.stringify(data) : undefined,
750
- signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
751
- });
1098
+ const response = await this._fetchWithRetry(
1099
+ url.toString(),
1100
+ {
1101
+ method,
1102
+ headers,
1103
+ body: data ? JSON.stringify(data) : undefined,
1104
+ signal: this._readTimeout > 0 ? AbortSignal.timeout(this._readTimeout) : undefined
1105
+ },
1106
+ `${method} ${path}`
1107
+ );
752
1108
 
753
1109
  logGeneratedAtHeader(response, method, path);
754
1110
 
@@ -1460,17 +1816,21 @@ export class PDFDancer {
1460
1816
 
1461
1817
  const fingerprint = await this._getFingerprint();
1462
1818
 
1463
- const response = await fetch(this._buildUrl('/font/register'), {
1464
- method: 'POST',
1465
- headers: {
1466
- 'Authorization': `Bearer ${this._token}`,
1467
- 'X-Session-Id': this._sessionId,
1468
- 'X-Generated-At': generateTimestamp(),
1469
- 'X-Fingerprint': fingerprint
1819
+ const response = await this._fetchWithRetry(
1820
+ this._buildUrl('/font/register'),
1821
+ {
1822
+ method: 'POST',
1823
+ headers: {
1824
+ 'Authorization': `Bearer ${this._token}`,
1825
+ 'X-Session-Id': this._sessionId,
1826
+ 'X-Generated-At': generateTimestamp(),
1827
+ 'X-Fingerprint': fingerprint
1828
+ },
1829
+ body: formData,
1830
+ signal: AbortSignal.timeout(60000)
1470
1831
  },
1471
- body: formData,
1472
- signal: AbortSignal.timeout(60000)
1473
- });
1832
+ 'POST /font/register'
1833
+ );
1474
1834
 
1475
1835
  logGeneratedAtHeader(response, 'POST', '/font/register');
1476
1836
 
@@ -1874,4 +2234,50 @@ export class PDFDancer {
1874
2234
  async selectLines() {
1875
2235
  return this.selectTextLines();
1876
2236
  }
2237
+
2238
+ // Singular convenience methods - return the first element or null
2239
+
2240
+ async selectImage() {
2241
+ const images = await this.selectImages();
2242
+ return images.length > 0 ? images[0] : null;
2243
+ }
2244
+
2245
+ async selectPath() {
2246
+ const paths = await this.selectPaths();
2247
+ return paths.length > 0 ? paths[0] : null;
2248
+ }
2249
+
2250
+ async selectForm() {
2251
+ const forms = await this.selectForms();
2252
+ return forms.length > 0 ? forms[0] : null;
2253
+ }
2254
+
2255
+ async selectFormField() {
2256
+ const fields = await this.selectFormFields();
2257
+ return fields.length > 0 ? fields[0] : null;
2258
+ }
2259
+
2260
+ async selectFieldByName(fieldName: string) {
2261
+ const fields = await this.selectFieldsByName(fieldName);
2262
+ return fields.length > 0 ? fields[0] : null;
2263
+ }
2264
+
2265
+ async selectParagraph() {
2266
+ const paragraphs = await this.selectParagraphs();
2267
+ return paragraphs.length > 0 ? paragraphs[0] : null;
2268
+ }
2269
+
2270
+ async selectParagraphMatching(pattern: string) {
2271
+ const paragraphs = await this.selectParagraphsMatching(pattern);
2272
+ return paragraphs.length > 0 ? paragraphs[0] : null;
2273
+ }
2274
+
2275
+ async selectTextLine() {
2276
+ const lines = await this.selectTextLines();
2277
+ return lines.length > 0 ? lines[0] : null;
2278
+ }
2279
+
2280
+ async selectLine() {
2281
+ return this.selectTextLine();
2282
+ }
1877
2283
  }