]> git.gir.st - VimFx.git/blob - extension/lib/utils.coffee
Fix broken Customize button in options page
[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 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 value = @newFunc(obj)
52 @bucket.set(obj, value)
53 return value
54
55 forget: (obj) ->
56 @bucket.delete(obj)
57
58 getEventWindow = (event) ->
59 if event.originalTarget instanceof Window
60 return event.originalTarget
61 else
62 doc = event.originalTarget.ownerDocument or event.originalTarget
63 if doc instanceof HTMLDocument or doc instanceof XULDocument
64 return doc.defaultView
65
66 getEventRootWindow = (event) ->
67 return unless window = getEventWindow(event)
68 return getRootWindow(window)
69
70 getEventCurrentTabWindow = (event) ->
71 return unless rootWindow = getEventRootWindow(event)
72 return getCurrentTabWindow(rootWindow)
73
74 getRootWindow = (window) ->
75 return window
76 .QueryInterface(Ci.nsIInterfaceRequestor)
77 .getInterface(Ci.nsIWebNavigation)
78 .QueryInterface(Ci.nsIDocShellTreeItem)
79 .rootTreeItem
80 .QueryInterface(Ci.nsIInterfaceRequestor)
81 .getInterface(Window)
82
83 getCurrentTabWindow = (window) ->
84 return window.gBrowser.selectedTab.linkedBrowser.contentWindow
85
86 blurActiveElement = (window) ->
87 # Only blur editable elements, in order to interfere with the browser as
88 # little as possible.
89 { activeElement } = window.document
90 if activeElement and isElementEditable(activeElement)
91 activeElement.blur()
92
93 isTextInputElement = (element) ->
94 return element instanceof HTMLInputElement or
95 element instanceof HTMLTextAreaElement
96
97 isElementEditable = (element) ->
98 return element.isContentEditable or
99 element instanceof HTMLInputElement or
100 element instanceof HTMLTextAreaElement or
101 element instanceof HTMLSelectElement or
102 element instanceof XULMenuListElement or
103 element.isContentEditable or
104 isElementGoogleEditable(element)
105
106 isElementGoogleEditable = (element) ->
107 # `g_editable` is a non-standard attribute commonly used by Google.
108 return element.getAttribute?('g_editable') == 'true' or
109 (element instanceof HTMLElement and
110 element.ownerDocument.body?.getAttribute('g_editable') == 'true')
111
112 isElementVisible = (element) ->
113 document = element.ownerDocument
114 window = document.defaultView
115 computedStyle = window.getComputedStyle(element, null)
116 return computedStyle.getPropertyValue('visibility') == 'visible' and
117 computedStyle.getPropertyValue('display') != 'none' and
118 computedStyle.getPropertyValue('opacity') != '0'
119
120 getSessionStore = ->
121 Cc['@mozilla.org/browser/sessionstore;1'].getService(Ci.nsISessionStore)
122
123 loadCss = do ->
124 sss = Cc['@mozilla.org/content/style-sheet-service;1']
125 .getService(Ci.nsIStyleSheetService)
126 return (name) ->
127 uri = getResourceURI("resources/#{ name }.css")
128 # `AGENT_SHEET` is used to override userContent.css and Stylish. Custom
129 # website themes installed by users often make the hint markers unreadable,
130 # for example. Just using `!important` in the CSS is not enough.
131 unless sss.sheetRegistered(uri, sss.AGENT_SHEET)
132 sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET)
133
134 module.onShutdown(->
135 sss.unregisterSheet(uri, sss.AGENT_SHEET)
136 )
137
138 # Store events that we’ve simulated. A `WeakMap` is used in order not to leak
139 # memory. This approach is better than for example setting `event.simulated =
140 # true`, since that tells the sites that the click was simulated, and allows
141 # sites to spoof it.
142 simulated_events = new WeakMap()
143
144 # Simulate mouse click with a full chain of events. Copied from Vimium
145 # codebase.
146 simulateClick = (element, modifiers = {}) ->
147 document = element.ownerDocument
148 window = document.defaultView
149
150 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click']
151 for event in eventSequence
152 mouseEvent = document.createEvent('MouseEvents')
153 mouseEvent.initMouseEvent(
154 event, true, true, window, 1, 0, 0, 0, 0,
155 modifiers.ctrlKey, false, false, modifiers.metaKey,
156 0, null
157 )
158 simulated_events.set(mouseEvent, true)
159 # Debugging note: Firefox will not execute the element's default action if
160 # we dispatch this click event, but Webkit will. Dispatching a click on an
161 # input box does not seem to focus it; we do that separately.
162 element.dispatchEvent(mouseEvent)
163
164 isEventSimulated = (event) ->
165 return simulated_events.has(event)
166
167 WHEEL_MODE_PIXEL = Ci.nsIDOMWheelEvent.DOM_DELTA_PIXEL
168 WHEEL_MODE_LINE = Ci.nsIDOMWheelEvent.DOM_DELTA_LINE
169 WHEEL_MODE_PAGE = Ci.nsIDOMWheelEvent.DOM_DELTA_PAGE
170
171 # Simulate mouse scroll event by specific offsets given that mouse cursor is at
172 # specified position.
173 simulateWheel = (window, deltaX, deltaY, mode = WHEEL_MODE_PIXEL) ->
174 windowUtils = window
175 .QueryInterface(Ci.nsIInterfaceRequestor)
176 .getInterface(Ci.nsIDOMWindowUtils)
177
178 [pX, pY] = [window.innerWidth / 2, window.innerHeight / 2]
179 windowUtils.sendWheelEvent(
180 pX, pY, # Window offset (x, y) in pixels.
181 deltaX, deltaY, 0, # Deltas (x, y, z).
182 mode, # Mode (pixel, line, page).
183 0, # Key Modifiers.
184 0, 0, # Line or Page deltas (x, y).
185 0 # Options.
186 )
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 mearues 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 normalizedKey = (key) -> key.map(notation.normalize).join('')
341
342 # Get hint characters, convert them to lower case, and filter duplicates.
343 getHintChars = ->
344 hintChars = getPref('hint_chars')
345 # Make sure that hint chars contain at least two characters.
346 if not hintChars or hintChars.length < 2
347 hintChars = 'fj'
348
349 return removeDuplicateCharacters(hintChars)
350
351 # Remove duplicate characters from string (case insensitive).
352 removeDuplicateCharacters = (str) ->
353 return removeDuplicates( str.toLowerCase().split('') ).join('')
354
355 # Return URI to some file in the extension packaged as resource.
356 getResourceURI = do ->
357 baseURI = Services.io.newURI(__SCRIPT_URI_SPEC__, null, null)
358 return (path) -> return Services.io.newURI(path, null, baseURI)
359
360 # Escape a string to render it usable in regular expressions.
361 regexpEscape = (s) -> s and s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
362
363 removeDuplicates = (array) ->
364 # coffeelint: disable=no_backticks
365 return `[...new Set(array)]`
366 # coffeelint: enable=no_backticks
367
368 # Why isn’t `a[@href]` used, when `area[@href]` is? Some sites (such as
369 # StackExchange sites) leave out the `href` property and use the anchor as a
370 # JavaScript-powered button (instead of just using the `button` element).
371 ACTION_ELEMENT_TAGS = [
372 'a'
373 'area[@href]'
374 'button'
375 # When viewing an image directly, and it is larger than the viewport,
376 # clicking it toggles zoom.
377 'img[contains(@class, "decoded") and
378 (contains(@class, "overflowing") or
379 contains(@class, "shrinkToFit"))]'
380 ]
381
382 ACTION_ELEMENT_PROPERTIES = [
383 '@onclick'
384 '@onmousedown'
385 '@onmouseup'
386 '@oncommand'
387 '@role="link"'
388 '@role="button"'
389 'contains(@class, "button")'
390 'contains(@class, "js-new-tweets-bar")'
391 ]
392
393 EDITABLE_ELEMENT_TAGS = [
394 'textarea'
395 'select'
396 'input[not(@type="hidden" or @disabled)]'
397 ]
398
399 EDITABLE_ELEMENT_PROPERTIES = [
400 '@contenteditable=""'
401 'translate(@contenteditable, "TRUE", "true")="true"'
402 ]
403
404 FOCUSABLE_ELEMENT_TAGS = [
405 'frame'
406 'iframe'
407 'embed'
408 'object'
409 ]
410
411 FOCUSABLE_ELEMENT_PROPERTIES = [
412 '@tabindex!=-1'
413 ]
414
415 getMarkableElements = do ->
416 xpathify = (tags, properties) ->
417 return tags
418 .concat("*[#{ properties.join(' or ') }]")
419 .map((rule) -> "//#{ rule } | //xhtml:#{ rule }")
420 .join(' | ')
421
422 xpaths =
423 action: xpathify(ACTION_ELEMENT_TAGS, ACTION_ELEMENT_PROPERTIES )
424 editable: xpathify(EDITABLE_ELEMENT_TAGS, EDITABLE_ELEMENT_PROPERTIES )
425 focusable: xpathify(FOCUSABLE_ELEMENT_TAGS, FOCUSABLE_ELEMENT_PROPERTIES)
426 all: xpathify(
427 # coffeelint: disable=max_line_length
428 [ACTION_ELEMENT_TAGS..., EDITABLE_ELEMENT_TAGS..., FOCUSABLE_ELEMENT_TAGS... ],
429 [ACTION_ELEMENT_PROPERTIES..., EDITABLE_ELEMENT_PROPERTIES..., FOCUSABLE_ELEMENT_PROPERTIES...]
430 # coffeelint: enable=max_line_length
431 )
432
433 # The actual function that will return the desired elements.
434 return (document, { type }) ->
435 return xpathQueryAll(document, xpaths[type])
436
437 xpathHelper = (node, query, resultType) ->
438 document = node.ownerDocument ? node
439 namespaceResolver = (namespace) ->
440 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
441 return document.evaluate(query, node, namespaceResolver, resultType, null)
442
443 xpathQuery = (node, query) ->
444 result = xpathHelper(node, query, XPathResult.FIRST_ORDERED_NODE_TYPE)
445 return result.singleNodeValue
446
447 xpathQueryAll = (node, query) ->
448 result = xpathHelper(node, query, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
449 return (result.snapshotItem(i) for i in [0...result.snapshotLength] by 1)
450
451
452 exports.Bucket = Bucket
453 exports.getEventWindow = getEventWindow
454 exports.getEventRootWindow = getEventRootWindow
455 exports.getEventCurrentTabWindow = getEventCurrentTabWindow
456 exports.getRootWindow = getRootWindow
457 exports.getCurrentTabWindow = getCurrentTabWindow
458
459 exports.blurActiveElement = blurActiveElement
460 exports.isTextInputElement = isTextInputElement
461 exports.isElementEditable = isElementEditable
462 exports.isElementVisible = isElementVisible
463 exports.getSessionStore = getSessionStore
464
465 exports.loadCss = loadCss
466
467 exports.simulateClick = simulateClick
468 exports.isEventSimulated = isEventSimulated
469 exports.simulateWheel = simulateWheel
470 exports.WHEEL_MODE_PIXEL = WHEEL_MODE_PIXEL
471 exports.WHEEL_MODE_LINE = WHEEL_MODE_LINE
472 exports.WHEEL_MODE_PAGE = WHEEL_MODE_PAGE
473 exports.writeToClipboard = writeToClipboard
474 exports.timeIt = timeIt
475
476 exports.getMatchingBlacklistRules = getMatchingBlacklistRules
477 exports.isBlacklisted = isBlacklisted
478 exports.getBlacklistedKeys = getBlacklistedKeys
479 exports.updateBlacklist = updateBlacklist
480 exports.splitListString = splitListString
481 exports.getBestPatternMatch = getBestPatternMatch
482
483 exports.getVersion = getVersion
484 exports.parseHTML = parseHTML
485 exports.escapeHTML = escapeHTML
486 exports.createElement = createElement
487 exports.isURL = isURL
488 exports.browserSearchSubmission = browserSearchSubmission
489 exports.normalizedKey = normalizedKey
490 exports.getHintChars = getHintChars
491 exports.removeDuplicates = removeDuplicates
492 exports.removeDuplicateCharacters = removeDuplicateCharacters
493 exports.getResourceURI = getResourceURI
494 exports.getMarkableElements = getMarkableElements
495 exports.xpathQuery = xpathQuery
496 exports.xpathQueryAll = xpathQueryAll
497 exports.ADDON_ID = ADDON_ID
Imprint / Impressum