• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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