Most recently added scoped properties win in case of a specificity tie

This makes the scoped property system mimic the behavior of CSS. When 
there is a tie, the scoped properties loaded later in the cascade win.
I also optimize the scanning of all the properties, checking only those
sets of properties that have a value for the desired key path, to reduce
the need to match a ton of scope selectors.
This commit is contained in:
Nathan Sobo
2012-12-23 13:19:20 -07:00
parent bbd2e384c5
commit f76bab512f
2 changed files with 35 additions and 18 deletions

View File

@@ -10,3 +10,10 @@ describe "the `syntax` global", ->
expect(syntax.getProperty([".source.js", ".string.quoted.double.js"], "foo.bar.baz")).toBe 22
expect(syntax.getProperty([".source.js", ".variable.assignment.js"], "foo.bar.baz")).toBe 11
expect(syntax.getProperty([".text"], "foo.bar.baz")).toBe 1
it "favors the most recently added properties in the event of a specificity tie", ->
syntax.addProperties(".source.coffee .string.quoted.single", foo: bar: baz: 42)
syntax.addProperties(".source.coffee .string.quoted.double", foo: bar: baz: 22)
expect(syntax.getProperty([".source.coffee", ".string.quoted.single"], "foo.bar.baz")).toBe 42
expect(syntax.getProperty([".source.coffee", ".string.quoted.single.double"], "foo.bar.baz")).toBe 22

View File

@@ -7,38 +7,48 @@ module.exports =
class Syntax
constructor: ->
@globalProperties = {}
@scopedPropertiesIndex = 0
@scopedProperties = []
@propertiesBySelector = {}
addProperties: (args...) ->
scopeSelector = args.shift() if args.length > 1
selector = args.shift() if args.length > 1
properties = args.shift()
if scopeSelector
@propertiesBySelector[scopeSelector] ?= {}
_.extend(@propertiesBySelector[scopeSelector], properties)
if selector
@scopedProperties.unshift(
selector: selector,
properties: properties,
specificity: Specificity(selector),
index: @scopedPropertiesIndex++
)
else
_.extend(@globalProperties, properties)
getProperty: (scope, keyPath) ->
for object in @propertiesForScope(scope)
for object in @propertiesForScope(scope, keyPath)
value = _.valueForKeyPath(object, keyPath)
return value if value?
undefined
propertiesForScope: (scope) ->
matchingSelectors = []
element = @buildScopeElement(scope)
while element
matchingSelectors.push(@matchingSelectorsForElement(element)...)
element = element.parentNode
properties = matchingSelectors.map (selector) => @propertiesBySelector[selector]
properties.concat([@globalProperties])
propertiesForScope: (scope, keyPath) ->
matchingProperties = []
candidates = @scopedProperties.filter ({properties}) -> _.valueForKeyPath(properties, keyPath)?
if candidates.length
element = @buildScopeElement(scope)
while element
matchingProperties.push(@matchingPropertiesForElement(element, candidates)...)
element = element.parentNode
matchingProperties.concat([@globalProperties])
matchingSelectorsForElement: (element) ->
matchingSelectors = []
for selector of @propertiesBySelector
matchingSelectors.push(selector) if jQuery.find.matchesSelector(element, selector)
matchingSelectors.sort (a, b) -> Specificity(b) - Specificity(a)
matchingPropertiesForElement: (element, candidates) ->
matchingScopedProperties = candidates.filter ({selector}) -> jQuery.find.matchesSelector(element, selector)
matchingScopedProperties.sort (a, b) ->
if a.specificity == b.specificity
b.index - a.index
else
b.specificity - a.specificity
_.pluck matchingScopedProperties, 'properties'
buildScopeElement: (scope) ->
scope = new Array(scope...)