char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somport encoding extension to typescriptf2e5e0e

main
9.8 KiB309 linesraw
1import { registerUncloneable } from "ext:web/serialization.js";
2import {
3  byteLength as sharedLength,
4  growable,
5} from "ext:core/shared_array_buffer";
6import {
7  ArrayBuffer,
8  getter,
9  MathMin,
10  RangeError,
11  String,
12  StringPrototypeSlice as slice,
13  StringPrototypeToWellFormed as wellFormed,
14  TypedArrayPrototypeGetBuffer as buffer,
15  TypedArrayPrototypeGetByteLength as byteLength,
16  TypedArrayPrototypeGetByteOffset as byteOffset,
17  TypedArrayPrototypeGetTag as tag,
18  TypeError,
19  Uint8Array,
20  Uint8ArrayPrototypeSet as setBytes,
21  uncurryThis,
22  WeakMap,
23  WeakMapPrototypeSet as set,
24} from "ext:core/primordials";
25import { dictionary, state, string } from "ext:web/idl.js";
26import { installInterface } from "ext:web/interface.js";
27import {
28  TransformStream,
29  TransformStreamDefaultController,
30} from "ext:webstreams/streams.js";
31import {
32  op_encoding_close as dispose,
33  op_encoding_decode as decodeBytes,
34  op_encoding_encode as encodeBytes,
35  op_encoding_encode_into as encodeInto,
36  op_encoding_label as resolve,
37} from "ext:core/ops";
38import type { EncodeCounts } from "ext:core/ops";
39import type { ReadableStream, WritableStream } from "ext:webstreams/streams.js";
40
41interface DecoderState {
42  id: number | null;
43  encoding: string;
44  fatal: boolean;
45  ignoreBOM: boolean;
46}
47
48const isView = ArrayBuffer.isView;
49const abLength = getter(ArrayBuffer.prototype, "byteLength");
50const dvBuffer = getter(DataView.prototype, "buffer"),
51  dvOffset = getter(DataView.prototype, "byteOffset"),
52  dvLength = getter(DataView.prototype, "byteLength");
53const resizable = getter(ArrayBuffer.prototype, "resizable");
54const charCode = uncurryThis(String.prototype.charCodeAt);
55const register: (registry: FinalizationRegistry<DecoderState>, target: WeakKey, held: DecoderState) => void =
56  uncurryThis(FinalizationRegistry.prototype.register);
57const registry = new FinalizationRegistry<DecoderState>((s) => {
58  if (s.id !== null) dispose(s.id);
59});
60const encoders = new WeakMap<object, object>(),
61  decoders = new WeakMap<object, DecoderState>(),
62  encoderStreams = new WeakMap<object, { transform: TransformStream<unknown, Uint8Array<ArrayBuffer>> }>(),
63  decoderStreams = new WeakMap<object, { transform: TransformStream<unknown, string>; decoder: TextDecoder }>();
64registerUncloneable(encoders);
65registerUncloneable(decoders);
66registerUncloneable(encoderStreams);
67registerUncloneable(decoderStreams);
68const empty = new Uint8Array(0);
69// Capturing a generic prototype erases its chunk parameters; retain them at these bindings.
70const readable = getter(TransformStream.prototype, "readable") as
71  <I, O>(stream: TransformStream<I, O>) => ReadableStream<O>;
72const writable = getter(TransformStream.prototype, "writable") as
73  <I, O>(stream: TransformStream<I, O>) => WritableStream<I>;
74const enqueue = uncurryThis(
75  TransformStreamDefaultController.prototype.enqueue,
76) as <O>(controller: TransformStreamDefaultController<O>, chunk: NoInfer<O>) => void;
77
78function bufferLength(b: unknown): number {
79  let length, shared = false;
80  try {
81    length = abLength(b);
82  } catch {
83    try {
84      length = sharedLength(b);
85      shared = true;
86    } catch {
87      throw new TypeError("Expected an ArrayBuffer or an ArrayBuffer view");
88    }
89  }
90  if (shared ? growable(b) : resizable(b)) {
91    throw new TypeError(
92      "Resizable buffers are not allowed by the Encoding API",
93    );
94  }
95  return length;
96}
97function view(value: unknown): Uint8Array<ArrayBufferLike> {
98  if (!isView(value)) {
99    const length = bufferLength(value);
100    if (length === 0) return empty;
101    return new Uint8Array(value as ArrayBufferLike, 0, length);
102  }
103  const dataView = tag(value) === undefined,
104    b = dataView ? dvBuffer(value) : buffer(value);
105  bufferLength(b);
106  let offset, length;
107  if (dataView) {
108    try {
109      offset = dvOffset(value);
110      length = dvLength(value);
111    } catch {
112      return empty;
113    }
114  } else {
115    offset = byteOffset(value);
116    length = byteLength(value);
117  }
118  if (length === 0) return empty;
119  return new Uint8Array(b, offset, length);
120}
121function decodeInput(value: unknown): Uint8Array<ArrayBuffer> {
122  // Rust never borrows shared or resizable V8 backing storage.
123  const bytes = view(value);
124  try {
125    abLength(buffer(bytes));
126    return bytes as Uint8Array<ArrayBuffer>;
127  } catch {
128    return new Uint8Array(bytes);
129  }
130}
131function decode(s: DecoderState, bytes: Uint8Array<ArrayBuffer>, stream: boolean): string {
132  let result;
133  try {
134    result = decodeBytes([
135      s.id,
136      s.encoding,
137      s.fatal,
138      s.ignoreBOM,
139      bytes,
140      stream,
141    ]);
142  } catch (error) {
143    if (s.id !== null) dispose(s.id);
144    s.id = null;
145    throw error;
146  }
147  s.id = result[2];
148  if (result[1]) {
149    throw new TypeError("The encoded data is not valid for this encoding");
150  }
151  return result[0];
152}
153
154class TextEncoder {
155  constructor() {
156    set(encoders, this, {});
157  }
158  get encoding(): "utf-8" {
159    state(encoders, this);
160    return "utf-8";
161  }
162  encode(input: unknown = ""): Uint8Array<ArrayBuffer> {
163    state(encoders, this);
164    return encodeBytes(wellFormed(string(input)));
165  }
166  encodeInto(rawSource: unknown, destination: unknown): EncodeCounts {
167    state(encoders, this);
168    if (arguments.length < 2) {
169      throw new TypeError("TextEncoder.encodeInto requires two arguments");
170    }
171    const source = string(rawSource);
172    if (tag(destination) !== "Uint8Array") {
173      throw new TypeError("Destination must be a Uint8Array");
174    }
175    const storage = buffer(destination);
176    bufferLength(storage);
177    let shared = false;
178    try {
179      abLength(storage);
180    } catch {
181      shared = true;
182    }
183    if (!shared) return encodeInto(source, destination as Uint8Array<ArrayBuffer>);
184    const capacity = byteLength(destination);
185    // One extra UTF-16 unit keeps a pair intact at the bounded source slice.
186    const prefix = slice(source, 0, capacity + 1);
187    const bytes = new Uint8Array(MathMin(capacity, prefix.length * 3));
188    const result = encodeInto(prefix, bytes);
189    if (result.written) {
190      setBytes(destination as Uint8Array<ArrayBufferLike>, new Uint8Array(buffer(bytes), 0, result.written));
191    }
192    return result;
193  }
194}
195class TextDecoder {
196  constructor(rawLabel: unknown = "utf-8", rawOptions: unknown = undefined) {
197    const label = string(rawLabel);
198    const options = dictionary(rawOptions);
199    const fatal = !!options.fatal, ignoreBOM = !!options.ignoreBOM;
200    const encoding = resolve(label);
201    if (encoding === null) {
202      throw new RangeError(`Unsupported encoding: ${label}`);
203    }
204    const s: DecoderState = { id: null, encoding, fatal, ignoreBOM };
205    set(decoders, this, s);
206    register(registry, this, s);
207  }
208  get encoding(): string {
209    return state(decoders, this).encoding;
210  }
211  get fatal(): boolean {
212    return state(decoders, this).fatal;
213  }
214  get ignoreBOM(): boolean {
215    return state(decoders, this).ignoreBOM;
216  }
217  decode(input: unknown = undefined, rawOptions: unknown = undefined): string {
218    const s = state(decoders, this);
219    if (input !== undefined) view(input);
220    const options = dictionary(rawOptions);
221    const stream = !!options.stream;
222    return decode(s, input === undefined ? empty : decodeInput(input), stream);
223  }
224}
225class TextEncoderStream {
226  constructor() {
227    let pending = "";
228    const transform = new TransformStream<unknown, Uint8Array<ArrayBuffer>>(
229      {
230        __proto__: null,
231        transform(rawChunk, controller) {
232          let chunk = string(rawChunk);
233          if (chunk.length === 0) return;
234          chunk = pending + chunk;
235          pending = "";
236          const last = charCode(chunk, chunk.length - 1);
237          if (last >= 0xd800 && last <= 0xdbff) {
238            pending = slice(chunk, -1);
239            chunk = slice(chunk, 0, -1);
240          }
241          if (chunk.length) {
242            enqueue(controller, encodeBytes(wellFormed(chunk)));
243          }
244        },
245        flush(controller) {
246          if (pending) enqueue(controller, encodeBytes("\ufffd"));
247        },
248      },
249      { __proto__: null },
250      { __proto__: null },
251    );
252    set(encoderStreams, this, { transform });
253  }
254  get encoding(): "utf-8" {
255    state(encoderStreams, this);
256    return "utf-8";
257  }
258  get readable(): ReadableStream<Uint8Array<ArrayBuffer>> {
259    return readable(state(encoderStreams, this).transform);
260  }
261  get writable(): WritableStream<unknown> {
262    return writable(state(encoderStreams, this).transform);
263  }
264}
265class TextDecoderStream {
266  constructor(label: unknown = "utf-8", options: unknown = undefined) {
267    const decoder = new TextDecoder(label, options);
268    const transform = new TransformStream<unknown, string>(
269      {
270        __proto__: null,
271        transform(chunk, controller) {
272          const text = decode(
273            state(decoders, decoder),
274            decodeInput(chunk),
275            true,
276          );
277          if (text.length) enqueue(controller, text);
278        },
279        flush(controller) {
280          const text = decode(state(decoders, decoder), empty, false);
281          if (text.length) enqueue(controller, text);
282        },
283      },
284      { __proto__: null },
285      { __proto__: null },
286    );
287    set(decoderStreams, this, { transform, decoder });
288  }
289  get encoding(): string {
290    return state(decoders, state(decoderStreams, this).decoder).encoding;
291  }
292  get fatal(): boolean {
293    return state(decoders, state(decoderStreams, this).decoder).fatal;
294  }
295  get ignoreBOM(): boolean {
296    return state(decoders, state(decoderStreams, this).decoder).ignoreBOM;
297  }
298  get readable(): ReadableStream<string> {
299    return readable(state(decoderStreams, this).transform);
300  }
301  get writable(): WritableStream<unknown> {
302    return writable(state(decoderStreams, this).transform);
303  }
304}
305for (
306  const ctor of [TextEncoder, TextDecoder, TextEncoderStream, TextDecoderStream]
307) installInterface(ctor.name, ctor);
308
309export { TextDecoder, TextDecoderStream, TextEncoder, TextEncoderStream };