summaryrefslogtreecommitdiff
path: root/js
diff options
context:
space:
mode:
Diffstat (limited to 'js')
-rw-r--r--js/blob.ts8
-rw-r--r--js/dom_types.ts20
-rw-r--r--js/file.ts24
-rw-r--r--js/file_test.ts104
-rw-r--r--js/form_data.ts107
-rw-r--r--js/form_data_test.ts92
-rw-r--r--js/globals.ts4
-rw-r--r--js/mixins/dom_iterable.ts13
-rw-r--r--js/unit_tests.ts2
9 files changed, 358 insertions, 16 deletions
diff --git a/js/blob.ts b/js/blob.ts
index b57452dd5..8dcc48ba2 100644
--- a/js/blob.ts
+++ b/js/blob.ts
@@ -97,6 +97,12 @@ function toUint8Arrays(
ret.push(element[bytesSymbol]);
} else if (element instanceof Uint8Array) {
ret.push(element);
+ } else if (element instanceof Uint16Array) {
+ const uint8 = new Uint8Array(element.buffer);
+ ret.push(uint8);
+ } else if (element instanceof Uint32Array) {
+ const uint8 = new Uint8Array(element.buffer);
+ ret.push(uint8);
} else if (ArrayBuffer.isView(element)) {
// Convert view to Uint8Array.
const uint8 = new Uint8Array(element.buffer);
@@ -105,6 +111,8 @@ function toUint8Arrays(
// Create a new Uint8Array view for the given ArrayBuffer.
const uint8 = new Uint8Array(element);
ret.push(uint8);
+ } else {
+ ret.push(enc.encode(String(element)));
}
}
return ret;
diff --git a/js/dom_types.ts b/js/dom_types.ts
index bd73639ce..19a3d5fe2 100644
--- a/js/dom_types.ts
+++ b/js/dom_types.ts
@@ -34,7 +34,7 @@ type ReferrerPolicy =
| "origin-when-cross-origin"
| "unsafe-url";
export type BlobPart = BufferSource | Blob | string;
-type FormDataEntryValue = File | string;
+export type FormDataEntryValue = File | string;
export type EventListenerOrEventListenerObject =
| EventListener
| EventListenerObject;
@@ -173,7 +173,7 @@ interface Event {
readonly NONE: number;
}
-interface File extends Blob {
+export interface File extends Blob {
readonly lastModified: number;
readonly name: string;
}
@@ -242,22 +242,18 @@ interface ReadableStreamReader {
releaseLock(): void;
}
-export interface FormData {
+export interface FormData extends DomIterable<string, FormDataEntryValue> {
append(name: string, value: string | Blob, fileName?: string): void;
delete(name: string): void;
get(name: string): FormDataEntryValue | null;
getAll(name: string): FormDataEntryValue[];
has(name: string): boolean;
set(name: string, value: string | Blob, fileName?: string): void;
- forEach(
- callbackfn: (
- value: FormDataEntryValue,
- key: string,
- parent: FormData
- ) => void,
- // tslint:disable-next-line:no-any
- thisArg?: any
- ): void;
+}
+
+export interface FormDataConstructor {
+ new (): FormData;
+ prototype: FormData;
}
/** A blob object represents a file-like object of immutable, raw data. */
diff --git a/js/file.ts b/js/file.ts
new file mode 100644
index 000000000..8496d6edb
--- /dev/null
+++ b/js/file.ts
@@ -0,0 +1,24 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types";
+import * as blob from "./blob";
+
+export class DenoFile extends blob.DenoBlob implements domTypes.File {
+ lastModified: number;
+ name: string;
+
+ constructor(
+ fileBits: domTypes.BlobPart[],
+ fileName: string,
+ options?: domTypes.FilePropertyBag
+ ) {
+ options = options || {};
+ super(fileBits, options);
+
+ // 4.1.2.1 Replace any "/" character (U+002F SOLIDUS)
+ // with a ":" (U + 003A COLON)
+ this.name = String(fileName).replace(/\u002F/g, "\u003A");
+ // 4.1.3.3 If lastModified is not provided, set lastModified to the current
+ // date and time represented in number of milliseconds since the Unix Epoch.
+ this.lastModified = options.lastModified || Date.now();
+ }
+}
diff --git a/js/file_test.ts b/js/file_test.ts
new file mode 100644
index 000000000..a32c06947
--- /dev/null
+++ b/js/file_test.ts
@@ -0,0 +1,104 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEqual } from "./test_util.ts";
+
+function testFirstArgument(arg1, expectedSize) {
+ const file = new File(arg1, "name");
+ assert(file instanceof File);
+ assertEqual(file.name, "name");
+ assertEqual(file.size, expectedSize);
+ assertEqual(file.type, "");
+}
+
+test(function fileEmptyFileBits() {
+ testFirstArgument([], 0);
+});
+
+test(function fileStringFileBits() {
+ testFirstArgument(["bits"], 4);
+});
+
+test(function fileUnicodeStringFileBits() {
+ testFirstArgument(["𝓽𝓮𝔁𝓽"], 16);
+});
+
+test(function fileStringObjectFileBits() {
+ // tslint:disable-next-line no-construct
+ testFirstArgument([new String("string object")], 13);
+});
+
+test(function fileEmptyBlobFileBits() {
+ testFirstArgument([new Blob()], 0);
+});
+
+test(function fileBlobFileBits() {
+ testFirstArgument([new Blob(["bits"])], 4);
+});
+
+test(function fileEmptyFileFileBits() {
+ testFirstArgument([new File([], "world.txt")], 0);
+});
+
+test(function fileFileFileBits() {
+ testFirstArgument([new File(["bits"], "world.txt")], 4);
+});
+
+test(function fileArrayBufferFileBits() {
+ testFirstArgument([new ArrayBuffer(8)], 8);
+});
+
+test(function fileTypedArrayFileBits() {
+ testFirstArgument([new Uint8Array([0x50, 0x41, 0x53, 0x53])], 4);
+});
+
+test(function fileVariousFileBits() {
+ testFirstArgument(
+ [
+ "bits",
+ new Blob(["bits"]),
+ new Blob(),
+ new Uint8Array([0x50, 0x41]),
+ new Uint16Array([0x5353]),
+ new Uint32Array([0x53534150])
+ ],
+ 16
+ );
+});
+
+test(function fileNumberInFileBits() {
+ testFirstArgument([12], 2);
+});
+
+test(function fileArrayInFileBits() {
+ testFirstArgument([[1, 2, 3]], 5);
+});
+
+test(function fileObjectInFileBits() {
+ // "[object Object]"
+ testFirstArgument([{}], 15);
+});
+
+function testSecondArgument(arg2, expectedFileName) {
+ const file = new File(["bits"], arg2);
+ assert(file instanceof File);
+ assertEqual(file.name, expectedFileName);
+}
+
+test(function fileUsingFileName() {
+ testSecondArgument("dummy", "dummy");
+});
+
+test(function fileUsingSpecialCharacterInFileName() {
+ testSecondArgument("dummy/foo", "dummy:foo");
+});
+
+test(function fileUsingNullFileName() {
+ testSecondArgument(null, "null");
+});
+
+test(function fileUsingNumberFileName() {
+ testSecondArgument(1, "1");
+});
+
+test(function fileUsingEmptyStringFileName() {
+ testSecondArgument("", "");
+});
diff --git a/js/form_data.ts b/js/form_data.ts
new file mode 100644
index 000000000..662643520
--- /dev/null
+++ b/js/form_data.ts
@@ -0,0 +1,107 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import * as domTypes from "./dom_types";
+import * as blob from "./blob";
+import * as file from "./file";
+import { DomIterableMixin } from "./mixins/dom_iterable";
+
+const dataSymbol = Symbol("data");
+
+class FormDataBase {
+ private [dataSymbol]: Array<[string, domTypes.FormDataEntryValue]> = [];
+
+ /** Appends a new value onto an existing key inside a `FormData`
+ * object, or adds the key if it does not already exist.
+ *
+ * formData.append('name', 'first');
+ * formData.append('name', 'second');
+ */
+ append(name: string, value: string): void;
+ append(name: string, value: blob.DenoBlob, filename?: string): void;
+ append(name: string, value: string | blob.DenoBlob, filename?: string): void {
+ if (value instanceof blob.DenoBlob) {
+ const dfile = new file.DenoFile([value], filename || name);
+ this[dataSymbol].push([name, dfile]);
+ } else {
+ this[dataSymbol].push([name, String(value)]);
+ }
+ }
+
+ /** Deletes a key/value pair from a `FormData` object.
+ *
+ * formData.delete('name');
+ */
+ delete(name: string): void {
+ let i = 0;
+ while (i < this[dataSymbol].length) {
+ if (this[dataSymbol][i][0] === name) {
+ this[dataSymbol].splice(i, 1);
+ } else {
+ i++;
+ }
+ }
+ }
+
+ /** Returns an array of all the values associated with a given key
+ * from within a `FormData`.
+ *
+ * formData.getAll('name');
+ */
+ getAll(name: string): domTypes.FormDataEntryValue[] {
+ const values = [];
+ for (const entry of this[dataSymbol]) {
+ if (entry[0] === name) {
+ values.push(entry[1]);
+ }
+ }
+
+ return values;
+ }
+
+ /** Returns the first value associated with a given key from within a
+ * `FormData` object.
+ *
+ * formData.get('name');
+ */
+ get(name: string): domTypes.FormDataEntryValue | null {
+ for (const entry of this[dataSymbol]) {
+ if (entry[0] === name) {
+ return entry[1];
+ }
+ }
+
+ return null;
+ }
+
+ /** Returns a boolean stating whether a `FormData` object contains a
+ * certain key/value pair.
+ *
+ * formData.has('name');
+ */
+ has(name: string): boolean {
+ return this[dataSymbol].some(entry => entry[0] === name);
+ }
+
+ /** Sets a new value for an existing key inside a `FormData` object, or
+ * adds the key/value if it does not already exist.
+ *
+ * formData.set('name', 'value');
+ */
+ set(name: string, value: string): void;
+ set(name: string, value: blob.DenoBlob, filename?: string): void;
+ set(name: string, value: string | blob.DenoBlob, filename?: string): void {
+ this.delete(name);
+ if (value instanceof blob.DenoBlob) {
+ const dfile = new file.DenoFile([value], filename || name);
+ this[dataSymbol].push([name, dfile]);
+ } else {
+ this[dataSymbol].push([name, String(value)]);
+ }
+ }
+}
+
+// tslint:disable-next-line:variable-name
+export const FormData = DomIterableMixin<
+ string,
+ domTypes.FormDataEntryValue,
+ typeof FormDataBase
+>(FormDataBase, dataSymbol);
diff --git a/js/form_data_test.ts b/js/form_data_test.ts
new file mode 100644
index 000000000..04a05acaf
--- /dev/null
+++ b/js/form_data_test.ts
@@ -0,0 +1,92 @@
+// Copyright 2018 the Deno authors. All rights reserved. MIT license.
+import { test, assert, assertEqual } from "./test_util.ts";
+
+test(function formDataParamsAppendSuccess() {
+ const formData = new FormData();
+ formData.append("a", "true");
+ assertEqual(formData.get("a"), "true");
+});
+
+test(function formDataParamsDeleteSuccess() {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ assertEqual(formData.get("b"), "false");
+ formData.delete("b");
+ assertEqual(formData.get("a"), "true");
+ assertEqual(formData.get("b"), null);
+});
+
+test(function formDataParamsGetAllSuccess() {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ formData.append("a", "null");
+ assertEqual(formData.getAll("a"), ["true", "null"]);
+ assertEqual(formData.getAll("b"), ["false"]);
+ assertEqual(formData.getAll("c"), []);
+});
+
+test(function formDataParamsGetSuccess() {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ formData.append("a", "null");
+ formData.append("d", undefined);
+ formData.append("e", null);
+ assertEqual(formData.get("a"), "true");
+ assertEqual(formData.get("b"), "false");
+ assertEqual(formData.get("c"), null);
+ assertEqual(formData.get("d"), "undefined");
+ assertEqual(formData.get("e"), "null");
+});
+
+test(function formDataParamsHasSuccess() {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ assert(formData.has("a"));
+ assert(formData.has("b"));
+ assert(!formData.has("c"));
+});
+
+test(function formDataParamsSetSuccess() {
+ const formData = new FormData();
+ formData.append("a", "true");
+ formData.append("b", "false");
+ formData.append("a", "null");
+ assertEqual(formData.getAll("a"), ["true", "null"]);
+ assertEqual(formData.getAll("b"), ["false"]);
+ formData.set("a", "false");
+ assertEqual(formData.getAll("a"), ["false"]);
+ formData.set("d", undefined);
+ assertEqual(formData.get("d"), "undefined");
+ formData.set("e", null);
+ assertEqual(formData.get("e"), "null");
+});
+
+test(function formDataSetEmptyBlobSuccess() {
+ const formData = new FormData();
+ formData.set("a", new Blob([]), "blank.txt");
+ const file = formData.get("a");
+ assert(file instanceof File);
+ if (typeof file !== "string") {
+ assertEqual(file.name, "blank.txt");
+ }
+});
+
+test(function formDataParamsForEachSuccess() {
+ const init = [["a", "54"], ["b", "true"]];
+ const formData = new FormData();
+ for (const [name, value] of init) {
+ formData.append(name, value);
+ }
+ let callNum = 0;
+ formData.forEach((value, key, parent) => {
+ assertEqual(formData, parent);
+ assertEqual(value, init[callNum][1]);
+ assertEqual(key, init[callNum][0]);
+ callNum++;
+ });
+ assertEqual(callNum, init.length);
+});
diff --git a/js/globals.ts b/js/globals.ts
index a09e6ed9b..5a0ca7cc6 100644
--- a/js/globals.ts
+++ b/js/globals.ts
@@ -1,5 +1,7 @@
// Copyright 2018 the Deno authors. All rights reserved. MIT license.
import * as blob from "./blob";
+import * as file from "./file";
+import * as formdata from "./form_data";
import * as console_ from "./console";
import * as fetch_ from "./fetch";
import { Headers } from "./headers";
@@ -44,3 +46,5 @@ window.fetch = fetch_.fetch;
// runtime library
window.Headers = Headers as domTypes.HeadersConstructor;
window.Blob = blob.DenoBlob;
+window.File = file.DenoFile;
+window.FormData = formdata.FormData as domTypes.FormDataConstructor;
diff --git a/js/mixins/dom_iterable.ts b/js/mixins/dom_iterable.ts
index 015226589..eb5d08d13 100644
--- a/js/mixins/dom_iterable.ts
+++ b/js/mixins/dom_iterable.ts
@@ -21,22 +21,27 @@ export function DomIterableMixin<K, V, TBase extends Constructor>(
// Base class in a way where the Symbol `dataSymbol` is defined. So the
// runtime code works, but we do lose a little bit of type safety.
+ // Additionally, we have to not use .keys() nor .values() since the internal
+ // slot differs in type - some have a Map, which yields [K, V] in
+ // Symbol.iterator, and some have an Array, which yields V, in this case
+ // [K, V] too as they are arrays of tuples.
+
// tslint:disable-next-line:variable-name
const DomIterable = class extends Base {
*entries(): IterableIterator<[K, V]> {
- for (const entry of (this as any)[dataSymbol].entries()) {
+ for (const entry of (this as any)[dataSymbol]) {
yield entry;
}
}
*keys(): IterableIterator<K> {
- for (const key of (this as any)[dataSymbol].keys()) {
+ for (const [key] of (this as any)[dataSymbol]) {
yield key;
}
}
*values(): IterableIterator<V> {
- for (const value of (this as any)[dataSymbol].values()) {
+ for (const [, value] of (this as any)[dataSymbol]) {
yield value;
}
}
@@ -47,7 +52,7 @@ export function DomIterableMixin<K, V, TBase extends Constructor>(
thisArg?: any
): void {
callbackfn = callbackfn.bind(thisArg == null ? window : Object(thisArg));
- for (const [key, value] of (this as any)[dataSymbol].entries()) {
+ for (const [key, value] of (this as any)[dataSymbol]) {
callbackfn(value, key, this);
}
}
diff --git a/js/unit_tests.ts b/js/unit_tests.ts
index fb3d0018e..8f9f4d043 100644
--- a/js/unit_tests.ts
+++ b/js/unit_tests.ts
@@ -10,7 +10,9 @@ import "./console_test.ts";
import "./copy_file_test.ts";
import "./dir_test";
import "./fetch_test.ts";
+import "./file_test.ts";
import "./files_test.ts";
+import "./form_data_test.ts";
import "./headers_test.ts";
import "./make_temp_dir_test.ts";
import "./metrics_test.ts";