_ = require 'underscore' jQuery = require 'jquery' Specificity = require 'specificity' {$$} = require 'space-pen' fs = require 'fs' EventEmitter = require 'event-emitter' module.exports = class Syntax constructor: -> @grammars = [] @grammarsByFileType = {} @grammarsByScopeName = {} @globalProperties = {} @scopedPropertiesIndex = 0 @scopedProperties = [] addGrammar: (grammar) -> @grammars.push(grammar) for fileType in grammar.fileTypes @grammarsByFileType[fileType] = grammar @grammarsByScopeName[grammar.scopeName] = grammar grammarForFilePath: (filePath, fileContents) -> return @grammarsByFileType["txt"] unless filePath extension = fs.extension(filePath)?[1..] if filePath and extension.length == 0 extension = fs.base(filePath) @grammarByFirstLineRegex(filePath, fileContents) or @grammarsByFileType[extension] or @grammarByFileTypeSuffix(filePath) or @grammarsByFileType["txt"] grammarByFileTypeSuffix: (filePath) -> for fileType, grammar of @grammarsByFileType return grammar if _.endsWith(filePath, fileType) grammarByFirstLineRegex: (filePath, fileContents) -> try fileContents ?= fs.read(filePath) catch e return return unless fileContents lines = fileContents.split('\n') _.find @grammars, (grammar) -> regex = grammar.firstLineRegex return unless regex? escaped = false numberOfNewlinesInRegex = 0 for character in regex.source switch character when '\\' escaped = !escaped when 'n' numberOfNewlinesInRegex++ if escaped escaped = false else escaped = false regex.test(lines[0..numberOfNewlinesInRegex].join('\n')) grammarForScopeName: (scopeName) -> @grammarsByScopeName[scopeName] addProperties: (args...) -> selector = args.shift() if args.length > 1 properties = args.shift() 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, keyPath) value = _.valueForKeyPath(object, keyPath) return value if value? undefined 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]) 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...) element = $$ -> elementsForRemainingScopes = => classString = scope.shift() classes = classString.replace(/^\./, '').replace(/\./g, ' ') if scope.length @div class: classes, elementsForRemainingScopes else @div class: classes elementsForRemainingScopes() deepestChild = element.find(":not(:has(*))") if deepestChild.length deepestChild[0] else element[0] _.extend(Syntax.prototype, EventEmitter)