Created
September 7, 2013 04:12
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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