]> git.gir.st - VimFx.git/blob - extension/packages/utils.coffee
Merge branch 'develop' into better-hintmarkers
[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 return @bucket.set(obj, @newFunc(obj))
31
32 forget: (obj) ->
33 @bucket.delete(obj)
34
35 getEventWindow = (event) ->
36 if event.originalTarget instanceof Window
37 return event.originalTarget
38 else
39 doc = event.originalTarget.ownerDocument or event.originalTarget
40 if doc instanceof HTMLDocument or doc instanceof XULDocument
41 return doc.defaultView
42
43 getEventRootWindow = (event) ->
44 return unless window = getEventWindow(event)
45 return getRootWindow(window)
46
47 getEventCurrentTabWindow = (event) ->
48 return unless rootWindow = getEventRootWindow(event)
49 return getCurrentTabWindow(rootWindow)
50
51 getRootWindow = (window) ->
52 return window
53 .QueryInterface(Ci.nsIInterfaceRequestor)
54 .getInterface(Ci.nsIWebNavigation)
55 .QueryInterface(Ci.nsIDocShellTreeItem)
56 .rootTreeItem
57 .QueryInterface(Ci.nsIInterfaceRequestor)
58 .getInterface(Window)
59
60 getCurrentTabWindow = (window) ->
61 return window.gBrowser.selectedTab.linkedBrowser.contentWindow
62
63 blurActiveElement = (window) ->
64 # Only blur editable elements, in order to interfere with the browser as
65 # little as possible.
66 { activeElement } = window.document
67 if activeElement and isElementEditable(activeElement)
68 activeElement.blur()
69
70 isTextInputElement = (element) ->
71 return element instanceof HTMLInputElement or
72 element instanceof HTMLTextAreaElement
73
74 isElementEditable = (element) ->
75 return element.isContentEditable or
76 element instanceof HTMLInputElement or
77 element instanceof HTMLTextAreaElement or
78 element instanceof HTMLSelectElement or
79 element instanceof XULMenuListElement or
80 element.isContentEditable or
81 isElementGoogleEditable(element)
82
83 isElementGoogleEditable = (element) ->
84 # `g_editable` is a non-standard attribute commonly used by Google.
85 return element.getAttribute?('g_editable') == 'true' or
86 (element instanceof HTMLElement and
87 element.ownerDocument.body?.getAttribute('g_editable') == 'true')
88
89 isElementVisible = (element) ->
90 document = element.ownerDocument
91 window = document.defaultView
92 computedStyle = window.getComputedStyle(element, null)
93 return computedStyle.getPropertyValue('visibility') == 'visible' and
94 computedStyle.getPropertyValue('display') != 'none' and
95 computedStyle.getPropertyValue('opacity') != '0'
96
97 getSessionStore = ->
98 Cc['@mozilla.org/browser/sessionstore;1'].getService(Ci.nsISessionStore)
99
100 loadCss = do ->
101 sss = Cc['@mozilla.org/content/style-sheet-service;1']
102 .getService(Ci.nsIStyleSheetService)
103 return (name) ->
104 uri = getResourceURI("resources/#{ name }.css")
105 # `AGENT_SHEET` is used to override userContent.css and Stylish. Custom
106 # website themes installed by users often make the hint markers unreadable,
107 # for example. Just using `!important` in the CSS is not enough.
108 if !sss.sheetRegistered(uri, sss.AGENT_SHEET)
109 sss.loadAndRegisterSheet(uri, sss.AGENT_SHEET)
110
111 unloader.add(->
112 sss.unregisterSheet(uri, sss.AGENT_SHEET)
113 )
114
115 # Store events that we’ve simulated. A `WeakMap` is used in order not to leak
116 # memory. This approach is better than for example setting `event.simulated =
117 # true`, since that tells the sites that the click was simulated, and allows
118 # sites to spoof it.
119 simulated_events = new WeakMap()
120
121 # Simulate mouse click with a full chain of events. Copied from Vimium
122 # codebase.
123 simulateClick = (element, modifiers = {}) ->
124 document = element.ownerDocument
125 window = document.defaultView
126
127 eventSequence = ['mouseover', 'mousedown', 'mouseup', 'click']
128 for event in eventSequence
129 mouseEvent = document.createEvent('MouseEvents')
130 mouseEvent.initMouseEvent(
131 event, true, true, window, 1, 0, 0, 0, 0,
132 modifiers.ctrlKey, false, false, modifiers.metaKey,
133 0, null
134 )
135 simulated_events.set(mouseEvent, true)
136 # Debugging note: Firefox will not execute the element's default action if
137 # we dispatch this click event, but Webkit will. Dispatching a click on an
138 # input box does not seem to focus it; we do that separately.
139 element.dispatchEvent(mouseEvent)
140
141 isEventSimulated = (event) ->
142 return simulated_events.has(event)
143
144 WHEEL_MODE_PIXEL = Ci.nsIDOMWheelEvent.DOM_DELTA_PIXEL
145 WHEEL_MODE_LINE = Ci.nsIDOMWheelEvent.DOM_DELTA_LINE
146 WHEEL_MODE_PAGE = Ci.nsIDOMWheelEvent.DOM_DELTA_PAGE
147
148 # Simulate mouse scroll event by specific offsets given that mouse cursor is at
149 # specified position.
150 simulateWheel = (window, deltaX, deltaY, mode = WHEEL_MODE_PIXEL) ->
151 windowUtils = window
152 .QueryInterface(Ci.nsIInterfaceRequestor)
153 .getInterface(Ci.nsIDOMWindowUtils)
154
155 [pX, pY] = [window.innerWidth / 2, window.innerHeight / 2]
156 windowUtils.sendWheelEvent(
157 pX, pY, # Window offset (x, y) in pixels.
158 deltaX, deltaY, 0, # Deltas (x, y, z).
159 mode, # Mode (pixel, line, page).
160 0, # Key Modifiers.
161 0, 0, # Line or Page deltas (x, y).
162 0 # Options.
163 )
164
165 # Write a string to the system clipboard.
166 writeToClipboard = (text) ->
167 clipboardHelper = Cc['@mozilla.org/widget/clipboardhelper;1']
168 .getService(Ci.nsIClipboardHelper)
169 clipboardHelper.copyString(text)
170
171 # Read the system clipboard.
172 readFromClipboard = (window) ->
173 trans = Cc['@mozilla.org/widget/transferable;1']
174 .createInstance(Ci.nsITransferable)
175
176 if trans.init
177 privacyContext = window
178 .QueryInterface(Ci.nsIInterfaceRequestor)
179 .getInterface(Ci.nsIWebNavigation)
180 .QueryInterface(Ci.nsILoadContext)
181 trans.init(privacyContext)
182
183 trans.addDataFlavor('text/unicode')
184
185 clip = Cc['@mozilla.org/widget/clipboard;1'].getService(Ci.nsIClipboard)
186 clip.getData(trans, Ci.nsIClipboard.kGlobalClipboard)
187
188 str = {}
189 strLength = {}
190
191 trans.getTransferData('text/unicode', str, strLength)
192
193 if str
194 str = str.value.QueryInterface(Ci.nsISupportsString)
195 return str.data.substring(0, strLength.value / 2)
196
197 return undefined
198
199 # Executes function `func` and mearues how much time it took.
200 timeIt = (func, name) ->
201 console.time(name)
202 result = func()
203 console.timeEnd(name)
204 return result
205
206 isBlacklisted = (str) ->
207 matchingRules = getMatchingBlacklistRules(str)
208 return (matchingRules.length != 0)
209
210 # Returns all rules in the blacklist that match the provided string.
211 getMatchingBlacklistRules = (str) ->
212 return getBlacklist().filter((rule) ->
213 /// ^#{ simpleWildcards(rule) }$ ///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 return `[...new Set(array)]`
352
353 # Why isn’t `a[@href]` used, when `area[@href]` is? Some sites (such as
354 # StackExchange sites) leave out the `href` property and use the anchor as a
355 # JavaScript-powered button (instead of just using the `button` element).
356 ACTION_ELEMENT_TAGS = [
357 "a"
358 "area[@href]"
359 "button"
360 # When viewing an image directly, and it is larger than the viewport,
361 # clicking it toggles zoom.
362 "img[contains(@class, 'decoded') and
363 (contains(@class, 'overflowing') or
364 contains(@class, 'shrinkToFit'))]"
365 ]
366
367 ACTION_ELEMENT_PROPERTIES = [
368 "@onclick"
369 "@onmousedown"
370 "@onmouseup"
371 "@oncommand"
372 "@role='link'"
373 "@role='button'"
374 "contains(@class, 'button')"
375 "contains(@class, 'js-new-tweets-bar')"
376 ]
377
378 EDITABLE_ELEMENT_TAGS = [
379 "textarea"
380 "select"
381 "input[not(@type='hidden' or @disabled)]"
382 ]
383
384 EDITABLE_ELEMENT_PROPERTIES = [
385 "@contenteditable=''"
386 "translate(@contenteditable, 'TRUE', 'true')='true'"
387 ]
388
389 FOCUSABLE_ELEMENT_TAGS = [
390 "frame"
391 "iframe"
392 "embed"
393 "object"
394 ]
395
396 FOCUSABLE_ELEMENT_PROPERTIES = [
397 "@tabindex!=-1"
398 ]
399
400 getMarkableElements = do ->
401 xpathify = (tags, properties)->
402 return tags
403 .concat("*[#{ properties.join(' or ') }]")
404 .map((rule) -> "//#{ rule } | //xhtml:#{ rule }")
405 .join(" | ")
406
407 xpaths =
408 action: xpathify(ACTION_ELEMENT_TAGS, ACTION_ELEMENT_PROPERTIES )
409 editable: xpathify(EDITABLE_ELEMENT_TAGS, EDITABLE_ELEMENT_PROPERTIES )
410 focusable: xpathify(FOCUSABLE_ELEMENT_TAGS, FOCUSABLE_ELEMENT_PROPERTIES)
411 all: xpathify(
412 [ACTION_ELEMENT_TAGS..., EDITABLE_ELEMENT_TAGS..., FOCUSABLE_ELEMENT_TAGS... ],
413 [ACTION_ELEMENT_PROPERTIES..., EDITABLE_ELEMENT_PROPERTIES..., FOCUSABLE_ELEMENT_PROPERTIES...]
414 )
415
416 # The actual function that will return the desired elements.
417 return (document, { type }) ->
418 return xpathQueryAll(document, xpaths[type])
419
420 xpathHelper = (node, query, resultType) ->
421 document = node.ownerDocument ? node
422 namespaceResolver = (namespace) ->
423 if namespace == 'xhtml' then 'http://www.w3.org/1999/xhtml' else null
424 return document.evaluate(query, node, namespaceResolver, resultType, null)
425
426 xpathQuery = (node, query) ->
427 result = xpathHelper(node, query, XPathResult.FIRST_ORDERED_NODE_TYPE)
428 return result.singleNodeValue
429
430 xpathQueryAll = (node, query) ->
431 result = xpathHelper(node, query, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE)
432 return (result.snapshotItem(i) for i in [0...result.snapshotLength] by 1)
433
434
435 exports.Bucket = Bucket
436 exports.getEventWindow = getEventWindow
437 exports.getEventRootWindow = getEventRootWindow
438 exports.getEventCurrentTabWindow = getEventCurrentTabWindow
439 exports.getRootWindow = getRootWindow
440 exports.getCurrentTabWindow = getCurrentTabWindow
441
442 exports.blurActiveElement = blurActiveElement
443 exports.isTextInputElement = isTextInputElement
444 exports.isElementEditable = isElementEditable
445 exports.isElementVisible = isElementVisible
446 exports.getSessionStore = getSessionStore
447
448 exports.loadCss = loadCss
449
450 exports.simulateClick = simulateClick
451 exports.isEventSimulated = isEventSimulated
452 exports.simulateWheel = simulateWheel
453 exports.WHEEL_MODE_PIXEL = WHEEL_MODE_PIXEL
454 exports.WHEEL_MODE_LINE = WHEEL_MODE_LINE
455 exports.WHEEL_MODE_PAGE = WHEEL_MODE_PAGE
456 exports.readFromClipboard = readFromClipboard
457 exports.writeToClipboard = writeToClipboard
458 exports.timeIt = timeIt
459
460 exports.getMatchingBlacklistRules = getMatchingBlacklistRules
461 exports.isBlacklisted = isBlacklisted
462 exports.updateBlacklist = updateBlacklist
463 exports.splitListString = splitListString
464 exports.getBestPatternMatch = getBestPatternMatch
465
466 exports.getVersion = getVersion
467 exports.parseHTML = parseHTML
468 exports.createElement = createElement
469 exports.isURL = isURL
470 exports.browserSearchSubmission = browserSearchSubmission
471 exports.getHintChars = getHintChars
472 exports.removeDuplicates = removeDuplicates
473 exports.removeDuplicateCharacters = removeDuplicateCharacters
474 exports.getResourceURI = getResourceURI
475 exports.getMarkableElements = getMarkableElements
476 exports.xpathQuery = xpathQuery
477 exports.xpathQueryAll = xpathQueryAll
478 exports.ADDON_ID = ADDON_ID
Imprint / Impressum