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