-
Notifications
You must be signed in to change notification settings - Fork 187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow specifying styles as Map
s to guarantee ordering
#200
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
/* @flow */ | ||
import prefixAll from 'inline-style-prefixer/static'; | ||
|
||
import OrderedElements from './ordered-elements'; | ||
import { | ||
objectToPairs, kebabifyStyleName, recursiveMerge, stringifyValue, | ||
importantify, flatten | ||
|
@@ -143,18 +144,21 @@ export const generateCSS = ( | |
stringHandlers /* : StringHandlers */, | ||
useImportant /* : boolean */ | ||
) /* : string */ => { | ||
const merged = styleTypes.reduce(recursiveMerge); | ||
const merged /* : OrderedElements */ = styleTypes.reduce( | ||
recursiveMerge, | ||
new OrderedElements()); | ||
|
||
const plainDeclarations = {}; | ||
const plainDeclarations = new OrderedElements(); | ||
let generatedStyles = ""; | ||
|
||
Object.keys(merged).forEach(key => { | ||
// TODO(emily): benchmark this to see if a plain for loop would be faster. | ||
merged.forEach((key, val) => { | ||
// For each key, see if one of the selector handlers will handle these | ||
// styles. | ||
const foundHandler = selectorHandlers.some(handler => { | ||
const result = handler(key, selector, (newSelector) => { | ||
return generateCSS( | ||
newSelector, [merged[key]], selectorHandlers, | ||
newSelector, [val], selectorHandlers, | ||
stringHandlers, useImportant); | ||
}); | ||
if (result != null) { | ||
|
@@ -167,7 +171,7 @@ export const generateCSS = ( | |
// If none of the handlers handled it, add it to the list of plain | ||
// style declarations. | ||
if (!foundHandler) { | ||
plainDeclarations[key] = merged[key]; | ||
plainDeclarations.set(key, val); | ||
} | ||
}); | ||
|
||
|
@@ -186,31 +190,25 @@ export const generateCSS = ( | |
* See generateCSSRuleset for usage and documentation of paramater types. | ||
*/ | ||
const runStringHandlers = ( | ||
declarations /* : SheetDefinition */, | ||
declarations /* : OrderedElements */, | ||
stringHandlers /* : StringHandlers */, | ||
selectorHandlers /* : SelectorHandler[] */ | ||
) /* */ => { | ||
const result = {}; | ||
|
||
const hasStringHandlers = !!stringHandlers; | ||
const keys = Object.keys(declarations); | ||
for (let i = 0; i < keys.length; i += 1) { | ||
return declarations.map((key, val) => { | ||
// If a handler exists for this particular key, let it interpret | ||
// that value first before continuing | ||
if (hasStringHandlers && stringHandlers.hasOwnProperty(keys[i])) { | ||
if (hasStringHandlers && stringHandlers.hasOwnProperty(key)) { | ||
// TODO(emily): Pass in a callback which generates CSS, similar to | ||
// how our selector handlers work, instead of passing in | ||
// `selectorHandlers` and have them make calls to `generateCSS` | ||
// themselves. Right now, this is impractical because our string | ||
// handlers are very specialized and do complex things. | ||
result[keys[i]] = stringHandlers[keys[i]]( | ||
declarations[keys[i]], selectorHandlers); | ||
return stringHandlers[key](val, selectorHandlers); | ||
} else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: you are returning early on 207, so you can remove the |
||
result[keys[i]] = declarations[keys[i]]; | ||
return val; | ||
} | ||
} | ||
|
||
return result; | ||
}); | ||
}; | ||
|
||
/** | ||
|
@@ -246,44 +244,65 @@ const runStringHandlers = ( | |
*/ | ||
export const generateCSSRuleset = ( | ||
selector /* : string */, | ||
declarations /* : SheetDefinition */, | ||
declarations /* : OrderedElements */, | ||
stringHandlers /* : StringHandlers */, | ||
useImportant /* : boolean */, | ||
selectorHandlers /* : SelectorHandler[] */ | ||
) /* : string */ => { | ||
const handledDeclarations = runStringHandlers( | ||
const handledDeclarations /* : OrderedElements */ = runStringHandlers( | ||
declarations, stringHandlers, selectorHandlers); | ||
|
||
const prefixedDeclarations = prefixAll(handledDeclarations); | ||
// Calculate the order that we want to each element in `prefixedRules` to | ||
// be in, based on its index in the original key ordering. We have to do | ||
// this before the prefixing, because prefixAll mutates | ||
// handledDeclarations.elements. | ||
const sortOrder = {}; | ||
for (let i = 0; i < handledDeclarations.keyOrder.length; i++) { | ||
const key = handledDeclarations.keyOrder[i]; | ||
sortOrder[key] = i; | ||
|
||
// In order to keep most prefixed versions of keys in about the same | ||
// order that the original keys were in but placed before the | ||
// unprefixed version, we generate the prefixed forms of the keys and | ||
// set their order to the same as the original key minus a little bit. | ||
const capitalizedKey = `${key[0].toUpperCase()}${key.slice(1)}`; | ||
const prefixedKeys = [ | ||
`Webkit${capitalizedKey}`, | ||
`Moz${capitalizedKey}`, | ||
`ms${capitalizedKey}`, | ||
]; | ||
for (let j = 0; j < prefixedKeys.length; ++j) { | ||
if (!handledDeclarations.elements.hasOwnProperty(prefixedKeys[j])) { | ||
sortOrder[prefixedKeys[j]] = i - 0.5; | ||
} | ||
} | ||
} | ||
|
||
// NOTE(emily): This mutates handledDeclarations.elements. | ||
const prefixedDeclarations = prefixAll(handledDeclarations.elements); | ||
|
||
const prefixedRules = flatten( | ||
objectToPairs(prefixedDeclarations).map(([key, value]) => { | ||
if (Array.isArray(value)) { | ||
// inline-style-prefix-all returns an array when there should be | ||
// multiple rules, we will flatten to single rules | ||
|
||
const prefixedValues = []; | ||
const unprefixedValues = []; | ||
|
||
value.forEach(v => { | ||
if (v[0] === '-') { | ||
prefixedValues.push(v); | ||
} else { | ||
unprefixedValues.push(v); | ||
} | ||
}); | ||
|
||
prefixedValues.sort(); | ||
unprefixedValues.sort(); | ||
|
||
return prefixedValues | ||
.concat(unprefixedValues) | ||
.map(v => [key, v]); | ||
// inline-style-prefixer returns an array when there should be | ||
// multiple rules for the same key. Here we flatten to multiple | ||
// pairs with the same key. | ||
return value.map(v => [key, v]); | ||
} | ||
return [[key, value]]; | ||
}) | ||
); | ||
|
||
// Sort the prefixed rules according to the order that the keys were | ||
// in originally before we prefixed them. New, prefixed versions | ||
// of the rules aren't in the original list, so we set their order to -1 so | ||
// they sort to the top. | ||
prefixedRules.sort((a, b) => { | ||
const aOrder = sortOrder.hasOwnProperty(a[0]) ? sortOrder[a[0]] : -1; | ||
const bOrder = sortOrder.hasOwnProperty(b[0]) ? sortOrder[b[0]] : -1; | ||
return aOrder - bOrder; | ||
}); | ||
|
||
const transformValue = (useImportant === false) | ||
? stringifyValue | ||
: (key, value) => importantify(stringifyValue(key, value)); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* @flow */ | ||
/* global Map */ | ||
|
||
export default class OrderedElements { | ||
/* :: | ||
elements: {[string]: any}; | ||
keyOrder: string[]; | ||
|
||
static fromObject: ({[string]: any}) => OrderedElements; | ||
static fromMap: (Map<string,any>) => OrderedElements; | ||
static from: (Map<string,any> | {[string]: any} | OrderedElements) => | ||
OrderedElements; | ||
*/ | ||
|
||
constructor( | ||
elements /* : {[string]: any} */ = {}, | ||
keyOrder /* : string[] */ = [] | ||
) { | ||
this.elements = elements; | ||
this.keyOrder = keyOrder; | ||
} | ||
|
||
forEach(callback /* : (string, any) => void */) { | ||
for (let i = 0; i < this.keyOrder.length; i++) { | ||
callback(this.keyOrder[i], this.elements[this.keyOrder[i]]); | ||
} | ||
} | ||
|
||
map(callback /* : (string, any) => any */) /* : OrderedElements */ { | ||
const results = new OrderedElements(); | ||
for (let i = 0; i < this.keyOrder.length; i++) { | ||
results.set( | ||
this.keyOrder[i], | ||
callback(this.keyOrder[i], this.elements[this.keyOrder[i]]) | ||
); | ||
} | ||
return results; | ||
} | ||
|
||
set(key /* : string */, value /* : any */) { | ||
if (!this.elements.hasOwnProperty(key)) { | ||
this.keyOrder.push(key); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the element is already there, should it move it to the end in keyOrder? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think so. Or at least, that would be semantically different than what we've had before. That is, merging
with
gives
now. Moving to the end of the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's right. I'm wondering if the before behavior is unexpected and could cause similar bugs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the current way, it's impossible to make new styles appear after the old styles. Though if we switch it, it'll be impossible to make new styles appear before the old styles. I think either way has problems, but this at least stays consistent with what we have now. |
||
this.elements[key] = value; | ||
} | ||
|
||
get(key /* : string */) /* : any */ { | ||
return this.elements[key]; | ||
} | ||
|
||
has(key /* : string */) /* : boolean */ { | ||
return this.elements.hasOwnProperty(key); | ||
} | ||
} | ||
|
||
OrderedElements.fromObject = (obj) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this should take an optional array argument for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At that point, you'd basically just be calling the constructor, so I'm not sure I see the usefulness of this. Anyway, we're not using that for now; we can add it if it's necessary. |
||
return new OrderedElements(obj, Object.keys(obj)); | ||
}; | ||
|
||
OrderedElements.fromMap = (map) => { | ||
const ret = new OrderedElements(); | ||
map.forEach((val, key) => { | ||
ret.set(key, val); | ||
}); | ||
return ret; | ||
}; | ||
|
||
OrderedElements.from = (obj) => { | ||
if (obj instanceof OrderedElements) { | ||
// NOTE(emily): This makes a shallow copy of the previous elements, so | ||
// if the elements are deeply modified it will affect all copies. | ||
return new OrderedElements({...obj.elements}, obj.keyOrder.slice()); | ||
} else if ( | ||
// For some reason, flow complains about a plain | ||
// `typeof Map !== "undefined"` check. Casting `Map` to `any` solves | ||
// the problem. | ||
typeof /*::(*/ Map /*: any)*/ !== "undefined" && | ||
obj instanceof Map | ||
) { | ||
return OrderedElements.fromMap(obj); | ||
} else { | ||
return OrderedElements.fromObject(obj); | ||
} | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:( I undid some of your for-loop optimization here, but putting it back is lot uglier now with the
OrderedElements
thing. I re-did some of the optimizations inside of theOrderedElements
implementation, though.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries, we can always come back, profile, and optimize the hotspots again. 🚀