diff options
-rw-r--r-- | Cargo.lock | 9 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | cli/Cargo.toml | 1 | ||||
-rw-r--r-- | cli/build.rs | 75 | ||||
-rw-r--r-- | cli/dts/lib.deno.shared_globals.d.ts | 184 | ||||
-rw-r--r-- | cli/js.rs | 1 | ||||
-rw-r--r-- | cli/main.rs | 3 | ||||
-rw-r--r-- | cli/rt/07_base64.js | 157 | ||||
-rw-r--r-- | cli/tests/performance_stats.out | 2 | ||||
-rw-r--r-- | cli/tsc/99_main_compiler.js | 5 | ||||
-rw-r--r-- | op_crates/web/00_dom_exception.js (renamed from cli/rt/00_dom_exception.js) | 0 | ||||
-rw-r--r-- | op_crates/web/01_event.js (renamed from cli/rt/01_event.js) | 4 | ||||
-rw-r--r-- | op_crates/web/08_text_encoding.js (renamed from cli/rt/08_text_encoding.js) | 154 | ||||
-rw-r--r-- | op_crates/web/Cargo.toml | 20 | ||||
-rw-r--r-- | op_crates/web/event_target_test.js | 244 | ||||
-rw-r--r-- | op_crates/web/event_test.js | 111 | ||||
-rw-r--r-- | op_crates/web/lib.deno_web.d.ts | 187 | ||||
-rw-r--r-- | op_crates/web/lib.rs | 102 | ||||
-rw-r--r-- | op_crates/web/text_encoding_test.js | 243 |
19 files changed, 1159 insertions, 344 deletions
diff --git a/Cargo.lock b/Cargo.lock index a43eda71d..80f43a6eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,7 @@ dependencies = [ "clap", "deno_core", "deno_lint", + "deno_web", "dissimilar", "dlopen", "dprint-plugin-typescript", @@ -460,6 +461,14 @@ dependencies = [ ] [[package]] +name = "deno_web" +version = "0.1.0" +dependencies = [ + "deno_core", + "futures", +] + +[[package]] name = "derive_deref" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml index b89523628..80e6ce4e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "core", "test_plugin", "test_util", + "op_crates/web", ] exclude = [ "std/hash/_wasm" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 8a8958b63..bedbdb657 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -16,6 +16,7 @@ path = "main.rs" [build-dependencies] deno_core = { path = "../core", version = "0.51.0" } +deno_web = { path = "../op_crates/web", version = "0.1.0" } [target.'cfg(windows)'.build-dependencies] winres = "0.1" diff --git a/cli/build.rs b/cli/build.rs index 777e74558..5a7f173e5 100644 --- a/cli/build.rs +++ b/cli/build.rs @@ -38,6 +38,11 @@ fn create_compiler_snapshot( ) { let mut runtime_isolate = CoreIsolate::new(StartupData::None, true); let mut custom_libs: HashMap<String, PathBuf> = HashMap::new(); + let web_scripts = deno_web::get_scripts(); + custom_libs.insert( + "lib.deno.web.d.ts".to_string(), + PathBuf::from(web_scripts.declaration), + ); custom_libs.insert( "lib.deno.window.d.ts".to_string(), cwd.join("dts/lib.deno.window.d.ts"), @@ -80,6 +85,10 @@ fn main() { // op_fetch_asset::trace_serializer(); println!("cargo:rustc-env=TS_VERSION={}", ts_version()); + println!( + "cargo:rustc-env=DENO_WEB_LIB_PATH={}", + deno_web::get_scripts().declaration + ); println!( "cargo:rustc-env=TARGET={}", @@ -93,7 +102,7 @@ fn main() { let runtime_snapshot_path = o.join("CLI_SNAPSHOT.bin"); let compiler_snapshot_path = o.join("COMPILER_SNAPSHOT.bin"); - let js_files = get_js_files("rt"); + let js_files = get_js_files_for_rt(); create_runtime_snapshot(&runtime_snapshot_path, js_files); let js_files = get_js_files("tsc"); @@ -123,3 +132,67 @@ fn get_js_files(d: &str) -> Vec<String> { js_files.sort(); js_files } + +fn get_js_files_for_rt() -> Vec<String> { + let web_scripts = deno_web::get_scripts(); + + let f = vec![ + "rt/00_bootstrap_namespace.js", + &web_scripts.dom_exception, + "rt/01_build.js", + "rt/01_colors.js", + "rt/01_errors.js", + &web_scripts.event, + "rt/01_internals.js", + "rt/01_version.js", + "rt/01_web_util.js", + "rt/02_abort_signal.js", + "rt/02_console.js", + "rt/03_dom_iterable.js", + "rt/06_util.js", + &web_scripts.text_encoding, + "rt/10_dispatch_json.js", + "rt/10_dispatch_minimal.js", + "rt/11_crypto.js", + "rt/11_resources.js", + "rt/11_streams.js", + "rt/11_timers.js", + "rt/11_url.js", + "rt/11_workers.js", + "rt/12_io.js", + "rt/13_buffer.js", + "rt/20_blob.js", + "rt/20_headers.js", + "rt/20_streams_queuing_strategy.js", + "rt/21_dom_file.js", + "rt/22_form_data.js", + "rt/23_multipart.js", + "rt/24_body.js", + "rt/25_request.js", + "rt/26_fetch.js", + "rt/30_files.js", + "rt/30_fs.js", + "rt/30_metrics.js", + "rt/30_net.js", + "rt/30_os.js", + "rt/40_compiler_api.js", + "rt/40_diagnostics.js", + "rt/40_error_stack.js", + "rt/40_fs_events.js", + "rt/40_net_unstable.js", + "rt/40_performance.js", + "rt/40_permissions.js", + "rt/40_plugins.js", + "rt/40_process.js", + "rt/40_read_file.js", + "rt/40_repl.js", + "rt/40_signals.js", + "rt/40_testing.js", + "rt/40_tls.js", + "rt/40_tty.js", + "rt/40_write_file.js", + "rt/90_deno_ns.js", + "rt/99_main.js", + ]; + f.iter().map(|p| p.to_string()).collect() +} diff --git a/cli/dts/lib.deno.shared_globals.d.ts b/cli/dts/lib.deno.shared_globals.d.ts index e331baee1..ec5367962 100644 --- a/cli/dts/lib.deno.shared_globals.d.ts +++ b/cli/dts/lib.deno.shared_globals.d.ts @@ -4,6 +4,7 @@ /// <reference no-default-lib="true" /> /// <reference lib="esnext" /> +/// <reference lib="deno.web" /> // This follows the WebIDL at: https://webassembly.github.io/spec/js-api/ // and: https://webassembly.github.io/spec/web-api/ @@ -529,12 +530,6 @@ interface DOMStringList { [index: number]: string; } -declare class DOMException extends Error { - constructor(message?: string, name?: string); - readonly name: string; - readonly message: string; -} - type BufferSource = ArrayBufferView | ArrayBuffer; type BlobPart = BufferSource | Blob | string; @@ -1030,46 +1025,6 @@ declare function fetch( init?: RequestInit, ): Promise<Response>; -/** Decodes a string of data which has been encoded using base-64 encoding. - * - * console.log(atob("aGVsbG8gd29ybGQ=")); // outputs 'hello world' - */ -declare function atob(s: string): string; - -/** Creates a base-64 ASCII encoded string from the input string. - * - * console.log(btoa("hello world")); // outputs "aGVsbG8gd29ybGQ=" - */ -declare function btoa(s: string): string; - -declare class TextDecoder { - /** Returns encoding's name, lowercased. */ - readonly encoding: string; - /** Returns `true` if error mode is "fatal", and `false` otherwise. */ - readonly fatal: boolean; - /** Returns `true` if ignore BOM flag is set, and `false` otherwise. */ - readonly ignoreBOM = false; - constructor( - label?: string, - options?: { fatal?: boolean; ignoreBOM?: boolean }, - ); - /** Returns the result of running encoding's decoder. */ - decode(input?: BufferSource, options?: { stream?: false }): string; - readonly [Symbol.toStringTag]: string; -} - -declare class TextEncoder { - /** Returns "utf-8". */ - readonly encoding = "utf-8"; - /** Returns the result of running UTF-8's encoder. */ - encode(input?: string): Uint8Array; - encodeInto( - input: string, - dest: Uint8Array, - ): { read: number; written: number }; - readonly [Symbol.toStringTag]: string; -} - interface URLSearchParams { /** Appends a specified key/value pair as a new search parameter. * @@ -1444,143 +1399,6 @@ declare class PerformanceMeasure extends PerformanceEntry { readonly entryType: "measure"; } -interface EventInit { - bubbles?: boolean; - cancelable?: boolean; - composed?: boolean; -} - -/** An event which takes place in the DOM. */ -declare class Event { - constructor(type: string, eventInitDict?: EventInit); - /** Returns true or false depending on how event was initialized. True if - * event goes through its target's ancestors in reverse tree order, and - * false otherwise. */ - readonly bubbles: boolean; - cancelBubble: boolean; - /** Returns true or false depending on how event was initialized. Its return - * value does not always carry meaning, but true can indicate that part of the - * operation during which event was dispatched, can be canceled by invoking - * the preventDefault() method. */ - readonly cancelable: boolean; - /** Returns true or false depending on how event was initialized. True if - * event invokes listeners past a ShadowRoot node that is the root of its - * target, and false otherwise. */ - readonly composed: boolean; - /** Returns the object whose event listener's callback is currently being - * invoked. */ - readonly currentTarget: EventTarget | null; - /** Returns true if preventDefault() was invoked successfully to indicate - * cancellation, and false otherwise. */ - readonly defaultPrevented: boolean; - /** Returns the event's phase, which is one of NONE, CAPTURING_PHASE, - * AT_TARGET, and BUBBLING_PHASE. */ - readonly eventPhase: number; - /** Returns true if event was dispatched by the user agent, and false - * otherwise. */ - readonly isTrusted: boolean; - /** Returns the object to which event is dispatched (its target). */ - readonly target: EventTarget | null; - /** Returns the event's timestamp as the number of milliseconds measured - * relative to the time origin. */ - readonly timeStamp: number; - /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ - readonly type: string; - /** Returns the invocation target objects of event's path (objects on which - * listeners will be invoked), except for any nodes in shadow trees of which - * the shadow root's mode is "closed" that are not reachable from event's - * currentTarget. */ - composedPath(): EventTarget[]; - /** If invoked when the cancelable attribute value is true, and while - * executing a listener for the event with passive set to false, signals to - * the operation that caused event to be dispatched that it needs to be - * canceled. */ - preventDefault(): void; - /** Invoking this method prevents event from reaching any registered event - * listeners after the current one finishes running and, when dispatched in a - * tree, also prevents event from reaching any other objects. */ - stopImmediatePropagation(): void; - /** When dispatched in a tree, invoking this method prevents event from - * reaching any objects other than the current object. */ - stopPropagation(): void; - readonly AT_TARGET: number; - readonly BUBBLING_PHASE: number; - readonly CAPTURING_PHASE: number; - readonly NONE: number; - static readonly AT_TARGET: number; - static readonly BUBBLING_PHASE: number; - static readonly CAPTURING_PHASE: number; - static readonly NONE: number; -} - -/** - * EventTarget is a DOM interface implemented by objects that can receive events - * and may have listeners for them. - */ -declare class EventTarget { - /** Appends an event listener for events whose type attribute value is type. - * The callback argument sets the callback that will be invoked when the event - * is dispatched. - * - * The options argument sets listener-specific options. For compatibility this - * can be a boolean, in which case the method behaves exactly as if the value - * was specified as options's capture. - * - * When set to true, options's capture prevents callback from being invoked - * when the event's eventPhase attribute value is BUBBLING_PHASE. When false - * (or not present), callback will not be invoked when event's eventPhase - * attribute value is CAPTURING_PHASE. Either way, callback will be invoked if - * event's eventPhase attribute value is AT_TARGET. - * - * When set to true, options's passive indicates that the callback will not - * cancel the event by invoking preventDefault(). This is used to enable - * performance optimizations described in ยง 2.8 Observing event listeners. - * - * When set to true, options's once indicates that the callback will only be - * invoked once after which the event listener will be removed. - * - * The event listener is appended to target's event listener list and is not - * appended if it has the same type, callback, and capture. */ - addEventListener( - type: string, - listener: EventListenerOrEventListenerObject | null, - options?: boolean | AddEventListenerOptions, - ): void; - /** Dispatches a synthetic event event to target and returns true if either - * event's cancelable attribute value is false or its preventDefault() method - * was not invoked, and false otherwise. */ - dispatchEvent(event: Event): boolean; - /** Removes the event listener in target's event listener list with the same - * type, callback, and options. */ - removeEventListener( - type: string, - callback: EventListenerOrEventListenerObject | null, - options?: EventListenerOptions | boolean, - ): void; - [Symbol.toStringTag]: string; -} - -interface EventListener { - (evt: Event): void | Promise<void>; -} - -interface EventListenerObject { - handleEvent(evt: Event): void | Promise<void>; -} - -declare type EventListenerOrEventListenerObject = - | EventListener - | EventListenerObject; - -interface AddEventListenerOptions extends EventListenerOptions { - once?: boolean; - passive?: boolean; -} - -interface EventListenerOptions { - capture?: boolean; -} - /** Events measuring progress of an underlying process, like an HTTP request * (for an XMLHttpRequest, or the loading of the underlying resource of an * <img>, <audio>, <video>, <style> or <link>). */ @@ -5,6 +5,7 @@ pub static CLI_SNAPSHOT: &[u8] = pub static COMPILER_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/COMPILER_SNAPSHOT.bin")); pub static DENO_NS_LIB: &str = include_str!("dts/lib.deno.ns.d.ts"); +pub static DENO_WEB_LIB: &str = include_str!(env!("DENO_WEB_LIB_PATH")); pub static SHARED_GLOBALS_LIB: &str = include_str!("dts/lib.deno.shared_globals.d.ts"); pub static WINDOW_LIB: &str = include_str!("dts/lib.deno.window.d.ts"); diff --git a/cli/main.rs b/cli/main.rs index 97f3161df..728a23db0 100644 --- a/cli/main.rs +++ b/cli/main.rs @@ -287,8 +287,9 @@ async fn print_file_info( fn get_types(unstable: bool) -> String { let mut types = format!( - "{}\n{}\n{}", + "{}\n{}\n{}\n{}", crate::js::DENO_NS_LIB, + crate::js::DENO_WEB_LIB, crate::js::SHARED_GLOBALS_LIB, crate::js::WINDOW_LIB, ); diff --git a/cli/rt/07_base64.js b/cli/rt/07_base64.js deleted file mode 100644 index 86739fd52..000000000 --- a/cli/rt/07_base64.js +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. - -// Forked from https://github.com/beatgammit/base64-js -// Copyright (c) 2014 Jameson Little. MIT License. - -((window) => { - const lookup = []; - const revLookup = []; - - const code = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for (let i = 0, len = code.length; i < len; ++i) { - lookup[i] = code[i]; - revLookup[code.charCodeAt(i)] = i; - } - - // Support decoding URL-safe base64 strings, as Node.js does. - // See: https://en.wikipedia.org/wiki/Base64#URL_applications - revLookup["-".charCodeAt(0)] = 62; - revLookup["_".charCodeAt(0)] = 63; - - function getLens(b64) { - const len = b64.length; - - if (len % 4 > 0) { - throw new Error("Invalid string. Length must be a multiple of 4"); - } - - // Trim off extra bytes after placeholder bytes are found - // See: https://github.com/beatgammit/base64-js/issues/42 - let validLen = b64.indexOf("="); - if (validLen === -1) validLen = len; - - const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4); - - return [validLen, placeHoldersLen]; - } - - // base64 is 4/3 + up to two characters of the original data - function byteLength(b64) { - const lens = getLens(b64); - const validLen = lens[0]; - const placeHoldersLen = lens[1]; - return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; - } - - function _byteLength( - b64, - validLen, - placeHoldersLen, - ) { - return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; - } - - function toByteArray(b64) { - let tmp; - const lens = getLens(b64); - const validLen = lens[0]; - const placeHoldersLen = lens[1]; - - const arr = new Uint8Array(_byteLength(b64, validLen, placeHoldersLen)); - - let curByte = 0; - - // if there are placeholders, only get up to the last complete 4 chars - const len = placeHoldersLen > 0 ? validLen - 4 : validLen; - - let i; - for (i = 0; i < len; i += 4) { - tmp = (revLookup[b64.charCodeAt(i)] << 18) | - (revLookup[b64.charCodeAt(i + 1)] << 12) | - (revLookup[b64.charCodeAt(i + 2)] << 6) | - revLookup[b64.charCodeAt(i + 3)]; - arr[curByte++] = (tmp >> 16) & 0xff; - arr[curByte++] = (tmp >> 8) & 0xff; - arr[curByte++] = tmp & 0xff; - } - - if (placeHoldersLen === 2) { - tmp = (revLookup[b64.charCodeAt(i)] << 2) | - (revLookup[b64.charCodeAt(i + 1)] >> 4); - arr[curByte++] = tmp & 0xff; - } - - if (placeHoldersLen === 1) { - tmp = (revLookup[b64.charCodeAt(i)] << 10) | - (revLookup[b64.charCodeAt(i + 1)] << 4) | - (revLookup[b64.charCodeAt(i + 2)] >> 2); - arr[curByte++] = (tmp >> 8) & 0xff; - arr[curByte++] = tmp & 0xff; - } - - return arr; - } - - function tripletToBase64(num) { - return ( - lookup[(num >> 18) & 0x3f] + - lookup[(num >> 12) & 0x3f] + - lookup[(num >> 6) & 0x3f] + - lookup[num & 0x3f] - ); - } - - function encodeChunk(uint8, start, end) { - let tmp; - const output = []; - for (let i = start; i < end; i += 3) { - tmp = ((uint8[i] << 16) & 0xff0000) + - ((uint8[i + 1] << 8) & 0xff00) + - (uint8[i + 2] & 0xff); - output.push(tripletToBase64(tmp)); - } - return output.join(""); - } - - function fromByteArray(uint8) { - let tmp; - const len = uint8.length; - const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes - const parts = []; - const maxChunkLength = 16383; // must be multiple of 3 - - // go through the array every three bytes, we'll deal with trailing stuff later - for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { - parts.push( - encodeChunk( - uint8, - i, - i + maxChunkLength > len2 ? len2 : i + maxChunkLength, - ), - ); - } - - // pad the end with zeros, but make sure to not forget the extra bytes - if (extraBytes === 1) { - tmp = uint8[len - 1]; - parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + "=="); - } else if (extraBytes === 2) { - tmp = (uint8[len - 2] << 8) + uint8[len - 1]; - parts.push( - lookup[tmp >> 10] + - lookup[(tmp >> 4) & 0x3f] + - lookup[(tmp << 2) & 0x3f] + - "=", - ); - } - - return parts.join(""); - } - - window.__bootstrap.base64 = { - byteLength, - toByteArray, - fromByteArray, - }; -})(this); diff --git a/cli/tests/performance_stats.out b/cli/tests/performance_stats.out index 9fea7715a..c623d81da 100644 --- a/cli/tests/performance_stats.out +++ b/cli/tests/performance_stats.out @@ -1,5 +1,5 @@ [WILDCARD] -Files: 43 +Files: 44 Nodes: [WILDCARD] Identifiers: [WILDCARD] Symbols: [WILDCARD] diff --git a/cli/tsc/99_main_compiler.js b/cli/tsc/99_main_compiler.js index b8673b790..41051f2e0 100644 --- a/cli/tsc/99_main_compiler.js +++ b/cli/tsc/99_main_compiler.js @@ -776,6 +776,7 @@ delete Object.prototype.__proto__; // as these are internal APIs of TypeScript which maintain valid libs ts.libs.push("deno.ns", "deno.window", "deno.worker", "deno.shared_globals"); ts.libMap.set("deno.ns", "lib.deno.ns.d.ts"); + ts.libMap.set("deno.web", "lib.deno.web.d.ts"); ts.libMap.set("deno.window", "lib.deno.window.d.ts"); ts.libMap.set("deno.worker", "lib.deno.worker.d.ts"); ts.libMap.set("deno.shared_globals", "lib.deno.shared_globals.d.ts"); @@ -788,6 +789,10 @@ delete Object.prototype.__proto__; ts.ScriptTarget.ESNext, ); SNAPSHOT_HOST.getSourceFile( + `${ASSETS}/lib.deno.web.d.ts`, + ts.ScriptTarget.ESNext, + ); + SNAPSHOT_HOST.getSourceFile( `${ASSETS}/lib.deno.window.d.ts`, ts.ScriptTarget.ESNext, ); diff --git a/cli/rt/00_dom_exception.js b/op_crates/web/00_dom_exception.js index 6d72779b0..6d72779b0 100644 --- a/cli/rt/00_dom_exception.js +++ b/op_crates/web/00_dom_exception.js diff --git a/cli/rt/01_event.js b/op_crates/web/01_event.js index 35967e0a1..48899e6fd 100644 --- a/cli/rt/01_event.js +++ b/op_crates/web/01_event.js @@ -1038,6 +1038,10 @@ window.EventTarget = EventTarget; window.ErrorEvent = ErrorEvent; window.CustomEvent = CustomEvent; + window.dispatchEvent = EventTarget.prototype.dispatchEvent; + window.addEventListener = EventTarget.prototype.addEventListener; + window.removeEventListener = EventTarget.prototype.removeEventListener; + window.__bootstrap = (window.__bootstrap || {}); window.__bootstrap.eventTarget = { setEventTargetData, }; diff --git a/cli/rt/08_text_encoding.js b/op_crates/web/08_text_encoding.js index a7a9a49e4..b8959142f 100644 --- a/cli/rt/08_text_encoding.js +++ b/op_crates/web/08_text_encoding.js @@ -26,7 +26,6 @@ ((window) => { const core = Deno.core; - const base64 = window.__bootstrap.base64; const CONTINUE = null; const END_OF_STREAM = -1; @@ -679,6 +678,159 @@ return outString; } + // Following code is forked from https://github.com/beatgammit/base64-js + // Copyright (c) 2014 Jameson Little. MIT License. + const lookup = []; + const revLookup = []; + + const code = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (let i = 0, len = code.length; i < len; ++i) { + lookup[i] = code[i]; + revLookup[code.charCodeAt(i)] = i; + } + + // Support decoding URL-safe base64 strings, as Node.js does. + // See: https://en.wikipedia.org/wiki/Base64#URL_applications + revLookup["-".charCodeAt(0)] = 62; + revLookup["_".charCodeAt(0)] = 63; + + function getLens(b64) { + const len = b64.length; + + if (len % 4 > 0) { + throw new Error("Invalid string. Length must be a multiple of 4"); + } + + // Trim off extra bytes after placeholder bytes are found + // See: https://github.com/beatgammit/base64-js/issues/42 + let validLen = b64.indexOf("="); + if (validLen === -1) validLen = len; + + const placeHoldersLen = validLen === len ? 0 : 4 - (validLen % 4); + + return [validLen, placeHoldersLen]; + } + + // base64 is 4/3 + up to two characters of the original data + function byteLength(b64) { + const lens = getLens(b64); + const validLen = lens[0]; + const placeHoldersLen = lens[1]; + return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; + } + + function _byteLength( + b64, + validLen, + placeHoldersLen, + ) { + return ((validLen + placeHoldersLen) * 3) / 4 - placeHoldersLen; + } + + function toByteArray(b64) { + let tmp; + const lens = getLens(b64); + const validLen = lens[0]; + const placeHoldersLen = lens[1]; + + const arr = new Uint8Array(_byteLength(b64, validLen, placeHoldersLen)); + + let curByte = 0; + + // if there are placeholders, only get up to the last complete 4 chars + const len = placeHoldersLen > 0 ? validLen - 4 : validLen; + + let i; + for (i = 0; i < len; i += 4) { + tmp = (revLookup[b64.charCodeAt(i)] << 18) | + (revLookup[b64.charCodeAt(i + 1)] << 12) | + (revLookup[b64.charCodeAt(i + 2)] << 6) | + revLookup[b64.charCodeAt(i + 3)]; + arr[curByte++] = (tmp >> 16) & 0xff; + arr[curByte++] = (tmp >> 8) & 0xff; + arr[curByte++] = tmp & 0xff; + } + + if (placeHoldersLen === 2) { + tmp = (revLookup[b64.charCodeAt(i)] << 2) | + (revLookup[b64.charCodeAt(i + 1)] >> 4); + arr[curByte++] = tmp & 0xff; + } + + if (placeHoldersLen === 1) { + tmp = (revLookup[b64.charCodeAt(i)] << 10) | + (revLookup[b64.charCodeAt(i + 1)] << 4) | + (revLookup[b64.charCodeAt(i + 2)] >> 2); + arr[curByte++] = (tmp >> 8) & 0xff; + arr[curByte++] = tmp & 0xff; + } + + return arr; + } + + function tripletToBase64(num) { + return ( + lookup[(num >> 18) & 0x3f] + + lookup[(num >> 12) & 0x3f] + + lookup[(num >> 6) & 0x3f] + + lookup[num & 0x3f] + ); + } + + function encodeChunk(uint8, start, end) { + let tmp; + const output = []; + for (let i = start; i < end; i += 3) { + tmp = ((uint8[i] << 16) & 0xff0000) + + ((uint8[i + 1] << 8) & 0xff00) + + (uint8[i + 2] & 0xff); + output.push(tripletToBase64(tmp)); + } + return output.join(""); + } + + function fromByteArray(uint8) { + let tmp; + const len = uint8.length; + const extraBytes = len % 3; // if we have 1 byte left, pad 2 bytes + const parts = []; + const maxChunkLength = 16383; // must be multiple of 3 + + // go through the array every three bytes, we'll deal with trailing stuff later + for (let i = 0, len2 = len - extraBytes; i < len2; i += maxChunkLength) { + parts.push( + encodeChunk( + uint8, + i, + i + maxChunkLength > len2 ? len2 : i + maxChunkLength, + ), + ); + } + + // pad the end with zeros, but make sure to not forget the extra bytes + if (extraBytes === 1) { + tmp = uint8[len - 1]; + parts.push(lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f] + "=="); + } else if (extraBytes === 2) { + tmp = (uint8[len - 2] << 8) + uint8[len - 1]; + parts.push( + lookup[tmp >> 10] + + lookup[(tmp >> 4) & 0x3f] + + lookup[(tmp << 2) & 0x3f] + + "=", + ); + } + + return parts.join(""); + } + + const base64 = { + byteLength, + toByteArray, + fromByteArray, + }; + window.TextEncoder = TextEncoder; window.TextDecoder = TextDecoder; window.atob = atob; diff --git a/op_crates/web/Cargo.toml b/op_crates/web/Cargo.toml new file mode 100644 index 000000000..ae5677561 --- /dev/null +++ b/op_crates/web/Cargo.toml @@ -0,0 +1,20 @@ +# Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +[package] +name = "deno_web" +version = "0.1.0" +edition = "2018" +description = "Collection of Web APIs" +authors = ["the Deno authors"] +license = "MIT" +readme = "README.md" +repository = "https://github.com/denoland/deno" + +[lib] +path = "lib.rs" + +[dependencies] +deno_core = { version = "0.51.0", path = "../../core" } + +[dev-dependencies] +futures = "0.3.5" diff --git a/op_crates/web/event_target_test.js b/op_crates/web/event_target_test.js new file mode 100644 index 000000000..5e86c6efb --- /dev/null +++ b/op_crates/web/event_target_test.js @@ -0,0 +1,244 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +function assert(cond) { + if (!cond) { + throw Error("assert"); + } +} + +function addEventListenerTest() { + const document = new EventTarget(); + + assert(document.addEventListener("x", null, false) === undefined); + assert(document.addEventListener("x", null, true) === undefined); + assert(document.addEventListener("x", null) === undefined); +} + +function constructedEventTargetCanBeUsedAsExpected() { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = (e) => { + assert(e === event); + ++callCount; + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assert(callCount === 1); + + target.dispatchEvent(event); + assert(callCount === 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assert(callCount === 2); +} + +function anEventTargetCanBeSubclassed() { + class NicerEventTarget extends EventTarget { + on( + type, + callback, + options, + ) { + this.addEventListener(type, callback, options); + } + + off( + type, + callback, + options, + ) { + this.removeEventListener(type, callback, options); + } + } + + const target = new NicerEventTarget(); + new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = () => { + ++callCount; + }; + + target.on("foo", listener); + assert(callCount === 0); + + target.off("foo", listener); + assert(callCount === 0); +} + +function removingNullEventListenerShouldSucceed() { + const document = new EventTarget(); + assert(document.removeEventListener("x", null, false) === undefined); + assert(document.removeEventListener("x", null, true) === undefined); + assert(document.removeEventListener("x", null) === undefined); +} + +function constructedEventTargetUseObjectPrototype() { + const target = new EventTarget(); + const event = new Event("toString", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = (e) => { + assert(e === event); + ++callCount; + }; + + target.addEventListener("toString", listener); + + target.dispatchEvent(event); + assert(callCount === 1); + + target.dispatchEvent(event); + assert(callCount === 2); + + target.removeEventListener("toString", listener); + target.dispatchEvent(event); + assert(callCount === 2); +} + +function toStringShouldBeWebCompatible() { + const target = new EventTarget(); + assert(target.toString() === "[object EventTarget]"); +} + +function dispatchEventShouldNotThrowError() { + let hasThrown = false; + + try { + const target = new EventTarget(); + const event = new Event("hasOwnProperty", { + bubbles: true, + cancelable: false, + }); + const listener = () => {}; + target.addEventListener("hasOwnProperty", listener); + target.dispatchEvent(event); + } catch { + hasThrown = true; + } + + assert(hasThrown === false); +} + +function eventTargetThisShouldDefaultToWindow() { + const { + addEventListener, + dispatchEvent, + removeEventListener, + } = EventTarget.prototype; + let n = 1; + const event = new Event("hello"); + const listener = () => { + n = 2; + }; + + addEventListener("hello", listener); + globalThis.dispatchEvent(event); + assert(n === 2); + n = 1; + removeEventListener("hello", listener); + globalThis.dispatchEvent(event); + assert(n === 1); + + globalThis.addEventListener("hello", listener); + dispatchEvent(event); + assert(n === 2); + n = 1; + globalThis.removeEventListener("hello", listener); + dispatchEvent(event); + assert(n === 1); +} + +function eventTargetShouldAcceptEventListenerObject() { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = { + handleEvent(e) { + assert(e === event); + ++callCount; + }, + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assert(callCount === 1); + + target.dispatchEvent(event); + assert(callCount === 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assert(callCount === 2); +} + +function eventTargetShouldAcceptAsyncFunction() { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = (e) => { + assert(e === event); + ++callCount; + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assert(callCount === 1); + + target.dispatchEvent(event); + assert(callCount === 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assert(callCount === 2); +} + +function eventTargetShouldAcceptAsyncFunctionForEventListenerObject() { + const target = new EventTarget(); + const event = new Event("foo", { bubbles: true, cancelable: false }); + let callCount = 0; + + const listener = { + handleEvent(e) { + assert(e === event); + ++callCount; + }, + }; + + target.addEventListener("foo", listener); + + target.dispatchEvent(event); + assert(callCount === 1); + + target.dispatchEvent(event); + assert(callCount === 2); + + target.removeEventListener("foo", listener); + target.dispatchEvent(event); + assert(callCount === 2); +} + +function main() { + globalThis.__bootstrap.eventTarget.setEventTargetData(globalThis); + addEventListenerTest(); + constructedEventTargetCanBeUsedAsExpected(); + anEventTargetCanBeSubclassed(); + removingNullEventListenerShouldSucceed(); + constructedEventTargetUseObjectPrototype(); + toStringShouldBeWebCompatible(); + dispatchEventShouldNotThrowError(); + eventTargetThisShouldDefaultToWindow(); + eventTargetShouldAcceptEventListenerObject(); + eventTargetShouldAcceptAsyncFunction(); + eventTargetShouldAcceptAsyncFunctionForEventListenerObject(); +} + +main(); diff --git a/op_crates/web/event_test.js b/op_crates/web/event_test.js new file mode 100644 index 000000000..4f9f94fa9 --- /dev/null +++ b/op_crates/web/event_test.js @@ -0,0 +1,111 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +function assert(cond) { + if (!cond) { + throw Error("assert"); + } +} + +function eventInitializedWithType() { + const type = "click"; + const event = new Event(type); + + assert(event.isTrusted === false); + assert(event.target === null); + assert(event.currentTarget === null); + assert(event.type === "click"); + assert(event.bubbles === false); + assert(event.cancelable === false); +} + +function eventInitializedWithTypeAndDict() { + const init = "submit"; + const eventInit = { bubbles: true, cancelable: true }; + const event = new Event(init, eventInit); + + assert(event.isTrusted === false); + assert(event.target === null); + assert(event.currentTarget === null); + assert(event.type === "submit"); + assert(event.bubbles === true); + assert(event.cancelable === true); +} + +function eventComposedPathSuccess() { + const type = "click"; + const event = new Event(type); + const composedPath = event.composedPath(); + + assert(composedPath.length === 0); +} + +function eventStopPropagationSuccess() { + const type = "click"; + const event = new Event(type); + + assert(event.cancelBubble === false); + event.stopPropagation(); + assert(event.cancelBubble === true); +} + +function eventStopImmediatePropagationSuccess() { + const type = "click"; + const event = new Event(type); + + assert(event.cancelBubble === false); + event.stopImmediatePropagation(); + assert(event.cancelBubble === true); +} + +function eventPreventDefaultSuccess() { + const type = "click"; + const event = new Event(type); + + assert(event.defaultPrevented === false); + event.preventDefault(); + assert(event.defaultPrevented === false); + + const eventInit = { bubbles: true, cancelable: true }; + const cancelableEvent = new Event(type, eventInit); + assert(cancelableEvent.defaultPrevented === false); + cancelableEvent.preventDefault(); + assert(cancelableEvent.defaultPrevented === true); +} + +function eventInitializedWithNonStringType() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const type = undefined; + const event = new Event(type); + + assert(event.isTrusted === false); + assert(event.target === null); + assert(event.currentTarget === null); + assert(event.type === "undefined"); + assert(event.bubbles === false); + assert(event.cancelable === false); +} + +// ref https://github.com/web-platform-tests/wpt/blob/master/dom/events/Event-isTrusted.any.js +function eventIsTrusted() { + const desc1 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted"); + assert(desc1); + assert(typeof desc1.get === "function"); + + const desc2 = Object.getOwnPropertyDescriptor(new Event("x"), "isTrusted"); + assert(desc2); + assert(typeof desc2.get === "function"); + + assert(desc1.get === desc2.get); +} + +function main() { + eventInitializedWithType(); + eventInitializedWithTypeAndDict(); + eventComposedPathSuccess(); + eventStopPropagationSuccess(); + eventStopImmediatePropagationSuccess(); + eventPreventDefaultSuccess(); + eventInitializedWithNonStringType(); + eventIsTrusted(); +} + +main(); diff --git a/op_crates/web/lib.deno_web.d.ts b/op_crates/web/lib.deno_web.d.ts new file mode 100644 index 000000000..b402529e2 --- /dev/null +++ b/op_crates/web/lib.deno_web.d.ts @@ -0,0 +1,187 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +/// <reference no-default-lib="true" /> +/// <reference lib="esnext" /> + +declare class DOMException extends Error { + constructor(message?: string, name?: string); + readonly name: string; + readonly message: string; +} + +interface EventInit { + bubbles?: boolean; + cancelable?: boolean; + composed?: boolean; +} + +/** An event which takes place in the DOM. */ +declare class Event { + constructor(type: string, eventInitDict?: EventInit); + /** Returns true or false depending on how event was initialized. True if + * event goes through its target's ancestors in reverse tree order, and + * false otherwise. */ + readonly bubbles: boolean; + cancelBubble: boolean; + /** Returns true or false depending on how event was initialized. Its return + * value does not always carry meaning, but true can indicate that part of the + * operation during which event was dispatched, can be canceled by invoking + * the preventDefault() method. */ + readonly cancelable: boolean; + /** Returns true or false depending on how event was initialized. True if + * event invokes listeners past a ShadowRoot node that is the root of its + * target, and false otherwise. */ + readonly composed: boolean; + /** Returns the object whose event listener's callback is currently being + * invoked. */ + readonly currentTarget: EventTarget | null; + /** Returns true if preventDefault() was invoked successfully to indicate + * cancellation, and false otherwise. */ + readonly defaultPrevented: boolean; + /** Returns the event's phase, which is one of NONE, CAPTURING_PHASE, + * AT_TARGET, and BUBBLING_PHASE. */ + readonly eventPhase: number; + /** Returns true if event was dispatched by the user agent, and false + * otherwise. */ + readonly isTrusted: boolean; + /** Returns the object to which event is dispatched (its target). */ + readonly target: EventTarget | null; + /** Returns the event's timestamp as the number of milliseconds measured + * relative to the time origin. */ + readonly timeStamp: number; + /** Returns the type of event, e.g. "click", "hashchange", or "submit". */ + readonly type: string; + /** Returns the invocation target objects of event's path (objects on which + * listeners will be invoked), except for any nodes in shadow trees of which + * the shadow root's mode is "closed" that are not reachable from event's + * currentTarget. */ + composedPath(): EventTarget[]; + /** If invoked when the cancelable attribute value is true, and while + * executing a listener for the event with passive set to false, signals to + * the operation that caused event to be dispatched that it needs to be + * canceled. */ + preventDefault(): void; + /** Invoking this method prevents event from reaching any registered event + * listeners after the current one finishes running and, when dispatched in a + * tree, also prevents event from reaching any other objects. */ + stopImmediatePropagation(): void; + /** When dispatched in a tree, invoking this method prevents event from + * reaching any objects other than the current object. */ + stopPropagation(): void; + readonly AT_TARGET: number; + readonly BUBBLING_PHASE: number; + readonly CAPTURING_PHASE: number; + readonly NONE: number; + static readonly AT_TARGET: number; + static readonly BUBBLING_PHASE: number; + static readonly CAPTURING_PHASE: number; + static readonly NONE: number; +} + +/** + * EventTarget is a DOM interface implemented by objects that can receive events + * and may have listeners for them. + */ +declare class EventTarget { + /** Appends an event listener for events whose type attribute value is type. + * The callback argument sets the callback that will be invoked when the event + * is dispatched. + * + * The options argument sets listener-specific options. For compatibility this + * can be a boolean, in which case the method behaves exactly as if the value + * was specified as options's capture. + * + * When set to true, options's capture prevents callback from being invoked + * when the event's eventPhase attribute value is BUBBLING_PHASE. When false + * (or not present), callback will not be invoked when event's eventPhase + * attribute value is CAPTURING_PHASE. Either way, callback will be invoked if + * event's eventPhase attribute value is AT_TARGET. + * + * When set to true, options's passive indicates that the callback will not + * cancel the event by invoking preventDefault(). This is used to enable + * performance optimizations described in ยง 2.8 Observing event listeners. + * + * When set to true, options's once indicates that the callback will only be + * invoked once after which the event listener will be removed. + * + * The event listener is appended to target's event listener list and is not + * appended if it has the same type, callback, and capture. */ + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject | null, + options?: boolean | AddEventListenerOptions, + ): void; + /** Dispatches a synthetic event event to target and returns true if either + * event's cancelable attribute value is false or its preventDefault() method + * was not invoked, and false otherwise. */ + dispatchEvent(event: Event): boolean; + /** Removes the event listener in target's event listener list with the same + * type, callback, and options. */ + removeEventListener( + type: string, + callback: EventListenerOrEventListenerObject | null, + options?: EventListenerOptions | boolean, + ): void; + [Symbol.toStringTag]: string; +} + +interface EventListener { + (evt: Event): void | Promise<void>; +} + +interface EventListenerObject { + handleEvent(evt: Event): void | Promise<void>; +} + +declare type EventListenerOrEventListenerObject = + | EventListener + | EventListenerObject; + +interface AddEventListenerOptions extends EventListenerOptions { + once?: boolean; + passive?: boolean; +} + +interface EventListenerOptions { + capture?: boolean; +} + +/** Decodes a string of data which has been encoded using base-64 encoding. + * + * console.log(atob("aGVsbG8gd29ybGQ=")); // outputs 'hello world' + */ +declare function atob(s: string): string; + +/** Creates a base-64 ASCII encoded string from the input string. + * + * console.log(btoa("hello world")); // outputs "aGVsbG8gd29ybGQ=" + */ +declare function btoa(s: string): string; + +declare class TextDecoder { + /** Returns encoding's name, lowercased. */ + readonly encoding: string; + /** Returns `true` if error mode is "fatal", and `false` otherwise. */ + readonly fatal: boolean; + /** Returns `true` if ignore BOM flag is set, and `false` otherwise. */ + readonly ignoreBOM = false; + constructor( + label?: string, + options?: { fatal?: boolean; ignoreBOM?: boolean }, + ); + /** Returns the result of running encoding's decoder. */ + decode(input?: BufferSource, options?: { stream?: false }): string; + readonly [Symbol.toStringTag]: string; +} + +declare class TextEncoder { + /** Returns "utf-8". */ + readonly encoding = "utf-8"; + /** Returns the result of running UTF-8's encoder. */ + encode(input?: string): Uint8Array; + encodeInto( + input: string, + dest: Uint8Array, + ): { read: number; written: number }; + readonly [Symbol.toStringTag]: string; +} diff --git a/op_crates/web/lib.rs b/op_crates/web/lib.rs new file mode 100644 index 000000000..4cfe8d090 --- /dev/null +++ b/op_crates/web/lib.rs @@ -0,0 +1,102 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +use deno_core::crate_modules; +use std::path::PathBuf; + +crate_modules!(); + +pub struct WebScripts { + pub declaration: String, + pub dom_exception: String, + pub event: String, + pub text_encoding: String, +} + +fn get_str_path(file_name: &str) -> String { + PathBuf::from(DENO_CRATE_PATH) + .join(file_name) + .to_string_lossy() + .to_string() +} + +pub fn get_scripts() -> WebScripts { + WebScripts { + declaration: get_str_path("lib.deno_web.d.ts"), + dom_exception: get_str_path("00_dom_exception.js"), + event: get_str_path("01_event.js"), + text_encoding: get_str_path("08_text_encoding.js"), + } +} + +#[cfg(test)] +mod tests { + use deno_core::js_check; + use deno_core::CoreIsolate; + use deno_core::StartupData; + use futures::future::lazy; + use futures::future::FutureExt; + use futures::task::Context; + use futures::task::Poll; + + fn run_in_task<F>(f: F) + where + F: FnOnce(&mut Context) + Send + 'static, + { + futures::executor::block_on(lazy(move |cx| f(cx))); + } + + fn setup() -> CoreIsolate { + let mut isolate = CoreIsolate::new(StartupData::None, false); + js_check( + isolate + .execute("00_dom_exception.js", include_str!("00_dom_exception.js")), + ); + js_check(isolate.execute("01_event.js", include_str!("01_event.js"))); + js_check( + isolate + .execute("08_text_encoding.js", include_str!("08_text_encoding.js")), + ); + isolate + } + + #[test] + fn test_event() { + run_in_task(|mut cx| { + let mut isolate = setup(); + js_check(isolate.execute("event_test.js", include_str!("event_test.js"))); + if let Poll::Ready(Err(_)) = isolate.poll_unpin(&mut cx) { + unreachable!(); + } + }); + } + + #[test] + fn test_event_target() { + run_in_task(|mut cx| { + let mut isolate = setup(); + js_check( + isolate.execute( + "event_target_test.js", + include_str!("event_target_test.js"), + ), + ); + if let Poll::Ready(Err(_)) = isolate.poll_unpin(&mut cx) { + unreachable!(); + } + }); + } + + #[test] + fn test_text_encoding() { + run_in_task(|mut cx| { + let mut isolate = setup(); + js_check(isolate.execute( + "text_encoding_test.js", + include_str!("text_encoding_test.js"), + )); + if let Poll::Ready(Err(_)) = isolate.poll_unpin(&mut cx) { + unreachable!(); + } + }); + } +} diff --git a/op_crates/web/text_encoding_test.js b/op_crates/web/text_encoding_test.js new file mode 100644 index 000000000..f741fe409 --- /dev/null +++ b/op_crates/web/text_encoding_test.js @@ -0,0 +1,243 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +function assert(cond) { + if (!cond) { + throw Error("assert"); + } +} + +function assertArrayEquals(a1, a2) { + if (a1.length !== a2.length) throw Error("assert"); + + for (const index in a1) { + if (a1[index] !== a2[index]) { + throw Error("assert"); + } + } +} + +function btoaSuccess() { + const text = "hello world"; + const encoded = btoa(text); + assert(encoded === "aGVsbG8gd29ybGQ="); +} + +function atobSuccess() { + const encoded = "aGVsbG8gd29ybGQ="; + const decoded = atob(encoded); + assert(decoded === "hello world"); +} + +function atobWithAsciiWhitespace() { + const encodedList = [ + " aGVsbG8gd29ybGQ=", + " aGVsbG8gd29ybGQ=", + "aGVsbG8gd29ybGQ= ", + "aGVsbG8gd29ybGQ=\n", + "aGVsbG\t8gd29ybGQ=", + `aGVsbG\t8g + d29ybGQ=`, + ]; + + for (const encoded of encodedList) { + const decoded = atob(encoded); + assert(decoded === "hello world"); + } +} + +function atobThrows() { + let threw = false; + try { + atob("aGVsbG8gd29ybGQ=="); + } catch (e) { + threw = true; + } + assert(threw); +} + +function atobThrows2() { + let threw = false; + try { + atob("aGVsbG8gd29ybGQ==="); + } catch (e) { + threw = true; + } + assert(threw); +} + +function btoaFailed() { + let threw = false; + const text = "ไฝ ๅฅฝ"; + try { + btoa(text); + } catch (e) { + assert(e instanceof TypeError); + threw = true; + } + assert(threw); +} + +function textDecoder2() { + // deno-fmt-ignore + const fixture = new Uint8Array([ + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); + const decoder = new TextDecoder(); + assert(decoder.decode(fixture) === "๐ฝ๐ฎ๐๐ฝ"); +} + +function textDecoderIgnoreBOM() { + // deno-fmt-ignore + const fixture = new Uint8Array([ + 0xef, 0xbb, 0xbf, + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); + const decoder = new TextDecoder("utf-8", { ignoreBOM: true }); + assert(decoder.decode(fixture) === "๐ฝ๐ฎ๐๐ฝ"); +} + +function textDecoderNotBOM() { + // deno-fmt-ignore + const fixture = new Uint8Array([ + 0xef, 0xbb, 0x89, + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); + const decoder = new TextDecoder("utf-8", { ignoreBOM: true }); + assert(decoder.decode(fixture) === "๏ป๐ฝ๐ฎ๐๐ฝ"); +} + +function textDecoderASCII() { + const fixture = new Uint8Array([0x89, 0x95, 0x9f, 0xbf]); + const decoder = new TextDecoder("ascii"); + assert(decoder.decode(fixture) === "โฐโขลธยฟ"); +} + +function textDecoderErrorEncoding() { + let didThrow = false; + try { + new TextDecoder("foo"); + } catch (e) { + didThrow = true; + assert(e.message === "The encoding label provided ('foo') is invalid."); + } + assert(didThrow); +} + +function textEncoder() { + const fixture = "๐ฝ๐ฎ๐๐ฝ"; + const encoder = new TextEncoder(); + // deno-fmt-ignore + assertArrayEquals(Array.from(encoder.encode(fixture)), [ + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]); +} + +function textEncodeInto() { + const fixture = "text"; + const encoder = new TextEncoder(); + const bytes = new Uint8Array(5); + const result = encoder.encodeInto(fixture, bytes); + assert(result.read === 4); + assert(result.written === 4); + // deno-fmt-ignore + assertArrayEquals(Array.from(bytes), [ + 0x74, 0x65, 0x78, 0x74, 0x00, + ]); +} + +function textEncodeInto2() { + const fixture = "๐ฝ๐ฎ๐๐ฝ"; + const encoder = new TextEncoder(); + const bytes = new Uint8Array(17); + const result = encoder.encodeInto(fixture, bytes); + assert(result.read === 8); + assert(result.written === 16); + // deno-fmt-ignore + assertArrayEquals(Array.from(bytes), [ + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd, 0x00, + ]); +} + +function textEncodeInto3() { + const fixture = "๐ฝ๐ฎ๐๐ฝ"; + const encoder = new TextEncoder(); + const bytes = new Uint8Array(5); + const result = encoder.encodeInto(fixture, bytes); + assert(result.read === 2); + assert(result.written === 4); + // deno-fmt-ignore + assertArrayEquals(Array.from(bytes), [ + 0xf0, 0x9d, 0x93, 0xbd, 0x00, + ]); +} + +function textDecoderSharedUint8Array() { + const ab = new SharedArrayBuffer(6); + const dataView = new DataView(ab); + const charCodeA = "A".charCodeAt(0); + for (let i = 0; i < ab.byteLength; i++) { + dataView.setUint8(i, charCodeA + i); + } + const ui8 = new Uint8Array(ab); + const decoder = new TextDecoder(); + const actual = decoder.decode(ui8); + assert(actual === "ABCDEF"); +} + +function textDecoderSharedInt32Array() { + const ab = new SharedArrayBuffer(8); + const dataView = new DataView(ab); + const charCodeA = "A".charCodeAt(0); + for (let i = 0; i < ab.byteLength; i++) { + dataView.setUint8(i, charCodeA + i); + } + const i32 = new Int32Array(ab); + const decoder = new TextDecoder(); + const actual = decoder.decode(i32); + assert(actual === "ABCDEFGH"); +} + +function toStringShouldBeWebCompatibility() { + const encoder = new TextEncoder(); + assert(encoder.toString() === "[object TextEncoder]"); + + const decoder = new TextDecoder(); + assert(decoder.toString() === "[object TextDecoder]"); +} + +function main() { + btoaSuccess(); + atobSuccess(); + atobWithAsciiWhitespace(); + atobThrows(); + atobThrows2(); + btoaFailed(); + textDecoder2(); + textDecoderIgnoreBOM(); + textDecoderNotBOM(); + textDecoderASCII(); + textDecoderErrorEncoding(); + textEncoder(); + textEncodeInto(); + textEncodeInto2(); + textEncodeInto3(); + textDecoderSharedUint8Array(); + textDecoderSharedInt32Array(); + toStringShouldBeWebCompatibility(); +} + +main(); |