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