diff options
| -rw-r--r-- | background_scripts/commands.coffee | 6 | ||||
| -rw-r--r-- | background_scripts/completion.coffee | 4 | ||||
| -rw-r--r-- | background_scripts/main.coffee | 26 | ||||
| -rw-r--r-- | content_scripts/link_hints.coffee | 22 | ||||
| -rw-r--r-- | content_scripts/vimium_frontend.coffee | 14 | ||||
| -rw-r--r-- | content_scripts/vomnibar.coffee | 1 | ||||
| -rw-r--r-- | lib/dom_utils.coffee | 3 | ||||
| -rw-r--r-- | lib/utils.coffee | 137 | ||||
| -rw-r--r-- | tests/dom_tests/dom_tests.coffee | 19 | ||||
| -rw-r--r-- | tests/unit_tests/utils_test.coffee | 22 |
10 files changed, 131 insertions, 123 deletions
diff --git a/background_scripts/commands.coffee b/background_scripts/commands.coffee index e3001f4f..69c37ac4 100644 --- a/background_scripts/commands.coffee +++ b/background_scripts/commands.coffee @@ -43,9 +43,9 @@ Commands = # On the other hand, <c-a> and <c-A> are different named keys - for one of # them you have to press "shift" as well. normalizeKey: (key) -> - key.replace(/<[acm]-/ig, (match) -> match.toLowerCase()) - .replace(/<([acm]-)?([a-zA-Z0-9]{2,5})>/g, (match, optionalPrefix, keyName) -> - "<" + (if optionalPrefix then optionalPrefix else "") + keyName.toLowerCase() + ">") + key.replace(/<[acm]-/ig, (match) -> match.toLowerCase()) + .replace(/<([acm]-)?([a-zA-Z0-9]{2,5})>/g, (match, optionalPrefix, keyName) -> + "<" + (if optionalPrefix then optionalPrefix else "") + keyName.toLowerCase() + ">") parseCustomKeyMappings: (customKeyMappings) -> lines = customKeyMappings.split("\n") diff --git a/background_scripts/completion.coffee b/background_scripts/completion.coffee index 5920db0b..5539f7c7 100644 --- a/background_scripts/completion.coffee +++ b/background_scripts/completion.coffee @@ -26,7 +26,7 @@ class Suggestion generateHtml: -> return @html if @html - relevancyHtml = if @showRelevancy then "<span class='relevancy'>#{@computeRelevancy() + ''}</span>" else "" + relevancyHtml = if @showRelevancy then "<span class='relevancy'>#{@computeRelevancy()}</span>" else "" # NOTE(philc): We're using these vimium-specific class names so we don't collide with the page's CSS. @html = "<div class='vimiumReset vomnibarTopHalf'> @@ -90,7 +90,7 @@ class BookmarkCompleter performSearch: -> results = @bookmarks.filter (bookmark) => - RankingUtils.matches(@currentSearch.queryTerms, bookmark.url, bookmark.title) + RankingUtils.matches(@currentSearch.queryTerms, bookmark.url, bookmark.title) suggestions = results.map (bookmark) => new Suggestion(@currentSearch.queryTerms, "bookmark", bookmark.url, bookmark.title, @computeRelevancy) onComplete = @currentSearch.onComplete diff --git a/background_scripts/main.coffee b/background_scripts/main.coffee index bc3fde0b..a3857d61 100644 --- a/background_scripts/main.coffee +++ b/background_scripts/main.coffee @@ -265,17 +265,17 @@ selectTab = (callback, direction) -> chrome.tabs.getAllInWindow(null, (tabs) -> return unless tabs.length > 1 chrome.tabs.getSelected(null, (currentTab) -> - switch direction - when "next" - toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length] - when "previous" - toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length] - when "first" - toSelect = tabs[0] - when "last" - toSelect = tabs[tabs.length - 1] - selectionChangedHandlers.push(callback) - chrome.tabs.update(toSelect.id, { selected: true }))) + switch direction + when "next" + toSelect = tabs[(currentTab.index + 1 + tabs.length) % tabs.length] + when "previous" + toSelect = tabs[(currentTab.index - 1 + tabs.length) % tabs.length] + when "first" + toSelect = tabs[0] + when "last" + toSelect = tabs[tabs.length - 1] + selectionChangedHandlers.push(callback) + chrome.tabs.update(toSelect.id, { selected: true }))) updateOpenTabs = (tab) -> openTabs[tab.id] = { url: tab.url, positionIndex: tab.index, windowId: tab.windowId } @@ -367,7 +367,7 @@ updatePositionsAndWindowsForAllTabsInWindow = (windowId) -> splitKeyIntoFirstAndSecond = (key) -> if (key.search(namedKeyRegex) == 0) - { first: RegExp.$1, second: RegExp.$2 } + { first: RegExp.$1, second: RegExp.$2 } else { first: key[0], second: key.slice(1) } @@ -409,7 +409,7 @@ generateCompletionKeys = (keysToCheck) -> for key of Commands.keyToCommandRegistry splitKey = splitKeyIntoFirstAndSecond(key) if (splitKey.first == command) - completionKeys.push(splitKey.second) + completionKeys.push(splitKey.second) completionKeys diff --git a/content_scripts/link_hints.coffee b/content_scripts/link_hints.coffee index 1d5d668a..831408e2 100644 --- a/content_scripts/link_hints.coffee +++ b/content_scripts/link_hints.coffee @@ -34,10 +34,10 @@ LinkHints = # We use translate() instead of lower-case() because Chrome only supports XPath 1.0. # clickableElementsXPath: DomUtils.makeXPath( - ["a", "area[@href]", "textarea", "button", "select", - "input[not(@type='hidden' or @disabled or @readonly)]", - "*[@onclick or @tabindex or @role='link' or @role='button' or contains(@class, 'button') or " + - "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]) + ["a", "area[@href]", "textarea", "button", "select", + "input[not(@type='hidden' or @disabled or @readonly)]", + "*[@onclick or @tabindex or @role='link' or @role='button' or contains(@class, 'button') or " + + "@contenteditable='' or translate(@contenteditable, 'TRUE', 'true')='true']"]) # We need this as a top-level function because our command system doesn't yet support arguments. activateModeToOpenInNewTab: -> @activateMode(true, false, false) @@ -80,8 +80,8 @@ LinkHints = else if (withQueue) HUD.show("Open multiple links in a new tab") @linkActivator = (link) -> - # When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on windows) - # to open it in a new tab if necessary. + # When "clicking" on a link, dispatch the event with the appropriate meta key (CMD on Mac, CTRL on + # windows) to open it in a new tab if necessary. DomUtils.simulateClick(link, { metaKey: KeyboardUtils.platform == "Mac", ctrlKey: KeyboardUtils.platform != "Mac" }) @@ -211,7 +211,7 @@ LinkHints = # showMarker: (linkMarker, matchingCharCount) -> linkMarker.style.display = "" - # TODO(philc): + # TODO(philc): for j in [0...linkMarker.childNodes.length] if (j < matchingCharCount) linkMarker.childNodes[j].classList.add("matchingCharacter") @@ -269,8 +269,8 @@ alphabetHints = digitsNeeded = Math.ceil(@logXOfBase(linkCount, linkHintCharacters.length)) # Short hints are the number of hints we can possibly show which are (digitsNeeded - 1) digits in length. shortHintCount = Math.floor( - (Math.pow(linkHintCharacters.length, digitsNeeded) - linkCount) / - linkHintCharacters.length) + (Math.pow(linkHintCharacters.length, digitsNeeded) - linkCount) / + linkHintCharacters.length) longHintCount = linkCount - shortHintCount hintStrings = [] @@ -299,8 +299,8 @@ alphabetHints = result # - # Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of - # the hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits. + # Converts a number like "8" into a hint string like "JK". This is used to sequentially generate all of the + # hint text. The hint string will be "padded with zeroes" to ensure its length is equal to numHintDigits. # numberToHintString: (number, numHintDigits, characterSet) -> base = characterSet.length diff --git a/content_scripts/vimium_frontend.coffee b/content_scripts/vimium_frontend.coffee index ab3b1fbf..adec11c5 100644 --- a/content_scripts/vimium_frontend.coffee +++ b/content_scripts/vimium_frontend.coffee @@ -295,8 +295,8 @@ extend window, chrome.extension.sendRequest({ handler: "openUrlInNewTab", url: url, selected: true }) copyCurrentUrl: -> - # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background page - # to copy. + # TODO(ilya): When the following bug is fixed, revisit this approach of sending back to the background + # page to copy. # http://code.google.com/p/chromium/issues/detail?id=55188 chrome.extension.sendRequest { handler: "getCurrentTabUrl" }, (url) -> chrome.extension.sendRequest { handler: "copyToClipboard", data: url } @@ -895,9 +895,9 @@ window.showHelpDialog = (html, fid) -> this.dialogElement.style.maxHeight = window.innerHeight - 80 this.showAdvancedCommands(this.getShowAdvancedCommands()) - # + # # Advanced commands are hidden by default so they don't overwhelm new and casual users. - # + # toggleAdvancedCommands: (event) -> event.preventDefault() showAdvanced = VimiumHelpDialog.getShowAdvancedCommands() @@ -958,9 +958,9 @@ HUD = HUD.displayElement().style.display = "" showUpgradeNotification: (version) -> - HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to " + - "<a class='vimiumReset' href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'>" + - version + "</a>.<a class='vimiumReset close-button' href='#'>x</a>" + HUD.upgradeNotificationElement().innerHTML = "Vimium has been updated to + <a class='vimiumReset' href='https://chrome.google.com/extensions/detail/dbepggeogbaibhgnhhndojpepiihcmeb'> + #{version}</a>.<a class='vimiumReset close-button' href='#'>x</a>" links = HUD.upgradeNotificationElement().getElementsByTagName("a") links[0].addEventListener("click", HUD.onUpdateLinkClicked, false) links[1].addEventListener "click", (event) -> diff --git a/content_scripts/vomnibar.coffee b/content_scripts/vomnibar.coffee index 9c1e7a31..1b2af1c4 100644 --- a/content_scripts/vomnibar.coffee +++ b/content_scripts/vomnibar.coffee @@ -172,7 +172,6 @@ class VomnibarUI @input = document.querySelector("#vomnibar input") @input.addEventListener "input", => @update() - console.log("@input:", @input); @completionList = document.querySelector("#vomnibar ul") @completionList.style.display = "none" diff --git a/lib/dom_utils.coffee b/lib/dom_utils.coffee index d4a4d379..e7fa9c2f 100644 --- a/lib/dom_utils.coffee +++ b/lib/dom_utils.coffee @@ -85,7 +85,8 @@ DomUtils = if (clientRect.width == 0 || clientRect.height == 0) for child in element.children computedStyle = window.getComputedStyle(child, null) - # Ignore child elements which are not floated and not absolutely positioned for parent elements with zero width/height + # Ignore child elements which are not floated and not absolutely positioned for parent elements with + # zero width/height if (computedStyle.getPropertyValue('float') == 'none' && computedStyle.getPropertyValue('position') != 'absolute') continue childClientRect = @getVisibleClientRect(child) diff --git a/lib/utils.coffee b/lib/utils.coffee index 598b631a..a2221ba4 100644 --- a/lib/utils.coffee +++ b/lib/utils.coffee @@ -1,6 +1,6 @@ Utils = getCurrentVersion: -> - # Chromium #15242 will make this XHR request to access the manifest unnecessary. + # Chromium #15242 will make this XHR request to access the manifest unnecessary manifestRequest = new XMLHttpRequest() manifestRequest.open("GET", chrome.extension.getURL("manifest.json"), false) manifestRequest.send(null) @@ -25,10 +25,9 @@ Utils = escapeHtml: (string) -> string.replace(/</g, "<").replace(/>/g, ">") # Generates a unique ID - createUniqueId: (-> + createUniqueId: do -> id = 0 - return -> id += 1 - )() + -> id += 1 hasChromePrefix: (url) -> chromePrefixes = [ 'about', 'view-source' ] @@ -38,84 +37,79 @@ Utils = # Completes a partial URL (without scheme) createFullUrl: (partialUrl) -> - if (!/^[a-z]{3,}:\/\//.test(partialUrl)) + unless /^[a-z]{3,}:\/\//.test partialUrl "http://" + partialUrl else partialUrl - # Tries to detect, whether :str is a valid URL. + # Tries to detect if :str is a valid URL. isUrl: (str) -> - # more or less RFC compliant URL host part parsing. This should be sufficient - # for our needs + # Starts with a scheme: URL + return true if /^[a-z]{3,}:\/\//.test str + + # Must not contain spaces + return false if ' ' in str + + # More or less RFC compliant URL host part parsing. This should be sufficient for our needs urlRegex = new RegExp( - '^(?:([^:]+)(?::([^:]+))?@)?' + # user:password (optional) => \1, \2 - '([^:]+|\\[[^\\]]+\\])' + # host name (IPv6 addresses in square brackets allowed) => \3 - '(?::(\\d+))?$' # port number (optional) => \4 + '^(?:([^:]+)(?::([^:]+))?@)?' + # user:password (optional) => \1, \2 + '([^:]+|\\[[^\\]]+\\])' + # host name (IPv6 addresses in square brackets allowed) => \3 + '(?::(\\d+))?$' # port number (optional) => \4 ) - # these are all official ASCII TLDs that are longer than 3 characters - # (including the inofficial .onion TLD used by TOR) - longTlds = [ 'arpa', 'asia', 'coop', 'info', 'jobs', 'local', 'mobi', 'museum', 'name', 'onion' ] - - # are there more? - specialHostNames = [ 'localhost' ] - - # it starts with a scheme, so it's definitely an URL - if (/^[a-z]{3,}:\/\//.test(str)) - return true - - # spaces => definitely not a valid URL - if (str.indexOf(' ') >= 0) - return false - - # assuming that this is an URL, try to parse it into its meaningful parts. If matching fails, we're - # pretty sure that we don't have some kind of URL here. - match = urlRegex.exec(str.split('/')[0]) - if (!match) - return false - hostname = match[3] - - # allow known special host names - if (specialHostNames.indexOf(hostname) >= 0) - return true - - # allow IPv6 addresses (need to be wrapped in brackets, as required by RFC). It is sufficient to check - # for a colon here, as the regex wouldn't match colons in the host name unless it's an v6 address - if (hostname.indexOf(':') >= 0) - return true - - # at this point we have to make a decision. As a heuristic, we check if the input has dots in it. If - # yes, and if the last part could be a TLD, treat it as an URL. - dottedParts = hostname.split('.') - lastPart = dottedParts[dottedParts.length-1] - if (dottedParts.length > 1 && ((lastPart.length >= 2 && lastPart.length <= 3) || - longTlds.indexOf(lastPart) >= 0)) - return true - - # also allow IPv4 addresses - if (/^(\d{1,3}\.){3}\d{1,3}$/.test(hostname)) - return true - - # fallback: no URL + # Official ASCII TLDs that are longer than 3 characters + inofficial .onion TLD used by TOR + longTlds = ['arpa', 'asia', 'coop', 'info', 'jobs', 'local', 'mobi', 'museum', 'name', 'onion'] + + specialHostNames = ['localhost'] + + # Try to parse the URL into its meaningful parts. If matching fails we're pretty sure that we don't have + # some kind of URL here. + match = urlRegex.exec (str.split '/')[0] + return false unless match + hostName = match[3] + + # Allow known special host names + return true if hostName in specialHostNames + + # Allow IPv6 addresses (need to be wrapped in brackets as required by RFC). It is sufficient to check for + # a colon, as the regex wouldn't match colons in the host name unless it's an v6 address + return true if ':' in hostName + + # At this point we have to make a decision. As a heuristic, we check if the input has dots in it. If yes, + # and if the last part could be a TLD, treat it as an URL + dottedParts = hostName.split '.' + + if dottedParts.length > 1 + lastPart = dottedParts.pop() + return true if 2 <= lastPart.length <= 3 or lastPart in longTlds + + # Allow IPv4 addresses + return true if /^(\d{1,3}\.){3}\d{1,3}$/.test hostName + + # Fallback: no URL return false # Creates a search URL from the given :query. createSearchUrl: (query) -> - # we need to escape explictely to encode characters like "+" correctly + # Escape explicitly to encode characters like "+" correctly "http://www.google.com/search?q=" + encodeURIComponent(query) - # Converts :string into a google search if it's not already a URL. - # We don't bother with escaping characters as Chrome will do that for us. + # Converts :string into a Google search if it's not already a URL. We don't bother with escaping characters + # as Chrome will do that for us. convertToUrl: (string) -> string = string.trim() - # special-case about:[url] and view-source:[url] - if Utils.hasChromePrefix string then string + + # Special-case about:[url] and view-source:[url] + if Utils.hasChromePrefix string + string + else if Utils.isUrl string + Utils.createFullUrl string else - if (Utils.isUrl(string)) then Utils.createFullUrl(string) else Utils.createSearchUrl(string) + Utils.createSearchUrl string -# This creates a new function out of an existing function, where the new function takes fewer arguments. -# This allows us to pass around functions instead of functions + a partial list of arguments. -Function.prototype.curry = -> +# This creates a new function out of an existing function, where the new function takes fewer arguments. This +# allows us to pass around functions instead of functions + a partial list of arguments. +Function::curry = -> fixedArguments = Array.copy(arguments) fn = this -> fn.apply(this, fixedArguments.concat(Array.copy(arguments))) @@ -124,19 +118,7 @@ Array.copy = (array) -> Array.prototype.slice.call(array, 0) String::startsWith = (str) -> @indexOf(str) == 0 -# A very simple method for defining a new class (constructor and methods) using a single hash. -# No support for inheritance is included because we really shouldn't need it. -# TODO(philc): remove this. -Class = - extend: (properties) -> - newClass = -> - this.init.apply(this, arguments) if (this.init) - null - newClass.prototype = properties - newClass.constructor = newClass - newClass - -globalRoot = if window? then window else global +globalRoot = window ? global globalRoot.extend = (hash1, hash2) -> for key of hash2 hash1[key] = hash2[key] @@ -144,4 +126,3 @@ globalRoot.extend = (hash1, hash2) -> root = exports ? window root.Utils = Utils -root.Class = Class diff --git a/tests/dom_tests/dom_tests.coffee b/tests/dom_tests/dom_tests.coffee index f4e63270..3d981ee5 100644 --- a/tests/dom_tests/dom_tests.coffee +++ b/tests/dom_tests/dom_tests.coffee @@ -2,7 +2,7 @@ # Dispatching keyboard events via the DOM would require async tests, # which tend to be more complicated. Here we create mock events and # invoke the handlers directly. -# +# mockKeyboardEvent = (keyChar) -> event = {} event.charCode = (if keyCodes[keyChar] isnt undefined then keyCodes[keyChar] else keyChar.charCodeAt(0)) @@ -14,14 +14,14 @@ mockKeyboardEvent = (keyChar) -> # # Retrieve the hint markers as an array object. -# +# getHintMarkers = -> Array::slice.call document.getElementsByClassName("vimiumHintMarker"), 0 # # Generate tests that are common to both default and filtered # link hinting modes. -# +# createGeneralHintTests = (isFilteredMode) -> context "Link hints", @@ -125,7 +125,8 @@ context "Filtered link hints", context "Image hints", setup -> - testContent = "<a><img alt='alt text'/></a>" + "<a><img alt='alt text' title='some title'/></a>" + "<a><img title='some title'/></a>" + "<a><img src='' width='320px' height='100px'/></a>" + testContent = "<a><img alt='alt text'/></a><a><img alt='alt text' title='some title'/></a> + <a><img title='some title'/></a>" + "<a><img src='' width='320px' height='100px'/></a>" document.getElementById("test-div").innerHTML = testContent LinkHints.activateMode() @@ -143,7 +144,10 @@ context "Filtered link hints", context "Input hints", setup -> - testContent = "<input type='text' value='some value'/>" + "<input type='password' value='some value'/>" + "<textarea>some text</textarea>" + "<label for='test-input'/>a label</label><input type='text' id='test-input' value='some value'/>" + "<label for='test-input-2'/>a label: </label><input type='text' id='test-input-2' value='some value'/>" + testContent = "<input type='text' value='some value'/><input type='password' value='some value'/> + <textarea>some text</textarea><label for='test-input'/>a label</label> + <input type='text' id='test-input' value='some value'/> + <label for='test-input-2'/>a label: </label><input type='text' id='test-input-2' value='some value'/>" document.getElementById("test-div").innerHTML = testContent LinkHints.activateMode() @@ -162,7 +166,8 @@ context "Filtered link hints", context "Input focus", setup -> - testContent = "<input type='text' id='first'/>" + "<input style='display:none;' id='second'/>" + "<input type='password' id='third' value='some value'/>" + testContent = "<input type='text' id='first'/><input style='display:none;' id='second'/> + <input type='password' id='third' value='some value'/>" document.getElementById("test-div").innerHTML = testContent tearDown -> @@ -183,7 +188,7 @@ Tests.outputMethod = (args...) -> # escape html newOutput = newOutput.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") # highlight the source of the error - newOutput = newOutput.replace(/\/([^:/]+):([0-9]+):([0-9]+)/, "/<span class='errorPosition'>$1:$2</span>:$3") + newOutput = newOutput.replace /\/([^:/]+):([0-9]+):([0-9]+)/, "/<span class='errorPosition'>$1:$2</span>:$3" document.getElementById("output-div").innerHTML += "<div class='output-section'>" + newOutput + "</div>" console.log.apply console, args diff --git a/tests/unit_tests/utils_test.coffee b/tests/unit_tests/utils_test.coffee index 6a44b460..9da5ff65 100644 --- a/tests/unit_tests/utils_test.coffee +++ b/tests/unit_tests/utils_test.coffee @@ -1,6 +1,28 @@ require "./test_helper.js" extend(global, require "../../lib/utils.js") +context "isUrl", + should "accept valid URLs", -> + assert.isTrue Utils.isUrl "www.google.com" + assert.isTrue Utils.isUrl "www.bbc.co.uk" + assert.isTrue Utils.isUrl "yahoo.com" + assert.isTrue Utils.isUrl "nunames.nu" + assert.isTrue Utils.isUrl "user:pass@ftp.xyz.com/test" + + assert.isTrue Utils.isUrl "localhost/index.html" + assert.isTrue Utils.isUrl "127.0.0.1:8192/test.php" + + # IPv6 + assert.isTrue Utils.isUrl "[::]:9000" + + # Long TLDs + assert.isTrue Utils.isUrl "illinois.state.museum" + assert.isTrue Utils.isUrl "eqt5g4fuenphqinx.onion" + + should "reject invalid URLs", -> + assert.isFalse Utils.isUrl "a.x" + assert.isFalse Utils.isUrl "www-domain-tld" + context "convertToUrl", should "detect and clean up valid URLs", -> assert.equal "http://www.google.com/", Utils.convertToUrl("http://www.google.com/") |
