@ez-gform/core 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 webadeva
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @ez-gform/core
2
+
3
+ Read a Google Form's questions and submit answers to it, from any JavaScript.
4
+ No dependencies. Works in Node 20+ and browsers.
5
+
6
+ Using React? You want [`@ez-gform/react`](../react) instead.
7
+
8
+ ## Install
9
+
10
+ ```sh
11
+ pnpm add @ez-gform/core
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```ts
17
+ import { parseFormHtml, submitForm } from "@ez-gform/core";
18
+
19
+ // 1. Turn a public form's /viewform HTML into a typed schema.
20
+ const schema = parseFormHtml(html);
21
+
22
+ // 2. Submit answers, keyed by entry id.
23
+ const result = await submitForm(
24
+ schema.formId,
25
+ { "entry.123": "hello" },
26
+ { schema },
27
+ );
28
+ ```
29
+
30
+ `result.status` is:
31
+
32
+ - `"sent"` by default. The request is `no-cors` (browsers require it), so you
33
+ can't know whether Google accepted it.
34
+ - `"ok"` or `"error"` if you pass `mode: "cors"`. Only works outside the
35
+ browser (Node), where the response is readable.
36
+
37
+ ## Value shapes
38
+
39
+ What to pass for each kind of question:
40
+
41
+ | Question | Value |
42
+ | ------------------------- | ------------------------------------------------ |
43
+ | Short answer, paragraph | `"text"` |
44
+ | Multiple choice, dropdown | `"Option text"` (must match exactly) |
45
+ | Checkboxes | `["Option A", "Option B"]` |
46
+ | "Other" option | `{ other: "my text" }` |
47
+ | Linear scale | `4` |
48
+ | Date | `{ year, month, day }` (`year` is optional) |
49
+ | Date with time | `{ year, month, day, hour, minute }` |
50
+ | Time | `{ hour, minute }` |
51
+ | Grid | `{ "entry.<rowId>": "Column" }`, one key per row |
52
+ | Checkbox grid | `{ "entry.<rowId>": ["Col A", "Col B"] }` |
53
+
54
+ Empty strings, `null` and `undefined` are skipped.
55
+
56
+ Each grid row has its own entry id. Pass rows nested under the grid question's
57
+ id (`{ "entry.1": { "entry.10": "Agree" } }`) or flat
58
+ (`{ "entry.10": "Agree" }`); both work.
59
+
60
+ ## Other exports
61
+
62
+ | Export | What it does |
63
+ | ---------------------------------------- | ----------------------------------------------------- |
64
+ | `validateValues(values, schema)` | Finds unknown entry ids and missing required answers. |
65
+ | `encodeValues(values, schema)` | Answers → `URLSearchParams`, without submitting. |
66
+ | `buildSubmitBody` | The exact POST body `submitForm` sends. |
67
+ | `buildPrefillUrl` | A link to the form prefilled with your values. |
68
+ | `normalizeFormId`, `formUrls` | Accept any form URL or id; get its endpoints. |
69
+ | `parseFormData`, `extractPublicLoadData` | Lower-level parsing steps behind `parseFormHtml`. |
70
+ | `ParseError`, `ValidationError` | Error classes. |
package/dist/index.cjs ADDED
@@ -0,0 +1,577 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ ParseError: () => ParseError,
24
+ VERSION: () => VERSION,
25
+ ValidationError: () => ValidationError,
26
+ buildPrefillUrl: () => buildPrefillUrl,
27
+ buildSubmitBody: () => buildSubmitBody,
28
+ encodeValues: () => encodeValues,
29
+ extractPublicLoadData: () => extractPublicLoadData,
30
+ formUrls: () => formUrls,
31
+ normalizeFormId: () => normalizeFormId,
32
+ parseFormData: () => parseFormData,
33
+ parseFormHtml: () => parseFormHtml,
34
+ submitForm: () => submitForm,
35
+ validateValues: () => validateValues
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+
39
+ // src/encode.ts
40
+ var OTHER_SENTINEL = "__other_option__";
41
+ var isOtherValue = (v) => {
42
+ return typeof v === "object" && v !== null && !Array.isArray(v) && "other" in v;
43
+ };
44
+ var isDateValue = (v) => {
45
+ return typeof v === "object" && v !== null && !Array.isArray(v) && "day" in v && "month" in v;
46
+ };
47
+ var isTimeValue = (v) => {
48
+ return typeof v === "object" && v !== null && !Array.isArray(v) && "hour" in v && "minute" in v && !("day" in v);
49
+ };
50
+ var pad2 = (n) => {
51
+ return String(n).padStart(2, "0");
52
+ };
53
+ var appendScalar = (params, key, value) => {
54
+ if (value === void 0 || value === null) return;
55
+ const str = String(value);
56
+ if (str.length === 0) return;
57
+ params.append(key, str);
58
+ };
59
+ var appendChoiceItem = (params, entryId, item) => {
60
+ if (isOtherValue(item)) {
61
+ params.append(entryId, OTHER_SENTINEL);
62
+ appendScalar(params, `${entryId}.other_option_response`, item.other);
63
+ } else {
64
+ appendScalar(params, entryId, item);
65
+ }
66
+ };
67
+ var appendGridMap = (params, gridMap) => {
68
+ for (const [rowEntryId, colValue] of Object.entries(gridMap)) {
69
+ if (Array.isArray(colValue)) {
70
+ for (const v of colValue) appendScalar(params, rowEntryId, v);
71
+ } else {
72
+ appendScalar(params, rowEntryId, colValue);
73
+ }
74
+ }
75
+ };
76
+ var encodeValues = (values, _) => {
77
+ const params = new URLSearchParams();
78
+ for (const [entryId, rawValue] of Object.entries(values)) {
79
+ encodeOne(params, entryId, rawValue);
80
+ }
81
+ return params;
82
+ };
83
+ var encodeOne = (params, entryId, value) => {
84
+ if (value === null || value === void 0) return;
85
+ if (Array.isArray(value)) {
86
+ for (const item of value) appendChoiceItem(params, entryId, item);
87
+ return;
88
+ }
89
+ if (typeof value === "string" || typeof value === "number") {
90
+ appendScalar(params, entryId, value);
91
+ return;
92
+ }
93
+ if (isOtherValue(value)) {
94
+ appendChoiceItem(params, entryId, value);
95
+ return;
96
+ }
97
+ if (isDateValue(value)) {
98
+ if (value.year !== void 0)
99
+ appendScalar(params, `${entryId}_year`, value.year);
100
+ appendScalar(params, `${entryId}_month`, value.month);
101
+ appendScalar(params, `${entryId}_day`, value.day);
102
+ if (value.hour !== void 0)
103
+ appendScalar(params, `${entryId}_hour`, pad2(value.hour));
104
+ if (value.minute !== void 0)
105
+ appendScalar(params, `${entryId}_minute`, pad2(value.minute));
106
+ return;
107
+ }
108
+ if (isTimeValue(value)) {
109
+ appendScalar(params, `${entryId}_hour`, pad2(value.hour));
110
+ appendScalar(params, `${entryId}_minute`, pad2(value.minute));
111
+ return;
112
+ }
113
+ appendGridMap(params, value);
114
+ };
115
+ var validateValues = (values, schema) => {
116
+ const errors = [];
117
+ const knownEntryIds = /* @__PURE__ */ new Set();
118
+ for (const q of schema.questions) {
119
+ knownEntryIds.add(q.entryId);
120
+ for (const row of q.rows ?? []) knownEntryIds.add(row.entryId);
121
+ }
122
+ for (const key of Object.keys(values)) {
123
+ if (!knownEntryIds.has(key)) {
124
+ errors.push({
125
+ entryId: key,
126
+ message: `Unknown entry id "${key}" is not present in the form schema`
127
+ });
128
+ }
129
+ }
130
+ for (const q of schema.questions) {
131
+ if (!q.required) continue;
132
+ const idsToCheck = q.rows && q.rows.length > 0 ? q.rows.map((r) => {
133
+ return r.entryId;
134
+ }) : [q.entryId];
135
+ for (const id of idsToCheck) {
136
+ const v = values[id];
137
+ const isEmpty = v === void 0 || v === null || v === "" || Array.isArray(v) && v.length === 0 || typeof v === "object" && !Array.isArray(v) && Object.keys(v).length === 0;
138
+ if (isEmpty) {
139
+ errors.push({
140
+ entryId: id,
141
+ message: `Missing required answer for question "${q.title}" (${id})`
142
+ });
143
+ }
144
+ }
145
+ }
146
+ return errors.length === 0 ? { ok: true } : { ok: false, errors };
147
+ };
148
+
149
+ // src/errors.ts
150
+ var ParseError = class extends Error {
151
+ constructor(message) {
152
+ super(message);
153
+ this.name = "ParseError";
154
+ }
155
+ };
156
+ var ValidationError = class extends Error {
157
+ errors;
158
+ constructor(message, errors) {
159
+ super(message);
160
+ this.name = "ValidationError";
161
+ this.errors = errors;
162
+ }
163
+ };
164
+
165
+ // src/extract.ts
166
+ var ASSIGNMENT_MARKER = "FB_PUBLIC_LOAD_DATA_";
167
+ var extractPublicLoadData = (html) => {
168
+ const markerIndex = html.indexOf(ASSIGNMENT_MARKER);
169
+ if (markerIndex === -1) {
170
+ throw new ParseError(
171
+ "extractPublicLoadData: FB_PUBLIC_LOAD_DATA_ not found in HTML"
172
+ );
173
+ }
174
+ const equalsIndex = html.indexOf("=", markerIndex);
175
+ if (equalsIndex === -1) {
176
+ throw new ParseError(
177
+ "extractPublicLoadData: malformed FB_PUBLIC_LOAD_DATA_ assignment"
178
+ );
179
+ }
180
+ let start = equalsIndex + 1;
181
+ while (start < html.length && /\s/.test(html[start])) {
182
+ start++;
183
+ }
184
+ if (html[start] !== "[") {
185
+ throw new ParseError(
186
+ "extractPublicLoadData: expected array literal after FB_PUBLIC_LOAD_DATA_ ="
187
+ );
188
+ }
189
+ const end = findMatchingBracketEnd(html, start);
190
+ const literal = html.slice(start, end + 1);
191
+ try {
192
+ return JSON.parse(literal);
193
+ } catch (cause) {
194
+ throw new ParseError(
195
+ `extractPublicLoadData: failed to JSON.parse the extracted FB_PUBLIC_LOAD_DATA_ literal: ${cause instanceof Error ? cause.message : String(cause)}`
196
+ );
197
+ }
198
+ };
199
+ var findMatchingBracketEnd = (text, startIndex) => {
200
+ let depth = 0;
201
+ let inString = false;
202
+ let escaped = false;
203
+ for (let i = startIndex; i < text.length; i++) {
204
+ const ch = text[i];
205
+ if (inString) {
206
+ if (escaped) {
207
+ escaped = false;
208
+ } else if (ch === "\\") {
209
+ escaped = true;
210
+ } else if (ch === '"') {
211
+ inString = false;
212
+ }
213
+ continue;
214
+ }
215
+ if (ch === '"') {
216
+ inString = true;
217
+ } else if (ch === "[") {
218
+ depth++;
219
+ } else if (ch === "]") {
220
+ depth--;
221
+ if (depth === 0) {
222
+ return i;
223
+ }
224
+ }
225
+ }
226
+ throw new ParseError(
227
+ "extractPublicLoadData: unterminated array literal (no matching closing bracket)"
228
+ );
229
+ };
230
+ var extractFbzx = (html) => {
231
+ const match = /name="fbzx"\s+value="([^"]*)"/.exec(html);
232
+ return match?.[1];
233
+ };
234
+
235
+ // src/parse.ts
236
+ var asArray = (value, context) => {
237
+ if (!Array.isArray(value)) {
238
+ throw new ParseError(
239
+ `parseFormData: expected an array at ${context}, got ${typeof value}`
240
+ );
241
+ }
242
+ return value;
243
+ };
244
+ var optionalString = (value) => {
245
+ return typeof value === "string" && value.length > 0 ? value : void 0;
246
+ };
247
+ var toEntryId = (rawId) => {
248
+ return `entry.${String(rawId)}`;
249
+ };
250
+ var mapOptions = (rawOptions) => {
251
+ if (!Array.isArray(rawOptions)) return void 0;
252
+ return rawOptions.map((tuple) => {
253
+ const t = Array.isArray(tuple) ? tuple : [];
254
+ const value = typeof t[0] === "string" ? t[0] : "";
255
+ const isOther = t[4] === 1;
256
+ return { value, isOther };
257
+ });
258
+ };
259
+ var parseSubQuestions = (rawSubs) => {
260
+ const subs = asArray(rawSubs, "question[4]");
261
+ return subs.map((rawSub) => {
262
+ const s = asArray(rawSub, "question[4][i]");
263
+ return {
264
+ entryId: s[0],
265
+ options: s[1],
266
+ required: s[2],
267
+ extra: s[3],
268
+ rest: s
269
+ };
270
+ });
271
+ };
272
+ var TYPE_CODE_NAME = {
273
+ 0: "short_answer",
274
+ 1: "paragraph",
275
+ 2: "multiple_choice",
276
+ 3: "dropdown",
277
+ 4: "checkboxes",
278
+ 5: "linear_scale",
279
+ 6: "section_header",
280
+ 7: "grid",
281
+ // resolved to "grid" | "checkbox_grid" below
282
+ 8: "page_break",
283
+ 9: "date",
284
+ 10: "time",
285
+ 11: "image",
286
+ 12: "video",
287
+ 13: "file_upload",
288
+ // ponytail: rating (stars/hearts) has the same wire shape as a linear scale
289
+ // (options "1".."N", submits entry.N=<number>), so it reuses that type. Add a
290
+ // "rating" QuestionType if consumers need to render the icon.
291
+ 18: "linear_scale"
292
+ };
293
+ var parseFormData = (data, opts = {}) => {
294
+ if (!Array.isArray(data)) {
295
+ throw new ParseError(
296
+ "parseFormData: expected the top-level FB_PUBLIC_LOAD_DATA_ value to be an array"
297
+ );
298
+ }
299
+ const container = data[1];
300
+ if (!Array.isArray(container)) {
301
+ throw new ParseError(
302
+ "parseFormData: expected data[1] (main container) to be an array"
303
+ );
304
+ }
305
+ const description = optionalString(container[0]);
306
+ const rawQuestions = container[1];
307
+ if (!Array.isArray(rawQuestions)) {
308
+ throw new ParseError(
309
+ "parseFormData: expected data[1][1] (question list) to be an array"
310
+ );
311
+ }
312
+ const container8 = container[8];
313
+ const wrappedTitle = typeof container8 === "string" ? container8 : Array.isArray(container8) ? container8[1] : void 0;
314
+ const title = typeof wrappedTitle === "string" ? wrappedTitle : typeof data[3] === "string" ? data[3] : "";
315
+ let { formId } = opts;
316
+ if (!formId) {
317
+ const rawFormId = data[14];
318
+ if (typeof rawFormId === "string") {
319
+ formId = rawFormId.startsWith("e/") ? rawFormId.slice(2) : rawFormId;
320
+ } else {
321
+ formId = "";
322
+ }
323
+ }
324
+ const questions = [];
325
+ const sections = [{ title, description, questionIds: [] }];
326
+ let sawPageBreak = false;
327
+ for (const rawEntry of rawQuestions) {
328
+ const entry = asArray(rawEntry, "question entry");
329
+ const id = entry[0];
330
+ const entryTitle = typeof entry[1] === "string" ? entry[1] : "";
331
+ const entryDescription = optionalString(entry[2]);
332
+ const typeCode = entry[3];
333
+ if (typeof typeCode !== "number") {
334
+ throw new ParseError(
335
+ `parseFormData: question ${String(id)} has a non-numeric type code`
336
+ );
337
+ }
338
+ const kind = TYPE_CODE_NAME[typeCode];
339
+ if (kind === void 0) {
340
+ throw new ParseError(
341
+ `parseFormData: unknown question type code ${typeCode} for question ${String(id)}`
342
+ );
343
+ }
344
+ if (kind === "image" || kind === "video") {
345
+ continue;
346
+ }
347
+ if (kind === "section_header") {
348
+ const current = sections[sections.length - 1];
349
+ current.title = entryTitle;
350
+ current.description = entryDescription;
351
+ continue;
352
+ }
353
+ if (kind === "page_break") {
354
+ sawPageBreak = true;
355
+ sections.push({
356
+ title: entryTitle,
357
+ description: entryDescription,
358
+ questionIds: []
359
+ });
360
+ continue;
361
+ }
362
+ const subs = parseSubQuestions(entry[4]);
363
+ if (subs.length === 0) {
364
+ throw new ParseError(
365
+ `parseFormData: question ${String(id)} (type ${typeCode}) has no sub-question entries`
366
+ );
367
+ }
368
+ const firstSub = subs[0];
369
+ const question = {
370
+ id: String(id),
371
+ entryId: toEntryId(firstSub.entryId),
372
+ title: entryTitle,
373
+ description: entryDescription,
374
+ type: kind,
375
+ required: firstSub.required === 1
376
+ };
377
+ switch (kind) {
378
+ case "multiple_choice":
379
+ case "dropdown":
380
+ case "checkboxes": {
381
+ question.options = mapOptions(firstSub.options);
382
+ break;
383
+ }
384
+ case "linear_scale": {
385
+ const options = mapOptions(firstSub.options) ?? [];
386
+ const first = options[0]?.value;
387
+ const last = options[options.length - 1]?.value;
388
+ const min = first !== void 0 ? Number(first) : Number.NaN;
389
+ const max = last !== void 0 ? Number(last) : Number.NaN;
390
+ const labels = Array.isArray(firstSub.extra) ? firstSub.extra : [];
391
+ question.scale = {
392
+ min,
393
+ max,
394
+ lowLabel: optionalString(labels[0]),
395
+ highLabel: optionalString(labels[1])
396
+ };
397
+ break;
398
+ }
399
+ case "grid": {
400
+ const rows = subs.map((sub) => {
401
+ const label = Array.isArray(sub.extra) ? optionalString(sub.extra[0]) : void 0;
402
+ return { entryId: toEntryId(sub.entryId), label: label ?? "" };
403
+ });
404
+ const isCheckboxGrid = subs.some((sub) => {
405
+ const flag = sub.rest[sub.rest.length - 1];
406
+ return Array.isArray(flag) && flag[0] === 1;
407
+ });
408
+ question.type = isCheckboxGrid ? "checkbox_grid" : "grid";
409
+ question.rows = rows;
410
+ question.options = mapOptions(firstSub.options);
411
+ break;
412
+ }
413
+ case "date": {
414
+ const flags = Array.isArray(firstSub.rest[7]) ? firstSub.rest[7] : void 0;
415
+ question.date = {
416
+ includeTime: flags?.[0] === 1,
417
+ includeYear: flags ? flags[1] === 1 : true
418
+ };
419
+ break;
420
+ }
421
+ case "time": {
422
+ const flags = Array.isArray(firstSub.rest[6]) ? firstSub.rest[6] : void 0;
423
+ question.time = { isDuration: flags?.[0] === 1 };
424
+ break;
425
+ }
426
+ case "file_upload":
427
+ break;
428
+ default:
429
+ break;
430
+ }
431
+ questions.push(question);
432
+ const currentSection = sections[sections.length - 1];
433
+ currentSection.questionIds.push(question.id);
434
+ }
435
+ return {
436
+ formId: formId ?? "",
437
+ title,
438
+ description,
439
+ questions,
440
+ sections,
441
+ multiPage: sawPageBreak
442
+ };
443
+ };
444
+ var parseFormHtml = (html) => {
445
+ const data = extractPublicLoadData(html);
446
+ const schema = parseFormData(data);
447
+ const fbzx = extractFbzx(html);
448
+ return fbzx ? { ...schema, fbzx } : schema;
449
+ };
450
+
451
+ // src/url.ts
452
+ var BARE_ID_RE = /^[A-Za-z0-9_-]{10,}$/;
453
+ var normalizeFormId = (input) => {
454
+ const trimmed = input.trim();
455
+ if (trimmed.length === 0) {
456
+ throw new Error("normalizeFormId: input is empty");
457
+ }
458
+ if (trimmed.startsWith("e/")) {
459
+ const id = trimmed.slice(2);
460
+ return normalizeFormId(id);
461
+ }
462
+ if (/^https?:\/\//i.test(trimmed) || trimmed.startsWith("docs.google.com")) {
463
+ const url = new URL(
464
+ /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`
465
+ );
466
+ const parts = url.pathname.split("/").filter(Boolean);
467
+ const dIndex = parts.indexOf("d");
468
+ if (dIndex === -1 || dIndex + 1 >= parts.length) {
469
+ throw new Error(
470
+ `normalizeFormId: could not find form id in URL "${input}"`
471
+ );
472
+ }
473
+ const afterD = parts[dIndex + 1];
474
+ const id = afterD === "e" ? parts[dIndex + 2] : afterD;
475
+ if (!id) {
476
+ throw new Error(
477
+ `normalizeFormId: could not find form id in URL "${input}"`
478
+ );
479
+ }
480
+ return normalizeFormId(id);
481
+ }
482
+ if (!BARE_ID_RE.test(trimmed)) {
483
+ throw new Error(
484
+ `normalizeFormId: "${input}" is not a recognizable Google Form id or URL`
485
+ );
486
+ }
487
+ return trimmed;
488
+ };
489
+ var formUrls = (formId) => {
490
+ const id = normalizeFormId(formId);
491
+ return {
492
+ viewform: `https://docs.google.com/forms/d/e/${id}/viewform`,
493
+ formResponse: `https://docs.google.com/forms/d/e/${id}/formResponse`
494
+ };
495
+ };
496
+
497
+ // src/submit.ts
498
+ var buildPrefillUrl = (formId, values, schema) => {
499
+ const { viewform } = formUrls(formId);
500
+ const params = encodeValues(values, schema);
501
+ const url = new URL(viewform);
502
+ url.searchParams.set("usp", "pp_url");
503
+ for (const [key, value] of params) {
504
+ url.searchParams.append(key, value);
505
+ }
506
+ return url.toString();
507
+ };
508
+ var randomFbzx = () => {
509
+ const digits = Array.from({ length: 19 }, () => {
510
+ return Math.floor(Math.random() * 10);
511
+ }).join("");
512
+ return `-${digits}`;
513
+ };
514
+ var buildSubmitBody = (formId, values, schema) => {
515
+ normalizeFormId(formId);
516
+ const params = encodeValues(values, schema);
517
+ if (schema?.multiPage) {
518
+ const fbzx = schema.fbzx ?? randomFbzx();
519
+ const pageCount = Math.max(schema.sections.length, 1);
520
+ const pageHistory = Array.from({ length: pageCount }, (_, i) => {
521
+ return i;
522
+ }).join(",");
523
+ params.set("fbzx", fbzx);
524
+ params.set("pageHistory", pageHistory);
525
+ params.set("partialResponse", JSON.stringify([null, null, fbzx]));
526
+ }
527
+ params.set("submit", "Submit");
528
+ return params;
529
+ };
530
+ var submitForm = async (formId, values, opts = {}) => {
531
+ const { formResponse } = formUrls(formId);
532
+ const body = buildSubmitBody(formId, values, opts.schema);
533
+ const doFetch = opts.fetch ?? fetch;
534
+ const mode = opts.mode ?? "no-cors";
535
+ try {
536
+ const response = await doFetch(formResponse, {
537
+ method: "POST",
538
+ mode,
539
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
540
+ body: body.toString(),
541
+ signal: opts.signal
542
+ });
543
+ if (mode === "no-cors") {
544
+ return { status: "sent" };
545
+ }
546
+ if (response.ok) {
547
+ return { status: "ok", httpStatus: response.status };
548
+ }
549
+ return {
550
+ status: "error",
551
+ error: new Error(`formResponse returned HTTP ${response.status}`),
552
+ httpStatus: response.status
553
+ };
554
+ } catch (error) {
555
+ return { status: "error", error };
556
+ }
557
+ };
558
+
559
+ // src/index.ts
560
+ var VERSION = "0.1.0";
561
+ // Annotate the CommonJS export names for ESM import in node:
562
+ 0 && (module.exports = {
563
+ ParseError,
564
+ VERSION,
565
+ ValidationError,
566
+ buildPrefillUrl,
567
+ buildSubmitBody,
568
+ encodeValues,
569
+ extractPublicLoadData,
570
+ formUrls,
571
+ normalizeFormId,
572
+ parseFormData,
573
+ parseFormHtml,
574
+ submitForm,
575
+ validateValues
576
+ });
577
+ //# sourceMappingURL=index.cjs.map