Skip to content

Instantly share code, notes, and snippets.

@fmoo
Created September 7, 2013 04:12
Show Gist options
  • Save fmoo/6472770 to your computer and use it in GitHub Desktop.
Save fmoo/6472770 to your computer and use it in GitHub Desktop.
my rsrc.py "py-haste" clownery. Features include: (1) Static css / js analyzer (syntax parsing provided by pygments) (2) Tracks @requires and @provides dependencies. (3) Makes it easier to implement javelin behaviors. (4) Makes it *super* easy to use react components.
import os
import os.path
import re
from collections import defaultdict
from pygments.lexers.web import CssLexer, JavascriptLexer
from .json import dumps
import random
def readfile(path):
assert os.path.exists(path), path
assert os.path.isfile(path), path
return open(path, mode='r').read()
LEXERS = {
'.css': CssLexer,
'.js': JavascriptLexer,
}
def get_lexer(filename):
filename = filename.lower()
for k in LEXERS:
if filename.endswith(k):
return LEXERS[k]()
return None
def analyze_resources(basedir='.'):
"""Finds all the requires/provides and builds a dependency graph"""
deps = defaultdict(lambda: defaultdict(set))
paths = set()
for dirpath, dirnames, filenames in os.walk(basedir, followlinks=True):
if dirpath.endswith('/.module-cache'):
continue
for filename in filenames:
path = os.path.join(dirpath, filename)
_, _, relpath = path.partition(basedir)
lexer = get_lexer(path)
if lexer is None:
continue
paths.add(relpath)
prior_match = False
enable_react = False
react_name = None
react_state = 0
data = readfile(path)
for type, value in lexer.get_tokens(data):
# Yay, this is an atrocity
if enable_react:
if react_state == 0 and type == ('Keyword', 'Declaration'):
react_state = 1
elif react_state == 1 and type == ('Name', 'Other'):
react_state = 2
react_name = value
elif react_state == 2 and type == ('Operator', ) and value == '=':
react_state = 3
elif react_state == 3 and type == ('Name', 'Other') and value == 'React':
react_state = 4
elif react_state == 4 and type == ('Punctuation', ) and value == '.':
react_state = 5
elif react_state == 5 and type == ('Name', 'Other') and value == 'createClass':
print "Found react-%s in %s" % (react_name, relpath)
deps['provides'][relpath].add('react-' + react_name)
elif type == ('Text', ):
pass
else:
react_state = 0
if type == ('Comment', ) or type == ('Comment', 'Multiline'):
# So is this!
if value.find('@jsx React.DOM') != -1:
print "%s is reactJS" % relpath
enable_react = True
for line in value.split('\n'):
m = re.match('.*?@(\w+)\s+', line)
if m:
category = m.group(1)
if category in ['requires', 'provides']:
startpos = m.end()
values = re.split('\s+', line[startpos:])
for value in values:
deps[category][relpath].add(value)
prior_match = True
elif prior_match:
pre = line[:startpos]
if len(pre) != startpos:
continue
if re.match('^[\s\*]+$', pre):
values = re.split('\s+', line[startpos:])
for value in values:
deps[category][relpath].add(value)
else:
prior_match = False
feature_paths = defaultdict(set)
for relpath, symbols in deps['provides'].iteritems():
for symbol in symbols:
feature_paths[symbol].add(relpath)
return feature_paths, deps['requires'], paths
class ResourceLoader(object):
def __init__(self, feature_paths, path_frequired, paths):
self.feature_paths = feature_paths
self.path_frequired = path_frequired
self.static_paths = paths
self.path_features = defaultdict(list)
for feature, paths in self.feature_paths.iteritems():
for path in paths:
self.path_features[path].append(feature)
def iter_static_deps(self, feature, seen=None):
seen = seen or set() # seen paths
for path in self.feature_paths[feature]:
if path not in seen:
seen.add(path)
for depfeature in self.path_frequired[path]:
for depfeature2, deppath in \
self.iter_static_deps(depfeature, seen):
yield depfeature2, deppath
yield feature, path
def get_static_deps(self, feature):
return list(self.iter_static_deps(feature))
def make_context(self):
return ResourceContext(self)
def require_static(self, name):
if name not in self.static_paths:
raise ValueError("Unknown file: %s" % name)
return name
class ResourceContext(object):
_react = None
def __init__(self, loader):
self.loader = loader
self.css = []
self.js = []
self.features = set()
self.behaviors = defaultdict(list)
def require(self, feature):
print "require", feature
if feature in self.features:
return []
paths = []
for feature, path in self.loader.iter_static_deps(feature):
paths.append(self.require_static(path))
self.features.update(self.loader.path_features[path])
self.features.add(feature)
return paths
def _listRequirePath(self, added, path):
if path not in added:
added.append(self.loader.require_static(path))
def require_static(self, path, type=None):
print "require_static", path, type
if type is None:
base, type = os.path.splitext(path)
if type == '.js':
return self._listRequirePath(self.js, path)
elif type == '.css':
return self._listRequirePath(self.css, path)
else:
raise ValueError("WTF is '%s'?" % type)
return self
def behavior(self, behavior, **config):
self.require('javelin-behavior-' + behavior)
self.behaviors[behavior].append(config)
return self
def _renderIfNotLinked(self, name):
if name in self.js:
return ''
result = '<script type="text/javascript" src="%s"></script>' % name
self.js.append(name)
return result
def render_react(self, component, **config):
# TODO: support depending on unminified?
result = self._renderIfNotLinked(
self.loader.require_static('/react.js'))
for feature, path in self.loader.iter_static_deps('react-' + component):
result += self._renderIfNotLinked(path)
if config is None:
config = {}
id = config.get('id')
if id is None:
id = "%s_%s" % (component, random.randint(0, 2**64))
result += '<div id="%s"></div>' % id
result += '<script>React.renderComponent(%s(%s), ' \
'document.getElementById("%s"));</script>' % \
(component, dumps(config), id)
return result
@property
def react(self):
if self._react is None:
self._react = ReactContext(self)
return self._react
@property
def behaviors_json(self):
return dumps(self.behaviors)
class ReactContext(object):
def __init__(self, resource_context):
self.rsrc = resource_context
self.components = {}
def __getattr__(self, name):
if name not in self.components:
self.components[name] = ReactComponentContext(self.rsrc, name)
return self.components[name]
class ReactComponentContext(object):
def __init__(self, resource_context, name):
self.rsrc = resource_context
self.name = name
def __call__(self, **config):
return self.rsrc.render_react(self.name, **config)
if __name__ == '__main__':
ra = ResourceLoader(*analyze_resources('.'))
print ra.provides.keys()
# Just messing with stuff
print 'aphront-typeahead-control-css', ra.get_static_deps('aphront-typeahead-control-css')
print 'aphront-tokenizer-control-css', ra.get_static_deps('aphront-tokenizer-control-css')
print 'javelin-tokenizer', ra.get_static_deps('javelin-tokenizer')
#pprint.pprint(analyze_resources('.'))
# Do this once per request
rsrc = ra.make_context()
# Do things like this during init to add dependencies to the <head>
rsrc.require('javelin-tokenizer')
# In your template's <head>, you can do something like this for dependencies
# you requre in the backend python logic:
#
# {%- for css in rsrc.css %}
# <link rel="stylesheet" type="text/css" href="{{ css }}" />
# {%- endfor %}
# {% for js in rsrc.js %}
# <script type="text/javascript" src="{{ js }}"></script>
# {%- endfor %}
# Even if you didn't do that, you can just be lazy and do this for react stuff:
# { rsrc.react.CommentBox(url="comments.json" pollInterval=5000) }
print rsrc.react.CommentBox(url="comments.json", pollInterval=5000)
# To use Javelin (from 1 year ago at least, I haven't tested it lately), put this in your body:
# {% if "javelin-stratcom" in rsrc.features %}
# <script type="text/javascript">//<![CDATA[
# {# TODO: Support "sigils" #}
# JX.Stratcom.mergeData(0, []);
# //]]></script>
# {% endif %}
#
# {% if rsrc.behaviors %}
# <script type="text/javascript">//<![CDATA[
# JX.onload(function() {
# JX.initBehaviors({{ rsrc.behaviors_json }})
# });
# //]]></script>
# {% endif %}
# Then just do something like this to set autofocus on the tag with id="username"
rsrc.behavior('phabricator-autofocus', id='username')
# Or for a typeahead, something like this:
rsrc.behavior('aphront-basic-tokenizer',
id="my_tokenizer",
src="/songs/typeahead-all",
placeholder="Type song(s) names")
# But in your html you need the regular, nonintuitive tokenizer tags...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment