UNPKG

objj-runtime

Version:

JavaScript (ECMAScript) and Objective-J runtime

919 lines (701 loc) 26.1 kB
/* * CFBundle.js * Objective-J * * Created by Francisco Tolmasky. * Copyright 2008-2010, 280 North, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ var CFBundleUnloaded = 0, CFBundleLoading = 1 << 0, CFBundleLoadingInfoPlist = 1 << 1, CFBundleLoadingExecutable = 1 << 2, CFBundleLoadingSpritedImages = 1 << 3, CFBundleLoadingLocalizableStrings = 1 << 4, CFBundleLoaded = 1 << 5; var CFBundlesForURLStrings = { }, CFBundlesForClasses = { }, CFBundlesWithIdentifiers = { }, CFCacheBuster = new Date().getTime(), CFTotalBytesLoaded = 0, CPApplicationSizeInBytes = 0; var CPBundleDefaultBrowserLanguage = "CPBundleDefaultBrowserLanguage", CPBundleDefaultLanguage = "CPBundleDefaultLanguage"; GLOBAL(CFBundle) = function(/*CFURL|String*/ aURL) { aURL = makeAbsoluteURL(aURL).asDirectoryPathURL(); var URLString = aURL.absoluteString(), existingBundle = CFBundlesForURLStrings[URLString]; if (existingBundle) return existingBundle; CFBundlesForURLStrings[URLString] = this; this._bundleURL = aURL; this._resourcesDirectoryURL = new CFURL("Resources/", aURL); this._staticResource = NULL; this._isValid = NO; this._loadStatus = CFBundleUnloaded; this._loadRequests = []; this._infoDictionary = new CFDictionary(); this._eventDispatcher = new EventDispatcher(this); this._localizableStrings = []; this._loadedLanguage = NULL; } DISPLAY_NAME(CFBundle); CFBundle.environments = function() { // Passed in by GCC. return ENVIRONMENTS; }; DISPLAY_NAME(CFBundle.environments); CFBundle.bundleContainingURL = function(/*CFURL|String*/ aURL) { aURL = new CFURL(".", makeAbsoluteURL(aURL)); var previousURLString, URLString = aURL.absoluteString(); while (!previousURLString || previousURLString !== URLString) { var bundle = CFBundlesForURLStrings[URLString]; if (bundle && bundle._isValid) return bundle; aURL = new CFURL("..", aURL); previousURLString = URLString; URLString = aURL.absoluteString(); } return NULL; }; DISPLAY_NAME(CFBundle.bundleContainingURL); CFBundle.mainBundle = function() { return new CFBundle(mainBundleURL); }; DISPLAY_NAME(CFBundle.mainBundle); function addClassToBundle(aClass, aBundle) { if (aBundle) CFBundlesForClasses[aClass.name] = aBundle; } function resetBundle() { CFBundlesForURLStrings = { }; CFBundlesForClasses = { }; CFBundlesWithIdentifiers = { }; //CFCacheBuster = new Date().getTime(), CFTotalBytesLoaded = 0; CPApplicationSizeInBytes = 0; } CFBundle.bundleForClass = function(/*Class*/ aClass) { return CFBundlesForClasses[aClass.name] || CFBundle.mainBundle(); }; DISPLAY_NAME(CFBundle.bundleForClass); CFBundle.bundleWithIdentifier = function(/*String*/ bundleID) { return CFBundlesWithIdentifiers[bundleID] || NULL; }; DISPLAY_NAME(CFBundle.bundleWithIdentifier); CFBundle.prototype.bundleURL = function() { return this._bundleURL.absoluteURL(); }; DISPLAY_NAME(CFBundle.prototype.bundleURL); CFBundle.prototype.resourcesDirectoryURL = function() { return this._resourcesDirectoryURL; }; DISPLAY_NAME(CFBundle.prototype.resourcesDirectoryURL); CFBundle.prototype.resourceURL = function(/*String*/ aResourceName, /*String*/ aType, /*String*/ aSubDirectory, /*String*/ localizationName) { if (aType) aResourceName = aResourceName + "." + aType; if (localizationName) aResourceName = localizationName + aResourceName; if (aSubDirectory) aResourceName = aSubDirectory + "/" + aResourceName; var resourceURL = (new CFURL(aResourceName, this.resourcesDirectoryURL())).mappedURL(); return resourceURL.absoluteURL(); }; DISPLAY_NAME(CFBundle.prototype.resourceURL); CFBundle.prototype.mostEligibleEnvironmentURL = function() { if (this._mostEligibleEnvironmentURL === undefined) this._mostEligibleEnvironmentURL = new CFURL(this.mostEligibleEnvironment() + ".environment/", this.bundleURL()); return this._mostEligibleEnvironmentURL; } DISPLAY_NAME(CFBundle.prototype.mostEligibleEnvironmentURL); CFBundle.prototype.executableURL = function() { if (this._executableURL === undefined) { var executableSubPath = this.valueForInfoDictionaryKey("CPBundleExecutable"); if (!executableSubPath) this._executableURL = NULL; else this._executableURL = new CFURL(executableSubPath, this.mostEligibleEnvironmentURL()); } return this._executableURL; }; DISPLAY_NAME(CFBundle.prototype.executableURL); CFBundle.prototype.infoDictionary = function() { return this._infoDictionary; }; DISPLAY_NAME(CFBundle.prototype.infoDictionary); CFBundle.prototype.loadedLanguage = function() { return this._loadedLanguage; }; CFBundle.prototype.valueForInfoDictionaryKey = function(/*String*/ aKey) { return this._infoDictionary.valueForKey(aKey); }; DISPLAY_NAME(CFBundle.prototype.valueForInfoDictionaryKey); CFBundle.prototype.identifier = function() { return this._infoDictionary.valueForKey("CPBundleIdentifier"); }; DISPLAY_NAME(CFBundle.prototype.identifier); CFBundle.prototype.hasSpritedImages = function() { var environments = this._infoDictionary.valueForKey("CPBundleEnvironmentsWithImageSprites") || [], index = environments.length, mostEligibleEnvironment = this.mostEligibleEnvironment(); while (index--) if (environments[index] === mostEligibleEnvironment) return YES; return NO; }; DISPLAY_NAME(CFBundle.prototype.hasSpritedImages); CFBundle.prototype.environments = function() { return this._infoDictionary.valueForKey("CPBundleEnvironments") || ["ObjJ"]; }; DISPLAY_NAME(CFBundle.prototype.environments); CFBundle.prototype.mostEligibleEnvironment = function(/*Array*/ environments) { environments = environments || this.environments(); var objj_environments = CFBundle.environments(), index = 0, count = objj_environments.length, innerCount = environments.length; // Ugh, no indexOf, no objects-in-common. for(; index < count; ++index) { var innerIndex = 0, environment = objj_environments[index]; for (; innerIndex < innerCount; ++innerIndex) if(environment === environments[innerIndex]) return environment; } return NULL; }; DISPLAY_NAME(CFBundle.prototype.mostEligibleEnvironment); CFBundle.prototype.isLoading = function() { return this._loadStatus & CFBundleLoading; }; DISPLAY_NAME(CFBundle.prototype.isLoading); CFBundle.prototype.isLoaded = function() { return !!(this._loadStatus & CFBundleLoaded); }; DISPLAY_NAME(CFBundle.prototype.isLoaded); CFBundle.prototype.load = function(/*BOOL*/ shouldExecute) { if (this._loadStatus !== CFBundleUnloaded) return; this._loadStatus = CFBundleLoading | CFBundleLoadingInfoPlist; var self = this, bundleURL = this.bundleURL(), parentURL = new CFURL("..", bundleURL); if (parentURL.absoluteString() === bundleURL.absoluteString()) parentURL = parentURL.schemeAndAuthority(); StaticResource.resolveResourceAtURL(parentURL, YES, function(aStaticResource) { var resourceName = bundleURL.lastPathComponent(); self._staticResource = aStaticResource._children[resourceName] || new StaticResource(bundleURL, aStaticResource, YES, NO); function onsuccess(/*Event*/ anEvent) { self._loadStatus &= ~CFBundleLoadingInfoPlist; var infoDictionary = anEvent.request.responsePropertyList(); self._isValid = !!infoDictionary || CFBundle.mainBundle() === self; if (infoDictionary) { self._infoDictionary = infoDictionary; var identifier = self._infoDictionary.valueForKey("CPBundleIdentifier"); if (identifier) CFBundlesWithIdentifiers[identifier] = self; } if (!self._infoDictionary) { finishBundleLoadingWithError(self, new Error("Could not load bundle at \"" + path + "\"")); return; } if (self === CFBundle.mainBundle() && self.valueForInfoDictionaryKey("CPApplicationSize")) CPApplicationSizeInBytes = self.valueForInfoDictionaryKey("CPApplicationSize").valueForKey("executable") || 0; loadLanguageForBundle(self); loadExecutableAndResources(self, shouldExecute); } function onfailure() { self._isValid = CFBundle.mainBundle() === self; self._loadStatus = CFBundleUnloaded; finishBundleLoadingWithError(self, new Error("Could not load bundle at \"" + self.bundleURL() + "\"")); } new FileRequest(new CFURL("Info.plist", self.bundleURL()), onsuccess, onfailure); }); }; DISPLAY_NAME(CFBundle.prototype.load); function finishBundleLoadingWithError(/*CFBundle*/ aBundle, /*Event*/ anError) { resolveStaticResource(aBundle._staticResource); aBundle._eventDispatcher.dispatchEvent( { type:"error", error:anError, bundle:aBundle }); } function loadExecutableAndResources(/*Bundle*/ aBundle, /*BOOL*/ shouldExecute) { if (!aBundle.mostEligibleEnvironment()) return failure(); loadExecutableForBundle(aBundle, success, failure, progress); loadSpritedImagesForBundle(aBundle, success, failure, progress); loadLocalizableStringsForBundle(aBundle, success, failure, progress); if (aBundle._loadStatus === CFBundleLoading) return success(); function failure(/*Error*/ anError) { var loadRequests = aBundle._loadRequests, count = loadRequests.length; while (count--) loadRequests[count].abort(); this._loadRequests = []; aBundle._loadStatus = CFBundleUnloaded; finishBundleLoadingWithError(aBundle, anError || new Error("Could not recognize executable code format in Bundle " + aBundle)); } function progress(bytesLoaded) { if ((typeof CPApp === "undefined" || !CPApp || !CPApp._finishedLaunching) && typeof OBJJ_PROGRESS_CALLBACK === "function") { CFTotalBytesLoaded += bytesLoaded; var percent = CPApplicationSizeInBytes ? MAX(MIN(1.0, CFTotalBytesLoaded / CPApplicationSizeInBytes), 0.0) : 0; OBJJ_PROGRESS_CALLBACK(percent, CPApplicationSizeInBytes, aBundle.bundlePath()); } } function success() { if (aBundle._loadStatus === CFBundleLoading) aBundle._loadStatus = CFBundleLoaded; else return; // Set resolved to true here in case during evaluation this bundle // needs to resolve another bundle which in turn needs it to be resolved (cycle). resolveStaticResource(aBundle._staticResource); function complete() { aBundle._eventDispatcher.dispatchEvent( { type:"load", bundle:aBundle }); } if (shouldExecute) executeBundle(aBundle, complete); else complete(); } } function loadExecutableForBundle(/*Bundle*/ aBundle, success, failure, progress) { var executableURL = aBundle.executableURL(); if (!executableURL) return; aBundle._loadStatus |= CFBundleLoadingExecutable; new FileRequest(executableURL, function(/*Event*/ anEvent) { try { decompileStaticFile(aBundle, anEvent.request.responseText(), executableURL); aBundle._loadStatus &= ~CFBundleLoadingExecutable; success(); } catch(anException) { failure(anException); } }, failure, progress); } function spritedImagesTestURLStringForBundle(/*Bundle*/ aBundle) { return "mhtml:" + new CFURL("MHTMLTest.txt", aBundle.mostEligibleEnvironmentURL()); } function spritedImagesURLForBundle(/*Bundle*/ aBundle) { if (CFBundleSupportedSpriteType === CFBundleDataURLSpriteType) return new CFURL("dataURLs.txt", aBundle.mostEligibleEnvironmentURL()); if (CFBundleSupportedSpriteType === CFBundleMHTMLSpriteType || CFBundleSupportedSpriteType === CFBundleMHTMLUncachedSpriteType) return new CFURL("MHTMLPaths.txt", aBundle.mostEligibleEnvironmentURL()); return NULL; } function loadSpritedImagesForBundle(/*Bundle*/ aBundle, success, failure, progress) { if (!aBundle.hasSpritedImages()) return; aBundle._loadStatus |= CFBundleLoadingSpritedImages; if (!CFBundleHasTestedSpriteSupport()) return CFBundleTestSpriteSupport(spritedImagesTestURLStringForBundle(aBundle), function() { loadSpritedImagesForBundle(aBundle, success, failure, progress); }); var spritedImagesURL = spritedImagesURLForBundle(aBundle); if (!spritedImagesURL) { aBundle._loadStatus &= ~CFBundleLoadingSpritedImages; return success(); } new FileRequest(spritedImagesURL, function(/*Event*/ anEvent) { try { decompileStaticFile(aBundle, anEvent.request.responseText(), spritedImagesURL); aBundle._loadStatus &= ~CFBundleLoadingSpritedImages; success(); } catch(anException) { failure(anException); } }, failure, progress); } function loadLocalizableStringsForBundle(/*Bundle*/ aBundle, success, failure, progress) { var language = aBundle._loadedLanguage; if (!language) return; var localizableStrings = aBundle.valueForInfoDictionaryKey("CPBundleLocalizableStrings"); if (!localizableStrings) return; var self = aBundle, length = localizableStrings.length, languagePathURL = new CFURL(language + ".lproj/", self.resourcesDirectoryURL()), fileSuccessed = 0; for (var i = 0; i < length; i++) { var localizableString = localizableStrings[i]; function onsuccess(/*Event*/ anEvent) { var contentFile = anEvent.request.responseText(), tableName = new CFURL(anEvent.request._URL).lastPathComponent(); try { loadLocalizableContentForFileInBundle(self, contentFile, tableName); if (++fileSuccessed == length) { aBundle._loadStatus &= ~CFBundleLoadingLocalizableStrings; success(); } } catch(e) { failure(new Error("Error when parsing the localizable file " + tableName)); } } aBundle._loadStatus |= CFBundleLoadingLocalizableStrings; new FileRequest(new CFURL(localizableString, languagePathURL), onsuccess, failure, progress); } } function loadLocalizableContentForFileInBundle(bundle, contentFile, tableName) { var values = {}, lines = contentFile.split("\n"), currentContext; bundle._localizableStrings[tableName] = values; for (var i = 0; i < lines.length; i++) { var line = lines[i]; // Context if (line[0] == "/") { currentContext = line.substring(2, line.length - 2).trim(); continue; } // Key if (line[0] == "\"") { var split = line.split("\"") // Here we add twice the translated value, once with the context and once without // This will help in term of rapidity when asking the value of the key var key = split[1]; if (!(key in values)) values[key] = split[3]; key += currentContext; if (!(key in values)) values[key] = split[3]; continue; } } } function loadLanguageForBundle(aBundle) { if (aBundle._loadedLanguage) return; var defaultLanguage = aBundle.valueForInfoDictionaryKey(CPBundleDefaultLanguage); if (defaultLanguage != CPBundleDefaultBrowserLanguage && defaultLanguage) { aBundle._loadedLanguage = defaultLanguage; return; } if (typeof navigator == "undefined") return; // userLanguage is an IE only property. var language = (typeof navigator.language !== "undefined") ? navigator.language : navigator.userLanguage; if (!language) return; aBundle._loadedLanguage = language.substring(0, 2); } var CFBundleSpriteSupportListeners = [], CFBundleSupportedSpriteType = -1, CFBundleNoSpriteType = 0, CFBundleDataURLSpriteType = 1, CFBundleMHTMLSpriteType = 2, CFBundleMHTMLUncachedSpriteType = 3; function CFBundleHasTestedSpriteSupport() { return CFBundleSupportedSpriteType !== -1; } function CFBundleTestSpriteSupport(/*String*/ MHTMLPath, /*Function*/ aCallback) { if (CFBundleHasTestedSpriteSupport()) return; CFBundleSpriteSupportListeners.push(aCallback); if (CFBundleSpriteSupportListeners.length > 1) return; CFBundleSpriteSupportListeners.push(function() { var size = 0, sizeDictionary = CFBundle.mainBundle().valueForInfoDictionaryKey("CPApplicationSize"); if (!sizeDictionary) return; switch (CFBundleSupportedSpriteType) { case CFBundleDataURLSpriteType: size = sizeDictionary.valueForKey("data"); break; case CFBundleMHTMLSpriteType: case CFBundleMHTMLUncachedSpriteType: size = sizeDictionary.valueForKey("mhtml"); break; } CPApplicationSizeInBytes += size; }); CFBundleTestSpriteTypes([ CFBundleDataURLSpriteType, "data:image/gif;base64,R0lGODlhAQABAIAAAMc9BQAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==", CFBundleMHTMLSpriteType, MHTMLPath+"!test", CFBundleMHTMLUncachedSpriteType, MHTMLPath+"?"+CFCacheBuster+"!test" ]); } function CFBundleNotifySpriteSupportListeners() { var count = CFBundleSpriteSupportListeners.length; while (count--) CFBundleSpriteSupportListeners[count](); } function CFBundleTestSpriteTypes(/*Array*/ spriteTypes) { // If we don't support Images, then clearly we don't support sprites. if (!("Image" in global) || spriteTypes.length < 2) { CFBundleSupportedSpriteType = CFBundleNoSpriteType; CFBundleNotifySpriteSupportListeners(); return; } var image = new Image(); image.onload = function() { if (image.width === 1 && image.height === 1) { CFBundleSupportedSpriteType = spriteTypes[0]; CFBundleNotifySpriteSupportListeners(); } else image.onerror(); }; image.onerror = function() { CFBundleTestSpriteTypes(spriteTypes.slice(2)); }; image.src = spriteTypes[1]; } function executeBundle(/*Bundle*/ aBundle, /*Function*/ aCallback) { var staticResources = [aBundle._staticResource]; function executeStaticResources(index) { for (; index < staticResources.length; ++index) { var staticResource = staticResources[index]; if (staticResource.isNotFound()) continue; if (staticResource.isFile()) { var executable = new FileExecutable(staticResource.URL()); if (executable.hasLoadedFileDependencies()) executable.execute(); else { executable.loadFileDependencies(function() { executeStaticResources(index); }); return; } } else //if (staticResource.isDirectory()) { // We don't want to execute resources. if (staticResource.URL().absoluteString() === aBundle.resourcesDirectoryURL().absoluteString()) continue; var children = staticResource.children(); for (var name in children) if (hasOwnProperty.call(children, name)) staticResources.push(children[name]); } } aCallback(); } executeStaticResources(0); } var STATIC_MAGIC_NUMBER = "@STATIC", MARKER_PATH = "p", MARKER_URI = "u", MARKER_CODE = "c", MARKER_TEXT = "t", MARKER_IMPORT_STD = 'I', MARKER_IMPORT_LOCAL = 'i'; MARKER_SOURCE_MAP = 'S'; function decompileStaticFile(/*Bundle*/ aBundle, /*String*/ aString, /*String*/ aPath) { var stream = new MarkedStream(aString); if (stream.magicNumber() !== STATIC_MAGIC_NUMBER) throw new Error("Could not read static file: " + aPath); if (stream.version() !== "1.0") throw new Error("Could not read static file: " + aPath); var marker, bundleURL = aBundle.bundleURL(), file = NULL; while (marker = stream.getMarker()) { var text = stream.getString(); if (marker === MARKER_PATH) { var fileURL = new CFURL(text, bundleURL), parent = StaticResource.resourceAtURL(new CFURL(".", fileURL), YES); file = new StaticResource(fileURL, parent, NO, YES); } else if (marker === MARKER_URI) { var URL = new CFURL(text, bundleURL), mappedURLString = stream.getString(); if (mappedURLString.indexOf("mhtml:") === 0) { mappedURLString = "mhtml:" + new CFURL(mappedURLString.substr("mhtml:".length), bundleURL); if (CFBundleSupportedSpriteType === CFBundleMHTMLUncachedSpriteType) { var exclamationIndex = mappedURLString.indexOf("!"), firstPart = mappedURLString.substring(0, exclamationIndex), lastPart = mappedURLString.substring(exclamationIndex); mappedURLString = firstPart + "?" + CFCacheBuster + lastPart; } } CFURL.setMappedURLForURL(URL, new CFURL(mappedURLString)); // The unresolved directories must not be bundles. var parent = StaticResource.resourceAtURL(new CFURL(".", URL), YES); new StaticResource(URL, parent, NO, YES); } else if (marker === MARKER_TEXT) file.write(text); } } // Event Managament CFBundle.prototype.addEventListener = function(/*String*/ anEventName, /*Function*/ anEventListener) { this._eventDispatcher.addEventListener(anEventName, anEventListener); }; DISPLAY_NAME(CFBundle.prototype.addEventListener); CFBundle.prototype.removeEventListener = function(/*String*/ anEventName, /*Function*/ anEventListener) { this._eventDispatcher.removeEventListener(anEventName, anEventListener); }; DISPLAY_NAME(CFBundle.prototype.removeEventListener); CFBundle.prototype.onerror = function(/*Event*/ anEvent) { throw anEvent.error; }; DISPLAY_NAME(CFBundle.prototype.onerror); CFBundle.prototype.bundlePath = function() { return this.bundleURL().path(); }; CFBundle.prototype.path = function() { CPLog.warn("CFBundle.prototype.path is deprecated, use CFBundle.prototype.bundlePath instead."); return this.bundlePath.apply(this, arguments); }; CFBundle.prototype.pathForResource = function(aResource, aType, aSubDirectory, localizationName) { return this.resourceURL(aResource, aType, aSubDirectory, localizationName).absoluteString(); }; GLOBAL(CFBundleCopyLocalizedString) = function (/*Bundle*/ bundle, key, value, tableName) { return CFCopyLocalizedStringWithDefaultValue(key, tableName, bundle, value, ""); } GLOBAL(CFBundleCopyBundleLocalizations) = function (/*Bundle*/ aBundle) { return [this._loadedLanguage]; } GLOBAL(CFCopyLocalizedString) = function (key, comment) { return CFCopyLocalizedStringFromTable(key, "Localizable", comment); } GLOBAL(CFCopyLocalizedStringFromTable) = function (key, tableName, comment) { return CFCopyLocalizedStringFromTableInBundle(key, tableName, CFBundleGetMainBundle(), comment); } GLOBAL(CFCopyLocalizedStringFromTableInBundle) = function (key, tableName, bundle, comment) { return CFCopyLocalizedStringWithDefaultValue(key, tableName, bundle, null, comment); } GLOBAL(CFCopyLocalizedStringWithDefaultValue) = function (key, tableName, bundle, value, comment) { var string; if (!tableName) tableName = "Localizable"; tableName += ".strings"; var localizableString = bundle._localizableStrings[tableName]; string = localizableString ? localizableString[key + comment] : null; return string || (value || key); } GLOBAL(CFBundleGetMainBundle) = function () { return CFBundle.mainBundle(); };