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