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