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