]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Merge branch 'blacklist-keys' of https://github.com/harsh1618/VimFx into harsh1618...
[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 { getPref
23 , setPref
24 } = require('./prefs')
25
26 ADDON_ID = 'VimFx@akhodakivskiy.github.com'
27
28 { classes: Cc, interfaces: Ci, utils: Cu } = Components
29
30 HTMLInputElement = Ci.nsIDOMHTMLInputElement
31 HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement
32 HTMLSelectElement = Ci.nsIDOMHTMLSelectElement
33 XULMenuListElement = Ci.nsIDOMXULMenuListElement
34 XULDocument = Ci.nsIDOMXULDocument
35 XULElement = Ci.nsIDOMXULElement
36 XPathResult = Ci.nsIDOMXPathResult
37 HTMLDocument = Ci.nsIDOMHTMLDocument
38 HTMLElement = Ci.nsIDOMHTMLElement
39 Window = Ci.nsIDOMWindow
40 ChromeWindow = Ci.nsIDOMChromeWindow
41
42 class Bucket
43 constructor: (@newFunc) ->
44 @bucket = new WeakMap()
45
46 get: (obj) ->
47 if @bucket.has(obj)
48 return @bucket.get(obj)
49 else
50 return @bucket.set(obj, @newFunc(obj))
51
52 forget: (obj) ->
53 @bucket.delete(obj)
54
55 getEventWindow = (event) ->
56 if event.originalTarget instanceof Window
57 return event.originalTarget
58 else
59 doc = event.originalTarget.ownerDocument or event.originalTarget
60 if doc instanceof HTMLDocument or doc instanceof XULDocument
61 return doc.defaultView
62
63 getEventRootWindow = (event) ->
64 return unless window = getEventWindow(event)
65 return getRootWindow(window)
66
67 getEventCurrentTabWindow = (event) ->
68 return unless rootWindow = getEventRootWindow(event)
69 return getCurrentTabWindow(rootWindow)
70
71 getRootWindow = (window) ->
72 return window
73 .QueryInterface(Ci.nsIInterfaceRequestor)
74 .getInterface(Ci.nsIWebNavigation)
75 .QueryInterface(Ci.nsIDocShellTreeItem)
76 .rootTreeItem
77 .QueryInterface(Ci.nsIInterfaceRequestor)
78 .getInterface(Window)
79
80 getCurrentTabWindow = (window) ->
81 return window.gBrowser.selectedTab.linkedBrowser.contentWindow
82
83 blurActiveElement = (window) ->
84 # Only blur editable elements, in order to interfere with the browser as
85 # little as possible.
86 { activeElement } = window.document
87 if activeElement and isElementEditable(activeElement)
88 activeElement.blur()
89
90 isTextInputElement = (element) ->
91 return element instanceof HTMLInputElement or
92 element instanceof HTMLTextAreaElement
93
94 isElementEditable = (element) ->
95 return element.isContentEditable or
96 element instanceof HTMLInputElement or
97 element instanceof HTMLTextAreaElement or
98 element instanceof HTMLSelectElement or
99 element instanceof XULMenuListElement or
100 element.isContentEditable or
101 isElementGoogleEditable(element)
102
103 isElementGoogleEditable = (element) ->
104 # `g_editable` is a non-standard attribute commonly used by Google.
105 return element.getAttribute?('g_editable') == 'true' or
106 (element instanceof HTMLElement and
107 element.ownerDocument.body?.getAttribute('g_editable') == 'true')
108
109 isElementVisible = (element) ->
110 document = element.ownerDocument
111 window = document.defaultView
112 computedStyle = window.getComputedStyle(element, null)
113 return computedStyle.getPropertyValue('visibility') == 'visible' and
114 computedStyle.getPropertyValue('display') != 'none' and
115 computedStyle.getPropertyValue('opacity') != '0'
116
117 getSessionStore = ->
118 Cc['@mozilla.org/browser/sessionstore;1'].getService(Ci.nsISessionStore)
119
120 loadCss = do ->
121 sss = Cc['@mozilla.org/content/style-sheet-service;1']
122 .getService(Ci.nsIStyleSheetService)
123 return (name) ->
124 uri = getResourceURI("resources/#{ name }.css")
125 # `AGENT_SHEET` is used to override userContent.css and Stylish. Custom
126 # website themes installed by users often make the hint markers unreadable,
127 # for example. Just using `!important` in the CSS is not enough.
128 unless sss.sheetRegistered(uri, sss.AGENT_SHEET)
129 sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET)
130
131 module.onShutdown(->
132 sss.unregisterSheet(uri, sss.AGENT_SHEET)
133 )
134
135 # Store events that we’ve simulated. A `WeakMap` is used in order not to leak
136 # memory. This approach is better than for example setting `event.simulated =
137 # true`, since that tells the sites that the click was simulated, and allows
138 # sites to spoof it.
139 simulated_events = new WeakMap()
140
141 # Simulate mouse click with a full chain of events. Copied from Vimium
142 # codebase.
143 simulateClick = (element, modifiers = {}) ->
144 document = element.ownerDocument
145 window = document.defaultView
146
147 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click']
148 for event in eventSequence
149 mouseEvent = document.createEvent('MouseEvents')
150 mouseEvent.initMouseEvent(
151 event, true, true, window, 1, 0, 0, 0, 0,
152 modifiers.ctrlKey, false, false, modifiers.metaKey,
153 0, null
154 )
155 simulated_events.set(mouseEvent, true)
156 # Debugging note: Firefox will not execute the element's default action if
157 # we dispatch this click event, but Webkit will. Dispatching a click on an
158 # input box does not seem to focus it; we do that separately.
159 element.dispatchEvent(mouseEvent)
160
161 isEventSimulated = (event) ->
162 return simulated_events.has(event)
163
164 WHEEL_MODE_PIXEL = Ci.nsIDOMWheelEvent.DOM_DELTA_PIXEL
165 WHEEL_MODE_LINE = Ci.nsIDOMWheelEvent.DOM_DELTA_LINE
166 WHEEL_MODE_PAGE = Ci.nsIDOMWheelEvent.DOM_DELTA_PAGE
167
168 # Simulate mouse scroll event by specific offsets given that mouse cursor is at
169 # specified position.
170 simulateWheel = (window, deltaX, deltaY, mode = WHEEL_MODE_PIXEL) ->
171 windowUtils = window
172 .QueryInterface(Ci.nsIInterfaceRequestor)
173 .getInterface(Ci.nsIDOMWindowUtils)
174
175 [pX, pY] = [window.innerWidth / 2, window.innerHeight / 2]
176 windowUtils.sendWheelEvent(
177 pX, pY, # Window offset (x, y) in pixels.
178 deltaX, deltaY, 0, # Deltas (x, y, z).
179 mode, # Mode (pixel, line, page).
180 0, # Key Modifiers.
181 0, 0, # Line or Page deltas (x, y).
182 0 # Options.
183 )
184
185 # Write a string to the system clipboard.
186 writeToClipboard = (text) ->
187 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
188 .getService(Ci.nsIClipboardHelper)
189 clipboardHelper.copyString(text)
190
191 # Read the system clipboard.
192 readFromClipboard = (window) ->
193 trans = Cc['@mozilla.org/widget/transferable;1']
194 .createInstance(Ci.nsITransferable)
195
196 if trans.init
197 privacyContext = window
198 .QueryInterface(Ci.nsIInterfaceRequestor)
199 .getInterface(Ci.nsIWebNavigation)
200 .QueryInterface(Ci.nsILoadContext)
201 trans.init(privacyContext)
202
203 trans.addDataFlavor('text/unicode')
204
205 clip = Cc['@mozilla.org/widget/clipboard;1'].getService(Ci.nsIClipboard)
206 clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard)
207
208 str = {}
209 strLength = {}
210
211 trans.getTransferData('text/unicode', str, strLength)
212
213 if str
214 str = str.value.QueryInterface(Ci.nsISupportsString)
215 return str.data.substring(0, strLength.value / 2)
216
217 return undefined
218
219 # Executes function `func` and mearues how much time it took.
220 timeIt = (func, name) ->
221 console.time(name)
222 result = func()
223 console.timeEnd(name)
224 return result
225
226 isBlacklisted = (str) ->
227 matchingRules = getMatchingBlacklistRules(str)
228 return (matchingRules.length != 0)
229
230 # Returns all blacklisted keys in matching rules.
231 getBlacklistedKeys = (str) ->
232 matchingRules = getMatchingBlacklistRules(str)
233 blacklistedKeys = []
234 for rule in matchingRules when /##/.test(rule)
235 blacklistedKeys.push(x) for x in rule.split('##')[1].split('#')
236 return blacklistedKeys
237
238 # Returns all rules in the blacklist that match the provided string.
239 getMatchingBlacklistRules = (str) ->
240 return getBlacklist().filter((rule) ->
241 /// ^#{ simpleWildcards(rule.split('##')[0]) }$ ///i.test(str)
242 )
243
244 getBlacklist = ->
245 return splitListString(getPref('black_list'))
246
247 setBlacklist = (blacklist) ->
248 setPref('black_list', blacklist.join(','))
249
250 updateBlacklist = ({ add, remove } = {}) ->
251 blacklist = getBlacklist()
252
253 if add
254 blacklist.push(splitListString(add)...)
255
256 blacklist = blacklist.filter((rule) -> rule != '')
257 blacklist = removeDuplicates(blacklist)
258
259 if remove
260 for rule in splitListString(remove) when rule in blacklist
261 blacklist.splice(blacklist.indexOf(rule), 1)
262
263 setBlacklist(blacklist)
264
265 # Splits a comma/space separated list into an array.
266 splitListString = (str) ->
267 return str.split(/\s*,[\s,]*/)
268
269 # Prepares a string to be used in a regexp, where "*" matches zero or more
270 # characters and "!" matches one character.
271 simpleWildcards = (string) ->
272 return regexpEscape(string).replace(/\\\*/g, '.*').replace(/!/g, '.')
273
274 # Returns the first element that matches a pattern, favoring earlier patterns.
275 # The patterns are case insensitive `simpleWildcards`s and must match either in
276 # the beginning or at the end of a string. Moreover, a pattern does not match
277 # in the middle of words, so "previous" does not match "previously". If that is
278 # desired, a pattern such as "previous*" can be used instead. Note: We cannot
279 # use `\b` word boundaries, because they don’t work well with non-English
280 # characters. Instead we match a space as word boundary. Therefore we normalize
281 # the whitespace and add spaces at the edges of the element text.
282 getBestPatternMatch = (patterns, attrs, elements) ->
283 regexps = []
284 for pattern in patterns
285 wildcarded = simpleWildcards(pattern)
286 regexps.push(/// ^\s(?:#{ wildcarded })\s | \s(?:#{ wildcarded })\s$ ///i)
287
288 # Helper function that matches a string against all the patterns.
289 matches = (text) ->
290 normalizedText = " #{ text } ".replace(/\s+/g, ' ')
291 for re in regexps
292 if re.test(normalizedText)
293 return true
294 return false
295
296 # First search in attributes (favoring earlier attributes) as it's likely
297 # that they are more specific than text contexts.
298 for attr in attrs
299 for element in elements
300 if matches(element.getAttribute(attr))
301 return element
302
303 # Then search in element contents.
304 for element in elements
305 if matches(element.textContent)
306 return element
307
308 return null
309
310 # Get VimFx verion. AddonManager only provides async API to access addon data,
311 # so it's a bit tricky...
312 getVersion = do ->
313 version = null
314
315 scope = {}
316 Cu.import('resource://gre/modules/AddonManager.jsm', scope)
317 scope.AddonManager.getAddonByID(ADDON_ID, (addon) -> version = addon.version)
318
319 return ->
320 return version
321
322 parseHTML = (document, html) ->
323 parser = Cc['@mozilla.org/parserutils;1'].getService(Ci.nsIParserUtils)
324 flags = parser.SanitizerAllowStyle
325 return parser.parseFragment(html, flags, false, null,
326 document.documentElement)
327
328 createElement = (document, type, attributes = {}) ->
329 element = document.createElement(type)
330
331 for attribute, value of attributes
332 element.setAttribute(attribute, value)
333
334 if document instanceof HTMLDocument
335 element.classList.add('VimFxReset')
336
337 return element
338
339 isURL = (str) ->
340 try
341 url = Cc['@mozilla.org/network/io-service;1']
342 .getService(Ci.nsIIOService)
343 .newURI(str, null, null)
344 .QueryInterface(Ci.nsIURL)
345 return true
346 catch err
347 return false
348
349 # Use Firefox services to search for a given string.
350 browserSearchSubmission = (str) ->
351 ss = Cc['@mozilla.org/browser/search-service;1']
352 .getService(Ci.nsIBrowserSearchService)
353
354 engine = ss.currentEngine or ss.defaultEngine
355 return engine.getSubmission(str, null)
356
357 # Get hint characters, convert them to lower case, and filter duplicates.
358 getHintChars = ->
359 hintChars = getPref('hint_chars')
360 # Make sure that hint chars contain at least two characters.
361 if not hintChars or hintChars.length < 2
362 hintChars = 'fj'
363
364 return removeDuplicateCharacters(hintChars)
365
366 # Remove duplicate characters from string (case insensitive).
367 removeDuplicateCharacters = (str) ->
368 return removeDuplicates( str.toLowerCase().split('') ).join('')
369
370 # Return URI to some file in the extension packaged as resource.
371 getResourceURI = do ->
372 baseURI = Services.io.newURI(__SCRIPT_URI_SPEC__, null, null)
373 return (path) -> return Services.io.newURI(path, null, baseURI)
374
375 # Escape a string to render it usable in regular expressions.
376 regexpEscape = (s) -> s and s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
377
378 removeDuplicates = (array) ->
379 # coffeelint: disable=no_backticks
380 return `[...new Set(array)]`
381 # coffeelint: enable=no_backticks
382
383 # Why isn’t `a[@href]` used, when `area[@href]` is? Some sites (such as
384 # StackExchange sites) leave out the `href` property and use the anchor as a
385 # JavaScript-powered button (instead of just using the `button` element).
386 ACTION_ELEMENT_TAGS = [
387 'a'
388 'area[@href]'
389 'button'
390 # When viewing an image directly, and it is larger than the viewport,
391 # clicking it toggles zoom.
392 'img[contains(@class, "decoded") and
393 (contains(@class, "overflowing") or
394 contains(@class, "shrinkToFit"))]'
395 ]
396
397 ACTION_ELEMENT_PROPERTIES = [
398 '@onclick'
399 '@onmousedown'
400 '@onmouseup'
401 '@oncommand'
402 '@role="link"'
403 '@role="button"'
404 'contains(@class, "button")'
405 'contains(@class, "js-new-tweets-bar")'
406 ]
407
408 EDITABLE_ELEMENT_TAGS = [
409 'textarea'
410 'select'
411 'input[not(@type="hidden" or @disabled)]'
412 ]
413
414 EDITABLE_ELEMENT_PROPERTIES = [
415 '@contenteditable=""'
416 'translate(@contenteditable, "TRUE", "true")="true"'
417 ]
418
419 FOCUSABLE_ELEMENT_TAGS = [
420 'frame'
421 'iframe'
422 'embed'
423 'object'
424 ]
425
426 FOCUSABLE_ELEMENT_PROPERTIES = [
427 '@tabindex!=-1'
428 ]
429
430 getMarkableElements = do ->
431 xpathify = (tags, properties) ->
432 return tags
433 .concat("*[#{ properties.join(' or ') }]")
434 .map((rule) -> "//#{ rule } | //xhtml:#{ rule }")
435 .join(' | ')
436
437 xpaths =
438 action: xpathify(ACTION_ELEMENT_TAGS, ACTION_ELEMENT_PROPERTIES )
439 editable: xpathify(EDITABLE_ELEMENT_TAGS, EDITABLE_ELEMENT_PROPERTIES )
440 focusable: xpathify(FOCUSABLE_ELEMENT_TAGS, FOCUSABLE_ELEMENT_PROPERTIES)
441 all: xpathify(
442 # coffeelint: disable=max_line_length
443 [ACTION_ELEMENT_TAGS..., EDITABLE_ELEMENT_TAGS..., FOCUSABLE_ELEMENT_TAGS... ],
444 [ACTION_ELEMENT_PROPERTIES..., EDITABLE_ELEMENT_PROPERTIES..., FOCUSABLE_ELEMENT_PROPERTIES...]
445 # coffeelint: enable=max_line_length
446 )
447
448 # The actual function that will return the desired elements.
449 return (document, { type }) ->
450 return xpathQueryAll(document, xpaths[type])
451
452 xpathHelper = (node, query, resultType) ->
453 document = node.ownerDocument ? node
454 namespaceResolver = (namespace) ->
455 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
456 return document.evaluate(query, node, namespaceResolver, resultType, null)
457
458 xpathQuery = (node, query) ->
459 result = xpathHelper(node, query, XPathResult.FIRST_ORDERED_NODE_TYPE)
460 return result.singleNodeValue
461
462 xpathQueryAll = (node, query) ->
463 result = xpathHelper(node, query, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
464 return (result.snapshotItem(i) for i in [0...result.snapshotLength] by 1)
465
466
467 exports.Bucket = Bucket
468 exports.getEventWindow = getEventWindow
469 exports.getEventRootWindow = getEventRootWindow
470 exports.getEventCurrentTabWindow = getEventCurrentTabWindow
471 exports.getRootWindow = getRootWindow
472 exports.getCurrentTabWindow = getCurrentTabWindow
473
474 exports.blurActiveElement = blurActiveElement
475 exports.isTextInputElement = isTextInputElement
476 exports.isElementEditable = isElementEditable
477 exports.isElementVisible = isElementVisible
478 exports.getSessionStore = getSessionStore
479
480 exports.loadCss = loadCss
481
482 exports.simulateClick = simulateClick
483 exports.isEventSimulated = isEventSimulated
484 exports.simulateWheel = simulateWheel
485 exports.WHEEL_MODE_PIXEL = WHEEL_MODE_PIXEL
486 exports.WHEEL_MODE_LINE = WHEEL_MODE_LINE
487 exports.WHEEL_MODE_PAGE = WHEEL_MODE_PAGE
488 exports.readFromClipboard = readFromClipboard
489 exports.writeToClipboard = writeToClipboard
490 exports.timeIt = timeIt
491
492 exports.getMatchingBlacklistRules = getMatchingBlacklistRules
493 exports.isBlacklisted = isBlacklisted
494 exports.getBlacklistedKeys = getBlacklistedKeys
495 exports.updateBlacklist = updateBlacklist
496 exports.splitListString = splitListString
497 exports.getBestPatternMatch = getBestPatternMatch
498
499 exports.getVersion = getVersion
500 exports.parseHTML = parseHTML
501 exports.createElement = createElement
502 exports.isURL = isURL
503 exports.browserSearchSubmission = browserSearchSubmission
504 exports.getHintChars = getHintChars
505 exports.removeDuplicates = removeDuplicates
506 exports.removeDuplicateCharacters = removeDuplicateCharacters
507 exports.getResourceURI = getResourceURI
508 exports.getMarkableElements = getMarkableElements
509 exports.xpathQuery = xpathQuery
510 exports.xpathQueryAll = xpathQueryAll
511 exports.ADDON_ID = ADDON_ID
Imprint / Impressum