2015-02-26 12:07:05 -08:00
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- /
/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
/ * T h i s S o u r c e C o d e F o r m i s s u b j e c t t o t h e t e r m s o f t h e M o z i l l a P u b l i c
* License , v . 2.0 . If a copy of the MPL was not distributed with this
* file , You can obtain one at http : //mozilla.org/MPL/2.0/. */
/ * I m p l e m e n t a t i o n o f a s e r v i c e t h a t c o n v e r t s c e r t a i n v e n d o r - p r e f i x e d C S S
properties to their unprefixed equivalents , for sites on a whitelist . * /
"use strict" ;
const Cc = Components . classes ;
const Ci = Components . interfaces ;
const Cu = Components . utils ;
Cu . import ( "resource://gre/modules/XPCOMUtils.jsm" ) ;
function CSSUnprefixingService ( ) {
}
CSSUnprefixingService . prototype = {
// Boilerplate:
classID : Components . ID ( "{f0729490-e15c-4a2f-a3fb-99e1cc946b42}" ) ,
_xpcom _factory : XPCOMUtils . generateSingletonFactory ( CSSUnprefixingService ) ,
QueryInterface : XPCOMUtils . generateQI ( [ Ci . nsICSSUnprefixingService ] ) ,
// See documentation in nsICSSUnprefixingService.idl
generateUnprefixedDeclaration : function ( aPropName , aRightHalfOfDecl ,
aUnprefixedDecl /*out*/ ) {
// Convert our input strings to lower-case, for easier string-matching.
// (NOTE: If we ever need to add support for unprefixing properties that
// have case-sensitive parts, then we should do these toLowerCase()
// conversions in a more targeted way, to avoid breaking those properties.)
aPropName = aPropName . toLowerCase ( ) ;
aRightHalfOfDecl = aRightHalfOfDecl . toLowerCase ( ) ;
// We have several groups of supported properties:
// FIRST GROUP: Properties that can just be handled as aliases:
// ============================================================
const propertiesThatAreJustAliases = {
"-webkit-background-size" : "background-size" ,
"-webkit-box-flex" : "flex-grow" ,
"-webkit-box-ordinal-group" : "order" ,
"-webkit-box-sizing" : "box-sizing" ,
"-webkit-transform" : "transform" ,
2015-05-08 03:28:00 -07:00
"-webkit-transform-origin" : "transform-origin" ,
2015-02-26 12:07:05 -08:00
} ;
let unprefixedPropName = propertiesThatAreJustAliases [ aPropName ] ;
if ( unprefixedPropName !== undefined ) {
aUnprefixedDecl . value = unprefixedPropName + ":" + aRightHalfOfDecl ;
return true ;
}
// SECOND GROUP: Properties that take a single keyword, where the
// unprefixed version takes a different (but analogous) set of keywords:
// =====================================================================
const propertiesThatNeedKeywordMapping = {
"-webkit-box-align" : {
unprefixedPropName : "align-items" ,
valueMap : {
"start" : "flex-start" ,
"center" : "center" ,
"end" : "flex-end" ,
"baseline" : "baseline" ,
"stretch" : "stretch"
}
} ,
"-webkit-box-orient" : {
unprefixedPropName : "flex-direction" ,
valueMap : {
"horizontal" : "row" ,
"inline-axis" : "row" ,
"vertical" : "column" ,
"block-axis" : "column"
}
} ,
"-webkit-box-pack" : {
unprefixedPropName : "justify-content" ,
valueMap : {
"start" : "flex-start" ,
"center" : "center" ,
"end" : "flex-end" ,
"justify" : "space-between"
}
} ,
} ;
let propInfo = propertiesThatNeedKeywordMapping [ aPropName ] ;
if ( typeof ( propInfo ) != "undefined" ) {
// Regexp for parsing the right half of a declaration, for keyword-valued
// properties. Divides the right half of the declaration into:
// 1) any leading whitespace
// 2) the property value (one or more alphabetical character or hyphen)
// 3) anything after that (e.g. "!important", ";")
// Then we can look up the appropriate unprefixed-property value for the
// value (part 2), and splice that together with the other parts and with
// the unprefixed property-name to make the final declaration.
const keywordValuedPropertyRegexp = /^(\s*)([a-z\-]+)(.*)/ ;
let parts = keywordValuedPropertyRegexp . exec ( aRightHalfOfDecl ) ;
if ( ! parts ) {
// Failed to parse a keyword out of aRightHalfOfDecl. (It probably has
// no alphabetical characters.)
return false ;
}
let mappedKeyword = propInfo . valueMap [ parts [ 2 ] ] ;
if ( mappedKeyword === undefined ) {
// We found a keyword in aRightHalfOfDecl, but we don't have a mapping
// to an equivalent keyword for the unprefixed version of the property.
return false ;
}
aUnprefixedDecl . value = propInfo . unprefixedPropName + ":" +
parts [ 1 ] + // any leading whitespace
mappedKeyword +
parts [ 3 ] ; // any trailing text (e.g. !important, semicolon, etc)
return true ;
}
// THIRD GROUP: Properties that may need arbitrary string-replacement:
// ===================================================================
const propertiesThatNeedStringReplacement = {
// "-webkit-transition" takes a multi-part value. If "-webkit-transform"
// appears as part of that value, replace it w/ "transform".
// And regardless, we unprefix the "-webkit-transition" property-name.
// (We could handle other prefixed properties in addition to 'transform'
// here, but in practice "-webkit-transform" is the main one that's
// likely to be transitioned & that we're concerned about supporting.)
"-webkit-transition" : {
unprefixedPropName : "transition" ,
stringMap : {
"-webkit-transform" : "transform" ,
}
} ,
} ;
propInfo = propertiesThatNeedStringReplacement [ aPropName ] ;
if ( typeof ( propInfo ) != "undefined" ) {
let newRightHalf = aRightHalfOfDecl ;
for ( let strToReplace in propInfo . stringMap ) {
let replacement = propInfo . stringMap [ strToReplace ] ;
2015-03-10 13:47:08 -07:00
newRightHalf = newRightHalf . split ( strToReplace ) . join ( replacement ) ;
2015-02-26 12:07:05 -08:00
}
aUnprefixedDecl . value = propInfo . unprefixedPropName + ":" + newRightHalf ;
return true ;
}
// No known mapping for property aPropName.
return false ;
} ,
2015-05-07 09:04:42 -07:00
// See documentation in nsICSSUnprefixingService.idl
generateUnprefixedGradientValue : function ( aPrefixedFuncName ,
aPrefixedFuncBody ,
aUnprefixedFuncName , /*[out]*/
aUnprefixedFuncBody /*[out]*/ ) {
2015-05-07 09:04:42 -07:00
var unprefixedFuncName , newValue ;
if ( aPrefixedFuncName == "-webkit-gradient" ) {
// Create expression for oldGradientParser:
var parts = this . oldGradientParser ( aPrefixedFuncBody ) ;
var type = parts [ 0 ] . name ;
newValue = this . standardizeOldGradientArgs ( type , parts . slice ( 1 ) ) ;
unprefixedFuncName = type + "-gradient" ;
2015-05-07 09:04:42 -07:00
} else { // we're dealing with more modern syntax - should be somewhat easier, at least for linear gradients.
2015-05-08 09:09:00 -07:00
// Fix three things: remove -webkit-, add 'to ' before reversed top/bottom keywords (linear) or 'at ' before position keywords (radial), recalculate deg-values
2015-05-07 09:04:42 -07:00
// -webkit-linear-gradient( [ [ <angle> | [top | bottom] || [left | right] ],]? <color-stop>[, <color-stop>]+);
2015-05-07 09:04:42 -07:00
if ( aPrefixedFuncName != "-webkit-linear-gradient" &&
aPrefixedFuncName != "-webkit-radial-gradient" ) {
// Unrecognized prefixed gradient type
return false ;
}
unprefixedFuncName = aPrefixedFuncName . replace ( /-webkit-/ , '' ) ;
2015-05-07 09:04:42 -07:00
// Keywords top, bottom, left, right: can be stand-alone or combined pairwise but in any order ('top left' or 'left top')
2015-05-08 09:09:00 -07:00
// These give the starting edge or corner in the -webkit syntax. The standardised equivalent is 'to ' plus opposite values for linear gradients, 'at ' plus same values for radial gradients
if ( unprefixedFuncName . indexOf ( 'linear' ) > - 1 ) {
newValue = aPrefixedFuncBody . replace ( /(top|bottom|left|right)+\s*(top|bottom|left|right)*/ , function ( str ) {
var words = str . split ( /\s+/ ) ;
for ( var i = 0 ; i < words . length ; i ++ ) {
switch ( words [ i ] . toLowerCase ( ) ) {
case 'top' :
words [ i ] = 'bottom' ;
break ;
case 'bottom' :
words [ i ] = 'top' ;
break ;
case 'left' :
words [ i ] = 'right' ;
break ;
case 'right' :
words [ i ] = 'left' ;
}
2015-05-07 09:04:42 -07:00
}
2015-05-08 09:09:00 -07:00
str = words . join ( ' ' ) ;
return ( 'to ' + str ) ;
} ) ;
} else {
newValue = aPrefixedFuncBody . replace ( /(top|bottom|left|right)+\s/ , 'at $1 ' ) ;
}
2015-05-07 09:04:42 -07:00
newValue = newValue . replace ( /\d+deg/ , function ( val ) {
return ( 360 - ( parseInt ( val ) - 90 ) ) + 'deg' ;
} ) ;
}
2015-05-07 09:04:42 -07:00
aUnprefixedFuncName . value = unprefixedFuncName ;
aUnprefixedFuncBody . value = newValue ;
return true ;
} ,
2015-05-07 09:04:42 -07:00
2015-05-07 09:04:42 -07:00
// Helpers for generateUnprefixedGradientValue():
// ----------------------------------------------
oldGradientParser : function ( str ) {
2015-05-07 09:04:42 -07:00
/ * * T h i s m e t h o d t a k e s a l e g a c y - w e b k i t - g r a d i e n t ( ) m e t h o d c a l l a n d p a r s e s i t
to pull out the values , function names and their arguments .
It returns something like [ { name : '-webkit-gradient' , args : [ { name : 'linear' } , { name : 'top left' } ... ] } ]
* /
var objs = [ { } ] , path = [ ] , current , word = '' , separator _chars = [ ',' , '(' , ')' ] ;
current = objs [ 0 ] , path [ 0 ] = objs ;
//str = str.replace(/\s*\(/g, '('); // sorry, ws in front of ( would make parsing a lot harder
for ( var i = 0 ; i < str . length ; i ++ ) {
if ( separator _chars . indexOf ( str [ i ] ) === - 1 ) {
word += str [ i ] ;
} else { // now we have a "separator" - presumably we've also got a "word" or value
current . name = word . trim ( ) ;
//GM_log(word+' '+path.length+' '+str[i])
word = '' ;
if ( str [ i ] === '(' ) { // we assume the 'word' is a function, for example -webkit-gradient() or rgb(), so we create a place to record the arguments
if ( ! ( 'args' in current ) ) {
current . args = [ ] ;
}
current . args . push ( { } ) ;
path . push ( current . args ) ;
current = current . args [ current . args . length - 1 ] ;
path . push ( current ) ;
} else if ( str [ i ] === ')' ) { // function is ended, no more arguments - go back to appending details to the previous branch of the tree
current = path . pop ( ) ; // drop 'current'
current = path . pop ( ) ; // drop 'args' reference
} else {
path . pop ( ) ; // remove 'current' object from path, we have no arguments to add
var current _parent = path [ path . length - 1 ] || objs ; // last object on current path refers to array that contained the previous "current"
current _parent . push ( { } ) ; // we need a new object to hold this "word" or value
current = current _parent [ current _parent . length - 1 ] ; // that object is now the 'current'
path . push ( current ) ;
//GM_log(path.length)
}
}
}
return objs ;
2015-05-07 09:04:42 -07:00
} ,
2015-05-07 09:04:42 -07:00
2015-05-07 09:04:42 -07:00
/ * G i v e n a n a r r a y o f a r g s f o r " - w e b k i t - g r a d i e n t ( . . . ) " r e t u r n e d b y
* oldGradientParser ( ) , this function constructs a string representing the
* equivalent arguments for a standard "linear-gradient(...)" or
* "radial-gradient(...)" expression .
*
* @ param type Either 'linear' or 'radial' .
* @ param args An array of args for a "-webkit-gradient(...)" expression ,
* provided by oldGradientParser ( ) ( not including gradient type ) .
* /
standardizeOldGradientArgs : function ( type , args ) {
2015-05-07 09:04:42 -07:00
var stdArgStr = "" ;
var stops = [ ] ;
if ( /^linear/ . test ( type ) ) {
// linear gradient, args 1 and 2 tend to be start/end keywords
var points = [ ] . concat ( args [ 0 ] . name . split ( /\s+/ ) , args [ 1 ] . name . split ( /\s+/ ) ) ; // example: [left, top, right, top]
// Old webkit syntax "uses a two-point syntax that lets you explicitly state where a linear gradient starts and ends"
// if start/end keywords are percentages, let's massage the values a little more..
var rxPercTest = /\d+\%/ ;
if ( rxPercTest . test ( points [ 0 ] ) || points [ 0 ] == 0 ) {
var startX = parseInt ( points [ 0 ] ) , startY = parseInt ( points [ 1 ] ) , endX = parseInt ( points [ 2 ] ) , endY = parseInt ( points [ 3 ] ) ;
stdArgStr += ( ( Math . atan2 ( endY - startY , endX - startX ) ) * ( 180 / Math . PI ) + 90 ) + 'deg' ;
} else {
if ( points [ 1 ] === points [ 3 ] ) { // both 'top' or 'bottom, this linear gradient goes left-right
stdArgStr += 'to ' + points [ 2 ] ;
} else if ( points [ 0 ] === points [ 2 ] ) { // both 'left' or 'right', this linear gradient goes top-bottom
stdArgStr += 'to ' + points [ 3 ] ;
} else if ( points [ 1 ] === 'top' ) { // diagonal gradient - from top left to opposite corner is 135deg
stdArgStr += '135deg' ;
} else {
stdArgStr += '45deg' ;
}
}
} else if ( /^radial/i . test ( type ) ) { // oooh, radial gradients..
stdArgStr += 'circle ' + args [ 3 ] . name . replace ( /(\d+)$/ , '$1px' ) + ' at ' + args [ 0 ] . name . replace ( /(\d+) / , '$1px ' ) . replace ( /(\d+)$/ , '$1px' ) ;
}
var toColor ;
for ( var j = type === 'linear' ? 2 : 4 ; j < args . length ; j ++ ) {
var position , color , colorIndex ;
if ( args [ j ] . name === 'color-stop' ) {
position = args [ j ] . args [ 0 ] . name ;
colorIndex = 1 ;
} else if ( args [ j ] . name === 'to' ) {
position = '100%' ;
colorIndex = 0 ;
} else if ( args [ j ] . name === 'from' ) {
position = '0%' ;
colorIndex = 0 ;
} ;
if ( position . indexOf ( '%' ) === - 1 ) { // original Safari syntax had 0.5 equivalent to 50%
position = ( parseFloat ( position ) * 100 ) + '%' ;
} ;
color = args [ j ] . args [ colorIndex ] . name ;
if ( args [ j ] . args [ colorIndex ] . args ) { // the color is itself a function call, like rgb()
2015-05-07 09:04:42 -07:00
color += '(' + this . colorValue ( args [ j ] . args [ colorIndex ] . args ) + ')' ;
2015-05-07 09:04:42 -07:00
} ;
if ( args [ j ] . name === 'from' ) {
stops . unshift ( color + ' ' + position ) ;
} else if ( args [ j ] . name === 'to' ) {
toColor = color ;
} else {
stops . push ( color + ' ' + position ) ;
}
}
// translating values to right syntax
for ( var j = 0 ; j < stops . length ; j ++ ) {
stdArgStr += ', ' + stops [ j ] ;
}
if ( toColor ) {
stdArgStr += ', ' + toColor + ' 100%' ;
}
return stdArgStr ;
2015-05-07 09:04:42 -07:00
} ,
2015-05-07 09:04:42 -07:00
2015-05-07 09:04:42 -07:00
colorValue : function ( obj ) {
2015-05-07 09:04:42 -07:00
var ar = [ ] ;
for ( var i = 0 ; i < obj . length ; i ++ ) {
ar . push ( obj [ i ] . name ) ;
} ;
return ar . join ( ', ' ) ;
2015-05-07 09:04:42 -07:00
} ,
} ;
2015-05-07 09:04:42 -07:00
2015-02-26 12:07:05 -08:00
this . NSGetFactory = XPCOMUtils . generateNSGetFactory ( [ CSSUnprefixingService ] ) ;