]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Configure coffeelint and fix lint errors
[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 rules in the blacklist that match the provided string.
231 getMatchingBlacklistRules = (str) ->
232 return getBlacklist().filter((rule) ->
233 /// ^#{ simpleWildcards(rule) }$ ///i.test(str)
234 )
235
236 getBlacklist = ->
237 return splitListString(getPref('black_list'))
238
239 setBlacklist = (blacklist) ->
240 setPref('black_list', blacklist.join(','))
241
242 updateBlacklist = ({ add, remove } = {}) ->
243 blacklist = getBlacklist()
244
245 if add
246 blacklist.push(splitListString(add)...)
247
248 blacklist = blacklist.filter((rule) -> rule != '')
249 blacklist = removeDuplicates(blacklist)
250
251 if remove
252 for rule in splitListString(remove) when rule in blacklist
253 blacklist.splice(blacklist.indexOf(rule), 1)
254
255 setBlacklist(blacklist)
256
257 # Splits a comma/space separated list into an array.
258 splitListString = (str) ->
259 return str.split(/\s*,[\s,]*/)
260
261 # Prepares a string to be used in a regexp, where "*" matches zero or more
262 # characters and "!" matches one character.
263 simpleWildcards = (string) ->
264 return regexpEscape(string).replace(/\\\*/g, '.*').replace(/!/g, '.')
265
266 # Returns the first element that matches a pattern, favoring earlier patterns.
267 # The patterns are case insensitive `simpleWildcards`s and must match either in
268 # the beginning or at the end of a string. Moreover, a pattern does not match
269 # in the middle of words, so "previous" does not match "previously". If that is
270 # desired, a pattern such as "previous*" can be used instead. Note: We cannot
271 # use `\b` word boundaries, because they don’t work well with non-English
272 # characters. Instead we match a space as word boundary. Therefore we normalize
273 # the whitespace and add spaces at the edges of the element text.
274 getBestPatternMatch = (patterns, attrs, elements) ->
275 regexps = []
276 for pattern in patterns
277 wildcarded = simpleWildcards(pattern)
278 regexps.push(/// ^\s(?:#{ wildcarded })\s | \s(?:#{ wildcarded })\s$ ///i)
279
280 # Helper function that matches a string against all the patterns.
281 matches = (text) ->
282 normalizedText = " #{ text } ".replace(/\s+/g, ' ')
283 for re in regexps
284 if re.test(normalizedText)
285 return true
286 return false
287
288 # First search in attributes (favoring earlier attributes) as it's likely
289 # that they are more specific than text contexts.
290 for attr in attrs
291 for element in elements
292 if matches(element.getAttribute(attr))
293 return element
294
295 # Then search in element contents.
296 for element in elements
297 if matches(element.textContent)
298 return element
299
300 return null
301
302 # Get VimFx verion. AddonManager only provides async API to access addon data,
303 # so it's a bit tricky...
304 getVersion = do ->
305 version = null
306
307 scope = {}
308 Cu.import('resource://gre/modules/AddonManager.jsm', scope)
309 scope.AddonManager.getAddonByID(ADDON_ID, (addon) -> version = addon.version)
310
311 return ->
312 return version
313
314 parseHTML = (document, html) ->
315 parser = Cc['@mozilla.org/parserutils;1'].getService(Ci.nsIParserUtils)
316 flags = parser.SanitizerAllowStyle
317 return parser.parseFragment(html, flags, false, null,
318 document.documentElement)
319
320 createElement = (document, type, attributes = {}) ->
321 element = document.createElement(type)
322
323 for attribute, value of attributes
324 element.setAttribute(attribute, value)
325
326 if document instanceof HTMLDocument
327 element.classList.add('VimFxReset')
328
329 return element
330
331 isURL = (str) ->
332 try
333 url = Cc['@mozilla.org/network/io-service;1']
334 .getService(Ci.nsIIOService)
335 .newURI(str, null, null)
336 .QueryInterface(Ci.nsIURL)
337 return true
338 catch err
339 return false
340
341 # Use Firefox services to search for a given string.
342 browserSearchSubmission = (str) ->
343 ss = Cc['@mozilla.org/browser/search-service;1']
344 .getService(Ci.nsIBrowserSearchService)
345
346 engine = ss.currentEngine or ss.defaultEngine
347 return engine.getSubmission(str, null)
348
349 # Get hint characters, convert them to lower case, and filter duplicates.
350 getHintChars = ->
351 hintChars = getPref('hint_chars')
352 # Make sure that hint chars contain at least two characters.
353 if not hintChars or hintChars.length < 2
354 hintChars = 'fj'
355
356 return removeDuplicateCharacters(hintChars)
357
358 # Remove duplicate characters from string (case insensitive).
359 removeDuplicateCharacters = (str) ->
360 return removeDuplicates( str.toLowerCase().split('') ).join('')
361
362 # Return URI to some file in the extension packaged as resource.
363 getResourceURI = do ->
364 baseURI = Services.io.newURI(__SCRIPT_URI_SPEC__, null, null)
365 return (path) -> return Services.io.newURI(path, null, baseURI)
366
367 # Escape a string to render it usable in regular expressions.
368 regexpEscape = (s) -> s and s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
369
370 removeDuplicates = (array) ->
371 # coffeelint: disable=no_backticks
372 return `[...new Set(array)]`
373 # coffeelint: enable=no_backticks
374
375 # Why isn’t `a[@href]` used, when `area[@href]` is? Some sites (such as
376 # StackExchange sites) leave out the `href` property and use the anchor as a
377 # JavaScript-powered button (instead of just using the `button` element).
378 ACTION_ELEMENT_TAGS = [
379 'a'
380 'area[@href]'
381 'button'
382 # When viewing an image directly, and it is larger than the viewport,
383 # clicking it toggles zoom.
384 'img[contains(@class, "decoded") and
385 (contains(@class, "overflowing") or
386 contains(@class, "shrinkToFit"))]'
387 ]
388
389 ACTION_ELEMENT_PROPERTIES = [
390 '@onclick'
391 '@onmousedown'
392 '@onmouseup'
393 '@oncommand'
394 '@role="link"'
395 '@role="button"'
396 'contains(@class, "button")'
397 'contains(@class, "js-new-tweets-bar")'
398 ]
399
400 EDITABLE_ELEMENT_TAGS = [
401 'textarea'
402 'select'
403 'input[not(@type="hidden" or @disabled)]'
404 ]
405
406 EDITABLE_ELEMENT_PROPERTIES = [
407 '@contenteditable=""'
408 'translate(@contenteditable, "TRUE", "true")="true"'
409 ]
410
411 FOCUSABLE_ELEMENT_TAGS = [
412 'frame'
413 'iframe'
414 'embed'
415 'object'
416 ]
417
418 FOCUSABLE_ELEMENT_PROPERTIES = [
419 '@tabindex!=-1'
420 ]
421
422 getMarkableElements = do ->
423 xpathify = (tags, properties) ->
424 return tags
425 .concat("*[#{ properties.join(' or ') }]")
426 .map((rule) -> "//#{ rule } | //xhtml:#{ rule }")
427 .join(' | ')
428
429 xpaths =
430 action: xpathify(ACTION_ELEMENT_TAGS, ACTION_ELEMENT_PROPERTIES )
431 editable: xpathify(EDITABLE_ELEMENT_TAGS, EDITABLE_ELEMENT_PROPERTIES )
432 focusable: xpathify(FOCUSABLE_ELEMENT_TAGS, FOCUSABLE_ELEMENT_PROPERTIES)
433 all: xpathify(
434 # coffeelint: disable=max_line_length
435 [ACTION_ELEMENT_TAGS..., EDITABLE_ELEMENT_TAGS..., FOCUSABLE_ELEMENT_TAGS... ],
436 [ACTION_ELEMENT_PROPERTIES..., EDITABLE_ELEMENT_PROPERTIES..., FOCUSABLE_ELEMENT_PROPERTIES...]
437 # coffeelint: enable=max_line_length
438 )
439
440 # The actual function that will return the desired elements.
441 return (document, { type }) ->
442 return xpathQueryAll(document, xpaths[type])
443
444 xpathHelper = (node, query, resultType) ->
445 document = node.ownerDocument ? node
446 namespaceResolver = (namespace) ->
447 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
448 return document.evaluate(query, node, namespaceResolver, resultType, null)
449
450 xpathQuery = (node, query) ->
451 result = xpathHelper(node, query, XPathResult.FIRST_ORDERED_NODE_TYPE)
452 return result.singleNodeValue
453
454 xpathQueryAll = (node, query) ->
455 result = xpathHelper(node, query, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
456 return (result.snapshotItem(i) for i in [0...result.snapshotLength] by 1)
457
458
459 exports.Bucket = Bucket
460 exports.getEventWindow = getEventWindow
461 exports.getEventRootWindow = getEventRootWindow
462 exports.getEventCurrentTabWindow = getEventCurrentTabWindow
463 exports.getRootWindow = getRootWindow
464 exports.getCurrentTabWindow = getCurrentTabWindow
465
466 exports.blurActiveElement = blurActiveElement
467 exports.isTextInputElement = isTextInputElement
468 exports.isElementEditable = isElementEditable
469 exports.isElementVisible = isElementVisible
470 exports.getSessionStore = getSessionStore
471
472 exports.loadCss = loadCss
473
474 exports.simulateClick = simulateClick
475 exports.isEventSimulated = isEventSimulated
476 exports.simulateWheel = simulateWheel
477 exports.WHEEL_MODE_PIXEL = WHEEL_MODE_PIXEL
478 exports.WHEEL_MODE_LINE = WHEEL_MODE_LINE
479 exports.WHEEL_MODE_PAGE = WHEEL_MODE_PAGE
480 exports.readFromClipboard = readFromClipboard
481 exports.writeToClipboard = writeToClipboard
482 exports.timeIt = timeIt
483
484 exports.getMatchingBlacklistRules = getMatchingBlacklistRules
485 exports.isBlacklisted = isBlacklisted
486 exports.updateBlacklist = updateBlacklist
487 exports.splitListString = splitListString
488 exports.getBestPatternMatch = getBestPatternMatch
489
490 exports.getVersion = getVersion
491 exports.parseHTML = parseHTML
492 exports.createElement = createElement
493 exports.isURL = isURL
494 exports.browserSearchSubmission = browserSearchSubmission
495 exports.getHintChars = getHintChars
496 exports.removeDuplicates = removeDuplicates
497 exports.removeDuplicateCharacters = removeDuplicateCharacters
498 exports.getResourceURI = getResourceURI
499 exports.getMarkableElements = getMarkableElements
500 exports.xpathQuery = xpathQuery
501 exports.xpathQueryAll = xpathQueryAll
502 exports.ADDON_ID = ADDON_ID
Imprint / Impressum