| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of _js_helper; |
| |
| // TODO(ngeoffray): stop using this method once our optimizers can |
| // change str1.contains(str2) into str1.indexOf(str2) != -1. |
| bool contains(String userAgent, String name) { |
| return JS('int', '#.indexOf(#)', userAgent, name) != -1; |
| } |
| |
| int arrayLength(List array) { |
| return JS('int', '#.length', array); |
| } |
| |
| arrayGet(List array, int index) { |
| return JS('var', '#[#]', array, index); |
| } |
| |
| void arraySet(List array, int index, var value) { |
| JS('var', '#[#] = #', array, index, value); |
| } |
| |
| propertyGet(var object, String property) { |
| return JS('var', '#[#]', object, property); |
| } |
| |
| bool callHasOwnProperty(var function, var object, String property) { |
| return JS('bool', '#.call(#, #)', function, object, property); |
| } |
| |
| void propertySet(var object, String property, var value) { |
| JS('var', '#[#] = #', object, property, value); |
| } |
| |
| getPropertyFromPrototype(var object, String name) { |
| return JS('var', 'Object.getPrototypeOf(#)[#]', object, name); |
| } |
| |
| /// Returns a String tag identifying the type of the native object, or `null`. |
| /// The tag is not the name of the type, but usually the name of the JavaScript |
| /// constructor function. Initialized by [initHooks]. |
| Function? getTagFunction; |
| |
| /// If a lookup via [getTagFunction] on an object [object] that has [tag] fails, |
| /// this function is called to provide an alternate tag. This allows us to fail |
| /// gracefully if we can make a good guess, for example, when browsers add novel |
| /// kinds of HTMLElement that we have never heard of. Initialized by |
| /// [initHooks]. |
| Function? alternateTagFunction; |
| |
| /// Returns the prototype for the JavaScript constructor named by an input tag. |
| /// Returns `null` if there is no such constructor, or if pre-patching of the |
| /// constructor is to be avoided. Initialized by [initHooks]. |
| Function? prototypeForTagFunction; |
| |
| String toStringForNativeObject(var obj) { |
| // TODO(sra): Is this code dead? |
| // [getTagFunction] might be uninitialized, but in usual usage, toString has |
| // been called via an interceptor and initialized it. |
| String name = getTagFunction == null |
| ? '<Unknown>' |
| : JS('String', '#', getTagFunction!(obj)); |
| return 'Instance of $name'; |
| } |
| |
| int hashCodeForNativeObject(object) => Primitives.objectHashCode(object); |
| |
| /// Sets a JavaScript property on an object. |
| void defineProperty(var obj, String property, var value) { |
| JS( |
| 'void', |
| 'Object.defineProperty(#, #, ' |
| '{value: #, enumerable: false, writable: true, configurable: true})', |
| obj, |
| property, |
| value); |
| } |
| |
| // Is [obj] an instance of a Dart-defined class? |
| bool isDartObject(obj) { |
| // Some of the extra parens here are necessary. |
| return JS( |
| 'bool', |
| '((#) instanceof (#))', |
| obj, |
| JS_BUILTIN( |
| 'depends:none;effects:none;', JsBuiltin.dartObjectConstructor)); |
| } |
| |
| /// A JavaScript object mapping tags to the constructors of interceptors. |
| /// This is a JavaScript object with no prototype. |
| /// |
| /// Example: 'HTMLImageElement' maps to the ImageElement class constructor. |
| get interceptorsByTag => JS_EMBEDDED_GLOBAL('=Object', INTERCEPTORS_BY_TAG); |
| |
| /// A JavaScript object mapping tags to `true` or `false`. |
| /// |
| /// Example: 'HTMLImageElement' maps to `true` since, as there are no subclasses |
| /// of ImageElement, it is a leaf class in the native class hierarchy. |
| get leafTags => JS_EMBEDDED_GLOBAL('=Object', LEAF_TAGS); |
| |
| String findDispatchTagForInterceptorClass(interceptorClassConstructor) { |
| return JS( |
| '', r'#.#', interceptorClassConstructor, NATIVE_SUPERCLASS_TAG_NAME); |
| } |
| |
| /// Cache of dispatch records for instances. This is a JavaScript object used |
| /// as a map. Keys are instance tags, e.g. "!SomeThing". The cache permits the |
| /// sharing of one dispatch record between multiple instances. |
| var dispatchRecordsForInstanceTags; |
| |
| /// Cache of interceptors indexed by uncacheable tags, e.g. "~SomeThing". |
| /// This is a JavaScript object used as a map. |
| var interceptorsForUncacheableTags; |
| |
| lookupInterceptor(String tag) { |
| return propertyGet(interceptorsByTag, tag); |
| } |
| |
| // Dispatch tag marks are optional prefixes for a dispatch tag that direct how |
| // the interceptor for the tag may be cached. |
| |
| /// No caching permitted. |
| const UNCACHED_MARK = '~'; |
| |
| /// Dispatch record must be cached per instance |
| const INSTANCE_CACHED_MARK = '!'; |
| |
| /// Dispatch record is cached on immediate prototype. |
| const LEAF_MARK = '-'; |
| |
| /// Dispatch record is cached on immediate prototype with a prototype |
| /// verification to prevent the interceptor being associated with a subclass |
| /// before a dispatch record is cached on the subclass. |
| const INTERIOR_MARK = '+'; |
| |
| /// A 'discriminator' function is to be used. TBD. |
| const DISCRIMINATED_MARK = '*'; |
| |
| /// Returns the interceptor for a native object, or returns `null` if not found. |
| /// |
| /// A dispatch record is cached according to the specification of the dispatch |
| /// tag for [obj]. |
| @pragma('dart2js:noInline') |
| lookupAndCacheInterceptor(obj) { |
| assert(!isDartObject(obj)); |
| String tag = getTagFunction!(obj); |
| |
| // Fast path for instance (and uncached) tags because the lookup is repeated |
| // for each instance (or getInterceptor call). |
| var record = propertyGet(dispatchRecordsForInstanceTags, tag); |
| if (record != null) return patchInstance(obj, record); |
| var interceptor = propertyGet(interceptorsForUncacheableTags, tag); |
| if (interceptor != null) return interceptor; |
| |
| // This lookup works for derived dispatch tags because we add them all in |
| // [initNativeDispatch]. |
| var interceptorClass = lookupInterceptor(tag); |
| if (interceptorClass == null) { |
| String? altTag = alternateTagFunction!(obj, tag); |
| if (altTag != null) { |
| tag = altTag; |
| // Fast path for instance and uncached tags again. |
| record = propertyGet(dispatchRecordsForInstanceTags, tag); |
| if (record != null) return patchInstance(obj, record); |
| interceptor = propertyGet(interceptorsForUncacheableTags, tag); |
| if (interceptor != null) return interceptor; |
| |
| interceptorClass = lookupInterceptor(tag); |
| } |
| } |
| |
| if (interceptorClass == null) { |
| // This object is not known to Dart. There could be several reasons for |
| // that, including (but not limited to): |
| // |
| // * A bug in native code (hopefully this is caught during development). |
| // * An unknown DOM object encountered. |
| // * JavaScript code running in an unexpected context. For example, on |
| // node.js. |
| return null; |
| } |
| |
| interceptor = JS('', '#.prototype', interceptorClass); |
| |
| var mark = JS('String|Null', '#[0]', tag); |
| |
| if (mark == INSTANCE_CACHED_MARK) { |
| record = makeLeafDispatchRecord(interceptor); |
| propertySet(dispatchRecordsForInstanceTags, tag, record); |
| return patchInstance(obj, record); |
| } |
| |
| if (mark == UNCACHED_MARK) { |
| propertySet(interceptorsForUncacheableTags, tag, interceptor); |
| return interceptor; |
| } |
| |
| if (mark == LEAF_MARK) { |
| return patchProto(obj, makeLeafDispatchRecord(interceptor)); |
| } |
| |
| if (mark == INTERIOR_MARK) { |
| return patchInteriorProto(obj, interceptor); |
| } |
| |
| if (mark == DISCRIMINATED_MARK) { |
| // TODO(sra): Use discriminator of tag. |
| throw new UnimplementedError(tag); |
| } |
| |
| // [tag] was not explicitly an interior or leaf tag, so |
| var isLeaf = JS('bool', '(#[#]) === true', leafTags, tag); |
| if (isLeaf) { |
| return patchProto(obj, makeLeafDispatchRecord(interceptor)); |
| } else { |
| return patchInteriorProto(obj, interceptor); |
| } |
| } |
| |
| patchInstance(obj, record) { |
| setDispatchProperty(obj, record); |
| return dispatchRecordInterceptor(record); |
| } |
| |
| patchProto(obj, record) { |
| setDispatchProperty(JS('', 'Object.getPrototypeOf(#)', obj), record); |
| return dispatchRecordInterceptor(record); |
| } |
| |
| patchInteriorProto(obj, interceptor) { |
| var proto = JS('', 'Object.getPrototypeOf(#)', obj); |
| var record = makeDispatchRecord(interceptor, proto, null, null); |
| setDispatchProperty(proto, record); |
| return interceptor; |
| } |
| |
| makeLeafDispatchRecord(interceptor) { |
| var fieldName = JS_GET_NAME(JsGetName.IS_INDEXABLE_FIELD_NAME); |
| bool indexability = JS('bool', r'!!#[#]', interceptor, fieldName); |
| return makeDispatchRecord(interceptor, false, null, indexability); |
| } |
| |
| makeDefaultDispatchRecord(tag, interceptorClass, proto) { |
| var interceptor = JS('', '#.prototype', interceptorClass); |
| var isLeaf = JS('bool', '(#[#]) === true', leafTags, tag); |
| if (isLeaf) { |
| return makeLeafDispatchRecord(interceptor); |
| } else { |
| return makeDispatchRecord(interceptor, proto, null, null); |
| } |
| } |
| |
| /// [proto] should have no shadowing prototypes that are not also assigned a |
| /// dispatch rescord. |
| setNativeSubclassDispatchRecord(proto, interceptor) { |
| setDispatchProperty(proto, makeLeafDispatchRecord(interceptor)); |
| } |
| |
| String constructorNameFallback(object) { |
| return JS('returns:String;effects:none;depends:none', '#(#)', |
| _constructorNameFallback, object); |
| } |
| |
| var initNativeDispatchFlag; // null or true |
| |
| @pragma('dart2js:noInline') |
| void initNativeDispatch() { |
| if (true == initNativeDispatchFlag) return; |
| initNativeDispatchFlag = true; |
| initNativeDispatchContinue(); |
| } |
| |
| @pragma('dart2js:noInline') |
| void initNativeDispatchContinue() { |
| dispatchRecordsForInstanceTags = JS('', 'Object.create(null)'); |
| interceptorsForUncacheableTags = JS('', 'Object.create(null)'); |
| |
| initHooks(); |
| |
| // Try to pro-actively patch prototypes of DOM objects. For each of our known |
| // tags `TAG`, if `window.TAG` is a (constructor) function, set the dispatch |
| // property if the function's prototype to a dispatch record. |
| var map = interceptorsByTag; |
| JSArray tags = JS('JSMutableArray', 'Object.getOwnPropertyNames(#)', map); |
| |
| if (JS('bool', 'typeof window != "undefined"')) { |
| var context = JS('=Object', 'window'); |
| var fun = JS('=Object', 'function () {}'); |
| for (int i = 0; i < tags.length; i++) { |
| var tag = tags[i]; |
| var proto = prototypeForTagFunction!(tag); |
| if (proto != null) { |
| var interceptorClass = JS('', '#[#]', map, tag); |
| var record = makeDefaultDispatchRecord(tag, interceptorClass, proto); |
| if (record != null) { |
| setDispatchProperty(proto, record); |
| // Ensure the modified prototype is still fast by assigning it to |
| // the prototype property of a function object. |
| JS('', '#.prototype = #', fun, proto); |
| } |
| } |
| } |
| } |
| |
| // [interceptorsByTag] maps 'plain' dispatch tags. Add all the derived |
| // dispatch tags to simplify lookup of derived tags. |
| for (int i = 0; i < tags.length; i++) { |
| var tag = JS('String', '#[#]', tags, i); |
| if (JS('bool', '/^[A-Za-z_]/.test(#)', tag)) { |
| var interceptorClass = propertyGet(map, tag); |
| propertySet(map, INSTANCE_CACHED_MARK + tag, interceptorClass); |
| propertySet(map, UNCACHED_MARK + tag, interceptorClass); |
| propertySet(map, LEAF_MARK + tag, interceptorClass); |
| propertySet(map, INTERIOR_MARK + tag, interceptorClass); |
| propertySet(map, DISCRIMINATED_MARK + tag, interceptorClass); |
| } |
| } |
| } |
| |
| /// Initializes [getTagFunction] and [alternateTagFunction]. |
| /// |
| /// These functions are 'hook functions', collectively 'hooks'. They |
| /// initialized by applying a series of hooks transformers. Built-in hooks |
| /// transformers deal with various known browser behaviours. |
| /// |
| /// Each hook tranformer takes a 'hooks' input which is a JavaScript object |
| /// containing the hook functions, and returns the same or a new object with |
| /// replacements. The replacements can wrap the originals to provide alternate |
| /// or modified behaviour. |
| /// |
| /// { getTag: function(obj) {...}, |
| /// getUnknownTag: function(obj, tag) {...}, |
| /// prototypeForTag: function(tag) {...}, |
| /// discriminator: function(tag) {...}, |
| /// } |
| /// |
| /// * getTag(obj) returns the dispatch tag, or `null`. |
| /// * getUnknownTag(obj, tag) returns a tag when [getTag] fails. |
| /// * prototypeForTag(tag) returns the prototype of the constructor for tag, |
| /// or `null` if not available or prepatching is undesirable. |
| /// * discriminator(tag) returns a function TBD. |
| /// |
| /// The web site can adapt a dart2js application by loading code ahead of the |
| /// dart2js application that defines hook transformers to be after the built in |
| /// ones. Code defining a transformer HT should use the following pattern to |
| /// ensure multiple transformers can be composed: |
| /// |
| /// (dartNativeDispatchHooksTransformer = |
| /// window.dartNativeDispatchHooksTransformer || []).push(HT); |
| /// |
| /// |
| /// TODO: Implement and describe dispatch tags and their caching methods. |
| void initHooks() { |
| // The initial simple hooks: |
| var hooks = JS('', '#()', _baseHooks); |
| |
| // Customize for browsers where `object.constructor.name` fails: |
| var _fallbackConstructorHooksTransformer = JS('', '#(#)', |
| _fallbackConstructorHooksTransformerGenerator, _constructorNameFallback); |
| hooks = applyHooksTransformer(_fallbackConstructorHooksTransformer, hooks); |
| |
| // Customize for browsers: |
| hooks = applyHooksTransformer(_firefoxHooksTransformer, hooks); |
| hooks = applyHooksTransformer(_ieHooksTransformer, hooks); |
| hooks = applyHooksTransformer(_operaHooksTransformer, hooks); |
| hooks = applyHooksTransformer(_safariHooksTransformer, hooks); |
| |
| hooks = applyHooksTransformer(_fixDocumentHooksTransformer, hooks); |
| |
| // TODO(sra): Update ShadowDOM polyfil to use |
| // [dartNativeDispatchHooksTransformer] and remove this hook. |
| hooks = applyHooksTransformer( |
| _dartExperimentalFixupGetTagHooksTransformer, hooks); |
| |
| // Apply global hooks. |
| // |
| // If defined, dartNativeDispatchHookdTransformer should be a single function |
| // of a JavaScript Array of functions. |
| |
| if (JS('bool', 'typeof dartNativeDispatchHooksTransformer != "undefined"')) { |
| var transformers = JS('', 'dartNativeDispatchHooksTransformer'); |
| if (JS('bool', 'typeof # == "function"', transformers)) { |
| transformers = [transformers]; |
| } |
| if (JS('bool', '#.constructor == Array', transformers)) { |
| for (int i = 0; i < JS('int', '#.length', transformers); i++) { |
| var transformer = JS('', '#[#]', transformers, i); |
| if (JS('bool', 'typeof # == "function"', transformer)) { |
| hooks = applyHooksTransformer(transformer, hooks); |
| } |
| } |
| } |
| } |
| |
| var getTag = JS('', '#.getTag', hooks); |
| var getUnknownTag = JS('', '#.getUnknownTag', hooks); |
| var prototypeForTag = JS('', '#.prototypeForTag', hooks); |
| |
| getTagFunction = (o) => JS('String|Null', '#(#)', getTag, o); |
| alternateTagFunction = |
| (o, String tag) => JS('String|Null', '#(#, #)', getUnknownTag, o, tag); |
| prototypeForTagFunction = |
| (String tag) => JS('', '#(#)', prototypeForTag, tag); |
| } |
| |
| applyHooksTransformer(transformer, hooks) { |
| var newHooks = JS('=Object|Null', '#(#)', transformer, hooks); |
| return JS('', '# || #', newHooks, hooks); |
| } |
| |
| // JavaScript code fragments. |
| // |
| // This is a temporary place for the JavaScript code. |
| // |
| // TODO(sra): These code fragments are not minified. They could be generated by |
| // the code emitter, or JS_CONST could be improved to parse entire functions and |
| // take care of the minification. |
| |
| const _baseHooks = const JS_CONST(r''' |
| function() { |
| var toStringFunction = Object.prototype.toString; |
| function getTag(o) { |
| var s = toStringFunction.call(o); |
| return s.substring(8, s.length - 1); |
| } |
| function getUnknownTag(object, tag) { |
| // This code really belongs in [getUnknownTagGenericBrowser] but having it |
| // here allows [getUnknownTag] to be tested on d8. |
| if (/^HTML[A-Z].*Element$/.test(tag)) { |
| // Check that it is not a simple JavaScript object. |
| var name = toStringFunction.call(object); |
| if (name == "[object Object]") return null; |
| return "HTMLElement"; |
| } |
| } |
| function getUnknownTagGenericBrowser(object, tag) { |
| if (self.HTMLElement && object instanceof HTMLElement) return "HTMLElement"; |
| return getUnknownTag(object, tag); |
| } |
| function prototypeForTag(tag) { |
| if (typeof window == "undefined") return null; |
| if (typeof window[tag] == "undefined") return null; |
| var constructor = window[tag]; |
| if (typeof constructor != "function") return null; |
| return constructor.prototype; |
| } |
| function discriminator(tag) { return null; } |
| |
| var isBrowser = typeof navigator == "object"; |
| |
| return { |
| getTag: getTag, |
| getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag, |
| prototypeForTag: prototypeForTag, |
| discriminator: discriminator }; |
| }'''); |
| |
| /// Returns the name of the constructor function for browsers where |
| /// `object.constructor.name` is not reliable. |
| /// |
| /// This function is split out of |
| /// [_fallbackConstructorHooksTransformerGenerator] as it is called from both |
| /// the dispatch hooks and via [constructorNameFallback] from objectToString. |
| const _constructorNameFallback = const JS_CONST(r''' |
| function getTagFallback(o) { |
| var s = Object.prototype.toString.call(o); |
| return s.substring(8, s.length - 1); |
| }'''); |
| |
| const _fallbackConstructorHooksTransformerGenerator = const JS_CONST(r''' |
| function(getTagFallback) { |
| return function(hooks) { |
| // If we are not in a browser, assume we are in d8. |
| // TODO(sra): Recognize jsshell. |
| if (typeof navigator != "object") return hooks; |
| |
| var ua = navigator.userAgent; |
| // TODO(antonm): remove a reference to DumpRenderTree. |
| if (ua.indexOf("DumpRenderTree") >= 0) return hooks; |
| if (ua.indexOf("Chrome") >= 0) { |
| // Confirm constructor name is usable for dispatch. |
| function confirm(p) { |
| return typeof window == "object" && window[p] && window[p].name == p; |
| } |
| if (confirm("Window") && confirm("HTMLElement")) return hooks; |
| } |
| |
| hooks.getTag = getTagFallback; |
| }; |
| }'''); |
| |
| const _ieHooksTransformer = const JS_CONST(r''' |
| function(hooks) { |
| var userAgent = typeof navigator == "object" ? navigator.userAgent : ""; |
| if (userAgent.indexOf("Trident/") == -1) return hooks; |
| |
| var getTag = hooks.getTag; |
| |
| var quickMap = { |
| "BeforeUnloadEvent": "Event", |
| "DataTransfer": "Clipboard", |
| "HTMLDDElement": "HTMLElement", |
| "HTMLDTElement": "HTMLElement", |
| "HTMLPhraseElement": "HTMLElement", |
| "Position": "Geoposition" |
| }; |
| |
| function getTagIE(o) { |
| var tag = getTag(o); |
| var newTag = quickMap[tag]; |
| if (newTag) return newTag; |
| // Patches for types which report themselves as Objects. |
| if (tag == "Object") { |
| if (window.DataView && (o instanceof window.DataView)) return "DataView"; |
| } |
| return tag; |
| } |
| |
| function prototypeForTagIE(tag) { |
| var constructor = window[tag]; |
| if (constructor == null) return null; |
| return constructor.prototype; |
| } |
| |
| hooks.getTag = getTagIE; |
| hooks.prototypeForTag = prototypeForTagIE; |
| }'''); |
| |
| const _fixDocumentHooksTransformer = const JS_CONST(r''' |
| function(hooks) { |
| var getTag = hooks.getTag; |
| var prototypeForTag = hooks.prototypeForTag; |
| function getTagFixed(o) { |
| var tag = getTag(o); |
| if (tag == "Document") { |
| // Some browsers and the polymer polyfill call both HTML and XML documents |
| // "Document", so we check for the xmlVersion property, which is the empty |
| // string on HTML documents. Since both dart:html classes Document and |
| // HtmlDocument share the same type, we must patch the instances and not |
| // the prototype. |
| if (!!o.xmlVersion) return "!Document"; |
| return "!HTMLDocument"; |
| } |
| return tag; |
| } |
| |
| function prototypeForTagFixed(tag) { |
| if (tag == "Document") return null; // Do not pre-patch Document. |
| return prototypeForTag(tag); |
| } |
| |
| hooks.getTag = getTagFixed; |
| hooks.prototypeForTag = prototypeForTagFixed; |
| }'''); |
| |
| const _firefoxHooksTransformer = const JS_CONST(r''' |
| function(hooks) { |
| var userAgent = typeof navigator == "object" ? navigator.userAgent : ""; |
| if (userAgent.indexOf("Firefox") == -1) return hooks; |
| |
| var getTag = hooks.getTag; |
| |
| var quickMap = { |
| "BeforeUnloadEvent": "Event", |
| "DataTransfer": "Clipboard", |
| "GeoGeolocation": "Geolocation", |
| "Location": "!Location", // Fixes issue 18151 |
| "WorkerMessageEvent": "MessageEvent", |
| "XMLDocument": "!Document"}; |
| |
| function getTagFirefox(o) { |
| var tag = getTag(o); |
| return quickMap[tag] || tag; |
| } |
| |
| hooks.getTag = getTagFirefox; |
| }'''); |
| |
| const _operaHooksTransformer = const JS_CONST(r''' |
| function(hooks) { return hooks; } |
| '''); |
| |
| const _safariHooksTransformer = const JS_CONST(r''' |
| function(hooks) { return hooks; } |
| '''); |
| |
| const _dartExperimentalFixupGetTagHooksTransformer = const JS_CONST(r''' |
| function(hooks) { |
| if (typeof dartExperimentalFixupGetTag != "function") return hooks; |
| hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag); |
| }'''); |