]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Merge pull request #461 from lydell/scroll
[VimFx.git] / extension / lib / utils.coffee
1 ###
2 # Copyright Anton Khodakivskiy 2012, 2013, 2014.
3 # Copyright Simon Lydell 2013, 2014.
4 # Copyright Wang Zhuochun 2013.
5 #
6 # This file is part of VimFx.
7 #
8 # VimFx is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # VimFx is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with VimFx. If not, see <http://www.gnu.org/licenses/>.
20 ###
21
22 notation = require('vim-like-key-notation')
23 { getPref
24 , setPref
25 } = require('./prefs')
26
27 ADDON_ID = 'VimFx@akhodakivskiy.github.com'
28
29 { classes: Cc, interfaces: Ci, utils: Cu } = Components
30
31 Window = Ci.nsIDOMWindow
32 ChromeWindow = Ci.nsIDOMChromeWindow
33 Element = Ci.nsIDOMElement
34 HTMLDocument = Ci.nsIDOMHTMLDocument
35 HTMLAnchorElement = Ci.nsIDOMHTMLAnchorElement
36 HTMLButtonElement = Ci.nsIDOMHTMLButtonElement
37 HTMLInputElement = Ci.nsIDOMHTMLInputElement
38 HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement
39 HTMLSelectElement = Ci.nsIDOMHTMLSelectElement
40 XULDocument = Ci.nsIDOMXULDocument
41 XULButtonElement = Ci.nsIDOMXULButtonElement
42 XULControlElement = Ci.nsIDOMXULControlElement
43 XULMenuListElement = Ci.nsIDOMXULMenuListElement
44 XULTextBoxElement = Ci.nsIDOMXULTextBoxElement
45
46 class Bucket
47 constructor: (@newFunc) ->
48 @bucket = new WeakMap()
49
50 get: (obj) ->
51 if @bucket.has(obj)
52 return @bucket.get(obj)
53 else
54 value = @newFunc(obj)
55 @bucket.set(obj, value)
56 return value
57
58 forget: (obj) ->
59 @bucket.delete(obj)
60
61 getEventWindow = (event) ->
62 if event.originalTarget instanceof Window
63 return event.originalTarget
64 else
65 doc = event.originalTarget.ownerDocument or event.originalTarget
66 if doc instanceof HTMLDocument or doc instanceof XULDocument
67 return doc.defaultView
68
69 getEventRootWindow = (event) ->
70 return unless window = getEventWindow(event)
71 return getRootWindow(window)
72
73 getEventCurrentTabWindow = (event) ->
74 return unless rootWindow = getEventRootWindow(event)
75 return getCurrentTabWindow(rootWindow)
76
77 getRootWindow = (window) ->
78 return window
79 .QueryInterface(Ci.nsIInterfaceRequestor)
80 .getInterface(Ci.nsIWebNavigation)
81 .QueryInterface(Ci.nsIDocShellTreeItem)
82 .rootTreeItem
83 .QueryInterface(Ci.nsIInterfaceRequestor)
84 .getInterface(Window)
85
86 getCurrentTabWindow = (window) ->
87 return window.gBrowser.selectedTab.linkedBrowser.contentWindow
88
89 blurActiveElement = (window) ->
90 # Only blur focusable elements, in order to interfere with the browser as
91 # little as possible.
92 { activeElement } = window.document
93 if activeElement and activeElement.tabIndex > -1
94 activeElement.blur()
95
96 isProperLink = (element) ->
97 # `.getAttribute` is used below instead of `.hasAttribute` to exclude `<a
98 # href="">`s used as buttons on some sites.
99 return element.getAttribute('href') and
100 (element instanceof HTMLAnchorElement or
101 element.ownerDocument instanceof XULDocument) and
102 not element.href.endsWith('#') and
103 not element.href.startsWith('javascript:')
104
105 isTextInputElement = (element) ->
106 return (element instanceof HTMLInputElement and element.type in [
107 'text', 'search', 'tel', 'url', 'email', 'password', 'number'
108 ]) or
109 element instanceof HTMLTextAreaElement or
110 # `<select>` elements can also receive text input: You may type the
111 # text of an item to select it.
112 element instanceof HTMLSelectElement or
113 element instanceof XULMenuListElement or
114 element instanceof XULTextBoxElement
115
116 isContentEditable = (element) ->
117 return element.isContentEditable or
118 isGoogleEditable(element)
119
120 isGoogleEditable = (element) ->
121 # `g_editable` is a non-standard attribute commonly used by Google.
122 return element.getAttribute?('g_editable') == 'true' or
123 element.ownerDocument.body?.getAttribute('g_editable') == 'true'
124
125 isActivatable = (element) ->
126 return element instanceof HTMLAnchorElement or
127 element instanceof HTMLButtonElement or
128 (element instanceof HTMLInputElement and element.type in [
129 'button', 'submit', 'reset', 'image'
130 ]) or
131 element instanceof XULButtonElement
132
133 isAdjustable = (element) ->
134 return element instanceof HTMLInputElement and element.type in [
135 'checkbox', 'radio', 'file', 'color'
136 'date', 'time', 'datetime', 'datetime-local', 'month', 'week'
137 ] or
138 element instanceof XULControlElement or
139 # Youtube special case.
140 element.classList?.contains('html5-video-player') or
141 element.classList?.contains('ytp-button')
142
143 area = (element) ->
144 return element.clientWidth * element.clientHeight
145
146 getSessionStore = ->
147 Cc['@mozilla.org/browser/sessionstore;1'].getService(Ci.nsISessionStore)
148
149 loadCss = do ->
150 sss = Cc['@mozilla.org/content/style-sheet-service;1']
151 .getService(Ci.nsIStyleSheetService)
152 return (name) ->
153 uri = getResourceURI("resources/#{ name }.css")
154 # `AGENT_SHEET` is used to override userContent.css and Stylish. Custom
155 # website themes installed by users often make the hint markers unreadable,
156 # for example. Just using `!important` in the CSS is not enough.
157 unless sss.sheetRegistered(uri, sss.AGENT_SHEET)
158 sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET)
159
160 module.onShutdown(->
161 sss.unregisterSheet(uri, sss.AGENT_SHEET)
162 )
163
164 # Store events that we’ve simulated. A `WeakMap` is used in order not to leak
165 # memory. This approach is better than for example setting `event.simulated =
166 # true`, since that tells the sites that the click was simulated, and allows
167 # sites to spoof it.
168 simulated_events = new WeakMap()
169
170 # Simulate mouse click with a full chain of events. ('command' is for XUL
171 # elements.)
172 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click', 'command']
173 simulateClick = (element) ->
174 window = element.ownerDocument.defaultView
175 for type in eventSequence
176 mouseEvent = new window.MouseEvent(type, {
177 # Let the event bubble in order to trigger delegated event listeners.
178 bubbles: true
179 # Make the event cancelable so that `<a href="#">` can be used as a
180 # JavaScript-powered button without scrolling to the top of the page.
181 cancelable: true
182 })
183 element.dispatchEvent(mouseEvent)
184
185 isEventSimulated = (event) ->
186 return simulated_events.has(event)
187
188 # Write a string to the system clipboard.
189 writeToClipboard = (text) ->
190 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
191 .getService(Ci.nsIClipboardHelper)
192 clipboardHelper.copyString(text)
193
194 # Executes function `func` and measures how much time it took.
195 timeIt = (func, name) ->
196 console.time(name)
197 result = func()
198 console.timeEnd(name)
199 return result
200
201 isBlacklisted = (str) ->
202 matchingRules = getMatchingBlacklistRules(str)
203 return (matchingRules.length != 0)
204
205 # Returns all blacklisted keys in matching rules.
206 getBlacklistedKeys = (str) ->
207 matchingRules = getMatchingBlacklistRules(str)
208 blacklistedKeys = []
209 for rule in matchingRules when /##/.test(rule)
210 blacklistedKeys.push(x) for x in rule.split('##')[1].split('#')
211 return blacklistedKeys
212
213 # Returns all rules in the blacklist that match the provided string.
214 getMatchingBlacklistRules = (str) ->
215 return getBlacklist().filter((rule) ->
216 /// ^#{ simpleWildcards(rule.split('##')[0]) }$ ///i.test(str)
217 )
218
219 getBlacklist = ->
220 return splitListString(getPref('black_list'))
221
222 setBlacklist = (blacklist) ->
223 setPref('black_list', blacklist.join(','))
224
225 updateBlacklist = ({ add, remove } = {}) ->
226 blacklist = getBlacklist()
227
228 if add
229 blacklist.push(splitListString(add)...)
230
231 blacklist = blacklist.filter((rule) -> rule != '')
232 blacklist = removeDuplicates(blacklist)
233
234 if remove
235 for rule in splitListString(remove) when rule in blacklist
236 blacklist.splice(blacklist.indexOf(rule), 1)
237
238 setBlacklist(blacklist)
239
240 # Splits a comma/space separated list into an array.
241 splitListString = (str) ->
242 return str.split(/\s*,[\s,]*/)
243
244 # Prepares a string to be used in a regexp, where "*" matches zero or more
245 # characters and "!" matches one character.
246 simpleWildcards = (string) ->
247 return regexpEscape(string).replace(/\\\*/g, '.*').replace(/!/g, '.')
248
249 # Returns the first element that matches a pattern, favoring earlier patterns.
250 # The patterns are case insensitive `simpleWildcards`s and must match either in
251 # the beginning or at the end of a string. Moreover, a pattern does not match
252 # in the middle of words, so "previous" does not match "previously". If that is
253 # desired, a pattern such as "previous*" can be used instead. Note: We cannot
254 # use `\b` word boundaries, because they don’t work well with non-English
255 # characters. Instead we match a space as word boundary. Therefore we normalize
256 # the whitespace and add spaces at the edges of the element text.
257 getBestPatternMatch = (patterns, attrs, elements) ->
258 regexps = []
259 for pattern in patterns
260 wildcarded = simpleWildcards(pattern)
261 regexps.push(/// ^\s(?:#{ wildcarded })\s | \s(?:#{ wildcarded })\s$ ///i)
262
263 # Helper function that matches a string against all the patterns.
264 matches = (text) ->
265 normalizedText = " #{ text } ".replace(/\s+/g, ' ')
266 for re in regexps
267 if re.test(normalizedText)
268 return true
269 return false
270
271 # First search in attributes (favoring earlier attributes) as it's likely
272 # that they are more specific than text contexts.
273 for attr in attrs
274 for element in elements
275 if matches(element.getAttribute(attr))
276 return element
277
278 # Then search in element contents.
279 for element in elements
280 if matches(element.textContent)
281 return element
282
283 return null
284
285 # Get VimFx verion. AddonManager only provides async API to access addon data,
286 # so it's a bit tricky...
287 getVersion = do ->
288 version = null
289
290 scope = {}
291 Cu.import('resource://gre/modules/AddonManager.jsm', scope)
292 scope.AddonManager.getAddonByID(ADDON_ID, (addon) -> version = addon.version)
293
294 return ->
295 return version
296
297 parseHTML = (document, html) ->
298 parser = Cc['@mozilla.org/parserutils;1'].getService(Ci.nsIParserUtils)
299 flags = parser.SanitizerAllowStyle
300 return parser.parseFragment(html, flags, false, null,
301 document.documentElement)
302
303 escapeHTML = (s) ->
304 return s
305 .replace(/&/g, '&amp;')
306 .replace(/</g, '&lt;')
307 .replace(/>/g, '&gt;')
308 .replace(/"/g, '&quot;')
309 .replace(/'/g, '&apos;')
310
311 createElement = (document, type, attributes = {}) ->
312 element = document.createElement(type)
313
314 for attribute, value of attributes
315 element.setAttribute(attribute, value)
316
317 if document instanceof HTMLDocument
318 element.classList.add('VimFxReset')
319
320 return element
321
322 isURL = (str) ->
323 try
324 url = Cc['@mozilla.org/network/io-service;1']
325 .getService(Ci.nsIIOService)
326 .newURI(str, null, null)
327 .QueryInterface(Ci.nsIURL)
328 return true
329 catch err
330 return false
331
332 # Use Firefox services to search for a given string.
333 browserSearchSubmission = (str) ->
334 ss = Cc['@mozilla.org/browser/search-service;1']
335 .getService(Ci.nsIBrowserSearchService)
336
337 engine = ss.currentEngine or ss.defaultEngine
338 return engine.getSubmission(str, null)
339
340 openTab = (rootWindow, url, options) ->
341 { gBrowser } = rootWindow
342 rootWindow.TreeStyleTabService?.readyToOpenChildTab(gBrowser.selectedTab)
343 gBrowser.loadOneTab(url, options)
344
345 normalizedKey = (key) -> key.map(notation.normalize).join('')
346
347 # Get hint characters, convert them to lower case, and filter duplicates.
348 getHintChars = ->
349 hintChars = getPref('hint_chars')
350 # Make sure that hint chars contain at least two characters.
351 if not hintChars or hintChars.length < 2
352 hintChars = 'fj'
353
354 return removeDuplicateCharacters(hintChars)
355
356 # Remove duplicate characters from string (case insensitive).
357 removeDuplicateCharacters = (str) ->
358 return removeDuplicates( str.toLowerCase().split('') ).join('')
359
360 # Return URI to some file in the extension packaged as resource.
361 getResourceURI = do ->
362 baseURI = Services.io.newURI(__SCRIPT_URI_SPEC__, null, null)
363 return (path) -> return Services.io.newURI(path, null, baseURI)
364
365 # Escape a string to render it usable in regular expressions.
366 regexpEscape = (s) -> s and s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
367
368 removeDuplicates = (array) ->
369 # coffeelint: disable=no_backticks
370 return `[...new Set(array)]`
371 # coffeelint: enable=no_backticks
372
373 exports.Bucket = Bucket
374 exports.getEventWindow = getEventWindow
375 exports.getEventRootWindow = getEventRootWindow
376 exports.getEventCurrentTabWindow = getEventCurrentTabWindow
377 exports.getRootWindow = getRootWindow
378 exports.getCurrentTabWindow = getCurrentTabWindow
379
380 exports.blurActiveElement = blurActiveElement
381 exports.isProperLink = isProperLink
382 exports.isTextInputElement = isTextInputElement
383 exports.isContentEditable = isContentEditable
384 exports.isActivatable = isActivatable
385 exports.isAdjustable = isAdjustable
386 exports.area = area
387 exports.getSessionStore = getSessionStore
388
389 exports.loadCss = loadCss
390
391 exports.simulateClick = simulateClick
392 exports.isEventSimulated = isEventSimulated
393 exports.writeToClipboard = writeToClipboard
394 exports.timeIt = timeIt
395
396 exports.getMatchingBlacklistRules = getMatchingBlacklistRules
397 exports.isBlacklisted = isBlacklisted
398 exports.getBlacklistedKeys = getBlacklistedKeys
399 exports.updateBlacklist = updateBlacklist
400 exports.splitListString = splitListString
401 exports.getBestPatternMatch = getBestPatternMatch
402
403 exports.getVersion = getVersion
404 exports.parseHTML = parseHTML
405 exports.escapeHTML = escapeHTML
406 exports.createElement = createElement
407 exports.isURL = isURL
408 exports.browserSearchSubmission = browserSearchSubmission
409 exports.openTab = openTab
410 exports.normalizedKey = normalizedKey
411 exports.getHintChars = getHintChars
412 exports.removeDuplicates = removeDuplicates
413 exports.removeDuplicateCharacters = removeDuplicateCharacters
414 exports.getResourceURI = getResourceURI
415 exports.ADDON_ID = ADDON_ID
Imprint / Impressum