summaryrefslogtreecommitdiff
path: root/op_crates/web
diff options
context:
space:
mode:
Diffstat (limited to 'op_crates/web')
-rw-r--r--op_crates/web/00_dom_exception.js15
-rw-r--r--op_crates/web/01_event.js1048
-rw-r--r--op_crates/web/08_text_encoding.js838
-rw-r--r--op_crates/web/Cargo.toml20
-rw-r--r--op_crates/web/event_target_test.js244
-rw-r--r--op_crates/web/event_test.js111
-rw-r--r--op_crates/web/lib.deno_web.d.ts187
-rw-r--r--op_crates/web/lib.rs102
-rw-r--r--op_crates/web/text_encoding_test.js243
9 files changed, 2808 insertions, 0 deletions
diff --git a/op_crates/web/00_dom_exception.js b/op_crates/web/00_dom_exception.js
new file mode 100644
index 000000000..6d72779b0
--- /dev/null
+++ b/op_crates/web/00_dom_exception.js
@@ -0,0 +1,15 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+class DOMException extends Error {
+ #name = "";
+
+ constructor(message = "", name = "Error") {
+ super(message);
+ this.#name = name;
+ }
+
+ get name() {
+ return this.#name;
+ }
+}
diff --git a/op_crates/web/01_event.js b/op_crates/web/01_event.js
new file mode 100644
index 000000000..48899e6fd
--- /dev/null
+++ b/op_crates/web/01_event.js
@@ -0,0 +1,1048 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+// This module follows most of the WHATWG Living Standard for the DOM logic.
+// Many parts of the DOM are not implemented in Deno, but the logic for those
+// parts still exists. This means you will observe a lot of strange structures
+// and impossible logic branches based on what Deno currently supports.
+
+((window) => {
+ const eventData = new WeakMap();
+
+ function requiredArguments(
+ name,
+ length,
+ required,
+ ) {
+ if (length < required) {
+ const errMsg = `${name} requires at least ${required} argument${
+ required === 1 ? "" : "s"
+ }, but only ${length} present`;
+ throw new TypeError(errMsg);
+ }
+ }
+
+ // accessors for non runtime visible data
+
+ function getDispatched(event) {
+ return Boolean(eventData.get(event)?.dispatched);
+ }
+
+ function getPath(event) {
+ return eventData.get(event)?.path ?? [];
+ }
+
+ function getStopImmediatePropagation(event) {
+ return Boolean(eventData.get(event)?.stopImmediatePropagation);
+ }
+
+ function setCurrentTarget(
+ event,
+ value,
+ ) {
+ event.currentTarget = value;
+ }
+
+ function setDispatched(event, value) {
+ const data = eventData.get(event);
+ if (data) {
+ data.dispatched = value;
+ }
+ }
+
+ function setEventPhase(event, value) {
+ event.eventPhase = value;
+ }
+
+ function setInPassiveListener(event, value) {
+ const data = eventData.get(event);
+ if (data) {
+ data.inPassiveListener = value;
+ }
+ }
+
+ function setPath(event, value) {
+ const data = eventData.get(event);
+ if (data) {
+ data.path = value;
+ }
+ }
+
+ function setRelatedTarget(
+ event,
+ value,
+ ) {
+ if ("relatedTarget" in event) {
+ event.relatedTarget = value;
+ }
+ }
+
+ function setTarget(event, value) {
+ event.target = value;
+ }
+
+ function setStopImmediatePropagation(
+ event,
+ value,
+ ) {
+ const data = eventData.get(event);
+ if (data) {
+ data.stopImmediatePropagation = value;
+ }
+ }
+
+ // Type guards that widen the event type
+
+ function hasRelatedTarget(
+ event,
+ ) {
+ return "relatedTarget" in event;
+ }
+
+ function isTrusted() {
+ return eventData.get(this).isTrusted;
+ }
+
+ class Event {
+ #canceledFlag = false;
+ #stopPropagationFlag = false;
+ #attributes = {};
+
+ constructor(type, eventInitDict = {}) {
+ requiredArguments("Event", arguments.length, 1);
+ type = String(type);
+ this.#attributes = {
+ type,
+ bubbles: eventInitDict.bubbles ?? false,
+ cancelable: eventInitDict.cancelable ?? false,
+ composed: eventInitDict.composed ?? false,
+ currentTarget: null,
+ eventPhase: Event.NONE,
+ target: null,
+ timeStamp: Date.now(),
+ };
+ eventData.set(this, {
+ dispatched: false,
+ inPassiveListener: false,
+ isTrusted: false,
+ path: [],
+ stopImmediatePropagation: false,
+ });
+ Reflect.defineProperty(this, "isTrusted", {
+ enumerable: true,
+ get: isTrusted,
+ });
+ }
+
+ get bubbles() {
+ return this.#attributes.bubbles;
+ }
+
+ get cancelBubble() {
+ return this.#stopPropagationFlag;
+ }
+
+ set cancelBubble(value) {
+ this.#stopPropagationFlag = value;
+ }
+
+ get cancelable() {
+ return this.#attributes.cancelable;
+ }
+
+ get composed() {
+ return this.#attributes.composed;
+ }
+
+ get currentTarget() {
+ return this.#attributes.currentTarget;
+ }
+
+ set currentTarget(value) {
+ this.#attributes = {
+ type: this.type,
+ bubbles: this.bubbles,
+ cancelable: this.cancelable,
+ composed: this.composed,
+ currentTarget: value,
+ eventPhase: this.eventPhase,
+ target: this.target,
+ timeStamp: this.timeStamp,
+ };
+ }
+
+ get defaultPrevented() {
+ return this.#canceledFlag;
+ }
+
+ get eventPhase() {
+ return this.#attributes.eventPhase;
+ }
+
+ set eventPhase(value) {
+ this.#attributes = {
+ type: this.type,
+ bubbles: this.bubbles,
+ cancelable: this.cancelable,
+ composed: this.composed,
+ currentTarget: this.currentTarget,
+ eventPhase: value,
+ target: this.target,
+ timeStamp: this.timeStamp,
+ };
+ }
+
+ get initialized() {
+ return true;
+ }
+
+ get target() {
+ return this.#attributes.target;
+ }
+
+ set target(value) {
+ this.#attributes = {
+ type: this.type,
+ bubbles: this.bubbles,
+ cancelable: this.cancelable,
+ composed: this.composed,
+ currentTarget: this.currentTarget,
+ eventPhase: this.eventPhase,
+ target: value,
+ timeStamp: this.timeStamp,
+ };
+ }
+
+ get timeStamp() {
+ return this.#attributes.timeStamp;
+ }
+
+ get type() {
+ return this.#attributes.type;
+ }
+
+ composedPath() {
+ const path = eventData.get(this).path;
+ if (path.length === 0) {
+ return [];
+ }
+
+ if (!this.currentTarget) {
+ throw new Error("assertion error");
+ }
+ const composedPath = [
+ {
+ item: this.currentTarget,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: [],
+ },
+ ];
+
+ let currentTargetIndex = 0;
+ let currentTargetHiddenSubtreeLevel = 0;
+
+ for (let index = path.length - 1; index >= 0; index--) {
+ const { item, rootOfClosedTree, slotInClosedTree } = path[index];
+
+ if (rootOfClosedTree) {
+ currentTargetHiddenSubtreeLevel++;
+ }
+
+ if (item === this.currentTarget) {
+ currentTargetIndex = index;
+ break;
+ }
+
+ if (slotInClosedTree) {
+ currentTargetHiddenSubtreeLevel--;
+ }
+ }
+
+ let currentHiddenLevel = currentTargetHiddenSubtreeLevel;
+ let maxHiddenLevel = currentTargetHiddenSubtreeLevel;
+
+ for (let i = currentTargetIndex - 1; i >= 0; i--) {
+ const { item, rootOfClosedTree, slotInClosedTree } = path[i];
+
+ if (rootOfClosedTree) {
+ currentHiddenLevel++;
+ }
+
+ if (currentHiddenLevel <= maxHiddenLevel) {
+ composedPath.unshift({
+ item,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: [],
+ });
+ }
+
+ if (slotInClosedTree) {
+ currentHiddenLevel--;
+
+ if (currentHiddenLevel < maxHiddenLevel) {
+ maxHiddenLevel = currentHiddenLevel;
+ }
+ }
+ }
+
+ currentHiddenLevel = currentTargetHiddenSubtreeLevel;
+ maxHiddenLevel = currentTargetHiddenSubtreeLevel;
+
+ for (let index = currentTargetIndex + 1; index < path.length; index++) {
+ const { item, rootOfClosedTree, slotInClosedTree } = path[index];
+
+ if (slotInClosedTree) {
+ currentHiddenLevel++;
+ }
+
+ if (currentHiddenLevel <= maxHiddenLevel) {
+ composedPath.push({
+ item,
+ itemInShadowTree: false,
+ relatedTarget: null,
+ rootOfClosedTree: false,
+ slotInClosedTree: false,
+ target: null,
+ touchTargetList: [],
+ });
+ }
+
+ if (rootOfClosedTree) {
+ currentHiddenLevel--;
+
+ if (currentHiddenLevel < maxHiddenLevel) {
+ maxHiddenLevel = currentHiddenLevel;
+ }
+ }
+ }
+ return composedPath.map((p) => p.item);
+ }
+
+ preventDefault() {
+ if (this.cancelable && !eventData.get(this).inPassiveListener) {
+ this.#canceledFlag = true;
+ }
+ }
+
+ stopPropagation() {
+ this.#stopPropagationFlag = true;
+ }
+
+ stopImmediatePropagation() {
+ this.#stopPropagationFlag = true;
+ eventData.get(this).stopImmediatePropagation = true;
+ }
+
+ get NONE() {
+ return Event.NONE;
+ }
+
+ get CAPTURING_PHASE() {
+ return Event.CAPTURING_PHASE;
+ }
+
+ get AT_TARGET() {
+ return Event.AT_TARGET;
+ }
+
+ get BUBBLING_PHASE() {
+ return Event.BUBBLING_PHASE;
+ }
+
+ static get NONE() {
+ return 0;
+ }
+
+ static get CAPTURING_PHASE() {
+ return 1;
+ }
+
+ static get AT_TARGET() {
+ return 2;
+ }
+
+ static get BUBBLING_PHASE() {
+ return 3;
+ }
+ }
+
+ function defineEnumerableProps(
+ Ctor,
+ props,
+ ) {
+ for (const prop of props) {
+ Reflect.defineProperty(Ctor.prototype, prop, { enumerable: true });
+ }
+ }
+
+ defineEnumerableProps(Event, [
+ "bubbles",
+ "cancelable",
+ "composed",
+ "currentTarget",
+ "defaultPrevented",
+ "eventPhase",
+ "target",
+ "timeStamp",
+ "type",
+ ]);
+
+ // This is currently the only node type we are using, so instead of implementing
+ // the whole of the Node interface at the moment, this just gives us the one
+ // value to power the standards based logic
+ const DOCUMENT_FRAGMENT_NODE = 11;
+
+ // DOM Logic Helper functions and type guards
+
+ /** Get the parent node, for event targets that have a parent.
+ *
+ * Ref: https://dom.spec.whatwg.org/#get-the-parent */
+ function getParent(eventTarget) {
+ return isNode(eventTarget) ? eventTarget.parentNode : null;
+ }
+
+ function getRoot(eventTarget) {
+ return isNode(eventTarget)
+ ? eventTarget.getRootNode({ composed: true })
+ : null;
+ }
+
+ function isNode(
+ eventTarget,
+ ) {
+ return Boolean(eventTarget && "nodeType" in eventTarget);
+ }
+
+ // https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor
+ function isShadowInclusiveAncestor(
+ ancestor,
+ node,
+ ) {
+ while (isNode(node)) {
+ if (node === ancestor) {
+ return true;
+ }
+
+ if (isShadowRoot(node)) {
+ node = node && getHost(node);
+ } else {
+ node = getParent(node);
+ }
+ }
+
+ return false;
+ }
+
+ function isShadowRoot(nodeImpl) {
+ return Boolean(
+ nodeImpl &&
+ isNode(nodeImpl) &&
+ nodeImpl.nodeType === DOCUMENT_FRAGMENT_NODE &&
+ getHost(nodeImpl) != null,
+ );
+ }
+
+ function isSlotable(
+ nodeImpl,
+ ) {
+ return Boolean(isNode(nodeImpl) && "assignedSlot" in nodeImpl);
+ }
+
+ // DOM Logic functions
+
+ /** Append a path item to an event's path.
+ *
+ * Ref: https://dom.spec.whatwg.org/#concept-event-path-append
+ */
+ function appendToEventPath(
+ eventImpl,
+ target,
+ targetOverride,
+ relatedTarget,
+ touchTargets,
+ slotInClosedTree,
+ ) {
+ const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target));
+ const rootOfClosedTree = isShadowRoot(target) &&
+ getMode(target) === "closed";
+
+ getPath(eventImpl).push({
+ item: target,
+ itemInShadowTree,
+ target: targetOverride,
+ relatedTarget,
+ touchTargetList: touchTargets,
+ rootOfClosedTree,
+ slotInClosedTree,
+ });
+ }
+
+ function dispatch(
+ targetImpl,
+ eventImpl,
+ targetOverride,
+ ) {
+ let clearTargets = false;
+ let activationTarget = null;
+
+ setDispatched(eventImpl, true);
+
+ targetOverride = targetOverride ?? targetImpl;
+ const eventRelatedTarget = hasRelatedTarget(eventImpl)
+ ? eventImpl.relatedTarget
+ : null;
+ let relatedTarget = retarget(eventRelatedTarget, targetImpl);
+
+ if (targetImpl !== relatedTarget || targetImpl === eventRelatedTarget) {
+ const touchTargets = [];
+
+ appendToEventPath(
+ eventImpl,
+ targetImpl,
+ targetOverride,
+ relatedTarget,
+ touchTargets,
+ false,
+ );
+
+ const isActivationEvent = eventImpl.type === "click";
+
+ if (isActivationEvent && getHasActivationBehavior(targetImpl)) {
+ activationTarget = targetImpl;
+ }
+
+ let slotInClosedTree = false;
+ let slotable = isSlotable(targetImpl) && getAssignedSlot(targetImpl)
+ ? targetImpl
+ : null;
+ let parent = getParent(targetImpl);
+
+ // Populate event path
+ // https://dom.spec.whatwg.org/#event-path
+ while (parent !== null) {
+ if (slotable !== null) {
+ slotable = null;
+
+ const parentRoot = getRoot(parent);
+ if (
+ isShadowRoot(parentRoot) &&
+ parentRoot &&
+ getMode(parentRoot) === "closed"
+ ) {
+ slotInClosedTree = true;
+ }
+ }
+
+ relatedTarget = retarget(eventRelatedTarget, parent);
+
+ if (
+ isNode(parent) &&
+ isShadowInclusiveAncestor(getRoot(targetImpl), parent)
+ ) {
+ appendToEventPath(
+ eventImpl,
+ parent,
+ null,
+ relatedTarget,
+ touchTargets,
+ slotInClosedTree,
+ );
+ } else if (parent === relatedTarget) {
+ parent = null;
+ } else {
+ targetImpl = parent;
+
+ if (
+ isActivationEvent &&
+ activationTarget === null &&
+ getHasActivationBehavior(targetImpl)
+ ) {
+ activationTarget = targetImpl;
+ }
+
+ appendToEventPath(
+ eventImpl,
+ parent,
+ targetImpl,
+ relatedTarget,
+ touchTargets,
+ slotInClosedTree,
+ );
+ }
+
+ if (parent !== null) {
+ parent = getParent(parent);
+ }
+
+ slotInClosedTree = false;
+ }
+
+ let clearTargetsTupleIndex = -1;
+ const path = getPath(eventImpl);
+ for (
+ let i = path.length - 1;
+ i >= 0 && clearTargetsTupleIndex === -1;
+ i--
+ ) {
+ if (path[i].target !== null) {
+ clearTargetsTupleIndex = i;
+ }
+ }
+ const clearTargetsTuple = path[clearTargetsTupleIndex];
+
+ clearTargets = (isNode(clearTargetsTuple.target) &&
+ isShadowRoot(getRoot(clearTargetsTuple.target))) ||
+ (isNode(clearTargetsTuple.relatedTarget) &&
+ isShadowRoot(getRoot(clearTargetsTuple.relatedTarget)));
+
+ setEventPhase(eventImpl, Event.CAPTURING_PHASE);
+
+ for (let i = path.length - 1; i >= 0; --i) {
+ const tuple = path[i];
+
+ if (tuple.target === null) {
+ invokeEventListeners(tuple, eventImpl);
+ }
+ }
+
+ for (let i = 0; i < path.length; i++) {
+ const tuple = path[i];
+
+ if (tuple.target !== null) {
+ setEventPhase(eventImpl, Event.AT_TARGET);
+ } else {
+ setEventPhase(eventImpl, Event.BUBBLING_PHASE);
+ }
+
+ if (
+ (eventImpl.eventPhase === Event.BUBBLING_PHASE &&
+ eventImpl.bubbles) ||
+ eventImpl.eventPhase === Event.AT_TARGET
+ ) {
+ invokeEventListeners(tuple, eventImpl);
+ }
+ }
+ }
+
+ setEventPhase(eventImpl, Event.NONE);
+ setCurrentTarget(eventImpl, null);
+ setPath(eventImpl, []);
+ setDispatched(eventImpl, false);
+ eventImpl.cancelBubble = false;
+ setStopImmediatePropagation(eventImpl, false);
+
+ if (clearTargets) {
+ setTarget(eventImpl, null);
+ setRelatedTarget(eventImpl, null);
+ }
+
+ // TODO: invoke activation targets if HTML nodes will be implemented
+ // if (activationTarget !== null) {
+ // if (!eventImpl.defaultPrevented) {
+ // activationTarget._activationBehavior();
+ // }
+ // }
+
+ return !eventImpl.defaultPrevented;
+ }
+
+ /** Inner invoking of the event listeners where the resolved listeners are
+ * called.
+ *
+ * Ref: https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke */
+ function innerInvokeEventListeners(
+ eventImpl,
+ targetListeners,
+ ) {
+ let found = false;
+
+ const { type } = eventImpl;
+
+ if (!targetListeners || !targetListeners[type]) {
+ return found;
+ }
+
+ // Copy event listeners before iterating since the list can be modified during the iteration.
+ const handlers = targetListeners[type].slice();
+
+ for (let i = 0; i < handlers.length; i++) {
+ const listener = handlers[i];
+
+ let capture, once, passive;
+ if (typeof listener.options === "boolean") {
+ capture = listener.options;
+ once = false;
+ passive = false;
+ } else {
+ capture = listener.options.capture;
+ once = listener.options.once;
+ passive = listener.options.passive;
+ }
+
+ // Check if the event listener has been removed since the listeners has been cloned.
+ if (!targetListeners[type].includes(listener)) {
+ continue;
+ }
+
+ found = true;
+
+ if (
+ (eventImpl.eventPhase === Event.CAPTURING_PHASE && !capture) ||
+ (eventImpl.eventPhase === Event.BUBBLING_PHASE && capture)
+ ) {
+ continue;
+ }
+
+ if (once) {
+ targetListeners[type].splice(
+ targetListeners[type].indexOf(listener),
+ 1,
+ );
+ }
+
+ if (passive) {
+ setInPassiveListener(eventImpl, true);
+ }
+
+ if (typeof listener.callback === "object") {
+ if (typeof listener.callback.handleEvent === "function") {
+ listener.callback.handleEvent(eventImpl);
+ }
+ } else {
+ listener.callback.call(eventImpl.currentTarget, eventImpl);
+ }
+
+ setInPassiveListener(eventImpl, false);
+
+ if (getStopImmediatePropagation(eventImpl)) {
+ return found;
+ }
+ }
+
+ return found;
+ }
+
+ /** Invokes the listeners on a given event path with the supplied event.
+ *
+ * Ref: https://dom.spec.whatwg.org/#concept-event-listener-invoke */
+ function invokeEventListeners(tuple, eventImpl) {
+ const path = getPath(eventImpl);
+ const tupleIndex = path.indexOf(tuple);
+ for (let i = tupleIndex; i >= 0; i--) {
+ const t = path[i];
+ if (t.target) {
+ setTarget(eventImpl, t.target);
+ break;
+ }
+ }
+
+ setRelatedTarget(eventImpl, tuple.relatedTarget);
+
+ if (eventImpl.cancelBubble) {
+ return;
+ }
+
+ setCurrentTarget(eventImpl, tuple.item);
+
+ innerInvokeEventListeners(eventImpl, getListeners(tuple.item));
+ }
+
+ function normalizeAddEventHandlerOptions(
+ options,
+ ) {
+ if (typeof options === "boolean" || typeof options === "undefined") {
+ return {
+ capture: Boolean(options),
+ once: false,
+ passive: false,
+ };
+ } else {
+ return options;
+ }
+ }
+
+ function normalizeEventHandlerOptions(
+ options,
+ ) {
+ if (typeof options === "boolean" || typeof options === "undefined") {
+ return {
+ capture: Boolean(options),
+ };
+ } else {
+ return options;
+ }
+ }
+
+ /** Retarget the target following the spec logic.
+ *
+ * Ref: https://dom.spec.whatwg.org/#retarget */
+ function retarget(a, b) {
+ while (true) {
+ if (!isNode(a)) {
+ return a;
+ }
+
+ const aRoot = a.getRootNode();
+
+ if (aRoot) {
+ if (
+ !isShadowRoot(aRoot) ||
+ (isNode(b) && isShadowInclusiveAncestor(aRoot, b))
+ ) {
+ return a;
+ }
+
+ a = getHost(aRoot);
+ }
+ }
+ }
+
+ // Accessors for non-public data
+
+ const eventTargetData = new WeakMap();
+
+ function setEventTargetData(value) {
+ eventTargetData.set(value, getDefaultTargetData());
+ }
+
+ function getAssignedSlot(target) {
+ return Boolean(eventTargetData.get(target)?.assignedSlot);
+ }
+
+ function getHasActivationBehavior(target) {
+ return Boolean(
+ eventTargetData.get(target)?.hasActivationBehavior,
+ );
+ }
+
+ function getHost(target) {
+ return eventTargetData.get(target)?.host ?? null;
+ }
+
+ function getListeners(target) {
+ return eventTargetData.get(target)?.listeners ?? {};
+ }
+
+ function getMode(target) {
+ return eventTargetData.get(target)?.mode ?? null;
+ }
+
+ function getDefaultTargetData() {
+ return {
+ assignedSlot: false,
+ hasActivationBehavior: false,
+ host: null,
+ listeners: Object.create(null),
+ mode: "",
+ };
+ }
+
+ class EventTarget {
+ constructor() {
+ eventTargetData.set(this, getDefaultTargetData());
+ }
+
+ addEventListener(
+ type,
+ callback,
+ options,
+ ) {
+ requiredArguments("EventTarget.addEventListener", arguments.length, 2);
+ if (callback === null) {
+ return;
+ }
+
+ options = normalizeAddEventHandlerOptions(options);
+ const { listeners } = eventTargetData.get(this ?? globalThis);
+
+ if (!(type in listeners)) {
+ listeners[type] = [];
+ }
+
+ for (const listener of listeners[type]) {
+ if (
+ ((typeof listener.options === "boolean" &&
+ listener.options === options.capture) ||
+ (typeof listener.options === "object" &&
+ listener.options.capture === options.capture)) &&
+ listener.callback === callback
+ ) {
+ return;
+ }
+ }
+
+ listeners[type].push({ callback, options });
+ }
+
+ removeEventListener(
+ type,
+ callback,
+ options,
+ ) {
+ requiredArguments("EventTarget.removeEventListener", arguments.length, 2);
+
+ const listeners = eventTargetData.get(this ?? globalThis).listeners;
+ if (callback !== null && type in listeners) {
+ listeners[type] = listeners[type].filter(
+ (listener) => listener.callback !== callback,
+ );
+ } else if (callback === null || !listeners[type]) {
+ return;
+ }
+
+ options = normalizeEventHandlerOptions(options);
+
+ for (let i = 0; i < listeners[type].length; ++i) {
+ const listener = listeners[type][i];
+ if (
+ ((typeof listener.options === "boolean" &&
+ listener.options === options.capture) ||
+ (typeof listener.options === "object" &&
+ listener.options.capture === options.capture)) &&
+ listener.callback === callback
+ ) {
+ listeners[type].splice(i, 1);
+ break;
+ }
+ }
+ }
+
+ dispatchEvent(event) {
+ requiredArguments("EventTarget.dispatchEvent", arguments.length, 1);
+ const self = this ?? globalThis;
+
+ const listeners = eventTargetData.get(self).listeners;
+ if (!(event.type in listeners)) {
+ return true;
+ }
+
+ if (getDispatched(event)) {
+ throw new DOMException("Invalid event state.", "InvalidStateError");
+ }
+
+ if (event.eventPhase !== Event.NONE) {
+ throw new DOMException("Invalid event state.", "InvalidStateError");
+ }
+
+ return dispatch(self, event);
+ }
+
+ get [Symbol.toStringTag]() {
+ return "EventTarget";
+ }
+
+ getParent(_event) {
+ return null;
+ }
+ }
+
+ defineEnumerableProps(EventTarget, [
+ "addEventListener",
+ "removeEventListener",
+ "dispatchEvent",
+ ]);
+
+ class ErrorEvent extends Event {
+ #message = "";
+ #filename = "";
+ #lineno = "";
+ #colno = "";
+ #error = "";
+
+ get message() {
+ return this.#message;
+ }
+ get filename() {
+ return this.#filename;
+ }
+ get lineno() {
+ return this.#lineno;
+ }
+ get colno() {
+ return this.#colno;
+ }
+ get error() {
+ return this.#error;
+ }
+
+ constructor(
+ type,
+ {
+ bubbles,
+ cancelable,
+ composed,
+ message = "",
+ filename = "",
+ lineno = 0,
+ colno = 0,
+ error = null,
+ } = {},
+ ) {
+ super(type, {
+ bubbles: bubbles,
+ cancelable: cancelable,
+ composed: composed,
+ });
+
+ this.#message = message;
+ this.#filename = filename;
+ this.#lineno = lineno;
+ this.#colno = colno;
+ this.#error = error;
+ }
+
+ get [Symbol.toStringTag]() {
+ return "ErrorEvent";
+ }
+ }
+
+ defineEnumerableProps(ErrorEvent, [
+ "message",
+ "filename",
+ "lineno",
+ "colno",
+ "error",
+ ]);
+
+ class CustomEvent extends Event {
+ #detail = null;
+
+ constructor(type, eventInitDict = {}) {
+ super(type, eventInitDict);
+ requiredArguments("CustomEvent", arguments.length, 1);
+ const { detail } = eventInitDict;
+ this.#detail = detail;
+ }
+
+ get detail() {
+ return this.#detail;
+ }
+
+ get [Symbol.toStringTag]() {
+ return "CustomEvent";
+ }
+ }
+
+ Reflect.defineProperty(CustomEvent.prototype, "detail", {
+ enumerable: true,
+ });
+
+ window.Event = Event;
+ 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,
+ };
+})(this);
diff --git a/op_crates/web/08_text_encoding.js b/op_crates/web/08_text_encoding.js
new file mode 100644
index 000000000..b8959142f
--- /dev/null
+++ b/op_crates/web/08_text_encoding.js
@@ -0,0 +1,838 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+// The following code is based off of text-encoding at:
+// https://github.com/inexorabletash/text-encoding
+//
+// Anyone is free to copy, modify, publish, use, compile, sell, or
+// distribute this software, either in source code form or as a compiled
+// binary, for any purpose, commercial or non-commercial, and by any
+// means.
+//
+// In jurisdictions that recognize copyright laws, the author or authors
+// of this software dedicate any and all copyright interest in the
+// software to the public domain. We make this dedication for the benefit
+// of the public at large and to the detriment of our heirs and
+// successors. We intend this dedication to be an overt act of
+// relinquishment in perpetuity of all present and future rights to this
+// software under copyright law.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+// OTHER DEALINGS IN THE SOFTWARE.
+
+((window) => {
+ const core = Deno.core;
+
+ const CONTINUE = null;
+ const END_OF_STREAM = -1;
+ const FINISHED = -1;
+
+ function decoderError(fatal) {
+ if (fatal) {
+ throw new TypeError("Decoder error.");
+ }
+ return 0xfffd; // default code point
+ }
+
+ function inRange(a, min, max) {
+ return min <= a && a <= max;
+ }
+
+ function isASCIIByte(a) {
+ return inRange(a, 0x00, 0x7f);
+ }
+
+ function stringToCodePoints(input) {
+ const u = [];
+ for (const c of input) {
+ u.push(c.codePointAt(0));
+ }
+ return u;
+ }
+
+ class UTF8Encoder {
+ handler(codePoint) {
+ if (codePoint === END_OF_STREAM) {
+ return "finished";
+ }
+
+ if (inRange(codePoint, 0x00, 0x7f)) {
+ return [codePoint];
+ }
+
+ let count;
+ let offset;
+ if (inRange(codePoint, 0x0080, 0x07ff)) {
+ count = 1;
+ offset = 0xc0;
+ } else if (inRange(codePoint, 0x0800, 0xffff)) {
+ count = 2;
+ offset = 0xe0;
+ } else if (inRange(codePoint, 0x10000, 0x10ffff)) {
+ count = 3;
+ offset = 0xf0;
+ } else {
+ throw TypeError(
+ `Code point out of range: \\x${codePoint.toString(16)}`,
+ );
+ }
+
+ const bytes = [(codePoint >> (6 * count)) + offset];
+
+ while (count > 0) {
+ const temp = codePoint >> (6 * (count - 1));
+ bytes.push(0x80 | (temp & 0x3f));
+ count--;
+ }
+
+ return bytes;
+ }
+ }
+
+ function atob(s) {
+ s = String(s);
+ s = s.replace(/[\t\n\f\r ]/g, "");
+
+ if (s.length % 4 === 0) {
+ s = s.replace(/==?$/, "");
+ }
+
+ const rem = s.length % 4;
+ if (rem === 1 || /[^+/0-9A-Za-z]/.test(s)) {
+ throw new DOMException(
+ "The string to be decoded is not correctly encoded",
+ "DataDecodeError",
+ );
+ }
+
+ // base64-js requires length exactly times of 4
+ if (rem > 0) {
+ s = s.padEnd(s.length + (4 - rem), "=");
+ }
+
+ const byteArray = base64.toByteArray(s);
+ let result = "";
+ for (let i = 0; i < byteArray.length; i++) {
+ result += String.fromCharCode(byteArray[i]);
+ }
+ return result;
+ }
+
+ function btoa(s) {
+ const byteArray = [];
+ for (let i = 0; i < s.length; i++) {
+ const charCode = s[i].charCodeAt(0);
+ if (charCode > 0xff) {
+ throw new TypeError(
+ "The string to be encoded contains characters " +
+ "outside of the Latin1 range.",
+ );
+ }
+ byteArray.push(charCode);
+ }
+ const result = base64.fromByteArray(Uint8Array.from(byteArray));
+ return result;
+ }
+
+ class SingleByteDecoder {
+ #index = [];
+ #fatal = false;
+
+ constructor(
+ index,
+ { ignoreBOM = false, fatal = false } = {},
+ ) {
+ if (ignoreBOM) {
+ throw new TypeError("Ignoring the BOM is available only with utf-8.");
+ }
+ this.#fatal = fatal;
+ this.#index = index;
+ }
+ handler(_stream, byte) {
+ if (byte === END_OF_STREAM) {
+ return FINISHED;
+ }
+ if (isASCIIByte(byte)) {
+ return byte;
+ }
+ const codePoint = this.#index[byte - 0x80];
+
+ if (codePoint == null) {
+ return decoderError(this.#fatal);
+ }
+
+ return codePoint;
+ }
+ }
+
+ // The encodingMap is a hash of labels that are indexed by the conical
+ // encoding.
+ const encodingMap = {
+ "windows-1252": [
+ "ansi_x3.4-1968",
+ "ascii",
+ "cp1252",
+ "cp819",
+ "csisolatin1",
+ "ibm819",
+ "iso-8859-1",
+ "iso-ir-100",
+ "iso8859-1",
+ "iso88591",
+ "iso_8859-1",
+ "iso_8859-1:1987",
+ "l1",
+ "latin1",
+ "us-ascii",
+ "windows-1252",
+ "x-cp1252",
+ ],
+ "utf-8": ["unicode-1-1-utf-8", "utf-8", "utf8"],
+ };
+ // We convert these into a Map where every label resolves to its canonical
+ // encoding type.
+ const encodings = new Map();
+ for (const key of Object.keys(encodingMap)) {
+ const labels = encodingMap[key];
+ for (const label of labels) {
+ encodings.set(label, key);
+ }
+ }
+
+ // A map of functions that return new instances of a decoder indexed by the
+ // encoding type.
+ const decoders = new Map();
+
+ // Single byte decoders are an array of code point lookups
+ const encodingIndexes = new Map();
+ // deno-fmt-ignore
+ encodingIndexes.set("windows-1252", [
+ 8364,
+ 129,
+ 8218,
+ 402,
+ 8222,
+ 8230,
+ 8224,
+ 8225,
+ 710,
+ 8240,
+ 352,
+ 8249,
+ 338,
+ 141,
+ 381,
+ 143,
+ 144,
+ 8216,
+ 8217,
+ 8220,
+ 8221,
+ 8226,
+ 8211,
+ 8212,
+ 732,
+ 8482,
+ 353,
+ 8250,
+ 339,
+ 157,
+ 382,
+ 376,
+ 160,
+ 161,
+ 162,
+ 163,
+ 164,
+ 165,
+ 166,
+ 167,
+ 168,
+ 169,
+ 170,
+ 171,
+ 172,
+ 173,
+ 174,
+ 175,
+ 176,
+ 177,
+ 178,
+ 179,
+ 180,
+ 181,
+ 182,
+ 183,
+ 184,
+ 185,
+ 186,
+ 187,
+ 188,
+ 189,
+ 190,
+ 191,
+ 192,
+ 193,
+ 194,
+ 195,
+ 196,
+ 197,
+ 198,
+ 199,
+ 200,
+ 201,
+ 202,
+ 203,
+ 204,
+ 205,
+ 206,
+ 207,
+ 208,
+ 209,
+ 210,
+ 211,
+ 212,
+ 213,
+ 214,
+ 215,
+ 216,
+ 217,
+ 218,
+ 219,
+ 220,
+ 221,
+ 222,
+ 223,
+ 224,
+ 225,
+ 226,
+ 227,
+ 228,
+ 229,
+ 230,
+ 231,
+ 232,
+ 233,
+ 234,
+ 235,
+ 236,
+ 237,
+ 238,
+ 239,
+ 240,
+ 241,
+ 242,
+ 243,
+ 244,
+ 245,
+ 246,
+ 247,
+ 248,
+ 249,
+ 250,
+ 251,
+ 252,
+ 253,
+ 254,
+ 255,
+ ]);
+ for (const [key, index] of encodingIndexes) {
+ decoders.set(
+ key,
+ (options) => {
+ return new SingleByteDecoder(index, options);
+ },
+ );
+ }
+
+ function codePointsToString(codePoints) {
+ let s = "";
+ for (const cp of codePoints) {
+ s += String.fromCodePoint(cp);
+ }
+ return s;
+ }
+
+ class Stream {
+ #tokens = [];
+ constructor(tokens) {
+ this.#tokens = [...tokens];
+ this.#tokens.reverse();
+ }
+
+ endOfStream() {
+ return !this.#tokens.length;
+ }
+
+ read() {
+ return !this.#tokens.length ? END_OF_STREAM : this.#tokens.pop();
+ }
+
+ prepend(token) {
+ if (Array.isArray(token)) {
+ while (token.length) {
+ this.#tokens.push(token.pop());
+ }
+ } else {
+ this.#tokens.push(token);
+ }
+ }
+
+ push(token) {
+ if (Array.isArray(token)) {
+ while (token.length) {
+ this.#tokens.unshift(token.shift());
+ }
+ } else {
+ this.#tokens.unshift(token);
+ }
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ function isEitherArrayBuffer(x) {
+ return x instanceof SharedArrayBuffer || x instanceof ArrayBuffer;
+ }
+
+ class TextDecoder {
+ #encoding = "";
+
+ get encoding() {
+ return this.#encoding;
+ }
+ fatal = false;
+ ignoreBOM = false;
+
+ constructor(label = "utf-8", options = { fatal: false }) {
+ if (options.ignoreBOM) {
+ this.ignoreBOM = true;
+ }
+ if (options.fatal) {
+ this.fatal = true;
+ }
+ label = String(label).trim().toLowerCase();
+ const encoding = encodings.get(label);
+ if (!encoding) {
+ throw new RangeError(
+ `The encoding label provided ('${label}') is invalid.`,
+ );
+ }
+ if (!decoders.has(encoding) && encoding !== "utf-8") {
+ throw new TypeError(`Internal decoder ('${encoding}') not found.`);
+ }
+ this.#encoding = encoding;
+ }
+
+ decode(
+ input,
+ options = { stream: false },
+ ) {
+ if (options.stream) {
+ throw new TypeError("Stream not supported.");
+ }
+
+ let bytes;
+ if (input instanceof Uint8Array) {
+ bytes = input;
+ } else if (isEitherArrayBuffer(input)) {
+ bytes = new Uint8Array(input);
+ } else if (
+ typeof input === "object" &&
+ "buffer" in input &&
+ isEitherArrayBuffer(input.buffer)
+ ) {
+ bytes = new Uint8Array(
+ input.buffer,
+ input.byteOffset,
+ input.byteLength,
+ );
+ } else {
+ bytes = new Uint8Array(0);
+ }
+
+ // For simple utf-8 decoding "Deno.core.decode" can be used for performance
+ if (
+ this.#encoding === "utf-8" &&
+ this.fatal === false &&
+ this.ignoreBOM === false
+ ) {
+ return core.decode(bytes);
+ }
+
+ // For performance reasons we utilise a highly optimised decoder instead of
+ // the general decoder.
+ if (this.#encoding === "utf-8") {
+ return decodeUtf8(bytes, this.fatal, this.ignoreBOM);
+ }
+
+ const decoder = decoders.get(this.#encoding)({
+ fatal: this.fatal,
+ ignoreBOM: this.ignoreBOM,
+ });
+ const inputStream = new Stream(bytes);
+ const output = [];
+
+ while (true) {
+ const result = decoder.handler(inputStream, inputStream.read());
+ if (result === FINISHED) {
+ break;
+ }
+
+ if (result !== CONTINUE) {
+ output.push(result);
+ }
+ }
+
+ if (output.length > 0 && output[0] === 0xfeff) {
+ output.shift();
+ }
+
+ return codePointsToString(output);
+ }
+
+ get [Symbol.toStringTag]() {
+ return "TextDecoder";
+ }
+ }
+
+ class TextEncoder {
+ encoding = "utf-8";
+ encode(input = "") {
+ // Deno.core.encode() provides very efficient utf-8 encoding
+ if (this.encoding === "utf-8") {
+ return core.encode(input);
+ }
+
+ const encoder = new UTF8Encoder();
+ const inputStream = new Stream(stringToCodePoints(input));
+ const output = [];
+
+ while (true) {
+ const result = encoder.handler(inputStream.read());
+ if (result === "finished") {
+ break;
+ }
+ output.push(...result);
+ }
+
+ return new Uint8Array(output);
+ }
+ encodeInto(input, dest) {
+ const encoder = new UTF8Encoder();
+ const inputStream = new Stream(stringToCodePoints(input));
+
+ let written = 0;
+ let read = 0;
+ while (true) {
+ const result = encoder.handler(inputStream.read());
+ if (result === "finished") {
+ break;
+ }
+ if (dest.length - written >= result.length) {
+ read++;
+ dest.set(result, written);
+ written += result.length;
+ if (result.length > 3) {
+ // increment read a second time if greater than U+FFFF
+ read++;
+ }
+ } else {
+ break;
+ }
+ }
+
+ return {
+ read,
+ written,
+ };
+ }
+ get [Symbol.toStringTag]() {
+ return "TextEncoder";
+ }
+ }
+
+ // This function is based on Bjoern Hoehrmann's DFA UTF-8 decoder.
+ // See http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ for details.
+ //
+ // Copyright (c) 2008-2009 Bjoern Hoehrmann <bjoern@hoehrmann.de>
+ //
+ // Permission is hereby granted, free of charge, to any person obtaining a copy
+ // of this software and associated documentation files (the "Software"), to deal
+ // in the Software without restriction, including without limitation the rights
+ // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ // copies of the Software, and to permit persons to whom the Software is
+ // furnished to do so, subject to the following conditions:
+ //
+ // The above copyright notice and this permission notice shall be included in
+ // all copies or substantial portions of the Software.
+ //
+ // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ // SOFTWARE.
+ function decodeUtf8(
+ input,
+ fatal,
+ ignoreBOM,
+ ) {
+ let outString = "";
+
+ // Prepare a buffer so that we don't have to do a lot of string concats, which
+ // are very slow.
+ const outBufferLength = Math.min(1024, input.length);
+ const outBuffer = new Uint16Array(outBufferLength);
+ let outIndex = 0;
+
+ let state = 0;
+ let codepoint = 0;
+ let type;
+
+ let i =
+ ignoreBOM && input[0] === 0xef && input[1] === 0xbb && input[2] === 0xbf
+ ? 3
+ : 0;
+
+ for (; i < input.length; ++i) {
+ // Encoding error handling
+ if (state === 12 || (state !== 0 && (input[i] & 0xc0) !== 0x80)) {
+ if (fatal) {
+ throw new TypeError(
+ `Decoder error. Invalid byte in sequence at position ${i} in data.`,
+ );
+ }
+ outBuffer[outIndex++] = 0xfffd; // Replacement character
+ if (outIndex === outBufferLength) {
+ outString += String.fromCharCode.apply(null, outBuffer);
+ outIndex = 0;
+ }
+ state = 0;
+ }
+
+ // deno-fmt-ignore
+ type = [
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+ 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,9,
+ 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7, 7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,
+ 8,8,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
+ 10,3,3,3,3,3,3,3,3,3,3,3,3,4,3,3, 11,6,6,6,5,8,8,8,8,8,8,8,8,8,8,8
+ ][input[i]];
+ codepoint = state !== 0
+ ? (input[i] & 0x3f) | (codepoint << 6)
+ : (0xff >> type) & input[i];
+ // deno-fmt-ignore
+ state = [
+ 0,12,24,36,60,96,84,12,12,12,48,72, 12,12,12,12,12,12,12,12,12,12,12,12,
+ 12, 0,12,12,12,12,12, 0,12, 0,12,12, 12,24,12,12,12,12,12,24,12,24,12,12,
+ 12,12,12,12,12,12,12,24,12,12,12,12, 12,24,12,12,12,12,12,12,12,24,12,12,
+ 12,12,12,12,12,12,12,36,12,36,12,12, 12,36,12,12,12,12,12,36,12,36,12,12,
+ 12,36,12,12,12,12,12,12,12,12,12,12
+ ][state + type];
+
+ if (state !== 0) continue;
+
+ // Add codepoint to buffer (as charcodes for utf-16), and flush buffer to
+ // string if needed.
+ if (codepoint > 0xffff) {
+ outBuffer[outIndex++] = 0xd7c0 + (codepoint >> 10);
+ if (outIndex === outBufferLength) {
+ outString += String.fromCharCode.apply(null, outBuffer);
+ outIndex = 0;
+ }
+ outBuffer[outIndex++] = 0xdc00 | (codepoint & 0x3ff);
+ if (outIndex === outBufferLength) {
+ outString += String.fromCharCode.apply(null, outBuffer);
+ outIndex = 0;
+ }
+ } else {
+ outBuffer[outIndex++] = codepoint;
+ if (outIndex === outBufferLength) {
+ outString += String.fromCharCode.apply(null, outBuffer);
+ outIndex = 0;
+ }
+ }
+ }
+
+ // Add a replacement character if we ended in the middle of a sequence or
+ // encountered an invalid code at the end.
+ if (state !== 0) {
+ if (fatal) throw new TypeError(`Decoder error. Unexpected end of data.`);
+ outBuffer[outIndex++] = 0xfffd; // Replacement character
+ }
+
+ // Final flush of buffer
+ outString += String.fromCharCode.apply(
+ null,
+ outBuffer.subarray(0, outIndex),
+ );
+
+ 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;
+ window.btoa = btoa;
+})(this);
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();