]> git.gir.st - VimFx.git/blob - extension/packages/utils.coffee
Fix #392: Get vim instance properly onlocationchange
[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 computedStyle = window.getComputedStyle(element, null)
95 return computedStyle.getPropertyValue('visibility') == 'visible' and
96 computedStyle.getPropertyValue('display') != 'none' and
97 computedStyle.getPropertyValue('opacity') != '0'
98
99 getSessionStore = ->
100 Cc['@mozilla.org/browser/sessionstore;1'].getService(Ci.nsISessionStore)
101
102 loadCss = do ->
103 sss = Cc['@mozilla.org/content/style-sheet-service;1']
104 .getService(Ci.nsIStyleSheetService)
105 return (name) ->
106 uri = getResourceURI("resources/#{ name }.css")
107 # `AGENT_SHEET` is used to override userContent.css and Stylish. Custom
108 # website themes installed by users often make the hint markers unreadable,
109 # for example. Just using `!important` in the CSS is not enough.
110 if !sss.sheetRegistered(uri, sss.AGENT_SHEET)
111 sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET)
112
113 unloader.add(->
114 sss.unregisterSheet(uri, sss.AGENT_SHEET)
115 )
116
117 # Store events that we’ve simulated. A `WeakMap` is used in order not to leak
118 # memory. This approach is better than for example setting `event.simulated =
119 # true`, since that tells the sites that the click was simulated, and allows
120 # sites to spoof it.
121 simulated_events = new WeakMap()
122
123 # Simulate mouse click with a full chain of events. Copied from Vimium
124 # codebase.
125 simulateClick = (element, modifiers = {}) ->
126 document = element.ownerDocument
127 window = document.defaultView
128
129 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click']
130 for event in eventSequence
131 mouseEvent = document.createEvent('MouseEvents')
132 mouseEvent.initMouseEvent(
133 event, true, true, window, 1, 0, 0, 0, 0,
134 modifiers.ctrlKey, false, false, modifiers.metaKey,
135 0, null
136 )
137 simulated_events.set(mouseEvent, true)
138 # Debugging note: Firefox will not execute the element's default action if
139 # we dispatch this click event, but Webkit will. Dispatching a click on an
140 # input box does not seem to focus it; we do that separately.
141 element.dispatchEvent(mouseEvent)
142
143 isEventSimulated = (event) ->
144 return simulated_events.has(event)
145
146 WHEEL_MODE_PIXEL = Ci.nsIDOMWheelEvent.DOM_DELTA_PIXEL
147 WHEEL_MODE_LINE = Ci.nsIDOMWheelEvent.DOM_DELTA_LINE
148 WHEEL_MODE_PAGE = Ci.nsIDOMWheelEvent.DOM_DELTA_PAGE
149
150 # Simulate mouse scroll event by specific offsets given that mouse cursor is at
151 # specified position.
152 simulateWheel = (window, deltaX, deltaY, mode = WHEEL_MODE_PIXEL) ->
153 windowUtils = window
154 .QueryInterface(Ci.nsIInterfaceRequestor)
155 .getInterface(Ci.nsIDOMWindowUtils)
156
157 [pX, pY] = [window.innerWidth / 2, window.innerHeight / 2]
158 windowUtils.sendWheelEvent(
159 pX, pY, # Window offset (x, y) in pixels.
160 deltaX, deltaY, 0, # Deltas (x, y, z).
161 mode, # Mode (pixel, line, page).
162 0, # Key Modifiers.
163 0, 0, # Line or Page deltas (x, y).
164 0 # Options.
165 )
166
167 # Write a string to the system clipboard.
168 writeToClipboard = (text) ->
169 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
170 .getService(Ci.nsIClipboardHelper)
171 clipboardHelper.copyString(text)
172
173 # Read the system clipboard.
174 readFromClipboard = (window) ->
175 trans = Cc['@mozilla.org/widget/transferable;1']
176 .createInstance(Ci.nsITransferable)
177
178 if trans.init
179 privacyContext = window
180 .QueryInterface(Ci.nsIInterfaceRequestor)
181 .getInterface(Ci.nsIWebNavigation)
182 .QueryInterface(Ci.nsILoadContext)
183 trans.init(privacyContext)
184
185 trans.addDataFlavor('text/unicode')
186
187 clip = Cc['@mozilla.org/widget/clipboard;1'].getService(Ci.nsIClipboard)
188 clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard)
189
190 str = {}
191 strLength = {}
192
193 trans.getTransferData('text/unicode', str, strLength)
194
195 if str
196 str = str.value.QueryInterface(Ci.nsISupportsString)
197 return str.data.substring(0, strLength.value / 2)
198
199 return undefined
200
201 # Executes function `func` and mearues how much time it took.
202 timeIt = (func, name) ->
203 console.time(name)
204 result = func()
205 console.timeEnd(name)
206 return result
207
208 isBlacklisted = (str) ->
209 matchingRules = getMatchingBlacklistRules(str)
210 return (matchingRules.length != 0)
211
212 # Returns all rules in the blacklist that match the provided string.
213 getMatchingBlacklistRules = (str) ->
214 return getBlacklist().filter((rule) ->
215 /// ^#{ simpleWildcards(rule) }$ ///i.test(str)
216 )
217
218 getBlacklist = ->
219 return splitListString(getPref('black_list'))
220
221 setBlacklist = (blacklist) ->
222 setPref('black_list', blacklist.join(','))
223
224 updateBlacklist = ({ add, remove } = {}) ->
225 blacklist = getBlacklist()
226
227 if add
228 blacklist.push(splitListString(add)...)
229
230 blacklist = blacklist.filter((rule) -> rule != '')
231 blacklist = removeDuplicates(blacklist)
232
233 if remove
234 for rule in splitListString(remove) when rule in blacklist
235 blacklist.splice(blacklist.indexOf(rule), 1)
236
237 setBlacklist(blacklist)
238
239 # Splits a comma/space separated list into an array.
240 splitListString = (str) ->
241 return str.split(/\s*,[\s,]*/)
242
243 # Prepares a string to be used in a regexp, where "*" matches zero or more
244 # characters and "!" matches one character.
245 simpleWildcards = (string) ->
246 return regexpEscape(string).replace(/\\\*/g, '.*').replace(/!/g, '.')
247
248 # Returns the first element that matches a pattern, favoring earlier patterns.
249 # The patterns are case insensitive `simpleWildcards`s and must match either in
250 # the beginning or at the end of a string. Moreover, a pattern does not match
251 # in the middle of words, so "previous" does not match "previously". If that is
252 # desired, a pattern such as "previous*" can be used instead. Note: We cannot
253 # use `\b` word boundaries, because they don’t work well with non-English
254 # characters. Instead we match a space as word boundary. Therefore we normalize
255 # the whitespace and add spaces at the edges of the element text.
256 getBestPatternMatch = (patterns, attrs, elements) ->
257 regexps = []
258 for pattern in patterns
259 wildcarded = simpleWildcards(pattern)
260 regexps.push(/// ^\s(?:#{ wildcarded })\s | \s(?:#{ wildcarded })\s$ ///i)
261
262 # Helper function that matches a string against all the patterns.
263 matches = (text) ->
264 normalizedText = " #{ text } ".replace(/\s+/g, ' ')
265 for re in regexps
266 if re.test(normalizedText)
267 return true
268 return false
269
270 # First search in attributes (favoring earlier attributes) as it's likely
271 # that they are more specific than text contexts.
272 for attr in attrs
273 for element in elements
274 if matches(element.getAttribute(attr))
275 return element
276
277 # Then search in element contents.
278 for element in elements
279 if matches(element.textContent)
280 return element
281
282 return null
283
284 # Get VimFx verion. AddonManager only provides async API to access addon data,
285 # so it's a bit tricky...
286 getVersion = do ->
287 version = null
288
289 scope = {}
290 Cu.import('resource://gre/modules/AddonManager.jsm', scope)
291 scope.AddonManager.getAddonByID(ADDON_ID, (addon) -> version = addon.version)
292
293 return ->
294 return version
295
296 parseHTML = (document, html) ->
297 parser = Cc['@mozilla.org/parserutils;1'].getService(Ci.nsIParserUtils)
298 flags = parser.SanitizerAllowStyle
299 return parser.parseFragment(html, flags, false, null,
300 document.documentElement)
301
302 createElement = (document, type, attributes = {}) ->
303 element = document.createElement(type)
304
305 for attribute, value of attributes
306 element.setAttribute(attribute, value)
307
308 if document instanceof HTMLDocument
309 element.classList.add('VimFxReset')
310
311 return element
312
313 isURL = (str) ->
314 try
315 url = Cc['@mozilla.org/network/io-service;1']
316 .getService(Ci.nsIIOService)
317 .newURI(str, null, null)
318 .QueryInterface(Ci.nsIURL)
319 return true
320 catch err
321 return false
322
323 # Use Firefox services to search for a given string.
324 browserSearchSubmission = (str) ->
325 ss = Cc['@mozilla.org/browser/search-service;1']
326 .getService(Ci.nsIBrowserSearchService)
327
328 engine = ss.currentEngine or ss.defaultEngine
329 return engine.getSubmission(str, null)
330
331 # Get hint characters, convert them to lower case, and filter duplicates.
332 getHintChars = ->
333 hintChars = getPref('hint_chars')
334 # Make sure that hint chars contain at least two characters.
335 if not hintChars or hintChars.length < 2
336 hintChars = 'fj'
337
338 return removeDuplicateCharacters(hintChars)
339
340 # Remove duplicate characters from string (case insensitive).
341 removeDuplicateCharacters = (str) ->
342 return removeDuplicates( str.toLowerCase().split('') ).join('')
343
344 # Return URI to some file in the extension packaged as resource.
345 getResourceURI = do ->
346 baseURI = Services.io.newURI(__SCRIPT_URI_SPEC__, null, null)
347 return (path) -> return Services.io.newURI(path, null, baseURI)
348
349 # Escape a string to render it usable in regular expressions.
350 regexpEscape = (s) -> s and s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
351
352 removeDuplicates = (array) ->
353 return `[...new Set(array)]`
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 "iframe"
393 "embed"
394 "object"
395 ]
396
397 FOCUSABLE_ELEMENT_PROPERTIES = [
398 "@tabindex!=-1"
399 ]
400
401 getMarkableElements = do ->
402 xpathify = (tags, properties)->
403 return tags
404 .concat("*[#{ properties.join(' or ') }]")
405 .map((rule) -> "//#{ rule } | //xhtml:#{ rule }")
406 .join(" | ")
407
408 xpaths =
409 action: xpathify(ACTION_ELEMENT_TAGS, ACTION_ELEMENT_PROPERTIES )
410 editable: xpathify(EDITABLE_ELEMENT_TAGS, EDITABLE_ELEMENT_PROPERTIES )
411 focusable: xpathify(FOCUSABLE_ELEMENT_TAGS, FOCUSABLE_ELEMENT_PROPERTIES)
412 all: xpathify(
413 [ACTION_ELEMENT_TAGS..., EDITABLE_ELEMENT_TAGS..., FOCUSABLE_ELEMENT_TAGS... ],
414 [ACTION_ELEMENT_PROPERTIES..., EDITABLE_ELEMENT_PROPERTIES..., FOCUSABLE_ELEMENT_PROPERTIES...]
415 )
416
417 # The actual function that will return the desired elements.
418 return (document, { type }) ->
419 return xpathQueryAll(document, xpaths[type])
420
421 xpathHelper = (node, query, resultType) ->
422 document = node.ownerDocument ? node
423 namespaceResolver = (namespace) ->
424 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
425 return document.evaluate(query, node, namespaceResolver, resultType, null)
426
427 xpathQuery = (node, query) ->
428 result = xpathHelper(node, query, XPathResult.FIRST_ORDERED_NODE_TYPE)
429 return result.singleNodeValue
430
431 xpathQueryAll = (node, query) ->
432 result = xpathHelper(node, query, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
433 return (result.snapshotItem(i) for i in [0...result.snapshotLength] by 1)
434
435
436 exports.Bucket = Bucket
437 exports.getEventWindow = getEventWindow
438 exports.getEventRootWindow = getEventRootWindow
439 exports.getEventCurrentTabWindow = getEventCurrentTabWindow
440 exports.getRootWindow = getRootWindow
441 exports.getCurrentTabWindow = getCurrentTabWindow
442
443 exports.blurActiveElement = blurActiveElement
444 exports.isTextInputElement = isTextInputElement
445 exports.isElementEditable = isElementEditable
446 exports.isElementVisible = isElementVisible
447 exports.getSessionStore = getSessionStore
448
449 exports.loadCss = loadCss
450
451 exports.simulateClick = simulateClick
452 exports.isEventSimulated = isEventSimulated
453 exports.simulateWheel = simulateWheel
454 exports.WHEEL_MODE_PIXEL = WHEEL_MODE_PIXEL
455 exports.WHEEL_MODE_LINE = WHEEL_MODE_LINE
456 exports.WHEEL_MODE_PAGE = WHEEL_MODE_PAGE
457 exports.readFromClipboard = readFromClipboard
458 exports.writeToClipboard = writeToClipboard
459 exports.timeIt = timeIt
460
461 exports.getMatchingBlacklistRules = getMatchingBlacklistRules
462 exports.isBlacklisted = isBlacklisted
463 exports.updateBlacklist = updateBlacklist
464 exports.splitListString = splitListString
465 exports.getBestPatternMatch = getBestPatternMatch
466
467 exports.getVersion = getVersion
468 exports.parseHTML = parseHTML
469 exports.createElement = createElement
470 exports.isURL = isURL
471 exports.browserSearchSubmission = browserSearchSubmission
472 exports.getHintChars = getHintChars
473 exports.removeDuplicates = removeDuplicates
474 exports.removeDuplicateCharacters = removeDuplicateCharacters
475 exports.getResourceURI = getResourceURI
476 exports.getMarkableElements = getMarkableElements
477 exports.xpathQuery = xpathQuery
478 exports.xpathQueryAll = xpathQueryAll
479 exports.ADDON_ID = ADDON_ID
Imprint / Impressum