1'use strict'; 2 3const { 4 ObjectDefineProperties, 5 String, 6 StringPrototypeCharCodeAt, 7 Symbol, 8 Uint8Array, 9} = primordials; 10 11const { 12 TextDecoder, 13 TextEncoder, 14} = require('internal/encoding'); 15 16const { 17 TransformStream, 18} = require('internal/webstreams/transformstream'); 19 20const { customInspect } = require('internal/webstreams/util'); 21 22const { 23 codes: { 24 ERR_INVALID_THIS, 25 }, 26} = require('internal/errors'); 27 28const { 29 customInspectSymbol: kInspect, 30 kEmptyObject, 31 kEnumerableProperty, 32} = require('internal/util'); 33 34const kHandle = Symbol('kHandle'); 35const kTransform = Symbol('kTransform'); 36const kType = Symbol('kType'); 37const kPendingHighSurrogate = Symbol('kPendingHighSurrogate'); 38 39/** 40 * @typedef {import('./readablestream').ReadableStream} ReadableStream 41 * @typedef {import('./writablestream').WritableStream} WritableStream 42 */ 43 44function isTextEncoderStream(value) { 45 return typeof value?.[kHandle] === 'object' && 46 value?.[kType] === 'TextEncoderStream'; 47} 48 49function isTextDecoderStream(value) { 50 return typeof value?.[kHandle] === 'object' && 51 value?.[kType] === 'TextDecoderStream'; 52} 53 54class TextEncoderStream { 55 constructor() { 56 this[kPendingHighSurrogate] = null; 57 this[kType] = 'TextEncoderStream'; 58 this[kHandle] = new TextEncoder(); 59 this[kTransform] = new TransformStream({ 60 transform: (chunk, controller) => { 61 // https://encoding.spec.whatwg.org/#encode-and-enqueue-a-chunk 62 chunk = String(chunk); 63 let finalChunk = ''; 64 for (let i = 0; i < chunk.length; i++) { 65 const item = chunk[i]; 66 const codeUnit = StringPrototypeCharCodeAt(item, 0); 67 if (this[kPendingHighSurrogate] !== null) { 68 const highSurrogate = this[kPendingHighSurrogate]; 69 this[kPendingHighSurrogate] = null; 70 if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) { 71 finalChunk += highSurrogate + item; 72 continue; 73 } 74 finalChunk += '\uFFFD'; 75 } 76 if (0xD800 <= codeUnit && codeUnit <= 0xDBFF) { 77 this[kPendingHighSurrogate] = item; 78 continue; 79 } 80 if (0xDC00 <= codeUnit && codeUnit <= 0xDFFF) { 81 finalChunk += '\uFFFD'; 82 continue; 83 } 84 finalChunk += item; 85 } 86 if (finalChunk) { 87 const value = this[kHandle].encode(finalChunk); 88 controller.enqueue(value); 89 } 90 }, 91 flush: (controller) => { 92 // https://encoding.spec.whatwg.org/#encode-and-flush 93 if (this[kPendingHighSurrogate] !== null) { 94 controller.enqueue(new Uint8Array([0xEF, 0xBF, 0xBD])); 95 } 96 }, 97 }); 98 } 99 100 /** 101 * @readonly 102 * @type {string} 103 */ 104 get encoding() { 105 if (!isTextEncoderStream(this)) 106 throw new ERR_INVALID_THIS('TextEncoderStream'); 107 return this[kHandle].encoding; 108 } 109 110 /** 111 * @readonly 112 * @type {ReadableStream} 113 */ 114 get readable() { 115 if (!isTextEncoderStream(this)) 116 throw new ERR_INVALID_THIS('TextEncoderStream'); 117 return this[kTransform].readable; 118 } 119 120 /** 121 * @readonly 122 * @type {WritableStream} 123 */ 124 get writable() { 125 if (!isTextEncoderStream(this)) 126 throw new ERR_INVALID_THIS('TextEncoderStream'); 127 return this[kTransform].writable; 128 } 129 130 [kInspect](depth, options) { 131 if (!isTextEncoderStream(this)) 132 throw new ERR_INVALID_THIS('TextEncoderStream'); 133 return customInspect(depth, options, 'TextEncoderStream', { 134 encoding: this[kHandle].encoding, 135 readable: this[kTransform].readable, 136 writable: this[kTransform].writable, 137 }); 138 } 139} 140 141class TextDecoderStream { 142 /** 143 * @param {string} [encoding] 144 * @param {{ 145 * fatal? : boolean, 146 * ignoreBOM? : boolean, 147 * }} [options] 148 */ 149 constructor(encoding = 'utf-8', options = kEmptyObject) { 150 this[kType] = 'TextDecoderStream'; 151 this[kHandle] = new TextDecoder(encoding, options); 152 this[kTransform] = new TransformStream({ 153 transform: (chunk, controller) => { 154 const value = this[kHandle].decode(chunk, { stream: true }); 155 if (value) 156 controller.enqueue(value); 157 }, 158 flush: (controller) => { 159 const value = this[kHandle].decode(); 160 if (value) 161 controller.enqueue(value); 162 controller.terminate(); 163 }, 164 }); 165 } 166 167 /** 168 * @readonly 169 * @type {string} 170 */ 171 get encoding() { 172 if (!isTextDecoderStream(this)) 173 throw new ERR_INVALID_THIS('TextDecoderStream'); 174 return this[kHandle].encoding; 175 } 176 177 /** 178 * @readonly 179 * @type {boolean} 180 */ 181 get fatal() { 182 if (!isTextDecoderStream(this)) 183 throw new ERR_INVALID_THIS('TextDecoderStream'); 184 return this[kHandle].fatal; 185 } 186 187 /** 188 * @readonly 189 * @type {boolean} 190 */ 191 get ignoreBOM() { 192 if (!isTextDecoderStream(this)) 193 throw new ERR_INVALID_THIS('TextDecoderStream'); 194 return this[kHandle].ignoreBOM; 195 } 196 197 /** 198 * @readonly 199 * @type {ReadableStream} 200 */ 201 get readable() { 202 if (!isTextDecoderStream(this)) 203 throw new ERR_INVALID_THIS('TextDecoderStream'); 204 return this[kTransform].readable; 205 } 206 207 /** 208 * @readonly 209 * @type {WritableStream} 210 */ 211 get writable() { 212 if (!isTextDecoderStream(this)) 213 throw new ERR_INVALID_THIS('TextDecoderStream'); 214 return this[kTransform].writable; 215 } 216 217 [kInspect](depth, options) { 218 if (!isTextDecoderStream(this)) 219 throw new ERR_INVALID_THIS('TextDecoderStream'); 220 return customInspect(depth, options, 'TextDecoderStream', { 221 encoding: this[kHandle].encoding, 222 fatal: this[kHandle].fatal, 223 ignoreBOM: this[kHandle].ignoreBOM, 224 readable: this[kTransform].readable, 225 writable: this[kTransform].writable, 226 }); 227 } 228} 229 230ObjectDefineProperties(TextEncoderStream.prototype, { 231 encoding: kEnumerableProperty, 232 readable: kEnumerableProperty, 233 writable: kEnumerableProperty, 234}); 235 236ObjectDefineProperties(TextDecoderStream.prototype, { 237 encoding: kEnumerableProperty, 238 fatal: kEnumerableProperty, 239 ignoreBOM: kEnumerableProperty, 240 readable: kEnumerableProperty, 241 writable: kEnumerableProperty, 242}); 243 244module.exports = { 245 TextEncoderStream, 246 TextDecoderStream, 247}; 248