|
1 |
| -// Based on https://github.com/tmpvar/jsdom/blob/aa85b2abf07766ff7bf5c1f6daafb3726f2f2db5/lib/jsdom/living/blob.js |
2 |
| -// (MIT licensed) |
3 |
| - |
4 |
| -const {Readable: ReadableStream} = require('stream'); |
| 1 | +const {Readable} = require('stream'); |
5 | 2 |
|
| 3 | +/** |
| 4 | + * @type {WeakMap<Blob, {type: string, size: number, parts: (Blob | Buffer)[] }>} |
| 5 | + */ |
6 | 6 | const wm = new WeakMap();
|
7 | 7 |
|
| 8 | +async function * read(parts) { |
| 9 | + for (const part of parts) { |
| 10 | + if ('stream' in part) { |
| 11 | + yield * part.stream(); |
| 12 | + } else { |
| 13 | + yield part; |
| 14 | + } |
| 15 | + } |
| 16 | +} |
| 17 | + |
| 18 | +/** |
| 19 | + * @template T |
| 20 | + * @param {T} object |
| 21 | + * @returns {T is Blob} |
| 22 | + */ |
| 23 | +const isBlob = object => { |
| 24 | + return ( |
| 25 | + typeof object === 'object' && |
| 26 | + typeof object.stream === 'function' && |
| 27 | + typeof object.constructor === 'function' && |
| 28 | + /^(Blob|File)$/.test(object[Symbol.toStringTag]) |
| 29 | + ); |
| 30 | +}; |
| 31 | + |
8 | 32 | class Blob {
|
| 33 | + /** |
| 34 | + * The Blob() constructor returns a new Blob object. The content |
| 35 | + * of the blob consists of the concatenation of the values given |
| 36 | + * in the parameter array. |
| 37 | + * |
| 38 | + * @param {(ArrayBufferLike | ArrayBufferView | Blob | Buffer | string)[]} blobParts |
| 39 | + * @param {{ type?: string }} [options] |
| 40 | + */ |
9 | 41 | constructor(blobParts = [], options = {type: ''}) {
|
10 |
| - const buffers = []; |
11 | 42 | let size = 0;
|
12 | 43 |
|
13 |
| - blobParts.forEach(element => { |
| 44 | + const parts = blobParts.map(element => { |
14 | 45 | let buffer;
|
15 |
| - if (element instanceof Buffer) { |
| 46 | + if (Buffer.isBuffer(element)) { |
16 | 47 | buffer = element;
|
17 | 48 | } else if (ArrayBuffer.isView(element)) {
|
18 | 49 | buffer = Buffer.from(element.buffer, element.byteOffset, element.byteLength);
|
19 | 50 | } else if (element instanceof ArrayBuffer) {
|
20 | 51 | buffer = Buffer.from(element);
|
21 |
| - } else if (element instanceof Blob) { |
22 |
| - buffer = wm.get(element).buffer; |
| 52 | + } else if (isBlob(element)) { |
| 53 | + buffer = element; |
23 | 54 | } else {
|
24 | 55 | buffer = Buffer.from(typeof element === 'string' ? element : String(element));
|
25 | 56 | }
|
26 | 57 |
|
27 |
| - size += buffer.length; |
28 |
| - buffers.push(buffer); |
| 58 | + size += buffer.length || buffer.size || 0; |
| 59 | + return buffer; |
29 | 60 | });
|
30 | 61 |
|
31 |
| - const buffer = Buffer.concat(buffers, size); |
32 |
| - |
33 | 62 | const type = options.type === undefined ? '' : String(options.type).toLowerCase();
|
34 | 63 |
|
35 | 64 | wm.set(this, {
|
36 | 65 | type: /[^\u0020-\u007E]/.test(type) ? '' : type,
|
37 | 66 | size,
|
38 |
| - buffer |
| 67 | + parts |
39 | 68 | });
|
40 | 69 | }
|
41 | 70 |
|
| 71 | + /** |
| 72 | + * The Blob interface's size property returns the |
| 73 | + * size of the Blob in bytes. |
| 74 | + */ |
42 | 75 | get size() {
|
43 | 76 | return wm.get(this).size;
|
44 | 77 | }
|
45 | 78 |
|
| 79 | + /** |
| 80 | + * The type property of a Blob object returns the MIME type of the file. |
| 81 | + */ |
46 | 82 | get type() {
|
47 | 83 | return wm.get(this).type;
|
48 | 84 | }
|
49 | 85 |
|
50 |
| - text() { |
51 |
| - return Promise.resolve(wm.get(this).buffer.toString()); |
| 86 | + /** |
| 87 | + * The text() method in the Blob interface returns a Promise |
| 88 | + * that resolves with a string containing the contents of |
| 89 | + * the blob, interpreted as UTF-8. |
| 90 | + * |
| 91 | + * @return {Promise<string>} |
| 92 | + */ |
| 93 | + async text() { |
| 94 | + return Buffer.from(await this.arrayBuffer()).toString(); |
52 | 95 | }
|
53 | 96 |
|
54 |
| - arrayBuffer() { |
55 |
| - const buf = wm.get(this).buffer; |
56 |
| - const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength); |
57 |
| - return Promise.resolve(ab); |
58 |
| - } |
| 97 | + /** |
| 98 | + * The arrayBuffer() method in the Blob interface returns a |
| 99 | + * Promise that resolves with the contents of the blob as |
| 100 | + * binary data contained in an ArrayBuffer. |
| 101 | + * |
| 102 | + * @return {Promise<ArrayBuffer>} |
| 103 | + */ |
| 104 | + async arrayBuffer() { |
| 105 | + const data = new Uint8Array(this.size); |
| 106 | + let offset = 0; |
| 107 | + for await (const chunk of this.stream()) { |
| 108 | + data.set(chunk, offset); |
| 109 | + offset += chunk.length; |
| 110 | + } |
59 | 111 |
|
60 |
| - stream() { |
61 |
| - const readable = new ReadableStream(); |
62 |
| - readable._read = () => { }; |
63 |
| - readable.push(wm.get(this).buffer); |
64 |
| - readable.push(null); |
65 |
| - return readable; |
| 112 | + return data.buffer; |
66 | 113 | }
|
67 | 114 |
|
68 |
| - toString() { |
69 |
| - return '[object Blob]'; |
| 115 | + /** |
| 116 | + * The Blob interface's stream() method is difference from native |
| 117 | + * and uses node streams instead of whatwg streams. |
| 118 | + * |
| 119 | + * @returns {Readable} Node readable stream |
| 120 | + */ |
| 121 | + stream() { |
| 122 | + return Readable.from(read(wm.get(this).parts)); |
70 | 123 | }
|
71 | 124 |
|
72 |
| - slice(...args) { |
| 125 | + /** |
| 126 | + * The Blob interface's slice() method creates and returns a |
| 127 | + * new Blob object which contains data from a subset of the |
| 128 | + * blob on which it's called. |
| 129 | + * |
| 130 | + * @param {number} [start] |
| 131 | + * @param {number} [end] |
| 132 | + * @param {string} [contentType] |
| 133 | + */ |
| 134 | + slice(start = 0, end = this.size, type = '') { |
73 | 135 | const {size} = this;
|
74 | 136 |
|
75 |
| - const start = args[0]; |
76 |
| - const end = args[1]; |
77 |
| - let relativeStart; |
78 |
| - let relativeEnd; |
| 137 | + let relativeStart = start < 0 ? Math.max(size + start, 0) : Math.min(start, size); |
| 138 | + let relativeEnd = end < 0 ? Math.max(size + end, 0) : Math.min(end, size); |
79 | 139 |
|
80 |
| - if (start === undefined) { |
81 |
| - relativeStart = 0; // |
82 |
| - } else if (start < 0) { |
83 |
| - relativeStart = Math.max(size + start, 0); // |
84 |
| - } else { |
85 |
| - relativeStart = Math.min(start, size); |
| 140 | + const span = Math.max(relativeEnd - relativeStart, 0); |
| 141 | + const parts = wm.get(this).parts.values(); |
| 142 | + const blobParts = []; |
| 143 | + let added = 0; |
| 144 | + |
| 145 | + for (const part of parts) { |
| 146 | + const size = ArrayBuffer.isView(part) ? part.byteLength : part.size; |
| 147 | + if (relativeStart && size <= relativeStart) { |
| 148 | + // Skip the beginning and change the relative |
| 149 | + // start & end position as we skip the unwanted parts |
| 150 | + relativeStart -= size; |
| 151 | + relativeEnd -= size; |
| 152 | + } else { |
| 153 | + const chunk = part.slice(relativeStart, Math.min(size, relativeEnd)); |
| 154 | + blobParts.push(chunk); |
| 155 | + added += size; |
| 156 | + relativeStart = 0; // All next sequental parts should start at 0 |
| 157 | + |
| 158 | + // don't add the overflow to new blobParts |
| 159 | + if (added >= span) { |
| 160 | + break; |
| 161 | + } |
| 162 | + } |
86 | 163 | }
|
87 | 164 |
|
88 |
| - if (end === undefined) { |
89 |
| - relativeEnd = size; // |
90 |
| - } else if (end < 0) { |
91 |
| - relativeEnd = Math.max(size + end, 0); // |
92 |
| - } else { |
93 |
| - relativeEnd = Math.min(end, size); |
94 |
| - } |
| 165 | + const blob = new Blob([], {type}); |
| 166 | + Object.assign(wm.get(blob), {size: span, parts: blobParts}); |
95 | 167 |
|
96 |
| - const span = Math.max(relativeEnd - relativeStart, 0); |
97 |
| - const slicedBuffer = wm.get(this).buffer.slice( |
98 |
| - relativeStart, |
99 |
| - relativeStart + span |
100 |
| - ); |
101 |
| - const blob = new Blob([], {type: args[2]}); |
102 |
| - const _ = wm.get(blob); |
103 |
| - _.buffer = slicedBuffer; |
104 | 168 | return blob;
|
105 | 169 | }
|
106 | 170 | }
|
|
0 commit comments