diff options
Diffstat (limited to 'op_crates/web')
-rw-r--r-- | op_crates/web/00_dom_exception.js | 15 | ||||
-rw-r--r-- | op_crates/web/01_event.js | 1048 | ||||
-rw-r--r-- | op_crates/web/08_text_encoding.js | 838 | ||||
-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 |
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(); |